Skip to content

fix(background): rasterize the heropattern tile at its target scale - #282

Closed
LeadcodeDev wants to merge 1 commit into
fix/heropattern-scroll-periodfrom
fix/heropattern-rasterize-scale
Closed

LeadcodeDev wants to merge 1 commit into
fix/heropattern-scroll-periodfrom
fix/heropattern-rasterize-scale

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity Medium, category quality. Location: crates/rustmotion/src/engine/render/background.rs:605

Impact

tile_w = def.width * cfg.scale is computed at line 581 and used only for the overscan margin; the pixmap at 605-606 is sized from the unscaled def.width/def.height and resvg renders with Transform::default() (identity). The magnification then happens on the raster, via the shader matrix, with SamplingOptions::default() — which I read in skia-safe-0.99.0/src/core/sampling_options.rs:70-80: filter: FilterMode::Nearest, mipmap: MipmapMode::None. So heropattern with scale: 4 on a 1080p frame draws hard 4x4 pixel blocks from a 20x20 source raster, when the source is vector art that resvg could have rendered crisply at any size. The whole point of shipping 87 SVG patterns is lost above scale 1. It is also wasteful below 1: a scale: 0.25 pattern is rasterized at full size and then point-sampled down, aliasing badly.

Fix

Size the pixmap at the scaled tile ((def.width * cfg.scale).ceil() x (def.height * cfg.scale).ceil(), clamped to a sane maximum) and pass tiny_skia::Transform::from_scale(cfg.scale, cfg.scale) to resvg::render, then drop the shader matrix so the tile is used 1:1. At minimum, pass SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear) instead of the nearest-neighbour default.

Evidence the audit read

let pw = def.width.ceil() as u32;
    let ph = def.height.ceil() as u32;
    let Some(mut pixmap) = tiny_skia::Pixmap::new(pw, ph) else {
        return;
    };
    resvg::render(&tree, tiny_skia::Transform::default(), &mut pixmap.as_mut());
...
    let matrix = if cfg.scale != 1.0 {
        Some(skia_safe::Matrix::scale((cfg.scale, cfg.scale)))
    } else { None };
    let Some(shader) = tile_image.to_shader(
        (skia_safe::TileMode::Repeat, skia_safe::TileMode::Repeat),
        skia_safe::SamplingOptions::default(), matrix.as_ref(),

Stacked on fix/heropattern-scroll-period, which carries the previous finding of this workstream. GitHub shows only this finding's diff; merge in order.

Part of the September 2026 audit remediation chantier. Refs #220 (RM-22).

tile_w = def.width * cfg.scale is computed at line 581 and used only for the
overscan margin; the pixmap at 605-606 is sized from the unscaled
def.width/def.height and resvg renders with Transform::default() (identity).
The magnification then happens on the raster, via the shader matrix, with
SamplingOptions::default() — which I read in skia-
safe-0.99.0/src/core/sampling_options.rs:70-80: filter: FilterMode::Nearest,
mipmap: MipmapMode::None. So heropattern with scale: 4 on a 1080p frame
draws hard 4x4 pixel blocks from a 20x20 source raster, when the source is
vector art that resvg could have rendered crisply at any size. The whole
point of shipping 87 SVG patterns is lost above scale 1. It is also wasteful
below 1: a scale: 0.25 pattern is rasterized at full size and then point-
sampled down, aliasing badly.

Fix: Size the pixmap at the scaled tile ((def.width * cfg.scale).ceil() x
(def.height * cfg.scale).ceil(), clamped to a sane maximum) and pass
tiny_skia::Transform::from_scale(cfg.scale, cfg.scale) to resvg::render,
then drop the shader matrix so the tile is used 1:1. At minimum, pass
SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear) instead of the
nearest-neighbour default.

Refs #220
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 22, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 22, 2026
@LeadcodeDev
LeadcodeDev deleted the branch fix/heropattern-scroll-period September 22, 2026 08:53
@LeadcodeDev
LeadcodeDev deleted the fix/heropattern-rasterize-scale branch September 29, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant