From ac7086528fd8b415f195dbc15613d7cdcfbd90d9 Mon Sep 17 00:00:00 2001 From: Moulins Date: Tue, 23 Jan 2024 04:28:22 +0100 Subject: [PATCH] avm1: Implement proper behavior for Gradient***Filter array setters Among other things: - resizing `colors` should also resize `alphas` and `ratios` - shrinking `ratios` should also shrink `colors` and `alphas`, but growing it doesn't change the size --- core/src/avm1/globals/gradient_filter.rs | 106 +++++++++--------- swf/src/types.rs | 2 +- .../tests/swfs/avm1/bitmap_filters/test.toml | 1 - 3 files changed, 54 insertions(+), 55 deletions(-) diff --git a/core/src/avm1/globals/gradient_filter.rs b/core/src/avm1/globals/gradient_filter.rs index ad1ccc22b..4b1fe68be 100644 --- a/core/src/avm1/globals/gradient_filter.rs +++ b/core/src/avm1/globals/gradient_filter.rs @@ -10,10 +10,8 @@ use crate::context::{GcContext, UpdateContext}; use crate::string::{AvmString, WStr}; use gc_arena::{Collect, GcCell, Mutation}; use std::ops::Deref; -use swf::{Color, Fixed16, Fixed8, GradientFilterFlags}; +use swf::{Color, Fixed16, Fixed8, GradientFilterFlags, GradientRecord}; -// [NA] Why the max? Why is colors limited but ratios isn't? -// TODO: Make it all vec maybe? const MAX_COLORS: usize = 16; #[derive(Clone, Debug, Collect)] @@ -22,9 +20,8 @@ struct GradientFilterData { distance: f64, // TODO: Introduce `Angle` struct. angle: f64, - colors: [Color; MAX_COLORS], + colors: [GradientRecord; MAX_COLORS], num_colors: usize, - ratios: Vec, blur_x: f64, blur_y: f64, // TODO: Introduce unsigned `Fixed8`? @@ -41,15 +38,7 @@ impl From<&GradientFilterData> for swf::GradientFilter { flags |= filter.type_.as_gradient_flags(); flags.set(GradientFilterFlags::KNOCKOUT, filter.knockout); swf::GradientFilter { - colors: filter - .colors - .iter() - .zip(filter.ratios.iter()) - .map(|(color, ratio)| swf::GradientRecord { - color: *color, - ratio: *ratio, - }) - .collect(), + colors: filter.colors.into_iter().take(filter.num_colors).collect(), blur_x: Fixed16::from_f64(filter.blur_x), blur_y: Fixed16::from_f64(filter.blur_y), angle: Fixed16::from_f64(filter.angle), @@ -62,26 +51,25 @@ impl From<&GradientFilterData> for swf::GradientFilter { impl From for GradientFilterData { fn from(filter: swf::GradientFilter) -> GradientFilterData { - let mut colors = [Color::WHITE; MAX_COLORS]; - let mut ratios = vec![]; + let mut colors = [GradientRecord::default(); MAX_COLORS]; + let num_colors = filter.colors.len().min(MAX_COLORS); + for (i, slot) in colors[..num_colors].iter_mut().enumerate() { + *slot = filter.colors[i]; + } + let quality = filter.num_passes().into(); let knockout = filter.is_knockout(); - for (i, record) in filter.colors.into_iter().take(MAX_COLORS).enumerate() { - colors[i] = record.color; - ratios.push(record.ratio); - } Self { distance: filter.distance.into(), angle: filter.angle.into(), colors, - num_colors: ratios.len(), + num_colors, quality, strength: (filter.strength.to_f64() * 256.0) as u16, knockout, blur_x: filter.blur_x.into(), blur_y: filter.blur_y.into(), type_: filter.flags.into(), - ratios, } } } @@ -94,7 +82,6 @@ impl Default for GradientFilterData { angle: 0.785398163, // ~45 degrees colors: Default::default(), num_colors: 0, - ratios: vec![], blur_x: 4.0, blur_y: 4.0, strength: 1 << 8, @@ -187,7 +174,7 @@ impl<'gc> GradientFilter<'gc> { context.avm1.prototypes().array, read.colors[..read.num_colors] .iter() - .map(|c| c.to_rgb().into()), + .map(|r| r.color.to_rgb().into()), ) } @@ -196,16 +183,24 @@ impl<'gc> GradientFilter<'gc> { activation: &mut Activation<'_, 'gc>, value: Option<&Value<'gc>>, ) -> Result<(), Error<'gc>> { - if let Some(Value::Object(object)) = value { - let num_colors = (object.length(activation)? as usize).min(MAX_COLORS); - self.0.write(activation.context.gc_context).num_colors = num_colors; - for i in 0..num_colors { - let rgb = object - .get_element(activation, i as i32) - .coerce_to_u32(activation)?; - let alpha = self.0.read().colors[i].a; - self.0.write(activation.context.gc_context).colors[i] = Color::from_rgb(rgb, alpha); - } + let Some(value) = value else { return Ok(()) }; + + // FP 11 and FP 32 behave differently here: in FP 11, only "true" objects resize + // the matrix, but in FP 32 strings will too (and so fill the matrix with `NaN` + // values, as they have a `length` but no actual elements). + let object = value.coerce_to_object(activation); + let length = usize::try_from(object.length(activation)?).unwrap_or_default(); + let num_colors = length.min(MAX_COLORS); + + self.0.write(activation.gc()).num_colors = num_colors; + + for i in 0..num_colors { + let rgb = object + .get_element(activation, i as i32) + .coerce_to_i32(activation)? as u32; + let mut write = self.0.write(activation.gc()); + let alpha = write.colors[i].color.a; + write.colors[i].color = Color::from_rgb(rgb, alpha); } Ok(()) } @@ -217,7 +212,7 @@ impl<'gc> GradientFilter<'gc> { context.avm1.prototypes().array, read.colors[..read.num_colors] .iter() - .map(|c| (f64::from(c.a) / 255.0).into()), + .map(|r| (f64::from(r.color.a) / 255.0).into()), ) } @@ -227,9 +222,12 @@ impl<'gc> GradientFilter<'gc> { value: Option<&Value<'gc>>, ) -> Result<(), Error<'gc>> { if let Some(Value::Object(object)) = value { + // Note that unlike `colors` and `ratios`, setting `alphas` doesn't change + // the number of colors in the gradient. let num_colors = self.0.read().num_colors; + let length = usize::try_from(object.length(activation)?).unwrap_or_default(); for i in 0..num_colors { - let alpha = if i < object.length(activation)? as usize { + let alpha = if i < length { let alpha = object .get_element(activation, i as i32) .coerce_to_f64(activation)?; @@ -241,17 +239,20 @@ impl<'gc> GradientFilter<'gc> { } else { u8::MAX }; - self.0.write(activation.context.gc_context).colors[i].a = alpha; + self.0.write(activation.gc()).colors[i].color.a = alpha; } } Ok(()) } fn ratios(&self, context: &mut UpdateContext<'_, 'gc>) -> ArrayObject<'gc> { + let read = self.0.read(); ArrayObject::new( context.gc_context, context.avm1.prototypes().array, - self.0.read().ratios.iter().map(|&x| x.into()), + read.colors[..read.num_colors] + .iter() + .map(|r| r.ratio.into()), ) } @@ -261,21 +262,20 @@ impl<'gc> GradientFilter<'gc> { value: Option<&Value<'gc>>, ) -> Result<(), Error<'gc>> { if let Some(Value::Object(object)) = value { - let num_colors = self - .0 - .read() - .num_colors - .min(object.length(activation)? as usize); - self.0.write(activation.context.gc_context).num_colors = num_colors; - let ratios: Result, Error<'gc>> = (0..num_colors) - .map(|i| { - Ok(object - .get_element(activation, i as i32) - .coerce_to_i32(activation)? - .clamp(0, u8::MAX.into()) as u8) - }) - .collect(); - self.0.write(activation.context.gc_context).ratios = ratios?; + let num_colors = usize::try_from(object.length(activation)?).unwrap_or_default(); + let mut write = self.0.write(activation.context.gc_context); + // Modifying `ratios` can only reduce the number of colors, never increase it. + let num_colors = num_colors.min(write.num_colors); + write.num_colors = num_colors; + drop(write); + + for i in 0..num_colors { + let ratio = object + .get_element(activation, i as i32) + .coerce_to_i32(activation)? + .clamp(0, u8::MAX.into()) as u8; + self.0.write(activation.context.gc_context).colors[i].ratio = ratio; + } } Ok(()) } diff --git a/swf/src/types.rs b/swf/src/types.rs index ec7781692..35560dadd 100644 --- a/swf/src/types.rs +++ b/swf/src/types.rs @@ -805,7 +805,7 @@ impl GradientInterpolation { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct GradientRecord { pub ratio: u8, pub color: Color, diff --git a/tests/tests/swfs/avm1/bitmap_filters/test.toml b/tests/tests/swfs/avm1/bitmap_filters/test.toml index c3f9982af..363cdc221 100644 --- a/tests/tests/swfs/avm1/bitmap_filters/test.toml +++ b/tests/tests/swfs/avm1/bitmap_filters/test.toml @@ -1,5 +1,4 @@ num_ticks = 1 -known_failure = true [approximations] number_patterns = ["angle=([\\d.]+)"]