Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 63 additions & 4 deletions core/src/display_object/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use crate::library::MovieLibrarySource;
use crate::prelude::*;
use crate::tag_utils::SwfMovie;
use crate::tessellation_cache::TessellationCache;
use crate::vminterface::Instantiator;
use core::fmt;
use gc_arena::barrier::unlock;
Expand Down Expand Up @@ -63,6 +64,7 @@
),
shape: swf_shape,
movie,
scaled_handle: RefCell::new(TessellationCache::new()),
};

Graphic(Gc::new(
Expand Down Expand Up @@ -96,6 +98,7 @@
shape: Vec::new(),
},
movie: context.root_swf.clone(),
scaled_handle: RefCell::new(TessellationCache::new()),
};

Graphic(Gc::new(
Expand All @@ -121,6 +124,51 @@
fn set_shared(self, mc: &Mutation<'gc>, shared: Gc<'gc, GraphicShared>) {
unlock!(Gc::write(mc, self.0), GraphicData, shared).set(shared);
}

/// Returns the best shape handle for the current scale, retessellating if necessary.
fn get_or_retessellate_handle(
self,
context: &mut RenderContext,
base_handle: &ShapeHandle,
current_scale: f32,
) -> ShapeHandle {
// Since graphics are created from a shared shape, we may be able to reuse a
// cached tessellation from another instance at a similar scale.
let shared = self.0.shared.get();

{
let mut cache = shared.scaled_handle.borrow_mut();
if let Some(handle) = cache.find_near_and_touch(current_scale) {
// Found a cached handle at a similar scale; reuse it.
return handle;
}
}

// Retessellate at the new scale
let library = context.library.library_for_movie(shared.movie.clone());
if let Some(library) = library {
let new_handle = context.renderer.register_shape_with_scale(
(&shared.shape).into(),
&MovieLibrarySource { library },
current_scale,
);

{
let mut cache = shared.scaled_handle.borrow_mut();
tracing::debug!(
"Graphic id={} retessellated: new_scale={:.2}, cache_size={}",
shared.id,

Check warning on line 160 in core/src/display_object/graphic.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (160)
current_scale,
cache.len()

Check warning on line 162 in core/src/display_object/graphic.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (162)
);
cache.insert(current_scale, new_handle.clone());
}

new_handle
} else {
base_handle.clone()

Check warning on line 169 in core/src/display_object/graphic.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (169)
}
}
}

impl<'gc> TDisplayObject<'gc> for Graphic<'gc> {
Expand Down Expand Up @@ -201,10 +249,19 @@

if let Some(drawing) = self.0.drawing.get() {
drawing.borrow().render(context);
} else if let Some(render_handle) = self.0.shared.get().render_handle.clone() {
context
.commands
.render_shape(render_handle, context.transform_stack.transform())
} else if let Some(base_handle) = self.0.shared.get().render_handle.clone() {
let transform = context.transform_stack.transform();

// Calculate the current scale from the transform, to determine if
// we can reuse a cached tessellation or need to retessellate.
let matrix = &transform.matrix;
let scale_x = f32::abs(matrix.a + matrix.c);
let scale_y = f32::abs(matrix.b + matrix.d);
let current_scale = ((scale_x * scale_x + scale_y * scale_y) / 2.0).sqrt();

let handle = self.get_or_retessellate_handle(context, &base_handle, current_scale);

context.commands.render_shape(handle, transform)
}
}

Expand Down Expand Up @@ -278,4 +335,6 @@
render_handle: Option<ShapeHandle>,
bounds: Rectangle<Twips>,
movie: Arc<SwfMovie>,
#[collect(require_static)]
scaled_handle: RefCell<TessellationCache>,
}
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod streams;
pub mod string;
mod system_properties;
pub mod tag_utils;
mod tessellation_cache;
pub mod timer;
mod types;
mod vminterface;
Expand Down
112 changes: 112 additions & 0 deletions core/src/tessellation_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use ruffle_render::backend::ShapeHandle;

/// The scale ratio threshold beyond which a graphic or a drawing will be retessellated.
const RETESSELLATION_SCALE_THRESHOLD: f32 = 2.0;

/// The inverse of the retessellation scale threshold
/// Used to avoid computing the inverse repeatedly when comparing scale ratios against the threshold.
const RETESSELLATION_SCALE_THRESHOLD_INVERSE: f32 = 1.0 / RETESSELLATION_SCALE_THRESHOLD;

/// The maximum number of retessellated shapes to cache per original shape.
const RETESSELLATION_CACHE_SIZE: usize = 4;

#[derive(Clone, Debug)]
pub(crate) struct TessellationCache {
entries: [Option<(f32, ShapeHandle)>; RETESSELLATION_CACHE_SIZE],
len: usize,
}

/// A cache for tessellated shapes at different scales, using a simple LRU eviction policy.
/// (LRU index: 0, MRU index: len - 1)
///
/// The cache stores a fixed number of entries, and when it is full, the least recently
/// used entry is evicted to make room for new entries.
///
/// This is used to avoid retessellating shapes at similar scales multiple times, which can be expensive.
impl TessellationCache {
/// Creates a new, empty tessellation cache.
pub(crate) fn new() -> Self {
Self {
entries: std::array::from_fn(|_| None),
len: 0,
}
}

/// Finds the cached shape handle with the closest scale to the target scale.
///
/// If the closest scale is NOT within the retessellation threshold,
/// then `None` is returned, indicating that the shape should be retessellated at the target scale.
pub(crate) fn find_near_and_touch(&mut self, target_scale: f32) -> Option<ShapeHandle> {
let mut best_index = None;
let mut best_deviation = f32::INFINITY;

for index in 0..self.len {
if let Some((cached_scale, _)) = &self.entries[index] {
let ratio = f32::abs(target_scale / cached_scale);

Comment thread
darktohka marked this conversation as resolved.
// Check if the cached scale is within the retessellation threshold of the target scale.
if ratio <= RETESSELLATION_SCALE_THRESHOLD
&& ratio >= RETESSELLATION_SCALE_THRESHOLD_INVERSE
{
// Choose the cached entry with the smallest deviation from the target scale.
let deviation = f32::abs(ratio - 1.0);

if deviation < best_deviation {
best_deviation = deviation;
best_index = Some(index);
}
}
}

Check warning on line 59 in core/src/tessellation_cache.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (59)
}

// If we found a suitable cached entry, move it to the most recently used position.
best_index.map(|index| self.touch_entry(index))
}

/// Inserts a new cached shape handle with the given scale.
///
/// If the cache is full, the least recently used entry will be evicted to make room for the new entry.
/// We assume that the caller has already checked that the new entry is not too similar to existing entries.
pub(crate) fn insert(&mut self, scale: f32, handle: ShapeHandle) {
if self.len < RETESSELLATION_CACHE_SIZE {
// If the cache is not full, simply add the new entry at the end.
self.entries[self.len] = Some((scale, handle));
self.len += 1;
return;
}

// If the cache is full, evict the least recently used entry.
for i in 1..self.len {
self.entries[i - 1] = self.entries[i].take();
}

self.entries[self.len - 1] = Some((scale, handle));
}

/// Returns the number of cached entries currently stored in the cache.
pub(crate) fn len(&self) -> usize {
self.len
}

Check warning on line 89 in core/src/tessellation_cache.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (87–89)

/// Moves the entry at the given index to the most recently used position and returns its shape handle.
fn touch_entry(&mut self, index: usize) -> ShapeHandle {
if index == self.len - 1 {
// The entry is already the most recently used, so we can return its handle directly.
return self.entries[index]
.as_ref()
.expect("tessellation cache entry exists")
.1
.clone();
}

let entry = self.entries[index].take().expect("entry exists");

for i in index + 1..self.len {
self.entries[i - 1] = self.entries[i].take();
}

// Move touched entry to MRU position (end)
self.entries[self.len - 1] = Some(entry.clone());
entry.1
}
}
10 changes: 10 additions & 0 deletions render/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ pub trait RenderBackend: Any {
bitmap_source: &dyn BitmapSource,
) -> ShapeHandle;

fn register_shape_with_scale(
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
_scale: f32,
) -> ShapeHandle {
// Default implementation ignores scale
self.register_shape(shape, bitmap_source)
}

fn render_offscreen(
&mut self,
handle: BitmapHandle,
Expand Down
43 changes: 36 additions & 7 deletions render/src/tessellator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
is_stroke: bool,
}

const TESSELLATION_EPSILON: f32 = 0.0000001;

impl ShapeTessellator {
pub fn new() -> Self {
Self {
Expand All @@ -38,6 +40,16 @@
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
) -> Mesh {
self.tessellate_shape_with_scale(shape, bitmap_source, 1.0)
}

Check warning on line 45 in render/src/tessellator.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered lines (43–45)

#[instrument(level = "debug", skip_all)]
pub fn tessellate_shape_with_scale(
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
scale: f32,
) -> Mesh {
self.mesh = Vec::new();
self.gradients = IndexSet::new();
Expand Down Expand Up @@ -151,16 +163,33 @@
let mut buffers_builder =
BuffersBuilder::new(&mut self.lyon_mesh, RuffleVertexCtor { color });
let result = match path {
DrawPath::Fill { winding_rule, .. } => self.fill_tess.tessellate_path(
&lyon_path,
&FillOptions::default().with_fill_rule(winding_rule.into()),
&mut buffers_builder,
),
DrawPath::Fill { winding_rule, .. } => {
// Larger scales require more precise tessellation to avoid artifacts
let tolerance = FillOptions::DEFAULT_TOLERANCE / scale;
Comment thread
darktohka marked this conversation as resolved.
self.fill_tess.tessellate_path(
&lyon_path,
&FillOptions::default()
.with_fill_rule(winding_rule.into())
.with_tolerance(tolerance),
&mut buffers_builder,
)
}
DrawPath::Stroke { style, .. } => {
// TODO(Herschel): 0 width indicates "hairline".
let width = (style.width().to_pixels() as f32).max(1.0);
// This calculation ensures that hairline strokes are rendered with
// a minimum width of 1 pixel, while still allowing for proper scaling
let width = style.width().to_pixels() as f32;
let min_screen_width = if f32::abs(scale) > TESSELLATION_EPSILON {
1.0 / scale
} else {
1.0

Check warning on line 184 in render/src/tessellator.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (184)
};
let width = width.max(min_screen_width);

// Larger scales require more precise tessellation to avoid artifacts
let tolerance = StrokeOptions::DEFAULT_TOLERANCE / scale;
Comment thread
darktohka marked this conversation as resolved.
let mut stroke_options = StrokeOptions::default()
.with_line_width(width)
.with_tolerance(tolerance)
.with_start_cap(match style.start_cap() {
swf::LineCapStyle::None => tessellation::LineCap::Butt,
swf::LineCapStyle::Round => tessellation::LineCap::Round,
Expand Down
18 changes: 14 additions & 4 deletions render/webgl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,12 +567,13 @@ impl WebGlRenderBackend {
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
scale: f32,
) -> Result<Vec<Draw>, Error> {
use ruffle_render::tessellator::DrawType as TessDrawType;

let lyon_mesh = self
.shape_tessellator
.tessellate_shape(shape, bitmap_source);
let lyon_mesh =
self.shape_tessellator
.tessellate_shape_with_scale(shape, bitmap_source, scale);

let mut draws = Vec::with_capacity(lyon_mesh.draws.len());
for draw in lyon_mesh.draws {
Expand Down Expand Up @@ -1040,7 +1041,16 @@ impl RenderBackend for WebGlRenderBackend {
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
) -> ShapeHandle {
let mesh = match self.register_shape_internal(shape, bitmap_source) {
self.register_shape_with_scale(shape, bitmap_source, 1.0)
}

fn register_shape_with_scale(
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
scale: f32,
) -> ShapeHandle {
let mesh = match self.register_shape_internal(shape, bitmap_source, scale) {
Ok(draws) => Mesh {
draws,
gl2: self.gl2.clone(),
Expand Down
20 changes: 16 additions & 4 deletions render/wgpu/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,12 @@ impl<T: RenderTarget> WgpuRenderBackend<T> {
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
scale: f32,
) -> Mesh {
let shape_id = shape.id;
let lyon_mesh = self
.shape_tessellator
.tessellate_shape(shape, bitmap_source);
let lyon_mesh =
self.shape_tessellator
.tessellate_shape_with_scale(shape, bitmap_source, scale);

let mut draws = Vec::with_capacity(lyon_mesh.draws.len());
let mut uniform_buffer = BufferBuilder::new_for_uniform(&self.descriptors.limits);
Expand Down Expand Up @@ -492,7 +493,18 @@ impl<T: RenderTarget + 'static> RenderBackend for WgpuRenderBackend<T> {
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
) -> ShapeHandle {
let mesh = self.register_shape_internal(shape, bitmap_source);
let mesh = self.register_shape_internal(shape, bitmap_source, 1.0);
ShapeHandle(Arc::new(mesh))
}

#[instrument(level = "debug", skip_all)]
fn register_shape_with_scale(
&mut self,
shape: DistilledShape,
bitmap_source: &dyn BitmapSource,
scale: f32,
) -> ShapeHandle {
let mesh = self.register_shape_internal(shape, bitmap_source, scale);
ShapeHandle(Arc::new(mesh))
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/tests/swfs/from_shumway/acid/acid-clip/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ max_outliers = 0

[[image_comparisons.output.checks]]
tolerance = 64
max_outliers = 125
max_outliers = 140

[[image_comparisons.output.checks]]
tolerance = 0
Expand Down
Binary file modified tests/tests/swfs/from_shumway/acid/acid-mask/output.01.ruffle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/tests/swfs/from_shumway/acid/acid-mask/output.05.ruffle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/tests/swfs/from_shumway/acid/acid-mask/output.10.ruffle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading