From 6a5c5ab1df88ef08c80d251e48e345f19399eb78 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Mon, 26 Aug 2019 16:38:37 -0700 Subject: [PATCH] chore: Add rustfmt.toml and rustfmt pass --- core/src/avm1.rs | 2460 +++++++++++----------- core/src/backend.rs | 4 +- core/src/backend/audio.rs | 146 +- core/src/backend/audio/decoders.rs | 152 +- core/src/backend/audio/decoders/adpcm.rs | 344 +-- core/src/backend/audio/decoders/mp3.rs | 248 +-- core/src/backend/render.rs | 556 ++--- core/src/bounding_box.rs | 148 +- core/src/button.rs | 602 +++--- core/src/color_transform.rs | 176 +- core/src/events.rs | 58 +- core/src/morph_shape.rs | 822 ++++---- core/src/player.rs | 1088 +++++----- core/src/tag_utils.rs | 104 +- core/src/transform.rs | 5 +- core/tests/integration_tests.rs | 160 +- desktop/src/audio.rs | 312 +-- desktop/src/render.rs | 2318 ++++++++++---------- rustfmt.toml | 1 + web/src/audio.rs | 939 +++++---- web/src/render.rs | 1610 +++++++------- 21 files changed, 6159 insertions(+), 6094 deletions(-) create mode 100644 rustfmt.toml diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 1a68165c7..8c976a5b4 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1,1230 +1,1230 @@ -use crate::prelude::*; -use rand::{rngs::SmallRng, Rng, SeedableRng}; -use std::collections::HashMap; -use std::io::Cursor; -use swf::avm1::read::Reader; - -pub struct ActionContext<'a, 'gc, 'gc_context> { - pub gc_context: gc_arena::MutationContext<'gc, 'gc_context>, - pub global_time: u64, - pub root: DisplayNode<'gc>, - pub start_clip: DisplayNode<'gc>, - pub active_clip: DisplayNode<'gc>, - pub audio: &'a mut dyn crate::backend::audio::AudioBackend, -} - -pub struct Avm1 { - swf_version: u8, - stack: Vec, - rng: SmallRng, - constant_pool: Vec, - locals: HashMap, -} - -type Error = Box; - -impl Avm1 { - pub fn new(swf_version: u8) -> Self { - Self { - swf_version, - stack: vec![], - rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms. - constant_pool: vec![], - locals: HashMap::new(), - } - } - - pub fn do_action(&mut self, context: &mut ActionContext, code: &[u8]) -> Result<(), Error> { - let mut reader = Reader::new(Cursor::new(code), self.swf_version); - - while let Some(action) = reader.read_action()? { - use swf::avm1::types::Action; - let result = match action { - Action::Add => self.action_add(context), - Action::Add2 => self.action_add_2(context), - Action::And => self.action_and(context), - Action::AsciiToChar => self.action_ascii_to_char(context), - Action::BitAnd => self.action_bit_and(context), - Action::BitLShift => self.action_bit_lshift(context), - Action::BitOr => self.action_bit_or(context), - Action::BitRShift => self.action_bit_rshift(context), - Action::BitURShift => self.action_bit_urshift(context), - Action::BitXor => self.action_bit_xor(context), - Action::Call => self.action_call(context), - Action::CallFunction => self.action_call_function(context), - Action::CallMethod => self.action_call_method(context), - Action::CharToAscii => self.action_char_to_ascii(context), - Action::ConstantPool(constant_pool) => { - self.action_constant_pool(context, &constant_pool[..]) - } - Action::Decrement => self.action_decrement(context), - Action::DefineFunction { - name, - params, - actions, - } => self.action_define_function(context, &name, ¶ms[..], &actions[..]), - Action::DefineLocal => self.action_define_local(context), - Action::DefineLocal2 => self.action_define_local_2(context), - Action::Delete => self.action_delete(context), - Action::Delete2 => self.action_delete_2(context), - Action::Divide => self.action_divide(context), - Action::EndDrag => self.action_end_drag(context), - Action::Enumerate => self.action_enumerate(context), - Action::Equals => self.action_equals(context), - Action::Equals2 => self.action_equals_2(context), - Action::GetMember => self.action_get_member(context), - Action::GetProperty => self.action_get_property(context), - Action::GetTime => self.action_get_time(context), - Action::GetVariable => self.action_get_variable(context), - Action::GetUrl { url, target } => self.action_get_url(context, &url, &target), - Action::GetUrl2 { - send_vars_method, - is_target_sprite, - is_load_vars, - } => { - self.action_get_url_2(context, send_vars_method, is_target_sprite, is_load_vars) - } - Action::GotoFrame(frame) => self.action_goto_frame(context, frame), - Action::GotoFrame2 { - set_playing, - scene_offset, - } => self.action_goto_frame_2(context, set_playing, scene_offset), - Action::GotoLabel(label) => self.action_goto_label(context, &label), - Action::If { offset } => self.action_if(context, offset, &mut reader), - Action::Increment => self.action_increment(context), - Action::InitArray => self.action_init_array(context), - Action::InitObject => self.action_init_object(context), - Action::Jump { offset } => self.action_jump(context, offset, &mut reader), - Action::Less => self.action_less(context), - Action::Less2 => self.action_less_2(context), - Action::MBAsciiToChar => self.action_mb_ascii_to_char(context), - Action::MBCharToAscii => self.action_mb_char_to_ascii(context), - Action::MBStringLength => self.action_mb_string_length(context), - Action::MBStringExtract => self.action_mb_string_extract(context), - Action::Modulo => self.action_modulo(context), - Action::Multiply => self.action_multiply(context), - Action::NextFrame => self.action_next_frame(context), - Action::NewMethod => self.action_new_method(context), - Action::NewObject => self.action_new_object(context), - Action::Not => self.action_not(context), - Action::Or => self.action_or(context), - Action::Play => self.play(context), - Action::Pop => self.action_pop(context), - Action::PreviousFrame => self.prev_frame(context), - Action::Push(values) => self.action_push(context, &values[..]), - Action::PushDuplicate => self.action_push_duplicate(context), - Action::RandomNumber => self.action_random_number(context), - Action::RemoveSprite => self.action_remove_sprite(context), - Action::Return => self.action_return(context), - Action::SetMember => self.action_set_member(context), - Action::SetProperty => self.action_set_property(context), - Action::SetTarget(target) => self.action_set_target(context, &target), - Action::SetVariable => self.action_set_variable(context), - Action::StackSwap => self.action_stack_swap(context), - Action::StartDrag => self.action_start_drag(context), - Action::Stop => self.action_stop(context), - Action::StopSounds => self.action_stop_sounds(context), - Action::StoreRegister(register) => self.action_store_register(context, register), - Action::StringAdd => self.action_string_add(context), - Action::StringEquals => self.action_string_equals(context), - Action::StringExtract => self.action_string_extract(context), - Action::StringLength => self.action_string_length(context), - Action::StringLess => self.action_string_less(context), - Action::Subtract => self.action_subtract(context), - Action::TargetPath => self.action_target_path(context), - Action::ToggleQuality => self.toggle_quality(context), - Action::ToInteger => self.action_to_integer(context), - Action::ToNumber => self.action_to_number(context), - Action::ToString => self.action_to_string(context), - Action::Trace => self.action_trace(context), - Action::TypeOf => self.action_type_of(context), - Action::WaitForFrame { - frame, - num_actions_to_skip, - } => self.action_wait_for_frame(context, frame, num_actions_to_skip, &mut reader), - Action::WaitForFrame2 { - num_actions_to_skip, - } => self.action_wait_for_frame_2(context, num_actions_to_skip, &mut reader), - Action::With { .. } => self.action_with(context), - _ => self.unknown_op(context, action), - }; - if let Err(ref e) = result { - log::error!("AVM1 error: {}", e); - return result; - } - } - - Ok(()) - } - - pub fn resolve_slash_path<'gc>( - start: DisplayNode<'gc>, - root: DisplayNode<'gc>, - mut path: &str, - ) -> Option> { - let mut cur_clip = if path.bytes().nth(0).unwrap_or(0) == b'/' { - path = &path[1..]; - root - } else { - start - }; - if !path.is_empty() { - for name in path.split('/') { - let next_clip = if let Some(clip) = cur_clip.read().as_movie_clip() { - if let Some(child) = clip.get_child_by_name(name) { - *child - } else { - return None; - } - } else { - return None; - }; - cur_clip = next_clip; - } - } - Some(cur_clip) - } - - pub fn resolve_slash_path_variable<'gc, 's>( - start: DisplayNode<'gc>, - root: DisplayNode<'gc>, - path: &'s str, - ) -> Option<(DisplayNode<'gc>, &'s str)> { - if !path.is_empty() { - let mut var_iter = path.splitn(2, ':'); - match (var_iter.next(), var_iter.next()) { - (Some(var_name), None) => return Some((start, var_name)), - (Some(path), Some(var_name)) => { - if let Some(node) = Self::resolve_slash_path(start, root, path) { - return Some((node, var_name)); - } - } - _ => (), - } - } - - None - } - - fn push(&mut self, value: impl Into) { - self.stack.push(value.into()); - } - - fn pop(&mut self) -> Result { - self.stack.pop().ok_or_else(|| "Stack underflow".into()) - } - - fn unknown_op( - &mut self, - _context: &mut ActionContext, - action: swf::avm1::types::Action, - ) -> Result<(), Error> { - log::error!("Unknown AVM1 opcode: {:?}", action); - Err("Unknown op".into()) - } - - fn action_add(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?; - let b = self.pop()?; - self.push(Value::Number(b.into_number_v1() + a.into_number_v1())); - Ok(()) - } - - fn action_add_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // ECMA-262 s. 11.6.1 - let a = self.pop()?; - let b = self.pop()?; - // TODO(Herschel): - if let Value::String(a) = a { - let mut s = b.into_string(); - s.push_str(&a); - self.push(Value::String(s)); - } else if let Value::String(mut b) = b { - b.push_str(&a.into_string()); - self.push(Value::String(b)); - } else { - self.push(Value::Number(b.into_number() + a.into_number())); - } - Ok(()) - } - - fn action_and(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 logical and - let a = self.pop()?; - let b = self.pop()?; - let result = b.into_number_v1() != 0.0 && a.into_number_v1() != 0.0; - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - fn action_ascii_to_char(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Results on incorrect operands? - let val = (self.pop()?.as_f64()? as u8) as char; - self.push(Value::String(val.to_string())); - Ok(()) - } - - fn action_char_to_ascii(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Results on incorrect operands? - let s = self.pop()?.into_string(); - let result = s.bytes().nth(0).unwrap_or(0); - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_bit_and(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.as_u32()?; - let b = self.pop()?.as_u32()?; - let result = a & b; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_bit_lshift(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.as_i32()? & 0b11111; // Only 5 bits used for shift count - let b = self.pop()?.as_i32()?; - let result = b << a; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_bit_or(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.as_u32()?; - let b = self.pop()?.as_u32()?; - let result = a | b; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_bit_rshift(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.as_i32()? & 0b11111; // Only 5 bits used for shift count - let b = self.pop()?.as_i32()?; - let result = b >> a; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_bit_urshift(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.as_u32()? & 0b11111; // Only 5 bits used for shift count - let b = self.pop()?.as_u32()?; - let result = b >> a; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_bit_xor(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.as_u32()?; - let b = self.pop()?.as_u32()?; - let result = b ^ a; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_call(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _val = self.pop()?; - // TODO(Herschel) - Err("Unimplemented action: Call".into()) - } - - fn action_call_function(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _fn_name = self.pop()?.as_string()?; - let num_args = self.pop()?.as_i64()?; // TODO(Herschel): max arg count? - for _ in 0..num_args { - self.pop()?; - } - - self.stack.push(Value::Undefined); - // TODO(Herschel) - Err("Unimplemented action: CallFunction".into()) - } - - fn action_call_method(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _method_name = self.pop()?.as_string()?; - let _object = self.pop()?.as_object()?; - let num_args = self.pop()?.as_i64()?; // TODO(Herschel): max arg count? - for _ in 0..num_args { - self.pop()?; - } - - self.stack.push(Value::Undefined); - // TODO(Herschel) - Err("Unimplemented action: CallMethod".into()) - } - - fn action_constant_pool( - &mut self, - _context: &mut ActionContext, - constant_pool: &[String], - ) -> Result<(), Error> { - self.constant_pool = constant_pool.to_vec(); - Ok(()) - } - - fn action_decrement(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.into_number(); - self.push(Value::Number(a - 1.0)); - Ok(()) - } - - fn action_define_function( - &mut self, - _context: &mut ActionContext, - _name: &str, - _params: &[String], - _actions: &[swf::avm1::types::Action], - ) -> Result<(), Error> { - // TODO(Herschel) - Err("Unimplemented action: DefineFunction".into()) - } - - fn action_define_local(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let value = self.pop()?; - let name = self.pop()?; - self.locals.insert(name.as_string()?.clone(), value); - Ok(()) - } - - fn action_define_local_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let name = self.pop()?; - self.locals - .insert(name.as_string()?.clone(), Value::Undefined); - Ok(()) - } - - fn action_delete(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _name = self.pop()?.as_string()?; - let _object = self.pop()?.as_object()?; - Err("Unimplemented action: Delete".into()) - // TODO(Herschel) - } - - fn action_delete_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _name = self.pop()?.as_string()?; - Err("Unimplemented action: Delete2".into()) - // TODO(Herschel) - } - - fn action_divide(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 divide - let a = self.pop()?; - let b = self.pop()?; - - // TODO(Herschel): SWF19: "If A is zero, the result NaN, Infinity, or -Infinity is pushed to the in SWF 5 and later. - // In SWF 4, the result is the string #ERROR#."" - // Seems to be unture for SWF v4, I get 1.#INF. - - self.push(Value::Number(b.into_number_v1() / a.into_number_v1())); - Ok(()) - } - - fn action_end_drag(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel) - log::error!("Unimplemented action: EndDrag"); - Ok(()) - } - - fn action_enumerate(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _name = self.pop()?.as_string()?; - self.push(Value::Null); // Sentinel that indicates end of enumeration - // TODO(Herschel): Push each property name onto the stack - Err("Unimplemented action: Enumerate".into()) - } - - #[allow(clippy::float_cmp)] - fn action_equals(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 equality - let a = self.pop()?; - let b = self.pop()?; - let result = b.into_number_v1() == a.into_number_v1(); - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - #[allow(clippy::float_cmp)] - fn action_equals_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // Version >=5 equality - let a = self.pop()?; - let b = self.pop()?; - let result = match (b, a) { - (Value::Undefined, Value::Undefined) => true, - (Value::Null, Value::Null) => true, - (Value::Null, Value::Undefined) => true, - (Value::Undefined, Value::Null) => true, - (Value::Number(a), Value::Number(b)) => a == b, - (Value::String(a), Value::String(b)) => a == b, - (Value::Object(_a), Value::Object(_b)) => false, // TODO(Herschel) - (Value::String(a), Value::Number(b)) => a.parse().unwrap_or(std::f64::NAN) == b, - (Value::Number(a), Value::String(b)) => a == b.parse().unwrap_or(std::f64::NAN), - _ => false, - }; - self.push(Value::Bool(result)); - Ok(()) - } - - fn action_get_member(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _name = self.pop()?.as_string()?; - let _object = self.pop()?.as_object()?; - // TODO(Herschel) - Err("Unimplemented action: GetMember".into()) - } - - fn action_get_property(&mut self, context: &mut ActionContext) -> Result<(), Error> { - let prop_index = self.pop()?.as_u32()? as usize; - let clip_path = self.pop()?; - let ret = if let Some(clip) = - Avm1::resolve_slash_path(context.active_clip, context.root, clip_path.as_string()?) - { - if let Some(clip) = clip.read().as_movie_clip() { - match prop_index { - 0 => Value::Number(f64::from(clip.x())), - 1 => Value::Number(f64::from(clip.y())), - 2 => Value::Number(f64::from(clip.x_scale())), - 3 => Value::Number(f64::from(clip.y_scale())), - 4 => Value::Number(f64::from(clip.current_frame())), - 5 => Value::Number(f64::from(clip.total_frames())), - 10 => Value::Number(f64::from(clip.rotation())), - 12 => Value::Number(f64::from(clip.frames_loaded())), - _ => { - log::error!("GetProperty: Unimplemented property index {}", prop_index); - Value::Undefined - } - } - } else { - Value::Undefined - } - } else { - Value::Undefined - }; - self.push(ret); - Ok(()) - } - - fn action_get_time(&mut self, context: &mut ActionContext) -> Result<(), Error> { - self.stack.push(Value::Number(context.global_time as f64)); - Ok(()) - } - - fn action_get_variable(&mut self, context: &mut ActionContext) -> Result<(), Error> { - // Flash 4-style variable - let var_path = self.pop()?; - if let Some((node, var_name)) = Self::resolve_slash_path_variable( - context.active_clip, - context.root, - var_path.as_string()?, - ) { - if let Some(clip) = node.read().as_movie_clip() { - self.push(clip.get_variable(var_name)); - } - } else { - self.push(Value::Undefined); - } - Ok(()) - } - - fn action_get_url( - &mut self, - _context: &mut ActionContext, - _url: &str, - _target: &str, - ) -> Result<(), Error> { - // TODO(Herschel): Noop for now. Need a UI/ActionScript/network backend - // to handle network requests appropriately for the platform. - Ok(()) - } - - fn action_get_url_2( - &mut self, - _context: &mut ActionContext, - _method: swf::avm1::types::SendVarsMethod, - _is_target_sprite: bool, - _is_load_vars: bool, - ) -> Result<(), Error> { - // TODO(Herschel): Noop for now. Need a UI/ActionScript/network backend - // to handle network requests appropriately for the platform. - let _url = self.pop()?.into_string(); - - Ok(()) - } - - fn action_goto_frame(&mut self, context: &mut ActionContext, frame: u16) -> Result<(), Error> { - let mut display_object = context.active_clip.write(context.gc_context); - let clip = display_object.as_movie_clip_mut().unwrap(); - clip.goto_frame(frame + 1, true); - Ok(()) - } - - fn action_goto_frame_2( - &mut self, - context: &mut ActionContext, - set_playing: bool, - scene_offset: u16, - ) -> Result<(), Error> { - // Version 4+ gotoAndPlay/gotoAndStop - // Param can either be a frame number or a frame label. - let mut display_object = context.active_clip.write(context.gc_context); - let clip = display_object.as_movie_clip_mut().unwrap(); - match self.pop()? { - Value::Number(frame) => { - clip.goto_frame(scene_offset + (frame as u16) + 1, !set_playing) - } - Value::String(frame_label) => { - if let Some(frame) = clip.frame_label_to_number(&frame_label) { - clip.goto_frame(scene_offset + frame, !set_playing) - } else { - log::warn!( - "ActionGotoFrame2 failed: Movie clip {} does not contain frame label '{}'", - clip.id(), - frame_label - ); - } - } - _ => return Err("Expected frame number or label".into()), - } - Ok(()) - } - - fn action_goto_label(&mut self, context: &mut ActionContext, label: &str) -> Result<(), Error> { - let mut display_object = context.active_clip.write(context.gc_context); - if let Some(clip) = display_object.as_movie_clip_mut() { - if let Some(frame) = clip.frame_label_to_number(label) { - clip.goto_frame(frame, true); - } else { - log::warn!("ActionGoToLabel: Frame label '{}' not found", label); - } - } else { - log::warn!("ActionGoToLabel: Expected movie clip"); - } - Ok(()) - } - - fn action_if( - &mut self, - _context: &mut ActionContext, - jump_offset: i16, - reader: &mut Reader>, - ) -> Result<(), Error> { - let val = self.pop()?; - if val.as_bool() { - use swf::read::SwfRead; - let pos = reader.get_inner().position(); - let new_pos = ((pos as i64) + i64::from(jump_offset)) as u64; - reader.get_inner().set_position(new_pos); - } - Ok(()) - } - - fn action_increment(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?.into_number(); - self.push(Value::Number(a + 1.0)); - Ok(()) - } - - fn action_init_array(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let num_elements = self.pop()?.as_i64()?; - for _ in 0..num_elements { - let _value = self.pop()?; - } - - // TODO(Herschel) - Err("Unimplemented action: InitArray".into()) - } - - fn action_init_object(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let num_props = self.pop()?.as_i64()?; - for _ in 0..num_props { - let _value = self.pop()?; - let _name = self.pop()?; - } - - // TODO(Herschel) - Err("Unimplemented action: InitObject".into()) - } - - fn action_jump( - &mut self, - _context: &mut ActionContext, - jump_offset: i16, - reader: &mut Reader>, - ) -> Result<(), Error> { - // TODO(Herschel): Handle out-of-bounds. - use swf::read::SwfRead; - let pos = reader.get_inner().position(); - let new_pos = ((pos as i64) + i64::from(jump_offset)) as u64; - reader.get_inner().set_position(new_pos); - Ok(()) - } - - fn action_less(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 less than - let a = self.pop()?; - let b = self.pop()?; - let result = b.into_number_v1() < a.into_number_v1(); - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - fn action_less_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // ECMA-262 s. 11.8.5 - let a = self.pop()?; - let b = self.pop()?; - - let result = match (a, b) { - (Value::String(a), Value::String(b)) => b.to_string().bytes().lt(a.to_string().bytes()), - (a, b) => b.into_number() < a.into_number(), - }; - - self.push(Value::Bool(result)); - Ok(()) - } - - fn action_mb_ascii_to_char(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Results on incorrect operands? - use std::convert::TryFrom; - let val = char::try_from(self.pop()?.as_f64()? as u32)?; - self.push(Value::String(val.to_string())); - Ok(()) - } - - fn action_mb_char_to_ascii(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Results on incorrect operands? - let s = self.pop()?.into_string(); - let result = s.chars().nth(0).unwrap_or('\0') as u32; - self.push(Value::Number(result.into())); - Ok(()) - } - - fn action_mb_string_extract(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Result with incorrect operands? - let len = self.pop()?.as_f64()? as usize; - let start = self.pop()?.as_f64()? as usize; - let s = self.pop()?.into_string(); - let result = s[len..len + start].to_string(); // TODO(Herschel): Flash uses UTF-16 internally. - self.push(Value::String(result)); - Ok(()) - } - - fn action_mb_string_length(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Result with non-string operands? - let val = self.pop()?.into_string().len(); - self.push(Value::Number(val as f64)); - Ok(()) - } - - fn action_multiply(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 multiply - let a = self.pop()?; - let b = self.pop()?; - self.push(Value::Number(a.into_number_v1() * b.into_number_v1())); - Ok(()) - } - - fn action_modulo(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO: Wrong operands? - let a = self.pop()?.as_f64()?; - let b = self.pop()?.as_f64()?; - self.push(Value::Number(a % b)); - Ok(()) - } - - fn action_not(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 logical not - let val = self.pop()?; - let result = val.into_number_v1() == 0.0; - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - fn action_next_frame(&mut self, context: &mut ActionContext) -> Result<(), Error> { - let mut display_object = context.active_clip.write(context.gc_context); - let clip = display_object.as_movie_clip_mut().unwrap(); - clip.next_frame(); - Ok(()) - } - - fn action_new_method(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _name = self.pop()?.as_string()?; - let _object = self.pop()?.as_object()?; - let _num_args = self.pop()?.as_i64()?; - self.push(Value::Undefined); - // TODO(Herschel) - Err("Unimplemented action: NewMethod".into()) - } - - fn action_new_object(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _object = self.pop()?.as_string()?; - let num_args = self.pop()?.as_i64()?; - for _ in 0..num_args { - let _arg = self.pop()?; - } - self.push(Value::Undefined); - // TODO(Herschel) - Err("Unimplemented action: NewObject".into()) - } - - fn action_or(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 logical or - let a = self.pop()?; - let b = self.pop()?; - let result = b.into_number_v1() != 0.0 || a.into_number_v1() != 0.0; - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - fn play(&mut self, context: &mut ActionContext) -> Result<(), Error> { - let mut display_object = context.active_clip.write(context.gc_context); - if let Some(clip) = display_object.as_movie_clip_mut() { - clip.play() - } else { - log::warn!("Play failed: Not a MovieClip"); - } - Ok(()) - } - - fn prev_frame(&mut self, context: &mut ActionContext) -> Result<(), Error> { - let mut display_object = context.active_clip.write(context.gc_context); - let clip = display_object.as_movie_clip_mut().unwrap(); - clip.prev_frame(); - Ok(()) - } - - fn action_pop(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - self.pop()?; - Ok(()) - } - - fn action_push( - &mut self, - _context: &mut ActionContext, - values: &[swf::avm1::types::Value], - ) -> Result<(), Error> { - for value in values { - use swf::avm1::types::Value as SwfValue; - let value = match value { - SwfValue::Undefined => Value::Undefined, - SwfValue::Null => Value::Null, - SwfValue::Bool(v) => Value::Bool(*v), - SwfValue::Int(v) => Value::Number(f64::from(*v)), - SwfValue::Float(v) => Value::Number(f64::from(*v)), - SwfValue::Double(v) => Value::Number(*v), - SwfValue::Str(v) => Value::String(v.clone()), - SwfValue::Register(_v) => { - log::error!("Register push unimplemented"); - Value::Undefined - } - SwfValue::ConstantPool(i) => { - if let Some(value) = self.constant_pool.get(*i as usize) { - Value::String(value.clone()) - } else { - log::warn!( - "ActionPush: Constant pool index {} out of range (len = {})", - i, - self.constant_pool.len() - ); - Value::Undefined - } - } - }; - self.push(value); - } - Ok(()) - } - - fn action_push_duplicate(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let val = self.stack.last().ok_or("Stack underflow")?.clone(); - self.push(val); - Ok(()) - } - - fn action_random_number(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let max = self.pop()?.as_f64()? as u32; - let val = self.rng.gen_range(0, max); - self.push(Value::Number(val.into())); - Ok(()) - } - - fn action_remove_sprite(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _target = self.pop()?.into_string(); - // TODO(Herschel) - Err("Unimplemented action: RemoveSprite".into()) - } - - fn action_return(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _result = self.pop()?; - // TODO(Herschel) - Err("Unimplemented action: Return".into()) - } - - fn action_set_member(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _value = self.pop()?; - let _name = self.pop()?; - let _object = self.pop()?; - // TODO(Herschel) - Err("Unimplemented action: SetMember".into()) - } - - fn action_set_property(&mut self, context: &mut ActionContext) -> Result<(), Error> { - let value = self.pop()?.into_number_v1() as f32; - let prop_index = self.pop()?.as_u32()? as usize; - let clip_path = self.pop()?; - let path = clip_path.as_string()?; - if let Some(clip) = Avm1::resolve_slash_path(context.active_clip, context.root, path) { - if let Some(clip) = clip.write(context.gc_context).as_movie_clip_mut() { - match prop_index { - 0 => clip.set_x(value), - 1 => clip.set_y(value), - 2 => clip.set_x_scale(value), - 3 => clip.set_y_scale(value), - 10 => clip.set_rotation(value), - _ => log::error!( - "ActionSetProperty: Unimplemented property index {}", - prop_index - ), - } - } - } else { - log::warn!("ActionSetProperty: Invalid path {}", path); - } - Ok(()) - } - - fn action_set_variable(&mut self, context: &mut ActionContext) -> Result<(), Error> { - // Flash 4-style variable - let value = self.pop()?; - let var_path = self.pop()?; - if let Some((node, var_name)) = Self::resolve_slash_path_variable( - context.active_clip, - context.root, - var_path.as_string()?, - ) { - if let Some(clip) = node.write(context.gc_context).as_movie_clip_mut() { - clip.set_variable(var_name, value); - } - } - Ok(()) - } - - fn action_set_target( - &mut self, - context: &mut ActionContext, - target: &str, - ) -> Result<(), Error> { - if target.is_empty() { - context.active_clip = context.start_clip; - } else if let Some(clip) = - Avm1::resolve_slash_path(context.start_clip, context.root, target) - { - context.active_clip = clip; - } else { - log::warn!("SetTarget failed: {} not found", target); - // TODO: Do we change active_clip to something? Undefined? - } - Ok(()) - } - - fn action_stack_swap(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?; - let b = self.pop()?; - self.push(a); - self.push(b); - Ok(()) - } - - fn action_start_drag(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _target = self.pop()?; - let _lock_center = self.pop()?.as_bool(); - let constrain = self.pop()?.as_bool(); - if constrain { - let _y2 = self.pop()?; - let _x2 = self.pop()?; - let _y1 = self.pop()?; - let _x1 = self.pop()?; - } - log::error!("Unimplemented action: StartDrag"); - Ok(()) - } - - fn action_stop(&mut self, context: &mut ActionContext) -> Result<(), Error> { - let mut display_object = context.active_clip.write(context.gc_context); - if let Some(clip) = display_object.as_movie_clip_mut() { - clip.stop(); - } else { - log::warn!("Stop failed: Not a MovieClip"); - } - Ok(()) - } - - fn action_stop_sounds(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - Err("Unimplemented action: StopSounds".into()) - } - - fn action_store_register( - &mut self, - _context: &mut ActionContext, - _register: u8, - ) -> Result<(), Error> { - // Does NOT pop the value from the stack. - let _val = self.stack.last().ok_or("Stack underflow")?; - Err("Unimplemented action: StoreRegister".into()) - } - - fn action_string_add(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // SWFv4 string concatenation - // TODO(Herschel): Result with non-string operands? - let a = self.pop()?.into_string(); - let mut b = self.pop()?.into_string(); - b.push_str(&a); - self.push(Value::String(b)); - Ok(()) - } - - fn action_string_equals(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 strcmp - let a = self.pop()?; - let b = self.pop()?; - let result = b.into_string() == a.into_string(); - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - fn action_string_extract(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // SWFv4 substring - // TODO(Herschel): Result with incorrect operands? - let len = self.pop()?.as_f64()? as usize; - let start = self.pop()?.as_f64()? as usize; - let s = self.pop()?.into_string(); - // This is specifically a non-UTF8 aware substring. - // SWFv4 only used ANSI strings. - let result = s - .bytes() - .skip(start) - .take(len) - .map(|c| c as char) - .collect::(); - self.push(Value::String(result)); - Ok(()) - } - - fn action_string_length(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 strlen - // Only returns byte length. - // TODO(Herschel): Result with non-string operands? - let val = self.pop()?.into_string().bytes().len() as f64; - self.push(Value::Number(val)); - Ok(()) - } - - fn action_string_less(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // AS1 strcmp - let a = self.pop()?; - let b = self.pop()?; - // This is specifically a non-UTF8 aware comparison. - let result = b.into_string().bytes().lt(a.into_string().bytes()); - self.push(Value::from_bool_v1(result, self.swf_version)); - Ok(()) - } - - fn action_subtract(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let a = self.pop()?; - let b = self.pop()?; - self.push(Value::Number(a.into_number_v1() + b.into_number_v1())); - Ok(()) - } - - fn action_target_path(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel) - let _clip = self.pop()?.as_object()?; - self.push(Value::Undefined); - Err("Unimplemented action: TargetPath".into()) - } - - fn toggle_quality(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - // TODO(Herschel): Noop for now? Could chang anti-aliasing on render backend. - Ok(()) - } - - fn action_to_integer(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let val = self.pop()?; - self.push(Value::Number(val.into_number_v1().trunc())); - Ok(()) - } - - fn action_to_number(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let val = self.pop()?; - self.push(Value::Number(val.into_number())); - Ok(()) - } - - fn action_to_string(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let val = self.pop()?; - self.push(Value::String(val.into_string())); - Ok(()) - } - - fn action_trace(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let val = self.pop()?; - log::info!(target: "avm_trace", "{}", val.into_string()); - Ok(()) - } - - fn action_type_of(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _type_str = match self.pop()? { - Value::Undefined => "undefined", - Value::Null => "null", - Value::Number(_) => "number", - Value::Bool(_) => "boolean", - Value::String(_) => "string", - Value::Object(_) => "object", - }; - // TODO(Herschel): function, movieclip - Ok(()) - } - - fn action_wait_for_frame( - &mut self, - _context: &mut ActionContext, - _frame: u16, - num_actions_to_skip: u8, - reader: &mut Reader>, - ) -> Result<(), Error> { - // TODO(Herschel): Always true for now. - let loaded = true; - if !loaded { - // Note that the offset is given in # of actions, NOT in bytes. - // Read the actions and toss them away. - for _ in 0..num_actions_to_skip { - reader.read_action()?; - } - } - Ok(()) - } - - fn action_wait_for_frame_2( - &mut self, - _context: &mut ActionContext, - num_actions_to_skip: u8, - reader: &mut Reader>, - ) -> Result<(), Error> { - // TODO(Herschel): Always true for now. - let _frame_num = self.pop()?.as_f64()? as u16; - let loaded = true; - if !loaded { - // Note that the offset is given in # of actions, NOT in bytes. - // Read the actions and toss them away. - for _ in 0..num_actions_to_skip { - reader.read_action()?; - } - } - Ok(()) - } - - fn action_with(&mut self, _context: &mut ActionContext) -> Result<(), Error> { - let _object = self.pop()?.as_object()?; - Err("Unimplemented action: With".into()) - } -} - -type ObjectPtr = std::marker::PhantomData<()>; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum Value { - Undefined, - Null, - Bool(bool), - Number(f64), - String(String), - Object(ObjectPtr), -} - -impl Value { - fn into_number_v1(self) -> f64 { - match self { - Value::Bool(true) => 1.0, - Value::Number(v) => v, - Value::String(v) => v.parse().unwrap_or(0.0), - _ => 0.0, - } - } - - fn into_number(self) -> f64 { - // ECMA-262 2nd edtion s. 9.3 ToNumber - use std::f64::NAN; - match self { - Value::Undefined => NAN, - Value::Null => NAN, - Value::Bool(false) => 0.0, - Value::Bool(true) => 1.0, - Value::Number(v) => v, - Value::String(v) => v.parse().unwrap_or(NAN), // TODO(Herschel): Handle Infinity/etc.? - Value::Object(_object) => { - log::error!("Unimplemented: Object ToNumber"); - 0.0 - } - } - } - - fn from_bool_v1(value: bool, swf_version: u8) -> Value { - // SWF version 4 did not have true bools and will push bools as 0 or 1. - // e.g. SWF19 p. 72: - // "If the numbers are equal, true is pushed to the stack for SWF 5 and later. For SWF 4, 1 is pushed to the stack." - if swf_version >= 5 { - Value::Bool(value) - } else { - Value::Number(if value { 1.0 } else { 0.0 }) - } - } - - fn into_string(self) -> String { - match self { - Value::Undefined => "undefined".to_string(), - Value::Null => "null".to_string(), - Value::Bool(v) => v.to_string(), - Value::Number(v) => v.to_string(), // TODO(Herschel): Rounding for int? - Value::String(v) => v, - Value::Object(_) => "[Object object]".to_string(), // TODO(Herschel): - } - } - - fn as_bool(&self) -> bool { - match *self { - Value::Bool(v) => v, - Value::Number(v) => v != 0.0, - // TODO(Herschel): Value::String(v) => ?? - _ => false, - } - } - - fn as_i32(&self) -> Result { - self.as_f64().map(|n| n as i32) - } - - fn as_u32(&self) -> Result { - self.as_f64().map(|n| n as u32) - } - - fn as_i64(&self) -> Result { - self.as_f64().map(|n| n as i64) - } - - fn as_f64(&self) -> Result { - match *self { - Value::Number(v) => Ok(v), - _ => Err(format!("Expected Number, found {:?}", self).into()), - } - } - - fn as_string(&self) -> Result<&String, Error> { - match self { - Value::String(s) => Ok(s), - _ => Err(format!("Expected String, found {:?}", self).into()), - } - } - - fn as_object(&self) -> Result<&ObjectPtr, Error> { - if let Value::Object(object) = self { - Ok(object) - } else { - Err(format!("Expected Object, found {:?}", self).into()) - } - } -} +use crate::prelude::*; +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use std::collections::HashMap; +use std::io::Cursor; +use swf::avm1::read::Reader; + +pub struct ActionContext<'a, 'gc, 'gc_context> { + pub gc_context: gc_arena::MutationContext<'gc, 'gc_context>, + pub global_time: u64, + pub root: DisplayNode<'gc>, + pub start_clip: DisplayNode<'gc>, + pub active_clip: DisplayNode<'gc>, + pub audio: &'a mut dyn crate::backend::audio::AudioBackend, +} + +pub struct Avm1 { + swf_version: u8, + stack: Vec, + rng: SmallRng, + constant_pool: Vec, + locals: HashMap, +} + +type Error = Box; + +impl Avm1 { + pub fn new(swf_version: u8) -> Self { + Self { + swf_version, + stack: vec![], + rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms. + constant_pool: vec![], + locals: HashMap::new(), + } + } + + pub fn do_action(&mut self, context: &mut ActionContext, code: &[u8]) -> Result<(), Error> { + let mut reader = Reader::new(Cursor::new(code), self.swf_version); + + while let Some(action) = reader.read_action()? { + use swf::avm1::types::Action; + let result = match action { + Action::Add => self.action_add(context), + Action::Add2 => self.action_add_2(context), + Action::And => self.action_and(context), + Action::AsciiToChar => self.action_ascii_to_char(context), + Action::BitAnd => self.action_bit_and(context), + Action::BitLShift => self.action_bit_lshift(context), + Action::BitOr => self.action_bit_or(context), + Action::BitRShift => self.action_bit_rshift(context), + Action::BitURShift => self.action_bit_urshift(context), + Action::BitXor => self.action_bit_xor(context), + Action::Call => self.action_call(context), + Action::CallFunction => self.action_call_function(context), + Action::CallMethod => self.action_call_method(context), + Action::CharToAscii => self.action_char_to_ascii(context), + Action::ConstantPool(constant_pool) => { + self.action_constant_pool(context, &constant_pool[..]) + } + Action::Decrement => self.action_decrement(context), + Action::DefineFunction { + name, + params, + actions, + } => self.action_define_function(context, &name, ¶ms[..], &actions[..]), + Action::DefineLocal => self.action_define_local(context), + Action::DefineLocal2 => self.action_define_local_2(context), + Action::Delete => self.action_delete(context), + Action::Delete2 => self.action_delete_2(context), + Action::Divide => self.action_divide(context), + Action::EndDrag => self.action_end_drag(context), + Action::Enumerate => self.action_enumerate(context), + Action::Equals => self.action_equals(context), + Action::Equals2 => self.action_equals_2(context), + Action::GetMember => self.action_get_member(context), + Action::GetProperty => self.action_get_property(context), + Action::GetTime => self.action_get_time(context), + Action::GetVariable => self.action_get_variable(context), + Action::GetUrl { url, target } => self.action_get_url(context, &url, &target), + Action::GetUrl2 { + send_vars_method, + is_target_sprite, + is_load_vars, + } => { + self.action_get_url_2(context, send_vars_method, is_target_sprite, is_load_vars) + } + Action::GotoFrame(frame) => self.action_goto_frame(context, frame), + Action::GotoFrame2 { + set_playing, + scene_offset, + } => self.action_goto_frame_2(context, set_playing, scene_offset), + Action::GotoLabel(label) => self.action_goto_label(context, &label), + Action::If { offset } => self.action_if(context, offset, &mut reader), + Action::Increment => self.action_increment(context), + Action::InitArray => self.action_init_array(context), + Action::InitObject => self.action_init_object(context), + Action::Jump { offset } => self.action_jump(context, offset, &mut reader), + Action::Less => self.action_less(context), + Action::Less2 => self.action_less_2(context), + Action::MBAsciiToChar => self.action_mb_ascii_to_char(context), + Action::MBCharToAscii => self.action_mb_char_to_ascii(context), + Action::MBStringLength => self.action_mb_string_length(context), + Action::MBStringExtract => self.action_mb_string_extract(context), + Action::Modulo => self.action_modulo(context), + Action::Multiply => self.action_multiply(context), + Action::NextFrame => self.action_next_frame(context), + Action::NewMethod => self.action_new_method(context), + Action::NewObject => self.action_new_object(context), + Action::Not => self.action_not(context), + Action::Or => self.action_or(context), + Action::Play => self.play(context), + Action::Pop => self.action_pop(context), + Action::PreviousFrame => self.prev_frame(context), + Action::Push(values) => self.action_push(context, &values[..]), + Action::PushDuplicate => self.action_push_duplicate(context), + Action::RandomNumber => self.action_random_number(context), + Action::RemoveSprite => self.action_remove_sprite(context), + Action::Return => self.action_return(context), + Action::SetMember => self.action_set_member(context), + Action::SetProperty => self.action_set_property(context), + Action::SetTarget(target) => self.action_set_target(context, &target), + Action::SetVariable => self.action_set_variable(context), + Action::StackSwap => self.action_stack_swap(context), + Action::StartDrag => self.action_start_drag(context), + Action::Stop => self.action_stop(context), + Action::StopSounds => self.action_stop_sounds(context), + Action::StoreRegister(register) => self.action_store_register(context, register), + Action::StringAdd => self.action_string_add(context), + Action::StringEquals => self.action_string_equals(context), + Action::StringExtract => self.action_string_extract(context), + Action::StringLength => self.action_string_length(context), + Action::StringLess => self.action_string_less(context), + Action::Subtract => self.action_subtract(context), + Action::TargetPath => self.action_target_path(context), + Action::ToggleQuality => self.toggle_quality(context), + Action::ToInteger => self.action_to_integer(context), + Action::ToNumber => self.action_to_number(context), + Action::ToString => self.action_to_string(context), + Action::Trace => self.action_trace(context), + Action::TypeOf => self.action_type_of(context), + Action::WaitForFrame { + frame, + num_actions_to_skip, + } => self.action_wait_for_frame(context, frame, num_actions_to_skip, &mut reader), + Action::WaitForFrame2 { + num_actions_to_skip, + } => self.action_wait_for_frame_2(context, num_actions_to_skip, &mut reader), + Action::With { .. } => self.action_with(context), + _ => self.unknown_op(context, action), + }; + if let Err(ref e) = result { + log::error!("AVM1 error: {}", e); + return result; + } + } + + Ok(()) + } + + pub fn resolve_slash_path<'gc>( + start: DisplayNode<'gc>, + root: DisplayNode<'gc>, + mut path: &str, + ) -> Option> { + let mut cur_clip = if path.bytes().nth(0).unwrap_or(0) == b'/' { + path = &path[1..]; + root + } else { + start + }; + if !path.is_empty() { + for name in path.split('/') { + let next_clip = if let Some(clip) = cur_clip.read().as_movie_clip() { + if let Some(child) = clip.get_child_by_name(name) { + *child + } else { + return None; + } + } else { + return None; + }; + cur_clip = next_clip; + } + } + Some(cur_clip) + } + + pub fn resolve_slash_path_variable<'gc, 's>( + start: DisplayNode<'gc>, + root: DisplayNode<'gc>, + path: &'s str, + ) -> Option<(DisplayNode<'gc>, &'s str)> { + if !path.is_empty() { + let mut var_iter = path.splitn(2, ':'); + match (var_iter.next(), var_iter.next()) { + (Some(var_name), None) => return Some((start, var_name)), + (Some(path), Some(var_name)) => { + if let Some(node) = Self::resolve_slash_path(start, root, path) { + return Some((node, var_name)); + } + } + _ => (), + } + } + + None + } + + fn push(&mut self, value: impl Into) { + self.stack.push(value.into()); + } + + fn pop(&mut self) -> Result { + self.stack.pop().ok_or_else(|| "Stack underflow".into()) + } + + fn unknown_op( + &mut self, + _context: &mut ActionContext, + action: swf::avm1::types::Action, + ) -> Result<(), Error> { + log::error!("Unknown AVM1 opcode: {:?}", action); + Err("Unknown op".into()) + } + + fn action_add(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?; + let b = self.pop()?; + self.push(Value::Number(b.into_number_v1() + a.into_number_v1())); + Ok(()) + } + + fn action_add_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // ECMA-262 s. 11.6.1 + let a = self.pop()?; + let b = self.pop()?; + // TODO(Herschel): + if let Value::String(a) = a { + let mut s = b.into_string(); + s.push_str(&a); + self.push(Value::String(s)); + } else if let Value::String(mut b) = b { + b.push_str(&a.into_string()); + self.push(Value::String(b)); + } else { + self.push(Value::Number(b.into_number() + a.into_number())); + } + Ok(()) + } + + fn action_and(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 logical and + let a = self.pop()?; + let b = self.pop()?; + let result = b.into_number_v1() != 0.0 && a.into_number_v1() != 0.0; + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + fn action_ascii_to_char(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Results on incorrect operands? + let val = (self.pop()?.as_f64()? as u8) as char; + self.push(Value::String(val.to_string())); + Ok(()) + } + + fn action_char_to_ascii(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Results on incorrect operands? + let s = self.pop()?.into_string(); + let result = s.bytes().nth(0).unwrap_or(0); + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_bit_and(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.as_u32()?; + let b = self.pop()?.as_u32()?; + let result = a & b; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_bit_lshift(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.as_i32()? & 0b11111; // Only 5 bits used for shift count + let b = self.pop()?.as_i32()?; + let result = b << a; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_bit_or(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.as_u32()?; + let b = self.pop()?.as_u32()?; + let result = a | b; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_bit_rshift(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.as_i32()? & 0b11111; // Only 5 bits used for shift count + let b = self.pop()?.as_i32()?; + let result = b >> a; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_bit_urshift(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.as_u32()? & 0b11111; // Only 5 bits used for shift count + let b = self.pop()?.as_u32()?; + let result = b >> a; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_bit_xor(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.as_u32()?; + let b = self.pop()?.as_u32()?; + let result = b ^ a; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_call(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _val = self.pop()?; + // TODO(Herschel) + Err("Unimplemented action: Call".into()) + } + + fn action_call_function(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _fn_name = self.pop()?.as_string()?; + let num_args = self.pop()?.as_i64()?; // TODO(Herschel): max arg count? + for _ in 0..num_args { + self.pop()?; + } + + self.stack.push(Value::Undefined); + // TODO(Herschel) + Err("Unimplemented action: CallFunction".into()) + } + + fn action_call_method(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _method_name = self.pop()?.as_string()?; + let _object = self.pop()?.as_object()?; + let num_args = self.pop()?.as_i64()?; // TODO(Herschel): max arg count? + for _ in 0..num_args { + self.pop()?; + } + + self.stack.push(Value::Undefined); + // TODO(Herschel) + Err("Unimplemented action: CallMethod".into()) + } + + fn action_constant_pool( + &mut self, + _context: &mut ActionContext, + constant_pool: &[String], + ) -> Result<(), Error> { + self.constant_pool = constant_pool.to_vec(); + Ok(()) + } + + fn action_decrement(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.into_number(); + self.push(Value::Number(a - 1.0)); + Ok(()) + } + + fn action_define_function( + &mut self, + _context: &mut ActionContext, + _name: &str, + _params: &[String], + _actions: &[swf::avm1::types::Action], + ) -> Result<(), Error> { + // TODO(Herschel) + Err("Unimplemented action: DefineFunction".into()) + } + + fn action_define_local(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let value = self.pop()?; + let name = self.pop()?; + self.locals.insert(name.as_string()?.clone(), value); + Ok(()) + } + + fn action_define_local_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let name = self.pop()?; + self.locals + .insert(name.as_string()?.clone(), Value::Undefined); + Ok(()) + } + + fn action_delete(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _name = self.pop()?.as_string()?; + let _object = self.pop()?.as_object()?; + Err("Unimplemented action: Delete".into()) + // TODO(Herschel) + } + + fn action_delete_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _name = self.pop()?.as_string()?; + Err("Unimplemented action: Delete2".into()) + // TODO(Herschel) + } + + fn action_divide(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 divide + let a = self.pop()?; + let b = self.pop()?; + + // TODO(Herschel): SWF19: "If A is zero, the result NaN, Infinity, or -Infinity is pushed to the in SWF 5 and later. + // In SWF 4, the result is the string #ERROR#."" + // Seems to be unture for SWF v4, I get 1.#INF. + + self.push(Value::Number(b.into_number_v1() / a.into_number_v1())); + Ok(()) + } + + fn action_end_drag(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel) + log::error!("Unimplemented action: EndDrag"); + Ok(()) + } + + fn action_enumerate(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _name = self.pop()?.as_string()?; + self.push(Value::Null); // Sentinel that indicates end of enumeration + // TODO(Herschel): Push each property name onto the stack + Err("Unimplemented action: Enumerate".into()) + } + + #[allow(clippy::float_cmp)] + fn action_equals(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 equality + let a = self.pop()?; + let b = self.pop()?; + let result = b.into_number_v1() == a.into_number_v1(); + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + #[allow(clippy::float_cmp)] + fn action_equals_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // Version >=5 equality + let a = self.pop()?; + let b = self.pop()?; + let result = match (b, a) { + (Value::Undefined, Value::Undefined) => true, + (Value::Null, Value::Null) => true, + (Value::Null, Value::Undefined) => true, + (Value::Undefined, Value::Null) => true, + (Value::Number(a), Value::Number(b)) => a == b, + (Value::String(a), Value::String(b)) => a == b, + (Value::Object(_a), Value::Object(_b)) => false, // TODO(Herschel) + (Value::String(a), Value::Number(b)) => a.parse().unwrap_or(std::f64::NAN) == b, + (Value::Number(a), Value::String(b)) => a == b.parse().unwrap_or(std::f64::NAN), + _ => false, + }; + self.push(Value::Bool(result)); + Ok(()) + } + + fn action_get_member(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _name = self.pop()?.as_string()?; + let _object = self.pop()?.as_object()?; + // TODO(Herschel) + Err("Unimplemented action: GetMember".into()) + } + + fn action_get_property(&mut self, context: &mut ActionContext) -> Result<(), Error> { + let prop_index = self.pop()?.as_u32()? as usize; + let clip_path = self.pop()?; + let ret = if let Some(clip) = + Avm1::resolve_slash_path(context.active_clip, context.root, clip_path.as_string()?) + { + if let Some(clip) = clip.read().as_movie_clip() { + match prop_index { + 0 => Value::Number(f64::from(clip.x())), + 1 => Value::Number(f64::from(clip.y())), + 2 => Value::Number(f64::from(clip.x_scale())), + 3 => Value::Number(f64::from(clip.y_scale())), + 4 => Value::Number(f64::from(clip.current_frame())), + 5 => Value::Number(f64::from(clip.total_frames())), + 10 => Value::Number(f64::from(clip.rotation())), + 12 => Value::Number(f64::from(clip.frames_loaded())), + _ => { + log::error!("GetProperty: Unimplemented property index {}", prop_index); + Value::Undefined + } + } + } else { + Value::Undefined + } + } else { + Value::Undefined + }; + self.push(ret); + Ok(()) + } + + fn action_get_time(&mut self, context: &mut ActionContext) -> Result<(), Error> { + self.stack.push(Value::Number(context.global_time as f64)); + Ok(()) + } + + fn action_get_variable(&mut self, context: &mut ActionContext) -> Result<(), Error> { + // Flash 4-style variable + let var_path = self.pop()?; + if let Some((node, var_name)) = Self::resolve_slash_path_variable( + context.active_clip, + context.root, + var_path.as_string()?, + ) { + if let Some(clip) = node.read().as_movie_clip() { + self.push(clip.get_variable(var_name)); + } + } else { + self.push(Value::Undefined); + } + Ok(()) + } + + fn action_get_url( + &mut self, + _context: &mut ActionContext, + _url: &str, + _target: &str, + ) -> Result<(), Error> { + // TODO(Herschel): Noop for now. Need a UI/ActionScript/network backend + // to handle network requests appropriately for the platform. + Ok(()) + } + + fn action_get_url_2( + &mut self, + _context: &mut ActionContext, + _method: swf::avm1::types::SendVarsMethod, + _is_target_sprite: bool, + _is_load_vars: bool, + ) -> Result<(), Error> { + // TODO(Herschel): Noop for now. Need a UI/ActionScript/network backend + // to handle network requests appropriately for the platform. + let _url = self.pop()?.into_string(); + + Ok(()) + } + + fn action_goto_frame(&mut self, context: &mut ActionContext, frame: u16) -> Result<(), Error> { + let mut display_object = context.active_clip.write(context.gc_context); + let clip = display_object.as_movie_clip_mut().unwrap(); + clip.goto_frame(frame + 1, true); + Ok(()) + } + + fn action_goto_frame_2( + &mut self, + context: &mut ActionContext, + set_playing: bool, + scene_offset: u16, + ) -> Result<(), Error> { + // Version 4+ gotoAndPlay/gotoAndStop + // Param can either be a frame number or a frame label. + let mut display_object = context.active_clip.write(context.gc_context); + let clip = display_object.as_movie_clip_mut().unwrap(); + match self.pop()? { + Value::Number(frame) => { + clip.goto_frame(scene_offset + (frame as u16) + 1, !set_playing) + } + Value::String(frame_label) => { + if let Some(frame) = clip.frame_label_to_number(&frame_label) { + clip.goto_frame(scene_offset + frame, !set_playing) + } else { + log::warn!( + "ActionGotoFrame2 failed: Movie clip {} does not contain frame label '{}'", + clip.id(), + frame_label + ); + } + } + _ => return Err("Expected frame number or label".into()), + } + Ok(()) + } + + fn action_goto_label(&mut self, context: &mut ActionContext, label: &str) -> Result<(), Error> { + let mut display_object = context.active_clip.write(context.gc_context); + if let Some(clip) = display_object.as_movie_clip_mut() { + if let Some(frame) = clip.frame_label_to_number(label) { + clip.goto_frame(frame, true); + } else { + log::warn!("ActionGoToLabel: Frame label '{}' not found", label); + } + } else { + log::warn!("ActionGoToLabel: Expected movie clip"); + } + Ok(()) + } + + fn action_if( + &mut self, + _context: &mut ActionContext, + jump_offset: i16, + reader: &mut Reader>, + ) -> Result<(), Error> { + let val = self.pop()?; + if val.as_bool() { + use swf::read::SwfRead; + let pos = reader.get_inner().position(); + let new_pos = ((pos as i64) + i64::from(jump_offset)) as u64; + reader.get_inner().set_position(new_pos); + } + Ok(()) + } + + fn action_increment(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?.into_number(); + self.push(Value::Number(a + 1.0)); + Ok(()) + } + + fn action_init_array(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let num_elements = self.pop()?.as_i64()?; + for _ in 0..num_elements { + let _value = self.pop()?; + } + + // TODO(Herschel) + Err("Unimplemented action: InitArray".into()) + } + + fn action_init_object(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let num_props = self.pop()?.as_i64()?; + for _ in 0..num_props { + let _value = self.pop()?; + let _name = self.pop()?; + } + + // TODO(Herschel) + Err("Unimplemented action: InitObject".into()) + } + + fn action_jump( + &mut self, + _context: &mut ActionContext, + jump_offset: i16, + reader: &mut Reader>, + ) -> Result<(), Error> { + // TODO(Herschel): Handle out-of-bounds. + use swf::read::SwfRead; + let pos = reader.get_inner().position(); + let new_pos = ((pos as i64) + i64::from(jump_offset)) as u64; + reader.get_inner().set_position(new_pos); + Ok(()) + } + + fn action_less(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 less than + let a = self.pop()?; + let b = self.pop()?; + let result = b.into_number_v1() < a.into_number_v1(); + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + fn action_less_2(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // ECMA-262 s. 11.8.5 + let a = self.pop()?; + let b = self.pop()?; + + let result = match (a, b) { + (Value::String(a), Value::String(b)) => b.to_string().bytes().lt(a.to_string().bytes()), + (a, b) => b.into_number() < a.into_number(), + }; + + self.push(Value::Bool(result)); + Ok(()) + } + + fn action_mb_ascii_to_char(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Results on incorrect operands? + use std::convert::TryFrom; + let val = char::try_from(self.pop()?.as_f64()? as u32)?; + self.push(Value::String(val.to_string())); + Ok(()) + } + + fn action_mb_char_to_ascii(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Results on incorrect operands? + let s = self.pop()?.into_string(); + let result = s.chars().nth(0).unwrap_or('\0') as u32; + self.push(Value::Number(result.into())); + Ok(()) + } + + fn action_mb_string_extract(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Result with incorrect operands? + let len = self.pop()?.as_f64()? as usize; + let start = self.pop()?.as_f64()? as usize; + let s = self.pop()?.into_string(); + let result = s[len..len + start].to_string(); // TODO(Herschel): Flash uses UTF-16 internally. + self.push(Value::String(result)); + Ok(()) + } + + fn action_mb_string_length(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Result with non-string operands? + let val = self.pop()?.into_string().len(); + self.push(Value::Number(val as f64)); + Ok(()) + } + + fn action_multiply(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 multiply + let a = self.pop()?; + let b = self.pop()?; + self.push(Value::Number(a.into_number_v1() * b.into_number_v1())); + Ok(()) + } + + fn action_modulo(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO: Wrong operands? + let a = self.pop()?.as_f64()?; + let b = self.pop()?.as_f64()?; + self.push(Value::Number(a % b)); + Ok(()) + } + + fn action_not(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 logical not + let val = self.pop()?; + let result = val.into_number_v1() == 0.0; + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + fn action_next_frame(&mut self, context: &mut ActionContext) -> Result<(), Error> { + let mut display_object = context.active_clip.write(context.gc_context); + let clip = display_object.as_movie_clip_mut().unwrap(); + clip.next_frame(); + Ok(()) + } + + fn action_new_method(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _name = self.pop()?.as_string()?; + let _object = self.pop()?.as_object()?; + let _num_args = self.pop()?.as_i64()?; + self.push(Value::Undefined); + // TODO(Herschel) + Err("Unimplemented action: NewMethod".into()) + } + + fn action_new_object(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _object = self.pop()?.as_string()?; + let num_args = self.pop()?.as_i64()?; + for _ in 0..num_args { + let _arg = self.pop()?; + } + self.push(Value::Undefined); + // TODO(Herschel) + Err("Unimplemented action: NewObject".into()) + } + + fn action_or(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 logical or + let a = self.pop()?; + let b = self.pop()?; + let result = b.into_number_v1() != 0.0 || a.into_number_v1() != 0.0; + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + fn play(&mut self, context: &mut ActionContext) -> Result<(), Error> { + let mut display_object = context.active_clip.write(context.gc_context); + if let Some(clip) = display_object.as_movie_clip_mut() { + clip.play() + } else { + log::warn!("Play failed: Not a MovieClip"); + } + Ok(()) + } + + fn prev_frame(&mut self, context: &mut ActionContext) -> Result<(), Error> { + let mut display_object = context.active_clip.write(context.gc_context); + let clip = display_object.as_movie_clip_mut().unwrap(); + clip.prev_frame(); + Ok(()) + } + + fn action_pop(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + self.pop()?; + Ok(()) + } + + fn action_push( + &mut self, + _context: &mut ActionContext, + values: &[swf::avm1::types::Value], + ) -> Result<(), Error> { + for value in values { + use swf::avm1::types::Value as SwfValue; + let value = match value { + SwfValue::Undefined => Value::Undefined, + SwfValue::Null => Value::Null, + SwfValue::Bool(v) => Value::Bool(*v), + SwfValue::Int(v) => Value::Number(f64::from(*v)), + SwfValue::Float(v) => Value::Number(f64::from(*v)), + SwfValue::Double(v) => Value::Number(*v), + SwfValue::Str(v) => Value::String(v.clone()), + SwfValue::Register(_v) => { + log::error!("Register push unimplemented"); + Value::Undefined + } + SwfValue::ConstantPool(i) => { + if let Some(value) = self.constant_pool.get(*i as usize) { + Value::String(value.clone()) + } else { + log::warn!( + "ActionPush: Constant pool index {} out of range (len = {})", + i, + self.constant_pool.len() + ); + Value::Undefined + } + } + }; + self.push(value); + } + Ok(()) + } + + fn action_push_duplicate(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let val = self.stack.last().ok_or("Stack underflow")?.clone(); + self.push(val); + Ok(()) + } + + fn action_random_number(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let max = self.pop()?.as_f64()? as u32; + let val = self.rng.gen_range(0, max); + self.push(Value::Number(val.into())); + Ok(()) + } + + fn action_remove_sprite(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _target = self.pop()?.into_string(); + // TODO(Herschel) + Err("Unimplemented action: RemoveSprite".into()) + } + + fn action_return(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _result = self.pop()?; + // TODO(Herschel) + Err("Unimplemented action: Return".into()) + } + + fn action_set_member(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _value = self.pop()?; + let _name = self.pop()?; + let _object = self.pop()?; + // TODO(Herschel) + Err("Unimplemented action: SetMember".into()) + } + + fn action_set_property(&mut self, context: &mut ActionContext) -> Result<(), Error> { + let value = self.pop()?.into_number_v1() as f32; + let prop_index = self.pop()?.as_u32()? as usize; + let clip_path = self.pop()?; + let path = clip_path.as_string()?; + if let Some(clip) = Avm1::resolve_slash_path(context.active_clip, context.root, path) { + if let Some(clip) = clip.write(context.gc_context).as_movie_clip_mut() { + match prop_index { + 0 => clip.set_x(value), + 1 => clip.set_y(value), + 2 => clip.set_x_scale(value), + 3 => clip.set_y_scale(value), + 10 => clip.set_rotation(value), + _ => log::error!( + "ActionSetProperty: Unimplemented property index {}", + prop_index + ), + } + } + } else { + log::warn!("ActionSetProperty: Invalid path {}", path); + } + Ok(()) + } + + fn action_set_variable(&mut self, context: &mut ActionContext) -> Result<(), Error> { + // Flash 4-style variable + let value = self.pop()?; + let var_path = self.pop()?; + if let Some((node, var_name)) = Self::resolve_slash_path_variable( + context.active_clip, + context.root, + var_path.as_string()?, + ) { + if let Some(clip) = node.write(context.gc_context).as_movie_clip_mut() { + clip.set_variable(var_name, value); + } + } + Ok(()) + } + + fn action_set_target( + &mut self, + context: &mut ActionContext, + target: &str, + ) -> Result<(), Error> { + if target.is_empty() { + context.active_clip = context.start_clip; + } else if let Some(clip) = + Avm1::resolve_slash_path(context.start_clip, context.root, target) + { + context.active_clip = clip; + } else { + log::warn!("SetTarget failed: {} not found", target); + // TODO: Do we change active_clip to something? Undefined? + } + Ok(()) + } + + fn action_stack_swap(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?; + let b = self.pop()?; + self.push(a); + self.push(b); + Ok(()) + } + + fn action_start_drag(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _target = self.pop()?; + let _lock_center = self.pop()?.as_bool(); + let constrain = self.pop()?.as_bool(); + if constrain { + let _y2 = self.pop()?; + let _x2 = self.pop()?; + let _y1 = self.pop()?; + let _x1 = self.pop()?; + } + log::error!("Unimplemented action: StartDrag"); + Ok(()) + } + + fn action_stop(&mut self, context: &mut ActionContext) -> Result<(), Error> { + let mut display_object = context.active_clip.write(context.gc_context); + if let Some(clip) = display_object.as_movie_clip_mut() { + clip.stop(); + } else { + log::warn!("Stop failed: Not a MovieClip"); + } + Ok(()) + } + + fn action_stop_sounds(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + Err("Unimplemented action: StopSounds".into()) + } + + fn action_store_register( + &mut self, + _context: &mut ActionContext, + _register: u8, + ) -> Result<(), Error> { + // Does NOT pop the value from the stack. + let _val = self.stack.last().ok_or("Stack underflow")?; + Err("Unimplemented action: StoreRegister".into()) + } + + fn action_string_add(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // SWFv4 string concatenation + // TODO(Herschel): Result with non-string operands? + let a = self.pop()?.into_string(); + let mut b = self.pop()?.into_string(); + b.push_str(&a); + self.push(Value::String(b)); + Ok(()) + } + + fn action_string_equals(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 strcmp + let a = self.pop()?; + let b = self.pop()?; + let result = b.into_string() == a.into_string(); + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + fn action_string_extract(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // SWFv4 substring + // TODO(Herschel): Result with incorrect operands? + let len = self.pop()?.as_f64()? as usize; + let start = self.pop()?.as_f64()? as usize; + let s = self.pop()?.into_string(); + // This is specifically a non-UTF8 aware substring. + // SWFv4 only used ANSI strings. + let result = s + .bytes() + .skip(start) + .take(len) + .map(|c| c as char) + .collect::(); + self.push(Value::String(result)); + Ok(()) + } + + fn action_string_length(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 strlen + // Only returns byte length. + // TODO(Herschel): Result with non-string operands? + let val = self.pop()?.into_string().bytes().len() as f64; + self.push(Value::Number(val)); + Ok(()) + } + + fn action_string_less(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // AS1 strcmp + let a = self.pop()?; + let b = self.pop()?; + // This is specifically a non-UTF8 aware comparison. + let result = b.into_string().bytes().lt(a.into_string().bytes()); + self.push(Value::from_bool_v1(result, self.swf_version)); + Ok(()) + } + + fn action_subtract(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let a = self.pop()?; + let b = self.pop()?; + self.push(Value::Number(a.into_number_v1() + b.into_number_v1())); + Ok(()) + } + + fn action_target_path(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel) + let _clip = self.pop()?.as_object()?; + self.push(Value::Undefined); + Err("Unimplemented action: TargetPath".into()) + } + + fn toggle_quality(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + // TODO(Herschel): Noop for now? Could chang anti-aliasing on render backend. + Ok(()) + } + + fn action_to_integer(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let val = self.pop()?; + self.push(Value::Number(val.into_number_v1().trunc())); + Ok(()) + } + + fn action_to_number(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let val = self.pop()?; + self.push(Value::Number(val.into_number())); + Ok(()) + } + + fn action_to_string(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let val = self.pop()?; + self.push(Value::String(val.into_string())); + Ok(()) + } + + fn action_trace(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let val = self.pop()?; + log::info!(target: "avm_trace", "{}", val.into_string()); + Ok(()) + } + + fn action_type_of(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _type_str = match self.pop()? { + Value::Undefined => "undefined", + Value::Null => "null", + Value::Number(_) => "number", + Value::Bool(_) => "boolean", + Value::String(_) => "string", + Value::Object(_) => "object", + }; + // TODO(Herschel): function, movieclip + Ok(()) + } + + fn action_wait_for_frame( + &mut self, + _context: &mut ActionContext, + _frame: u16, + num_actions_to_skip: u8, + reader: &mut Reader>, + ) -> Result<(), Error> { + // TODO(Herschel): Always true for now. + let loaded = true; + if !loaded { + // Note that the offset is given in # of actions, NOT in bytes. + // Read the actions and toss them away. + for _ in 0..num_actions_to_skip { + reader.read_action()?; + } + } + Ok(()) + } + + fn action_wait_for_frame_2( + &mut self, + _context: &mut ActionContext, + num_actions_to_skip: u8, + reader: &mut Reader>, + ) -> Result<(), Error> { + // TODO(Herschel): Always true for now. + let _frame_num = self.pop()?.as_f64()? as u16; + let loaded = true; + if !loaded { + // Note that the offset is given in # of actions, NOT in bytes. + // Read the actions and toss them away. + for _ in 0..num_actions_to_skip { + reader.read_action()?; + } + } + Ok(()) + } + + fn action_with(&mut self, _context: &mut ActionContext) -> Result<(), Error> { + let _object = self.pop()?.as_object()?; + Err("Unimplemented action: With".into()) + } +} + +type ObjectPtr = std::marker::PhantomData<()>; + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum Value { + Undefined, + Null, + Bool(bool), + Number(f64), + String(String), + Object(ObjectPtr), +} + +impl Value { + fn into_number_v1(self) -> f64 { + match self { + Value::Bool(true) => 1.0, + Value::Number(v) => v, + Value::String(v) => v.parse().unwrap_or(0.0), + _ => 0.0, + } + } + + fn into_number(self) -> f64 { + // ECMA-262 2nd edtion s. 9.3 ToNumber + use std::f64::NAN; + match self { + Value::Undefined => NAN, + Value::Null => NAN, + Value::Bool(false) => 0.0, + Value::Bool(true) => 1.0, + Value::Number(v) => v, + Value::String(v) => v.parse().unwrap_or(NAN), // TODO(Herschel): Handle Infinity/etc.? + Value::Object(_object) => { + log::error!("Unimplemented: Object ToNumber"); + 0.0 + } + } + } + + fn from_bool_v1(value: bool, swf_version: u8) -> Value { + // SWF version 4 did not have true bools and will push bools as 0 or 1. + // e.g. SWF19 p. 72: + // "If the numbers are equal, true is pushed to the stack for SWF 5 and later. For SWF 4, 1 is pushed to the stack." + if swf_version >= 5 { + Value::Bool(value) + } else { + Value::Number(if value { 1.0 } else { 0.0 }) + } + } + + fn into_string(self) -> String { + match self { + Value::Undefined => "undefined".to_string(), + Value::Null => "null".to_string(), + Value::Bool(v) => v.to_string(), + Value::Number(v) => v.to_string(), // TODO(Herschel): Rounding for int? + Value::String(v) => v, + Value::Object(_) => "[Object object]".to_string(), // TODO(Herschel): + } + } + + fn as_bool(&self) -> bool { + match *self { + Value::Bool(v) => v, + Value::Number(v) => v != 0.0, + // TODO(Herschel): Value::String(v) => ?? + _ => false, + } + } + + fn as_i32(&self) -> Result { + self.as_f64().map(|n| n as i32) + } + + fn as_u32(&self) -> Result { + self.as_f64().map(|n| n as u32) + } + + fn as_i64(&self) -> Result { + self.as_f64().map(|n| n as i64) + } + + fn as_f64(&self) -> Result { + match *self { + Value::Number(v) => Ok(v), + _ => Err(format!("Expected Number, found {:?}", self).into()), + } + } + + fn as_string(&self) -> Result<&String, Error> { + match self { + Value::String(s) => Ok(s), + _ => Err(format!("Expected String, found {:?}", self).into()), + } + } + + fn as_object(&self) -> Result<&ObjectPtr, Error> { + if let Value::Object(object) = self { + Ok(object) + } else { + Err(format!("Expected Object, found {:?}", self).into()) + } + } +} diff --git a/core/src/backend.rs b/core/src/backend.rs index 6fe49ca57..5f874853b 100644 --- a/core/src/backend.rs +++ b/core/src/backend.rs @@ -1,2 +1,2 @@ -pub mod audio; -pub mod render; +pub mod audio; +pub mod render; diff --git a/core/src/backend/audio.rs b/core/src/backend/audio.rs index cbb2bec39..e3d7fba46 100644 --- a/core/src/backend/audio.rs +++ b/core/src/backend/audio.rs @@ -1,73 +1,73 @@ -use generational_arena::{Arena, Index}; - -pub mod decoders; -pub mod swf { - pub use swf::{read, AudioCompression, CharacterId, Sound, SoundFormat, SoundStreamHead}; -} - -pub type AudioStreamHandle = Index; -pub type SoundHandle = Index; - -type Error = Box; - -pub trait AudioBackend { - fn prime_audio(&mut self) {} - fn register_sound(&mut self, swf_sound: &swf::Sound) -> Result; - fn preload_sound_stream_head( - &mut self, - _clip_id: swf::CharacterId, - _stream_info: &swf::SoundStreamHead, - ) { - } - fn preload_sound_stream_block(&mut self, _clip_id: swf::CharacterId, _audio_data: &[u8]) {} - fn preload_sound_stream_end(&mut self, _clip_id: swf::CharacterId) {} - fn play_sound(&mut self, sound: SoundHandle); - fn start_stream( - &mut self, - clip_id: crate::prelude::CharacterId, - clip_data: crate::tag_utils::SwfSlice, - handle: &swf::SoundStreamHead, - ) -> AudioStreamHandle; - // TODO: Eventually remove this/move it to library. - fn is_loading_complete(&self) -> bool { - true - } - fn tick(&mut self) {} -} - -pub struct NullAudioBackend { - sounds: Arena<()>, - streams: Arena<()>, -} - -impl NullAudioBackend { - pub fn new() -> NullAudioBackend { - NullAudioBackend { - streams: Arena::new(), - sounds: Arena::new(), - } - } -} - -impl AudioBackend for NullAudioBackend { - fn register_sound(&mut self, _sound: &swf::Sound) -> Result { - Ok(self.sounds.insert(())) - } - - fn play_sound(&mut self, _sound: SoundHandle) {} - - fn start_stream( - &mut self, - _clip_id: crate::prelude::CharacterId, - _clip_data: crate::tag_utils::SwfSlice, - _handle: &swf::SoundStreamHead, - ) -> AudioStreamHandle { - self.streams.insert(()) - } -} - -impl Default for NullAudioBackend { - fn default() -> Self { - NullAudioBackend::new() - } -} +use generational_arena::{Arena, Index}; + +pub mod decoders; +pub mod swf { + pub use swf::{read, AudioCompression, CharacterId, Sound, SoundFormat, SoundStreamHead}; +} + +pub type AudioStreamHandle = Index; +pub type SoundHandle = Index; + +type Error = Box; + +pub trait AudioBackend { + fn prime_audio(&mut self) {} + fn register_sound(&mut self, swf_sound: &swf::Sound) -> Result; + fn preload_sound_stream_head( + &mut self, + _clip_id: swf::CharacterId, + _stream_info: &swf::SoundStreamHead, + ) { + } + fn preload_sound_stream_block(&mut self, _clip_id: swf::CharacterId, _audio_data: &[u8]) {} + fn preload_sound_stream_end(&mut self, _clip_id: swf::CharacterId) {} + fn play_sound(&mut self, sound: SoundHandle); + fn start_stream( + &mut self, + clip_id: crate::prelude::CharacterId, + clip_data: crate::tag_utils::SwfSlice, + handle: &swf::SoundStreamHead, + ) -> AudioStreamHandle; + // TODO: Eventually remove this/move it to library. + fn is_loading_complete(&self) -> bool { + true + } + fn tick(&mut self) {} +} + +pub struct NullAudioBackend { + sounds: Arena<()>, + streams: Arena<()>, +} + +impl NullAudioBackend { + pub fn new() -> NullAudioBackend { + NullAudioBackend { + streams: Arena::new(), + sounds: Arena::new(), + } + } +} + +impl AudioBackend for NullAudioBackend { + fn register_sound(&mut self, _sound: &swf::Sound) -> Result { + Ok(self.sounds.insert(())) + } + + fn play_sound(&mut self, _sound: SoundHandle) {} + + fn start_stream( + &mut self, + _clip_id: crate::prelude::CharacterId, + _clip_data: crate::tag_utils::SwfSlice, + _handle: &swf::SoundStreamHead, + ) -> AudioStreamHandle { + self.streams.insert(()) + } +} + +impl Default for NullAudioBackend { + fn default() -> Self { + NullAudioBackend::new() + } +} diff --git a/core/src/backend/audio/decoders.rs b/core/src/backend/audio/decoders.rs index c1965c1fd..02d5daec8 100644 --- a/core/src/backend/audio/decoders.rs +++ b/core/src/backend/audio/decoders.rs @@ -1,76 +1,76 @@ -mod adpcm; -mod mp3; - -pub use adpcm::AdpcmDecoder; -pub use mp3::Mp3Decoder; - -pub trait Decoder: Iterator { - fn num_channels(&self) -> u8; - fn sample_rate(&self) -> u16; -} - -pub fn stream_tag_reader( - swf_data: crate::tag_utils::SwfSlice, -) -> IterRead> { - use std::io::{Cursor, Read}; - use swf::TagCode; - - let mut reader = swf::read::Reader::new(Cursor::new(swf_data), 8); - let mut audio_data = vec![]; - let mut cur_byte = 0; - let mut frame = 1; - let iter = std::iter::from_fn(move || { - if cur_byte >= audio_data.len() { - cur_byte = 0; - let tag_callback = - |reader: &mut swf::read::Reader>, - tag_code, - tag_len| match tag_code { - TagCode::ShowFrame => { - frame += 1; - Ok(()) - } - TagCode::SoundStreamBlock => { - audio_data.clear(); - let mut data = vec![]; - reader - .get_mut() - .take(tag_len as u64) - .read_to_end(&mut data)?; - audio_data.extend(data[4..].iter()); - Ok(()) - } - _ => Ok(()), - }; - - let _ = - crate::tag_utils::decode_tags(&mut reader, tag_callback, TagCode::SoundStreamBlock); - } - - if cur_byte < audio_data.len() { - let byte = audio_data[cur_byte]; - cur_byte += 1; - Some(byte) - } else { - None - } - }); - IterRead(iter) -} - -pub struct IterRead>(I); - -impl> std::io::Read for IterRead { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let mut n = 0; - for out in buf { - if let Some(v) = self.0.next() { - *out = v; - n += 1; - } else { - break; - } - } - Ok(n) - } -} +mod adpcm; +mod mp3; + +pub use adpcm::AdpcmDecoder; +pub use mp3::Mp3Decoder; + +pub trait Decoder: Iterator { + fn num_channels(&self) -> u8; + fn sample_rate(&self) -> u16; +} + +pub fn stream_tag_reader( + swf_data: crate::tag_utils::SwfSlice, +) -> IterRead> { + use std::io::{Cursor, Read}; + use swf::TagCode; + + let mut reader = swf::read::Reader::new(Cursor::new(swf_data), 8); + let mut audio_data = vec![]; + let mut cur_byte = 0; + let mut frame = 1; + let iter = std::iter::from_fn(move || { + if cur_byte >= audio_data.len() { + cur_byte = 0; + let tag_callback = + |reader: &mut swf::read::Reader>, + tag_code, + tag_len| match tag_code { + TagCode::ShowFrame => { + frame += 1; + Ok(()) + } + TagCode::SoundStreamBlock => { + audio_data.clear(); + let mut data = vec![]; + reader + .get_mut() + .take(tag_len as u64) + .read_to_end(&mut data)?; + audio_data.extend(data[4..].iter()); + Ok(()) + } + _ => Ok(()), + }; + + let _ = + crate::tag_utils::decode_tags(&mut reader, tag_callback, TagCode::SoundStreamBlock); + } + + if cur_byte < audio_data.len() { + let byte = audio_data[cur_byte]; + cur_byte += 1; + Some(byte) + } else { + None + } + }); + IterRead(iter) +} + +pub struct IterRead>(I); + +impl> std::io::Read for IterRead { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let mut n = 0; + for out in buf { + if let Some(v) = self.0.next() { + *out = v; + n += 1; + } else { + break; + } + } + Ok(n) + } +} diff --git a/core/src/backend/audio/decoders/adpcm.rs b/core/src/backend/audio/decoders/adpcm.rs index 1f082ae04..2f5a733c6 100644 --- a/core/src/backend/audio/decoders/adpcm.rs +++ b/core/src/backend/audio/decoders/adpcm.rs @@ -1,172 +1,172 @@ -use super::Decoder; -use bitstream_io::{BigEndian, BitReader}; -use std::io::Read; - -pub struct AdpcmDecoder { - inner: BitReader, - sample_rate: u16, - is_stereo: bool, - bits_per_sample: usize, - sample_num: u16, - left_sample: i32, - left_step_index: i16, - left_step: i32, - right_sample: i32, - right_step_index: i16, - right_step: i32, - cur_channel: u8, -} - -impl AdpcmDecoder { - const INDEX_TABLE: [&'static [i16]; 4] = [ - &[-1, 2], - &[-1, -1, 2, 4], - &[-1, -1, -1, -1, 2, 4, 6, 8], - &[-1, -1, -1, -1, -1, -1, -1, -1, 1, 2, 4, 6, 8, 10, 13, 16], - ]; - - const STEP_TABLE: [i32; 89] = [ - 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, - 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, - 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, - 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, - 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, - 29794, 32767, - ]; - - pub fn new(inner: R, is_stereo: bool, sample_rate: u16) -> Result { - let mut reader = BitReader::new(inner); - let bits_per_sample = reader.read::(2)? as usize + 2; - - let left_sample = 0; - let left_step_index = 0; - let left_step = 0; - let right_sample = 0; - let right_step_index = 0; - let right_step = 0; - Ok(Self { - inner: reader, - sample_rate, - is_stereo, - bits_per_sample, - sample_num: 0, - left_sample, - left_step, - left_step_index, - right_sample, - right_step, - right_step_index, - cur_channel: 2, - }) - } - - pub fn next_sample(&mut self) -> Result<(), std::io::Error> { - self.cur_channel = 0; - - if self.sample_num == 0 { - // The initial sample values are NOT byte-aligned. - self.left_sample = self.inner.read_signed(16)?; - self.left_step_index = self.inner.read::(6)? as i16; - self.left_step = Self::STEP_TABLE[self.left_step_index as usize]; - if self.is_stereo { - self.right_sample = self.inner.read_signed(16)?; - self.right_step_index = self.inner.read::(6)? as i16; - self.right_step = Self::STEP_TABLE[self.right_step_index as usize]; - } - } - - self.sample_num = (self.sample_num + 1) % 4095; - - let data: i32 = self.inner.read::(self.bits_per_sample as u32)? as i32; - self.left_step = Self::STEP_TABLE[self.left_step_index as usize]; - - // (data + 0.5) * step / 2^(bits_per_sample - 2) - // Data is sign-magnitude, NOT two's complement. - // TODO(Herschel): Other implementations use some bit-tricks for this. - let sign_mask = 1 << (self.bits_per_sample - 1); - let magnitude = data & !sign_mask; - let delta = (2 * magnitude + 1) * self.left_step / sign_mask; - - if (data & sign_mask) != 0 { - self.left_sample -= delta; - } else { - self.left_sample += delta; - } - if self.left_sample < -32768 { - self.left_sample = 32768; - } else if self.left_sample > 32767 { - self.left_sample = 32767; - } - - let i = magnitude as usize; - self.left_step_index += Self::INDEX_TABLE[self.bits_per_sample - 2][i]; - if self.left_step_index < 0 { - self.left_step_index = 0; - } else if self.left_step_index >= Self::STEP_TABLE.len() as i16 { - self.left_step_index = Self::STEP_TABLE.len() as i16 - 1; - } - - if self.is_stereo { - let data = self.inner.read::(self.bits_per_sample as u32)? as i32; - self.right_step = Self::STEP_TABLE[self.right_step_index as usize]; - - let sign_mask = 1 << (self.bits_per_sample - 1); - let magnitude = data & !sign_mask; - let delta = (2 * magnitude + 1) * self.right_step / sign_mask; - - if (data & sign_mask) != 0 { - self.right_sample -= delta; - } else { - self.right_sample += delta; - } - if self.right_sample < -32768 { - self.right_sample = 32768; - } else if self.right_sample > 32767 { - self.right_sample = 32767; - } - - let i = magnitude as usize; - self.right_step_index += Self::INDEX_TABLE[self.bits_per_sample - 2][i]; - if self.right_step_index < 0 { - self.right_step_index = 0; - } else if self.right_step_index >= Self::STEP_TABLE.len() as i16 { - self.right_step_index = Self::STEP_TABLE.len() as i16 - 1; - } - } - - Ok(()) - } -} - -impl Iterator for AdpcmDecoder { - type Item = i16; - fn next(&mut self) -> Option { - if self.cur_channel >= if self.is_stereo { 2 } else { 1 } { - self.next_sample().ok()?; - } - - let sample = if self.cur_channel == 0 { - self.left_sample - } else { - self.right_sample - }; - self.cur_channel += 1; - Some(sample as i16) - } -} - -impl Decoder for AdpcmDecoder { - #[inline] - fn num_channels(&self) -> u8 { - if self.is_stereo { - 2 - } else { - 1 - } - } - - #[inline] - fn sample_rate(&self) -> u16 { - self.sample_rate - } -} +use super::Decoder; +use bitstream_io::{BigEndian, BitReader}; +use std::io::Read; + +pub struct AdpcmDecoder { + inner: BitReader, + sample_rate: u16, + is_stereo: bool, + bits_per_sample: usize, + sample_num: u16, + left_sample: i32, + left_step_index: i16, + left_step: i32, + right_sample: i32, + right_step_index: i16, + right_step: i32, + cur_channel: u8, +} + +impl AdpcmDecoder { + const INDEX_TABLE: [&'static [i16]; 4] = [ + &[-1, 2], + &[-1, -1, 2, 4], + &[-1, -1, -1, -1, 2, 4, 6, 8], + &[-1, -1, -1, -1, -1, -1, -1, -1, 1, 2, 4, 6, 8, 10, 13, 16], + ]; + + const STEP_TABLE: [i32; 89] = [ + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, + 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, + 408, 449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, + 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, + 8630, 9493, 10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, + 29794, 32767, + ]; + + pub fn new(inner: R, is_stereo: bool, sample_rate: u16) -> Result { + let mut reader = BitReader::new(inner); + let bits_per_sample = reader.read::(2)? as usize + 2; + + let left_sample = 0; + let left_step_index = 0; + let left_step = 0; + let right_sample = 0; + let right_step_index = 0; + let right_step = 0; + Ok(Self { + inner: reader, + sample_rate, + is_stereo, + bits_per_sample, + sample_num: 0, + left_sample, + left_step, + left_step_index, + right_sample, + right_step, + right_step_index, + cur_channel: 2, + }) + } + + pub fn next_sample(&mut self) -> Result<(), std::io::Error> { + self.cur_channel = 0; + + if self.sample_num == 0 { + // The initial sample values are NOT byte-aligned. + self.left_sample = self.inner.read_signed(16)?; + self.left_step_index = self.inner.read::(6)? as i16; + self.left_step = Self::STEP_TABLE[self.left_step_index as usize]; + if self.is_stereo { + self.right_sample = self.inner.read_signed(16)?; + self.right_step_index = self.inner.read::(6)? as i16; + self.right_step = Self::STEP_TABLE[self.right_step_index as usize]; + } + } + + self.sample_num = (self.sample_num + 1) % 4095; + + let data: i32 = self.inner.read::(self.bits_per_sample as u32)? as i32; + self.left_step = Self::STEP_TABLE[self.left_step_index as usize]; + + // (data + 0.5) * step / 2^(bits_per_sample - 2) + // Data is sign-magnitude, NOT two's complement. + // TODO(Herschel): Other implementations use some bit-tricks for this. + let sign_mask = 1 << (self.bits_per_sample - 1); + let magnitude = data & !sign_mask; + let delta = (2 * magnitude + 1) * self.left_step / sign_mask; + + if (data & sign_mask) != 0 { + self.left_sample -= delta; + } else { + self.left_sample += delta; + } + if self.left_sample < -32768 { + self.left_sample = 32768; + } else if self.left_sample > 32767 { + self.left_sample = 32767; + } + + let i = magnitude as usize; + self.left_step_index += Self::INDEX_TABLE[self.bits_per_sample - 2][i]; + if self.left_step_index < 0 { + self.left_step_index = 0; + } else if self.left_step_index >= Self::STEP_TABLE.len() as i16 { + self.left_step_index = Self::STEP_TABLE.len() as i16 - 1; + } + + if self.is_stereo { + let data = self.inner.read::(self.bits_per_sample as u32)? as i32; + self.right_step = Self::STEP_TABLE[self.right_step_index as usize]; + + let sign_mask = 1 << (self.bits_per_sample - 1); + let magnitude = data & !sign_mask; + let delta = (2 * magnitude + 1) * self.right_step / sign_mask; + + if (data & sign_mask) != 0 { + self.right_sample -= delta; + } else { + self.right_sample += delta; + } + if self.right_sample < -32768 { + self.right_sample = 32768; + } else if self.right_sample > 32767 { + self.right_sample = 32767; + } + + let i = magnitude as usize; + self.right_step_index += Self::INDEX_TABLE[self.bits_per_sample - 2][i]; + if self.right_step_index < 0 { + self.right_step_index = 0; + } else if self.right_step_index >= Self::STEP_TABLE.len() as i16 { + self.right_step_index = Self::STEP_TABLE.len() as i16 - 1; + } + } + + Ok(()) + } +} + +impl Iterator for AdpcmDecoder { + type Item = i16; + fn next(&mut self) -> Option { + if self.cur_channel >= if self.is_stereo { 2 } else { 1 } { + self.next_sample().ok()?; + } + + let sample = if self.cur_channel == 0 { + self.left_sample + } else { + self.right_sample + }; + self.cur_channel += 1; + Some(sample as i16) + } +} + +impl Decoder for AdpcmDecoder { + #[inline] + fn num_channels(&self) -> u8 { + if self.is_stereo { + 2 + } else { + 1 + } + } + + #[inline] + fn sample_rate(&self) -> u16 { + self.sample_rate + } +} diff --git a/core/src/backend/audio/decoders/mp3.rs b/core/src/backend/audio/decoders/mp3.rs index 16f5d9b92..053c01cf6 100644 --- a/core/src/backend/audio/decoders/mp3.rs +++ b/core/src/backend/audio/decoders/mp3.rs @@ -1,124 +1,124 @@ -#[cfg(feature = "minimp3")] -#[allow(dead_code)] -pub struct Mp3Decoder { - decoder: minimp3::Decoder, - sample_rate: u32, - num_channels: u16, - cur_frame: minimp3::Frame, - cur_sample: usize, - num_samples: usize, -} - -#[cfg(feature = "minimp3")] -impl Mp3Decoder { - pub fn new(num_channels: u16, sample_rate: u32, reader: R) -> Self { - Mp3Decoder { - decoder: minimp3::Decoder::new(reader), - num_channels, - sample_rate, - cur_frame: unsafe { std::mem::zeroed::() }, - cur_sample: 0, - num_samples: 0, - } - } - - fn next_frame(&mut self) { - if let Ok(frame) = self.decoder.next_frame() { - self.num_samples = frame.data.len(); - self.cur_frame = frame; - } else { - self.num_samples = 0; - } - self.cur_sample = 0; - } -} - -#[cfg(feature = "minimp3")] -impl Iterator for Mp3Decoder { - type Item = i16; - - #[inline] - fn next(&mut self) -> Option { - if self.cur_sample >= self.num_samples { - self.next_frame(); - } - - if self.num_samples > 0 { - let sample = self.cur_frame.data[self.cur_sample]; - self.cur_sample += 1; - Some(sample) - } else { - None - } - } -} - -#[cfg(all(feature = "puremp3", not(feature = "minimp3")))] -pub struct Mp3Decoder { - decoder: puremp3::Mp3Decoder, - sample_rate: u32, - num_channels: u16, - cur_frame: puremp3::Frame, - cur_sample: usize, - cur_channel: usize, -} - -#[cfg(all(feature = "puremp3", not(feature = "minimp3")))] -impl Mp3Decoder { - pub fn new(num_channels: u16, sample_rate: u32, reader: R) -> Self { - Mp3Decoder { - decoder: puremp3::Mp3Decoder::new(reader), - num_channels, - sample_rate, - cur_frame: unsafe { std::mem::zeroed::() }, - cur_sample: 0, - cur_channel: 0, - } - } - - fn next_frame(&mut self) { - if let Ok(frame) = self.decoder.next_frame() { - self.cur_frame = frame; - } else { - self.cur_frame.num_samples = 0; - } - self.cur_sample = 0; - self.cur_channel = 0; - } -} - -impl super::Decoder for Mp3Decoder { - #[inline] - fn num_channels(&self) -> u8 { - self.num_channels as u8 - } - - #[inline] - fn sample_rate(&self) -> u16 { - self.sample_rate as u16 - } -} - -#[cfg(all(feature = "puremp3", not(feature = "minimp3")))] -impl Iterator for Mp3Decoder { - type Item = i16; - - #[inline] - fn next(&mut self) -> Option { - if self.cur_sample >= self.cur_frame.num_samples { - self.next_frame(); - } - - if self.cur_frame.num_samples > 0 { - let sample = self.cur_frame.samples[self.cur_channel][self.cur_sample]; - self.cur_channel += 1; - if self.cur_channel >= usize::from(self.num_channels) { - self.cur_channel = 0; - self.cur_sample += 1; - } - Some((sample * 32767.0) as i16) - } else { - None - } - } -} +#[cfg(feature = "minimp3")] +#[allow(dead_code)] +pub struct Mp3Decoder { + decoder: minimp3::Decoder, + sample_rate: u32, + num_channels: u16, + cur_frame: minimp3::Frame, + cur_sample: usize, + num_samples: usize, +} + +#[cfg(feature = "minimp3")] +impl Mp3Decoder { + pub fn new(num_channels: u16, sample_rate: u32, reader: R) -> Self { + Mp3Decoder { + decoder: minimp3::Decoder::new(reader), + num_channels, + sample_rate, + cur_frame: unsafe { std::mem::zeroed::() }, + cur_sample: 0, + num_samples: 0, + } + } + + fn next_frame(&mut self) { + if let Ok(frame) = self.decoder.next_frame() { + self.num_samples = frame.data.len(); + self.cur_frame = frame; + } else { + self.num_samples = 0; + } + self.cur_sample = 0; + } +} + +#[cfg(feature = "minimp3")] +impl Iterator for Mp3Decoder { + type Item = i16; + + #[inline] + fn next(&mut self) -> Option { + if self.cur_sample >= self.num_samples { + self.next_frame(); + } + + if self.num_samples > 0 { + let sample = self.cur_frame.data[self.cur_sample]; + self.cur_sample += 1; + Some(sample) + } else { + None + } + } +} + +#[cfg(all(feature = "puremp3", not(feature = "minimp3")))] +pub struct Mp3Decoder { + decoder: puremp3::Mp3Decoder, + sample_rate: u32, + num_channels: u16, + cur_frame: puremp3::Frame, + cur_sample: usize, + cur_channel: usize, +} + +#[cfg(all(feature = "puremp3", not(feature = "minimp3")))] +impl Mp3Decoder { + pub fn new(num_channels: u16, sample_rate: u32, reader: R) -> Self { + Mp3Decoder { + decoder: puremp3::Mp3Decoder::new(reader), + num_channels, + sample_rate, + cur_frame: unsafe { std::mem::zeroed::() }, + cur_sample: 0, + cur_channel: 0, + } + } + + fn next_frame(&mut self) { + if let Ok(frame) = self.decoder.next_frame() { + self.cur_frame = frame; + } else { + self.cur_frame.num_samples = 0; + } + self.cur_sample = 0; + self.cur_channel = 0; + } +} + +impl super::Decoder for Mp3Decoder { + #[inline] + fn num_channels(&self) -> u8 { + self.num_channels as u8 + } + + #[inline] + fn sample_rate(&self) -> u16 { + self.sample_rate as u16 + } +} + +#[cfg(all(feature = "puremp3", not(feature = "minimp3")))] +impl Iterator for Mp3Decoder { + type Item = i16; + + #[inline] + fn next(&mut self) -> Option { + if self.cur_sample >= self.cur_frame.num_samples { + self.next_frame(); + } + + if self.cur_frame.num_samples > 0 { + let sample = self.cur_frame.samples[self.cur_channel][self.cur_sample]; + self.cur_channel += 1; + if self.cur_channel >= usize::from(self.num_channels) { + self.cur_channel = 0; + self.cur_sample += 1; + } + Some((sample * 32767.0) as i16) + } else { + None + } + } +} diff --git a/core/src/backend/render.rs b/core/src/backend/render.rs index f74aec188..4ba3a9036 100644 --- a/core/src/backend/render.rs +++ b/core/src/backend/render.rs @@ -1,278 +1,278 @@ -pub use crate::{transform::Transform, Color}; -use std::io::Read; -pub use swf; - -pub trait RenderBackend { - fn set_viewport_dimensions(&mut self, width: u32, height: u32); - fn register_shape(&mut self, shape: &swf::Shape) -> ShapeHandle; - fn register_glyph_shape(&mut self, shape: &swf::Glyph) -> ShapeHandle; - fn register_bitmap_jpeg( - &mut self, - id: swf::CharacterId, - data: &[u8], - jpeg_tables: &[u8], - ) -> BitmapHandle; - fn register_bitmap_jpeg_2(&mut self, id: swf::CharacterId, data: &[u8]) -> BitmapHandle; - fn register_bitmap_jpeg_3( - &mut self, - id: swf::CharacterId, - jpeg_data: &[u8], - alpha_data: &[u8], - ) -> BitmapHandle; - fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle; - - fn begin_frame(&mut self); - fn clear(&mut self, color: Color); - fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform); - fn end_frame(&mut self); - fn draw_pause_overlay(&mut self); - fn draw_letterbox(&mut self, letterbox: Letterbox); -} - -#[derive(Copy, Clone, Debug)] -pub struct ShapeHandle(pub usize); - -#[derive(Copy, Clone, Debug)] -pub struct BitmapHandle(pub usize); - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Letterbox { - None, - Letterbox(f32), - Pillarbox(f32), -} - -pub struct NullRenderer; - -impl RenderBackend for NullRenderer { - fn set_viewport_dimensions(&mut self, _width: u32, _height: u32) {} - fn register_shape(&mut self, _shape: &swf::Shape) -> ShapeHandle { - ShapeHandle(0) - } - fn register_glyph_shape(&mut self, _shape: &swf::Glyph) -> ShapeHandle { - ShapeHandle(0) - } - fn register_bitmap_jpeg( - &mut self, - _id: swf::CharacterId, - _data: &[u8], - _jpeg_tables: &[u8], - ) -> BitmapHandle { - BitmapHandle(0) - } - fn register_bitmap_jpeg_2(&mut self, _id: swf::CharacterId, _data: &[u8]) -> BitmapHandle { - BitmapHandle(0) - } - fn register_bitmap_jpeg_3( - &mut self, - _id: swf::CharacterId, - _data: &[u8], - _alpha_data: &[u8], - ) -> BitmapHandle { - BitmapHandle(0) - } - fn register_bitmap_png(&mut self, _swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { - BitmapHandle(0) - } - fn begin_frame(&mut self) {} - fn end_frame(&mut self) {} - fn clear(&mut self, _color: Color) {} - fn render_shape(&mut self, _shape: ShapeHandle, _transform: &Transform) {} - fn draw_pause_overlay(&mut self) {} - fn draw_letterbox(&mut self, _letterbox: Letterbox) {} -} - -pub fn glue_swf_jpeg_to_tables(jpeg_tables: &[u8], jpeg_data: &[u8]) -> Vec { - let mut full_jpeg = Vec::with_capacity(jpeg_tables.len() + jpeg_data.len() - 4); - full_jpeg.extend_from_slice(&jpeg_tables[..jpeg_tables.len() - 2]); - full_jpeg.extend_from_slice(&jpeg_data[2..]); - full_jpeg -} - -/// Removes potential invalid JPEG data from SWF DefineBitsJPEG tags. -/// -/// SWF19 p.138: -/// "Before version 8 of the SWF file format, SWF files could contain an erroneous header of 0xFF, 0xD9, 0xFF, 0xD8 before the JPEG SOI marker." -/// These bytes need to be removed for the JPEG to decode properly. -pub fn remove_invalid_jpeg_data(mut data: &[u8]) -> std::borrow::Cow<[u8]> { - // TODO: Might be better to return an Box> instead of a Cow here, - // where the spliced iter is a data[..n].chain(data[n+4..])? - if data[..4] == [0xFF, 0xD9, 0xFF, 0xD8] { - data = &data[4..]; - } - if let Some(pos) = (0..data.len() - 4).find(|&n| data[n..n + 4] == [0xFF, 0xD9, 0xFF, 0xD8]) { - let mut out_data = Vec::with_capacity(data.len() - 4); - out_data.extend_from_slice(&data[..pos]); - out_data.extend_from_slice(&data[pos + 4..]); - std::borrow::Cow::from(out_data) - } else { - std::borrow::Cow::Borrowed(data) - } -} - -/// Decodes a JPEG with optional alpha data. -/// -pub fn define_bits_jpeg_to_rgba( - jpeg_data: &[u8], - alpha_data: &[u8], -) -> Result<(u32, u32, Vec), Box> { - let jpeg_data = remove_invalid_jpeg_data(jpeg_data); - - let mut decoder = jpeg_decoder::Decoder::new(&jpeg_data[..]); - decoder.read_info().unwrap(); - let metadata = decoder.info().unwrap(); - let decoded_data = decoder.decode().expect("failed to decode image"); - - // Decompress the alpha data (DEFLATE compression). - let alpha_data = { - let mut data = vec![]; - let mut decoder = libflate::zlib::Decoder::new(alpha_data)?; - decoder.read_to_end(&mut data)?; - data - }; - - let mut rgba = Vec::with_capacity((decoded_data.len() / 3) * 4); - let mut i = 0; - let mut a = 0; - while i < decoded_data.len() { - rgba.push(decoded_data[i]); - rgba.push(decoded_data[i + 1]); - rgba.push(decoded_data[i + 2]); - rgba.push(alpha_data[a]); - i += 3; - a += 1; - } - - Ok((metadata.width.into(), metadata.height.into(), rgba)) -} - -/// Decodes the bitmap data in DefineBitsLossless tag into RGBA. -/// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly -/// palletized. -pub fn define_bits_lossless_to_rgba( - swf_tag: &swf::DefineBitsLossless, -) -> Result, Box> { - // Decompress the image data (DEFLATE compression). - let mut decoded_data = { - let mut data = vec![]; - let mut decoder = libflate::zlib::Decoder::new(&swf_tag.data[..])?; - decoder.read_to_end(&mut data)?; - data - }; - - // Swizzle/de-palettize the bitmap. - let out_data = match (swf_tag.version, swf_tag.format) { - (1, swf::BitmapFormat::Rgb15) => unimplemented!("15-bit PNG"), - (1, swf::BitmapFormat::Rgb32) => { - let mut i = 0; - while i < decoded_data.len() { - decoded_data[i] = decoded_data[i + 1]; - decoded_data[i + 1] = decoded_data[i + 2]; - decoded_data[i + 2] = decoded_data[i + 3]; - decoded_data[i + 3] = 0xff; - i += 4; - } - decoded_data - } - (2, swf::BitmapFormat::Rgb32) => { - let mut i = 0; - while i < decoded_data.len() { - let alpha = decoded_data[i]; - decoded_data[i] = decoded_data[i + 1]; - decoded_data[i + 1] = decoded_data[i + 2]; - decoded_data[i + 2] = decoded_data[i + 3]; - decoded_data[i + 3] = alpha; - i += 4; - } - decoded_data - } - (1, swf::BitmapFormat::ColorMap8) => { - let mut i = 0; - let padded_width = (swf_tag.width + 0b11) & !0b11; - - let mut palette = Vec::with_capacity(swf_tag.num_colors as usize + 1); - for _ in 0..=swf_tag.num_colors { - palette.push(Color { - r: decoded_data[i], - g: decoded_data[i + 1], - b: decoded_data[i + 2], - a: 255, - }); - i += 3; - } - let mut out_data = vec![]; - for _ in 0..swf_tag.height { - for _ in 0..swf_tag.width { - let entry = decoded_data[i] as usize; - if entry < palette.len() { - let color = &palette[entry]; - out_data.push(color.r); - out_data.push(color.g); - out_data.push(color.b); - out_data.push(color.a); - } else { - out_data.push(0); - out_data.push(0); - out_data.push(0); - out_data.push(255); - } - i += 1; - } - i += (padded_width - swf_tag.width) as usize; - } - out_data - } - (2, swf::BitmapFormat::ColorMap8) => { - let mut i = 0; - let padded_width = (swf_tag.width + 0b11) & !0b11; - - let mut palette = Vec::with_capacity(swf_tag.num_colors as usize + 1); - for _ in 0..=swf_tag.num_colors { - palette.push(Color { - r: decoded_data[i], - g: decoded_data[i + 1], - b: decoded_data[i + 2], - a: decoded_data[i + 3], - }); - i += 4; - } - let mut out_data = vec![]; - for _ in 0..swf_tag.height { - for _ in 0..swf_tag.width { - let entry = decoded_data[i] as usize; - if entry < palette.len() { - let color = &palette[entry]; - out_data.push(color.r); - out_data.push(color.g); - out_data.push(color.b); - out_data.push(color.a); - } else { - out_data.push(0); - out_data.push(0); - out_data.push(0); - out_data.push(0); - } - i += 1; - } - i += (padded_width - swf_tag.width) as usize; - } - out_data - } - _ => unimplemented!("{:?} {:?}", swf_tag.version, swf_tag.format), - }; - - Ok(out_data) -} - -/// Images in SWFs are stored with premultiplied alpha. -/// Converts RGBA premultiplied alpha to standard RBGA. -pub fn unmultiply_alpha_rgba(rgba: &mut [u8]) { - rgba.chunks_exact_mut(4).for_each(|rgba| { - if rgba[3] > 0 { - let a = f32::from(rgba[3]) / 255.0; - rgba[0] = (f32::from(rgba[0]) / a) as u8; - rgba[1] = (f32::from(rgba[1]) / a) as u8; - rgba[2] = (f32::from(rgba[2]) / a) as u8; - } - }) -} +pub use crate::{transform::Transform, Color}; +use std::io::Read; +pub use swf; + +pub trait RenderBackend { + fn set_viewport_dimensions(&mut self, width: u32, height: u32); + fn register_shape(&mut self, shape: &swf::Shape) -> ShapeHandle; + fn register_glyph_shape(&mut self, shape: &swf::Glyph) -> ShapeHandle; + fn register_bitmap_jpeg( + &mut self, + id: swf::CharacterId, + data: &[u8], + jpeg_tables: &[u8], + ) -> BitmapHandle; + fn register_bitmap_jpeg_2(&mut self, id: swf::CharacterId, data: &[u8]) -> BitmapHandle; + fn register_bitmap_jpeg_3( + &mut self, + id: swf::CharacterId, + jpeg_data: &[u8], + alpha_data: &[u8], + ) -> BitmapHandle; + fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle; + + fn begin_frame(&mut self); + fn clear(&mut self, color: Color); + fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform); + fn end_frame(&mut self); + fn draw_pause_overlay(&mut self); + fn draw_letterbox(&mut self, letterbox: Letterbox); +} + +#[derive(Copy, Clone, Debug)] +pub struct ShapeHandle(pub usize); + +#[derive(Copy, Clone, Debug)] +pub struct BitmapHandle(pub usize); + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Letterbox { + None, + Letterbox(f32), + Pillarbox(f32), +} + +pub struct NullRenderer; + +impl RenderBackend for NullRenderer { + fn set_viewport_dimensions(&mut self, _width: u32, _height: u32) {} + fn register_shape(&mut self, _shape: &swf::Shape) -> ShapeHandle { + ShapeHandle(0) + } + fn register_glyph_shape(&mut self, _shape: &swf::Glyph) -> ShapeHandle { + ShapeHandle(0) + } + fn register_bitmap_jpeg( + &mut self, + _id: swf::CharacterId, + _data: &[u8], + _jpeg_tables: &[u8], + ) -> BitmapHandle { + BitmapHandle(0) + } + fn register_bitmap_jpeg_2(&mut self, _id: swf::CharacterId, _data: &[u8]) -> BitmapHandle { + BitmapHandle(0) + } + fn register_bitmap_jpeg_3( + &mut self, + _id: swf::CharacterId, + _data: &[u8], + _alpha_data: &[u8], + ) -> BitmapHandle { + BitmapHandle(0) + } + fn register_bitmap_png(&mut self, _swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { + BitmapHandle(0) + } + fn begin_frame(&mut self) {} + fn end_frame(&mut self) {} + fn clear(&mut self, _color: Color) {} + fn render_shape(&mut self, _shape: ShapeHandle, _transform: &Transform) {} + fn draw_pause_overlay(&mut self) {} + fn draw_letterbox(&mut self, _letterbox: Letterbox) {} +} + +pub fn glue_swf_jpeg_to_tables(jpeg_tables: &[u8], jpeg_data: &[u8]) -> Vec { + let mut full_jpeg = Vec::with_capacity(jpeg_tables.len() + jpeg_data.len() - 4); + full_jpeg.extend_from_slice(&jpeg_tables[..jpeg_tables.len() - 2]); + full_jpeg.extend_from_slice(&jpeg_data[2..]); + full_jpeg +} + +/// Removes potential invalid JPEG data from SWF DefineBitsJPEG tags. +/// +/// SWF19 p.138: +/// "Before version 8 of the SWF file format, SWF files could contain an erroneous header of 0xFF, 0xD9, 0xFF, 0xD8 before the JPEG SOI marker." +/// These bytes need to be removed for the JPEG to decode properly. +pub fn remove_invalid_jpeg_data(mut data: &[u8]) -> std::borrow::Cow<[u8]> { + // TODO: Might be better to return an Box> instead of a Cow here, + // where the spliced iter is a data[..n].chain(data[n+4..])? + if data[..4] == [0xFF, 0xD9, 0xFF, 0xD8] { + data = &data[4..]; + } + if let Some(pos) = (0..data.len() - 4).find(|&n| data[n..n + 4] == [0xFF, 0xD9, 0xFF, 0xD8]) { + let mut out_data = Vec::with_capacity(data.len() - 4); + out_data.extend_from_slice(&data[..pos]); + out_data.extend_from_slice(&data[pos + 4..]); + std::borrow::Cow::from(out_data) + } else { + std::borrow::Cow::Borrowed(data) + } +} + +/// Decodes a JPEG with optional alpha data. +/// +pub fn define_bits_jpeg_to_rgba( + jpeg_data: &[u8], + alpha_data: &[u8], +) -> Result<(u32, u32, Vec), Box> { + let jpeg_data = remove_invalid_jpeg_data(jpeg_data); + + let mut decoder = jpeg_decoder::Decoder::new(&jpeg_data[..]); + decoder.read_info().unwrap(); + let metadata = decoder.info().unwrap(); + let decoded_data = decoder.decode().expect("failed to decode image"); + + // Decompress the alpha data (DEFLATE compression). + let alpha_data = { + let mut data = vec![]; + let mut decoder = libflate::zlib::Decoder::new(alpha_data)?; + decoder.read_to_end(&mut data)?; + data + }; + + let mut rgba = Vec::with_capacity((decoded_data.len() / 3) * 4); + let mut i = 0; + let mut a = 0; + while i < decoded_data.len() { + rgba.push(decoded_data[i]); + rgba.push(decoded_data[i + 1]); + rgba.push(decoded_data[i + 2]); + rgba.push(alpha_data[a]); + i += 3; + a += 1; + } + + Ok((metadata.width.into(), metadata.height.into(), rgba)) +} + +/// Decodes the bitmap data in DefineBitsLossless tag into RGBA. +/// DefineBitsLossless is Zlib encoded pixel data (similar to PNG), possibly +/// palletized. +pub fn define_bits_lossless_to_rgba( + swf_tag: &swf::DefineBitsLossless, +) -> Result, Box> { + // Decompress the image data (DEFLATE compression). + let mut decoded_data = { + let mut data = vec![]; + let mut decoder = libflate::zlib::Decoder::new(&swf_tag.data[..])?; + decoder.read_to_end(&mut data)?; + data + }; + + // Swizzle/de-palettize the bitmap. + let out_data = match (swf_tag.version, swf_tag.format) { + (1, swf::BitmapFormat::Rgb15) => unimplemented!("15-bit PNG"), + (1, swf::BitmapFormat::Rgb32) => { + let mut i = 0; + while i < decoded_data.len() { + decoded_data[i] = decoded_data[i + 1]; + decoded_data[i + 1] = decoded_data[i + 2]; + decoded_data[i + 2] = decoded_data[i + 3]; + decoded_data[i + 3] = 0xff; + i += 4; + } + decoded_data + } + (2, swf::BitmapFormat::Rgb32) => { + let mut i = 0; + while i < decoded_data.len() { + let alpha = decoded_data[i]; + decoded_data[i] = decoded_data[i + 1]; + decoded_data[i + 1] = decoded_data[i + 2]; + decoded_data[i + 2] = decoded_data[i + 3]; + decoded_data[i + 3] = alpha; + i += 4; + } + decoded_data + } + (1, swf::BitmapFormat::ColorMap8) => { + let mut i = 0; + let padded_width = (swf_tag.width + 0b11) & !0b11; + + let mut palette = Vec::with_capacity(swf_tag.num_colors as usize + 1); + for _ in 0..=swf_tag.num_colors { + palette.push(Color { + r: decoded_data[i], + g: decoded_data[i + 1], + b: decoded_data[i + 2], + a: 255, + }); + i += 3; + } + let mut out_data = vec![]; + for _ in 0..swf_tag.height { + for _ in 0..swf_tag.width { + let entry = decoded_data[i] as usize; + if entry < palette.len() { + let color = &palette[entry]; + out_data.push(color.r); + out_data.push(color.g); + out_data.push(color.b); + out_data.push(color.a); + } else { + out_data.push(0); + out_data.push(0); + out_data.push(0); + out_data.push(255); + } + i += 1; + } + i += (padded_width - swf_tag.width) as usize; + } + out_data + } + (2, swf::BitmapFormat::ColorMap8) => { + let mut i = 0; + let padded_width = (swf_tag.width + 0b11) & !0b11; + + let mut palette = Vec::with_capacity(swf_tag.num_colors as usize + 1); + for _ in 0..=swf_tag.num_colors { + palette.push(Color { + r: decoded_data[i], + g: decoded_data[i + 1], + b: decoded_data[i + 2], + a: decoded_data[i + 3], + }); + i += 4; + } + let mut out_data = vec![]; + for _ in 0..swf_tag.height { + for _ in 0..swf_tag.width { + let entry = decoded_data[i] as usize; + if entry < palette.len() { + let color = &palette[entry]; + out_data.push(color.r); + out_data.push(color.g); + out_data.push(color.b); + out_data.push(color.a); + } else { + out_data.push(0); + out_data.push(0); + out_data.push(0); + out_data.push(0); + } + i += 1; + } + i += (padded_width - swf_tag.width) as usize; + } + out_data + } + _ => unimplemented!("{:?} {:?}", swf_tag.version, swf_tag.format), + }; + + Ok(out_data) +} + +/// Images in SWFs are stored with premultiplied alpha. +/// Converts RGBA premultiplied alpha to standard RBGA. +pub fn unmultiply_alpha_rgba(rgba: &mut [u8]) { + rgba.chunks_exact_mut(4).for_each(|rgba| { + if rgba[3] > 0 { + let a = f32::from(rgba[3]) / 255.0; + rgba[0] = (f32::from(rgba[0]) / a) as u8; + rgba[1] = (f32::from(rgba[1]) / a) as u8; + rgba[2] = (f32::from(rgba[2]) / a) as u8; + } + }) +} diff --git a/core/src/bounding_box.rs b/core/src/bounding_box.rs index 8ed8e8eb2..6bc4fc078 100644 --- a/core/src/bounding_box.rs +++ b/core/src/bounding_box.rs @@ -1,74 +1,74 @@ -use crate::matrix::Matrix; -use swf::Twips; - -#[derive(Clone, Debug)] -pub struct BoundingBox { - pub x_min: Twips, - pub y_min: Twips, - pub x_max: Twips, - pub y_max: Twips, - pub valid: bool, -} - -impl BoundingBox { - pub fn transform(&self, matrix: &Matrix) -> Self { - if !self.valid { - return Self::default(); - } - - use std::cmp::{max, min}; - let pt0 = *matrix * (self.x_min, self.y_min); - let pt1 = *matrix * (self.x_min, self.y_max); - let pt2 = *matrix * (self.x_max, self.y_min); - let pt3 = *matrix * (self.x_max, self.y_max); - BoundingBox { - x_min: min(pt0.0, min(pt1.0, min(pt2.0, pt3.0))), - y_min: min(pt0.1, min(pt1.1, min(pt2.1, pt3.1))), - x_max: max(pt0.0, max(pt1.0, max(pt2.0, pt3.0))), - y_max: max(pt0.1, max(pt1.1, max(pt2.1, pt3.1))), - valid: true, - } - } - - pub fn intersects(&self, other: &BoundingBox) -> bool { - if !self.valid || !other.valid { - return false; - } - - use std::cmp::{max, min}; - let x_min = max(self.x_min, other.x_min); - let y_min = max(self.y_min, other.y_min); - let x_max = min(self.x_max, other.x_max); - let y_max = min(self.y_max, other.y_max); - - x_min <= x_max && y_min <= y_max - } - - pub fn contains(&self, (x, y): (Twips, Twips)) -> bool { - self.valid && x >= self.x_min && x <= self.x_max && y >= self.y_min && y <= self.y_max - } -} - -impl Default for BoundingBox { - fn default() -> Self { - Self { - x_min: Default::default(), - y_min: Default::default(), - x_max: Default::default(), - y_max: Default::default(), - valid: false, - } - } -} - -impl From for BoundingBox { - fn from(rect: swf::Rectangle) -> Self { - Self { - x_min: rect.x_min, - y_min: rect.y_min, - x_max: rect.x_max, - y_max: rect.y_max, - valid: true, - } - } -} +use crate::matrix::Matrix; +use swf::Twips; + +#[derive(Clone, Debug)] +pub struct BoundingBox { + pub x_min: Twips, + pub y_min: Twips, + pub x_max: Twips, + pub y_max: Twips, + pub valid: bool, +} + +impl BoundingBox { + pub fn transform(&self, matrix: &Matrix) -> Self { + if !self.valid { + return Self::default(); + } + + use std::cmp::{max, min}; + let pt0 = *matrix * (self.x_min, self.y_min); + let pt1 = *matrix * (self.x_min, self.y_max); + let pt2 = *matrix * (self.x_max, self.y_min); + let pt3 = *matrix * (self.x_max, self.y_max); + BoundingBox { + x_min: min(pt0.0, min(pt1.0, min(pt2.0, pt3.0))), + y_min: min(pt0.1, min(pt1.1, min(pt2.1, pt3.1))), + x_max: max(pt0.0, max(pt1.0, max(pt2.0, pt3.0))), + y_max: max(pt0.1, max(pt1.1, max(pt2.1, pt3.1))), + valid: true, + } + } + + pub fn intersects(&self, other: &BoundingBox) -> bool { + if !self.valid || !other.valid { + return false; + } + + use std::cmp::{max, min}; + let x_min = max(self.x_min, other.x_min); + let y_min = max(self.y_min, other.y_min); + let x_max = min(self.x_max, other.x_max); + let y_max = min(self.y_max, other.y_max); + + x_min <= x_max && y_min <= y_max + } + + pub fn contains(&self, (x, y): (Twips, Twips)) -> bool { + self.valid && x >= self.x_min && x <= self.x_max && y >= self.y_min && y <= self.y_max + } +} + +impl Default for BoundingBox { + fn default() -> Self { + Self { + x_min: Default::default(), + y_min: Default::default(), + x_max: Default::default(), + y_max: Default::default(), + valid: false, + } + } +} + +impl From for BoundingBox { + fn from(rect: swf::Rectangle) -> Self { + Self { + x_min: rect.x_min, + y_min: rect.y_min, + x_max: rect.x_max, + y_max: rect.y_max, + valid: true, + } + } +} diff --git a/core/src/button.rs b/core/src/button.rs index e1c26f694..8d8008816 100644 --- a/core/src/button.rs +++ b/core/src/button.rs @@ -1,301 +1,301 @@ -use crate::display_object::{DisplayObject, DisplayObjectBase}; -use crate::events::ButtonEvent; -use crate::player::{RenderContext, UpdateContext}; -use crate::prelude::*; -use std::collections::BTreeMap; - -#[derive(Clone)] -pub struct Button<'gc> { - base: DisplayObjectBase<'gc>, - static_data: gc_arena::Gc<'gc, ButtonStatic>, - state: ButtonState, - children: [BTreeMap>; 4], - tracking: ButtonTracking, -} - -const UP_STATE: usize = 0; -const OVER_STATE: usize = 1; -const DOWN_STATE: usize = 2; -const HIT_STATE: usize = 3; - -impl<'gc> Button<'gc> { - pub fn from_swf_tag( - button: &swf::Button, - library: &crate::library::Library<'gc>, - gc_context: gc_arena::MutationContext<'gc, '_>, - ) -> Self { - use swf::ButtonState; - let mut children = [ - BTreeMap::new(), - BTreeMap::new(), - BTreeMap::new(), - BTreeMap::new(), - ]; - for record in &button.records { - match library.instantiate_display_object(record.id, gc_context) { - Ok(child) => { - child - .write(gc_context) - .set_matrix(&record.matrix.clone().into()); - child - .write(gc_context) - .set_color_transform(&record.color_transform.clone().into()); - for state in &record.states { - let i = match state { - ButtonState::Up => UP_STATE, - ButtonState::Over => OVER_STATE, - ButtonState::Down => DOWN_STATE, - ButtonState::HitTest => HIT_STATE, - }; - children[i].insert(record.depth, child); - } - } - Err(error) => { - log::error!( - "Button ID {}: could not instantiate child ID {}: {}", - button.id, - record.id, - error - ); - } - } - } - - let mut actions = vec![]; - for action in &button.actions { - let action_data = crate::tag_utils::SwfSlice { - data: std::sync::Arc::new(action.action_data.clone()), - start: 0, - end: action.action_data.len(), - }; - for condition in &action.conditions { - let button_action = ButtonAction { - action_data: action_data.clone(), - condition: *condition, - key_code: action.key_code, - }; - actions.push(button_action); - } - } - - let static_data = ButtonStatic { - id: button.id, - actions, - }; - - Button { - base: Default::default(), - static_data: gc_arena::Gc::allocate(gc_context, static_data), - children, - state: self::ButtonState::Up, - tracking: if button.is_track_as_menu { - ButtonTracking::Menu - } else { - ButtonTracking::Push - }, - } - } - - fn children_in_state( - &self, - state: ButtonState, - ) -> impl std::iter::DoubleEndedIterator> { - let i = match state { - ButtonState::Up => UP_STATE, - ButtonState::Over => OVER_STATE, - ButtonState::Down => DOWN_STATE, - ButtonState::Hit => HIT_STATE, - }; - self.children[i].values() - } - - fn children_in_state_mut( - &mut self, - state: ButtonState, - ) -> impl std::iter::DoubleEndedIterator> { - let i = match state { - ButtonState::Up => UP_STATE, - ButtonState::Over => OVER_STATE, - ButtonState::Down => DOWN_STATE, - ButtonState::Hit => HIT_STATE, - }; - self.children[i].values_mut() - } - - pub fn handle_button_event( - &mut self, - context: &mut crate::player::UpdateContext<'_, 'gc, '_>, - event: ButtonEvent, - ) { - let new_state = match event { - ButtonEvent::RollOut => ButtonState::Up, - ButtonEvent::RollOver => ButtonState::Over, - ButtonEvent::Press => ButtonState::Down, - ButtonEvent::Release => ButtonState::Over, - ButtonEvent::KeyPress(key) => { - self.run_actions(context, swf::ButtonActionCondition::KeyPress, Some(key)); - self.state - } - }; - - match (self.state, new_state) { - (ButtonState::Up, ButtonState::Over) => { - self.run_actions(context, swf::ButtonActionCondition::IdleToOverUp, None); - } - (ButtonState::Over, ButtonState::Up) => { - self.run_actions(context, swf::ButtonActionCondition::OverUpToIdle, None); - } - (ButtonState::Over, ButtonState::Down) => { - self.run_actions(context, swf::ButtonActionCondition::OverUpToOverDown, None); - } - (ButtonState::Down, ButtonState::Over) => { - self.run_actions(context, swf::ButtonActionCondition::OverDownToOverUp, None); - } - _ => (), - } - - self.state = new_state; - } - - fn run_actions( - &mut self, - context: &mut UpdateContext<'_, 'gc, '_>, - condition: swf::ButtonActionCondition, - key_code: Option, - ) { - if let Some(parent) = self.parent() { - for action in &self.static_data.actions { - if action.condition == condition && action.key_code == key_code { - // Note that AVM1 buttons run actions relative to their parent, not themselves. - context.actions.push((parent, action.action_data.clone())); - } - } - } - } -} - -impl<'gc> DisplayObject<'gc> for Button<'gc> { - impl_display_object!(base); - - fn run_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - // TODO: Set parent for all children. Yuck... Do this on creation instead. - for state in &mut self.children { - for child in state.values_mut() { - child - .write(context.gc_context) - .set_parent(Some(context.active_clip)); - } - } - - for child in self.children_in_state_mut(self.state) { - child - .write(context.gc_context) - .set_parent(Some(context.active_clip)); - context.active_clip = *child; - child.write(context.gc_context).run_frame(context); - } - } - - fn run_post_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - for child in self.children_in_state_mut(self.state) { - context.active_clip = *child; - child.write(context.gc_context).run_post_frame(context); - } - } - - fn render(&self, context: &mut RenderContext<'_, 'gc>) { - context.transform_stack.push(self.transform()); - - for child in self.children_in_state(self.state) { - child.read().render(context); - } - context.transform_stack.pop(); - } - - fn hit_test(&self, point: (Twips, Twips)) -> bool { - // Use hit state to determine hit area; otherwise use current state. - let hit_state = if !self.children[HIT_STATE].is_empty() { - ButtonState::Hit - } else { - self.state - }; - for child in self.children_in_state(hit_state).rev() { - if child.read().world_bounds().contains(point) { - return true; - } - } - - false - } - - fn mouse_pick( - &self, - self_node: DisplayNode<'gc>, - point: (Twips, Twips), - ) -> Option> { - // The button is hovered if the mouse is over any child nodes. - if self.hit_test(point) { - Some(self_node) - } else { - None - } - } - - fn as_button(&self) -> Option<&Self> { - Some(self) - } - - fn as_button_mut(&mut self) -> Option<&mut Self> { - Some(self) - } -} - -unsafe impl<'gc> gc_arena::Collect for Button<'gc> { - #[inline] - fn trace(&self, cc: gc_arena::CollectionContext) { - for state in &self.children { - for child in state.values() { - child.trace(cc); - } - } - self.base.trace(cc); - self.static_data.trace(cc); - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[allow(dead_code)] -enum ButtonState { - Up, - Over, - Down, - Hit, -} - -#[derive(Clone)] -struct ButtonAction { - action_data: crate::tag_utils::SwfSlice, - condition: swf::ButtonActionCondition, - key_code: Option, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum ButtonTracking { - Push, - Menu, -} - -/// Static data shared between all instances of a button. -#[allow(dead_code)] -#[derive(Clone)] -struct ButtonStatic { - id: CharacterId, - actions: Vec, -} - -unsafe impl<'gc> gc_arena::Collect for ButtonStatic { - #[inline] - fn needs_trace() -> bool { - false - } -} +use crate::display_object::{DisplayObject, DisplayObjectBase}; +use crate::events::ButtonEvent; +use crate::player::{RenderContext, UpdateContext}; +use crate::prelude::*; +use std::collections::BTreeMap; + +#[derive(Clone)] +pub struct Button<'gc> { + base: DisplayObjectBase<'gc>, + static_data: gc_arena::Gc<'gc, ButtonStatic>, + state: ButtonState, + children: [BTreeMap>; 4], + tracking: ButtonTracking, +} + +const UP_STATE: usize = 0; +const OVER_STATE: usize = 1; +const DOWN_STATE: usize = 2; +const HIT_STATE: usize = 3; + +impl<'gc> Button<'gc> { + pub fn from_swf_tag( + button: &swf::Button, + library: &crate::library::Library<'gc>, + gc_context: gc_arena::MutationContext<'gc, '_>, + ) -> Self { + use swf::ButtonState; + let mut children = [ + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + BTreeMap::new(), + ]; + for record in &button.records { + match library.instantiate_display_object(record.id, gc_context) { + Ok(child) => { + child + .write(gc_context) + .set_matrix(&record.matrix.clone().into()); + child + .write(gc_context) + .set_color_transform(&record.color_transform.clone().into()); + for state in &record.states { + let i = match state { + ButtonState::Up => UP_STATE, + ButtonState::Over => OVER_STATE, + ButtonState::Down => DOWN_STATE, + ButtonState::HitTest => HIT_STATE, + }; + children[i].insert(record.depth, child); + } + } + Err(error) => { + log::error!( + "Button ID {}: could not instantiate child ID {}: {}", + button.id, + record.id, + error + ); + } + } + } + + let mut actions = vec![]; + for action in &button.actions { + let action_data = crate::tag_utils::SwfSlice { + data: std::sync::Arc::new(action.action_data.clone()), + start: 0, + end: action.action_data.len(), + }; + for condition in &action.conditions { + let button_action = ButtonAction { + action_data: action_data.clone(), + condition: *condition, + key_code: action.key_code, + }; + actions.push(button_action); + } + } + + let static_data = ButtonStatic { + id: button.id, + actions, + }; + + Button { + base: Default::default(), + static_data: gc_arena::Gc::allocate(gc_context, static_data), + children, + state: self::ButtonState::Up, + tracking: if button.is_track_as_menu { + ButtonTracking::Menu + } else { + ButtonTracking::Push + }, + } + } + + fn children_in_state( + &self, + state: ButtonState, + ) -> impl std::iter::DoubleEndedIterator> { + let i = match state { + ButtonState::Up => UP_STATE, + ButtonState::Over => OVER_STATE, + ButtonState::Down => DOWN_STATE, + ButtonState::Hit => HIT_STATE, + }; + self.children[i].values() + } + + fn children_in_state_mut( + &mut self, + state: ButtonState, + ) -> impl std::iter::DoubleEndedIterator> { + let i = match state { + ButtonState::Up => UP_STATE, + ButtonState::Over => OVER_STATE, + ButtonState::Down => DOWN_STATE, + ButtonState::Hit => HIT_STATE, + }; + self.children[i].values_mut() + } + + pub fn handle_button_event( + &mut self, + context: &mut crate::player::UpdateContext<'_, 'gc, '_>, + event: ButtonEvent, + ) { + let new_state = match event { + ButtonEvent::RollOut => ButtonState::Up, + ButtonEvent::RollOver => ButtonState::Over, + ButtonEvent::Press => ButtonState::Down, + ButtonEvent::Release => ButtonState::Over, + ButtonEvent::KeyPress(key) => { + self.run_actions(context, swf::ButtonActionCondition::KeyPress, Some(key)); + self.state + } + }; + + match (self.state, new_state) { + (ButtonState::Up, ButtonState::Over) => { + self.run_actions(context, swf::ButtonActionCondition::IdleToOverUp, None); + } + (ButtonState::Over, ButtonState::Up) => { + self.run_actions(context, swf::ButtonActionCondition::OverUpToIdle, None); + } + (ButtonState::Over, ButtonState::Down) => { + self.run_actions(context, swf::ButtonActionCondition::OverUpToOverDown, None); + } + (ButtonState::Down, ButtonState::Over) => { + self.run_actions(context, swf::ButtonActionCondition::OverDownToOverUp, None); + } + _ => (), + } + + self.state = new_state; + } + + fn run_actions( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + condition: swf::ButtonActionCondition, + key_code: Option, + ) { + if let Some(parent) = self.parent() { + for action in &self.static_data.actions { + if action.condition == condition && action.key_code == key_code { + // Note that AVM1 buttons run actions relative to their parent, not themselves. + context.actions.push((parent, action.action_data.clone())); + } + } + } + } +} + +impl<'gc> DisplayObject<'gc> for Button<'gc> { + impl_display_object!(base); + + fn run_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { + // TODO: Set parent for all children. Yuck... Do this on creation instead. + for state in &mut self.children { + for child in state.values_mut() { + child + .write(context.gc_context) + .set_parent(Some(context.active_clip)); + } + } + + for child in self.children_in_state_mut(self.state) { + child + .write(context.gc_context) + .set_parent(Some(context.active_clip)); + context.active_clip = *child; + child.write(context.gc_context).run_frame(context); + } + } + + fn run_post_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { + for child in self.children_in_state_mut(self.state) { + context.active_clip = *child; + child.write(context.gc_context).run_post_frame(context); + } + } + + fn render(&self, context: &mut RenderContext<'_, 'gc>) { + context.transform_stack.push(self.transform()); + + for child in self.children_in_state(self.state) { + child.read().render(context); + } + context.transform_stack.pop(); + } + + fn hit_test(&self, point: (Twips, Twips)) -> bool { + // Use hit state to determine hit area; otherwise use current state. + let hit_state = if !self.children[HIT_STATE].is_empty() { + ButtonState::Hit + } else { + self.state + }; + for child in self.children_in_state(hit_state).rev() { + if child.read().world_bounds().contains(point) { + return true; + } + } + + false + } + + fn mouse_pick( + &self, + self_node: DisplayNode<'gc>, + point: (Twips, Twips), + ) -> Option> { + // The button is hovered if the mouse is over any child nodes. + if self.hit_test(point) { + Some(self_node) + } else { + None + } + } + + fn as_button(&self) -> Option<&Self> { + Some(self) + } + + fn as_button_mut(&mut self) -> Option<&mut Self> { + Some(self) + } +} + +unsafe impl<'gc> gc_arena::Collect for Button<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + for state in &self.children { + for child in state.values() { + child.trace(cc); + } + } + self.base.trace(cc); + self.static_data.trace(cc); + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[allow(dead_code)] +enum ButtonState { + Up, + Over, + Down, + Hit, +} + +#[derive(Clone)] +struct ButtonAction { + action_data: crate::tag_utils::SwfSlice, + condition: swf::ButtonActionCondition, + key_code: Option, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ButtonTracking { + Push, + Menu, +} + +/// Static data shared between all instances of a button. +#[allow(dead_code)] +#[derive(Clone)] +struct ButtonStatic { + id: CharacterId, + actions: Vec, +} + +unsafe impl<'gc> gc_arena::Collect for ButtonStatic { + #[inline] + fn needs_trace() -> bool { + false + } +} diff --git a/core/src/color_transform.rs b/core/src/color_transform.rs index a3015ee6e..f7a61b2e1 100644 --- a/core/src/color_transform.rs +++ b/core/src/color_transform.rs @@ -1,88 +1,88 @@ -#[derive(Copy, Clone, Debug)] -pub struct ColorTransform { - pub r_mult: f32, - pub g_mult: f32, - pub b_mult: f32, - pub a_mult: f32, - pub r_add: f32, - pub g_add: f32, - pub b_add: f32, - pub a_add: f32, -} - -impl From for ColorTransform { - fn from(color_transform: swf::ColorTransform) -> ColorTransform { - ColorTransform { - r_mult: color_transform.r_multiply, - g_mult: color_transform.g_multiply, - b_mult: color_transform.b_multiply, - a_mult: color_transform.a_multiply, - r_add: f32::from(color_transform.r_add) / 255.0, - g_add: f32::from(color_transform.g_add) / 255.0, - b_add: f32::from(color_transform.b_add) / 255.0, - a_add: f32::from(color_transform.a_add) / 255.0, - } - } -} - -impl ColorTransform { - #[allow(clippy::float_cmp)] - pub fn is_identity(&self) -> bool { - self.r_mult == 1.0 - && self.g_mult == 1.0 - && self.b_mult == 1.0 - && self.a_mult == 1.0 - && self.r_add == 0.0 - && self.g_add == 0.0 - && self.b_add == 0.0 - && self.a_add == 0.0 - } -} - -impl std::default::Default for ColorTransform { - fn default() -> ColorTransform { - ColorTransform { - r_mult: 1.0, - b_mult: 1.0, - g_mult: 1.0, - a_mult: 1.0, - r_add: 0.0, - b_add: 0.0, - g_add: 0.0, - a_add: 0.0, - } - } -} - -impl std::ops::Mul for ColorTransform { - type Output = Self; - fn mul(self, rhs: Self) -> Self { - ColorTransform { - r_mult: self.r_mult * rhs.r_mult, - g_mult: self.g_mult * rhs.g_mult, - b_mult: self.b_mult * rhs.b_mult, - a_mult: self.a_mult * rhs.a_mult, - - r_add: self.r_mult * rhs.r_add + self.r_add, - g_add: self.g_mult * rhs.g_add + self.g_add, - b_add: self.b_mult * rhs.b_add + self.b_add, - a_add: self.a_mult * rhs.a_add + self.a_add, - } - } -} - -impl std::ops::MulAssign for ColorTransform { - fn mul_assign(&mut self, rhs: Self) { - *self = ColorTransform { - r_mult: self.r_mult * rhs.r_mult, - g_mult: self.g_mult * rhs.g_mult, - b_mult: self.b_mult * rhs.b_mult, - a_mult: self.a_mult * rhs.a_mult, - - r_add: self.r_mult * rhs.r_add + self.r_add, - g_add: self.g_mult * rhs.b_add + self.g_add, - b_add: self.b_mult * rhs.g_add + self.b_add, - a_add: self.a_mult * rhs.a_add + self.a_add, - } - } -} +#[derive(Copy, Clone, Debug)] +pub struct ColorTransform { + pub r_mult: f32, + pub g_mult: f32, + pub b_mult: f32, + pub a_mult: f32, + pub r_add: f32, + pub g_add: f32, + pub b_add: f32, + pub a_add: f32, +} + +impl From for ColorTransform { + fn from(color_transform: swf::ColorTransform) -> ColorTransform { + ColorTransform { + r_mult: color_transform.r_multiply, + g_mult: color_transform.g_multiply, + b_mult: color_transform.b_multiply, + a_mult: color_transform.a_multiply, + r_add: f32::from(color_transform.r_add) / 255.0, + g_add: f32::from(color_transform.g_add) / 255.0, + b_add: f32::from(color_transform.b_add) / 255.0, + a_add: f32::from(color_transform.a_add) / 255.0, + } + } +} + +impl ColorTransform { + #[allow(clippy::float_cmp)] + pub fn is_identity(&self) -> bool { + self.r_mult == 1.0 + && self.g_mult == 1.0 + && self.b_mult == 1.0 + && self.a_mult == 1.0 + && self.r_add == 0.0 + && self.g_add == 0.0 + && self.b_add == 0.0 + && self.a_add == 0.0 + } +} + +impl std::default::Default for ColorTransform { + fn default() -> ColorTransform { + ColorTransform { + r_mult: 1.0, + b_mult: 1.0, + g_mult: 1.0, + a_mult: 1.0, + r_add: 0.0, + b_add: 0.0, + g_add: 0.0, + a_add: 0.0, + } + } +} + +impl std::ops::Mul for ColorTransform { + type Output = Self; + fn mul(self, rhs: Self) -> Self { + ColorTransform { + r_mult: self.r_mult * rhs.r_mult, + g_mult: self.g_mult * rhs.g_mult, + b_mult: self.b_mult * rhs.b_mult, + a_mult: self.a_mult * rhs.a_mult, + + r_add: self.r_mult * rhs.r_add + self.r_add, + g_add: self.g_mult * rhs.g_add + self.g_add, + b_add: self.b_mult * rhs.b_add + self.b_add, + a_add: self.a_mult * rhs.a_add + self.a_add, + } + } +} + +impl std::ops::MulAssign for ColorTransform { + fn mul_assign(&mut self, rhs: Self) { + *self = ColorTransform { + r_mult: self.r_mult * rhs.r_mult, + g_mult: self.g_mult * rhs.g_mult, + b_mult: self.b_mult * rhs.b_mult, + a_mult: self.a_mult * rhs.a_mult, + + r_add: self.r_mult * rhs.r_add + self.r_add, + g_add: self.g_mult * rhs.b_add + self.g_add, + b_add: self.b_mult * rhs.g_add + self.b_add, + a_add: self.a_mult * rhs.a_add + self.a_add, + } + } +} diff --git a/core/src/events.rs b/core/src/events.rs index dc03764ae..fd1a9886a 100644 --- a/core/src/events.rs +++ b/core/src/events.rs @@ -1,29 +1,29 @@ -#[allow(clippy::enum_variant_names)] -#[derive(Debug)] -pub enum PlayerEvent { - MouseMove { x: f64, y: f64 }, - MouseUp { x: f64, y: f64 }, - MouseDown { x: f64, y: f64 }, - MouseLeft, -} - -/// The events that an AVM1 button can fire. -/// -/// In Flash, these are created using `on` code on the button instance: -/// ```ignore -/// on(release) { -/// trace("Button clicked"); -/// } -/// ``` -#[derive(Debug)] -pub enum ButtonEvent { - Press, - Release, - RollOut, - RollOver, - KeyPress(KeyCode), -} - -/// Flash virtual keycode. -/// TODO: This will eventually move to a separate module. -pub type KeyCode = u8; +#[allow(clippy::enum_variant_names)] +#[derive(Debug)] +pub enum PlayerEvent { + MouseMove { x: f64, y: f64 }, + MouseUp { x: f64, y: f64 }, + MouseDown { x: f64, y: f64 }, + MouseLeft, +} + +/// The events that an AVM1 button can fire. +/// +/// In Flash, these are created using `on` code on the button instance: +/// ```ignore +/// on(release) { +/// trace("Button clicked"); +/// } +/// ``` +#[derive(Debug)] +pub enum ButtonEvent { + Press, + Release, + RollOut, + RollOver, + KeyPress(KeyCode), +} + +/// Flash virtual keycode. +/// TODO: This will eventually move to a separate module. +pub type KeyCode = u8; diff --git a/core/src/morph_shape.rs b/core/src/morph_shape.rs index 07e0a2725..d91c8c210 100644 --- a/core/src/morph_shape.rs +++ b/core/src/morph_shape.rs @@ -1,411 +1,411 @@ -use crate::backend::render::{RenderBackend, ShapeHandle}; -use crate::display_object::{DisplayObject, DisplayObjectBase}; -use crate::player::{RenderContext, UpdateContext}; -use crate::prelude::*; -use std::collections::HashMap; -use swf::Twips; - -#[derive(Clone)] -pub struct MorphShape<'gc> { - base: DisplayObjectBase<'gc>, - - start: swf::MorphShape, - - end: swf::MorphShape, - - frames: HashMap, - - ratio: u16, -} - -impl<'gc> MorphShape<'gc> { - pub fn from_swf_tag(swf_tag: &swf::DefineMorphShape, renderer: &mut dyn RenderBackend) -> Self { - // Convert the MorphShape into a normal Shape. - // TODO(Herschel): impl From in swf crate? - let mut morph_shape = Self { - start: swf_tag.start.clone(), - end: swf_tag.end.clone(), - base: Default::default(), - frames: HashMap::new(), - ratio: 0, - }; - - morph_shape.register_ratio(renderer, 0); - morph_shape.register_ratio(renderer, 65535); - - morph_shape - } - - pub fn register_ratio(&mut self, renderer: &mut dyn RenderBackend, ratio: u16) { - if self.frames.contains_key(&ratio) { - // Already registered. - return; - } - - // Interpolate MorphShapes into a Shape. - use swf::{FillStyle, Gradient, LineStyle, ShapeRecord, ShapeStyles}; - // Start shape is ratio 65535, end shape is ratio 0. - let b = f32::from(ratio) / 65535.0; - let a = 1.0 - b; - let fill_styles: Vec = self - .start - .fill_styles - .iter() - .zip(self.end.fill_styles.iter()) - .map(|(start, end)| match (start, end) { - (FillStyle::Color(start), FillStyle::Color(end)) => FillStyle::Color(Color { - r: (a * f32::from(start.r) + b * f32::from(end.r)) as u8, - g: (a * f32::from(start.g) + b * f32::from(end.g)) as u8, - b: (a * f32::from(start.b) + b * f32::from(end.b)) as u8, - a: (a * f32::from(start.a) + b * f32::from(end.a)) as u8, - }), - (FillStyle::LinearGradient(start), FillStyle::LinearGradient(end)) => { - let records: Vec = start - .records - .iter() - .zip(end.records.iter()) - .map(|(start, end)| swf::GradientRecord { - ratio: (f32::from(start.ratio) * a + f32::from(end.ratio) * b) as u8, - color: Color { - r: (a * f32::from(start.color.r) + b * f32::from(end.color.r)) - as u8, - g: (a * f32::from(start.color.g) + b * f32::from(end.color.g)) - as u8, - b: (a * f32::from(start.color.b) + b * f32::from(end.color.b)) - as u8, - a: (a * f32::from(start.color.a) + b * f32::from(end.color.a)) - as u8, - }, - }) - .collect(); - - FillStyle::LinearGradient(Gradient { - matrix: start.matrix.clone(), - spread: start.spread, - interpolation: start.interpolation, - records, - }) - } - _ => { - log::info!("Unhandled morph shape combination: {:?} {:?}", start, end); - start.clone() - } - }) - .collect(); - let line_styles: Vec = self - .start - .line_styles - .iter() - .zip(self.end.line_styles.iter()) - .map(|(start, end)| LineStyle { - width: Twips::new( - ((start.width.get() as f32) * a + (end.width.get() as f32) * b) as i32, - ), - color: Color { - r: (a * f32::from(start.color.r) + b * f32::from(end.color.r)) as u8, - g: (a * f32::from(start.color.g) + b * f32::from(end.color.g)) as u8, - b: (a * f32::from(start.color.b) + b * f32::from(end.color.b)) as u8, - a: (a * f32::from(start.color.a) + b * f32::from(end.color.a)) as u8, - }, - start_cap: start.start_cap, - end_cap: start.end_cap, - join_style: start.join_style, - fill_style: None, - allow_scale_x: start.allow_scale_x, - allow_scale_y: start.allow_scale_y, - is_pixel_hinted: start.is_pixel_hinted, - allow_close: start.allow_close, - }) - .collect(); - - let mut shape = Vec::with_capacity(self.start.shape.len()); - let mut start_iter = self.start.shape.iter(); - let mut end_iter = self.end.shape.iter(); - let mut start = start_iter.next(); - let mut end = end_iter.next(); - let mut start_x = Twips::new(0); - let mut start_y = Twips::new(0); - let mut end_x = Twips::new(0); - let mut end_y = Twips::new(0); - // TODO: Feels like this could be cleaned up a bit. - // We step through both the start records and end records, interpolating edges pairwise. - // Fill style/line style changes should only appear in the start records. - // However, StyleChangeRecord move_to can appear it both start and end records, - // and not necessarily in matching pairs; therefore, we have to keep track of the pen position - // in case one side is missing a move_to; it will implicitly use the last pen position. - while let (Some(s), Some(e)) = (start, end) { - match (s, e) { - (ShapeRecord::StyleChange(start_change), ShapeRecord::StyleChange(end_change)) => { - let mut style_change = start_change.clone(); - if let Some((s_x, s_y)) = start_change.move_to { - if let Some((e_x, e_y)) = end_change.move_to { - start_x = s_x; - start_y = s_y; - end_x = e_x; - end_y = e_y; - style_change.move_to = Some(( - Twips::new( - (start_x.get() as f32 * a + end_x.get() as f32 * b) as i32, - ), - Twips::new( - (start_y.get() as f32 * a + end_y.get() as f32 * b) as i32, - ), - )); - } else { - panic!("Expected move_to for morph shape") - } - } - shape.push(ShapeRecord::StyleChange(style_change)); - start = start_iter.next(); - end = end_iter.next(); - } - (ShapeRecord::StyleChange(start_change), _) => { - let mut style_change = start_change.clone(); - if let Some((s_x, s_y)) = start_change.move_to { - start_x = s_x; - start_y = s_y; - style_change.move_to = Some(( - Twips::new((start_x.get() as f32 * a + end_x.get() as f32 * b) as i32), - Twips::new((start_y.get() as f32 * a + end_y.get() as f32 * b) as i32), - )); - } - shape.push(ShapeRecord::StyleChange(style_change)); - Self::update_pos(&mut start_x, &mut start_y, s); - start = start_iter.next(); - } - (_, ShapeRecord::StyleChange(end_change)) => { - let mut style_change = end_change.clone(); - if let Some((e_x, e_y)) = end_change.move_to { - end_x = e_x; - end_y = e_y; - style_change.move_to = Some(( - Twips::new((start_x.get() as f32 * a + end_x.get() as f32 * b) as i32), - Twips::new((start_y.get() as f32 * a + end_y.get() as f32 * b) as i32), - )); - } - shape.push(ShapeRecord::StyleChange(style_change)); - Self::update_pos(&mut end_x, &mut end_y, s); - end = end_iter.next(); - continue; - } - _ => { - shape.push(Self::interpolate_edges(s, e, a)); - Self::update_pos(&mut start_x, &mut start_y, s); - Self::update_pos(&mut end_x, &mut end_y, e); - start = start_iter.next(); - end = end_iter.next(); - } - } - } - - let styles = ShapeStyles { - fill_styles, - line_styles, - }; - - let bounds = crate::shape_utils::calculate_shape_bounds(&shape[..]); - let shape = swf::Shape { - version: 4, - id: 0, - shape_bounds: bounds.clone(), - edge_bounds: bounds, - has_fill_winding_rule: false, - has_non_scaling_strokes: false, - has_scaling_strokes: true, - styles, - shape, - }; - - let shape_handle = renderer.register_shape(&shape); - self.frames.insert(ratio, shape_handle); - } - - fn update_pos(x: &mut Twips, y: &mut Twips, record: &swf::ShapeRecord) { - use swf::ShapeRecord; - match record { - ShapeRecord::StraightEdge { delta_x, delta_y } => { - *x += *delta_x; - *y += *delta_y; - } - ShapeRecord::CurvedEdge { - control_delta_x, - control_delta_y, - anchor_delta_x, - anchor_delta_y, - } => { - *x += *control_delta_x + *anchor_delta_x; - *y += *control_delta_y + *anchor_delta_y; - } - ShapeRecord::StyleChange(ref style_change) => { - if let Some((move_x, move_y)) = style_change.move_to { - *x = move_x; - *y = move_y; - } - } - } - } - - fn interpolate_edges( - start: &swf::ShapeRecord, - end: &swf::ShapeRecord, - a: f32, - ) -> swf::ShapeRecord { - use swf::ShapeRecord; - let b = 1.0 - a; - match (start, end) { - ( - ShapeRecord::StraightEdge { - delta_x: start_dx, - delta_y: start_dy, - }, - ShapeRecord::StraightEdge { - delta_x: end_dx, - delta_y: end_dy, - }, - ) => ShapeRecord::StraightEdge { - delta_x: Twips::new((start_dx.get() as f32 * a + end_dx.get() as f32 * b) as i32), - delta_y: Twips::new((start_dy.get() as f32 * a + end_dy.get() as f32 * b) as i32), - }, - - ( - ShapeRecord::CurvedEdge { - control_delta_x: start_cdx, - control_delta_y: start_cdy, - anchor_delta_x: start_adx, - anchor_delta_y: start_ady, - }, - ShapeRecord::CurvedEdge { - control_delta_x: end_cdx, - control_delta_y: end_cdy, - anchor_delta_x: end_adx, - anchor_delta_y: end_ady, - }, - ) => ShapeRecord::CurvedEdge { - control_delta_x: Twips::new( - (start_cdx.get() as f32 * a + end_cdx.get() as f32 * b) as i32, - ), - control_delta_y: Twips::new( - (start_cdy.get() as f32 * a + end_cdy.get() as f32 * b) as i32, - ), - anchor_delta_x: Twips::new( - (start_adx.get() as f32 * a + end_adx.get() as f32 * b) as i32, - ), - anchor_delta_y: Twips::new( - (start_ady.get() as f32 * a + end_ady.get() as f32 * b) as i32, - ), - }, - - ( - ShapeRecord::StraightEdge { - delta_x: start_dx, - delta_y: start_dy, - }, - ShapeRecord::CurvedEdge { - control_delta_x: end_cdx, - control_delta_y: end_cdy, - anchor_delta_x: end_adx, - anchor_delta_y: end_ady, - }, - ) => { - let start_cdx = *start_dx / 2; - let start_cdy = *start_dy / 2; - let start_adx = start_cdx; - let start_ady = start_cdy; - ShapeRecord::CurvedEdge { - control_delta_x: Twips::new( - (start_cdx.get() as f32 * a + end_cdx.get() as f32 * b) as i32, - ), - control_delta_y: Twips::new( - (start_cdy.get() as f32 * a + end_cdy.get() as f32 * b) as i32, - ), - anchor_delta_x: Twips::new( - (start_adx.get() as f32 * a + end_adx.get() as f32 * b) as i32, - ), - anchor_delta_y: Twips::new( - (start_ady.get() as f32 * a + end_ady.get() as f32 * b) as i32, - ), - } - } - - ( - ShapeRecord::CurvedEdge { - control_delta_x: start_cdx, - control_delta_y: start_cdy, - anchor_delta_x: start_adx, - anchor_delta_y: start_ady, - }, - ShapeRecord::StraightEdge { - delta_x: end_dx, - delta_y: end_dy, - }, - ) => { - let end_cdx = *end_dx / 2; - let end_cdy = *end_dy / 2; - let end_adx = end_cdx; - let end_ady = end_cdy; - ShapeRecord::CurvedEdge { - control_delta_x: Twips::new( - (start_cdx.get() as f32 * a + end_cdx.get() as f32 * b) as i32, - ), - control_delta_y: Twips::new( - (start_cdy.get() as f32 * a + end_cdy.get() as f32 * b) as i32, - ), - anchor_delta_x: Twips::new( - (start_adx.get() as f32 * a + end_adx.get() as f32 * b) as i32, - ), - anchor_delta_y: Twips::new( - (start_ady.get() as f32 * a + end_ady.get() as f32 * b) as i32, - ), - } - } - _ => unreachable!("{:?} {:?}", start, end), - } - } - - pub fn ratio(&self) -> u16 { - self.ratio - } - - pub fn set_ratio(&mut self, ratio: u16) { - self.ratio = ratio; - } -} - -impl<'gc> DisplayObject<'gc> for MorphShape<'gc> { - impl_display_object!(base); - - fn as_morph_shape(&self) -> Option<&Self> { - Some(self) - } - - fn as_morph_shape_mut(&mut self) -> Option<&mut Self> { - Some(self) - } - - fn run_frame(&mut self, context: &mut UpdateContext) { - if !self.frames.contains_key(&self.ratio) { - self.register_ratio(context.renderer, self.ratio); - } - } - - fn render(&self, context: &mut RenderContext) { - context.transform_stack.push(self.transform()); - - if let Some(shape) = self.frames.get(&self.ratio) { - context - .renderer - .render_shape(*shape, context.transform_stack.transform()); - } else { - warn!("Missing ratio for morph shape"); - } - - context.transform_stack.pop(); - } -} - -unsafe impl<'gc> gc_arena::Collect for MorphShape<'gc> { - #[inline] - fn trace(&self, cc: gc_arena::CollectionContext) { - self.base.trace(cc); - } -} +use crate::backend::render::{RenderBackend, ShapeHandle}; +use crate::display_object::{DisplayObject, DisplayObjectBase}; +use crate::player::{RenderContext, UpdateContext}; +use crate::prelude::*; +use std::collections::HashMap; +use swf::Twips; + +#[derive(Clone)] +pub struct MorphShape<'gc> { + base: DisplayObjectBase<'gc>, + + start: swf::MorphShape, + + end: swf::MorphShape, + + frames: HashMap, + + ratio: u16, +} + +impl<'gc> MorphShape<'gc> { + pub fn from_swf_tag(swf_tag: &swf::DefineMorphShape, renderer: &mut dyn RenderBackend) -> Self { + // Convert the MorphShape into a normal Shape. + // TODO(Herschel): impl From in swf crate? + let mut morph_shape = Self { + start: swf_tag.start.clone(), + end: swf_tag.end.clone(), + base: Default::default(), + frames: HashMap::new(), + ratio: 0, + }; + + morph_shape.register_ratio(renderer, 0); + morph_shape.register_ratio(renderer, 65535); + + morph_shape + } + + pub fn register_ratio(&mut self, renderer: &mut dyn RenderBackend, ratio: u16) { + if self.frames.contains_key(&ratio) { + // Already registered. + return; + } + + // Interpolate MorphShapes into a Shape. + use swf::{FillStyle, Gradient, LineStyle, ShapeRecord, ShapeStyles}; + // Start shape is ratio 65535, end shape is ratio 0. + let b = f32::from(ratio) / 65535.0; + let a = 1.0 - b; + let fill_styles: Vec = self + .start + .fill_styles + .iter() + .zip(self.end.fill_styles.iter()) + .map(|(start, end)| match (start, end) { + (FillStyle::Color(start), FillStyle::Color(end)) => FillStyle::Color(Color { + r: (a * f32::from(start.r) + b * f32::from(end.r)) as u8, + g: (a * f32::from(start.g) + b * f32::from(end.g)) as u8, + b: (a * f32::from(start.b) + b * f32::from(end.b)) as u8, + a: (a * f32::from(start.a) + b * f32::from(end.a)) as u8, + }), + (FillStyle::LinearGradient(start), FillStyle::LinearGradient(end)) => { + let records: Vec = start + .records + .iter() + .zip(end.records.iter()) + .map(|(start, end)| swf::GradientRecord { + ratio: (f32::from(start.ratio) * a + f32::from(end.ratio) * b) as u8, + color: Color { + r: (a * f32::from(start.color.r) + b * f32::from(end.color.r)) + as u8, + g: (a * f32::from(start.color.g) + b * f32::from(end.color.g)) + as u8, + b: (a * f32::from(start.color.b) + b * f32::from(end.color.b)) + as u8, + a: (a * f32::from(start.color.a) + b * f32::from(end.color.a)) + as u8, + }, + }) + .collect(); + + FillStyle::LinearGradient(Gradient { + matrix: start.matrix.clone(), + spread: start.spread, + interpolation: start.interpolation, + records, + }) + } + _ => { + log::info!("Unhandled morph shape combination: {:?} {:?}", start, end); + start.clone() + } + }) + .collect(); + let line_styles: Vec = self + .start + .line_styles + .iter() + .zip(self.end.line_styles.iter()) + .map(|(start, end)| LineStyle { + width: Twips::new( + ((start.width.get() as f32) * a + (end.width.get() as f32) * b) as i32, + ), + color: Color { + r: (a * f32::from(start.color.r) + b * f32::from(end.color.r)) as u8, + g: (a * f32::from(start.color.g) + b * f32::from(end.color.g)) as u8, + b: (a * f32::from(start.color.b) + b * f32::from(end.color.b)) as u8, + a: (a * f32::from(start.color.a) + b * f32::from(end.color.a)) as u8, + }, + start_cap: start.start_cap, + end_cap: start.end_cap, + join_style: start.join_style, + fill_style: None, + allow_scale_x: start.allow_scale_x, + allow_scale_y: start.allow_scale_y, + is_pixel_hinted: start.is_pixel_hinted, + allow_close: start.allow_close, + }) + .collect(); + + let mut shape = Vec::with_capacity(self.start.shape.len()); + let mut start_iter = self.start.shape.iter(); + let mut end_iter = self.end.shape.iter(); + let mut start = start_iter.next(); + let mut end = end_iter.next(); + let mut start_x = Twips::new(0); + let mut start_y = Twips::new(0); + let mut end_x = Twips::new(0); + let mut end_y = Twips::new(0); + // TODO: Feels like this could be cleaned up a bit. + // We step through both the start records and end records, interpolating edges pairwise. + // Fill style/line style changes should only appear in the start records. + // However, StyleChangeRecord move_to can appear it both start and end records, + // and not necessarily in matching pairs; therefore, we have to keep track of the pen position + // in case one side is missing a move_to; it will implicitly use the last pen position. + while let (Some(s), Some(e)) = (start, end) { + match (s, e) { + (ShapeRecord::StyleChange(start_change), ShapeRecord::StyleChange(end_change)) => { + let mut style_change = start_change.clone(); + if let Some((s_x, s_y)) = start_change.move_to { + if let Some((e_x, e_y)) = end_change.move_to { + start_x = s_x; + start_y = s_y; + end_x = e_x; + end_y = e_y; + style_change.move_to = Some(( + Twips::new( + (start_x.get() as f32 * a + end_x.get() as f32 * b) as i32, + ), + Twips::new( + (start_y.get() as f32 * a + end_y.get() as f32 * b) as i32, + ), + )); + } else { + panic!("Expected move_to for morph shape") + } + } + shape.push(ShapeRecord::StyleChange(style_change)); + start = start_iter.next(); + end = end_iter.next(); + } + (ShapeRecord::StyleChange(start_change), _) => { + let mut style_change = start_change.clone(); + if let Some((s_x, s_y)) = start_change.move_to { + start_x = s_x; + start_y = s_y; + style_change.move_to = Some(( + Twips::new((start_x.get() as f32 * a + end_x.get() as f32 * b) as i32), + Twips::new((start_y.get() as f32 * a + end_y.get() as f32 * b) as i32), + )); + } + shape.push(ShapeRecord::StyleChange(style_change)); + Self::update_pos(&mut start_x, &mut start_y, s); + start = start_iter.next(); + } + (_, ShapeRecord::StyleChange(end_change)) => { + let mut style_change = end_change.clone(); + if let Some((e_x, e_y)) = end_change.move_to { + end_x = e_x; + end_y = e_y; + style_change.move_to = Some(( + Twips::new((start_x.get() as f32 * a + end_x.get() as f32 * b) as i32), + Twips::new((start_y.get() as f32 * a + end_y.get() as f32 * b) as i32), + )); + } + shape.push(ShapeRecord::StyleChange(style_change)); + Self::update_pos(&mut end_x, &mut end_y, s); + end = end_iter.next(); + continue; + } + _ => { + shape.push(Self::interpolate_edges(s, e, a)); + Self::update_pos(&mut start_x, &mut start_y, s); + Self::update_pos(&mut end_x, &mut end_y, e); + start = start_iter.next(); + end = end_iter.next(); + } + } + } + + let styles = ShapeStyles { + fill_styles, + line_styles, + }; + + let bounds = crate::shape_utils::calculate_shape_bounds(&shape[..]); + let shape = swf::Shape { + version: 4, + id: 0, + shape_bounds: bounds.clone(), + edge_bounds: bounds, + has_fill_winding_rule: false, + has_non_scaling_strokes: false, + has_scaling_strokes: true, + styles, + shape, + }; + + let shape_handle = renderer.register_shape(&shape); + self.frames.insert(ratio, shape_handle); + } + + fn update_pos(x: &mut Twips, y: &mut Twips, record: &swf::ShapeRecord) { + use swf::ShapeRecord; + match record { + ShapeRecord::StraightEdge { delta_x, delta_y } => { + *x += *delta_x; + *y += *delta_y; + } + ShapeRecord::CurvedEdge { + control_delta_x, + control_delta_y, + anchor_delta_x, + anchor_delta_y, + } => { + *x += *control_delta_x + *anchor_delta_x; + *y += *control_delta_y + *anchor_delta_y; + } + ShapeRecord::StyleChange(ref style_change) => { + if let Some((move_x, move_y)) = style_change.move_to { + *x = move_x; + *y = move_y; + } + } + } + } + + fn interpolate_edges( + start: &swf::ShapeRecord, + end: &swf::ShapeRecord, + a: f32, + ) -> swf::ShapeRecord { + use swf::ShapeRecord; + let b = 1.0 - a; + match (start, end) { + ( + ShapeRecord::StraightEdge { + delta_x: start_dx, + delta_y: start_dy, + }, + ShapeRecord::StraightEdge { + delta_x: end_dx, + delta_y: end_dy, + }, + ) => ShapeRecord::StraightEdge { + delta_x: Twips::new((start_dx.get() as f32 * a + end_dx.get() as f32 * b) as i32), + delta_y: Twips::new((start_dy.get() as f32 * a + end_dy.get() as f32 * b) as i32), + }, + + ( + ShapeRecord::CurvedEdge { + control_delta_x: start_cdx, + control_delta_y: start_cdy, + anchor_delta_x: start_adx, + anchor_delta_y: start_ady, + }, + ShapeRecord::CurvedEdge { + control_delta_x: end_cdx, + control_delta_y: end_cdy, + anchor_delta_x: end_adx, + anchor_delta_y: end_ady, + }, + ) => ShapeRecord::CurvedEdge { + control_delta_x: Twips::new( + (start_cdx.get() as f32 * a + end_cdx.get() as f32 * b) as i32, + ), + control_delta_y: Twips::new( + (start_cdy.get() as f32 * a + end_cdy.get() as f32 * b) as i32, + ), + anchor_delta_x: Twips::new( + (start_adx.get() as f32 * a + end_adx.get() as f32 * b) as i32, + ), + anchor_delta_y: Twips::new( + (start_ady.get() as f32 * a + end_ady.get() as f32 * b) as i32, + ), + }, + + ( + ShapeRecord::StraightEdge { + delta_x: start_dx, + delta_y: start_dy, + }, + ShapeRecord::CurvedEdge { + control_delta_x: end_cdx, + control_delta_y: end_cdy, + anchor_delta_x: end_adx, + anchor_delta_y: end_ady, + }, + ) => { + let start_cdx = *start_dx / 2; + let start_cdy = *start_dy / 2; + let start_adx = start_cdx; + let start_ady = start_cdy; + ShapeRecord::CurvedEdge { + control_delta_x: Twips::new( + (start_cdx.get() as f32 * a + end_cdx.get() as f32 * b) as i32, + ), + control_delta_y: Twips::new( + (start_cdy.get() as f32 * a + end_cdy.get() as f32 * b) as i32, + ), + anchor_delta_x: Twips::new( + (start_adx.get() as f32 * a + end_adx.get() as f32 * b) as i32, + ), + anchor_delta_y: Twips::new( + (start_ady.get() as f32 * a + end_ady.get() as f32 * b) as i32, + ), + } + } + + ( + ShapeRecord::CurvedEdge { + control_delta_x: start_cdx, + control_delta_y: start_cdy, + anchor_delta_x: start_adx, + anchor_delta_y: start_ady, + }, + ShapeRecord::StraightEdge { + delta_x: end_dx, + delta_y: end_dy, + }, + ) => { + let end_cdx = *end_dx / 2; + let end_cdy = *end_dy / 2; + let end_adx = end_cdx; + let end_ady = end_cdy; + ShapeRecord::CurvedEdge { + control_delta_x: Twips::new( + (start_cdx.get() as f32 * a + end_cdx.get() as f32 * b) as i32, + ), + control_delta_y: Twips::new( + (start_cdy.get() as f32 * a + end_cdy.get() as f32 * b) as i32, + ), + anchor_delta_x: Twips::new( + (start_adx.get() as f32 * a + end_adx.get() as f32 * b) as i32, + ), + anchor_delta_y: Twips::new( + (start_ady.get() as f32 * a + end_ady.get() as f32 * b) as i32, + ), + } + } + _ => unreachable!("{:?} {:?}", start, end), + } + } + + pub fn ratio(&self) -> u16 { + self.ratio + } + + pub fn set_ratio(&mut self, ratio: u16) { + self.ratio = ratio; + } +} + +impl<'gc> DisplayObject<'gc> for MorphShape<'gc> { + impl_display_object!(base); + + fn as_morph_shape(&self) -> Option<&Self> { + Some(self) + } + + fn as_morph_shape_mut(&mut self) -> Option<&mut Self> { + Some(self) + } + + fn run_frame(&mut self, context: &mut UpdateContext) { + if !self.frames.contains_key(&self.ratio) { + self.register_ratio(context.renderer, self.ratio); + } + } + + fn render(&self, context: &mut RenderContext) { + context.transform_stack.push(self.transform()); + + if let Some(shape) = self.frames.get(&self.ratio) { + context + .renderer + .render_shape(*shape, context.transform_stack.transform()); + } else { + warn!("Missing ratio for morph shape"); + } + + context.transform_stack.pop(); + } +} + +unsafe impl<'gc> gc_arena::Collect for MorphShape<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + self.base.trace(cc); + } +} diff --git a/core/src/player.rs b/core/src/player.rs index 1a22af2c2..ff90fe1fe 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -1,544 +1,544 @@ -use crate::avm1::Avm1; -use crate::backend::{audio::AudioBackend, render::Letterbox, render::RenderBackend}; -use crate::events::{ButtonEvent, PlayerEvent}; -use crate::library::Library; -use crate::movie_clip::MovieClip; -use crate::prelude::*; -use crate::transform::TransformStack; -use gc_arena::{make_arena, ArenaParameters, Collect, GcCell, MutationContext}; -use log::info; -use std::sync::Arc; - -#[derive(Collect)] -#[collect(empty_drop)] -struct GcRoot<'gc> { - library: GcCell<'gc, Library<'gc>>, - root: DisplayNode<'gc>, - mouse_hover_node: GcCell<'gc, Option>>, // TODO: Remove GcCell wrapped inside GcCell. -} - -make_arena!(GcArena, GcRoot); - -pub struct Player { - swf_data: Arc>, - swf_version: u8, - - is_playing: bool, - - avm: Avm1, - audio: Audio, - renderer: Renderer, - transform_stack: TransformStack, - view_matrix: Matrix, - inverse_view_matrix: Matrix, - - gc_arena: GcArena, - background_color: Color, - - frame_rate: f64, - frame_accumulator: f64, - global_time: u64, - - viewport_width: u32, - viewport_height: u32, - movie_width: u32, - movie_height: u32, - letterbox: Letterbox, - - mouse_pos: (Twips, Twips), - is_mouse_down: bool, -} - -impl Player { - pub fn new( - renderer: Renderer, - audio: Audio, - swf_data: Vec, - ) -> Result> { - let (header, mut reader) = swf::read::read_swf_header(&swf_data[..]).unwrap(); - // Decompress the entire SWF in memory. - let mut data = Vec::new(); - reader.get_mut().read_to_end(&mut data)?; - let swf_len = data.len(); - - info!("{}x{}", header.stage_size.x_max, header.stage_size.y_max); - - let movie_width = (header.stage_size.x_max - header.stage_size.x_min).to_pixels() as u32; - let movie_height = (header.stage_size.y_max - header.stage_size.y_min).to_pixels() as u32; - - let mut player = Player { - swf_data: Arc::new(data), - swf_version: header.version, - - is_playing: false, - - avm: Avm1::new(header.version), - renderer, - audio, - - background_color: Color { - r: 255, - g: 255, - b: 255, - a: 255, - }, - transform_stack: TransformStack::new(), - view_matrix: Default::default(), - inverse_view_matrix: Default::default(), - - gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| GcRoot { - library: GcCell::allocate(gc_context, Library::new()), - root: GcCell::allocate( - gc_context, - Box::new(MovieClip::new_with_data( - gc_context, - 0, - 0, - swf_len, - header.num_frames, - )), - ), - mouse_hover_node: GcCell::allocate(gc_context, None), - }), - - frame_rate: header.frame_rate.into(), - frame_accumulator: 0.0, - global_time: 0, - - movie_width, - movie_height, - viewport_width: movie_width, - viewport_height: movie_height, - letterbox: Letterbox::None, - - mouse_pos: (Twips::new(0), Twips::new(0)), - is_mouse_down: false, - }; - - player.build_matrices(); - player.preload(); - - Ok(player) - } - - pub fn tick(&mut self, dt: f64) { - // Don't run until preloading is complete. - // TODO: Eventually we want to stream content similar to the Flash player. - if !self.audio.is_loading_complete() { - return; - } - - if self.is_playing() { - self.frame_accumulator += dt; - self.global_time += dt as u64; - let frame_time = 1000.0 / self.frame_rate; - - let needs_render = self.frame_accumulator >= frame_time; - - const MAX_FRAMES_PER_TICK: u32 = 5; // Sanity cap on frame tick. - let mut frame = 0; - while frame < MAX_FRAMES_PER_TICK && self.frame_accumulator >= frame_time { - self.frame_accumulator -= frame_time; - self.run_frame(); - frame += 1; - } - - // Sanity: If we had too many frames to tick, just reset the accumulator - // to prevent running at turbo speed. - if self.frame_accumulator >= frame_time { - self.frame_accumulator = 0.0; - } - - if needs_render { - self.render(); - } - - self.audio.tick(); - } - } - - pub fn is_playing(&self) -> bool { - self.is_playing - } - - pub fn set_is_playing(&mut self, v: bool) { - if v { - // Allow auto-play after user gesture for web backends. - self.audio.prime_audio(); - } - self.is_playing = v; - } - - pub fn movie_width(&self) -> u32 { - self.movie_width - } - - pub fn movie_height(&self) -> u32 { - self.movie_height - } - - pub fn viewport_dimensions(&self) -> (u32, u32) { - (self.viewport_width, self.viewport_height) - } - - pub fn set_viewport_dimensions(&mut self, width: u32, height: u32) { - self.viewport_width = width; - self.viewport_height = height; - self.build_matrices(); - } - - pub fn handle_event(&mut self, event: PlayerEvent) { - let mut needs_render = false; - - // Update mouse position from mouse events. - if let PlayerEvent::MouseMove { x, y } - | PlayerEvent::MouseDown { x, y } - | PlayerEvent::MouseUp { x, y } = event - { - self.mouse_pos = - self.inverse_view_matrix * (Twips::from_pixels(x), Twips::from_pixels(y)); - if self.update_roll_over() { - needs_render = true; - } - } - - let ( - global_time, - swf_data, - swf_version, - background_color, - renderer, - audio, - avm, - is_mouse_down, - ) = ( - self.global_time, - &mut self.swf_data, - self.swf_version, - &mut self.background_color, - &mut self.renderer, - &mut self.audio, - &mut self.avm, - &mut self.is_mouse_down, - ); - - self.gc_arena.mutate(|gc_context, gc_root| { - let mut update_context = UpdateContext { - global_time, - swf_data, - swf_version, - library: gc_root.library.write(gc_context), - background_color, - avm, - renderer, - audio, - actions: vec![], - gc_context, - active_clip: gc_root.root, - }; - - if let Some(node) = &*gc_root.mouse_hover_node.read() { - if let Some(button) = node.write(gc_context).as_button_mut() { - match event { - PlayerEvent::MouseDown { .. } => { - *is_mouse_down = true; - needs_render = true; - update_context.active_clip = *node; - button.handle_button_event(&mut update_context, ButtonEvent::Press); - } - - PlayerEvent::MouseUp { .. } => { - *is_mouse_down = false; - needs_render = true; - update_context.active_clip = *node; - button.handle_button_event(&mut update_context, ButtonEvent::Release); - } - - _ => (), - } - } - } - - Self::run_actions(&mut update_context, gc_root.root); - }); - - if needs_render { - // Update display after mouse events. - self.render(); - } - } - - fn update_roll_over(&mut self) -> bool { - // TODO: While the mouse is down, maintain the hovered node. - if self.is_mouse_down { - return false; - } - - let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = ( - self.global_time, - &mut self.swf_data, - self.swf_version, - &mut self.background_color, - &mut self.renderer, - &mut self.audio, - &mut self.avm, - ); - - let mouse_pos = &self.mouse_pos; - // Check hovered object. - self.gc_arena.mutate(|gc_context, gc_root| { - let new_hover_node = gc_root - .root - .read() - .mouse_pick(gc_root.root, (mouse_pos.0, mouse_pos.1)); - let mut cur_hover_node = gc_root.mouse_hover_node.write(gc_context); - if cur_hover_node.map(GcCell::as_ptr) != new_hover_node.map(GcCell::as_ptr) { - let mut update_context = UpdateContext { - global_time, - swf_data, - swf_version, - library: gc_root.library.write(gc_context), - background_color, - avm, - renderer, - audio, - actions: vec![], - gc_context, - active_clip: gc_root.root, - }; - - // RollOut of previous node. - if let Some(node) = &*cur_hover_node { - if let Some(button) = node.write(gc_context).as_button_mut() { - update_context.active_clip = *node; - button.handle_button_event(&mut update_context, ButtonEvent::RollOut); - } - } - - // RollOver on new node. - if let Some(node) = new_hover_node { - if let Some(button) = node.write(gc_context).as_button_mut() { - update_context.active_clip = node; - button.handle_button_event(&mut update_context, ButtonEvent::RollOver); - } - } - - *cur_hover_node = new_hover_node; - - Self::run_actions(&mut update_context, gc_root.root); - true - } else { - false - } - }) - } - - fn preload(&mut self) { - let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = ( - self.global_time, - &mut self.swf_data, - self.swf_version, - &mut self.background_color, - &mut self.renderer, - &mut self.audio, - &mut self.avm, - ); - - self.gc_arena.mutate(|gc_context, gc_root| { - let mut update_context = UpdateContext { - global_time, - swf_data, - swf_version, - library: gc_root.library.write(gc_context), - background_color, - avm, - renderer, - audio, - actions: vec![], - gc_context, - active_clip: gc_root.root, - }; - - gc_root.root.write(gc_context).preload(&mut update_context); - }); - } - - pub fn run_frame(&mut self) { - let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = ( - self.global_time, - &mut self.swf_data, - self.swf_version, - &mut self.background_color, - &mut self.renderer, - &mut self.audio, - &mut self.avm, - ); - - self.gc_arena.mutate(|gc_context, gc_root| { - let mut update_context = UpdateContext { - global_time, - swf_data, - swf_version, - library: gc_root.library.write(gc_context), - background_color, - avm, - renderer, - audio, - actions: vec![], - gc_context, - active_clip: gc_root.root, - }; - - gc_root - .root - .write(gc_context) - .run_frame(&mut update_context); - - Self::run_actions(&mut update_context, gc_root.root); - }); - - // Update mouse state (check for new hovered button, etc.) - self.update_roll_over(); - - // GC - self.gc_arena.collect_debt(); - } - - pub fn render(&mut self) { - let view_bounds = BoundingBox { - x_min: Twips::new(0), - y_min: Twips::new(0), - x_max: Twips::from_pixels(self.movie_width.into()), - y_max: Twips::from_pixels(self.movie_height.into()), - valid: true, - }; - - self.renderer.begin_frame(); - - self.renderer.clear(self.background_color.clone()); - - let (renderer, transform_stack) = (&mut self.renderer, &mut self.transform_stack); - - transform_stack.push(&crate::transform::Transform { - matrix: self.view_matrix, - ..Default::default() - }); - self.gc_arena.mutate(|_gc_context, gc_root| { - let mut render_context = RenderContext { - renderer, - library: gc_root.library.read(), - transform_stack, - view_bounds, - }; - gc_root.root.read().render(&mut render_context); - }); - transform_stack.pop(); - - if !self.is_playing() { - self.renderer.draw_pause_overlay(); - } - - self.renderer.draw_letterbox(self.letterbox); - self.renderer.end_frame(); - } - - pub fn renderer(&self) -> &Renderer { - &self.renderer - } - - pub fn renderer_mut(&mut self) -> &mut Renderer { - &mut self.renderer - } - - fn run_actions<'gc>(update_context: &mut UpdateContext<'_, 'gc, '_>, root: DisplayNode<'gc>) { - // TODO: Loop here because goto-ing a frame can queue up for actions. - // I think this will eventually be cleaned up; - // Need to figure out the proper order of operations between ticking a clip - // and running the actions. - let mut actions = std::mem::replace(&mut update_context.actions, vec![]); - while !actions.is_empty() { - { - let mut action_context = crate::avm1::ActionContext { - gc_context: update_context.gc_context, - global_time: update_context.global_time, - root, - start_clip: root, - active_clip: root, - audio: update_context.audio, - }; - for (active_clip, action) in actions { - action_context.start_clip = active_clip; - action_context.active_clip = active_clip; - let _ = update_context - .avm - .do_action(&mut action_context, action.as_ref()); - } - } - - // Run goto queues. - update_context.active_clip = root; - root.write(update_context.gc_context) - .run_post_frame(update_context); - - actions = std::mem::replace(&mut update_context.actions, vec![]); - } - } - - fn build_matrices(&mut self) { - // Create view matrix to scale stage into viewport area. - let (movie_width, movie_height) = (self.movie_width as f32, self.movie_height as f32); - let (viewport_width, viewport_height) = - (self.viewport_width as f32, self.viewport_height as f32); - let movie_aspect = movie_width / movie_height; - let viewport_aspect = viewport_width / viewport_height; - let (scale, margin_width, margin_height) = if viewport_aspect > movie_aspect { - let scale = viewport_height / movie_height; - (scale, (viewport_width - movie_width * scale) / 2.0, 0.0) - } else { - let scale = viewport_width / movie_width; - (scale, 0.0, (viewport_height - movie_height * scale) / 2.0) - }; - self.view_matrix = Matrix { - a: scale, - b: 0.0, - c: 0.0, - d: scale, - tx: margin_width * 20.0, - ty: margin_height * 20.0, - }; - self.inverse_view_matrix = self.view_matrix; - self.inverse_view_matrix.invert(); - - // Calculate letterbox dimensions. - // TODO: Letterbox should be an option; the original Flash Player defaults to showing content - // in the extra margins. - self.letterbox = if margin_width > 0.0 { - Letterbox::Pillarbox(margin_width) - } else if margin_height > 0.0 { - Letterbox::Letterbox(margin_height) - } else { - Letterbox::None - }; - } -} - -pub struct UpdateContext<'a, 'gc, 'gc_context> { - pub swf_version: u8, - pub swf_data: &'a Arc>, - pub global_time: u64, - pub library: std::cell::RefMut<'a, Library<'gc>>, - pub gc_context: MutationContext<'gc, 'gc_context>, - pub background_color: &'a mut Color, - pub avm: &'a mut Avm1, - pub renderer: &'a mut dyn RenderBackend, - pub audio: &'a mut dyn AudioBackend, - pub actions: Vec<(DisplayNode<'gc>, crate::tag_utils::SwfSlice)>, - pub active_clip: DisplayNode<'gc>, -} - -pub struct RenderContext<'a, 'gc> { - pub renderer: &'a mut dyn RenderBackend, - pub library: std::cell::Ref<'a, Library<'gc>>, - pub transform_stack: &'a mut TransformStack, - pub view_bounds: BoundingBox, -} +use crate::avm1::Avm1; +use crate::backend::{audio::AudioBackend, render::Letterbox, render::RenderBackend}; +use crate::events::{ButtonEvent, PlayerEvent}; +use crate::library::Library; +use crate::movie_clip::MovieClip; +use crate::prelude::*; +use crate::transform::TransformStack; +use gc_arena::{make_arena, ArenaParameters, Collect, GcCell, MutationContext}; +use log::info; +use std::sync::Arc; + +#[derive(Collect)] +#[collect(empty_drop)] +struct GcRoot<'gc> { + library: GcCell<'gc, Library<'gc>>, + root: DisplayNode<'gc>, + mouse_hover_node: GcCell<'gc, Option>>, // TODO: Remove GcCell wrapped inside GcCell. +} + +make_arena!(GcArena, GcRoot); + +pub struct Player { + swf_data: Arc>, + swf_version: u8, + + is_playing: bool, + + avm: Avm1, + audio: Audio, + renderer: Renderer, + transform_stack: TransformStack, + view_matrix: Matrix, + inverse_view_matrix: Matrix, + + gc_arena: GcArena, + background_color: Color, + + frame_rate: f64, + frame_accumulator: f64, + global_time: u64, + + viewport_width: u32, + viewport_height: u32, + movie_width: u32, + movie_height: u32, + letterbox: Letterbox, + + mouse_pos: (Twips, Twips), + is_mouse_down: bool, +} + +impl Player { + pub fn new( + renderer: Renderer, + audio: Audio, + swf_data: Vec, + ) -> Result> { + let (header, mut reader) = swf::read::read_swf_header(&swf_data[..]).unwrap(); + // Decompress the entire SWF in memory. + let mut data = Vec::new(); + reader.get_mut().read_to_end(&mut data)?; + let swf_len = data.len(); + + info!("{}x{}", header.stage_size.x_max, header.stage_size.y_max); + + let movie_width = (header.stage_size.x_max - header.stage_size.x_min).to_pixels() as u32; + let movie_height = (header.stage_size.y_max - header.stage_size.y_min).to_pixels() as u32; + + let mut player = Player { + swf_data: Arc::new(data), + swf_version: header.version, + + is_playing: false, + + avm: Avm1::new(header.version), + renderer, + audio, + + background_color: Color { + r: 255, + g: 255, + b: 255, + a: 255, + }, + transform_stack: TransformStack::new(), + view_matrix: Default::default(), + inverse_view_matrix: Default::default(), + + gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| GcRoot { + library: GcCell::allocate(gc_context, Library::new()), + root: GcCell::allocate( + gc_context, + Box::new(MovieClip::new_with_data( + gc_context, + 0, + 0, + swf_len, + header.num_frames, + )), + ), + mouse_hover_node: GcCell::allocate(gc_context, None), + }), + + frame_rate: header.frame_rate.into(), + frame_accumulator: 0.0, + global_time: 0, + + movie_width, + movie_height, + viewport_width: movie_width, + viewport_height: movie_height, + letterbox: Letterbox::None, + + mouse_pos: (Twips::new(0), Twips::new(0)), + is_mouse_down: false, + }; + + player.build_matrices(); + player.preload(); + + Ok(player) + } + + pub fn tick(&mut self, dt: f64) { + // Don't run until preloading is complete. + // TODO: Eventually we want to stream content similar to the Flash player. + if !self.audio.is_loading_complete() { + return; + } + + if self.is_playing() { + self.frame_accumulator += dt; + self.global_time += dt as u64; + let frame_time = 1000.0 / self.frame_rate; + + let needs_render = self.frame_accumulator >= frame_time; + + const MAX_FRAMES_PER_TICK: u32 = 5; // Sanity cap on frame tick. + let mut frame = 0; + while frame < MAX_FRAMES_PER_TICK && self.frame_accumulator >= frame_time { + self.frame_accumulator -= frame_time; + self.run_frame(); + frame += 1; + } + + // Sanity: If we had too many frames to tick, just reset the accumulator + // to prevent running at turbo speed. + if self.frame_accumulator >= frame_time { + self.frame_accumulator = 0.0; + } + + if needs_render { + self.render(); + } + + self.audio.tick(); + } + } + + pub fn is_playing(&self) -> bool { + self.is_playing + } + + pub fn set_is_playing(&mut self, v: bool) { + if v { + // Allow auto-play after user gesture for web backends. + self.audio.prime_audio(); + } + self.is_playing = v; + } + + pub fn movie_width(&self) -> u32 { + self.movie_width + } + + pub fn movie_height(&self) -> u32 { + self.movie_height + } + + pub fn viewport_dimensions(&self) -> (u32, u32) { + (self.viewport_width, self.viewport_height) + } + + pub fn set_viewport_dimensions(&mut self, width: u32, height: u32) { + self.viewport_width = width; + self.viewport_height = height; + self.build_matrices(); + } + + pub fn handle_event(&mut self, event: PlayerEvent) { + let mut needs_render = false; + + // Update mouse position from mouse events. + if let PlayerEvent::MouseMove { x, y } + | PlayerEvent::MouseDown { x, y } + | PlayerEvent::MouseUp { x, y } = event + { + self.mouse_pos = + self.inverse_view_matrix * (Twips::from_pixels(x), Twips::from_pixels(y)); + if self.update_roll_over() { + needs_render = true; + } + } + + let ( + global_time, + swf_data, + swf_version, + background_color, + renderer, + audio, + avm, + is_mouse_down, + ) = ( + self.global_time, + &mut self.swf_data, + self.swf_version, + &mut self.background_color, + &mut self.renderer, + &mut self.audio, + &mut self.avm, + &mut self.is_mouse_down, + ); + + self.gc_arena.mutate(|gc_context, gc_root| { + let mut update_context = UpdateContext { + global_time, + swf_data, + swf_version, + library: gc_root.library.write(gc_context), + background_color, + avm, + renderer, + audio, + actions: vec![], + gc_context, + active_clip: gc_root.root, + }; + + if let Some(node) = &*gc_root.mouse_hover_node.read() { + if let Some(button) = node.write(gc_context).as_button_mut() { + match event { + PlayerEvent::MouseDown { .. } => { + *is_mouse_down = true; + needs_render = true; + update_context.active_clip = *node; + button.handle_button_event(&mut update_context, ButtonEvent::Press); + } + + PlayerEvent::MouseUp { .. } => { + *is_mouse_down = false; + needs_render = true; + update_context.active_clip = *node; + button.handle_button_event(&mut update_context, ButtonEvent::Release); + } + + _ => (), + } + } + } + + Self::run_actions(&mut update_context, gc_root.root); + }); + + if needs_render { + // Update display after mouse events. + self.render(); + } + } + + fn update_roll_over(&mut self) -> bool { + // TODO: While the mouse is down, maintain the hovered node. + if self.is_mouse_down { + return false; + } + + let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = ( + self.global_time, + &mut self.swf_data, + self.swf_version, + &mut self.background_color, + &mut self.renderer, + &mut self.audio, + &mut self.avm, + ); + + let mouse_pos = &self.mouse_pos; + // Check hovered object. + self.gc_arena.mutate(|gc_context, gc_root| { + let new_hover_node = gc_root + .root + .read() + .mouse_pick(gc_root.root, (mouse_pos.0, mouse_pos.1)); + let mut cur_hover_node = gc_root.mouse_hover_node.write(gc_context); + if cur_hover_node.map(GcCell::as_ptr) != new_hover_node.map(GcCell::as_ptr) { + let mut update_context = UpdateContext { + global_time, + swf_data, + swf_version, + library: gc_root.library.write(gc_context), + background_color, + avm, + renderer, + audio, + actions: vec![], + gc_context, + active_clip: gc_root.root, + }; + + // RollOut of previous node. + if let Some(node) = &*cur_hover_node { + if let Some(button) = node.write(gc_context).as_button_mut() { + update_context.active_clip = *node; + button.handle_button_event(&mut update_context, ButtonEvent::RollOut); + } + } + + // RollOver on new node. + if let Some(node) = new_hover_node { + if let Some(button) = node.write(gc_context).as_button_mut() { + update_context.active_clip = node; + button.handle_button_event(&mut update_context, ButtonEvent::RollOver); + } + } + + *cur_hover_node = new_hover_node; + + Self::run_actions(&mut update_context, gc_root.root); + true + } else { + false + } + }) + } + + fn preload(&mut self) { + let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = ( + self.global_time, + &mut self.swf_data, + self.swf_version, + &mut self.background_color, + &mut self.renderer, + &mut self.audio, + &mut self.avm, + ); + + self.gc_arena.mutate(|gc_context, gc_root| { + let mut update_context = UpdateContext { + global_time, + swf_data, + swf_version, + library: gc_root.library.write(gc_context), + background_color, + avm, + renderer, + audio, + actions: vec![], + gc_context, + active_clip: gc_root.root, + }; + + gc_root.root.write(gc_context).preload(&mut update_context); + }); + } + + pub fn run_frame(&mut self) { + let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = ( + self.global_time, + &mut self.swf_data, + self.swf_version, + &mut self.background_color, + &mut self.renderer, + &mut self.audio, + &mut self.avm, + ); + + self.gc_arena.mutate(|gc_context, gc_root| { + let mut update_context = UpdateContext { + global_time, + swf_data, + swf_version, + library: gc_root.library.write(gc_context), + background_color, + avm, + renderer, + audio, + actions: vec![], + gc_context, + active_clip: gc_root.root, + }; + + gc_root + .root + .write(gc_context) + .run_frame(&mut update_context); + + Self::run_actions(&mut update_context, gc_root.root); + }); + + // Update mouse state (check for new hovered button, etc.) + self.update_roll_over(); + + // GC + self.gc_arena.collect_debt(); + } + + pub fn render(&mut self) { + let view_bounds = BoundingBox { + x_min: Twips::new(0), + y_min: Twips::new(0), + x_max: Twips::from_pixels(self.movie_width.into()), + y_max: Twips::from_pixels(self.movie_height.into()), + valid: true, + }; + + self.renderer.begin_frame(); + + self.renderer.clear(self.background_color.clone()); + + let (renderer, transform_stack) = (&mut self.renderer, &mut self.transform_stack); + + transform_stack.push(&crate::transform::Transform { + matrix: self.view_matrix, + ..Default::default() + }); + self.gc_arena.mutate(|_gc_context, gc_root| { + let mut render_context = RenderContext { + renderer, + library: gc_root.library.read(), + transform_stack, + view_bounds, + }; + gc_root.root.read().render(&mut render_context); + }); + transform_stack.pop(); + + if !self.is_playing() { + self.renderer.draw_pause_overlay(); + } + + self.renderer.draw_letterbox(self.letterbox); + self.renderer.end_frame(); + } + + pub fn renderer(&self) -> &Renderer { + &self.renderer + } + + pub fn renderer_mut(&mut self) -> &mut Renderer { + &mut self.renderer + } + + fn run_actions<'gc>(update_context: &mut UpdateContext<'_, 'gc, '_>, root: DisplayNode<'gc>) { + // TODO: Loop here because goto-ing a frame can queue up for actions. + // I think this will eventually be cleaned up; + // Need to figure out the proper order of operations between ticking a clip + // and running the actions. + let mut actions = std::mem::replace(&mut update_context.actions, vec![]); + while !actions.is_empty() { + { + let mut action_context = crate::avm1::ActionContext { + gc_context: update_context.gc_context, + global_time: update_context.global_time, + root, + start_clip: root, + active_clip: root, + audio: update_context.audio, + }; + for (active_clip, action) in actions { + action_context.start_clip = active_clip; + action_context.active_clip = active_clip; + let _ = update_context + .avm + .do_action(&mut action_context, action.as_ref()); + } + } + + // Run goto queues. + update_context.active_clip = root; + root.write(update_context.gc_context) + .run_post_frame(update_context); + + actions = std::mem::replace(&mut update_context.actions, vec![]); + } + } + + fn build_matrices(&mut self) { + // Create view matrix to scale stage into viewport area. + let (movie_width, movie_height) = (self.movie_width as f32, self.movie_height as f32); + let (viewport_width, viewport_height) = + (self.viewport_width as f32, self.viewport_height as f32); + let movie_aspect = movie_width / movie_height; + let viewport_aspect = viewport_width / viewport_height; + let (scale, margin_width, margin_height) = if viewport_aspect > movie_aspect { + let scale = viewport_height / movie_height; + (scale, (viewport_width - movie_width * scale) / 2.0, 0.0) + } else { + let scale = viewport_width / movie_width; + (scale, 0.0, (viewport_height - movie_height * scale) / 2.0) + }; + self.view_matrix = Matrix { + a: scale, + b: 0.0, + c: 0.0, + d: scale, + tx: margin_width * 20.0, + ty: margin_height * 20.0, + }; + self.inverse_view_matrix = self.view_matrix; + self.inverse_view_matrix.invert(); + + // Calculate letterbox dimensions. + // TODO: Letterbox should be an option; the original Flash Player defaults to showing content + // in the extra margins. + self.letterbox = if margin_width > 0.0 { + Letterbox::Pillarbox(margin_width) + } else if margin_height > 0.0 { + Letterbox::Letterbox(margin_height) + } else { + Letterbox::None + }; + } +} + +pub struct UpdateContext<'a, 'gc, 'gc_context> { + pub swf_version: u8, + pub swf_data: &'a Arc>, + pub global_time: u64, + pub library: std::cell::RefMut<'a, Library<'gc>>, + pub gc_context: MutationContext<'gc, 'gc_context>, + pub background_color: &'a mut Color, + pub avm: &'a mut Avm1, + pub renderer: &'a mut dyn RenderBackend, + pub audio: &'a mut dyn AudioBackend, + pub actions: Vec<(DisplayNode<'gc>, crate::tag_utils::SwfSlice)>, + pub active_clip: DisplayNode<'gc>, +} + +pub struct RenderContext<'a, 'gc> { + pub renderer: &'a mut dyn RenderBackend, + pub library: std::cell::Ref<'a, Library<'gc>>, + pub transform_stack: &'a mut TransformStack, + pub view_bounds: BoundingBox, +} diff --git a/core/src/tag_utils.rs b/core/src/tag_utils.rs index 19b190284..e4f5ce433 100644 --- a/core/src/tag_utils.rs +++ b/core/src/tag_utils.rs @@ -1,52 +1,52 @@ -use swf::TagCode; - -pub type DecodeResult = Result<(), Box>; -pub type SwfStream = swf::read::Reader>; - -#[derive(Debug, Clone)] -pub struct SwfSlice { - pub data: std::sync::Arc>, - pub start: usize, - pub end: usize, -} - -impl AsRef<[u8]> for SwfSlice { - fn as_ref(&self) -> &[u8] { - &self.data[self.start..self.end] - } -} - -pub fn decode_tags<'a, R, F>( - reader: &'a mut SwfStream, - mut tag_callback: F, - stop_tag: TagCode, -) -> Result<(), Box> -where - R: 'a + AsRef<[u8]>, - F: FnMut(&mut SwfStream, TagCode, usize) -> DecodeResult, -{ - loop { - let (tag_code, tag_len) = reader.read_tag_code_and_length()?; - let end_pos = reader.get_ref().position() + tag_len as u64; - - let tag = TagCode::from_u16(tag_code); - if let Some(tag) = tag { - let result = tag_callback(reader, tag, tag_len); - - if let Err(_e) = result { - log::error!("Error running definition tag: {:?}", tag); - } - - if stop_tag == tag { - break; - } - } else { - log::warn!("Unknown tag code: {:?}", tag_code); - } - - use std::io::{Seek, SeekFrom}; - reader.get_mut().seek(SeekFrom::Start(end_pos))?; - } - - Ok(()) -} +use swf::TagCode; + +pub type DecodeResult = Result<(), Box>; +pub type SwfStream = swf::read::Reader>; + +#[derive(Debug, Clone)] +pub struct SwfSlice { + pub data: std::sync::Arc>, + pub start: usize, + pub end: usize, +} + +impl AsRef<[u8]> for SwfSlice { + fn as_ref(&self) -> &[u8] { + &self.data[self.start..self.end] + } +} + +pub fn decode_tags<'a, R, F>( + reader: &'a mut SwfStream, + mut tag_callback: F, + stop_tag: TagCode, +) -> Result<(), Box> +where + R: 'a + AsRef<[u8]>, + F: FnMut(&mut SwfStream, TagCode, usize) -> DecodeResult, +{ + loop { + let (tag_code, tag_len) = reader.read_tag_code_and_length()?; + let end_pos = reader.get_ref().position() + tag_len as u64; + + let tag = TagCode::from_u16(tag_code); + if let Some(tag) = tag { + let result = tag_callback(reader, tag, tag_len); + + if let Err(_e) = result { + log::error!("Error running definition tag: {:?}", tag); + } + + if stop_tag == tag { + break; + } + } else { + log::warn!("Unknown tag code: {:?}", tag_code); + } + + use std::io::{Seek, SeekFrom}; + reader.get_mut().seek(SeekFrom::Start(end_pos))?; + } + + Ok(()) +} diff --git a/core/src/transform.rs b/core/src/transform.rs index 6b97c9e93..63d5838fb 100644 --- a/core/src/transform.rs +++ b/core/src/transform.rs @@ -31,7 +31,10 @@ impl TransformStack { let cur_transform = self.transform(); let matrix = cur_transform.matrix * transform.matrix; let color_transform = cur_transform.color_transform * transform.color_transform; - self.0.push(Transform { matrix, color_transform }); + self.0.push(Transform { + matrix, + color_transform, + }); } pub fn pop(&mut self) { diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index feaebe497..a846a9555 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -1,80 +1,80 @@ -//! Tests running SWFs in a headless Ruffle instance. -//! -//! Trace output can be compared with correct output from the official Flash Payer. - -use log::{Metadata, Record}; -use ruffle_core::backend::{audio::NullAudioBackend, render::NullRenderer}; -use ruffle_core::Player; -use std::cell::RefCell; - -type Error = Box; - -// This macro generates test cases for a given list of SWFs. -macro_rules! swf_tests { - ($(($name:ident, $path:expr, $num_frames:literal),)*) => { - $( - #[test] - fn $name() -> Result<(), Error> { - test_swf( - concat!("tests/swfs/", $path, "/test.swf"), - $num_frames, - concat!("tests/swfs/", $path, "/output.txt"), - ) - } - )* - } -} - -// List of SWFs to test. -// Format: (test_name, test_folder, number_of_frames_to_run) -// The test folder is a relative to core/tests/swfs -// Inside the folder is expected to be "test.swf" and "output.txt" with the correct output. -swf_tests! { - (single_frame, "avm1/single_frame", 2), - (looping, "avm1/looping", 6), -} - -/// Loads an SWF and runs it through the Ruffle core for a number of frames. -/// Tests that the trace output matches the given expected output. -fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Result<(), Error> { - let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info)); - let expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n"); - - let swf_data = std::fs::read(swf_path)?; - let mut player = Player::new(NullRenderer, NullAudioBackend::new(), swf_data)?; - - for _ in 0..num_frames { - player.run_frame(); - } - - assert_eq!(trace_log(), expected_output); - - Ok(()) -} - -thread_local! { - static TRACE_LOG: RefCell = RefCell::new(String::new()); -} - -static TRACE_LOGGER: TraceLogger = TraceLogger; - -/// `TraceLogger` captures output from AVM trace actions into a String. -struct TraceLogger; - -fn trace_log() -> String { - TRACE_LOG.with(|log| log.borrow().clone()) -} - -impl log::Log for TraceLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.target() == "avm_trace" - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - TRACE_LOG.with(|log| log.borrow_mut().push_str(&format!("{}\n", record.args()))); - } - } - - fn flush(&self) {} -} +//! Tests running SWFs in a headless Ruffle instance. +//! +//! Trace output can be compared with correct output from the official Flash Payer. + +use log::{Metadata, Record}; +use ruffle_core::backend::{audio::NullAudioBackend, render::NullRenderer}; +use ruffle_core::Player; +use std::cell::RefCell; + +type Error = Box; + +// This macro generates test cases for a given list of SWFs. +macro_rules! swf_tests { + ($(($name:ident, $path:expr, $num_frames:literal),)*) => { + $( + #[test] + fn $name() -> Result<(), Error> { + test_swf( + concat!("tests/swfs/", $path, "/test.swf"), + $num_frames, + concat!("tests/swfs/", $path, "/output.txt"), + ) + } + )* + } +} + +// List of SWFs to test. +// Format: (test_name, test_folder, number_of_frames_to_run) +// The test folder is a relative to core/tests/swfs +// Inside the folder is expected to be "test.swf" and "output.txt" with the correct output. +swf_tests! { + (single_frame, "avm1/single_frame", 2), + (looping, "avm1/looping", 6), +} + +/// Loads an SWF and runs it through the Ruffle core for a number of frames. +/// Tests that the trace output matches the given expected output. +fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Result<(), Error> { + let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info)); + let expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n"); + + let swf_data = std::fs::read(swf_path)?; + let mut player = Player::new(NullRenderer, NullAudioBackend::new(), swf_data)?; + + for _ in 0..num_frames { + player.run_frame(); + } + + assert_eq!(trace_log(), expected_output); + + Ok(()) +} + +thread_local! { + static TRACE_LOG: RefCell = RefCell::new(String::new()); +} + +static TRACE_LOGGER: TraceLogger = TraceLogger; + +/// `TraceLogger` captures output from AVM trace actions into a String. +struct TraceLogger; + +fn trace_log() -> String { + TRACE_LOG.with(|log| log.borrow().clone()) +} + +impl log::Log for TraceLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.target() == "avm_trace" + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + TRACE_LOG.with(|log| log.borrow_mut().push_str(&format!("{}\n", record.args()))); + } + } + + fn flush(&self) {} +} diff --git a/desktop/src/audio.rs b/desktop/src/audio.rs index a3f8de954..d1a06f212 100644 --- a/desktop/src/audio.rs +++ b/desktop/src/audio.rs @@ -1,156 +1,156 @@ -use generational_arena::Arena; -use ruffle_core::backend::audio::decoders::{stream_tag_reader, AdpcmDecoder, Decoder, Mp3Decoder}; -use ruffle_core::backend::audio::{swf, AudioBackend, AudioStreamHandle, SoundHandle}; -use std::io::Cursor; -use std::sync::Arc; - -pub struct RodioAudioBackend { - sounds: Arena, - active_sounds: Arena, - streams: Arena, - device: rodio::Device, -} - -#[allow(dead_code)] -struct AudioStream { - clip_id: swf::CharacterId, - info: swf::SoundStreamHead, - sink: rodio::Sink, -} - -#[allow(dead_code)] -struct Sound { - format: swf::SoundFormat, - data: Arc>, -} - -impl RodioAudioBackend { - pub fn new() -> Result> { - Ok(Self { - sounds: Arena::new(), - streams: Arena::new(), - active_sounds: Arena::new(), - device: rodio::default_output_device().ok_or("Unable to create output device")?, - }) - } -} - -impl AudioBackend for RodioAudioBackend { - fn register_sound( - &mut self, - swf_sound: &swf::Sound, - ) -> Result> { - let sound = Sound { - format: swf_sound.format.clone(), - data: Arc::new(swf_sound.data.clone()), - }; - Ok(self.sounds.insert(sound)) - } - - fn start_stream( - &mut self, - clip_id: swf::CharacterId, - clip_data: ruffle_core::tag_utils::SwfSlice, - stream_info: &swf::SoundStreamHead, - ) -> AudioStreamHandle { - let sink = rodio::Sink::new(&self.device); - - let format = &stream_info.stream_format; - let decoder = Mp3Decoder::new( - if format.is_stereo { 2 } else { 1 }, - format.sample_rate.into(), - stream_tag_reader(clip_data), - ); - - let stream = AudioStream { - clip_id, - info: stream_info.clone(), - sink, - }; - stream.sink.append(DecoderSource(Box::new(decoder))); - self.streams.insert(stream) - } - - fn play_sound(&mut self, sound: SoundHandle) { - let sound = &self.sounds[sound]; - use swf::AudioCompression; - - match sound.format.compression { - AudioCompression::Uncompressed => { - let mut data = Vec::with_capacity(sound.data.len() / 2); - let mut i = 0; - while i < sound.data.len() { - let val = i16::from(sound.data[i]) | (i16::from(sound.data[i + 1]) << 8); - data.push(val); - i += 2; - } - let buffer = rodio::buffer::SamplesBuffer::new( - if sound.format.is_stereo { 2 } else { 1 }, - sound.format.sample_rate.into(), - data, - ); - let sink = rodio::Sink::new(&self.device); - sink.append(buffer); - self.active_sounds.insert(sink); - } - AudioCompression::Adpcm => { - let decoder = AdpcmDecoder::new( - Cursor::new(sound.data.to_vec()), - sound.format.is_stereo, - sound.format.sample_rate, - ) - .unwrap(); - let sink = rodio::Sink::new(&self.device); - sink.append(DecoderSource(Box::new(decoder))); - self.active_sounds.insert(sink); - } - AudioCompression::Mp3 => { - let decoder = Mp3Decoder::new( - if sound.format.is_stereo { 2 } else { 1 }, - sound.format.sample_rate.into(), - Cursor::new(sound.data.to_vec()), - ); - let sink = rodio::Sink::new(&self.device); - sink.append(DecoderSource(Box::new(decoder))); - self.active_sounds.insert(sink); - } - _ => unimplemented!(), - } - } - - fn tick(&mut self) { - self.active_sounds.retain(|_, sink| !sink.empty()); - } -} - -struct DecoderSource(Box); - -impl Iterator for DecoderSource { - type Item = i16; - - #[inline] - fn next(&mut self) -> Option { - self.0.next() - } -} -impl rodio::Source for DecoderSource { - #[inline] - fn current_frame_len(&self) -> Option { - None - } - - #[inline] - fn channels(&self) -> u16 { - self.0.num_channels().into() - } - - #[inline] - fn sample_rate(&self) -> u32 { - self.0.sample_rate().into() - } - - #[inline] - fn total_duration(&self) -> Option { - None - } -} +use generational_arena::Arena; +use ruffle_core::backend::audio::decoders::{stream_tag_reader, AdpcmDecoder, Decoder, Mp3Decoder}; +use ruffle_core::backend::audio::{swf, AudioBackend, AudioStreamHandle, SoundHandle}; +use std::io::Cursor; +use std::sync::Arc; + +pub struct RodioAudioBackend { + sounds: Arena, + active_sounds: Arena, + streams: Arena, + device: rodio::Device, +} + +#[allow(dead_code)] +struct AudioStream { + clip_id: swf::CharacterId, + info: swf::SoundStreamHead, + sink: rodio::Sink, +} + +#[allow(dead_code)] +struct Sound { + format: swf::SoundFormat, + data: Arc>, +} + +impl RodioAudioBackend { + pub fn new() -> Result> { + Ok(Self { + sounds: Arena::new(), + streams: Arena::new(), + active_sounds: Arena::new(), + device: rodio::default_output_device().ok_or("Unable to create output device")?, + }) + } +} + +impl AudioBackend for RodioAudioBackend { + fn register_sound( + &mut self, + swf_sound: &swf::Sound, + ) -> Result> { + let sound = Sound { + format: swf_sound.format.clone(), + data: Arc::new(swf_sound.data.clone()), + }; + Ok(self.sounds.insert(sound)) + } + + fn start_stream( + &mut self, + clip_id: swf::CharacterId, + clip_data: ruffle_core::tag_utils::SwfSlice, + stream_info: &swf::SoundStreamHead, + ) -> AudioStreamHandle { + let sink = rodio::Sink::new(&self.device); + + let format = &stream_info.stream_format; + let decoder = Mp3Decoder::new( + if format.is_stereo { 2 } else { 1 }, + format.sample_rate.into(), + stream_tag_reader(clip_data), + ); + + let stream = AudioStream { + clip_id, + info: stream_info.clone(), + sink, + }; + stream.sink.append(DecoderSource(Box::new(decoder))); + self.streams.insert(stream) + } + + fn play_sound(&mut self, sound: SoundHandle) { + let sound = &self.sounds[sound]; + use swf::AudioCompression; + + match sound.format.compression { + AudioCompression::Uncompressed => { + let mut data = Vec::with_capacity(sound.data.len() / 2); + let mut i = 0; + while i < sound.data.len() { + let val = i16::from(sound.data[i]) | (i16::from(sound.data[i + 1]) << 8); + data.push(val); + i += 2; + } + let buffer = rodio::buffer::SamplesBuffer::new( + if sound.format.is_stereo { 2 } else { 1 }, + sound.format.sample_rate.into(), + data, + ); + let sink = rodio::Sink::new(&self.device); + sink.append(buffer); + self.active_sounds.insert(sink); + } + AudioCompression::Adpcm => { + let decoder = AdpcmDecoder::new( + Cursor::new(sound.data.to_vec()), + sound.format.is_stereo, + sound.format.sample_rate, + ) + .unwrap(); + let sink = rodio::Sink::new(&self.device); + sink.append(DecoderSource(Box::new(decoder))); + self.active_sounds.insert(sink); + } + AudioCompression::Mp3 => { + let decoder = Mp3Decoder::new( + if sound.format.is_stereo { 2 } else { 1 }, + sound.format.sample_rate.into(), + Cursor::new(sound.data.to_vec()), + ); + let sink = rodio::Sink::new(&self.device); + sink.append(DecoderSource(Box::new(decoder))); + self.active_sounds.insert(sink); + } + _ => unimplemented!(), + } + } + + fn tick(&mut self) { + self.active_sounds.retain(|_, sink| !sink.empty()); + } +} + +struct DecoderSource(Box); + +impl Iterator for DecoderSource { + type Item = i16; + + #[inline] + fn next(&mut self) -> Option { + self.0.next() + } +} +impl rodio::Source for DecoderSource { + #[inline] + fn current_frame_len(&self) -> Option { + None + } + + #[inline] + fn channels(&self) -> u16 { + self.0.num_channels().into() + } + + #[inline] + fn sample_rate(&self) -> u32 { + self.0.sample_rate().into() + } + + #[inline] + fn total_duration(&self) -> Option { + None + } +} diff --git a/desktop/src/render.rs b/desktop/src/render.rs index e0dfe21ab..b28f57e04 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -1,1159 +1,1159 @@ -#![allow(clippy::invalid_ref)] - -use glium::uniforms::{Sampler, UniformValue, Uniforms}; -use glium::{draw_parameters::DrawParameters, implement_vertex, uniform, Display, Frame, Surface}; -use glutin::WindowedContext; -use lyon::tessellation::geometry_builder::{BuffersBuilder, VertexBuffers}; -use lyon::{ - path::PathEvent, tessellation, tessellation::FillTessellator, tessellation::StrokeTessellator, -}; -use ruffle_core::backend::render::swf::{self, FillStyle}; -use ruffle_core::backend::render::{ - BitmapHandle, Color, Letterbox, RenderBackend, ShapeHandle, Transform, -}; -use ruffle_core::shape_utils::{DrawCommand, DrawPath}; -use swf::Twips; - -pub struct GliumRenderBackend { - display: Display, - target: Option, - shader_program: glium::Program, - gradient_shader_program: glium::Program, - bitmap_shader_program: glium::Program, - meshes: Vec, - textures: Vec<(swf::CharacterId, Texture)>, - viewport_width: f32, - viewport_height: f32, - view_matrix: [[f32; 4]; 4], -} - -impl GliumRenderBackend { - pub fn new( - windowed_context: WindowedContext, - ) -> Result> { - let display = Display::from_gl_window(windowed_context)?; - - use glium::program::ProgramCreationInput; - let shader_program = glium::Program::new( - &display, - ProgramCreationInput::SourceCode { - vertex_shader: VERTEX_SHADER, - fragment_shader: FRAGMENT_SHADER, - geometry_shader: None, - tessellation_control_shader: None, - tessellation_evaluation_shader: None, - transform_feedback_varyings: None, - outputs_srgb: true, - uses_point_size: false, - }, - )?; - - let gradient_shader_program = glium::Program::new( - &display, - ProgramCreationInput::SourceCode { - vertex_shader: TEXTURE_VERTEX_SHADER, - fragment_shader: GRADIENT_FRAGMENT_SHADER, - geometry_shader: None, - tessellation_control_shader: None, - tessellation_evaluation_shader: None, - transform_feedback_varyings: None, - outputs_srgb: true, - uses_point_size: false, - }, - )?; - - let bitmap_shader_program = glium::Program::new( - &display, - ProgramCreationInput::SourceCode { - vertex_shader: TEXTURE_VERTEX_SHADER, - fragment_shader: BITMAP_FRAGMENT_SHADER, - geometry_shader: None, - tessellation_control_shader: None, - tessellation_evaluation_shader: None, - transform_feedback_varyings: None, - outputs_srgb: true, - uses_point_size: false, - }, - )?; - - let mut renderer = GliumRenderBackend { - display, - shader_program, - gradient_shader_program, - bitmap_shader_program, - target: None, - meshes: vec![], - textures: vec![], - viewport_width: 500.0, - viewport_height: 500.0, - view_matrix: [[0.0; 4]; 4], - }; - renderer.build_matrices(); - Ok(renderer) - } - - pub fn display(&self) -> &Display { - &self.display - } - - fn register_shape_internal(&mut self, shape: &swf::Shape) -> ShapeHandle { - let handle = ShapeHandle(self.meshes.len()); - let paths = ruffle_core::shape_utils::swf_shape_to_paths(shape); - - use lyon::tessellation::{FillOptions, StrokeOptions}; - - let mut mesh = Mesh { draws: vec![] }; - - //let mut vertices: Vec = vec![]; - //let mut indices: Vec = vec![]; - - let mut fill_tess = FillTessellator::new(); - let mut stroke_tess = StrokeTessellator::new(); - let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); - - fn flush_draw( - draw: DrawType, - mesh: &mut Mesh, - lyon_mesh: &mut VertexBuffers, - display: &Display, - ) { - if lyon_mesh.vertices.is_empty() { - return; - } - - let vertex_buffer = glium::VertexBuffer::new(display, &lyon_mesh.vertices[..]).unwrap(); - - let index_buffer = glium::IndexBuffer::new( - display, - glium::index::PrimitiveType::TrianglesList, - &lyon_mesh.indices[..], - ) - .unwrap(); - - mesh.draws.push(Draw { - draw_type: draw, - vertex_buffer, - index_buffer, - }); - - *lyon_mesh = VertexBuffers::new(); - } - - for path in paths { - match path { - DrawPath::Fill { style, commands } => match style { - FillStyle::Color(color) => { - let color = [ - f32::from(color.r) / 255.0, - f32::from(color.g) / 255.0, - f32::from(color.b) / 255.0, - f32::from(color.a) / 255.0, - ]; - - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color, - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - if let Err(e) = fill_tess.tessellate_path( - ruffle_path_to_lyon_path(commands, true), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - println!("Failure"); - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - } - FillStyle::LinearGradient(gradient) => { - flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); - - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color: [1.0, 1.0, 1.0, 1.0], - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - if let Err(e) = fill_tess.tessellate_path( - ruffle_path_to_lyon_path(commands, true), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - println!("Failure"); - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - - let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); - let mut ratios: Vec = Vec::with_capacity(8); - for record in &gradient.records { - colors.push([ - f32::from(record.color.r) / 255.0, - f32::from(record.color.g) / 255.0, - f32::from(record.color.b) / 255.0, - f32::from(record.color.a) / 255.0, - ]); - ratios.push(f32::from(record.ratio) / 255.0); - } - - let uniforms = GradientUniforms { - gradient_type: 0, - ratios, - colors, - num_colors: gradient.records.len() as u32, - matrix: swf_to_gl_matrix(gradient.matrix.clone()), - repeat_mode: 0, - focal_point: 0.0, - }; - - flush_draw( - DrawType::Gradient(uniforms), - &mut mesh, - &mut lyon_mesh, - &self.display, - ); - } - FillStyle::RadialGradient(gradient) => { - flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); - - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color: [1.0, 1.0, 1.0, 1.0], - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - if let Err(e) = fill_tess.tessellate_path( - ruffle_path_to_lyon_path(commands, true), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - println!("Failure"); - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - - let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); - let mut ratios: Vec = Vec::with_capacity(8); - for record in &gradient.records { - colors.push([ - f32::from(record.color.r) / 255.0, - f32::from(record.color.g) / 255.0, - f32::from(record.color.b) / 255.0, - f32::from(record.color.a) / 255.0, - ]); - ratios.push(f32::from(record.ratio) / 255.0); - } - - let uniforms = GradientUniforms { - gradient_type: 1, - ratios, - colors, - num_colors: gradient.records.len() as u32, - matrix: swf_to_gl_matrix(gradient.matrix.clone()), - repeat_mode: 0, - focal_point: 0.0, - }; - - flush_draw( - DrawType::Gradient(uniforms), - &mut mesh, - &mut lyon_mesh, - &self.display, - ); - } - FillStyle::FocalGradient { - gradient, - focal_point, - } => { - flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); - - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color: [1.0, 1.0, 1.0, 1.0], - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - if let Err(e) = fill_tess.tessellate_path( - ruffle_path_to_lyon_path(commands, true), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - println!("Failure"); - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - - let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); - let mut ratios: Vec = Vec::with_capacity(8); - for record in &gradient.records { - colors.push([ - f32::from(record.color.r) / 255.0, - f32::from(record.color.g) / 255.0, - f32::from(record.color.b) / 255.0, - f32::from(record.color.a) / 255.0, - ]); - ratios.push(f32::from(record.ratio) / 255.0); - } - - let uniforms = GradientUniforms { - gradient_type: 1, - ratios, - colors, - num_colors: gradient.records.len() as u32, - matrix: swf_to_gl_matrix(gradient.matrix.clone()), - repeat_mode: 0, - focal_point: *focal_point, - }; - - flush_draw( - DrawType::Gradient(uniforms), - &mut mesh, - &mut lyon_mesh, - &self.display, - ); - } - FillStyle::Bitmap { - id, - matrix, - is_smoothed, - is_repeating, - } => { - flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); - - let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color: [1.0, 1.0, 1.0, 1.0], - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - if let Err(e) = fill_tess.tessellate_path( - ruffle_path_to_lyon_path(commands, true), - &FillOptions::even_odd(), - &mut buffers_builder, - ) { - println!("Failure"); - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - - let texture = &self - .textures - .iter() - .find(|(other_id, _tex)| *other_id == *id) - .unwrap() - .1; - - let uniforms = BitmapUniforms { - matrix: swf_bitmap_to_gl_matrix( - matrix.clone(), - texture.width, - texture.height, - ), - id: *id, - }; - - flush_draw( - DrawType::Bitmap { - uniforms, - is_smoothed: *is_smoothed, - is_repeating: *is_repeating, - }, - &mut mesh, - &mut lyon_mesh, - &self.display, - ); - } - }, - DrawPath::Stroke { - style, - commands, - is_closed, - } => { - let color = [ - f32::from(style.color.r) / 255.0, - f32::from(style.color.g) / 255.0, - f32::from(style.color.b) / 255.0, - f32::from(style.color.a) / 255.0, - ]; - - let vertex_ctor = move |vertex: tessellation::StrokeVertex| Vertex { - position: [vertex.position.x, vertex.position.y], - color, - }; - let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); - - // TODO(Herschel): 0 width indicates "hairline". - let width = if style.width.to_pixels() >= 1.0 { - style.width.to_pixels() as f32 - } else { - 1.0 - }; - - let mut options = StrokeOptions::default() - .with_line_width(width) - .with_line_join(match style.join_style { - swf::LineJoinStyle::Round => tessellation::LineJoin::Round, - swf::LineJoinStyle::Bevel => tessellation::LineJoin::Bevel, - swf::LineJoinStyle::Miter(_) => tessellation::LineJoin::MiterClip, - }) - .with_start_cap(match style.start_cap { - swf::LineCapStyle::None => tessellation::LineCap::Butt, - swf::LineCapStyle::Round => tessellation::LineCap::Round, - swf::LineCapStyle::Square => tessellation::LineCap::Square, - }) - .with_end_cap(match style.end_cap { - swf::LineCapStyle::None => tessellation::LineCap::Butt, - swf::LineCapStyle::Round => tessellation::LineCap::Round, - swf::LineCapStyle::Square => tessellation::LineCap::Square, - }); - - if let swf::LineJoinStyle::Miter(limit) = style.join_style { - options = options.with_miter_limit(limit); - } - - if let Err(e) = stroke_tess.tessellate_path( - ruffle_path_to_lyon_path(commands, is_closed), - &options, - &mut buffers_builder, - ) { - log::error!("Tessellation failure: {:?}", e); - self.meshes.push(mesh); - return handle; - } - } - } - } - - flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); - - self.meshes.push(mesh); - - handle - } - - fn build_matrices(&mut self) { - self.view_matrix = [ - [1.0 / (self.viewport_width as f32 / 2.0), 0.0, 0.0, 0.0], - [0.0, -1.0 / (self.viewport_height as f32 / 2.0), 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [-1.0, 1.0, 0.0, 1.0], - ]; - } -} - -impl RenderBackend for GliumRenderBackend { - fn set_viewport_dimensions(&mut self, width: u32, height: u32) { - self.viewport_width = width as f32; - self.viewport_height = height as f32; - self.build_matrices(); - } - - fn register_shape(&mut self, shape: &swf::Shape) -> ShapeHandle { - self.register_shape_internal(shape) - } - - fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { - let shape = swf::Shape { - version: 2, - id: 0, - shape_bounds: Default::default(), - edge_bounds: Default::default(), - has_fill_winding_rule: false, - has_non_scaling_strokes: false, - has_scaling_strokes: true, - styles: swf::ShapeStyles { - fill_styles: vec![FillStyle::Color(Color { - r: 255, - g: 255, - b: 255, - a: 255, - })], - line_styles: vec![], - }, - shape: glyph.shape_records.clone(), - }; - self.register_shape_internal(&shape) - } - - fn register_bitmap_jpeg( - &mut self, - id: swf::CharacterId, - data: &[u8], - jpeg_tables: &[u8], - ) -> BitmapHandle { - if !jpeg_tables.is_empty() { - let mut full_jpeg = jpeg_tables[..jpeg_tables.len() - 2].to_vec(); - full_jpeg.extend_from_slice(&data[2..]); - - self.register_bitmap_jpeg_2(id, &full_jpeg[..]) - } else { - self.register_bitmap_jpeg_2(id, &data[..]) - } - } - - fn register_bitmap_jpeg_2(&mut self, id: swf::CharacterId, data: &[u8]) -> BitmapHandle { - let data = ruffle_core::backend::render::remove_invalid_jpeg_data(data); - - let mut decoder = jpeg_decoder::Decoder::new(&data[..]); - decoder.read_info().unwrap(); - let metadata = decoder.info().unwrap(); - let decoded_data = decoder.decode().expect("failed to decode image"); - let image = glium::texture::RawImage2d::from_raw_rgb( - decoded_data, - (metadata.width.into(), metadata.height.into()), - ); - - let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); - - let handle = BitmapHandle(self.textures.len()); - self.textures.push(( - id, - Texture { - texture, - width: metadata.width.into(), - height: metadata.height.into(), - }, - )); - - handle - } - - fn register_bitmap_jpeg_3( - &mut self, - id: swf::CharacterId, - jpeg_data: &[u8], - alpha_data: &[u8], - ) -> BitmapHandle { - let (width, height, rgba) = - ruffle_core::backend::render::define_bits_jpeg_to_rgba(jpeg_data, alpha_data) - .expect("Error decoding DefineBitsJPEG3"); - - let image = glium::texture::RawImage2d::from_raw_rgba(rgba, (width, height)); - let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); - let handle = BitmapHandle(self.textures.len()); - self.textures.push(( - id, - Texture { - texture, - width, - height, - }, - )); - - handle - } - - fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { - let decoded_data = ruffle_core::backend::render::define_bits_lossless_to_rgba(swf_tag) - .expect("Error decoding DefineBitsLossless"); - - let image = glium::texture::RawImage2d::from_raw_rgba( - decoded_data, - (swf_tag.width.into(), swf_tag.height.into()), - ); - - let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); - - let handle = BitmapHandle(self.textures.len()); - self.textures.push(( - swf_tag.id, - Texture { - texture, - width: swf_tag.width.into(), - height: swf_tag.height.into(), - }, - )); - - handle - } - - fn begin_frame(&mut self) { - assert!(self.target.is_none()); - self.target = Some(self.display.draw()); - } - - fn end_frame(&mut self) { - assert!(self.target.is_some()); - - let target = self.target.take().unwrap(); - target.finish().unwrap(); - } - - fn clear(&mut self, color: Color) { - let target = self.target.as_mut().unwrap(); - target.clear_color_srgb( - f32::from(color.r) / 255.0, - f32::from(color.g) / 255.0, - f32::from(color.b) / 255.0, - f32::from(color.a) / 255.0, - ); - } - - fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) { - let target = self.target.as_mut().unwrap(); - - let mesh = &self.meshes[shape.0]; - - let world_matrix = [ - [transform.matrix.a, transform.matrix.b, 0.0, 0.0], - [transform.matrix.c, transform.matrix.d, 0.0, 0.0], - [0.0, 0.0, 1.0, 0.0], - [ - transform.matrix.tx / 20.0, - transform.matrix.ty / 20.0, - 0.0, - 1.0, - ], - ]; - - let mult_color = [ - transform.color_transform.r_mult, - transform.color_transform.g_mult, - transform.color_transform.b_mult, - transform.color_transform.a_mult, - ]; - - let add_color = [ - transform.color_transform.r_add, - transform.color_transform.g_add, - transform.color_transform.b_add, - transform.color_transform.a_add, - ]; - - for draw in &mesh.draws { - match &draw.draw_type { - DrawType::Color => { - target - .draw( - &draw.vertex_buffer, - &draw.index_buffer, - &self.shader_program, - &uniform! { view_matrix: self.view_matrix, world_matrix: world_matrix, mult_color: mult_color, add_color: add_color }, - &color_draw_parameters() - ) - .unwrap(); - } - DrawType::Gradient(gradient_uniforms) => { - let uniforms = GradientUniformsFull { - view_matrix: self.view_matrix, - world_matrix, - mult_color, - add_color, - gradient: gradient_uniforms.clone(), - }; - - target - .draw( - &draw.vertex_buffer, - &draw.index_buffer, - &self.gradient_shader_program, - &uniforms, - &color_draw_parameters(), - ) - .unwrap(); - } - DrawType::Bitmap { - uniforms, - is_smoothed, - is_repeating, - } => { - let texture = &self - .textures - .iter() - .find(|(id, _tex)| *id == uniforms.id) - .unwrap() - .1; - - // Set texture sampler smooth/repeat parameters. - use glium::uniforms::{ - MagnifySamplerFilter, MinifySamplerFilter, SamplerWrapFunction, - }; - let texture = &texture - .texture - .sampled() - .magnify_filter(if *is_smoothed { - MagnifySamplerFilter::Linear - } else { - MagnifySamplerFilter::Nearest - }) - .minify_filter(if *is_smoothed { - MinifySamplerFilter::LinearMipmapLinear - } else { - MinifySamplerFilter::Nearest - }) - .wrap_function(if *is_repeating { - SamplerWrapFunction::Repeat - } else { - SamplerWrapFunction::Clamp - }); - - let uniforms = BitmapUniformsFull { - view_matrix: self.view_matrix, - world_matrix, - mult_color, - add_color, - matrix: uniforms.matrix, - texture, - }; - - target - .draw( - &draw.vertex_buffer, - &draw.index_buffer, - &self.bitmap_shader_program, - &uniforms, - &bitmap_draw_parameters(), - ) - .unwrap(); - } - } - } - } - - fn draw_pause_overlay(&mut self) {} - - fn draw_letterbox(&mut self, letterbox: Letterbox) { - let target = self.target.as_mut().unwrap(); - let black = Some((0.0, 0.0, 0.0, 1.0)); - match letterbox { - Letterbox::None => (), - Letterbox::Letterbox(margin_height) => { - target.clear( - Some(&glium::Rect { - left: 0, - bottom: 0, - width: self.viewport_width as u32, - height: margin_height as u32, - }), - black, - true, - None, - None, - ); - target.clear( - Some(&glium::Rect { - left: 0, - bottom: (self.viewport_height - margin_height) as u32, - width: self.viewport_width as u32, - height: margin_height as u32, - }), - black, - true, - None, - None, - ); - } - Letterbox::Pillarbox(margin_width) => { - target.clear( - Some(&glium::Rect { - left: 0, - bottom: 0, - width: margin_width as u32, - height: self.viewport_height as u32, - }), - black, - true, - None, - None, - ); - target.clear( - Some(&glium::Rect { - left: (self.viewport_width - margin_width) as u32, - bottom: 0, - width: margin_width as u32, - height: self.viewport_height as u32, - }), - black, - true, - None, - None, - ); - } - } - } -} - -struct Texture { - width: u32, - height: u32, - texture: glium::Texture2d, -} - -#[derive(Copy, Clone, Debug)] -struct Vertex { - position: [f32; 2], - color: [f32; 4], -} - -implement_vertex!(Vertex, position, color); - -#[derive(Clone, Debug)] -struct GradientUniforms { - matrix: [[f32; 3]; 3], - gradient_type: i32, - ratios: Vec, - colors: Vec<[f32; 4]>, - num_colors: u32, - repeat_mode: i32, - focal_point: f32, -} - -impl Uniforms for GradientUniforms { - fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { - visit("u_matrix", UniformValue::Mat3(self.matrix)); - visit( - "u_gradient_type", - UniformValue::SignedInt(self.gradient_type), - ); - for i in 0..self.num_colors as usize { - visit( - &format!("u_ratios[{}]", i)[..], - UniformValue::Float(self.ratios[i]), - ); - visit( - &format!("u_colors[{}]", i)[..], - UniformValue::Vec4(self.colors[i]), - ); - } - visit("u_num_colors", UniformValue::UnsignedInt(self.num_colors)); - visit("u_repeat_mode", UniformValue::SignedInt(self.repeat_mode)); - visit("u_focal_point", UniformValue::Float(self.focal_point)); - } -} - -#[derive(Clone, Debug)] -struct GradientUniformsFull { - world_matrix: [[f32; 4]; 4], - view_matrix: [[f32; 4]; 4], - mult_color: [f32; 4], - add_color: [f32; 4], - gradient: GradientUniforms, -} - -impl Uniforms for GradientUniformsFull { - fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { - visit("world_matrix", UniformValue::Mat4(self.world_matrix)); - visit("view_matrix", UniformValue::Mat4(self.view_matrix)); - visit("mult_color", UniformValue::Vec4(self.mult_color)); - visit("add_color", UniformValue::Vec4(self.add_color)); - self.gradient.visit_values(visit); - } -} - -#[derive(Clone, Debug)] -struct BitmapUniforms { - matrix: [[f32; 3]; 3], - id: swf::CharacterId, -} - -impl Uniforms for BitmapUniforms { - fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { - visit("u_matrix", UniformValue::Mat3(self.matrix)); - } -} - -#[derive(Clone, Debug)] -struct BitmapUniformsFull<'a> { - world_matrix: [[f32; 4]; 4], - view_matrix: [[f32; 4]; 4], - mult_color: [f32; 4], - add_color: [f32; 4], - matrix: [[f32; 3]; 3], - texture: &'a Sampler<'a, glium::Texture2d>, -} - -impl<'a> Uniforms for BitmapUniformsFull<'a> { - fn visit_values<'v, F: FnMut(&str, UniformValue<'v>)>(&'v self, mut visit: F) { - use glium::uniforms::AsUniformValue; - visit("world_matrix", UniformValue::Mat4(self.world_matrix)); - visit("view_matrix", UniformValue::Mat4(self.view_matrix)); - visit("mult_color", UniformValue::Vec4(self.mult_color)); - visit("add_color", UniformValue::Vec4(self.add_color)); - visit("u_matrix", UniformValue::Mat3(self.matrix)); - visit("u_texture", self.texture.as_uniform_value()); - } -} - -const VERTEX_SHADER: &str = r#" - #version 140 - - uniform mat4 view_matrix; - uniform mat4 world_matrix; - uniform vec4 mult_color; - uniform vec4 add_color; - - in vec2 position; - in vec4 color; - out vec4 frag_color; - - void main() { - frag_color = color * mult_color + add_color; - gl_Position = view_matrix * world_matrix * vec4(position, 0.0, 1.0); - } -"#; - -const FRAGMENT_SHADER: &str = r#" - #version 140 - in vec4 frag_color; - out vec4 out_color; - void main() { - out_color = frag_color; - } -"#; - -const TEXTURE_VERTEX_SHADER: &str = r#" - #version 140 - - uniform mat4 view_matrix; - uniform mat4 world_matrix; - - uniform mat3 u_matrix; - - in vec2 position; - in vec4 color; - out vec2 frag_uv; - - void main() { - frag_uv = vec2(u_matrix * vec3(position, 1.0)); - gl_Position = view_matrix * world_matrix * vec4(position, 0.0, 1.0); - } -"#; - -const GRADIENT_FRAGMENT_SHADER: &str = r#" -#version 140 - uniform vec4 mult_color; - uniform vec4 add_color; - - uniform int u_gradient_type; - uniform float u_ratios[8]; - uniform vec4 u_colors[8]; - uniform uint u_num_colors; - uniform int u_repeat_mode; - uniform float u_focal_point; - - in vec2 frag_uv; - out vec4 out_color; - - void main() { - vec4 color; - int last = int(int(u_num_colors) - 1); - float t; - if( u_gradient_type == 0 ) - { - t = frag_uv.x; - } - else if( u_gradient_type == 1 ) - { - t = length(frag_uv * 2.0 - 1.0); - } - else if( u_gradient_type == 2 ) - { - vec2 uv = frag_uv * 2.0 - 1.0; - vec2 d = vec2(u_focal_point, 0.0) - uv; - float l = length(d); - d /= l; - t = l / (sqrt(1.0 - u_focal_point*u_focal_point*d.y*d.y) + u_focal_point*d.x); - } - if( u_repeat_mode == 0 ) - { - // Clamp - t = clamp(t, 0.0, 1.0); - } - else if( u_repeat_mode == 1 ) - { - // Repeat - t = fract(t); - } - else - { - // Mirror - if( t < 0.0 ) - { - t = -t; - } - if( (int(t)&1) == 0 ) { - t = fract(t); - } else { - t = 1.0 - fract(t); - } - } - int i = 0; - int j = 1; - while( t > u_ratios[j] ) - { - i = j; - j++; - } - float a = (t - u_ratios[i]) / (u_ratios[j] - u_ratios[i]); - color = mix(u_colors[i], u_colors[j], a); - out_color = mult_color * color + add_color; - } -"#; - -const BITMAP_FRAGMENT_SHADER: &str = r#" -#version 140 - uniform vec4 mult_color; - uniform vec4 add_color; - - in vec2 frag_uv; - out vec4 out_color; - - uniform sampler2D u_texture; - - void main() { - vec4 color = texture(u_texture, frag_uv); - out_color = mult_color * color + add_color; - } -"#; - -struct Mesh { - draws: Vec, -} - -struct Draw { - draw_type: DrawType, - vertex_buffer: glium::VertexBuffer, - index_buffer: glium::IndexBuffer, -} - -enum DrawType { - Color, - Gradient(GradientUniforms), - Bitmap { - uniforms: BitmapUniforms, - is_smoothed: bool, - is_repeating: bool, - }, -} - -fn point(x: Twips, y: Twips) -> lyon::math::Point { - lyon::math::Point::new(x.to_pixels() as f32, y.to_pixels() as f32) -} - -fn ruffle_path_to_lyon_path( - commands: Vec, - mut is_closed: bool, -) -> impl Iterator { - use lyon::geom::{LineSegment, QuadraticBezierSegment}; - - let mut cur = lyon::math::Point::new(0.0, 0.0); - let mut i = commands.into_iter(); - std::iter::from_fn(move || match i.next() { - Some(DrawCommand::MoveTo { x, y }) => { - cur = point(x, y); - Some(PathEvent::MoveTo(cur)) - } - Some(DrawCommand::LineTo { x, y }) => { - let next = point(x, y); - let cmd = PathEvent::Line(LineSegment { - from: cur, - to: next, - }); - cur = next; - Some(cmd) - } - Some(DrawCommand::CurveTo { x1, y1, x2, y2 }) => { - let next = point(x2, y2); - let cmd = PathEvent::Quadratic(QuadraticBezierSegment { - from: cur, - ctrl: point(x1, y1), - to: next, - }); - cur = next; - Some(cmd) - } - None => { - if is_closed { - is_closed = false; - Some(PathEvent::Close(LineSegment { from: cur, to: cur })) - } else { - None - } - } - }) -} - -#[allow(clippy::many_single_char_names)] -fn swf_to_gl_matrix(m: swf::Matrix) -> [[f32; 3]; 3] { - let tx = m.translate_x.get() as f32; - let ty = m.translate_y.get() as f32; - let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; - let mut a = m.scale_y / det; - let mut b = -m.rotate_skew_1 / det; - let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; - let mut d = -m.rotate_skew_0 / det; - let mut e = m.scale_x / det; - let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; - - a *= 20.0 / 32768.0; - b *= 20.0 / 32768.0; - d *= 20.0 / 32768.0; - e *= 20.0 / 32768.0; - - c /= 32768.0; - f /= 32768.0; - c += 0.5; - f += 0.5; - [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] -} - -#[allow(clippy::many_single_char_names)] -fn swf_bitmap_to_gl_matrix(m: swf::Matrix, bitmap_width: u32, bitmap_height: u32) -> [[f32; 3]; 3] { - let bitmap_width = bitmap_width as f32; - let bitmap_height = bitmap_height as f32; - - let tx = m.translate_x.get() as f32; - let ty = m.translate_y.get() as f32; - let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; - let mut a = m.scale_y / det; - let mut b = -m.rotate_skew_1 / det; - let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; - let mut d = -m.rotate_skew_0 / det; - let mut e = m.scale_x / det; - let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; - - a *= 20.0 / bitmap_width; - b *= 20.0 / bitmap_width; - d *= 20.0 / bitmap_height; - e *= 20.0 / bitmap_height; - - c /= bitmap_width; - f /= bitmap_height; - - [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] -} - -/// Returns the drawing parameters for standard color/gradient fills. -#[inline] -fn color_draw_parameters() -> DrawParameters<'static> { - DrawParameters { - blend: glium::Blend::alpha_blending(), - ..Default::default() - } -} - -/// Returns the drawing parameters for bitmaps with pre-multipled alpha. -#[inline] -fn bitmap_draw_parameters() -> DrawParameters<'static> { - use glium::{BlendingFunction, LinearBlendingFactor}; - DrawParameters { - blend: glium::Blend { - color: BlendingFunction::Addition { - source: LinearBlendingFactor::One, - destination: LinearBlendingFactor::OneMinusSourceAlpha, - }, - alpha: BlendingFunction::Addition { - source: LinearBlendingFactor::SourceAlpha, - destination: LinearBlendingFactor::OneMinusSourceAlpha, - }, - ..Default::default() - }, - ..Default::default() - } -} +#![allow(clippy::invalid_ref)] + +use glium::uniforms::{Sampler, UniformValue, Uniforms}; +use glium::{draw_parameters::DrawParameters, implement_vertex, uniform, Display, Frame, Surface}; +use glutin::WindowedContext; +use lyon::tessellation::geometry_builder::{BuffersBuilder, VertexBuffers}; +use lyon::{ + path::PathEvent, tessellation, tessellation::FillTessellator, tessellation::StrokeTessellator, +}; +use ruffle_core::backend::render::swf::{self, FillStyle}; +use ruffle_core::backend::render::{ + BitmapHandle, Color, Letterbox, RenderBackend, ShapeHandle, Transform, +}; +use ruffle_core::shape_utils::{DrawCommand, DrawPath}; +use swf::Twips; + +pub struct GliumRenderBackend { + display: Display, + target: Option, + shader_program: glium::Program, + gradient_shader_program: glium::Program, + bitmap_shader_program: glium::Program, + meshes: Vec, + textures: Vec<(swf::CharacterId, Texture)>, + viewport_width: f32, + viewport_height: f32, + view_matrix: [[f32; 4]; 4], +} + +impl GliumRenderBackend { + pub fn new( + windowed_context: WindowedContext, + ) -> Result> { + let display = Display::from_gl_window(windowed_context)?; + + use glium::program::ProgramCreationInput; + let shader_program = glium::Program::new( + &display, + ProgramCreationInput::SourceCode { + vertex_shader: VERTEX_SHADER, + fragment_shader: FRAGMENT_SHADER, + geometry_shader: None, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + transform_feedback_varyings: None, + outputs_srgb: true, + uses_point_size: false, + }, + )?; + + let gradient_shader_program = glium::Program::new( + &display, + ProgramCreationInput::SourceCode { + vertex_shader: TEXTURE_VERTEX_SHADER, + fragment_shader: GRADIENT_FRAGMENT_SHADER, + geometry_shader: None, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + transform_feedback_varyings: None, + outputs_srgb: true, + uses_point_size: false, + }, + )?; + + let bitmap_shader_program = glium::Program::new( + &display, + ProgramCreationInput::SourceCode { + vertex_shader: TEXTURE_VERTEX_SHADER, + fragment_shader: BITMAP_FRAGMENT_SHADER, + geometry_shader: None, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + transform_feedback_varyings: None, + outputs_srgb: true, + uses_point_size: false, + }, + )?; + + let mut renderer = GliumRenderBackend { + display, + shader_program, + gradient_shader_program, + bitmap_shader_program, + target: None, + meshes: vec![], + textures: vec![], + viewport_width: 500.0, + viewport_height: 500.0, + view_matrix: [[0.0; 4]; 4], + }; + renderer.build_matrices(); + Ok(renderer) + } + + pub fn display(&self) -> &Display { + &self.display + } + + fn register_shape_internal(&mut self, shape: &swf::Shape) -> ShapeHandle { + let handle = ShapeHandle(self.meshes.len()); + let paths = ruffle_core::shape_utils::swf_shape_to_paths(shape); + + use lyon::tessellation::{FillOptions, StrokeOptions}; + + let mut mesh = Mesh { draws: vec![] }; + + //let mut vertices: Vec = vec![]; + //let mut indices: Vec = vec![]; + + let mut fill_tess = FillTessellator::new(); + let mut stroke_tess = StrokeTessellator::new(); + let mut lyon_mesh: VertexBuffers<_, u32> = VertexBuffers::new(); + + fn flush_draw( + draw: DrawType, + mesh: &mut Mesh, + lyon_mesh: &mut VertexBuffers, + display: &Display, + ) { + if lyon_mesh.vertices.is_empty() { + return; + } + + let vertex_buffer = glium::VertexBuffer::new(display, &lyon_mesh.vertices[..]).unwrap(); + + let index_buffer = glium::IndexBuffer::new( + display, + glium::index::PrimitiveType::TrianglesList, + &lyon_mesh.indices[..], + ) + .unwrap(); + + mesh.draws.push(Draw { + draw_type: draw, + vertex_buffer, + index_buffer, + }); + + *lyon_mesh = VertexBuffers::new(); + } + + for path in paths { + match path { + DrawPath::Fill { style, commands } => match style { + FillStyle::Color(color) => { + let color = [ + f32::from(color.r) / 255.0, + f32::from(color.g) / 255.0, + f32::from(color.b) / 255.0, + f32::from(color.a) / 255.0, + ]; + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color, + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + } + FillStyle::LinearGradient(gradient) => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + let mut ratios: Vec = Vec::with_capacity(8); + for record in &gradient.records { + colors.push([ + f32::from(record.color.r) / 255.0, + f32::from(record.color.g) / 255.0, + f32::from(record.color.b) / 255.0, + f32::from(record.color.a) / 255.0, + ]); + ratios.push(f32::from(record.ratio) / 255.0); + } + + let uniforms = GradientUniforms { + gradient_type: 0, + ratios, + colors, + num_colors: gradient.records.len() as u32, + matrix: swf_to_gl_matrix(gradient.matrix.clone()), + repeat_mode: 0, + focal_point: 0.0, + }; + + flush_draw( + DrawType::Gradient(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + FillStyle::RadialGradient(gradient) => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + let mut ratios: Vec = Vec::with_capacity(8); + for record in &gradient.records { + colors.push([ + f32::from(record.color.r) / 255.0, + f32::from(record.color.g) / 255.0, + f32::from(record.color.b) / 255.0, + f32::from(record.color.a) / 255.0, + ]); + ratios.push(f32::from(record.ratio) / 255.0); + } + + let uniforms = GradientUniforms { + gradient_type: 1, + ratios, + colors, + num_colors: gradient.records.len() as u32, + matrix: swf_to_gl_matrix(gradient.matrix.clone()), + repeat_mode: 0, + focal_point: 0.0, + }; + + flush_draw( + DrawType::Gradient(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + FillStyle::FocalGradient { + gradient, + focal_point, + } => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let mut colors: Vec<[f32; 4]> = Vec::with_capacity(8); + let mut ratios: Vec = Vec::with_capacity(8); + for record in &gradient.records { + colors.push([ + f32::from(record.color.r) / 255.0, + f32::from(record.color.g) / 255.0, + f32::from(record.color.b) / 255.0, + f32::from(record.color.a) / 255.0, + ]); + ratios.push(f32::from(record.ratio) / 255.0); + } + + let uniforms = GradientUniforms { + gradient_type: 1, + ratios, + colors, + num_colors: gradient.records.len() as u32, + matrix: swf_to_gl_matrix(gradient.matrix.clone()), + repeat_mode: 0, + focal_point: *focal_point, + }; + + flush_draw( + DrawType::Gradient(uniforms), + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + FillStyle::Bitmap { + id, + matrix, + is_smoothed, + is_repeating, + } => { + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + let vertex_ctor = move |vertex: tessellation::FillVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color: [1.0, 1.0, 1.0, 1.0], + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + if let Err(e) = fill_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, true), + &FillOptions::even_odd(), + &mut buffers_builder, + ) { + println!("Failure"); + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + + let texture = &self + .textures + .iter() + .find(|(other_id, _tex)| *other_id == *id) + .unwrap() + .1; + + let uniforms = BitmapUniforms { + matrix: swf_bitmap_to_gl_matrix( + matrix.clone(), + texture.width, + texture.height, + ), + id: *id, + }; + + flush_draw( + DrawType::Bitmap { + uniforms, + is_smoothed: *is_smoothed, + is_repeating: *is_repeating, + }, + &mut mesh, + &mut lyon_mesh, + &self.display, + ); + } + }, + DrawPath::Stroke { + style, + commands, + is_closed, + } => { + let color = [ + f32::from(style.color.r) / 255.0, + f32::from(style.color.g) / 255.0, + f32::from(style.color.b) / 255.0, + f32::from(style.color.a) / 255.0, + ]; + + let vertex_ctor = move |vertex: tessellation::StrokeVertex| Vertex { + position: [vertex.position.x, vertex.position.y], + color, + }; + let mut buffers_builder = BuffersBuilder::new(&mut lyon_mesh, vertex_ctor); + + // TODO(Herschel): 0 width indicates "hairline". + let width = if style.width.to_pixels() >= 1.0 { + style.width.to_pixels() as f32 + } else { + 1.0 + }; + + let mut options = StrokeOptions::default() + .with_line_width(width) + .with_line_join(match style.join_style { + swf::LineJoinStyle::Round => tessellation::LineJoin::Round, + swf::LineJoinStyle::Bevel => tessellation::LineJoin::Bevel, + swf::LineJoinStyle::Miter(_) => tessellation::LineJoin::MiterClip, + }) + .with_start_cap(match style.start_cap { + swf::LineCapStyle::None => tessellation::LineCap::Butt, + swf::LineCapStyle::Round => tessellation::LineCap::Round, + swf::LineCapStyle::Square => tessellation::LineCap::Square, + }) + .with_end_cap(match style.end_cap { + swf::LineCapStyle::None => tessellation::LineCap::Butt, + swf::LineCapStyle::Round => tessellation::LineCap::Round, + swf::LineCapStyle::Square => tessellation::LineCap::Square, + }); + + if let swf::LineJoinStyle::Miter(limit) = style.join_style { + options = options.with_miter_limit(limit); + } + + if let Err(e) = stroke_tess.tessellate_path( + ruffle_path_to_lyon_path(commands, is_closed), + &options, + &mut buffers_builder, + ) { + log::error!("Tessellation failure: {:?}", e); + self.meshes.push(mesh); + return handle; + } + } + } + } + + flush_draw(DrawType::Color, &mut mesh, &mut lyon_mesh, &self.display); + + self.meshes.push(mesh); + + handle + } + + fn build_matrices(&mut self) { + self.view_matrix = [ + [1.0 / (self.viewport_width as f32 / 2.0), 0.0, 0.0, 0.0], + [0.0, -1.0 / (self.viewport_height as f32 / 2.0), 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [-1.0, 1.0, 0.0, 1.0], + ]; + } +} + +impl RenderBackend for GliumRenderBackend { + fn set_viewport_dimensions(&mut self, width: u32, height: u32) { + self.viewport_width = width as f32; + self.viewport_height = height as f32; + self.build_matrices(); + } + + fn register_shape(&mut self, shape: &swf::Shape) -> ShapeHandle { + self.register_shape_internal(shape) + } + + fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { + let shape = swf::Shape { + version: 2, + id: 0, + shape_bounds: Default::default(), + edge_bounds: Default::default(), + has_fill_winding_rule: false, + has_non_scaling_strokes: false, + has_scaling_strokes: true, + styles: swf::ShapeStyles { + fill_styles: vec![FillStyle::Color(Color { + r: 255, + g: 255, + b: 255, + a: 255, + })], + line_styles: vec![], + }, + shape: glyph.shape_records.clone(), + }; + self.register_shape_internal(&shape) + } + + fn register_bitmap_jpeg( + &mut self, + id: swf::CharacterId, + data: &[u8], + jpeg_tables: &[u8], + ) -> BitmapHandle { + if !jpeg_tables.is_empty() { + let mut full_jpeg = jpeg_tables[..jpeg_tables.len() - 2].to_vec(); + full_jpeg.extend_from_slice(&data[2..]); + + self.register_bitmap_jpeg_2(id, &full_jpeg[..]) + } else { + self.register_bitmap_jpeg_2(id, &data[..]) + } + } + + fn register_bitmap_jpeg_2(&mut self, id: swf::CharacterId, data: &[u8]) -> BitmapHandle { + let data = ruffle_core::backend::render::remove_invalid_jpeg_data(data); + + let mut decoder = jpeg_decoder::Decoder::new(&data[..]); + decoder.read_info().unwrap(); + let metadata = decoder.info().unwrap(); + let decoded_data = decoder.decode().expect("failed to decode image"); + let image = glium::texture::RawImage2d::from_raw_rgb( + decoded_data, + (metadata.width.into(), metadata.height.into()), + ); + + let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); + + let handle = BitmapHandle(self.textures.len()); + self.textures.push(( + id, + Texture { + texture, + width: metadata.width.into(), + height: metadata.height.into(), + }, + )); + + handle + } + + fn register_bitmap_jpeg_3( + &mut self, + id: swf::CharacterId, + jpeg_data: &[u8], + alpha_data: &[u8], + ) -> BitmapHandle { + let (width, height, rgba) = + ruffle_core::backend::render::define_bits_jpeg_to_rgba(jpeg_data, alpha_data) + .expect("Error decoding DefineBitsJPEG3"); + + let image = glium::texture::RawImage2d::from_raw_rgba(rgba, (width, height)); + let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); + let handle = BitmapHandle(self.textures.len()); + self.textures.push(( + id, + Texture { + texture, + width, + height, + }, + )); + + handle + } + + fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { + let decoded_data = ruffle_core::backend::render::define_bits_lossless_to_rgba(swf_tag) + .expect("Error decoding DefineBitsLossless"); + + let image = glium::texture::RawImage2d::from_raw_rgba( + decoded_data, + (swf_tag.width.into(), swf_tag.height.into()), + ); + + let texture = glium::texture::Texture2d::new(&self.display, image).unwrap(); + + let handle = BitmapHandle(self.textures.len()); + self.textures.push(( + swf_tag.id, + Texture { + texture, + width: swf_tag.width.into(), + height: swf_tag.height.into(), + }, + )); + + handle + } + + fn begin_frame(&mut self) { + assert!(self.target.is_none()); + self.target = Some(self.display.draw()); + } + + fn end_frame(&mut self) { + assert!(self.target.is_some()); + + let target = self.target.take().unwrap(); + target.finish().unwrap(); + } + + fn clear(&mut self, color: Color) { + let target = self.target.as_mut().unwrap(); + target.clear_color_srgb( + f32::from(color.r) / 255.0, + f32::from(color.g) / 255.0, + f32::from(color.b) / 255.0, + f32::from(color.a) / 255.0, + ); + } + + fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) { + let target = self.target.as_mut().unwrap(); + + let mesh = &self.meshes[shape.0]; + + let world_matrix = [ + [transform.matrix.a, transform.matrix.b, 0.0, 0.0], + [transform.matrix.c, transform.matrix.d, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [ + transform.matrix.tx / 20.0, + transform.matrix.ty / 20.0, + 0.0, + 1.0, + ], + ]; + + let mult_color = [ + transform.color_transform.r_mult, + transform.color_transform.g_mult, + transform.color_transform.b_mult, + transform.color_transform.a_mult, + ]; + + let add_color = [ + transform.color_transform.r_add, + transform.color_transform.g_add, + transform.color_transform.b_add, + transform.color_transform.a_add, + ]; + + for draw in &mesh.draws { + match &draw.draw_type { + DrawType::Color => { + target + .draw( + &draw.vertex_buffer, + &draw.index_buffer, + &self.shader_program, + &uniform! { view_matrix: self.view_matrix, world_matrix: world_matrix, mult_color: mult_color, add_color: add_color }, + &color_draw_parameters() + ) + .unwrap(); + } + DrawType::Gradient(gradient_uniforms) => { + let uniforms = GradientUniformsFull { + view_matrix: self.view_matrix, + world_matrix, + mult_color, + add_color, + gradient: gradient_uniforms.clone(), + }; + + target + .draw( + &draw.vertex_buffer, + &draw.index_buffer, + &self.gradient_shader_program, + &uniforms, + &color_draw_parameters(), + ) + .unwrap(); + } + DrawType::Bitmap { + uniforms, + is_smoothed, + is_repeating, + } => { + let texture = &self + .textures + .iter() + .find(|(id, _tex)| *id == uniforms.id) + .unwrap() + .1; + + // Set texture sampler smooth/repeat parameters. + use glium::uniforms::{ + MagnifySamplerFilter, MinifySamplerFilter, SamplerWrapFunction, + }; + let texture = &texture + .texture + .sampled() + .magnify_filter(if *is_smoothed { + MagnifySamplerFilter::Linear + } else { + MagnifySamplerFilter::Nearest + }) + .minify_filter(if *is_smoothed { + MinifySamplerFilter::LinearMipmapLinear + } else { + MinifySamplerFilter::Nearest + }) + .wrap_function(if *is_repeating { + SamplerWrapFunction::Repeat + } else { + SamplerWrapFunction::Clamp + }); + + let uniforms = BitmapUniformsFull { + view_matrix: self.view_matrix, + world_matrix, + mult_color, + add_color, + matrix: uniforms.matrix, + texture, + }; + + target + .draw( + &draw.vertex_buffer, + &draw.index_buffer, + &self.bitmap_shader_program, + &uniforms, + &bitmap_draw_parameters(), + ) + .unwrap(); + } + } + } + } + + fn draw_pause_overlay(&mut self) {} + + fn draw_letterbox(&mut self, letterbox: Letterbox) { + let target = self.target.as_mut().unwrap(); + let black = Some((0.0, 0.0, 0.0, 1.0)); + match letterbox { + Letterbox::None => (), + Letterbox::Letterbox(margin_height) => { + target.clear( + Some(&glium::Rect { + left: 0, + bottom: 0, + width: self.viewport_width as u32, + height: margin_height as u32, + }), + black, + true, + None, + None, + ); + target.clear( + Some(&glium::Rect { + left: 0, + bottom: (self.viewport_height - margin_height) as u32, + width: self.viewport_width as u32, + height: margin_height as u32, + }), + black, + true, + None, + None, + ); + } + Letterbox::Pillarbox(margin_width) => { + target.clear( + Some(&glium::Rect { + left: 0, + bottom: 0, + width: margin_width as u32, + height: self.viewport_height as u32, + }), + black, + true, + None, + None, + ); + target.clear( + Some(&glium::Rect { + left: (self.viewport_width - margin_width) as u32, + bottom: 0, + width: margin_width as u32, + height: self.viewport_height as u32, + }), + black, + true, + None, + None, + ); + } + } + } +} + +struct Texture { + width: u32, + height: u32, + texture: glium::Texture2d, +} + +#[derive(Copy, Clone, Debug)] +struct Vertex { + position: [f32; 2], + color: [f32; 4], +} + +implement_vertex!(Vertex, position, color); + +#[derive(Clone, Debug)] +struct GradientUniforms { + matrix: [[f32; 3]; 3], + gradient_type: i32, + ratios: Vec, + colors: Vec<[f32; 4]>, + num_colors: u32, + repeat_mode: i32, + focal_point: f32, +} + +impl Uniforms for GradientUniforms { + fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { + visit("u_matrix", UniformValue::Mat3(self.matrix)); + visit( + "u_gradient_type", + UniformValue::SignedInt(self.gradient_type), + ); + for i in 0..self.num_colors as usize { + visit( + &format!("u_ratios[{}]", i)[..], + UniformValue::Float(self.ratios[i]), + ); + visit( + &format!("u_colors[{}]", i)[..], + UniformValue::Vec4(self.colors[i]), + ); + } + visit("u_num_colors", UniformValue::UnsignedInt(self.num_colors)); + visit("u_repeat_mode", UniformValue::SignedInt(self.repeat_mode)); + visit("u_focal_point", UniformValue::Float(self.focal_point)); + } +} + +#[derive(Clone, Debug)] +struct GradientUniformsFull { + world_matrix: [[f32; 4]; 4], + view_matrix: [[f32; 4]; 4], + mult_color: [f32; 4], + add_color: [f32; 4], + gradient: GradientUniforms, +} + +impl Uniforms for GradientUniformsFull { + fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { + visit("world_matrix", UniformValue::Mat4(self.world_matrix)); + visit("view_matrix", UniformValue::Mat4(self.view_matrix)); + visit("mult_color", UniformValue::Vec4(self.mult_color)); + visit("add_color", UniformValue::Vec4(self.add_color)); + self.gradient.visit_values(visit); + } +} + +#[derive(Clone, Debug)] +struct BitmapUniforms { + matrix: [[f32; 3]; 3], + id: swf::CharacterId, +} + +impl Uniforms for BitmapUniforms { + fn visit_values<'a, F: FnMut(&str, UniformValue<'a>)>(&'a self, mut visit: F) { + visit("u_matrix", UniformValue::Mat3(self.matrix)); + } +} + +#[derive(Clone, Debug)] +struct BitmapUniformsFull<'a> { + world_matrix: [[f32; 4]; 4], + view_matrix: [[f32; 4]; 4], + mult_color: [f32; 4], + add_color: [f32; 4], + matrix: [[f32; 3]; 3], + texture: &'a Sampler<'a, glium::Texture2d>, +} + +impl<'a> Uniforms for BitmapUniformsFull<'a> { + fn visit_values<'v, F: FnMut(&str, UniformValue<'v>)>(&'v self, mut visit: F) { + use glium::uniforms::AsUniformValue; + visit("world_matrix", UniformValue::Mat4(self.world_matrix)); + visit("view_matrix", UniformValue::Mat4(self.view_matrix)); + visit("mult_color", UniformValue::Vec4(self.mult_color)); + visit("add_color", UniformValue::Vec4(self.add_color)); + visit("u_matrix", UniformValue::Mat3(self.matrix)); + visit("u_texture", self.texture.as_uniform_value()); + } +} + +const VERTEX_SHADER: &str = r#" + #version 140 + + uniform mat4 view_matrix; + uniform mat4 world_matrix; + uniform vec4 mult_color; + uniform vec4 add_color; + + in vec2 position; + in vec4 color; + out vec4 frag_color; + + void main() { + frag_color = color * mult_color + add_color; + gl_Position = view_matrix * world_matrix * vec4(position, 0.0, 1.0); + } +"#; + +const FRAGMENT_SHADER: &str = r#" + #version 140 + in vec4 frag_color; + out vec4 out_color; + void main() { + out_color = frag_color; + } +"#; + +const TEXTURE_VERTEX_SHADER: &str = r#" + #version 140 + + uniform mat4 view_matrix; + uniform mat4 world_matrix; + + uniform mat3 u_matrix; + + in vec2 position; + in vec4 color; + out vec2 frag_uv; + + void main() { + frag_uv = vec2(u_matrix * vec3(position, 1.0)); + gl_Position = view_matrix * world_matrix * vec4(position, 0.0, 1.0); + } +"#; + +const GRADIENT_FRAGMENT_SHADER: &str = r#" +#version 140 + uniform vec4 mult_color; + uniform vec4 add_color; + + uniform int u_gradient_type; + uniform float u_ratios[8]; + uniform vec4 u_colors[8]; + uniform uint u_num_colors; + uniform int u_repeat_mode; + uniform float u_focal_point; + + in vec2 frag_uv; + out vec4 out_color; + + void main() { + vec4 color; + int last = int(int(u_num_colors) - 1); + float t; + if( u_gradient_type == 0 ) + { + t = frag_uv.x; + } + else if( u_gradient_type == 1 ) + { + t = length(frag_uv * 2.0 - 1.0); + } + else if( u_gradient_type == 2 ) + { + vec2 uv = frag_uv * 2.0 - 1.0; + vec2 d = vec2(u_focal_point, 0.0) - uv; + float l = length(d); + d /= l; + t = l / (sqrt(1.0 - u_focal_point*u_focal_point*d.y*d.y) + u_focal_point*d.x); + } + if( u_repeat_mode == 0 ) + { + // Clamp + t = clamp(t, 0.0, 1.0); + } + else if( u_repeat_mode == 1 ) + { + // Repeat + t = fract(t); + } + else + { + // Mirror + if( t < 0.0 ) + { + t = -t; + } + if( (int(t)&1) == 0 ) { + t = fract(t); + } else { + t = 1.0 - fract(t); + } + } + int i = 0; + int j = 1; + while( t > u_ratios[j] ) + { + i = j; + j++; + } + float a = (t - u_ratios[i]) / (u_ratios[j] - u_ratios[i]); + color = mix(u_colors[i], u_colors[j], a); + out_color = mult_color * color + add_color; + } +"#; + +const BITMAP_FRAGMENT_SHADER: &str = r#" +#version 140 + uniform vec4 mult_color; + uniform vec4 add_color; + + in vec2 frag_uv; + out vec4 out_color; + + uniform sampler2D u_texture; + + void main() { + vec4 color = texture(u_texture, frag_uv); + out_color = mult_color * color + add_color; + } +"#; + +struct Mesh { + draws: Vec, +} + +struct Draw { + draw_type: DrawType, + vertex_buffer: glium::VertexBuffer, + index_buffer: glium::IndexBuffer, +} + +enum DrawType { + Color, + Gradient(GradientUniforms), + Bitmap { + uniforms: BitmapUniforms, + is_smoothed: bool, + is_repeating: bool, + }, +} + +fn point(x: Twips, y: Twips) -> lyon::math::Point { + lyon::math::Point::new(x.to_pixels() as f32, y.to_pixels() as f32) +} + +fn ruffle_path_to_lyon_path( + commands: Vec, + mut is_closed: bool, +) -> impl Iterator { + use lyon::geom::{LineSegment, QuadraticBezierSegment}; + + let mut cur = lyon::math::Point::new(0.0, 0.0); + let mut i = commands.into_iter(); + std::iter::from_fn(move || match i.next() { + Some(DrawCommand::MoveTo { x, y }) => { + cur = point(x, y); + Some(PathEvent::MoveTo(cur)) + } + Some(DrawCommand::LineTo { x, y }) => { + let next = point(x, y); + let cmd = PathEvent::Line(LineSegment { + from: cur, + to: next, + }); + cur = next; + Some(cmd) + } + Some(DrawCommand::CurveTo { x1, y1, x2, y2 }) => { + let next = point(x2, y2); + let cmd = PathEvent::Quadratic(QuadraticBezierSegment { + from: cur, + ctrl: point(x1, y1), + to: next, + }); + cur = next; + Some(cmd) + } + None => { + if is_closed { + is_closed = false; + Some(PathEvent::Close(LineSegment { from: cur, to: cur })) + } else { + None + } + } + }) +} + +#[allow(clippy::many_single_char_names)] +fn swf_to_gl_matrix(m: swf::Matrix) -> [[f32; 3]; 3] { + let tx = m.translate_x.get() as f32; + let ty = m.translate_y.get() as f32; + let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; + let mut a = m.scale_y / det; + let mut b = -m.rotate_skew_1 / det; + let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; + let mut d = -m.rotate_skew_0 / det; + let mut e = m.scale_x / det; + let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; + + a *= 20.0 / 32768.0; + b *= 20.0 / 32768.0; + d *= 20.0 / 32768.0; + e *= 20.0 / 32768.0; + + c /= 32768.0; + f /= 32768.0; + c += 0.5; + f += 0.5; + [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] +} + +#[allow(clippy::many_single_char_names)] +fn swf_bitmap_to_gl_matrix(m: swf::Matrix, bitmap_width: u32, bitmap_height: u32) -> [[f32; 3]; 3] { + let bitmap_width = bitmap_width as f32; + let bitmap_height = bitmap_height as f32; + + let tx = m.translate_x.get() as f32; + let ty = m.translate_y.get() as f32; + let det = m.scale_x * m.scale_y - m.rotate_skew_1 * m.rotate_skew_0; + let mut a = m.scale_y / det; + let mut b = -m.rotate_skew_1 / det; + let mut c = -(tx * m.scale_y - m.rotate_skew_1 * ty) / det; + let mut d = -m.rotate_skew_0 / det; + let mut e = m.scale_x / det; + let mut f = (tx * m.rotate_skew_0 - m.scale_x * ty) / det; + + a *= 20.0 / bitmap_width; + b *= 20.0 / bitmap_width; + d *= 20.0 / bitmap_height; + e *= 20.0 / bitmap_height; + + c /= bitmap_width; + f /= bitmap_height; + + [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] +} + +/// Returns the drawing parameters for standard color/gradient fills. +#[inline] +fn color_draw_parameters() -> DrawParameters<'static> { + DrawParameters { + blend: glium::Blend::alpha_blending(), + ..Default::default() + } +} + +/// Returns the drawing parameters for bitmaps with pre-multipled alpha. +#[inline] +fn bitmap_draw_parameters() -> DrawParameters<'static> { + use glium::{BlendingFunction, LinearBlendingFactor}; + DrawParameters { + blend: glium::Blend { + color: BlendingFunction::Addition { + source: LinearBlendingFactor::One, + destination: LinearBlendingFactor::OneMinusSourceAlpha, + }, + alpha: BlendingFunction::Addition { + source: LinearBlendingFactor::SourceAlpha, + destination: LinearBlendingFactor::OneMinusSourceAlpha, + }, + ..Default::default() + }, + ..Default::default() + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 000000000..4c46e8b5e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +newline_style = "Unix" \ No newline at end of file diff --git a/web/src/audio.rs b/web/src/audio.rs index 3e5fbc5d7..69ecc72a3 100644 --- a/web/src/audio.rs +++ b/web/src/audio.rs @@ -1,439 +1,500 @@ -use fnv::FnvHashMap; -use generational_arena::Arena; -use ruffle_core::backend::audio::decoders::{AdpcmDecoder, Mp3Decoder}; -use ruffle_core::backend::audio::{AudioBackend, AudioStreamHandle, SoundHandle}; -use ruffle_core::backend::audio::swf::{self, AudioCompression}; -use std::cell::{Cell, RefCell}; -use std::rc::Rc; -use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::AudioContext; - -pub struct WebAudioBackend { - context: AudioContext, - sounds: Arena, - stream_data: FnvHashMap, - id_to_sound: FnvHashMap, - left_samples: Vec, - right_samples: Vec, -} - -thread_local! { - static STREAMS: RefCell> = RefCell::new(Arena::new()); - static NUM_SOUNDS_LOADING: Cell = Cell::new(0); -} - -struct StreamData { - format: swf::SoundFormat, - audio_data: Vec, - num_sample_frames: u32, - samples_per_block: u32, -} - -type AudioBufferPtr = Rc>; - -// A sound can be either as a JS AudioBuffer and as a on--the-fly decoded stream using a ScriptProcessorNode. -#[allow(dead_code)] -enum SoundSource { - // Pre-decoded audio buffer. - AudioBuffer(AudioBufferPtr), - - // Decode the audio data on the fly from a byte stream. - Decoder(Vec), -} - -struct Sound { - format: swf::SoundFormat, - source: SoundSource, -} - -type Decoder = Box>; - -#[allow(dead_code)] -enum AudioStream { - Decoder { decoder: Decoder, is_stereo: bool, },// closure: Option>> } , - AudioBuffer { node: web_sys::AudioBufferSourceNode }, -} - -type Error = Box; - -impl WebAudioBackend { - pub fn new() -> Result { - let context = AudioContext::new().map_err(|_| "Unable to create AudioContext")?; - Ok(Self { - context, - sounds: Arena::new(), - stream_data: FnvHashMap::default(), - id_to_sound: FnvHashMap::default(), - left_samples: vec![], - right_samples: vec![], - }) - } - - fn play_sound_internal(&mut self, handle: SoundHandle) -> SoundHandle { - let sound = self.sounds.get(handle).unwrap(); - match &sound.source { - SoundSource::AudioBuffer(audio_buffer) => { - let audio_buffer = audio_buffer.borrow(); - let node = self.context.create_buffer_source().unwrap(); - node.set_buffer(Some(&*audio_buffer)); - node - .connect_with_audio_node(&self.context.destination()) - .unwrap(); - node.start().unwrap(); - - let audio_stream = AudioStream::AudioBuffer { - node - }; - STREAMS.with(|streams| { - let mut streams = streams.borrow_mut(); - streams.insert(audio_stream) - }) - } - SoundSource::Decoder(audio_data) => { - let decoder: Decoder = match sound.format.compression { - AudioCompression::Adpcm => Box::new(AdpcmDecoder::new( - std::io::Cursor::new(audio_data.to_vec()), - sound.format.is_stereo, - sound.format.sample_rate - ).unwrap()), - AudioCompression::Mp3 => Box::new(Mp3Decoder::new( - if sound.format.is_stereo { - 2 - } else { - 1 - }, - sound.format.sample_rate.into(), - std::io::Cursor::new(audio_data.to_vec())//&sound.data[..] - )), - _ => unimplemented!() - }; - - let decoder: Decoder = if sound.format.sample_rate != self.context.sample_rate() as u16 { - Box::new(resample(decoder, sound.format.sample_rate, self.context.sample_rate() as u16, sound.format.is_stereo)) - } else { - decoder - }; - - let audio_stream = AudioStream::Decoder { - decoder, - is_stereo: sound.format.is_stereo, - //closure: None, - }; - STREAMS.with(|streams| { - let mut streams = streams.borrow_mut(); - let stream_handle = streams.insert(audio_stream); - let script_processor_node = self.context.create_script_processor_with_buffer_size_and_number_of_input_channels_and_number_of_output_channels(4096, 0, if sound.format.is_stereo { 2 } else { 1 }).unwrap(); - let script_node = script_processor_node.clone(); - - let closure = Closure::wrap(Box::new(move |event| { - STREAMS.with(|streams| { - let mut streams = streams.borrow_mut(); - let audio_stream = streams.get_mut(stream_handle).unwrap(); - let complete = WebAudioBackend::update_script_processor(audio_stream, event); - if complete { - streams.remove(stream_handle); - script_node.disconnect().unwrap(); - } - }) - }) as Box); - script_processor_node.set_onaudioprocess(Some(closure.as_ref().unchecked_ref())); - // TODO: This will leak memory per playing sound. Remember and properly drop the closure. - closure.forget(); - - stream_handle - }) - } - } - } - - fn decompress_to_audio_buffer(&mut self, format: &swf::SoundFormat, audio_data: &[u8], num_sample_frames: u32) -> AudioBufferPtr { - if format.compression == AudioCompression::Mp3 { - return self.decompress_mp3_to_audio_buffer(format, audio_data, num_sample_frames); - } - - // This sucks. Firefox doesn't like 5512Hz sample rate, so manually double up the samples. - // 5512Hz should be relatively rare. - let audio_buffer = if format.sample_rate > 5512 { - self.context.create_buffer( - if format.is_stereo { 2 } else { 1 }, - num_sample_frames, - f32::from(format.sample_rate) - ).unwrap() - } else { - self.context.create_buffer( - if format.is_stereo { 2 } else { 1 }, - num_sample_frames * 2, - 11025.0 - ).unwrap() - }; - - match format.compression { - AudioCompression::Uncompressed => { - // TODO: Check for is_16_bit. - self.left_samples = audio_data.iter().step_by(2).cloned().map(|n| f32::from(n) / 32767.0).collect(); - if format.is_stereo { - self.right_samples = audio_data.iter().skip(1).step_by(2).cloned().map(|n| f32::from(n) / 32767.0).collect(); - } - } - AudioCompression::Adpcm => { - let mut decoder = AdpcmDecoder::new(audio_data, - format.is_stereo, - format.sample_rate - ).unwrap(); - if format.is_stereo { - while let (Some(l), Some(r)) = (decoder.next(), decoder.next()) { - self.left_samples.push(f32::from(l) / 32767.0); - self.right_samples.push(f32::from(r) / 32767.0); - } - } else { - self.left_samples = decoder.map(|n| f32::from(n) / 32767.0).collect(); - } - } - _ => unimplemented!(), - } - - // Double up samples for 5512Hz audio to satisfy Firefox. - if format.sample_rate == 5512 { - let mut samples = Vec::with_capacity(self.left_samples.len() * 2); - for sample in &self.left_samples { - samples.push(*sample); - samples.push(*sample); - } - self.left_samples = samples; - - if format.is_stereo { - let mut samples = Vec::with_capacity(self.right_samples.len() * 2); - for sample in &self.right_samples { - samples.push(*sample); - samples.push(*sample); - } - self.right_samples = samples; - } - } - - audio_buffer.copy_to_channel(&mut self.left_samples, 0).unwrap(); - if format.is_stereo { - audio_buffer.copy_to_channel(&mut self.right_samples, 1).unwrap(); - } - - Rc::new(RefCell::new(audio_buffer)) - } - - fn decompress_mp3_to_audio_buffer(&mut self, format: &swf::SoundFormat, audio_data: &[u8], _num_sample_frames: u32) -> AudioBufferPtr { - // We use the Web decodeAudioData API to decode MP3 data. - // TODO: Is it possible we finish loading before the MP3 is decoding? - let audio_buffer = self.context.create_buffer(1, 1, self.context.sample_rate()).unwrap(); - let audio_buffer = Rc::new(RefCell::new(audio_buffer)); - - let data_array = unsafe { js_sys::Uint8Array::view(&audio_data[..]) }; - let array_buffer = data_array.buffer().slice_with_end( - data_array.byte_offset(), - data_array.byte_offset() + data_array.byte_length(), - ); - - NUM_SOUNDS_LOADING.with(|n| n.set(n.get() + 1)); - - let _num_channels = if format.is_stereo { 2 } else { 1 }; - let buffer_ptr = Rc::clone(&audio_buffer); - let success_closure = Closure::wrap(Box::new(move |buffer: web_sys::AudioBuffer| { - *buffer_ptr.borrow_mut() = buffer; - NUM_SOUNDS_LOADING.with(|n| n.set(n.get() - 1)); - }) - as Box); - let error_closure = Closure::wrap(Box::new(move || { - log::info!("Error decoding MP3 audio"); - NUM_SOUNDS_LOADING.with(|n| n.set(n.get() - 1)); - }) - as Box); - self.context.decode_audio_data_with_success_callback_and_error_callback( - &array_buffer, - success_closure.as_ref().unchecked_ref(), - error_closure.as_ref().unchecked_ref() - ).unwrap(); - - // TODO: This will leak memory (once per decompressed MP3). - // Not a huge deal as there are probably not many MP3s in an SWF. - success_closure.forget(); - error_closure.forget(); - - audio_buffer - } - - fn update_script_processor( - audio_stream: &mut AudioStream, - event: web_sys::AudioProcessingEvent, - ) -> bool { - let mut complete = false; - let mut left_samples = vec![]; - let mut right_samples = vec![]; - if let AudioStream::Decoder { decoder, is_stereo, .. } = audio_stream { - let output_buffer = event.output_buffer().unwrap(); - let num_frames = output_buffer.length() as usize; - - for _ in 0..num_frames { - if let (Some(l), Some(r)) = (decoder.next(), decoder.next()) { - left_samples.push(f32::from(l) / 32767.0); - if *is_stereo { - right_samples.push(f32::from(r) / 32767.0); - } - } else { - complete = true; - break; - } - } - output_buffer.copy_to_channel(&mut left_samples[..], 0).unwrap(); - if *is_stereo { - output_buffer.copy_to_channel(&mut right_samples[..], 1).unwrap(); - } - } - - complete - } -} - -impl AudioBackend for WebAudioBackend { - fn register_sound(&mut self, sound: &swf::Sound) -> Result { - // Slice off latency seek for MP3 data. - let data = if sound.format.compression == AudioCompression::Mp3 { - &sound.data[2..] - } else { - &sound.data[..] - }; - - let sound = Sound { - format: sound.format.clone(), - source: SoundSource::AudioBuffer(self.decompress_to_audio_buffer(&sound.format, data, sound.num_samples)), - }; - Ok(self.sounds.insert(sound)) - } - - fn preload_sound_stream_head(&mut self, clip_id: swf::CharacterId, stream_info: &swf::SoundStreamHead) { - self.stream_data.entry(clip_id).or_insert_with(|| { - StreamData { - format: stream_info.stream_format.clone(), - audio_data: vec![], - num_sample_frames: 0, - samples_per_block: stream_info.num_samples_per_block.into(), - } - }); - } - - fn preload_sound_stream_block(&mut self, clip_id: swf::CharacterId, audio_data: &[u8]) { - if let Some(stream) = self.stream_data.get_mut(&clip_id) { - match stream.format.compression { - AudioCompression::Uncompressed | AudioCompression::UncompressedUnknownEndian => { - let frame_len = if stream.format.is_stereo { 2 } else { 1 } * if stream.format.is_16_bit { 2 } else { 1 }; - stream.num_sample_frames += (audio_data.len() as u32) / frame_len; - stream.audio_data.extend_from_slice(audio_data); - } - AudioCompression::Mp3 => { - let num_sample_frames = (u32::from(audio_data[2]) << 8) | u32::from(audio_data[3]); - stream.num_sample_frames += num_sample_frames; - // MP3 streaming data: - // First two bytes = number of samples - // Second two bytes = 'latency seek' (amount to skip when seeking to this frame) - stream.audio_data.extend_from_slice(&audio_data[4..]); - } - _ => { - // TODO: This is a guess and will vary slightly from block to block! - stream.num_sample_frames += stream.samples_per_block; - } - } - } - } - - fn preload_sound_stream_end(&mut self, clip_id: swf::CharacterId) { - if let Some(stream) = self.stream_data.remove(&clip_id) { - if !stream.audio_data.is_empty() - { - let audio_buffer = self.decompress_to_audio_buffer(&stream.format, &stream.audio_data[..], stream.num_sample_frames); - let handle = self.sounds.insert(Sound { - format: stream.format, - source: SoundSource::AudioBuffer(audio_buffer), - }); - self.id_to_sound.insert(clip_id, handle); - } - } - } - - fn play_sound(&mut self, sound: SoundHandle) { - self.play_sound_internal(sound); - } - - fn start_stream( - &mut self, - clip_id: swf::CharacterId, - _clip_data: ruffle_core::tag_utils::SwfSlice, - _stream_info: &swf::SoundStreamHead, - ) -> AudioStreamHandle { - if let Some(&handle) = self.id_to_sound.get(&clip_id) { - self.play_sound_internal(handle) - } else { - log::error!("Missing stream for clip {}", clip_id); - // TODO: Return dummy sound. - panic!(); - } - } - - fn is_loading_complete(&self) -> bool { - NUM_SOUNDS_LOADING.with(|n| n.get() == 0) - } - - fn prime_audio(&mut self) { - // Allow audio to start playing after a user gesture. - let _ = self.context.resume(); - } -} - -// Janky resmapling code. -// TODO: Clean this up. -fn resample(mut input: impl Iterator, input_sample_rate: u16, output_sample_rate: u16, is_stereo: bool) -> impl Iterator { - let (mut left0, mut right0) = if is_stereo { - (input.next(), input.next()) - } else { - let sample = input.next(); - (sample, sample) - }; - let (mut left1, mut right1) = if is_stereo { - (input.next(), input.next()) - } else { - let sample = input.next(); - (sample, sample) - }; - let (mut left, mut right) = (left0.unwrap(), right0.unwrap()); - let dt_input = 1.0 / f64::from(input_sample_rate); - let dt_output = 1.0 / f64::from(output_sample_rate); - let mut t = 0.0; - let mut cur_channel = 0; - std::iter::from_fn(move || { - if cur_channel == 1 { - cur_channel = 0; - return Some(right); - } - if let (Some(l0), Some(r0), Some(l1), Some(r1)) = (left0, right0, left1, right1) { - let a = t / dt_input; - let l0 = f64::from(l0); - let l1 = f64::from(l1); - let r0 = f64::from(r0); - let r1 = f64::from(r1); - left = (l0 + (l1 - l0) * a) as i16; - right = (r0 + (r1 - r0) * a) as i16; - t += dt_output; - while t >= dt_input { - t -= dt_input; - left0 = left1; - right0 = right1; - left1 = input.next(); - if is_stereo { - right1 = input.next(); - } else { - right1 = left1; - } - } - cur_channel = 1; - Some(left) - } else { - None - } - }) -} +use fnv::FnvHashMap; +use generational_arena::Arena; +use ruffle_core::backend::audio::decoders::{AdpcmDecoder, Mp3Decoder}; +use ruffle_core::backend::audio::swf::{self, AudioCompression}; +use ruffle_core::backend::audio::{AudioBackend, AudioStreamHandle, SoundHandle}; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::AudioContext; + +pub struct WebAudioBackend { + context: AudioContext, + sounds: Arena, + stream_data: FnvHashMap, + id_to_sound: FnvHashMap, + left_samples: Vec, + right_samples: Vec, +} + +thread_local! { + static STREAMS: RefCell> = RefCell::new(Arena::new()); + static NUM_SOUNDS_LOADING: Cell = Cell::new(0); +} + +struct StreamData { + format: swf::SoundFormat, + audio_data: Vec, + num_sample_frames: u32, + samples_per_block: u32, +} + +type AudioBufferPtr = Rc>; + +// A sound can be either as a JS AudioBuffer and as a on--the-fly decoded stream using a ScriptProcessorNode. +#[allow(dead_code)] +enum SoundSource { + // Pre-decoded audio buffer. + AudioBuffer(AudioBufferPtr), + + // Decode the audio data on the fly from a byte stream. + Decoder(Vec), +} + +struct Sound { + format: swf::SoundFormat, + source: SoundSource, +} + +type Decoder = Box>; + +#[allow(dead_code)] +enum AudioStream { + Decoder { + decoder: Decoder, + is_stereo: bool, + }, // closure: Option>> } , + AudioBuffer { + node: web_sys::AudioBufferSourceNode, + }, +} + +type Error = Box; + +impl WebAudioBackend { + pub fn new() -> Result { + let context = AudioContext::new().map_err(|_| "Unable to create AudioContext")?; + Ok(Self { + context, + sounds: Arena::new(), + stream_data: FnvHashMap::default(), + id_to_sound: FnvHashMap::default(), + left_samples: vec![], + right_samples: vec![], + }) + } + + fn play_sound_internal(&mut self, handle: SoundHandle) -> SoundHandle { + let sound = self.sounds.get(handle).unwrap(); + match &sound.source { + SoundSource::AudioBuffer(audio_buffer) => { + let audio_buffer = audio_buffer.borrow(); + let node = self.context.create_buffer_source().unwrap(); + node.set_buffer(Some(&*audio_buffer)); + node.connect_with_audio_node(&self.context.destination()) + .unwrap(); + node.start().unwrap(); + + let audio_stream = AudioStream::AudioBuffer { node }; + STREAMS.with(|streams| { + let mut streams = streams.borrow_mut(); + streams.insert(audio_stream) + }) + } + SoundSource::Decoder(audio_data) => { + let decoder: Decoder = match sound.format.compression { + AudioCompression::Adpcm => Box::new( + AdpcmDecoder::new( + std::io::Cursor::new(audio_data.to_vec()), + sound.format.is_stereo, + sound.format.sample_rate, + ) + .unwrap(), + ), + AudioCompression::Mp3 => Box::new(Mp3Decoder::new( + if sound.format.is_stereo { 2 } else { 1 }, + sound.format.sample_rate.into(), + std::io::Cursor::new(audio_data.to_vec()), //&sound.data[..] + )), + _ => unimplemented!(), + }; + + let decoder: Decoder = + if sound.format.sample_rate != self.context.sample_rate() as u16 { + Box::new(resample( + decoder, + sound.format.sample_rate, + self.context.sample_rate() as u16, + sound.format.is_stereo, + )) + } else { + decoder + }; + + let audio_stream = AudioStream::Decoder { + decoder, + is_stereo: sound.format.is_stereo, + //closure: None, + }; + STREAMS.with(|streams| { + let mut streams = streams.borrow_mut(); + let stream_handle = streams.insert(audio_stream); + let script_processor_node = self.context.create_script_processor_with_buffer_size_and_number_of_input_channels_and_number_of_output_channels(4096, 0, if sound.format.is_stereo { 2 } else { 1 }).unwrap(); + let script_node = script_processor_node.clone(); + let closure = Closure::wrap(Box::new(move |event| { + STREAMS.with(|streams| { + let mut streams = streams.borrow_mut(); + let audio_stream = streams.get_mut(stream_handle).unwrap(); + let complete = WebAudioBackend::update_script_processor(audio_stream, event); + if complete { + streams.remove(stream_handle); + script_node.disconnect().unwrap(); + } + }) + }) as Box); + script_processor_node.set_onaudioprocess(Some(closure.as_ref().unchecked_ref())); + // TODO: This will leak memory per playing sound. Remember and properly drop the closure. + closure.forget(); + + stream_handle + }) + } + } + } + + fn decompress_to_audio_buffer( + &mut self, + format: &swf::SoundFormat, + audio_data: &[u8], + num_sample_frames: u32, + ) -> AudioBufferPtr { + if format.compression == AudioCompression::Mp3 { + return self.decompress_mp3_to_audio_buffer(format, audio_data, num_sample_frames); + } + + // This sucks. Firefox doesn't like 5512Hz sample rate, so manually double up the samples. + // 5512Hz should be relatively rare. + let audio_buffer = if format.sample_rate > 5512 { + self.context + .create_buffer( + if format.is_stereo { 2 } else { 1 }, + num_sample_frames, + f32::from(format.sample_rate), + ) + .unwrap() + } else { + self.context + .create_buffer( + if format.is_stereo { 2 } else { 1 }, + num_sample_frames * 2, + 11025.0, + ) + .unwrap() + }; + + match format.compression { + AudioCompression::Uncompressed => { + // TODO: Check for is_16_bit. + self.left_samples = audio_data + .iter() + .step_by(2) + .cloned() + .map(|n| f32::from(n) / 32767.0) + .collect(); + if format.is_stereo { + self.right_samples = audio_data + .iter() + .skip(1) + .step_by(2) + .cloned() + .map(|n| f32::from(n) / 32767.0) + .collect(); + } + } + AudioCompression::Adpcm => { + let mut decoder = + AdpcmDecoder::new(audio_data, format.is_stereo, format.sample_rate).unwrap(); + if format.is_stereo { + while let (Some(l), Some(r)) = (decoder.next(), decoder.next()) { + self.left_samples.push(f32::from(l) / 32767.0); + self.right_samples.push(f32::from(r) / 32767.0); + } + } else { + self.left_samples = decoder.map(|n| f32::from(n) / 32767.0).collect(); + } + } + _ => unimplemented!(), + } + + // Double up samples for 5512Hz audio to satisfy Firefox. + if format.sample_rate == 5512 { + let mut samples = Vec::with_capacity(self.left_samples.len() * 2); + for sample in &self.left_samples { + samples.push(*sample); + samples.push(*sample); + } + self.left_samples = samples; + + if format.is_stereo { + let mut samples = Vec::with_capacity(self.right_samples.len() * 2); + for sample in &self.right_samples { + samples.push(*sample); + samples.push(*sample); + } + self.right_samples = samples; + } + } + + audio_buffer + .copy_to_channel(&mut self.left_samples, 0) + .unwrap(); + if format.is_stereo { + audio_buffer + .copy_to_channel(&mut self.right_samples, 1) + .unwrap(); + } + + Rc::new(RefCell::new(audio_buffer)) + } + + fn decompress_mp3_to_audio_buffer( + &mut self, + format: &swf::SoundFormat, + audio_data: &[u8], + _num_sample_frames: u32, + ) -> AudioBufferPtr { + // We use the Web decodeAudioData API to decode MP3 data. + // TODO: Is it possible we finish loading before the MP3 is decoding? + let audio_buffer = self + .context + .create_buffer(1, 1, self.context.sample_rate()) + .unwrap(); + let audio_buffer = Rc::new(RefCell::new(audio_buffer)); + + let data_array = unsafe { js_sys::Uint8Array::view(&audio_data[..]) }; + let array_buffer = data_array.buffer().slice_with_end( + data_array.byte_offset(), + data_array.byte_offset() + data_array.byte_length(), + ); + + NUM_SOUNDS_LOADING.with(|n| n.set(n.get() + 1)); + + let _num_channels = if format.is_stereo { 2 } else { 1 }; + let buffer_ptr = Rc::clone(&audio_buffer); + let success_closure = Closure::wrap(Box::new(move |buffer: web_sys::AudioBuffer| { + *buffer_ptr.borrow_mut() = buffer; + NUM_SOUNDS_LOADING.with(|n| n.set(n.get() - 1)); + }) as Box); + let error_closure = Closure::wrap(Box::new(move || { + log::info!("Error decoding MP3 audio"); + NUM_SOUNDS_LOADING.with(|n| n.set(n.get() - 1)); + }) as Box); + self.context + .decode_audio_data_with_success_callback_and_error_callback( + &array_buffer, + success_closure.as_ref().unchecked_ref(), + error_closure.as_ref().unchecked_ref(), + ) + .unwrap(); + + // TODO: This will leak memory (once per decompressed MP3). + // Not a huge deal as there are probably not many MP3s in an SWF. + success_closure.forget(); + error_closure.forget(); + + audio_buffer + } + + fn update_script_processor( + audio_stream: &mut AudioStream, + event: web_sys::AudioProcessingEvent, + ) -> bool { + let mut complete = false; + let mut left_samples = vec![]; + let mut right_samples = vec![]; + if let AudioStream::Decoder { + decoder, is_stereo, .. + } = audio_stream + { + let output_buffer = event.output_buffer().unwrap(); + let num_frames = output_buffer.length() as usize; + + for _ in 0..num_frames { + if let (Some(l), Some(r)) = (decoder.next(), decoder.next()) { + left_samples.push(f32::from(l) / 32767.0); + if *is_stereo { + right_samples.push(f32::from(r) / 32767.0); + } + } else { + complete = true; + break; + } + } + output_buffer + .copy_to_channel(&mut left_samples[..], 0) + .unwrap(); + if *is_stereo { + output_buffer + .copy_to_channel(&mut right_samples[..], 1) + .unwrap(); + } + } + + complete + } +} + +impl AudioBackend for WebAudioBackend { + fn register_sound(&mut self, sound: &swf::Sound) -> Result { + // Slice off latency seek for MP3 data. + let data = if sound.format.compression == AudioCompression::Mp3 { + &sound.data[2..] + } else { + &sound.data[..] + }; + + let sound = Sound { + format: sound.format.clone(), + source: SoundSource::AudioBuffer(self.decompress_to_audio_buffer( + &sound.format, + data, + sound.num_samples, + )), + }; + Ok(self.sounds.insert(sound)) + } + + fn preload_sound_stream_head( + &mut self, + clip_id: swf::CharacterId, + stream_info: &swf::SoundStreamHead, + ) { + self.stream_data + .entry(clip_id) + .or_insert_with(|| StreamData { + format: stream_info.stream_format.clone(), + audio_data: vec![], + num_sample_frames: 0, + samples_per_block: stream_info.num_samples_per_block.into(), + }); + } + + fn preload_sound_stream_block(&mut self, clip_id: swf::CharacterId, audio_data: &[u8]) { + if let Some(stream) = self.stream_data.get_mut(&clip_id) { + match stream.format.compression { + AudioCompression::Uncompressed | AudioCompression::UncompressedUnknownEndian => { + let frame_len = if stream.format.is_stereo { 2 } else { 1 } + * if stream.format.is_16_bit { 2 } else { 1 }; + stream.num_sample_frames += (audio_data.len() as u32) / frame_len; + stream.audio_data.extend_from_slice(audio_data); + } + AudioCompression::Mp3 => { + let num_sample_frames = + (u32::from(audio_data[2]) << 8) | u32::from(audio_data[3]); + stream.num_sample_frames += num_sample_frames; + // MP3 streaming data: + // First two bytes = number of samples + // Second two bytes = 'latency seek' (amount to skip when seeking to this frame) + stream.audio_data.extend_from_slice(&audio_data[4..]); + } + _ => { + // TODO: This is a guess and will vary slightly from block to block! + stream.num_sample_frames += stream.samples_per_block; + } + } + } + } + + fn preload_sound_stream_end(&mut self, clip_id: swf::CharacterId) { + if let Some(stream) = self.stream_data.remove(&clip_id) { + if !stream.audio_data.is_empty() { + let audio_buffer = self.decompress_to_audio_buffer( + &stream.format, + &stream.audio_data[..], + stream.num_sample_frames, + ); + let handle = self.sounds.insert(Sound { + format: stream.format, + source: SoundSource::AudioBuffer(audio_buffer), + }); + self.id_to_sound.insert(clip_id, handle); + } + } + } + + fn play_sound(&mut self, sound: SoundHandle) { + self.play_sound_internal(sound); + } + + fn start_stream( + &mut self, + clip_id: swf::CharacterId, + _clip_data: ruffle_core::tag_utils::SwfSlice, + _stream_info: &swf::SoundStreamHead, + ) -> AudioStreamHandle { + if let Some(&handle) = self.id_to_sound.get(&clip_id) { + self.play_sound_internal(handle) + } else { + log::error!("Missing stream for clip {}", clip_id); + // TODO: Return dummy sound. + panic!(); + } + } + + fn is_loading_complete(&self) -> bool { + NUM_SOUNDS_LOADING.with(|n| n.get() == 0) + } + + fn prime_audio(&mut self) { + // Allow audio to start playing after a user gesture. + let _ = self.context.resume(); + } +} + +// Janky resmapling code. +// TODO: Clean this up. +fn resample( + mut input: impl Iterator, + input_sample_rate: u16, + output_sample_rate: u16, + is_stereo: bool, +) -> impl Iterator { + let (mut left0, mut right0) = if is_stereo { + (input.next(), input.next()) + } else { + let sample = input.next(); + (sample, sample) + }; + let (mut left1, mut right1) = if is_stereo { + (input.next(), input.next()) + } else { + let sample = input.next(); + (sample, sample) + }; + let (mut left, mut right) = (left0.unwrap(), right0.unwrap()); + let dt_input = 1.0 / f64::from(input_sample_rate); + let dt_output = 1.0 / f64::from(output_sample_rate); + let mut t = 0.0; + let mut cur_channel = 0; + std::iter::from_fn(move || { + if cur_channel == 1 { + cur_channel = 0; + return Some(right); + } + if let (Some(l0), Some(r0), Some(l1), Some(r1)) = (left0, right0, left1, right1) { + let a = t / dt_input; + let l0 = f64::from(l0); + let l1 = f64::from(l1); + let r0 = f64::from(r0); + let r1 = f64::from(r1); + left = (l0 + (l1 - l0) * a) as i16; + right = (r0 + (r1 - r0) * a) as i16; + t += dt_output; + while t >= dt_input { + t -= dt_input; + left0 = left1; + right0 = right1; + left1 = input.next(); + if is_stereo { + right1 = input.next(); + } else { + right1 = left1; + } + } + cur_channel = 1; + Some(left) + } else { + None + } + }) +} diff --git a/web/src/render.rs b/web/src/render.rs index 57c460e10..023a03965 100644 --- a/web/src/render.rs +++ b/web/src/render.rs @@ -1,805 +1,805 @@ -use ruffle_core::backend::render::{ - swf, swf::CharacterId, BitmapHandle, Color, Letterbox, RenderBackend, ShapeHandle, Transform, -}; -use std::collections::HashMap; -use wasm_bindgen::JsCast; -use web_sys::{CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlImageElement}; - -pub struct WebCanvasRenderBackend { - canvas: HtmlCanvasElement, - context: CanvasRenderingContext2d, - color_matrix: Element, - shapes: Vec, - bitmaps: Vec, - id_to_bitmap: HashMap, - viewport_width: u32, - viewport_height: u32, - use_color_transform_hack: bool, - pixelated_property_value: &'static str, -} - -struct ShapeData { - image: HtmlImageElement, - x_min: f64, - y_min: f64, -} - -#[allow(dead_code)] -struct BitmapData { - image: HtmlImageElement, - width: u32, - height: u32, - data: String, -} - -impl WebCanvasRenderBackend { - pub fn new(canvas: &HtmlCanvasElement) -> Result> { - // Request the CanvasRenderingContext2d. - // Disable alpha for possible speedup. - // TODO: Allow user to enable transparent background (transparent wmode in legacy Flash). - let context_options = js_sys::Object::new(); - let _ = js_sys::Reflect::set( - &context_options, - &"alpha".into(), - &wasm_bindgen::JsValue::FALSE, - ); - let context: CanvasRenderingContext2d = canvas - .get_context_with_context_options("2d", &context_options) - .map_err(|_| "Could not create context")? - .ok_or("Could not create context")? - .dyn_into() - .map_err(|_| "Expected CanvasRenderingContext2d")?; - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - - // Create a color matrix filter to handle Flash color effects. - // Ensure a previous instance of the color matrix filter node doesn't exist. - // TODO: Remove it in player.destroy()? This is dangerous if the client page has something with this id... - if let Some(element) = document.get_element_by_id("_svgfilter") { - element.remove(); - } - - let svg = document - .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") - .map_err(|_| "Couldn't make SVG")?; - - svg.set_id("_svgfilter"); - - svg.set_attribute("width", "0") - .map_err(|_| "Couldn't make SVG")?; - - svg.set_attribute("height", "0") - .map_err(|_| "Couldn't make SVG")?; - - svg.set_attribute_ns( - Some("http://www.w3.org/2000/xmlns/"), - "xmlns:xlink", - "http://www.w3.org/1999/xlink", - ) - .map_err(|_| "Couldn't make SVG")?; - - let filter = document - .create_element_ns(Some("http://www.w3.org/2000/svg"), "filter") - .map_err(|_| "Couldn't make SVG filter")?; - filter - .set_attribute("id", "_cm") - .map_err(|_| "Couldn't make SVG filter")?; - filter - .set_attribute("color-interpolation-filters", "sRGB") - .map_err(|_| "Couldn't make SVG filter")?; - - let color_matrix = document - .create_element_ns(Some("http://www.w3.org/2000/svg"), "feColorMatrix") - .map_err(|_| "Couldn't make SVG feColorMatrix element")?; - color_matrix - .set_attribute("type", "matrix") - .map_err(|_| "Couldn't make SVG feColorMatrix element")?; - color_matrix - .set_attribute("values", "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0") - .map_err(|_| "Couldn't make SVG feColorMatrix element")?; - - filter - .append_child(&color_matrix.clone()) - .map_err(|_| "append_child failed")?; - - svg.append_child(&filter) - .map_err(|_| "append_child failed")?; - - canvas - .append_child(&svg) - .map_err(|_| "append_child failed")?; - - // Check if we are on Firefox to use the color transform hack. - // TODO: We could turn this into a general util function to detect browser - // type, version, OS, etc. - let is_firefox = window - .navigator() - .user_agent() - .map(|s| s.contains("Firefox")) - .unwrap_or(false); - - let renderer = Self { - canvas: canvas.clone(), - color_matrix, - context, - shapes: vec![], - bitmaps: vec![], - id_to_bitmap: HashMap::new(), - viewport_width: 0, - viewport_height: 0, - use_color_transform_hack: is_firefox, - - // For rendering non-smoothed bitmaps. - // crisp-edges works in Firefox, pixelated works in Chrome (and others)? - pixelated_property_value: if is_firefox { - "crisp-edges" - } else { - "pixelated" - }, - }; - Ok(renderer) - } - - /// Converts an RGBA image into a PNG encoded as a base64 data URI. - fn rgba_to_png_data_uri( - rgba: &[u8], - width: u32, - height: u32, - ) -> Result> { - use png::{Encoder, HasParameters}; - let mut png_data: Vec = vec![]; - { - let mut encoder = Encoder::new(&mut png_data, width, height); - encoder.set(png::ColorType::RGBA).set(png::BitDepth::Eight); - let mut writer = encoder.write_header()?; - writer.write_image_data(&rgba)?; - } - - Ok(format!( - "data:image/png;base64,{}", - &base64::encode(&png_data[..]) - )) - } -} - -impl RenderBackend for WebCanvasRenderBackend { - fn set_viewport_dimensions(&mut self, width: u32, height: u32) { - self.viewport_width = width; - self.viewport_height = height; - } - - fn register_shape(&mut self, shape: &swf::Shape) -> ShapeHandle { - let handle = ShapeHandle(self.shapes.len()); - - let image = HtmlImageElement::new().unwrap(); - - let mut bitmaps = HashMap::new(); - for (id, handle) in &self.id_to_bitmap { - let bitmap_data = &self.bitmaps[handle.0]; - bitmaps.insert( - *id, - (&bitmap_data.data[..], bitmap_data.width, bitmap_data.height), - ); - } - - use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; - let svg = swf_shape_to_svg(&shape, &bitmaps, self.pixelated_property_value); - - let svg_encoded = format!( - "data:image/svg+xml,{}", - utf8_percent_encode(&svg, DEFAULT_ENCODE_SET) //&base64::encode(&svg[..]) - ); - - image.set_src(&svg_encoded); - - self.shapes.push(ShapeData { - image, - x_min: shape.shape_bounds.x_min.to_pixels(), - y_min: shape.shape_bounds.y_min.to_pixels(), - }); - - handle - } - - fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { - let bounds = glyph.bounds.clone().unwrap_or_else(|| { - ruffle_core::shape_utils::calculate_shape_bounds(&glyph.shape_records[..]) - }); - let shape = swf::Shape { - version: 2, - id: 0, - shape_bounds: bounds.clone(), - edge_bounds: bounds, - has_fill_winding_rule: false, - has_non_scaling_strokes: false, - has_scaling_strokes: true, - styles: swf::ShapeStyles { - fill_styles: vec![swf::FillStyle::Color(Color { - r: 255, - g: 255, - b: 255, - a: 255, - })], - line_styles: vec![], - }, - shape: glyph.shape_records.clone(), - }; - self.register_shape(&shape) - } - - fn register_bitmap_jpeg( - &mut self, - id: CharacterId, - data: &[u8], - jpeg_tables: &[u8], - ) -> BitmapHandle { - let mut full_jpeg = jpeg_tables[..jpeg_tables.len() - 2].to_vec(); - full_jpeg.extend_from_slice(&data[2..]); - - self.register_bitmap_jpeg_2(id, &full_jpeg[..]) - } - - fn register_bitmap_jpeg_2(&mut self, id: CharacterId, data: &[u8]) -> BitmapHandle { - let data = ruffle_core::backend::render::remove_invalid_jpeg_data(data); - let mut decoder = jpeg_decoder::Decoder::new(&data[..]); - decoder.read_info().unwrap(); - let metadata = decoder.info().unwrap(); - - let image = HtmlImageElement::new().unwrap(); - let jpeg_encoded = format!("data:image/jpeg;base64,{}", &base64::encode(&data[..])); - image.set_src(&jpeg_encoded); - - let handle = BitmapHandle(self.bitmaps.len()); - self.bitmaps.push(BitmapData { - image, - width: metadata.width.into(), - height: metadata.height.into(), - data: jpeg_encoded, - }); - self.id_to_bitmap.insert(id, handle); - handle - } - - fn register_bitmap_jpeg_3( - &mut self, - id: swf::CharacterId, - jpeg_data: &[u8], - alpha_data: &[u8], - ) -> BitmapHandle { - let (width, height, mut rgba) = - ruffle_core::backend::render::define_bits_jpeg_to_rgba(jpeg_data, alpha_data) - .expect("Error decoding DefineBitsJPEG3"); - - ruffle_core::backend::render::unmultiply_alpha_rgba(&mut rgba[..]); - let png = Self::rgba_to_png_data_uri(&rgba[..], width, height).unwrap(); - - let image = HtmlImageElement::new().unwrap(); - image.set_src(&png); - - let handle = BitmapHandle(self.bitmaps.len()); - self.bitmaps.push(BitmapData { - image, - width, - height, - data: png, - }); - - self.id_to_bitmap.insert(id, handle); - handle - } - - fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { - let mut rgba = ruffle_core::backend::render::define_bits_lossless_to_rgba(swf_tag) - .expect("Error decoding DefineBitsLossless"); - - ruffle_core::backend::render::unmultiply_alpha_rgba(&mut rgba[..]); - - let png = - Self::rgba_to_png_data_uri(&rgba[..], swf_tag.width.into(), swf_tag.height.into()) - .unwrap(); - - let image = HtmlImageElement::new().unwrap(); - image.set_src(&png); - - let handle = BitmapHandle(self.bitmaps.len()); - self.bitmaps.push(BitmapData { - image, - width: swf_tag.width.into(), - height: swf_tag.height.into(), - data: png, - }); - self.id_to_bitmap.insert(swf_tag.id, handle); - handle - } - - fn begin_frame(&mut self) { - // Reset canvas transform in case it was left in a dirty state. - self.context.reset_transform().unwrap(); - } - - fn end_frame(&mut self) { - // Noop - } - - fn clear(&mut self, color: Color) { - let width = self.canvas.width(); - let height = self.canvas.height(); - - let color = format!("rgb({}, {}, {})", color.r, color.g, color.b); - self.context.set_fill_style(&color.into()); - self.context - .fill_rect(0.0, 0.0, width.into(), height.into()); - } - - #[allow(clippy::float_cmp)] - fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) { - let shape = if let Some(shape) = self.shapes.get(shape.0) { - shape - } else { - return; - }; - - let matrix = transform.matrix; //self.view_matrix * transform.matrix; - - self.context - .set_transform( - matrix.a.into(), - matrix.b.into(), - matrix.c.into(), - matrix.d.into(), - f64::from(matrix.tx) / 20.0, - f64::from(matrix.ty) / 20.0, - ) - .unwrap(); - - let color_transform = &transform.color_transform; - if color_transform.r_mult == 1.0 - && color_transform.g_mult == 1.0 - && color_transform.b_mult == 1.0 - && color_transform.r_add == 0.0 - && color_transform.g_add == 0.0 - && color_transform.b_add == 0.0 - && color_transform.a_add == 0.0 - { - self.context.set_global_alpha(color_transform.a_mult.into()); - } else { - // TODO HACK: Firefox is having issues with additive alpha in color transforms (see #38). - // Hack this away and just use multiplicative (not accurate in many cases, but won't look awful). - let (a_mult, a_add) = if self.use_color_transform_hack && color_transform.a_add != 0.0 { - (color_transform.a_mult + color_transform.a_add, 0.0) - } else { - (color_transform.a_mult, color_transform.a_add) - }; - - let matrix_str = format!( - "{} 0 0 0 {} 0 {} 0 0 {} 0 0 {} 0 {} 0 0 0 {} {}", - color_transform.r_mult, - color_transform.r_add, - color_transform.g_mult, - color_transform.g_add, - color_transform.b_mult, - color_transform.b_add, - a_mult, - a_add - ); - self.color_matrix - .set_attribute("values", &matrix_str) - .unwrap(); - - self.context.set_filter("url('#_cm')"); - } - - self.context - .draw_image_with_html_image_element(&shape.image, shape.x_min, shape.y_min) - .unwrap(); - - self.context.set_filter("none"); - self.context.set_global_alpha(1.0); - } - - fn draw_pause_overlay(&mut self) { - let width = f64::from(self.canvas.width()); - let height = f64::from(self.canvas.height()); - self.context.set_fill_style(&"rgba(0, 0, 0, 0.5)".into()); - self.context.fill_rect(0.0, 0.0, width, height); - self.context.set_text_align("center"); - self.context.set_fill_style(&"white".into()); - self.context - .set_font(&format!("bold {}px sans-serif", height * 0.1)); - let _ = self - .context - .fill_text("Click to Play", width / 2.0, height / 2.0); - } - - fn draw_letterbox(&mut self, letterbox: Letterbox) { - self.context.reset_transform().unwrap(); - self.context.set_fill_style(&"black".into()); - - match letterbox { - Letterbox::None => (), - Letterbox::Letterbox(margin_height) => { - self.context - .fill_rect(0.0, 0.0, self.viewport_width.into(), margin_height.into()); - self.context.fill_rect( - 0.0, - (self.viewport_height as f32 - margin_height).into(), - self.viewport_width.into(), - self.viewport_height.into(), - ); - } - Letterbox::Pillarbox(margin_width) => { - self.context - .fill_rect(0.0, 0.0, margin_width.into(), self.viewport_height.into()); - self.context.fill_rect( - (self.viewport_width as f32 - margin_width).into(), - 0.0, - margin_width.into(), - self.viewport_height.into(), - ); - } - } - } -} - -fn swf_shape_to_svg( - shape: &swf::Shape, - bitmaps: &HashMap, - pixelated_property_value: &str, -) -> String { - use fnv::FnvHashSet; - use ruffle_core::matrix::Matrix; - use ruffle_core::shape_utils::{swf_shape_to_paths, DrawCommand, DrawPath}; - use svg::node::element::{ - path::Data, Definitions, Image, LinearGradient, Path as SvgPath, Pattern, RadialGradient, - Stop, - }; - use svg::Document; - use swf::{FillStyle, LineCapStyle, LineJoinStyle}; - - // Some browsers will vomit if you try to load/draw an image with 0 width/height. - // TODO(Herschel): Might be better to just return None in this case and skip - // rendering altogether. - let (width, height) = ( - f32::max( - (shape.shape_bounds.x_max - shape.shape_bounds.x_min).to_pixels() as f32, - 1.0, - ), - f32::max( - (shape.shape_bounds.y_max - shape.shape_bounds.y_min).to_pixels() as f32, - 1.0, - ), - ); - let mut document = Document::new() - .set("width", width) - .set("height", height) - .set( - "viewBox", - ( - shape.shape_bounds.x_min.get(), - shape.shape_bounds.y_min.get(), - (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get(), - (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get(), - ), - ) - // preserveAspectRatio must be off or Firefox will fudge with the dimensions when we draw an image onto canvas. - .set("preserveAspectRatio", "none") - .set("xmlns:xlink", "http://www.w3.org/1999/xlink"); - - let width = (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get() as f32; - let height = (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get() as f32; - - let mut bitmap_defs: FnvHashSet = FnvHashSet::default(); - - let mut defs = Definitions::new(); - let mut num_defs = 0; - - let mut svg_paths = vec![]; - let paths = swf_shape_to_paths(shape); - for path in paths { - match path { - DrawPath::Fill { style, commands } => { - let mut svg_path = SvgPath::new(); - - svg_path = svg_path.set( - "fill", - match style { - FillStyle::Color(Color { r, g, b, a }) => { - format!("rgba({},{},{},{})", r, g, b, f32::from(*a) / 255.0) - } - FillStyle::LinearGradient(gradient) => { - let matrix: Matrix = Matrix::from(gradient.matrix.clone()); - let shift = Matrix { - a: 32768.0 / width, - d: 32768.0 / height, - tx: -16384.0, - ty: -16384.0, - ..Default::default() - }; - let gradient_matrix = matrix * shift; - - let mut svg_gradient = LinearGradient::new() - .set("id", format!("f{}", num_defs)) - .set("gradientUnits", "userSpaceOnUse") - .set( - "gradientTransform", - format!( - "matrix({} {} {} {} {} {})", - gradient_matrix.a, - gradient_matrix.b, - gradient_matrix.c, - gradient_matrix.d, - gradient_matrix.tx, - gradient_matrix.ty - ), - ); - for record in &gradient.records { - let stop = Stop::new() - .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) - .set( - "stop-color", - format!( - "rgba({},{},{},{})", - record.color.r, - record.color.g, - record.color.b, - f32::from(record.color.a) / 255.0 - ), - ); - svg_gradient = svg_gradient.add(stop); - } - defs = defs.add(svg_gradient); - - let fill_id = format!("url(#f{})", num_defs); - num_defs += 1; - fill_id - } - FillStyle::RadialGradient(gradient) => { - let matrix = Matrix::from(gradient.matrix.clone()); - let shift = Matrix { - a: 32768.0 / width, - d: 32768.0 / height, - tx: -16384.0, - ty: -16384.0, - ..Default::default() - }; - let gradient_matrix = matrix * shift; - - let mut svg_gradient = RadialGradient::new() - .set("id", format!("f{}", num_defs)) - .set("gradientUnits", "userSpaceOnUse") - .set( - "gradientTransform", - format!( - "matrix({} {} {} {} {} {})", - gradient_matrix.a, - gradient_matrix.b, - gradient_matrix.c, - gradient_matrix.d, - gradient_matrix.tx, - gradient_matrix.ty - ), - ); - for record in &gradient.records { - let stop = Stop::new() - .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) - .set( - "stop-color", - format!( - "rgba({},{},{},{})", - record.color.r, - record.color.g, - record.color.b, - record.color.a - ), - ); - svg_gradient = svg_gradient.add(stop); - } - defs = defs.add(svg_gradient); - - let fill_id = format!("url(#f{})", num_defs); - num_defs += 1; - fill_id - } - FillStyle::FocalGradient { - gradient, - focal_point, - } => { - let matrix = Matrix::from(gradient.matrix.clone()); - let shift = Matrix { - a: 32768.0 / width, - d: 32768.0 / height, - tx: -16384.0, - ty: -16384.0, - ..Default::default() - }; - let gradient_matrix = matrix * shift; - - let mut svg_gradient = RadialGradient::new() - .set("id", format!("f{}", num_defs)) - .set("fx", -focal_point) - .set("gradientUnits", "userSpaceOnUse") - .set( - "gradientTransform", - format!( - "matrix({} {} {} {} {} {})", - gradient_matrix.a, - gradient_matrix.b, - gradient_matrix.c, - gradient_matrix.d, - gradient_matrix.tx, - gradient_matrix.ty - ), - ); - for record in &gradient.records { - let stop = Stop::new() - .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) - .set( - "stop-color", - format!( - "rgba({},{},{},{})", - record.color.r, - record.color.g, - record.color.b, - record.color.a - ), - ); - svg_gradient = svg_gradient.add(stop); - } - defs = defs.add(svg_gradient); - - let fill_id = format!("url(#f{})", num_defs); - num_defs += 1; - fill_id - } - FillStyle::Bitmap { - id, - matrix, - is_smoothed, - is_repeating, - } => { - let (bitmap_data, bitmap_width, bitmap_height) = - bitmaps.get(&id).unwrap_or(&("", 0, 0)); - - if !bitmap_defs.contains(&id) { - let mut image = Image::new() - .set("width", *bitmap_width) - .set("height", *bitmap_height) - .set("xlink:href", *bitmap_data); - - if !*is_smoothed { - image = image.set("image-rendering", pixelated_property_value); - } - - let mut bitmap_pattern = Pattern::new() - .set("id", format!("b{}", id)) - .set("patternUnits", "userSpaceOnUse"); - - if !*is_repeating { - bitmap_pattern = bitmap_pattern - .set("width", *bitmap_width) - .set("height", *bitmap_height); - } else { - bitmap_pattern = bitmap_pattern - .set("width", *bitmap_width) - .set("height", *bitmap_height) - .set( - "viewBox", - format!("0 0 {} {}", bitmap_width, bitmap_height), - ); - } - - bitmap_pattern = bitmap_pattern.add(image); - - defs = defs.add(bitmap_pattern); - bitmap_defs.insert(*id); - } - let a = Matrix::from(matrix.clone()); - let bitmap_matrix = a; - - let svg_pattern = Pattern::new() - .set("id", format!("f{}", num_defs)) - .set("xlink:href", format!("#b{}", id)) - .set( - "patternTransform", - format!( - "matrix({} {} {} {} {} {})", - bitmap_matrix.a, - bitmap_matrix.b, - bitmap_matrix.c, - bitmap_matrix.d, - bitmap_matrix.tx, - bitmap_matrix.ty - ), - ); - - defs = defs.add(svg_pattern); - - let fill_id = format!("url(#f{})", num_defs); - num_defs += 1; - fill_id - } - }, - ); - - let mut data = Data::new(); - for command in commands { - data = match command { - DrawCommand::MoveTo { x, y } => data.move_to((x.get(), y.get())), - DrawCommand::LineTo { x, y } => data.line_to((x.get(), y.get())), - DrawCommand::CurveTo { x1, y1, x2, y2 } => { - data.quadratic_curve_to((x1.get(), y1.get(), x2.get(), y2.get())) - } - }; - } - - svg_path = svg_path.set("d", data); - svg_paths.push(svg_path); - } - DrawPath::Stroke { - style, - commands, - is_closed, - } => { - let mut svg_path = SvgPath::new(); - svg_path = svg_path - .set("fill", "none") - .set( - "stroke", - format!( - "rgba({},{},{},{})", - style.color.r, style.color.g, style.color.b, style.color.a - ), - ) - .set("stroke-width", style.width.get()) - .set( - "stroke-linecap", - match style.start_cap { - LineCapStyle::Round => "round", - LineCapStyle::Square => "square", - LineCapStyle::None => "butt", - }, - ) - .set( - "stroke-linejoin", - match style.join_style { - LineJoinStyle::Round => "round", - LineJoinStyle::Bevel => "bevel", - LineJoinStyle::Miter(_) => "miter", - }, - ); - - if let LineJoinStyle::Miter(miter_limit) = style.join_style { - svg_path = svg_path.set("stroke-miterlimit", miter_limit); - } - - let mut data = Data::new(); - for command in commands { - data = match command { - DrawCommand::MoveTo { x, y } => data.move_to((x.get(), y.get())), - DrawCommand::LineTo { x, y } => data.line_to((x.get(), y.get())), - DrawCommand::CurveTo { x1, y1, x2, y2 } => { - data.quadratic_curve_to((x1.get(), y1.get(), x2.get(), y2.get())) - } - }; - } - if is_closed { - data = data.close(); - } - - svg_path = svg_path.set("d", data); - svg_paths.push(svg_path); - } - } - } - - if num_defs > 0 { - document = document.add(defs); - } - - for svg_path in svg_paths { - document = document.add(svg_path); - } - - document.to_string() -} +use ruffle_core::backend::render::{ + swf, swf::CharacterId, BitmapHandle, Color, Letterbox, RenderBackend, ShapeHandle, Transform, +}; +use std::collections::HashMap; +use wasm_bindgen::JsCast; +use web_sys::{CanvasRenderingContext2d, Element, HtmlCanvasElement, HtmlImageElement}; + +pub struct WebCanvasRenderBackend { + canvas: HtmlCanvasElement, + context: CanvasRenderingContext2d, + color_matrix: Element, + shapes: Vec, + bitmaps: Vec, + id_to_bitmap: HashMap, + viewport_width: u32, + viewport_height: u32, + use_color_transform_hack: bool, + pixelated_property_value: &'static str, +} + +struct ShapeData { + image: HtmlImageElement, + x_min: f64, + y_min: f64, +} + +#[allow(dead_code)] +struct BitmapData { + image: HtmlImageElement, + width: u32, + height: u32, + data: String, +} + +impl WebCanvasRenderBackend { + pub fn new(canvas: &HtmlCanvasElement) -> Result> { + // Request the CanvasRenderingContext2d. + // Disable alpha for possible speedup. + // TODO: Allow user to enable transparent background (transparent wmode in legacy Flash). + let context_options = js_sys::Object::new(); + let _ = js_sys::Reflect::set( + &context_options, + &"alpha".into(), + &wasm_bindgen::JsValue::FALSE, + ); + let context: CanvasRenderingContext2d = canvas + .get_context_with_context_options("2d", &context_options) + .map_err(|_| "Could not create context")? + .ok_or("Could not create context")? + .dyn_into() + .map_err(|_| "Expected CanvasRenderingContext2d")?; + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + + // Create a color matrix filter to handle Flash color effects. + // Ensure a previous instance of the color matrix filter node doesn't exist. + // TODO: Remove it in player.destroy()? This is dangerous if the client page has something with this id... + if let Some(element) = document.get_element_by_id("_svgfilter") { + element.remove(); + } + + let svg = document + .create_element_ns(Some("http://www.w3.org/2000/svg"), "svg") + .map_err(|_| "Couldn't make SVG")?; + + svg.set_id("_svgfilter"); + + svg.set_attribute("width", "0") + .map_err(|_| "Couldn't make SVG")?; + + svg.set_attribute("height", "0") + .map_err(|_| "Couldn't make SVG")?; + + svg.set_attribute_ns( + Some("http://www.w3.org/2000/xmlns/"), + "xmlns:xlink", + "http://www.w3.org/1999/xlink", + ) + .map_err(|_| "Couldn't make SVG")?; + + let filter = document + .create_element_ns(Some("http://www.w3.org/2000/svg"), "filter") + .map_err(|_| "Couldn't make SVG filter")?; + filter + .set_attribute("id", "_cm") + .map_err(|_| "Couldn't make SVG filter")?; + filter + .set_attribute("color-interpolation-filters", "sRGB") + .map_err(|_| "Couldn't make SVG filter")?; + + let color_matrix = document + .create_element_ns(Some("http://www.w3.org/2000/svg"), "feColorMatrix") + .map_err(|_| "Couldn't make SVG feColorMatrix element")?; + color_matrix + .set_attribute("type", "matrix") + .map_err(|_| "Couldn't make SVG feColorMatrix element")?; + color_matrix + .set_attribute("values", "1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0") + .map_err(|_| "Couldn't make SVG feColorMatrix element")?; + + filter + .append_child(&color_matrix.clone()) + .map_err(|_| "append_child failed")?; + + svg.append_child(&filter) + .map_err(|_| "append_child failed")?; + + canvas + .append_child(&svg) + .map_err(|_| "append_child failed")?; + + // Check if we are on Firefox to use the color transform hack. + // TODO: We could turn this into a general util function to detect browser + // type, version, OS, etc. + let is_firefox = window + .navigator() + .user_agent() + .map(|s| s.contains("Firefox")) + .unwrap_or(false); + + let renderer = Self { + canvas: canvas.clone(), + color_matrix, + context, + shapes: vec![], + bitmaps: vec![], + id_to_bitmap: HashMap::new(), + viewport_width: 0, + viewport_height: 0, + use_color_transform_hack: is_firefox, + + // For rendering non-smoothed bitmaps. + // crisp-edges works in Firefox, pixelated works in Chrome (and others)? + pixelated_property_value: if is_firefox { + "crisp-edges" + } else { + "pixelated" + }, + }; + Ok(renderer) + } + + /// Converts an RGBA image into a PNG encoded as a base64 data URI. + fn rgba_to_png_data_uri( + rgba: &[u8], + width: u32, + height: u32, + ) -> Result> { + use png::{Encoder, HasParameters}; + let mut png_data: Vec = vec![]; + { + let mut encoder = Encoder::new(&mut png_data, width, height); + encoder.set(png::ColorType::RGBA).set(png::BitDepth::Eight); + let mut writer = encoder.write_header()?; + writer.write_image_data(&rgba)?; + } + + Ok(format!( + "data:image/png;base64,{}", + &base64::encode(&png_data[..]) + )) + } +} + +impl RenderBackend for WebCanvasRenderBackend { + fn set_viewport_dimensions(&mut self, width: u32, height: u32) { + self.viewport_width = width; + self.viewport_height = height; + } + + fn register_shape(&mut self, shape: &swf::Shape) -> ShapeHandle { + let handle = ShapeHandle(self.shapes.len()); + + let image = HtmlImageElement::new().unwrap(); + + let mut bitmaps = HashMap::new(); + for (id, handle) in &self.id_to_bitmap { + let bitmap_data = &self.bitmaps[handle.0]; + bitmaps.insert( + *id, + (&bitmap_data.data[..], bitmap_data.width, bitmap_data.height), + ); + } + + use url::percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; + let svg = swf_shape_to_svg(&shape, &bitmaps, self.pixelated_property_value); + + let svg_encoded = format!( + "data:image/svg+xml,{}", + utf8_percent_encode(&svg, DEFAULT_ENCODE_SET) //&base64::encode(&svg[..]) + ); + + image.set_src(&svg_encoded); + + self.shapes.push(ShapeData { + image, + x_min: shape.shape_bounds.x_min.to_pixels(), + y_min: shape.shape_bounds.y_min.to_pixels(), + }); + + handle + } + + fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { + let bounds = glyph.bounds.clone().unwrap_or_else(|| { + ruffle_core::shape_utils::calculate_shape_bounds(&glyph.shape_records[..]) + }); + let shape = swf::Shape { + version: 2, + id: 0, + shape_bounds: bounds.clone(), + edge_bounds: bounds, + has_fill_winding_rule: false, + has_non_scaling_strokes: false, + has_scaling_strokes: true, + styles: swf::ShapeStyles { + fill_styles: vec![swf::FillStyle::Color(Color { + r: 255, + g: 255, + b: 255, + a: 255, + })], + line_styles: vec![], + }, + shape: glyph.shape_records.clone(), + }; + self.register_shape(&shape) + } + + fn register_bitmap_jpeg( + &mut self, + id: CharacterId, + data: &[u8], + jpeg_tables: &[u8], + ) -> BitmapHandle { + let mut full_jpeg = jpeg_tables[..jpeg_tables.len() - 2].to_vec(); + full_jpeg.extend_from_slice(&data[2..]); + + self.register_bitmap_jpeg_2(id, &full_jpeg[..]) + } + + fn register_bitmap_jpeg_2(&mut self, id: CharacterId, data: &[u8]) -> BitmapHandle { + let data = ruffle_core::backend::render::remove_invalid_jpeg_data(data); + let mut decoder = jpeg_decoder::Decoder::new(&data[..]); + decoder.read_info().unwrap(); + let metadata = decoder.info().unwrap(); + + let image = HtmlImageElement::new().unwrap(); + let jpeg_encoded = format!("data:image/jpeg;base64,{}", &base64::encode(&data[..])); + image.set_src(&jpeg_encoded); + + let handle = BitmapHandle(self.bitmaps.len()); + self.bitmaps.push(BitmapData { + image, + width: metadata.width.into(), + height: metadata.height.into(), + data: jpeg_encoded, + }); + self.id_to_bitmap.insert(id, handle); + handle + } + + fn register_bitmap_jpeg_3( + &mut self, + id: swf::CharacterId, + jpeg_data: &[u8], + alpha_data: &[u8], + ) -> BitmapHandle { + let (width, height, mut rgba) = + ruffle_core::backend::render::define_bits_jpeg_to_rgba(jpeg_data, alpha_data) + .expect("Error decoding DefineBitsJPEG3"); + + ruffle_core::backend::render::unmultiply_alpha_rgba(&mut rgba[..]); + let png = Self::rgba_to_png_data_uri(&rgba[..], width, height).unwrap(); + + let image = HtmlImageElement::new().unwrap(); + image.set_src(&png); + + let handle = BitmapHandle(self.bitmaps.len()); + self.bitmaps.push(BitmapData { + image, + width, + height, + data: png, + }); + + self.id_to_bitmap.insert(id, handle); + handle + } + + fn register_bitmap_png(&mut self, swf_tag: &swf::DefineBitsLossless) -> BitmapHandle { + let mut rgba = ruffle_core::backend::render::define_bits_lossless_to_rgba(swf_tag) + .expect("Error decoding DefineBitsLossless"); + + ruffle_core::backend::render::unmultiply_alpha_rgba(&mut rgba[..]); + + let png = + Self::rgba_to_png_data_uri(&rgba[..], swf_tag.width.into(), swf_tag.height.into()) + .unwrap(); + + let image = HtmlImageElement::new().unwrap(); + image.set_src(&png); + + let handle = BitmapHandle(self.bitmaps.len()); + self.bitmaps.push(BitmapData { + image, + width: swf_tag.width.into(), + height: swf_tag.height.into(), + data: png, + }); + self.id_to_bitmap.insert(swf_tag.id, handle); + handle + } + + fn begin_frame(&mut self) { + // Reset canvas transform in case it was left in a dirty state. + self.context.reset_transform().unwrap(); + } + + fn end_frame(&mut self) { + // Noop + } + + fn clear(&mut self, color: Color) { + let width = self.canvas.width(); + let height = self.canvas.height(); + + let color = format!("rgb({}, {}, {})", color.r, color.g, color.b); + self.context.set_fill_style(&color.into()); + self.context + .fill_rect(0.0, 0.0, width.into(), height.into()); + } + + #[allow(clippy::float_cmp)] + fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) { + let shape = if let Some(shape) = self.shapes.get(shape.0) { + shape + } else { + return; + }; + + let matrix = transform.matrix; //self.view_matrix * transform.matrix; + + self.context + .set_transform( + matrix.a.into(), + matrix.b.into(), + matrix.c.into(), + matrix.d.into(), + f64::from(matrix.tx) / 20.0, + f64::from(matrix.ty) / 20.0, + ) + .unwrap(); + + let color_transform = &transform.color_transform; + if color_transform.r_mult == 1.0 + && color_transform.g_mult == 1.0 + && color_transform.b_mult == 1.0 + && color_transform.r_add == 0.0 + && color_transform.g_add == 0.0 + && color_transform.b_add == 0.0 + && color_transform.a_add == 0.0 + { + self.context.set_global_alpha(color_transform.a_mult.into()); + } else { + // TODO HACK: Firefox is having issues with additive alpha in color transforms (see #38). + // Hack this away and just use multiplicative (not accurate in many cases, but won't look awful). + let (a_mult, a_add) = if self.use_color_transform_hack && color_transform.a_add != 0.0 { + (color_transform.a_mult + color_transform.a_add, 0.0) + } else { + (color_transform.a_mult, color_transform.a_add) + }; + + let matrix_str = format!( + "{} 0 0 0 {} 0 {} 0 0 {} 0 0 {} 0 {} 0 0 0 {} {}", + color_transform.r_mult, + color_transform.r_add, + color_transform.g_mult, + color_transform.g_add, + color_transform.b_mult, + color_transform.b_add, + a_mult, + a_add + ); + self.color_matrix + .set_attribute("values", &matrix_str) + .unwrap(); + + self.context.set_filter("url('#_cm')"); + } + + self.context + .draw_image_with_html_image_element(&shape.image, shape.x_min, shape.y_min) + .unwrap(); + + self.context.set_filter("none"); + self.context.set_global_alpha(1.0); + } + + fn draw_pause_overlay(&mut self) { + let width = f64::from(self.canvas.width()); + let height = f64::from(self.canvas.height()); + self.context.set_fill_style(&"rgba(0, 0, 0, 0.5)".into()); + self.context.fill_rect(0.0, 0.0, width, height); + self.context.set_text_align("center"); + self.context.set_fill_style(&"white".into()); + self.context + .set_font(&format!("bold {}px sans-serif", height * 0.1)); + let _ = self + .context + .fill_text("Click to Play", width / 2.0, height / 2.0); + } + + fn draw_letterbox(&mut self, letterbox: Letterbox) { + self.context.reset_transform().unwrap(); + self.context.set_fill_style(&"black".into()); + + match letterbox { + Letterbox::None => (), + Letterbox::Letterbox(margin_height) => { + self.context + .fill_rect(0.0, 0.0, self.viewport_width.into(), margin_height.into()); + self.context.fill_rect( + 0.0, + (self.viewport_height as f32 - margin_height).into(), + self.viewport_width.into(), + self.viewport_height.into(), + ); + } + Letterbox::Pillarbox(margin_width) => { + self.context + .fill_rect(0.0, 0.0, margin_width.into(), self.viewport_height.into()); + self.context.fill_rect( + (self.viewport_width as f32 - margin_width).into(), + 0.0, + margin_width.into(), + self.viewport_height.into(), + ); + } + } + } +} + +fn swf_shape_to_svg( + shape: &swf::Shape, + bitmaps: &HashMap, + pixelated_property_value: &str, +) -> String { + use fnv::FnvHashSet; + use ruffle_core::matrix::Matrix; + use ruffle_core::shape_utils::{swf_shape_to_paths, DrawCommand, DrawPath}; + use svg::node::element::{ + path::Data, Definitions, Image, LinearGradient, Path as SvgPath, Pattern, RadialGradient, + Stop, + }; + use svg::Document; + use swf::{FillStyle, LineCapStyle, LineJoinStyle}; + + // Some browsers will vomit if you try to load/draw an image with 0 width/height. + // TODO(Herschel): Might be better to just return None in this case and skip + // rendering altogether. + let (width, height) = ( + f32::max( + (shape.shape_bounds.x_max - shape.shape_bounds.x_min).to_pixels() as f32, + 1.0, + ), + f32::max( + (shape.shape_bounds.y_max - shape.shape_bounds.y_min).to_pixels() as f32, + 1.0, + ), + ); + let mut document = Document::new() + .set("width", width) + .set("height", height) + .set( + "viewBox", + ( + shape.shape_bounds.x_min.get(), + shape.shape_bounds.y_min.get(), + (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get(), + (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get(), + ), + ) + // preserveAspectRatio must be off or Firefox will fudge with the dimensions when we draw an image onto canvas. + .set("preserveAspectRatio", "none") + .set("xmlns:xlink", "http://www.w3.org/1999/xlink"); + + let width = (shape.shape_bounds.x_max - shape.shape_bounds.x_min).get() as f32; + let height = (shape.shape_bounds.y_max - shape.shape_bounds.y_min).get() as f32; + + let mut bitmap_defs: FnvHashSet = FnvHashSet::default(); + + let mut defs = Definitions::new(); + let mut num_defs = 0; + + let mut svg_paths = vec![]; + let paths = swf_shape_to_paths(shape); + for path in paths { + match path { + DrawPath::Fill { style, commands } => { + let mut svg_path = SvgPath::new(); + + svg_path = svg_path.set( + "fill", + match style { + FillStyle::Color(Color { r, g, b, a }) => { + format!("rgba({},{},{},{})", r, g, b, f32::from(*a) / 255.0) + } + FillStyle::LinearGradient(gradient) => { + let matrix: Matrix = Matrix::from(gradient.matrix.clone()); + let shift = Matrix { + a: 32768.0 / width, + d: 32768.0 / height, + tx: -16384.0, + ty: -16384.0, + ..Default::default() + }; + let gradient_matrix = matrix * shift; + + let mut svg_gradient = LinearGradient::new() + .set("id", format!("f{}", num_defs)) + .set("gradientUnits", "userSpaceOnUse") + .set( + "gradientTransform", + format!( + "matrix({} {} {} {} {} {})", + gradient_matrix.a, + gradient_matrix.b, + gradient_matrix.c, + gradient_matrix.d, + gradient_matrix.tx, + gradient_matrix.ty + ), + ); + for record in &gradient.records { + let stop = Stop::new() + .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) + .set( + "stop-color", + format!( + "rgba({},{},{},{})", + record.color.r, + record.color.g, + record.color.b, + f32::from(record.color.a) / 255.0 + ), + ); + svg_gradient = svg_gradient.add(stop); + } + defs = defs.add(svg_gradient); + + let fill_id = format!("url(#f{})", num_defs); + num_defs += 1; + fill_id + } + FillStyle::RadialGradient(gradient) => { + let matrix = Matrix::from(gradient.matrix.clone()); + let shift = Matrix { + a: 32768.0 / width, + d: 32768.0 / height, + tx: -16384.0, + ty: -16384.0, + ..Default::default() + }; + let gradient_matrix = matrix * shift; + + let mut svg_gradient = RadialGradient::new() + .set("id", format!("f{}", num_defs)) + .set("gradientUnits", "userSpaceOnUse") + .set( + "gradientTransform", + format!( + "matrix({} {} {} {} {} {})", + gradient_matrix.a, + gradient_matrix.b, + gradient_matrix.c, + gradient_matrix.d, + gradient_matrix.tx, + gradient_matrix.ty + ), + ); + for record in &gradient.records { + let stop = Stop::new() + .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) + .set( + "stop-color", + format!( + "rgba({},{},{},{})", + record.color.r, + record.color.g, + record.color.b, + record.color.a + ), + ); + svg_gradient = svg_gradient.add(stop); + } + defs = defs.add(svg_gradient); + + let fill_id = format!("url(#f{})", num_defs); + num_defs += 1; + fill_id + } + FillStyle::FocalGradient { + gradient, + focal_point, + } => { + let matrix = Matrix::from(gradient.matrix.clone()); + let shift = Matrix { + a: 32768.0 / width, + d: 32768.0 / height, + tx: -16384.0, + ty: -16384.0, + ..Default::default() + }; + let gradient_matrix = matrix * shift; + + let mut svg_gradient = RadialGradient::new() + .set("id", format!("f{}", num_defs)) + .set("fx", -focal_point) + .set("gradientUnits", "userSpaceOnUse") + .set( + "gradientTransform", + format!( + "matrix({} {} {} {} {} {})", + gradient_matrix.a, + gradient_matrix.b, + gradient_matrix.c, + gradient_matrix.d, + gradient_matrix.tx, + gradient_matrix.ty + ), + ); + for record in &gradient.records { + let stop = Stop::new() + .set("offset", format!("{}%", f32::from(record.ratio) / 2.55)) + .set( + "stop-color", + format!( + "rgba({},{},{},{})", + record.color.r, + record.color.g, + record.color.b, + record.color.a + ), + ); + svg_gradient = svg_gradient.add(stop); + } + defs = defs.add(svg_gradient); + + let fill_id = format!("url(#f{})", num_defs); + num_defs += 1; + fill_id + } + FillStyle::Bitmap { + id, + matrix, + is_smoothed, + is_repeating, + } => { + let (bitmap_data, bitmap_width, bitmap_height) = + bitmaps.get(&id).unwrap_or(&("", 0, 0)); + + if !bitmap_defs.contains(&id) { + let mut image = Image::new() + .set("width", *bitmap_width) + .set("height", *bitmap_height) + .set("xlink:href", *bitmap_data); + + if !*is_smoothed { + image = image.set("image-rendering", pixelated_property_value); + } + + let mut bitmap_pattern = Pattern::new() + .set("id", format!("b{}", id)) + .set("patternUnits", "userSpaceOnUse"); + + if !*is_repeating { + bitmap_pattern = bitmap_pattern + .set("width", *bitmap_width) + .set("height", *bitmap_height); + } else { + bitmap_pattern = bitmap_pattern + .set("width", *bitmap_width) + .set("height", *bitmap_height) + .set( + "viewBox", + format!("0 0 {} {}", bitmap_width, bitmap_height), + ); + } + + bitmap_pattern = bitmap_pattern.add(image); + + defs = defs.add(bitmap_pattern); + bitmap_defs.insert(*id); + } + let a = Matrix::from(matrix.clone()); + let bitmap_matrix = a; + + let svg_pattern = Pattern::new() + .set("id", format!("f{}", num_defs)) + .set("xlink:href", format!("#b{}", id)) + .set( + "patternTransform", + format!( + "matrix({} {} {} {} {} {})", + bitmap_matrix.a, + bitmap_matrix.b, + bitmap_matrix.c, + bitmap_matrix.d, + bitmap_matrix.tx, + bitmap_matrix.ty + ), + ); + + defs = defs.add(svg_pattern); + + let fill_id = format!("url(#f{})", num_defs); + num_defs += 1; + fill_id + } + }, + ); + + let mut data = Data::new(); + for command in commands { + data = match command { + DrawCommand::MoveTo { x, y } => data.move_to((x.get(), y.get())), + DrawCommand::LineTo { x, y } => data.line_to((x.get(), y.get())), + DrawCommand::CurveTo { x1, y1, x2, y2 } => { + data.quadratic_curve_to((x1.get(), y1.get(), x2.get(), y2.get())) + } + }; + } + + svg_path = svg_path.set("d", data); + svg_paths.push(svg_path); + } + DrawPath::Stroke { + style, + commands, + is_closed, + } => { + let mut svg_path = SvgPath::new(); + svg_path = svg_path + .set("fill", "none") + .set( + "stroke", + format!( + "rgba({},{},{},{})", + style.color.r, style.color.g, style.color.b, style.color.a + ), + ) + .set("stroke-width", style.width.get()) + .set( + "stroke-linecap", + match style.start_cap { + LineCapStyle::Round => "round", + LineCapStyle::Square => "square", + LineCapStyle::None => "butt", + }, + ) + .set( + "stroke-linejoin", + match style.join_style { + LineJoinStyle::Round => "round", + LineJoinStyle::Bevel => "bevel", + LineJoinStyle::Miter(_) => "miter", + }, + ); + + if let LineJoinStyle::Miter(miter_limit) = style.join_style { + svg_path = svg_path.set("stroke-miterlimit", miter_limit); + } + + let mut data = Data::new(); + for command in commands { + data = match command { + DrawCommand::MoveTo { x, y } => data.move_to((x.get(), y.get())), + DrawCommand::LineTo { x, y } => data.line_to((x.get(), y.get())), + DrawCommand::CurveTo { x1, y1, x2, y2 } => { + data.quadratic_curve_to((x1.get(), y1.get(), x2.get(), y2.get())) + } + }; + } + if is_closed { + data = data.close(); + } + + svg_path = svg_path.set("d", data); + svg_paths.push(svg_path); + } + } + } + + if num_defs > 0 { + document = document.add(defs); + } + + for svg_path in svg_paths { + document = document.add(svg_path); + } + + document.to_string() +}