Skip to content

core/render: Implement BitmapData.generateFilterRect - #24136

Open
falpi wants to merge 1 commit into
ruffle-rs:masterfrom
falpi:fix-bitmapdata-filter-expansion
Open

core/render: Implement BitmapData.generateFilterRect#24136
falpi wants to merge 1 commit into
ruffle-rs:masterfrom
falpi:fix-bitmapdata-filter-expansion

Conversation

@falpi

@falpi falpi commented Jul 6, 2026

Copy link
Copy Markdown

What

Implements BitmapData.generateFilterRect (previously a stub returning
sourceRect unchanged, for both AVM1 and AVM2) and makes the wgpu
applyFilter write the filter's expanded output — the pixels a filter
produces beyond the source rect (blur margins, drop shadow distance
offset), which were previously discarded because the filter was rendered
into a texture the size of the source rect.

Why

The two halves fix a silent rendering gap that affects, among others,
every default Flex tooltip. Flex's mx.graphics.RectangularDropShadow
(used by the halo ToolTipBorder skin, also under the Flex 4 Spark
theme) pre-renders its shadow like this:

  1. draws a small round rect onto a BitmapData;
  2. calls generateFilterRect to learn how far a DropShadowFilter
    expands it, deriving the shadow thickness on each edge;
  3. calls applyFilter and slices the result into corner/edge bitmaps,
    which it then tiles with beginBitmapFill.

With generateFilterRect returning the source rect unchanged, every
thickness came out as 0, no slices were created, and drawShadow()
drew nothing — Flex tooltips lost their drop shadow (and the rounded
bottom edge, which is the shadow). The same mechanism is used by the
mx preloader progress bars.

The two changes must ship together: with only generateFilterRect
fixed, the perimeter slices would read the destination bitmap's initial
fill (opaque white by default) instead of shadow pixels, which would
look worse than the current behavior.

How

  • Filter::calculate_dest_margins (render) — computes how far a
    filter's output extends beyond a source of a given pixel size,
    rounding outwards to whole pixels. Single source of truth so that
    generateFilterRect (what we report) and applyFilter (what we
    write) always agree.
  • generateFilterRect implemented natively for AVM2 and AVM1 on top of
    it. The AVM2 ShaderFilter special case (Flash reports the whole
    bitmap) is preserved, moved from ActionScript into the native method.
  • wgpu apply_filter copies the source region into a zero-padded
    intermediate texture, runs the filter over the whole padded area, and
    shifts the destination point back by the margins. Padding is skipped
    when the margins are empty or the padded texture would exceed
    max_texture_dimension_2d.

Notes

  • Margins derive from Filter::calculate_dest_rect, whose blur
    expansion is intentionally approximate vs. Flash (see PASS_SCALES),
    so the reported rect may be slightly larger than Flash Player's.
    Internal consistency between the two APIs — which is what
    RectangularDropShadow-style code depends on — is guaranteed.
  • A filter applied to a 0×0 source rect is still a noop (pre-existing
    FIXME, comment updated).

Checklist

  • I, a human, have self-reviewed this PR and fully understand the changes within.
  • I have made or updated tests where possible.
  • All of my commits are properly scoped, compile successfully, and pass all tests.
  • This PR does not make sense to split up into smaller PRs.
  • An LLM was involved in the authoring of this code.

Comment thread docs/fix-bitmapdata-filter-expansion.html Outdated
@torokati44

Copy link
Copy Markdown
Member

