Skip to content

fix(background): wrap heropattern scroll on its per-axis period - #248

Merged
LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/heropattern-scroll-period
Sep 22, 2026
Merged

LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/heropattern-scroll-period

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity Medium, category correctness. Location: crates/rustmotion/src/engine/render/background.rs:806

Impact

tile_spacing returns one scalar that line 830 applies to both axes, and the Heropattern arm never reads d.height. The function's own doc comment states the invariant it relies on — "since every tiled pattern is exactly periodic on spacing, translating by any offset congruent mod spacing produces byte-identical pixels" — which is false for any non-square pattern. I counted the table programmatically: 41 of the 87 entries in crates/rustmotion-core/src/engine/heropatterns.rs have width != height (architect 100x199, aztec 32x64, bamboo 16x32, bank_note 100x20, death_star 80x105, yyy 60x96...). Concrete case: {"animated-background":{"preset":"heropattern","heropattern":{"pattern":"aztec"},"direction":"down","speed":60}}. The shader tiles vertically on 64px but the offset wraps at 32px, so every 32px of travel the pattern snaps by half a period — a periodic jolt through the whole scene. The .max(20.0) clamp breaks it a second way for the 8 patterns narrower than 20px: bamboo (16 wide) wraps at 20, so even horizontal scroll jumps 4px per wrap. draw_world_bg_with_parallax reuses the same tile_spacing for the camera-pan modulo (line 89), so world views inherit it.

Fix

Make tile_spacing return a (f32, f32) period and have the Heropattern arm emit (d.width * cfg.scale, d.height * cfg.scale); wrap raw_x on the x period and raw_y on the y period at line 830, and do the same for the camera modulo at line 89. Drop the .max(20.0) floor or replace it with a multiple-of-period ceiling (period * (20.0 / period).ceil()) so the clamp preserves periodicity.

Evidence the audit read

BackgroundPreset::Heropattern(cfg) => {
            let def = crate::engine::heropatterns::find_pattern(&cfg.pattern);
            def.map(|d| d.width * cfg.scale).unwrap_or(60.0).max(20.0)
        }
...
pub(super) fn compute_scroll_offset(bg: &AnimatedBackground, time: f32) -> (f32, f32) {
    let (raw_x, raw_y) = raw_scroll_offset(bg, time);
    let spacing = tile_spacing(&bg.preset);
    (raw_x % spacing, raw_y % spacing)

Based directly on the chantier branch.

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

@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 21, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 21, 2026
@LeadcodeDev
LeadcodeDev force-pushed the fix/heropattern-scroll-period branch 2 times, most recently from f067906 to 67be435 Compare September 22, 2026 08:35
tile_spacing returns one scalar that line 830 applies to both axes, and the
Heropattern arm never reads d.height. The function's own doc comment states
the invariant it relies on — "since every tiled pattern is exactly periodic
on spacing, translating by any offset congruent mod spacing produces byte-
identical pixels" — which is false for any non-square pattern. I counted the
table programmatically: 41 of the 87 entries in crates/rustmotion-
core/src/engine/heropatterns.rs have width != height (architect 100x199,
aztec 32x64, bamboo 16x32, bank_note 100x20, death_star 80x105, yyy
60x96...). Concrete case: {"animated-background":{"preset":"heropattern","he
ropattern":{"pattern":"aztec"},"direction":"down","speed":60}}. The shader
tiles vertically on 64px but the offset wraps at 32px, so every 32px of
travel the pattern snaps by half a period — a periodic jolt through the
whole scene. The .max(20.0) clamp breaks it a second way for the 8 patterns
narrower than 20px: bamboo (16 wide) wraps at 20, so even horizontal scroll
jumps 4px per wrap. draw_world_bg_with_parallax reuses the same tile_spacing
for the camera-pan modulo (line 89), so world views inherit it.

Fix: Make tile_spacing return a (f32, f32) period and have the Heropattern
arm emit (d.width * cfg.scale, d.height * cfg.scale); wrap raw_x on the x
period and raw_y on the y period at line 830, and do the same for the camera
modulo at line 89. Drop the .max(20.0) floor or replace it with a multiple-
of-period ceiling (period * (20.0 / period).ceil()) so the clamp preserves
periodicity.

Refs #220
@LeadcodeDev
LeadcodeDev force-pushed the fix/heropattern-scroll-period branch from 67be435 to 5a7f871 Compare September 22, 2026 08:44
@LeadcodeDev
LeadcodeDev merged commit 9d4bcb5 into chantier/audit-2026-09 Sep 22, 2026
2 of 3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/heropattern-scroll-period branch September 22, 2026 08:53
LeadcodeDev added a commit that referenced this pull request Sep 22, 2026
tile_spacing returns one scalar that line 830 applies to both axes, and the
Heropattern arm never reads d.height. The function's own doc comment states
the invariant it relies on — "since every tiled pattern is exactly periodic
on spacing, translating by any offset congruent mod spacing produces byte-
identical pixels" — which is false for any non-square pattern. I counted the
table programmatically: 41 of the 87 entries in crates/rustmotion-
core/src/engine/heropatterns.rs have width != height (architect 100x199,
aztec 32x64, bamboo 16x32, bank_note 100x20, death_star 80x105, yyy
60x96...). Concrete case: {"animated-background":{"preset":"heropattern","he
ropattern":{"pattern":"aztec"},"direction":"down","speed":60}}. The shader
tiles vertically on 64px but the offset wraps at 32px, so every 32px of
travel the pattern snaps by half a period — a periodic jolt through the
whole scene. The .max(20.0) clamp breaks it a second way for the 8 patterns
narrower than 20px: bamboo (16 wide) wraps at 20, so even horizontal scroll
jumps 4px per wrap. draw_world_bg_with_parallax reuses the same tile_spacing
for the camera-pan modulo (line 89), so world views inherit it.

Fix: Make tile_spacing return a (f32, f32) period and have the Heropattern
arm emit (d.width * cfg.scale, d.height * cfg.scale); wrap raw_x on the x
period and raw_y on the y period at line 830, and do the same for the camera
modulo at line 89. Drop the .max(20.0) floor or replace it with a multiple-
of-period ceiling (period * (20.0 / period).ceil()) so the clamp preserves
periodicity.

Refs #220
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