diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 8d7d08cbe9dc..eb3fb48b96f9 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -17,11 +17,12 @@ use crate::prelude::*; use crate::string::AvmString; use crate::vminterface::Instantiator; use gc_arena::MutationContext; -use ruffle_render::shape_utils::DrawCommand; +use ruffle_render::bitmap::BitmapSize; +use ruffle_render::shape_utils::{DrawCommand, FillStyle, LineStyle}; use std::str::FromStr; use swf::{ - BlendMode, FillStyle, Fixed8, Gradient, GradientInterpolation, GradientRecord, GradientSpread, - LineCapStyle, LineJoinStyle, LineStyle, Rectangle, Twips, + BlendMode, Fixed8, Gradient, GradientInterpolation, GradientRecord, GradientSpread, + LineCapStyle, LineJoinStyle, Rectangle, Twips, }; macro_rules! mc_method { @@ -406,14 +407,6 @@ fn begin_bitmap_fill<'gc>( } else { return Ok(Value::Undefined); }; - let bitmap = ruffle_render::bitmap::BitmapInfo { - handle, - width: bitmap_data.width() as u16, - height: bitmap_data.height() as u16, - }; - let id = movie_clip - .drawing(activation.context.gc_context) - .add_bitmap(bitmap); let mut matrix = avm1::globals::matrix::object_to_matrix_or_default( args.get(1) @@ -437,7 +430,11 @@ fn begin_bitmap_fill<'gc>( movie_clip .drawing(activation.context.gc_context) .set_fill_style(Some(FillStyle::Bitmap { - id, + size: Some(BitmapSize { + width: bitmap_data.width() as u16, + height: bitmap_data.height() as u16, + }), + handle: Some(handle), matrix: matrix.into(), is_smoothed, is_repeating, diff --git a/core/src/avm2/globals/flash/display/graphics.rs b/core/src/avm2/globals/flash/display/graphics.rs index d0cc8df7b282..54f5b72e8523 100644 --- a/core/src/avm2/globals/flash/display/graphics.rs +++ b/core/src/avm2/globals/flash/display/graphics.rs @@ -8,9 +8,9 @@ use crate::avm2_stub_method; use crate::display_object::TDisplayObject; use crate::drawing::Drawing; use crate::string::WStr; -use ruffle_render::shape_utils::DrawCommand; +use ruffle_render::shape_utils::{DrawCommand, FillStyle, LineStyle}; use std::f64::consts::FRAC_1_SQRT_2; -use swf::{Color, FillStyle, Fixed8, LineCapStyle, LineJoinStyle, LineStyle, Twips}; +use swf::{Color, Fixed8, LineCapStyle, LineJoinStyle, Twips}; /// Convert an RGB `color` and `alpha` argument pair into a `swf::Color`. /// `alpha` is normalized from 0.0 - 1.0. diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 1eb91ee96a27..7376c02c17e0 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -28,7 +28,7 @@ use chrono::Utc; use core::fmt; use gc_arena::{Collect, Gc, GcCell, MutationContext}; use ruffle_render::commands::CommandHandler; -use ruffle_render::shape_utils::DrawCommand; +use ruffle_render::shape_utils::{DrawCommand, FillStyle, LineStyle}; use ruffle_render::transform::Transform; use std::{cell::Ref, cell::RefMut, sync::Arc}; use swf::{Color, Twips}; @@ -643,7 +643,7 @@ impl<'gc> EditText<'gc> { .intersects(EditTextFlag::BORDER | EditTextFlag::HAS_BACKGROUND) { let line_style = write.flags.contains(EditTextFlag::BORDER).then_some( - swf::LineStyle::new() + LineStyle::new() .with_width(Twips::new(1)) .with_color(write.border_color.clone()), ); @@ -652,7 +652,7 @@ impl<'gc> EditText<'gc> { let fill_style = write .flags .contains(EditTextFlag::HAS_BACKGROUND) - .then_some(swf::FillStyle::Color(write.background_color.clone())); + .then_some(FillStyle::Color(write.background_color.clone())); write.drawing.set_fill_style(fill_style); let width = write.bounds.width(); @@ -904,9 +904,11 @@ impl<'gc> EditText<'gc> { // Render glyph. let glyph_shape_handle = glyph.shape_handle(context.renderer); - context - .commands - .render_shape(glyph_shape_handle, context.transform_stack.transform()); + context.commands.render_shape( + glyph_shape_handle, + context.transform_stack.transform(), + false, + ); context.transform_stack.pop(); if let Some((caret_pos, length)) = caret { diff --git a/core/src/display_object/graphic.rs b/core/src/display_object/graphic.rs index f31034c33856..1116b3126e98 100644 --- a/core/src/display_object/graphic.rs +++ b/core/src/display_object/graphic.rs @@ -13,6 +13,8 @@ use core::fmt; use gc_arena::{Collect, GcCell, MutationContext}; use ruffle_render::backend::ShapeHandle; use ruffle_render::commands::CommandHandler; +use ruffle_render::shape_utils::{DistilledShape, ShapeStrokes}; +use ruffle_render::transform::Transform; use std::cell::{Ref, RefMut}; use std::sync::Arc; @@ -35,6 +37,10 @@ pub struct GraphicData<'gc> { static_data: gc_arena::Gc<'gc, GraphicStatic>, avm2_object: Option>, drawing: Option, + #[collect(require_static)] + strokes_handle: Option, + #[collect(require_static)] + last_scale: (f32, f32), } impl<'gc> Graphic<'gc> { @@ -45,16 +51,21 @@ impl<'gc> Graphic<'gc> { movie: Arc, ) -> Self { let library = context.library.library_for_movie(movie.clone()).unwrap(); + let bitmap_source = MovieLibrarySource { + library, + gc_context: context.gc_context, + }; + let shape = DistilledShape::from_shape(&swf_shape, &bitmap_source, context.renderer); + let static_data = GraphicStatic { id: swf_shape.id, bounds: swf_shape.shape_bounds.clone(), - render_handle: Some(context.renderer.register_shape( - (&swf_shape).into(), - &MovieLibrarySource { - library, - gc_context: context.gc_context, - }, - )), + fills_handle: Some( + context + .renderer + .register_shape_fills(&shape.fills, shape.id), + ), + strokes: Some(shape.strokes), shape: swf_shape, movie, }; @@ -66,6 +77,8 @@ impl<'gc> Graphic<'gc> { static_data: gc_arena::Gc::allocate(context.gc_context, static_data), avm2_object: None, drawing: None, + strokes_handle: None, + last_scale: (0.0, 0.0), }, )) } @@ -78,7 +91,8 @@ impl<'gc> Graphic<'gc> { let static_data = GraphicStatic { id: 0, bounds: Default::default(), - render_handle: None, + fills_handle: None, + strokes: None, shape: swf::Shape { version: 32, id: 0, @@ -102,6 +116,8 @@ impl<'gc> Graphic<'gc> { static_data: gc_arena::Gc::allocate(context.gc_context, static_data), avm2_object: Some(avm2_object), drawing: Some(drawing), + strokes_handle: None, + last_scale: (0.0, 0.0), }, )) } @@ -172,7 +188,9 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> { .library_for_movie_mut(self.movie()) .get_graphic(id) { - self.0.write(context.gc_context).static_data = new_graphic.0.read().static_data; + let mut write = self.0.write(context.gc_context); + write.static_data = new_graphic.0.read().static_data; + write.last_scale = (0.0, 0.0); // Force recreation of stroke } else { tracing::warn!("PlaceObject: expected Graphic at character ID {}", id); } @@ -182,7 +200,7 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> { // Noop } - fn render_self(&self, context: &mut RenderContext) { + fn render_self(&self, context: &mut RenderContext<'_, 'gc>) { if !context.is_offscreen && !self.world_bounds().intersects(&context.stage.view_bounds()) { // Off-screen; culled return; @@ -190,10 +208,68 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> { if let Some(drawing) = &self.0.read().drawing { drawing.render(context); - } else if let Some(render_handle) = self.0.read().static_data.render_handle { + return; + } + + if let Some(render_handle) = self.0.read().static_data.fills_handle { context .commands - .render_shape(render_handle, context.transform_stack.transform()) + .render_shape(render_handle, context.transform_stack.transform(), false) + } + + // Update the stroke if we're drawing it at a different scale than last time + let old_scale = self.0.read().last_scale; + let cur_matrix = context.transform_stack.transform().matrix; + let render_stroke_matrix = Matrix { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + tx: cur_matrix.tx, + ty: cur_matrix.ty, + }; + let cur_scale = ( + f32::abs(cur_matrix.a + cur_matrix.c), + f32::abs(cur_matrix.b + cur_matrix.d), + ); + if old_scale != cur_scale { + let mut write = self.0.write(context.gc_context); + if let Some(strokes) = &write.static_data.strokes { + let build_stroke_matrix = Matrix { + a: cur_matrix.a, + b: cur_matrix.b, + c: cur_matrix.c, + d: cur_matrix.d, + tx: Default::default(), + ty: Default::default(), + }; + if let Some(handle) = write.strokes_handle { + context.renderer.replace_shape_strokes( + strokes, + write.static_data.id, + build_stroke_matrix, + handle, + ); + } else { + write.strokes_handle = Some(context.renderer.register_shape_strokes( + strokes, + write.static_data.id, + build_stroke_matrix, + )); + } + } + write.last_scale = cur_scale; + } + + if let Some(render_handle) = self.0.read().strokes_handle { + context.commands.render_shape( + render_handle, + Transform { + matrix: render_stroke_matrix, + color_transform: context.transform_stack.transform().color_transform, + }, + true, + ); } } @@ -268,7 +344,8 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> { struct GraphicStatic { id: CharacterId, shape: swf::Shape, - render_handle: Option, + fills_handle: Option, + strokes: Option, bounds: Rectangle, movie: Arc, } diff --git a/core/src/display_object/morph_shape.rs b/core/src/display_object/morph_shape.rs index 26292fe39149..edede79d688c 100644 --- a/core/src/display_object/morph_shape.rs +++ b/core/src/display_object/morph_shape.rs @@ -7,6 +7,8 @@ use core::fmt; use gc_arena::{Collect, Gc, GcCell, MutationContext}; use ruffle_render::backend::ShapeHandle; use ruffle_render::commands::CommandHandler; +use ruffle_render::shape_utils::{DistilledShape, ShapeStrokes}; +use ruffle_render::transform::Transform; use std::cell::{Ref, RefCell, RefMut}; use std::sync::Arc; use swf::{Fixed16, Fixed8, Twips}; @@ -29,6 +31,11 @@ pub struct MorphShapeData<'gc> { base: DisplayObjectBase<'gc>, static_data: Gc<'gc, MorphShapeStatic>, ratio: u16, + #[collect(require_static)] + strokes_handle: Option, + #[collect(require_static)] + last_strokes: Option>, + last_scale: (f32, f32), } impl<'gc> MorphShape<'gc> { @@ -44,6 +51,9 @@ impl<'gc> MorphShape<'gc> { base: Default::default(), static_data: Gc::allocate(gc_context, static_data), ratio: 0, + strokes_handle: None, + last_strokes: None, + last_scale: (0.0, 0.0), }, )) } @@ -98,14 +108,67 @@ impl<'gc> TDisplayObject<'gc> for MorphShape<'gc> { // Noop } - fn render_self(&self, context: &mut RenderContext) { - let this = self.0.read(); - let ratio = this.ratio; - let static_data = this.static_data; - let shape_handle = static_data.get_shape(context, context.library, ratio); + fn render_self(&self, context: &mut RenderContext<'_, 'gc>) { + let ratio = self.0.read().ratio; + let static_data = self.0.read().static_data; + let (fills_handle, strokes) = static_data.get_shape(context, context.library, ratio); context .commands - .render_shape(shape_handle, context.transform_stack.transform()); + .render_shape(fills_handle, context.transform_stack.transform(), false); + + // Update the stroke if we're drawing it at a different scale than last time + let old_scale = self.0.read().last_scale; + let old_strokes = self.0.read().last_strokes.clone(); + let cur_matrix = context.transform_stack.transform().matrix; + let render_stroke_matrix = Matrix { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + tx: cur_matrix.tx, + ty: cur_matrix.ty, + }; + let cur_scale = ( + f32::abs(cur_matrix.a + cur_matrix.c), + f32::abs(cur_matrix.b + cur_matrix.d), + ); + if old_scale != cur_scale || old_strokes.as_ref() != Some(&strokes) { + let mut write = self.0.write(context.gc_context); + let build_stroke_matrix = Matrix { + a: cur_matrix.a, + b: cur_matrix.b, + c: cur_matrix.c, + d: cur_matrix.d, + tx: Default::default(), + ty: Default::default(), + }; + if let Some(handle) = write.strokes_handle { + context.renderer.replace_shape_strokes( + &strokes, + write.static_data.id, + build_stroke_matrix, + handle, + ); + } else { + write.strokes_handle = Some(context.renderer.register_shape_strokes( + &strokes, + write.static_data.id, + build_stroke_matrix, + )); + } + write.last_scale = cur_scale; + } + + if let Some(render_handle) = self.0.read().strokes_handle { + context.commands.render_shape( + render_handle, + Transform { + matrix: render_stroke_matrix, + color_transform: context.transform_stack.transform().color_transform, + }, + true, + ); + } } fn self_bounds(&self) -> Rectangle { @@ -146,7 +209,7 @@ impl<'gc> TDisplayObject<'gc> for MorphShape<'gc> { /// A precalculated intermediate frame for a morph shape. struct Frame { - shape_handle: Option, + shape_handles: Option<(ShapeHandle, Arc)>, shape: swf::Shape, bounds: Rectangle, } @@ -185,28 +248,30 @@ impl MorphShapeStatic { }) } - /// Retrieves the `ShapeHandle` for the given ratio. + /// Retrieves the fill & stroke `ShapeHandle`s for the given ratio. /// Lazily intializes and tessellates the shape if it does not yet exist. fn get_shape<'gc>( &self, context: &mut RenderContext<'_, 'gc>, library: &Library<'gc>, ratio: u16, - ) -> ShapeHandle { + ) -> (ShapeHandle, Arc) { let mut frame = self.get_frame(ratio); - if let Some(handle) = frame.shape_handle { - handle + if let Some(handles) = &frame.shape_handles { + handles.clone() } else { let library = library.library_for_movie(self.movie.clone()).unwrap(); - let handle = context.renderer.register_shape( - (&frame.shape).into(), - &MovieLibrarySource { - library, - gc_context: context.gc_context, - }, - ); - frame.shape_handle = Some(handle); - handle + let bitmap_source = MovieLibrarySource { + library, + gc_context: context.gc_context, + }; + let shape = DistilledShape::from_shape(&frame.shape, &bitmap_source, context.renderer); + let fills = context + .renderer + .register_shape_fills(&shape.fills, shape.id); + let strokes = Arc::new(shape.strokes); + frame.shape_handles = Some((fills, strokes.clone())); + (fills, strokes) } } @@ -329,7 +394,7 @@ impl MorphShapeStatic { }; Frame { - shape_handle: None, + shape_handles: None, shape, bounds, } diff --git a/core/src/display_object/text.rs b/core/src/display_object/text.rs index e837eaadfa35..39aff3427677 100644 --- a/core/src/display_object/text.rs +++ b/core/src/display_object/text.rs @@ -151,9 +151,11 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> { if let Some(glyph) = font.get_glyph(c.index as usize) { context.transform_stack.push(&transform); let glyph_shape_handle = glyph.shape_handle(context.renderer); - context - .commands - .render_shape(glyph_shape_handle, context.transform_stack.transform()); + context.commands.render_shape( + glyph_shape_handle, + context.transform_stack.transform(), + false, + ); context.transform_stack.pop(); transform.matrix.tx += Twips::new(c.advance); } diff --git a/core/src/drawing.rs b/core/src/drawing.rs index 769c67db839e..36856634674a 100644 --- a/core/src/drawing.rs +++ b/core/src/drawing.rs @@ -3,14 +3,22 @@ use gc_arena::Collect; use ruffle_render::backend::{RenderBackend, ShapeHandle}; use ruffle_render::bitmap::{BitmapHandle, BitmapInfo, BitmapSize, BitmapSource}; use ruffle_render::commands::CommandHandler; -use ruffle_render::shape_utils::{DistilledShape, DrawCommand, DrawPath}; -use std::cell::Cell; -use swf::{FillStyle, LineStyle, Rectangle, Twips}; +use ruffle_render::matrix::Matrix; +use ruffle_render::shape_utils::{ + DistilledShape, DrawCommand, FillPath, FillStyle, LineStyle, ShapeFills, ShapeStrokes, + StrokePath, +}; +use ruffle_render::transform::Transform; +use std::cell::{Cell, RefCell}; +use swf::{Rectangle, Twips}; #[derive(Clone, Debug, Collect)] #[collect(require_static)] pub struct Drawing { - render_handle: Cell>, + fills_handle: Cell>, + strokes_handle: Cell>, + shape_strokes: RefCell>, + last_scale: Cell<(f32, f32)>, shape_bounds: Rectangle, edge_bounds: Rectangle, dirty: Cell, @@ -32,7 +40,10 @@ impl Default for Drawing { impl Drawing { pub fn new() -> Self { Self { - render_handle: Cell::new(None), + fills_handle: Cell::new(None), + strokes_handle: Cell::new(None), + shape_strokes: RefCell::new(None), + last_scale: Cell::new((0.0, 0.0)), shape_bounds: Default::default(), edge_bounds: Default::default(), dirty: Cell::new(false), @@ -46,52 +57,6 @@ impl Drawing { } } - pub fn from_swf_shape(shape: &swf::Shape) -> Self { - let mut this = Self { - render_handle: Cell::new(None), - shape_bounds: shape.shape_bounds.clone(), - edge_bounds: shape.edge_bounds.clone(), - dirty: Cell::new(true), - paths: Vec::new(), - bitmaps: Vec::new(), - current_fill: None, - current_line: None, - pending_lines: Vec::new(), - cursor: (Twips::ZERO, Twips::ZERO), - fill_start: (Twips::ZERO, Twips::ZERO), - }; - - let shape: DistilledShape = shape.into(); - for path in shape.paths { - match path { - DrawPath::Stroke { - style, - is_closed: _, - commands, - } => { - this.set_line_style(Some(style.clone())); - - for command in commands { - this.draw_command(command); - } - - this.set_line_style(None); - } - DrawPath::Fill { style, commands } => { - this.set_fill_style(Some(style.clone())); - - for command in commands { - this.draw_command(command); - } - - this.set_fill_style(None); - } - } - } - - this - } - pub fn set_fill_style(&mut self, style: Option) { self.close_path(); if let Some(existing) = self.current_fill.take() { @@ -211,19 +176,20 @@ impl Drawing { pub fn render(&self, context: &mut RenderContext) { if self.dirty.get() { self.dirty.set(false); - let mut paths = Vec::with_capacity(self.paths.len()); + let mut fills = Vec::with_capacity(self.paths.len()); + let mut strokes = Vec::with_capacity(self.paths.len()); for path in &self.paths { match path { DrawingPath::Fill(fill) => { - paths.push(DrawPath::Fill { - style: &fill.style, + fills.push(FillPath { + style: fill.style.to_owned(), commands: fill.commands.to_owned(), }); } DrawingPath::Line(line) => { - paths.push(DrawPath::Stroke { - style: &line.style, + strokes.push(StrokePath { + style: line.style.to_owned(), commands: line.commands.to_owned(), is_closed: line.is_closed, }); @@ -232,8 +198,8 @@ impl Drawing { } if let Some(fill) = &self.current_fill { - paths.push(DrawPath::Fill { - style: &fill.style, + fills.push(FillPath { + style: fill.style.to_owned(), commands: fill.commands.to_owned(), }) } @@ -249,8 +215,8 @@ impl Drawing { } else { self.cursor == self.fill_start }; - paths.push(DrawPath::Stroke { - style: &line.style, + strokes.push(StrokePath { + style: line.style.to_owned(), commands, is_closed, }) @@ -267,31 +233,93 @@ impl Drawing { } else { self.cursor == self.fill_start }; - paths.push(DrawPath::Stroke { - style: &line.style, + strokes.push(StrokePath { + style: line.style.to_owned(), commands, is_closed, }) } let shape = DistilledShape { - paths, - shape_bounds: self.shape_bounds.clone(), - edge_bounds: self.edge_bounds.clone(), + fills: ShapeFills { + paths: fills, + bounds: self.shape_bounds.clone(), + }, + strokes: ShapeStrokes { + paths: strokes, + bounds: self.edge_bounds.clone(), + }, id: 0, }; - if let Some(handle) = self.render_handle.get() { - context.renderer.replace_shape(shape, self, handle); + if let Some(handle) = self.fills_handle.get() { + context + .renderer + .replace_shape_fills(&shape.fills, 0, handle); } else { - self.render_handle - .set(Some(context.renderer.register_shape(shape, self))); + self.fills_handle + .set(Some(context.renderer.register_shape_fills(&shape.fills, 0))); } + *self.shape_strokes.borrow_mut() = Some(shape.strokes); + self.last_scale.set((0.0, 0.0)); // Force recreation of stroke } - if let Some(handle) = self.render_handle.get() { + if let Some(handle) = self.fills_handle.get() { context .commands - .render_shape(handle, context.transform_stack.transform()); + .render_shape(handle, context.transform_stack.transform(), false); + } + + // Update the stroke if we're drawing it at a different scale than last time + let old_scale = self.last_scale.get(); + let cur_matrix = context.transform_stack.transform().matrix; + let render_stroke_matrix = Matrix { + a: 1.0, + b: 0.0, + c: 0.0, + d: 1.0, + tx: cur_matrix.tx, + ty: cur_matrix.ty, + }; + let cur_scale = ( + f32::abs(cur_matrix.a + cur_matrix.c), + f32::abs(cur_matrix.b + cur_matrix.d), + ); + if old_scale != cur_scale { + let build_stroke_matrix = Matrix { + a: cur_matrix.a, + b: cur_matrix.b, + c: cur_matrix.c, + d: cur_matrix.d, + tx: Default::default(), + ty: Default::default(), + }; + let strokes = self.shape_strokes.borrow(); + if let Some(strokes) = strokes.as_ref() { + if let Some(handle) = self.strokes_handle.get() { + context + .renderer + .replace_shape_strokes(strokes, 0, build_stroke_matrix, handle); + } else { + self.strokes_handle + .set(Some(context.renderer.register_shape_strokes( + strokes, + 0, + build_stroke_matrix, + ))); + } + } + self.last_scale.set(cur_scale); + } + + if let Some(render_handle) = self.strokes_handle.get() { + context.commands.render_shape( + render_handle, + Transform { + matrix: render_stroke_matrix, + color_transform: context.transform_stack.transform().color_transform, + }, + true, + ); } } diff --git a/core/src/html/layout.rs b/core/src/html/layout.rs index 6f67ab10d7f9..d2454943af89 100644 --- a/core/src/html/layout.rs +++ b/core/src/html/layout.rs @@ -8,7 +8,7 @@ use crate::html::text_format::{FormatSpans, TextFormat, TextSpan}; use crate::string::{utils as string_utils, WStr}; use crate::tag_utils::SwfMovie; use gc_arena::Collect; -use ruffle_render::shape_utils::DrawCommand; +use ruffle_render::shape_utils::{DrawCommand, LineStyle}; use std::cmp::{max, min}; use std::sync::Arc; use swf::Twips; @@ -146,7 +146,7 @@ impl<'a, 'gc> LayoutContext<'a, 'gc> { let mut has_underline: bool = false; line_drawing.set_line_style(Some( - swf::LineStyle::new() + LineStyle::new() .with_width(Twips::new(1)) .with_color(swf::Color::BLACK), )); diff --git a/render/canvas/src/lib.rs b/render/canvas/src/lib.rs index db9f8b964788..e80185307a72 100644 --- a/render/canvas/src/lib.rs +++ b/render/canvas/src/lib.rs @@ -5,20 +5,20 @@ use ruffle_render::backend::null::NullBitmapSource; use ruffle_render::backend::{ Context3D, Context3DCommand, RenderBackend, ShapeHandle, ViewportDimensions, }; -use ruffle_render::bitmap::{ - Bitmap, BitmapFormat, BitmapHandle, BitmapHandleImpl, BitmapSource, SyncHandle, -}; +use ruffle_render::bitmap::{Bitmap, BitmapFormat, BitmapHandle, BitmapHandleImpl, SyncHandle}; use ruffle_render::color_transform::ColorTransform; use ruffle_render::commands::{CommandHandler, CommandList}; use ruffle_render::error::Error; use ruffle_render::matrix::Matrix; use ruffle_render::quality::StageQuality; -use ruffle_render::shape_utils::{DistilledShape, DrawCommand, LineScaleMode, LineScales}; +use ruffle_render::shape_utils::{ + DrawCommand, FillStyle, LineScaleMode, LineScales, ShapeConverter, ShapeFills, ShapeStrokes, +}; use ruffle_render::transform::Transform; use ruffle_web_common::{JsError, JsResult}; use std::borrow::Cow; use std::sync::Arc; -use swf::{BlendMode, Color}; +use swf::{BlendMode, CharacterId, Color}; use wasm_bindgen::{Clamped, JsCast, JsValue}; use web_sys::{ CanvasGradient, CanvasPattern, CanvasRenderingContext2d, CanvasWindingRule, DomMatrix, Element, @@ -434,30 +434,55 @@ impl RenderBackend for WebCanvasRenderBackend { } } - fn register_shape( + fn register_shape_fills(&mut self, shape: &ShapeFills, _id: CharacterId) -> ShapeHandle { + let handle = ShapeHandle(self.shapes.len()); + let data = swf_shape_fills_to_canvas_commands(shape, self); + self.shapes.push(data); + handle + } + + fn replace_shape_fills(&mut self, shape: &ShapeFills, _id: CharacterId, handle: ShapeHandle) { + let data = swf_shape_fills_to_canvas_commands(shape, self); + self.shapes[handle.0] = data; + } + + fn register_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + _id: CharacterId, + _matrix: Matrix, ) -> ShapeHandle { let handle = ShapeHandle(self.shapes.len()); - let data = swf_shape_to_canvas_commands(&shape, bitmap_source, self); + let data = swf_shape_strokes_to_canvas_commands(shape, self); self.shapes.push(data); handle } - fn replace_shape( + fn replace_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + _id: CharacterId, + _matrix: Matrix, handle: ShapeHandle, ) { - let data = swf_shape_to_canvas_commands(&shape, bitmap_source, self); + let data = swf_shape_strokes_to_canvas_commands(shape, self); self.shapes[handle.0] = data; } fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { let shape = ruffle_render::shape_utils::swf_glyph_to_shape(glyph); - self.register_shape((&shape).into(), &NullBitmapSource) + let (fills, _strokes) = + ShapeConverter::from_shape(&shape, &NullBitmapSource, self).into_commands(); + + // TODO: Can glyphs have strokes? + + self.register_shape_fills( + &ShapeFills { + paths: fills, + bounds: shape.shape_bounds.clone(), + }, + shape.id, + ) } fn render_offscreen( @@ -534,7 +559,7 @@ impl CommandHandler for WebCanvasRenderBackend { panic!("Stage3D should not have been created on canvas backend") } - fn render_shape(&mut self, shape: ShapeHandle, transform: Transform) { + fn render_shape(&mut self, shape: ShapeHandle, transform: Transform, _is_stroke: bool) { match &self.mask_state { MaskState::DrawContent => { let mut line_scale = LineScales::new(&transform.matrix); @@ -821,14 +846,10 @@ fn draw_commands_to_path2d(commands: &[DrawCommand], is_closed: bool) -> Path2d path } -fn swf_shape_to_canvas_commands( - shape: &DistilledShape, - bitmap_source: &dyn BitmapSource, +fn swf_shape_fills_to_canvas_commands( + shape: &ShapeFills, backend: &mut WebCanvasRenderBackend, ) -> ShapeData { - use ruffle_render::shape_utils::DrawPath; - 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. @@ -840,136 +861,136 @@ fn swf_shape_to_canvas_commands( bounds_viewbox_matrix.set_d(1.0 / 20.0); for path in &shape.paths { - match path { - DrawPath::Fill { - commands, style, .. - } => { - let canvas_path = Path2d::new().expect("Path2d constructor must succeed"); - canvas_path.add_path_with_transformation( - &draw_commands_to_path2d(commands, false), - bounds_viewbox_matrix.unchecked_ref(), - ); + let canvas_path = Path2d::new().expect("Path2d constructor must succeed"); + canvas_path.add_path_with_transformation( + &draw_commands_to_path2d(&path.commands, false), + bounds_viewbox_matrix.unchecked_ref(), + ); - let fill_style = match style { - FillStyle::Color(color) => CanvasFillStyle::Color(color.into()), - FillStyle::LinearGradient(gradient) => CanvasFillStyle::Gradient( - create_linear_gradient(&backend.context, gradient, true) - .expect("Couldn't create linear gradient"), - ), - FillStyle::RadialGradient(gradient) => CanvasFillStyle::Gradient( - create_radial_gradient(&backend.context, gradient, 0.0, true) - .expect("Couldn't create radial gradient"), - ), - FillStyle::FocalGradient { - gradient, - focal_point, - } => CanvasFillStyle::Gradient( - create_radial_gradient( - &backend.context, - gradient, - focal_point.to_f64(), - true, - ) - .expect("Couldn't create radial gradient"), - ), - FillStyle::Bitmap { - id, - matrix, - is_smoothed, - is_repeating, - } => { - let bitmap = if let Some(bitmap) = create_bitmap_pattern( - *id, - *matrix, - *is_smoothed, - *is_repeating, - bitmap_source, - backend, - ) { - bitmap - } else { - continue; - }; - CanvasFillStyle::Bitmap(bitmap) - } + let fill_style = match &path.style { + FillStyle::Color(color) => CanvasFillStyle::Color(color.into()), + FillStyle::LinearGradient(gradient) => CanvasFillStyle::Gradient( + create_linear_gradient(&backend.context, gradient, true) + .expect("Couldn't create linear gradient"), + ), + FillStyle::RadialGradient(gradient) => CanvasFillStyle::Gradient( + create_radial_gradient(&backend.context, gradient, 0.0, true) + .expect("Couldn't create radial gradient"), + ), + FillStyle::FocalGradient { + gradient, + focal_point, + } => CanvasFillStyle::Gradient( + create_radial_gradient(&backend.context, gradient, focal_point.to_f64(), true) + .expect("Couldn't create radial gradient"), + ), + FillStyle::Bitmap { + size: _, + handle, + matrix, + is_smoothed, + is_repeating, + } => { + let bitmap = if let Some(bitmap) = create_bitmap_pattern( + handle.to_owned(), + *matrix, + *is_smoothed, + *is_repeating, + backend, + ) { + bitmap + } else { + continue; }; - - canvas_data.0.push(CanvasDrawCommand::Fill { - path: canvas_path, - fill_style, - }); + CanvasFillStyle::Bitmap(bitmap) } - DrawPath::Stroke { - commands, - style, - is_closed, - } => { - let canvas_path = Path2d::new().expect("Path2d constructor must succeed"); - canvas_path.add_path_with_transformation( - &draw_commands_to_path2d(commands, *is_closed), - bounds_viewbox_matrix.unchecked_ref(), - ); + }; - let stroke_style = match style.fill_style() { - FillStyle::Color(color) => CanvasStrokeStyle::Color(color.into()), - FillStyle::LinearGradient(gradient) => { - CanvasStrokeStyle::Gradient(gradient.clone(), None) - } - FillStyle::RadialGradient(gradient) => { - CanvasStrokeStyle::Gradient(gradient.clone(), Some(0.0)) - } - FillStyle::FocalGradient { - gradient, - focal_point, - } => CanvasStrokeStyle::Gradient(gradient.clone(), Some(focal_point.to_f64())), - FillStyle::Bitmap { - id, - matrix, - is_smoothed, - is_repeating, - } => { - let bitmap = if let Some(bitmap) = create_bitmap_pattern( - *id, - *matrix, - *is_smoothed, - *is_repeating, - bitmap_source, - backend, - ) { - bitmap - } else { - continue; - }; - CanvasStrokeStyle::Bitmap(bitmap) - } - }; + canvas_data.0.push(CanvasDrawCommand::Fill { + path: canvas_path, + fill_style, + }); + } - let line_cap = match style.start_cap() { - LineCapStyle::Round => "round", - LineCapStyle::Square => "square", - LineCapStyle::None => "butt", - }; - let (line_join, miter_limit) = match style.join_style() { - LineJoinStyle::Round => ("round", 999_999.0), - LineJoinStyle::Bevel => ("bevel", 999_999.0), - LineJoinStyle::Miter(ml) => ("miter", ml.to_f32()), + canvas_data +} + +fn swf_shape_strokes_to_canvas_commands( + shape: &ShapeStrokes, + backend: &mut WebCanvasRenderBackend, +) -> ShapeData { + use swf::{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 mut canvas_data = ShapeData(vec![]); + + let bounds_viewbox_matrix = DomMatrix::new().expect("DomMatrix constructor must succeed"); + bounds_viewbox_matrix.set_a(1.0 / 20.0); + bounds_viewbox_matrix.set_d(1.0 / 20.0); + + for path in &shape.paths { + let canvas_path = Path2d::new().expect("Path2d constructor must succeed"); + canvas_path.add_path_with_transformation( + &draw_commands_to_path2d(&path.commands, path.is_closed), + bounds_viewbox_matrix.unchecked_ref(), + ); + + let stroke_style = match path.style.fill_style() { + FillStyle::Color(color) => CanvasStrokeStyle::Color(color.into()), + FillStyle::LinearGradient(gradient) => { + CanvasStrokeStyle::Gradient(gradient.clone(), None) + } + FillStyle::RadialGradient(gradient) => { + CanvasStrokeStyle::Gradient(gradient.clone(), Some(0.0)) + } + FillStyle::FocalGradient { + gradient, + focal_point, + } => CanvasStrokeStyle::Gradient(gradient.clone(), Some(focal_point.to_f64())), + FillStyle::Bitmap { + size: _, + handle, + matrix, + is_smoothed, + is_repeating, + } => { + let bitmap = if let Some(bitmap) = create_bitmap_pattern( + handle.to_owned(), + *matrix, + *is_smoothed, + *is_repeating, + backend, + ) { + bitmap + } else { + continue; }; - canvas_data.0.push(CanvasDrawCommand::Stroke { - path: canvas_path, - line_width: style.width().to_pixels(), - stroke_style, - line_cap: line_cap.to_string(), - line_join: line_join.to_string(), - miter_limit: miter_limit as f64 / 20.0, - scale_mode: match (style.allow_scale_x(), style.allow_scale_y()) { - (false, false) => LineScaleMode::None, - (true, false) => LineScaleMode::Horizontal, - (false, true) => LineScaleMode::Vertical, - (true, true) => LineScaleMode::Both, - }, - }); + CanvasStrokeStyle::Bitmap(bitmap) } - } + }; + + let line_cap = match path.style.start_cap() { + LineCapStyle::Round => "round", + LineCapStyle::Square => "square", + LineCapStyle::None => "butt", + }; + let (line_join, miter_limit) = match path.style.join_style() { + LineJoinStyle::Round => ("round", 999_999.0), + LineJoinStyle::Bevel => ("bevel", 999_999.0), + LineJoinStyle::Miter(ml) => ("miter", ml.to_f32()), + }; + canvas_data.0.push(CanvasDrawCommand::Stroke { + path: canvas_path, + line_width: path.style.width().to_pixels(), + stroke_style, + line_cap: line_cap.to_string(), + line_join: line_join.to_string(), + miter_limit: miter_limit as f64 / 20.0, + scale_mode: path.style.scale_mode(), + }); } canvas_data } @@ -1172,14 +1193,13 @@ fn swf_to_canvas_gradient( /// Converts an SWF bitmap fill to a canvas pattern. fn create_bitmap_pattern( - id: swf::CharacterId, + handle: Option, matrix: swf::Matrix, is_smoothed: bool, is_repeating: bool, - bitmap_source: &dyn BitmapSource, backend: &mut WebCanvasRenderBackend, ) -> Option { - if let Some(handle) = bitmap_source.bitmap_handle(id, backend) { + if let Some(handle) = handle { let bitmap = as_bitmap_data(&handle); let repeat = if !is_repeating { // NOTE: The WebGL backend does clamping in this case, just like @@ -1195,7 +1215,7 @@ fn create_bitmap_pattern( { Ok(Some(pattern)) => pattern, _ => { - log::warn!("Unable to create bitmap pattern for bitmap ID {}", id); + log::warn!("Unable to create bitmap pattern for bitmap"); return None; } }; @@ -1206,7 +1226,7 @@ fn create_bitmap_pattern( smoothed: is_smoothed, }) } else { - log::warn!("Couldn't fill shape with unknown bitmap {}", id); + log::warn!("Couldn't fill shape with unknown bitmap"); None } } diff --git a/render/src/backend.rs b/render/src/backend.rs index d7349332ee75..0cb139573e4e 100644 --- a/render/src/backend.rs +++ b/render/src/backend.rs @@ -1,11 +1,12 @@ pub mod null; -use crate::bitmap::{Bitmap, BitmapHandle, BitmapSource, SyncHandle}; +use crate::bitmap::{Bitmap, BitmapHandle, SyncHandle}; use crate::commands::CommandList; use crate::error::Error; use crate::filters::Filter; +use crate::matrix::Matrix; use crate::quality::StageQuality; -use crate::shape_utils::DistilledShape; +use crate::shape_utils::{ShapeFills, ShapeStrokes}; use downcast_rs::{impl_downcast, Downcast}; use gc_arena::{Collect, GcCell, MutationContext}; use ruffle_wstr::WStr; @@ -13,21 +14,26 @@ use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::rc::Rc; use swf; +use swf::CharacterId; pub trait RenderBackend: Downcast { fn viewport_dimensions(&self) -> ViewportDimensions; // Do not call this method directly - use `player.set_viewport_dimensions`, // which will ensure that the stage is properly updated as well. fn set_viewport_dimensions(&mut self, dimensions: ViewportDimensions); - fn register_shape( + fn register_shape_fills(&mut self, shape: &ShapeFills, id: CharacterId) -> ShapeHandle; + fn replace_shape_fills(&mut self, shape: &ShapeFills, id: CharacterId, handle: ShapeHandle); + fn register_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + id: CharacterId, + matrix: Matrix, ) -> ShapeHandle; - fn replace_shape( + fn replace_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + id: CharacterId, + matrix: Matrix, handle: ShapeHandle, ); fn register_glyph_shape(&mut self, shape: &swf::Glyph) -> ShapeHandle; diff --git a/render/src/backend/null.rs b/render/src/backend/null.rs index 4f549240c020..690b681a39bf 100644 --- a/render/src/backend/null.rs +++ b/render/src/backend/null.rs @@ -5,10 +5,11 @@ use crate::backend::{RenderBackend, ShapeHandle, ViewportDimensions}; use crate::bitmap::{Bitmap, BitmapHandle, BitmapHandleImpl, BitmapSize, BitmapSource, SyncHandle}; use crate::commands::CommandList; use crate::error::Error; +use crate::matrix::Matrix; use crate::quality::StageQuality; -use crate::shape_utils::DistilledShape; +use crate::shape_utils::{ShapeFills, ShapeStrokes}; use gc_arena::MutationContext; -use swf::Color; +use swf::{CharacterId, Color}; use super::{Context3D, Context3DCommand}; @@ -43,17 +44,24 @@ impl RenderBackend for NullRenderer { fn set_viewport_dimensions(&mut self, dimensions: ViewportDimensions) { self.dimensions = dimensions; } - fn register_shape( + fn register_shape_fills(&mut self, _shape: &ShapeFills, _id: CharacterId) -> ShapeHandle { + ShapeHandle(0) + } + fn replace_shape_fills(&mut self, _shape: &ShapeFills, _id: CharacterId, _handle: ShapeHandle) { + } + fn register_shape_strokes( &mut self, - _shape: DistilledShape, - _bitmap_source: &dyn BitmapSource, + _shape: &ShapeStrokes, + _id: CharacterId, + _matrix: Matrix, ) -> ShapeHandle { ShapeHandle(0) } - fn replace_shape( + fn replace_shape_strokes( &mut self, - _shape: DistilledShape, - _bitmap_source: &dyn BitmapSource, + _shape: &ShapeStrokes, + _id: CharacterId, + _matrix: Matrix, _handle: ShapeHandle, ) { } diff --git a/render/src/bitmap.rs b/render/src/bitmap.rs index 8f6db17d3540..0e6b46eb7cb6 100644 --- a/render/src/bitmap.rs +++ b/render/src/bitmap.rs @@ -1,5 +1,6 @@ use gc_arena::Collect; use std::fmt::Debug; +use std::ptr; use std::sync::Arc; use downcast_rs::{impl_downcast, Downcast}; @@ -9,6 +10,12 @@ use crate::backend::RenderBackend; #[derive(Clone, Debug)] pub struct BitmapHandle(pub Arc); +impl PartialEq for BitmapHandle { + fn eq(&self, other: &Self) -> bool { + ptr::eq(self as *const _, other as *const _) + } +} + pub trait BitmapHandleImpl: Downcast + Debug {} impl_downcast!(BitmapHandleImpl); @@ -20,7 +27,7 @@ pub struct BitmapInfo { pub height: u16, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct BitmapSize { pub width: u16, pub height: u16, diff --git a/render/src/commands.rs b/render/src/commands.rs index 8a00a92e3bce..bb14e92731d1 100644 --- a/render/src/commands.rs +++ b/render/src/commands.rs @@ -7,7 +7,7 @@ use swf::{BlendMode, Color}; pub trait CommandHandler { fn render_bitmap(&mut self, bitmap: BitmapHandle, transform: Transform, smoothing: bool); fn render_stage3d(&mut self, bitmap: BitmapHandle, transform: Transform); - fn render_shape(&mut self, shape: ShapeHandle, transform: Transform); + fn render_shape(&mut self, shape: ShapeHandle, transform: Transform, is_stroke: bool); fn draw_rect(&mut self, color: Color, matrix: Matrix); fn push_mask(&mut self); fn activate_mask(&mut self); @@ -35,10 +35,14 @@ impl CommandList { transform, smoothing, } => handler.render_bitmap(bitmap, transform, smoothing), - Command::RenderShape { shape, transform } => handler.render_shape(shape, transform), Command::RenderStage3D { bitmap, transform } => { handler.render_stage3d(bitmap, transform) } + Command::RenderShape { + shape, + transform, + is_stroke, + } => handler.render_shape(shape, transform, is_stroke), Command::DrawRect { color, matrix } => handler.draw_rect(color, matrix), Command::PushMask => handler.push_mask(), Command::ActivateMask => handler.activate_mask(), @@ -64,9 +68,12 @@ impl CommandHandler for CommandList { .push(Command::RenderStage3D { bitmap, transform }); } - fn render_shape(&mut self, shape: ShapeHandle, transform: Transform) { - self.commands - .push(Command::RenderShape { shape, transform }); + fn render_shape(&mut self, shape: ShapeHandle, transform: Transform, is_stroke: bool) { + self.commands.push(Command::RenderShape { + shape, + transform, + is_stroke, + }); } fn draw_rect(&mut self, color: Color, matrix: Matrix) { @@ -108,6 +115,7 @@ pub enum Command { RenderShape { shape: ShapeHandle, transform: Transform, + is_stroke: bool, }, DrawRect { color: Color, diff --git a/render/src/shape_utils.rs b/render/src/shape_utils.rs index 4d05c2e6af71..34fdc5da3ad0 100644 --- a/render/src/shape_utils.rs +++ b/render/src/shape_utils.rs @@ -1,6 +1,11 @@ +use crate::backend::RenderBackend; +use crate::bitmap::{BitmapHandle, BitmapSize, BitmapSource}; use crate::matrix::Matrix; use smallvec::SmallVec; -use swf::{CharacterId, FillStyle, LineStyle, Rectangle, Shape, ShapeRecord, Twips}; +use swf::{ + CharacterId, Color, Fixed8, Gradient, LineCapStyle, LineJoinStyle, LineStyleFlag, Rectangle, + Shape, ShapeRecord, Twips, +}; pub fn calculate_shape_bounds(shape_records: &[swf::ShapeRecord]) -> swf::Rectangle { let mut bounds = swf::Rectangle { @@ -58,38 +63,300 @@ pub fn calculate_shape_bounds(shape_records: &[swf::ShapeRecord]) -> swf::Rectan bounds } -/// `DrawPath` represents a solid fill or a stroke. -/// Fills are always closed paths, while strokes may be open or closed. -/// Closed paths will have the first point equal to the last point. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum DrawPath<'a> { - Stroke { - style: &'a LineStyle, - is_closed: bool, - commands: Vec, +#[derive(Clone, Debug, PartialEq)] +pub enum FillStyle { + Color(Color), + LinearGradient(Gradient), + RadialGradient(Gradient), + FocalGradient { + gradient: Gradient, + focal_point: Fixed8, }, - Fill { - style: &'a FillStyle, - commands: Vec, + Bitmap { + size: Option, + handle: Option, + matrix: swf::Matrix, + is_smoothed: bool, + is_repeating: bool, }, } +impl FillStyle { + fn from_swf( + style: &swf::FillStyle, + bitmap_source: &dyn BitmapSource, + render_backend: &mut dyn RenderBackend, + ) -> Self { + match style { + swf::FillStyle::Color(color) => Self::Color(color.to_owned()), + swf::FillStyle::LinearGradient(gradient) => Self::LinearGradient(gradient.to_owned()), + swf::FillStyle::RadialGradient(gradient) => Self::RadialGradient(gradient.to_owned()), + swf::FillStyle::FocalGradient { + gradient, + focal_point, + } => Self::FocalGradient { + gradient: gradient.to_owned(), + focal_point: focal_point.to_owned(), + }, + swf::FillStyle::Bitmap { + id, + matrix, + is_smoothed, + is_repeating, + } => Self::Bitmap { + size: bitmap_source.bitmap_size(*id), + handle: bitmap_source.bitmap_handle(*id, render_backend), + matrix: *matrix, + is_smoothed: *is_smoothed, + is_repeating: *is_repeating, + }, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct LineStyle { + pub width: Twips, + pub fill_style: FillStyle, + pub flags: LineStyleFlag, + pub miter_limit: Fixed8, +} + +impl Default for LineStyle { + #[inline] + fn default() -> Self { + // Hairline black stroke. + Self { + width: Twips::ZERO, + fill_style: FillStyle::Color(Color::BLACK), + flags: Default::default(), + miter_limit: Default::default(), + } + } +} + +impl LineStyle { + fn from_swf( + style: &swf::LineStyle, + bitmap_source: &dyn BitmapSource, + render_backend: &mut dyn RenderBackend, + ) -> Self { + Self { + width: style.width(), + fill_style: FillStyle::from_swf(style.fill_style(), bitmap_source, render_backend), + flags: style.flags(), + miter_limit: style.miter_limit(), + } + } + + #[inline] + pub fn new() -> LineStyle { + Default::default() + } + + #[inline] + pub fn allow_close(&self) -> bool { + !self.flags.contains(LineStyleFlag::NO_CLOSE) + } + + #[inline] + pub fn with_allow_close(mut self, val: bool) -> Self { + self.flags.set(LineStyleFlag::NO_CLOSE, !val); + self + } + + #[inline] + pub fn allow_scale_x(&self) -> bool { + !self.flags.contains(LineStyleFlag::NO_H_SCALE) + } + + #[inline] + pub fn with_allow_scale_x(mut self, val: bool) -> Self { + self.flags.set(LineStyleFlag::NO_H_SCALE, !val); + self + } + + #[inline] + pub fn allow_scale_y(&self) -> bool { + !self.flags.contains(LineStyleFlag::NO_V_SCALE) + } + + #[inline] + pub fn with_allow_scale_y(mut self, val: bool) -> Self { + self.flags.set(LineStyleFlag::NO_V_SCALE, !val); + self + } + + #[inline] + pub fn scale_mode(&self) -> LineScaleMode { + match (self.allow_scale_x(), self.allow_scale_y()) { + (false, false) => LineScaleMode::None, + (true, false) => LineScaleMode::Horizontal, + (false, true) => LineScaleMode::Vertical, + (true, true) => LineScaleMode::Both, + } + } + + #[inline] + pub fn is_pixel_hinted(&self) -> bool { + self.flags.contains(LineStyleFlag::PIXEL_HINTING) + } + + #[inline] + pub fn with_is_pixel_hinted(mut self, val: bool) -> Self { + self.flags.set(LineStyleFlag::PIXEL_HINTING, val); + self + } + + #[inline] + pub fn start_cap(&self) -> LineCapStyle { + let cap = (self.flags & LineStyleFlag::START_CAP_STYLE).bits() >> 6; + LineCapStyle::from_u8(cap as u8).expect("Infallible") + } + + #[inline] + pub fn with_start_cap(mut self, val: LineCapStyle) -> Self { + self.flags -= LineStyleFlag::START_CAP_STYLE; + self.flags |= LineStyleFlag::from_bits_truncate((val as u16) << 6); + self + } + + #[inline] + pub fn end_cap(&self) -> LineCapStyle { + let cap = (self.flags & LineStyleFlag::END_CAP_STYLE).bits() >> 8; + LineCapStyle::from_u8(cap as u8).expect("Infallible") + } + + #[inline] + pub fn with_end_cap(mut self, val: LineCapStyle) -> Self { + self.flags -= LineStyleFlag::END_CAP_STYLE; + self.flags |= LineStyleFlag::from_bits_truncate((val as u16) << 8); + self + } + + #[inline] + pub fn join_style(&self) -> LineJoinStyle { + match self.flags & LineStyleFlag::JOIN_STYLE { + LineStyleFlag::ROUND => LineJoinStyle::Round, + LineStyleFlag::BEVEL => LineJoinStyle::Bevel, + LineStyleFlag::MITER => LineJoinStyle::Miter(self.miter_limit), + _ => unreachable!(), + } + } + + #[inline] + pub fn with_join_style(mut self, val: LineJoinStyle) -> Self { + self.flags -= LineStyleFlag::JOIN_STYLE; + self.flags |= match val { + LineJoinStyle::Round => LineStyleFlag::ROUND, + LineJoinStyle::Bevel => LineStyleFlag::BEVEL, + LineJoinStyle::Miter(miter_limit) => { + self.miter_limit = miter_limit; + LineStyleFlag::MITER + } + }; + self + } + + #[inline] + pub fn fill_style(&self) -> &FillStyle { + &self.fill_style + } + + #[inline] + pub fn with_fill_style(mut self, val: FillStyle) -> Self { + self.flags + .set(LineStyleFlag::HAS_FILL, !matches!(val, FillStyle::Color(_))); + self.fill_style = val; + self + } + + #[inline] + pub fn with_color(mut self, val: Color) -> Self { + self.flags.remove(LineStyleFlag::HAS_FILL); + self.fill_style = FillStyle::Color(val); + self + } + + #[inline] + pub fn width(&self) -> Twips { + self.width + } + + #[inline] + pub fn with_width(mut self, val: Twips) -> Self { + self.width = val; + self + } + + #[inline] + pub fn flags(&self) -> LineStyleFlag { + self.flags + } + + #[inline] + pub fn miter_limit(&self) -> Fixed8 { + self.miter_limit + } +} + +/// `StrokePath` represents a stroke. +/// Strokes may be open or closed. +/// Closed paths will have the first point equal to the last point. +#[derive(Clone, Debug, PartialEq)] +pub struct StrokePath { + pub style: LineStyle, + pub is_closed: bool, + pub commands: Vec, +} + +/// `StrokePath` represents a solid fill. +/// Fills are always closed paths. +/// Closed paths will have the first point equal to the last point. +#[derive(Clone, Debug, PartialEq)] +pub struct FillPath { + pub style: FillStyle, + pub commands: Vec, +} + /// `DistilledShape` represents a ready-to-be-consumed collection of paths (both fills and strokes) /// that has been converted down from another source (such as SWF's `swf::Shape` format). -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DistilledShape<'a> { - pub paths: Vec>, - pub shape_bounds: Rectangle, - pub edge_bounds: Rectangle, +#[derive(Clone, Debug, PartialEq)] +pub struct DistilledShape { + pub fills: ShapeFills, + pub strokes: ShapeStrokes, pub id: CharacterId, } -impl<'a> From<&'a swf::Shape> for DistilledShape<'a> { - fn from(shape: &'a Shape) -> Self { +#[derive(Clone, Debug, PartialEq)] +pub struct ShapeFills { + pub paths: Vec, + pub bounds: Rectangle, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ShapeStrokes { + pub paths: Vec, + pub bounds: Rectangle, +} + +impl DistilledShape { + pub fn from_shape( + shape: &Shape, + bitmap_source: &dyn BitmapSource, + renderer: &mut dyn RenderBackend, + ) -> Self { + let (fills, strokes) = + ShapeConverter::from_shape(shape, bitmap_source, renderer).into_commands(); Self { - paths: ShapeConverter::from_shape(shape).into_commands(), - shape_bounds: shape.shape_bounds.clone(), - edge_bounds: shape.edge_bounds.clone(), + fills: ShapeFills { + paths: fills, + bounds: shape.shape_bounds.clone(), + }, + strokes: ShapeStrokes { + paths: strokes, + bounds: shape.shape_bounds.clone(), + }, id: shape.id, } } @@ -319,6 +586,8 @@ impl ActivePath { pub struct ShapeConverter<'a> { // SWF shape commands. iter: std::slice::Iter<'a, swf::ShapeRecord>, + bitmap_source: &'a dyn BitmapSource, + renderer: &'a mut dyn RenderBackend, // Pen position. x: Twips, @@ -338,15 +607,22 @@ pub struct ShapeConverter<'a> { strokes: Vec, // Output. - commands: Vec>, + fill_commands: Vec, + stroke_commands: Vec, } impl<'a> ShapeConverter<'a> { const DEFAULT_CAPACITY: usize = 512; - fn from_shape(shape: &'a swf::Shape) -> Self { + pub fn from_shape( + shape: &'a swf::Shape, + bitmap_source: &'a dyn BitmapSource, + renderer: &'a mut dyn RenderBackend, + ) -> Self { ShapeConverter { iter: shape.shape.iter(), + bitmap_source, + renderer, x: Twips::ZERO, y: Twips::ZERO, @@ -361,11 +637,12 @@ impl<'a> ShapeConverter<'a> { fills: vec![PendingPath::new(); shape.styles.fill_styles.len()], strokes: vec![PendingPath::new(); shape.styles.line_styles.len()], - commands: Vec::with_capacity(Self::DEFAULT_CAPACITY), + fill_commands: Vec::with_capacity(Self::DEFAULT_CAPACITY), + stroke_commands: Vec::with_capacity(Self::DEFAULT_CAPACITY), } } - fn into_commands(mut self) -> Vec> { + pub fn into_commands(mut self) -> (Vec, Vec) { // As u32 is okay because SWF has a max of 65536 fills (TODO: should be u16?) let mut num_fill_styles = self.fill_styles.len() as u32; let mut num_line_styles = self.line_styles.len() as u32; @@ -469,7 +746,7 @@ impl<'a> ShapeConverter<'a> { // Flush any open paths. self.flush_layer(); - self.commands + (self.fill_commands, self.stroke_commands) } /// Adds a point to the current path for the active fills/strokes. @@ -500,7 +777,6 @@ impl<'a> ShapeConverter<'a> { fn flush_layer(&mut self) { self.flush_paths(); - // Draw fills, and then strokes. // Paths are drawn in order of style id, not based on the order of the draw commands. for (i, path) in self.fills.iter_mut().enumerate() { // These invariants are checked above (any invalid/empty fill ID should not have been added). @@ -509,14 +785,13 @@ impl<'a> ShapeConverter<'a> { continue; } let style = unsafe { self.fill_styles.get_unchecked(i) }; - self.commands.push(DrawPath::Fill { - style, + self.fill_commands.push(FillPath { + style: FillStyle::from_swf(style, self.bitmap_source, self.renderer), commands: path.to_draw_commands().collect(), }); path.segments.clear(); } - // Strokes are drawn last because they always appear on top of fills in the same layer. // Because path segments can either be open or closed, we convert each stroke segment into // a separate draw command. for (i, path) in self.strokes.iter_mut().enumerate() { @@ -526,8 +801,8 @@ impl<'a> ShapeConverter<'a> { if segment.is_empty() { continue; } - self.commands.push(DrawPath::Stroke { - style, + self.stroke_commands.push(StrokePath { + style: LineStyle::from_swf(style, self.bitmap_source, self.renderer), is_closed: segment.is_closed(), commands: segment.to_draw_commands().collect(), }); @@ -540,15 +815,24 @@ impl<'a> ShapeConverter<'a> { #[cfg(test)] mod tests { use super::*; + use crate::backend::null::{NullBitmapSource, NullRenderer}; + use crate::backend::ViewportDimensions; - const FILL_STYLES: [FillStyle; 1] = [FillStyle::Color(swf::Color { + const FILL_STYLES: [swf::FillStyle; 1] = [swf::FillStyle::Color(swf::Color { r: 255, g: 0, b: 0, a: 255, })]; - const LINE_STYLES: [LineStyle; 0] = []; + const FINAL_FILL_STYLE: FillStyle = FillStyle::Color(swf::Color { + r: 255, + g: 0, + b: 0, + a: 255, + }); + + const LINE_STYLES: [swf::LineStyle; 0] = []; /// Convenience method to quickly make a shape, fn build_shape(records: Vec) -> swf::Shape { @@ -595,9 +879,16 @@ mod tests { delta_y: Twips::from_pixels(-100.0), }, ]); - let commands = ShapeConverter::from_shape(&shape).into_commands(); - let expected = vec![DrawPath::Fill { - style: &FILL_STYLES[0], + let mut render_backend = NullRenderer::new(ViewportDimensions { + width: 100, + height: 100, + scale_factor: 1.0, + }); + let bitmap_source = NullBitmapSource; + let (fills, strokes) = + ShapeConverter::from_shape(&shape, &bitmap_source, &mut render_backend).into_commands(); + let expected = vec![FillPath { + style: FINAL_FILL_STYLE, commands: vec![ DrawCommand::MoveTo { x: Twips::from_pixels(100.0), @@ -621,7 +912,8 @@ mod tests { }, ], }]; - assert_eq!(commands, expected); + assert_eq!(fills, expected); + assert_eq!(strokes, vec![]); } /// A solid square with one edge flipped (fillstyle0 instead of fillstyle1). @@ -659,9 +951,16 @@ mod tests { delta_y: Twips::from_pixels(100.0), }, ]); - let commands = ShapeConverter::from_shape(&shape).into_commands(); - let expected = vec![DrawPath::Fill { - style: &FILL_STYLES[0], + let mut render_backend = NullRenderer::new(ViewportDimensions { + width: 100, + height: 100, + scale_factor: 1.0, + }); + let bitmap_source = NullBitmapSource; + let (fills, strokes) = + ShapeConverter::from_shape(&shape, &bitmap_source, &mut render_backend).into_commands(); + let expected = vec![FillPath { + style: FINAL_FILL_STYLE, commands: vec![ DrawCommand::MoveTo { x: Twips::from_pixels(100.0), @@ -685,7 +984,8 @@ mod tests { }, ], }]; - assert_eq!(commands, expected); + assert_eq!(fills, expected); + assert_eq!(strokes, vec![]); } } diff --git a/render/src/tessellator.rs b/render/src/tessellator.rs index 2576b180ff8e..9a33a1e4d6ac 100644 --- a/render/src/tessellator.rs +++ b/render/src/tessellator.rs @@ -1,5 +1,6 @@ -use crate::bitmap::BitmapSource; -use crate::shape_utils::{DistilledShape, DrawCommand, DrawPath}; +use crate::bitmap::BitmapHandle; +use crate::matrix::Matrix; +use crate::shape_utils::{DrawCommand, FillPath, FillStyle, LineScales, StrokePath}; use enum_map::Enum; use lyon::path::Path; use lyon::tessellation::{ @@ -8,178 +9,176 @@ use lyon::tessellation::{ FillTessellator, FillVertex, StrokeTessellator, StrokeVertex, StrokeVertexConstructor, }; use lyon::tessellation::{FillOptions, StrokeOptions}; -use swf::GradientRecord; +use swf::{Color, GradientRecord}; use tracing::instrument; -pub struct ShapeTessellator { +pub struct ShapeFillTessellator { fill_tess: FillTessellator, - stroke_tess: StrokeTessellator, mesh: Vec, lyon_mesh: VertexBuffers, - mask_index_count: Option, - is_stroke: bool, } -impl ShapeTessellator { +impl ShapeFillTessellator { pub fn new() -> Self { Self { fill_tess: FillTessellator::new(), - stroke_tess: StrokeTessellator::new(), mesh: Vec::new(), lyon_mesh: VertexBuffers::new(), - mask_index_count: None, - is_stroke: false, } } #[instrument(level = "debug", skip_all)] - pub fn tessellate_shape( - &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, - ) -> Mesh { + pub fn tessellate_shape(&mut self, shape: &[FillPath]) -> Mesh { self.mesh = Vec::new(); self.lyon_mesh = VertexBuffers::new(); - for path in shape.paths { - let (fill_style, lyon_path, next_is_stroke) = match &path { - DrawPath::Fill { style, commands } => { - (*style, ruffle_path_to_lyon_path(commands, true), false) - } - DrawPath::Stroke { - style, - commands, - is_closed, - } => ( - style.fill_style(), - ruffle_path_to_lyon_path(commands, *is_closed), - true, - ), - }; + for path in shape { + let (fill_style, lyon_path) = ( + &path.style, + ruffle_path_to_lyon_path(&path.commands, true, Matrix::IDENTITY), + ); + + let (draw, color, needs_flush) = + if let Some((draw, color, needs_flush)) = fill_to_draw(fill_style) { + (draw, color, needs_flush) + } else { + continue; + }; + + if needs_flush { + // We flush separate draw calls for non-solid color fills which require their own shader. + self.flush_draw(DrawType::Color); + } - let (draw, color, needs_flush) = match fill_style { - swf::FillStyle::Color(color) => (DrawType::Color, color.clone(), false), - swf::FillStyle::LinearGradient(gradient) => ( - DrawType::Gradient(swf_gradient_to_uniforms( - GradientType::Linear, - gradient, - swf::Fixed8::ZERO, - )), - swf::Color::WHITE, - true, - ), - swf::FillStyle::RadialGradient(gradient) => ( - DrawType::Gradient(swf_gradient_to_uniforms( - GradientType::Radial, - gradient, - swf::Fixed8::ZERO, - )), - swf::Color::WHITE, - true, - ), - swf::FillStyle::FocalGradient { - gradient, - focal_point, - } => ( - DrawType::Gradient(swf_gradient_to_uniforms( - GradientType::Focal, - gradient, - *focal_point, - )), - swf::Color::WHITE, - true, - ), - swf::FillStyle::Bitmap { - id, - matrix, - is_smoothed, - is_repeating, - } => { - if let Some(bitmap) = bitmap_source.bitmap_size(*id) { - ( - DrawType::Bitmap(Bitmap { - matrix: swf_bitmap_to_gl_matrix( - (*matrix).into(), - bitmap.width.into(), - bitmap.height.into(), - ), - bitmap_id: *id, - is_smoothed: *is_smoothed, - is_repeating: *is_repeating, - }), - swf::Color::WHITE, - true, - ) - } else { - // Missing bitmap -- incorrect character ID in SWF? - continue; + let mut buffers_builder = + BuffersBuilder::new(&mut self.lyon_mesh, RuffleVertexCtor { color }); + let result = self.fill_tess.tessellate_path( + &lyon_path, + &FillOptions::even_odd(), + &mut buffers_builder, + ); + match result { + Ok(_) => { + if needs_flush { + // Non-solid color fills are isolated draw calls; flush immediately. + self.flush_draw(draw); } } - }; + Err(e) => { + // This may simply be a degenerate path. + tracing::error!("Tessellation failure: {:?}", e); + } + } + } + self.flush_draw(DrawType::Color); + + self.lyon_mesh = VertexBuffers::new(); + std::mem::take(&mut self.mesh) + } - if needs_flush || (self.is_stroke && !next_is_stroke) { + fn flush_draw(&mut self, draw: DrawType) { + if self.lyon_mesh.vertices.is_empty() || self.lyon_mesh.indices.len() < 3 { + // Ignore degenerate fills + return; + } + let draw_mesh = std::mem::replace(&mut self.lyon_mesh, VertexBuffers::new()); + self.mesh.push(Draw { + draw_type: draw, + vertices: draw_mesh.vertices, + indices: draw_mesh.indices, + }); + } +} + +impl Default for ShapeFillTessellator { + fn default() -> Self { + Self::new() + } +} + +pub struct ShapeStrokeTessellator { + stroke_tess: StrokeTessellator, + mesh: Vec, + lyon_mesh: VertexBuffers, +} + +impl ShapeStrokeTessellator { + pub fn new() -> Self { + Self { + stroke_tess: StrokeTessellator::new(), + mesh: Vec::new(), + lyon_mesh: VertexBuffers::new(), + } + } + + #[instrument(level = "debug", skip_all)] + pub fn tessellate_shape(&mut self, shape: &[StrokePath], matrix: Matrix) -> Mesh { + self.mesh = Vec::new(); + self.lyon_mesh = VertexBuffers::new(); + let mut scales = LineScales::new(&matrix); + + for path in shape { + let (fill_style, lyon_path) = ( + &path.style.fill_style, + ruffle_path_to_lyon_path(&path.commands, path.is_closed, matrix), + ); + + let (draw, color, needs_flush) = + if let Some((draw, color, needs_flush)) = fill_to_draw(fill_style) { + (draw, color, needs_flush) + } else { + continue; + }; + + if needs_flush { // We flush separate draw calls in these cases: // * Non-solid color fills which require their own shader. // * Strokes followed by fills, because strokes need to be omitted // when using this shape as a mask. self.flush_draw(DrawType::Color); - } else if !self.is_stroke && next_is_stroke { - // Bake solid color fills followed by strokes into a single draw call, and adjust - // the index count to omit the strokes when rendering this shape as a mask. - debug_assert!(self.mask_index_count.is_none()); - self.mask_index_count = Some(self.lyon_mesh.indices.len() as u32); } - self.is_stroke = next_is_stroke; let mut buffers_builder = BuffersBuilder::new(&mut self.lyon_mesh, RuffleVertexCtor { color }); - let result = match path { - DrawPath::Fill { .. } => self.fill_tess.tessellate_path( - &lyon_path, - &FillOptions::even_odd(), - &mut buffers_builder, - ), - DrawPath::Stroke { style, .. } => { - // TODO(Herschel): 0 width indicates "hairline". - let width = (style.width().to_pixels() as f32).max(1.0); - let mut stroke_options = StrokeOptions::default() - .with_line_width(width) - .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, - }); - - let line_join = match style.join_style() { - swf::LineJoinStyle::Round => tessellation::LineJoin::Round, - swf::LineJoinStyle::Bevel => tessellation::LineJoin::Bevel, - swf::LineJoinStyle::Miter(limit) => { - // Avoid lyon assert with small miter limits. - let limit = limit.to_f32(); - if limit >= StrokeOptions::MINIMUM_MITER_LIMIT { - stroke_options = stroke_options.with_miter_limit(limit); - tessellation::LineJoin::MiterClip - } else { - tessellation::LineJoin::Bevel - } - } - }; - stroke_options = stroke_options.with_line_join(line_join); - self.stroke_tess.tessellate_path( - &lyon_path, - &stroke_options, - &mut buffers_builder, - ) + + let mut stroke_options = + StrokeOptions::default() + .with_line_width(scales.transform_width( + path.style.width.to_pixels() as f32, + path.style.scale_mode(), + )) + .with_start_cap(match path.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 path.style.end_cap() { + swf::LineCapStyle::None => tessellation::LineCap::Butt, + swf::LineCapStyle::Round => tessellation::LineCap::Round, + swf::LineCapStyle::Square => tessellation::LineCap::Square, + }); + + let line_join = match path.style.join_style() { + swf::LineJoinStyle::Round => tessellation::LineJoin::Round, + swf::LineJoinStyle::Bevel => tessellation::LineJoin::Bevel, + swf::LineJoinStyle::Miter(limit) => { + // Avoid lyon assert with small miter limits. + let limit = limit.to_f32(); + if limit >= StrokeOptions::MINIMUM_MITER_LIMIT { + stroke_options = stroke_options.with_miter_limit(limit); + tessellation::LineJoin::MiterClip + } else { + tessellation::LineJoin::Bevel + } } }; + stroke_options = stroke_options.with_line_join(line_join); + let result = + self.stroke_tess + .tessellate_path(&lyon_path, &stroke_options, &mut buffers_builder); match result { Ok(_) => { if needs_flush { - // Non-solid color fills are isolated draw calls; flush immediately. + // We flush separate draw calls for non-solid color fills which require their own shader. self.flush_draw(draw); } } @@ -189,8 +188,6 @@ impl ShapeTessellator { } } } - - // Flush the final pending draw. self.flush_draw(DrawType::Color); self.lyon_mesh = VertexBuffers::new(); @@ -205,17 +202,13 @@ impl ShapeTessellator { let draw_mesh = std::mem::replace(&mut self.lyon_mesh, VertexBuffers::new()); self.mesh.push(Draw { draw_type: draw, - mask_index_count: self - .mask_index_count - .unwrap_or(draw_mesh.indices.len() as u32), vertices: draw_mesh.vertices, indices: draw_mesh.indices, }); - self.mask_index_count = None; } } -impl Default for ShapeTessellator { +impl Default for ShapeStrokeTessellator { fn default() -> Self { Self::new() } @@ -227,7 +220,6 @@ pub struct Draw { pub draw_type: DrawType, pub vertices: Vec, pub indices: Vec, - pub mask_index_count: u32, } pub enum DrawType { @@ -266,9 +258,9 @@ pub struct Vertex { #[derive(Clone, Debug)] pub struct Bitmap { pub matrix: [[f32; 3]; 3], - pub bitmap_id: u16, pub is_smoothed: bool, pub is_repeating: bool, + pub handle: Option, } #[allow(clippy::many_single_char_names)] @@ -325,10 +317,11 @@ fn swf_bitmap_to_gl_matrix( [[a, d, 0.0], [b, e, 0.0], [c, f, 1.0]] } -fn ruffle_path_to_lyon_path(commands: &[DrawCommand], is_closed: bool) -> Path { - fn point(x: swf::Twips, y: swf::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: &[DrawCommand], is_closed: bool, matrix: Matrix) -> Path { + let point = |x: swf::Twips, y: swf::Twips| -> lyon::math::Point { + let point = matrix * (x, y); + lyon::math::Point::new(point.0.to_pixels() as f32, point.1.to_pixels() as f32) + }; let mut builder = Path::builder(); let mut move_to = Some((swf::Twips::default(), swf::Twips::default())); @@ -412,3 +405,66 @@ pub enum GradientType { Radial, Focal, } + +fn fill_to_draw(fill_style: &FillStyle) -> Option<(DrawType, Color, bool)> { + match fill_style { + FillStyle::Color(color) => Some((DrawType::Color, color.clone(), false)), + FillStyle::LinearGradient(gradient) => Some(( + DrawType::Gradient(swf_gradient_to_uniforms( + GradientType::Linear, + gradient, + swf::Fixed8::ZERO, + )), + swf::Color::WHITE, + true, + )), + FillStyle::RadialGradient(gradient) => Some(( + DrawType::Gradient(swf_gradient_to_uniforms( + GradientType::Radial, + gradient, + swf::Fixed8::ZERO, + )), + swf::Color::WHITE, + true, + )), + FillStyle::FocalGradient { + gradient, + focal_point, + } => Some(( + DrawType::Gradient(swf_gradient_to_uniforms( + GradientType::Focal, + gradient, + *focal_point, + )), + swf::Color::WHITE, + true, + )), + FillStyle::Bitmap { + size, + handle, + matrix, + is_smoothed, + is_repeating, + } => { + if let Some(bitmap) = size { + Some(( + DrawType::Bitmap(Bitmap { + matrix: swf_bitmap_to_gl_matrix( + (*matrix).into(), + bitmap.width.into(), + bitmap.height.into(), + ), + handle: handle.to_owned(), + is_smoothed: *is_smoothed, + is_repeating: *is_repeating, + }), + swf::Color::WHITE, + true, + )) + } else { + // Missing bitmap -- incorrect character ID in SWF? + None + } + } + } +} diff --git a/render/webgl/src/lib.rs b/render/webgl/src/lib.rs index 888fb1088e78..a9925cdb69b6 100644 --- a/render/webgl/src/lib.rs +++ b/render/webgl/src/lib.rs @@ -10,20 +10,20 @@ use ruffle_render::backend::null::NullBitmapSource; use ruffle_render::backend::{ Context3D, Context3DCommand, RenderBackend, ShapeHandle, ViewportDimensions, }; -use ruffle_render::bitmap::{ - Bitmap, BitmapFormat, BitmapHandle, BitmapHandleImpl, BitmapSource, SyncHandle, -}; +use ruffle_render::bitmap::{Bitmap, BitmapFormat, BitmapHandle, BitmapHandleImpl, SyncHandle}; use ruffle_render::commands::{CommandHandler, CommandList}; use ruffle_render::error::Error as BitmapError; +use ruffle_render::matrix::Matrix; use ruffle_render::quality::StageQuality; -use ruffle_render::shape_utils::DistilledShape; +use ruffle_render::shape_utils::{ShapeConverter, ShapeFills, ShapeStrokes}; use ruffle_render::tessellator::{ - Gradient as TessGradient, GradientType, ShapeTessellator, Vertex as TessVertex, + Gradient as TessGradient, GradientType, ShapeFillTessellator, ShapeStrokeTessellator, + Vertex as TessVertex, }; use ruffle_render::transform::Transform; use ruffle_web_common::{JsError, JsResult}; use std::sync::Arc; -use swf::{BlendMode, Color}; +use swf::{BlendMode, CharacterId, Color}; use thiserror::Error; use wasm_bindgen::{JsCast, JsValue}; use web_sys::{ @@ -128,7 +128,8 @@ pub struct WebGlRenderBackend { bitmap_program: ShaderProgram, gradient_program: ShaderProgram, - shape_tessellator: ShapeTessellator, + fill_tessellator: ShapeFillTessellator, + stroke_tessellator: ShapeStrokeTessellator, meshes: Vec, @@ -307,7 +308,8 @@ impl WebGlRenderBackend { gradient_program, bitmap_program, - shape_tessellator: ShapeTessellator::new(), + fill_tessellator: ShapeFillTessellator::new(), + stroke_tessellator: ShapeStrokeTessellator::new(), meshes: vec![], color_quad_shape: ShapeHandle(0), @@ -433,7 +435,6 @@ impl WebGlRenderBackend { buffer: index_buffer, }, num_indices: 6, - num_mask_indices: 6, }], }; Ok(quad_mesh) @@ -573,21 +574,31 @@ impl WebGlRenderBackend { Ok(()) } - fn register_shape_internal( + fn register_shape_fills_internal(&mut self, shape: &ShapeFills) -> Result { + let mesh = self.fill_tessellator.tessellate_shape(&shape.paths); + self.register_shape_mesh(mesh) + } + + fn register_shape_strokes_internal( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + matrix: Matrix, ) -> Result { - use ruffle_render::tessellator::DrawType as TessDrawType; + let mesh = self + .stroke_tessellator + .tessellate_shape(&shape.paths, matrix); + self.register_shape_mesh(mesh) + } - let lyon_mesh = self - .shape_tessellator - .tessellate_shape(shape, bitmap_source); + fn register_shape_mesh( + &mut self, + lyon_mesh: Vec, + ) -> Result { + use ruffle_render::tessellator::DrawType as TessDrawType; let mut draws = Vec::with_capacity(lyon_mesh.len()); for draw in lyon_mesh { let num_indices = draw.indices.len() as i32; - let num_mask_indices = draw.mask_index_count as i32; let vao = self.create_vertex_array()?; let vertex_buffer = self.gl.create_buffer().ok_or(Error::UnableToCreateBuffer)?; @@ -660,7 +671,6 @@ impl WebGlRenderBackend { buffer: index_buffer, }, num_indices, - num_mask_indices, }, TessDrawType::Gradient(gradient) => Draw { draw_type: DrawType::Gradient(Box::new(Gradient::from(gradient))), @@ -674,12 +684,11 @@ impl WebGlRenderBackend { buffer: index_buffer, }, num_indices, - num_mask_indices, }, TessDrawType::Bitmap(bitmap) => Draw { draw_type: DrawType::Bitmap(BitmapDraw { matrix: bitmap.matrix, - handle: bitmap_source.bitmap_handle(bitmap.bitmap_id, self), + handle: bitmap.handle, is_smoothed: bitmap.is_smoothed, is_repeating: bitmap.is_repeating, }), @@ -693,7 +702,6 @@ impl WebGlRenderBackend { buffer: index_buffer, }, num_indices, - num_mask_indices, }, }); @@ -983,36 +991,63 @@ impl RenderBackend for WebGlRenderBackend { self.viewport_scale_factor = dimensions.scale_factor } - fn register_shape( + fn register_shape_fills(&mut self, shape: &ShapeFills, _id: CharacterId) -> ShapeHandle { + let handle = ShapeHandle(self.meshes.len()); + match self.register_shape_fills_internal(shape) { + Ok(mesh) => self.meshes.push(mesh), + Err(e) => log::error!("Couldn't register shape fills: {:?}", e), + } + handle + } + + fn replace_shape_fills(&mut self, shape: &ShapeFills, _id: CharacterId, handle: ShapeHandle) { + self.delete_mesh(&self.meshes[handle.0]); + match self.register_shape_fills_internal(shape) { + Ok(mesh) => self.meshes[handle.0] = mesh, + Err(e) => log::error!("Couldn't replace shape fills: {:?}", e), + } + } + + fn register_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + _id: CharacterId, + matrix: Matrix, ) -> ShapeHandle { let handle = ShapeHandle(self.meshes.len()); - match self.register_shape_internal(shape, bitmap_source) { + match self.register_shape_strokes_internal(shape, matrix) { Ok(mesh) => self.meshes.push(mesh), - Err(e) => log::error!("Couldn't register shape: {:?}", e), + Err(e) => log::error!("Couldn't register shape strokes: {:?}", e), } handle } - fn replace_shape( + fn replace_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + _id: CharacterId, + matrix: Matrix, handle: ShapeHandle, ) { self.delete_mesh(&self.meshes[handle.0]); - match self.register_shape_internal(shape, bitmap_source) { + match self.register_shape_strokes_internal(shape, matrix) { Ok(mesh) => self.meshes[handle.0] = mesh, - Err(e) => log::error!("Couldn't replace shape: {:?}", e), + Err(e) => log::error!("Couldn't replace shape strokes: {:?}", e), } } fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { let shape = ruffle_render::shape_utils::swf_glyph_to_shape(glyph); let handle = ShapeHandle(self.meshes.len()); - match self.register_shape_internal((&shape).into(), &NullBitmapSource) { + let (fills, _strokes) = + ShapeConverter::from_shape(&shape, &NullBitmapSource, self).into_commands(); + + // TODO: Can glyphs have strokes? + + match self.register_shape_fills_internal(&ShapeFills { + paths: fills, + bounds: shape.shape_bounds.clone(), + }) { Ok(mesh) => self.meshes.push(mesh), Err(e) => log::error!("Couldn't register glyph shape: {:?}", e), } @@ -1234,7 +1269,14 @@ impl CommandHandler for WebGlRenderBackend { .draw_elements_with_i32(Gl::TRIANGLES, draw.num_indices, Gl::UNSIGNED_INT, 0); } - fn render_shape(&mut self, shape: ShapeHandle, transform: Transform) { + fn render_shape(&mut self, shape: ShapeHandle, transform: Transform, is_stroke: bool) { + let show_strokes = self.mask_state != MaskState::DrawMaskStencil + && self.mask_state != MaskState::ClearMaskStencil; + if !show_strokes && is_stroke { + // Ignore strokes when drawing a mask stencil. + return; + } + let world_matrix = [ [transform.matrix.a, transform.matrix.b, 0.0, 0.0], [transform.matrix.c, transform.matrix.d, 0.0, 0.0], @@ -1254,15 +1296,7 @@ impl CommandHandler for WebGlRenderBackend { let mesh = &self.meshes[shape.0]; for draw in &mesh.draws { - // Ignore strokes when drawing a mask stencil. - let num_indices = if self.mask_state != MaskState::DrawMaskStencil - && self.mask_state != MaskState::ClearMaskStencil - { - draw.num_indices - } else { - draw.num_mask_indices - }; - if num_indices == 0 { + if draw.num_indices == 0 { continue; } @@ -1378,7 +1412,7 @@ impl CommandHandler for WebGlRenderBackend { // Draw the triangles. self.gl - .draw_elements_with_i32(Gl::TRIANGLES, num_indices, Gl::UNSIGNED_INT, 0); + .draw_elements_with_i32(Gl::TRIANGLES, draw.num_indices, Gl::UNSIGNED_INT, 0); } } @@ -1577,7 +1611,6 @@ struct Draw { index_buffer: Buffer, vao: WebGlVertexArrayObject, num_indices: i32, - num_mask_indices: i32, } enum DrawType { diff --git a/render/wgpu/src/backend.rs b/render/wgpu/src/backend.rs index 22e39d0d51fd..eccae22783fd 100644 --- a/render/wgpu/src/backend.rs +++ b/render/wgpu/src/backend.rs @@ -1,7 +1,6 @@ -use crate::buffer_builder::BufferBuilder; use crate::buffer_pool::TexturePool; use crate::context3d::WgpuContext3D; -use crate::mesh::{Mesh, PendingDraw}; +use crate::mesh::Mesh; use crate::surface::Surface; use crate::target::RenderTargetFrame; use crate::target::TextureTarget; @@ -11,22 +10,24 @@ use crate::{ QueueSyncHandle, RenderTarget, SwapChainTarget, Texture, Transforms, }; use gc_arena::MutationContext; +use ruffle_render::backend::null::NullBitmapSource; use ruffle_render::backend::{Context3D, Context3DCommand}; use ruffle_render::backend::{RenderBackend, ShapeHandle, ViewportDimensions}; -use ruffle_render::bitmap::{Bitmap, BitmapHandle, BitmapSource, SyncHandle}; +use ruffle_render::bitmap::{Bitmap, BitmapHandle, SyncHandle}; use ruffle_render::commands::CommandList; use ruffle_render::error::Error as BitmapError; use ruffle_render::filters::Filter; +use ruffle_render::matrix::Matrix; use ruffle_render::quality::StageQuality; -use ruffle_render::shape_utils::DistilledShape; -use ruffle_render::tessellator::ShapeTessellator; +use ruffle_render::shape_utils::{ShapeConverter, ShapeFills, ShapeStrokes}; +use ruffle_render::tessellator::{ShapeFillTessellator, ShapeStrokeTessellator}; use std::borrow::Cow; use std::cell::Cell; use std::mem; use std::num::NonZeroU32; use std::path::Path; use std::sync::Arc; -use swf::Color; +use swf::{CharacterId, Color}; use tracing::instrument; use wgpu::Extent3d; @@ -37,7 +38,8 @@ pub struct WgpuRenderBackend { target: T, surface: Surface, meshes: Vec, - shape_tessellator: ShapeTessellator, + fill_tessellator: ShapeFillTessellator, + stroke_tessellator: ShapeStrokeTessellator, // This is currently unused - we just store it to report in // `get_viewport_dimensions` viewport_scale_factor: f64, @@ -183,7 +185,8 @@ impl WgpuRenderBackend { target, surface, meshes: Vec::new(), - shape_tessellator: ShapeTessellator::new(), + fill_tessellator: ShapeFillTessellator::new(), + stroke_tessellator: ShapeStrokeTessellator::new(), viewport_scale_factor: 1.0, texture_pool: TexturePool::new(), offscreen_texture_pool: TexturePool::new(), @@ -217,64 +220,21 @@ impl WgpuRenderBackend { Ok((adapter, device, queue)) } - fn register_shape_internal( + fn register_shape_fills_internal(&mut self, shape: &ShapeFills, shape_id: CharacterId) -> Mesh { + let mesh = self.fill_tessellator.tessellate_shape(&shape.paths); + Mesh::build(self, mesh, shape_id) + } + + fn register_shape_strokes_internal( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + shape_id: CharacterId, + matrix: Matrix, ) -> Mesh { - let shape_id = shape.id; - let lyon_mesh = self - .shape_tessellator - .tessellate_shape(shape, bitmap_source); - - let mut draws = Vec::with_capacity(lyon_mesh.len()); - let mut uniform_buffer = BufferBuilder::new( - self.descriptors.limits.min_uniform_buffer_offset_alignment as usize, - ); - let mut vertex_buffer = BufferBuilder::new(0); - let mut index_buffer = BufferBuilder::new(0); - for draw in lyon_mesh { - let draw_id = draws.len(); - if let Some(draw) = PendingDraw::new( - self, - bitmap_source, - draw, - shape_id, - draw_id, - &mut uniform_buffer, - &mut vertex_buffer, - &mut index_buffer, - ) { - draws.push(draw); - } - } - - let uniform_buffer = uniform_buffer.finish( - &self.descriptors.device, - create_debug_label!("Shape {} uniforms", shape_id), - wgpu::BufferUsages::UNIFORM, - ); - let vertex_buffer = vertex_buffer.finish( - &self.descriptors.device, - create_debug_label!("Shape {} vertices", shape_id), - wgpu::BufferUsages::VERTEX, - ); - let index_buffer = index_buffer.finish( - &self.descriptors.device, - create_debug_label!("Shape {} indices", shape_id), - wgpu::BufferUsages::INDEX, - ); - - let draws = draws - .into_iter() - .map(|d| d.finish(&self.descriptors, &uniform_buffer)) - .collect(); - - Mesh { - draws, - vertex_buffer, - index_buffer, - } + let mesh = self + .stroke_tessellator + .tessellate_shape(&shape.paths, matrix); + Mesh::build(self, mesh, shape_id) } pub fn descriptors(&self) -> &Arc { @@ -419,25 +379,41 @@ impl RenderBackend for WgpuRenderBackend { } #[instrument(level = "debug", skip_all)] - fn register_shape( + fn register_shape_fills(&mut self, shape: &ShapeFills, id: CharacterId) -> ShapeHandle { + let handle = ShapeHandle(self.meshes.len()); + let meshes = self.register_shape_fills_internal(shape, id); + self.meshes.push(meshes); + handle + } + + #[instrument(level = "debug", skip_all)] + fn replace_shape_fills(&mut self, shape: &ShapeFills, id: CharacterId, handle: ShapeHandle) { + let mesh = self.register_shape_fills_internal(shape, id); + self.meshes[handle.0] = mesh; + } + + #[instrument(level = "debug", skip_all)] + fn register_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + id: CharacterId, + matrix: Matrix, ) -> ShapeHandle { let handle = ShapeHandle(self.meshes.len()); - let mesh = self.register_shape_internal(shape, bitmap_source); - self.meshes.push(mesh); + let meshes = self.register_shape_strokes_internal(shape, id, matrix); + self.meshes.push(meshes); handle } #[instrument(level = "debug", skip_all)] - fn replace_shape( + fn replace_shape_strokes( &mut self, - shape: DistilledShape, - bitmap_source: &dyn BitmapSource, + shape: &ShapeStrokes, + id: CharacterId, + matrix: Matrix, handle: ShapeHandle, ) { - let mesh = self.register_shape_internal(shape, bitmap_source); + let mesh = self.register_shape_strokes_internal(shape, id, matrix); self.meshes[handle.0] = mesh; } @@ -445,9 +421,17 @@ impl RenderBackend for WgpuRenderBackend { fn register_glyph_shape(&mut self, glyph: &swf::Glyph) -> ShapeHandle { let shape = ruffle_render::shape_utils::swf_glyph_to_shape(glyph); let handle = ShapeHandle(self.meshes.len()); - let mesh = self.register_shape_internal( - (&shape).into(), - &ruffle_render::backend::null::NullBitmapSource, + let (fills, _strokes) = + ShapeConverter::from_shape(&shape, &NullBitmapSource, self).into_commands(); + + // TODO: Can glyphs have strokes? + + let mesh = self.register_shape_fills_internal( + &ShapeFills { + paths: fills, + bounds: shape.shape_bounds.clone(), + }, + shape.id, ); self.meshes.push(mesh); handle diff --git a/render/wgpu/src/lib.rs b/render/wgpu/src/lib.rs index 0fb6f954c4be..980d5d7342a6 100644 --- a/render/wgpu/src/lib.rs +++ b/render/wgpu/src/lib.rs @@ -110,8 +110,8 @@ struct PosVertex { position: [f32; 2], } -impl From for PosVertex { - fn from(vertex: TessVertex) -> Self { +impl From<&TessVertex> for PosVertex { + fn from(vertex: &TessVertex) -> Self { Self { position: [vertex.x, vertex.y], } @@ -125,8 +125,8 @@ struct PosColorVertex { color: [f32; 4], } -impl From for PosColorVertex { - fn from(vertex: TessVertex) -> Self { +impl From<&TessVertex> for PosColorVertex { + fn from(vertex: &TessVertex) -> Self { Self { position: [vertex.x, vertex.y], color: [ diff --git a/render/wgpu/src/mesh.rs b/render/wgpu/src/mesh.rs index 2a4df3cb275f..b3eccdc5b630 100644 --- a/render/wgpu/src/mesh.rs +++ b/render/wgpu/src/mesh.rs @@ -7,8 +7,6 @@ use std::ops::Range; use wgpu::util::DeviceExt; use crate::buffer_builder::BufferBuilder; -use ruffle_render::backend::RenderBackend; -use ruffle_render::bitmap::BitmapSource; use ruffle_render::tessellator::{Bitmap, Draw as LyonDraw, DrawType as TessDrawType, Gradient}; use swf::{CharacterId, GradientInterpolation}; @@ -22,13 +20,71 @@ pub struct Mesh { pub index_buffer: wgpu::Buffer, } +impl Mesh { + pub fn build( + backend: &mut WgpuRenderBackend, + lyon_mesh: Vec, + shape_id: CharacterId, + ) -> Self { + let mut draws = Vec::with_capacity(lyon_mesh.len()); + let mut uniform_buffer = BufferBuilder::new( + backend + .descriptors() + .limits + .min_uniform_buffer_offset_alignment as usize, + ); + let mut vertex_buffer = BufferBuilder::new(0); + let mut index_buffer = BufferBuilder::new(0); + for draw in lyon_mesh { + let draw_id = draws.len(); + if let Some(draw) = PendingDraw::new( + backend, + draw, + shape_id, + draw_id, + &mut uniform_buffer, + &mut vertex_buffer, + &mut index_buffer, + ) { + draws.push(draw); + } + } + + let uniform_buffer = uniform_buffer.finish( + &backend.descriptors().device, + create_debug_label!("Shape {} uniforms", shape_id), + wgpu::BufferUsages::UNIFORM, + ); + let vertex_buffer = vertex_buffer.finish( + &backend.descriptors().device, + create_debug_label!("Shape {} vertices", shape_id), + wgpu::BufferUsages::VERTEX, + ); + let index_buffer = index_buffer.finish( + &backend.descriptors().device, + create_debug_label!("Shape {} indices", shape_id), + wgpu::BufferUsages::INDEX, + ); + + let draws = draws + .into_iter() + .map(|d| d.finish(backend.descriptors(), &uniform_buffer)) + .collect(); + + Self { + draws, + vertex_buffer, + index_buffer, + } + } +} + #[derive(Debug)] pub struct PendingDraw { pub draw_type: PendingDrawType, pub vertices: Range, pub indices: Range, pub num_indices: u32, - pub num_mask_indices: u32, } impl PendingDraw { @@ -38,7 +94,6 @@ impl PendingDraw { vertices: self.vertices, indices: self.indices, num_indices: self.num_indices, - num_mask_indices: self.num_mask_indices, } } } @@ -49,14 +104,12 @@ pub struct Draw { pub vertices: Range, pub indices: Range, pub num_indices: u32, - pub num_mask_indices: u32, } impl PendingDraw { #[allow(clippy::too_many_arguments)] pub fn new( backend: &mut WgpuRenderBackend, - source: &dyn BitmapSource, draw: LyonDraw, shape_id: CharacterId, draw_id: usize, @@ -65,14 +118,10 @@ impl PendingDraw { index_buffer: &mut BufferBuilder, ) -> Option { let vertices = if matches!(draw.draw_type, TessDrawType::Color) { - let vertices: Vec<_> = draw - .vertices - .into_iter() - .map(PosColorVertex::from) - .collect(); + let vertices: Vec<_> = draw.vertices.iter().map(PosColorVertex::from).collect(); vertex_buffer.add(&vertices) } else { - let vertices: Vec<_> = draw.vertices.into_iter().map(PosVertex::from).collect(); + let vertices: Vec<_> = draw.vertices.iter().map(PosVertex::from).collect(); vertex_buffer.add(&vertices) }; @@ -89,7 +138,7 @@ impl PendingDraw { uniform_buffer, ), TessDrawType::Bitmap(bitmap) => { - PendingDrawType::bitmap(bitmap, shape_id, draw_id, source, backend, uniform_buffer)? + PendingDrawType::bitmap(bitmap, shape_id, draw_id, uniform_buffer)? } }; Some(PendingDraw { @@ -97,7 +146,6 @@ impl PendingDraw { vertices, indices, num_indices: index_count, - num_mask_indices: draw.mask_index_count, }) } } @@ -240,24 +288,26 @@ impl PendingDrawType { bitmap: Bitmap, shape_id: CharacterId, draw_id: usize, - source: &dyn BitmapSource, - backend: &mut dyn RenderBackend, uniform_buffers: &mut BufferBuilder, ) -> Option { - let handle = source.bitmap_handle(bitmap.bitmap_id, backend)?; - let texture = as_texture(&handle); - let texture_view = texture.texture.create_view(&Default::default()); - let texture_transforms_index = create_texture_transforms(&bitmap.matrix, uniform_buffers); - let bind_group_label = - create_debug_label!("Shape {} (bitmap) draw {} bindgroup", shape_id, draw_id); + if let Some(handle) = &bitmap.handle { + let texture = as_texture(handle); + let texture_view = texture.texture.create_view(&Default::default()); + let texture_transforms_index = + create_texture_transforms(&bitmap.matrix, uniform_buffers); + let bind_group_label = + create_debug_label!("Shape {} (bitmap) draw {} bindgroup", shape_id, draw_id); - Some(PendingDrawType::Bitmap { - texture_transforms_index, - texture_view, - is_repeating: bitmap.is_repeating, - is_smoothed: bitmap.is_smoothed, - bind_group_label, - }) + Some(PendingDrawType::Bitmap { + texture_transforms_index, + texture_view, + is_repeating: bitmap.is_repeating, + is_smoothed: bitmap.is_smoothed, + bind_group_label, + }) + } else { + None + } } pub fn finish(self, descriptors: &Descriptors, uniform_buffer: &wgpu::Buffer) -> DrawType { diff --git a/render/wgpu/src/surface/commands.rs b/render/wgpu/src/surface/commands.rs index 73367c595832..14bcb09e29f5 100644 --- a/render/wgpu/src/surface/commands.rs +++ b/render/wgpu/src/surface/commands.rs @@ -93,7 +93,11 @@ impl<'pass, 'frame: 'pass, 'global: 'frame> CommandRenderer<'pass, 'frame, 'glob transform, blend_mode, } => self.render_texture(transform, binds, *blend_mode), - DrawCommand::RenderShape { shape, transform } => self.render_shape(*shape, transform), + DrawCommand::RenderShape { + shape, + transform, + is_stroke, + } => self.render_shape(*shape, transform, *is_stroke), DrawCommand::DrawRect { color, matrix } => self.draw_rect(color, matrix), DrawCommand::PushMask => self.push_mask(), DrawCommand::ActivateMask => self.activate_mask(), @@ -297,23 +301,29 @@ impl<'pass, 'frame: 'pass, 'global: 'frame> CommandRenderer<'pass, 'frame, 'glob } } - pub fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform) { + pub fn render_shape(&mut self, shape: ShapeHandle, transform: &Transform, is_stroke: bool) { + let show_strokes = self.mask_state != MaskState::DrawMaskStencil + && self.mask_state != MaskState::ClearMaskStencil; + if !show_strokes && is_stroke { + return; + } + if cfg!(feature = "render_debug_labels") { self.render_pass .push_debug_group(&format!("render_shape {}", shape.0)); } let mesh = &self.meshes[shape.0]; + self.draw_mesh(transform, mesh); + + if cfg!(feature = "render_debug_labels") { + self.render_pass.pop_debug_group(); + } + } + + fn draw_mesh(&mut self, transform: &Transform, mesh: &'pass Mesh) { for draw in &mesh.draws { - let num_indices = if self.mask_state != MaskState::DrawMaskStencil - && self.mask_state != MaskState::ClearMaskStencil - { - draw.num_indices - } else { - // Omit strokes when drawing a mask stencil. - draw.num_mask_indices - }; - if num_indices == 0 { + if draw.num_indices == 0 { continue; } @@ -333,12 +343,9 @@ impl<'pass, 'frame: 'pass, 'global: 'frame> CommandRenderer<'pass, 'frame, 'glob self.draw( mesh.vertex_buffer.slice(draw.vertices.clone()), mesh.index_buffer.slice(draw.indices.clone()), - num_indices, + draw.num_indices, ); } - if cfg!(feature = "render_debug_labels") { - self.render_pass.pop_debug_group(); - } } pub fn draw_rect(&mut self, color: &Color, matrix: &Matrix) { @@ -436,6 +443,7 @@ pub enum DrawCommand { RenderShape { shape: ShapeHandle, transform: Transform, + is_stroke: bool, }, DrawRect { color: Color, @@ -575,9 +583,15 @@ pub fn chunk_blends<'a>( render_stage3d: true, }) } - Command::RenderShape { shape, transform } => { - current.push(DrawCommand::RenderShape { shape, transform }) - } + Command::RenderShape { + shape, + transform, + is_stroke, + } => current.push(DrawCommand::RenderShape { + shape, + transform, + is_stroke, + }), Command::DrawRect { color, matrix } => { current.push(DrawCommand::DrawRect { color, matrix }) } diff --git a/swf/src/types.rs b/swf/src/types.rs index 0eee5c3509c5..bcf6cdc77523 100644 --- a/swf/src/types.rs +++ b/swf/src/types.rs @@ -939,6 +939,16 @@ impl LineStyle { self.width = val; self } + + #[inline] + pub fn flags(&self) -> LineStyleFlag { + self.flags + } + + #[inline] + pub fn miter_limit(&self) -> Fixed8 { + self.miter_limit + } } impl Default for LineStyle { diff --git a/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/expected.png b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/expected.png new file mode 100644 index 000000000000..4d327b18f4e8 Binary files /dev/null and b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/expected.png differ diff --git a/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/output.txt b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/output.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.fla b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.fla new file mode 100644 index 000000000000..b12afca05a15 Binary files /dev/null and b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.fla differ diff --git a/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.swf b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.swf new file mode 100644 index 000000000000..3100abc59d1b Binary files /dev/null and b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.swf differ diff --git a/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.toml b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.toml new file mode 100644 index 000000000000..af63faefdf48 --- /dev/null +++ b/tests/tests/swfs/visual/simple_shapes/morph/rotating_shape/test.toml @@ -0,0 +1,7 @@ +num_frames = 2 + +[image_comparison] +tolerance = 0 + +[player_options] +with_renderer = { optional = false, sample_count = 1 } \ No newline at end of file diff --git a/tests/tests/swfs/visual/simple_shapes/strokes/scale/expected.png b/tests/tests/swfs/visual/simple_shapes/strokes/scale/expected.png index ae8ac9baebfa..ea647275e652 100644 Binary files a/tests/tests/swfs/visual/simple_shapes/strokes/scale/expected.png and b/tests/tests/swfs/visual/simple_shapes/strokes/scale/expected.png differ diff --git a/tests/tests/swfs/visual/simple_shapes/strokes/scale/test.toml b/tests/tests/swfs/visual/simple_shapes/strokes/scale/test.toml index f4264f815c5b..edf95a32f8c4 100644 --- a/tests/tests/swfs/visual/simple_shapes/strokes/scale/test.toml +++ b/tests/tests/swfs/visual/simple_shapes/strokes/scale/test.toml @@ -1,8 +1,7 @@ num_frames = 1 -ignore = true [image_comparison] -tolerance = 0 +max_outliers = 3 [player_options] -with_renderer = { optional = true, sample_count = 1 } \ No newline at end of file +with_renderer = { optional = false, sample_count = 1 } \ No newline at end of file