Referencing (mostly for the test, as it's ancient): #12417

@torokati44

Copy link
Copy Markdown
Member
  • All of my commits are properly scoped, compile successfully, and pass all tests.

The last part doesn't look like it holds...

@falpi

falpi commented Jul 6, 2026

Copy link
Copy Markdown
Author
  • All of my commits are properly scoped, compile successfully, and pass all tests.

The last part doesn't look like it holds...

My oversight. I’ve already integrated the checks into my CI/CD scripts, but I forgot the imgtests. I’ll check and fix it.

@kjarosh

kjarosh commented Jul 6, 2026

Copy link
Copy Markdown
Member

and fully understand the changes within

Can you describe, using your own words, what BitmapData.generateFilterRect does and why it's needed at all?

@falpi

falpi commented Jul 6, 2026

Copy link
Copy Markdown
Author

and fully understand the changes within

Can you describe, using your own words, what BitmapData.generateFilterRect does and why it's needed at all?

As I explained in the HTML document included in the branch "Every default Flex tooltip renders in Ruffle without its drop shadow, and with a squarish bottom edge instead of the rounded one Flash Player shows. The shadow is not a display-object filter: Flex pre-renders it through BitmapData.generateFilterRect + applyFilter and copies it in slices. generateFilterRect was a stub in Ruffle, so the slice thicknesses came out as zero and the shadow silently vanished."

Below, I am attaching an example of how default tooltips are rendered in Flex: the top box shows Ruffle (pre-PR), while the bottom one shows FP.

immagine

I would like to point out that every time I submit a PR, I am subjected to an interrogation—as if I had committed murder. To date, not a single one of my PRs has been accepted, even though I have always provided the requested explanations. It is not a problem for me: I am implementing fixes to get a real enterprise application running on Ruffle, and thanks to the LLM, I have been able to do so very quickly. Submitting PRs seemed like a courtesy, but if it is unwelcome—presumably due to the use of the LLM—just say so, and I will stop wasting time responding and submitting them.

@falpi
falpi force-pushed the fix-bitmapdata-filter-expansion branch from 40a82ff to ed85a7b Compare July 6, 2026 18:51
@torokati44

torokati44 commented Jul 6, 2026

Copy link
Copy Markdown
Member

The problem is not at all the use of LLMs, it's purely about the result. And this is how we can keep the quality up, by not just trusting anything an LLM spits out, but actually understanding whether it's any good. Even if that is becoming increasingly controversial...
And keep in mind, there aren't many maintainers, and they don't have all the time in the world. Many times, even perfectly good PRs made by regular contributors (or maintainers) spend a long time in review limbo because of this.

@falpi

falpi commented Jul 6, 2026

Copy link
Copy Markdown
Author

The problem is not at all the use of LLMs, it's purely about the result. And this is how we can keep the quality up, by not just trusting anything an LLM spits out, but actually understanding whether it's any good. Even if that is becoming increasingly controversial...
And keep in mind, there aren't many maintainers, and they don't have all the time in the world. Many times, even perfectly good PRs made by regular contributors (or maintainers) spend a long time in review limbo because of this.

Okay, I understand. The reason I always try to document everything well is to make your life at least a little easier, because I realize it's difficult to keep up with the current PR pace following the LLM boom. Regards

@falpi
falpi force-pushed the fix-bitmapdata-filter-expansion branch 2 times, most recently from 989ae87 to 4538cfa Compare July 7, 2026 21:00
generateFilterRect was a stub that returned sourceRect unchanged, and
the wgpu applyFilter rendered filters into a texture the size of the
source rect, discarding the pixels a filter produces beyond it (blur
margins, drop shadow distance offset). Together these two gaps hide
every default Flex tooltip's drop shadow and rounded bottom edge:
mx.graphics.RectangularDropShadow relies on the difference between
generateFilterRect's report and applyFilter's writes to slice its
tile-able shadow bitmaps.

Add Filter::calculate_dest_margins as the single source of truth for
the per-side write reach, with values measured pixel-by-pixel from
Flash Player captures: a blur of strength b at q passes reaches
ceil((b - 1) * q / 2) pixels per side; drop shadow and bevel add the
distance offset on the side(s) they point towards; inner filters do
not expand. Implement generateFilterRect natively on top of it for
both AVM1 and AVM2.

In the wgpu backend, apply filters over a padded copy of the source,
shifting the destination point back by the margins so the expanded
output is written to the destination, as Flash does. Two more
Flash-matching behaviours the goldens revealed: a blur samples real
source pixels beyond the source rect within the bitmap's physical
bounds (the padded copy for blurs grows by the write ring to feed
the kernel real neighbors), and beyond the bitmap's physical bounds
the padding stays transparent, replacing the old edge-replication
that came from UV clamping. Filters that composite the source over
their output (glow, drop shadow, bevel) get transparent-only padding
— neighbor pixels under those would be painted into the margins.

bitmapdata_filter_sourcerect (a true Flash golden) now passes with
zero outliers; its "FIXME We're way off here compared to FP" 1700-
outlier allowance is removed. The Ruffle-canonical goldens of
bitmapdata_applyfilter_blur, visual/filters/blur_{quality,fractional}
and away3d_advanced_shallow_water_demo are regenerated: they encoded
the old no-expansion / edge-replication behaviour. destpoint_edges
stays known_failure due to its unsupported ConvolutionFilter; its
Ruffle-expected image is regenerated and its tolerance is raised
from 1 to 2 to accommodate the ~2/channel variance between GPUs
that gradual blur fades reveal (the two CI runners themselves differ
by 237 pixels at tolerance 1; every measured delta ≤2).
@falpi
falpi force-pushed the fix-bitmapdata-filter-expansion branch from 4538cfa to f8805d7 Compare July 7, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants