Skip to content

render: Improve hairline strokes and scaling strokes on WebGL and WGPU#23011

Open
darktohka wants to merge 6 commits intoruffle-rs:masterfrom
darktohka:bugfix/hairline-strokes
Open

render: Improve hairline strokes and scaling strokes on WebGL and WGPU#23011
darktohka wants to merge 6 commits intoruffle-rs:masterfrom
darktohka:bugfix/hairline-strokes

Conversation

@darktohka
Copy link

@darktohka darktohka commented Feb 11, 2026

This pull request improves both hairline strokes and scaling strokes on the Web (WGPU, WebGL renderers) and Desktop (WGPU renderer) targets.

The main idea is to keep track of the scale of the graphics that are being tessellated on the rendering backends. The tessellated shapes are then stored in a tessellation cache, which is a simple LRU cache that keeps track of the most frequently tessellated shapes (4 max per shared graphic). This means that the last 4 uniquely used tessellated scale buckets will be left cached. Shapes will only be retessellated if they grow or shrink by 2x relative to a cached variant (controlled by RETESSELLATION_SCALE_THRESHOLD).

When a shape grows disproportionately, it is re-tessellated. The re-tessellation precision (threshold) is specified by the scale. The larger the scale, the more precise the tessellation will be: small objects are expected to have less detail either way.

Tessellation cache is reused between graphic instances that use the same graphic as an optimization.

Hairline stroke rendering is also improved.

This fixes issues such as (tested them): #18852 #21803 #751 #7369 #14268 #13984 #1955 #3216 #9044 #2023 #11704 #12360 #14551 #20211 #1412
Partially (composite issues - not all from these are fixed, just the strokes): #10524 #12057
Could not test (site locks, missing SWF, etc): #20345 #3216 #18855 #1625 #9309

Relevant technical discussions: #7042 #7369 #751

Before:
image
After:
image

Before:
image
After:
image

Before:
image
After:
image

Before:
image
After:
image

Copilot AI review requested due to automatic review settings February 11, 2026 23:52
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request significantly improves rendering quality of hairline and scaled strokes in Ruffle's WebGL and WGPU backends by implementing scale-aware tessellation. The implementation adds an LRU tessellation cache that stores up to 4 different tessellations per graphic at different scales, retessellating only when shapes grow or shrink by more than 2x. This approach addresses numerous long-standing rendering issues where strokes appeared too thick or too thin when graphics were scaled.

Changes:

  • Introduces TessellationCache with LRU eviction to cache tessellated shapes at different scales
  • Adds register_shape_with_scale() method to render backends to support scale-aware tessellation
  • Modifies tessellator to adjust hairline stroke width and tessellation tolerance based on scale
  • Updates Graphic display objects to calculate current scale and retrieve or create appropriately scaled tessellations

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
core/src/tessellation_cache.rs New LRU cache for storing up to 4 tessellated shapes per graphic at different scales
core/src/lib.rs Adds tessellation_cache module to the core library
core/src/display_object/graphic.rs Integrates tessellation cache; calculates scale from transform matrix and retrieves/creates scaled tessellations
render/src/backend.rs Adds register_shape_with_scale() trait method with default implementation
render/wgpu/src/backend.rs Implements scale-aware shape registration for WGPU backend
render/webgl/src/lib.rs Implements scale-aware shape registration for WebGL backend
render/src/tessellator.rs Adjusts hairline stroke width and tessellation tolerance based on scale to prevent artifacts

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@darktohka
Copy link
Author

darktohka commented Feb 12, 2026

Tests that have improved:

  • visual/simple_shapes/strokes/scale: Large black box fixed, triangular circle fixed, very slim square fixed
  • from_shumway/acid/acid-stroke-0: Fixed invisible square
  • from_gnash/misc-ming.all/shape_test: Fixed very thick line

Slightly different but visually indistinguishable (mostly due to precision increasing):

  • visual/cache_as_bitmap/scroll_rect_scaled: Edges are differently defined
  • visual/cache_as_bitmap/masks: Edge of squares are differently defined
  • from_shumway/acid/acid-video: Visual's position is a little bit off, but visually indistinguishable, Windows CI runner renders Ruffle version differently as well compared to Linux and Mac CI (3 pixels difference)
  • from_shumway/acid/acid-mask: Edges are differently defined
  • from_shumway/acid/acid-clip: Edges are differently defined
  • from_shumway/acid/acid-blend-2: Edges are differently defined
  • from_shumway/3_joystick: Different antialiasing around circle
  • avm1/edittext_stylesheet: Edges are differently defined
  • visual/edittext/edittext_caret_empty: Edges are differently defined
  • visual/edittext/edittext_gutter: Edges are differently defined
  • visual/edittext/edittext_underline_scale2: Edges are differently defined

Broken:

  • from_shumway/acid/acid-small: Wheels are different width than they should be, subjectively looks better visually though, marked as broken

@Lord-McSweeney Lord-McSweeney added A-rendering Area: Rendering & Graphics T-fix Type: Bug fix (in something that's supposed to work already) labels Feb 12, 2026
[image_comparisons."output.03"]
trigger = 3
tolerance = 128
max_outliers = 3
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to update edittext tests? They don't use strokes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I updated the edittext tests since they were failing on my local machine, but we might not have to edit them at all.

[image_comparisons.output]
tolerance = 15
max_outliers = 24
max_outliers = 62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried that we're getting farther away from FP after this PR. Have you investigated why?

Maybe the output is not from FP and we could fix up those tests before this PR.

Copy link
Author

@darktohka darktohka Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a mismatch between software rendering and hardware rendering...
Maybe the tests don't have to be edited in any way (or maybe it's fine to edit them) -

8 tests fail on the PR's CI (https://github.com/ruffle-rs/ruffle/actions/runs/21928846875/job/63327966747).

While the macOS job is 100% successful (the only one with hardware rendering !!!)

On my laptop, 11 tests failed: the 8 tests + the 3 "edittext" tests, you did point out that it is unnecessary to edit those tests.

Today, the same tests, all of them are successful on my laptop (NixOS, Linux, 64-bit AMD Ryzen 5 5600H + RTX 3060).

It's possible that commit dd53f0b (before the tests were edited) might run the tests 100% successfully with hardware rendering enabled:
cargo nextest run --profile ci --cargo-profile ci --workspace --locked --no-fail-fast -j 4 avm1 visual from_gnash from_shumway --features imgtests

Any help appreciated.

@kjarosh
Copy link
Member

kjarosh commented Feb 12, 2026

@darktohka Just a general remark about visual tests: the tolerance/max_outliers are set so that tests pass on CI and on devs' machines. You should either:

  1. run all tests on master and make them pass (in a separate PR), or
  2. do not look at your local results, only at CI.

If you make changes and edit tests to pass locally in the same PR, it will result in an unmergeable mess. If there are any changes to tests, we want them to be well documented, and well-thought-out.

I'd recommend to stick to the 2nd option for now. It should be relatively easy—set 0 tolerance, push, download image diffs, set appropriate tolerance and outliers based on image diffs.

At the end of the day, if your PR brings us closer to Flash Player, you shouldn't need to increase tolerance/outliers. If you do, it could mean a bad test that didn't have output from Flash Player. I can then take care of those tests and fix them before merging this PR.

TL;DR: my recommendation is to revert changes to tolerance/outliers in tests and see what happens.

@darktohka
Copy link
Author

Good point.

I will remove the change to the tests from this PR and keep only the functional changes, let's see what happens.

@darktohka darktohka force-pushed the bugfix/hairline-strokes branch from 6d8a2af to dd53f0b Compare February 12, 2026 23:00
@kjarosh
Copy link
Member

kjarosh commented Feb 12, 2026

I think those failures are caused by the fact that we are using Ruffle's and not FP's output. I'll take a look in my free time and I'll try fixing them up.

@danielhjacobs
Copy link
Contributor

danielhjacobs commented Feb 13, 2026

Worth noting that something similar seems to be true for #22961. That PR caused a bunch of changes to images because lyon changed a little bit about its rendering methods and most of those tests were from Ruffle, not FP. I fixed that up by downloading the images from CI, but ideally we'd replace those tests with FP images and then see whether the lyon update improves their consistency with Flash.

@kjarosh
Copy link
Member

kjarosh commented Feb 17, 2026

Made 2 PRs which fix tests failing here:

Hopefully after merging them and rebasing this PR, they should stop failing, and they should even improve a bit (but don't worry about that, we can lower tolerance later).

After those, there are few known failures failing, I'll try looking into them, but I think we're just closer to Flash Player and that's why they are failing.

@kjarosh
Copy link
Member

kjarosh commented Feb 18, 2026

@darktohka Can you rebase the PR on top of main? The majority of tests should stop failing.

@darktohka darktohka force-pushed the bugfix/hairline-strokes branch from dd53f0b to fee6a8e Compare February 18, 2026 21:21
@kjarosh
Copy link
Member

kjarosh commented Feb 18, 2026

Only one non-known-failure test is failing: from_shumway/acid/acid-clip. Looks like it could be a regression? After this PR we're farther away from Flash Player.

@darktohka
Copy link
Author

Testing from_shumway/acid/acid-clip, we get the following results:

Scenario Branch Test Target Outliers Max Difference
Tolerance = 125 Master from_shumway/acid/acid-clip 6598 144
Tolerance = 125 Current PR from_shumway/acid/acid-clip 6589 144
Tolerance = 64 Master from_shumway/acid/acid-clip 120 144
Tolerance = 64 Current PR from_shumway/acid/acid-clip 135 144

So the CI fails because it reached the threshold of 125 outliers when tolerance = 64.

Here are the image differences:

Master branch
output difference-color-linux-Vulkan-master-branch

Current PR

output difference-color-linux-Vulkan-this-pr

The other tests are improved and bring us closer to FP when you compare the current output with the Flash output (they fail since we're getting further from the master branch Ruffle which is currently incorrect)

I don't think we're going to get any closer (scientifically, 1-to-1) than this with adjusting the tessellation-based backend, since it's going to be lossy either way. It's very dependent on the tolerance and how the triangles are calculated. It's fundamentally rendering differently than Flash Player did.

However, I believe the impact we can make with actual content and games is great. So much content is improved with this change.

I have another PR in the works that will implement LineScaleMode for the WGPU/WebGL backends, but that doesn't get any closer 1-to-1 either:

Screenshot From 2026-02-19 01-32-49

@ncuxonaT
Copy link

ncuxonaT commented Mar 5, 2026

This should fix the missing ninja body and barely visible outlines of coins and mines in the N, right?
Ruffle:
N_ruffle
FP:
N_flashplayer

swf: N.zip

@darktohka
Copy link
Author

darktohka commented Mar 5, 2026

This should fix the missing ninja body and barely visible outlines of coins and mines in the N, right?

swf: N.zip

It fixes the barely visible outlines and improves on the mines, but does not improve the player character:

image

Strangely enough, JPEXS doesn't like the player character either:
image

@yangyangdaji
Copy link

yangyangdaji commented Mar 15, 2026

This PR Supersedes and Close #9981 ?

Would you test this #9981 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-rendering Area: Rendering & Graphics newsworthy T-fix Type: Bug fix (in something that's supposed to work already)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants