From 540ee9fbc5c9c0e8ed8293c5706036dc4d5320aa Mon Sep 17 00:00:00 2001 From: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:30:58 +0000 Subject: [PATCH] Motion Tile: mirror folds sample the edge texel, not the transparent outside MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tile boundary that lands exactly on an output pixel centre puts the tile fraction at exactly 0 or 1, and the sample position at the source's geometric edge — where sample_bilinear mixes in half a texel of the transparent outside. The fold column comes out half-faded: a visible seam line along the mirrored boundary, which sweeps the tile as Tile Center animates across pixel centres (#2820). Both the CPU path and the GPU kernel switch to the clamped sampler, which repeats the edge texel there; interior samples are untouched, and the default single-tile identity mapping already sampled texel centres. The regression test puts the folds exactly on pixel centres (tileWidth 50%, centre on the layer, mirror on) and asserts the fold columns stay opaque and equal the edge texels they mirror — it fails on the unfixed code at alpha 0.5. Fixes #2820 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- crates/effects/src/stylize2.rs | 37 +++++++++++++++++++++++++- crates/gpu/src/shaders/fx_stylize.wgsl | 4 ++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/effects/src/stylize2.rs b/crates/effects/src/stylize2.rs index e0354ea75..d1020b1bc 100644 --- a/crates/effects/src/stylize2.rs +++ b/crates/effects/src/stylize2.rs @@ -457,7 +457,11 @@ fn motion_tile(ctx: &EffectCtx, b: Buf) -> Buf { fy = 1.0 - fy; } } - *px = src.sample_bilinear(fx * lw * s + b.offset[0], fy * lh * s + b.offset[1]); + // Clamped: a tile edge lands exactly on a pixel centre whenever the boundary does + // (#2820's animated Tile Center walks it across the grid), and the plain sampler + // would mix in the transparent outside at the source's geometric edge, leaving a + // half-faded seam pixel along the fold. + *px = src.sample_bilinear_clamped(fx * lw * s + b.offset[0], fy * lh * s + b.offset[1]); } }); Buf { img: out, offset: noff, scale: s } @@ -808,6 +812,37 @@ mod tests { } } + #[test] + fn motion_tile_mirror_folds_stay_opaque() { + // A fold that lands exactly on a pixel centre samples the source's geometric edge, where + // the plain bilinear tap reaches half a texel into the transparent outside — a faint + // seam line on the mirrored boundary that sweeps the tile while Tile Center animates + // (#2820). The fold must repeat the edge texel instead: opaque, and exactly the pixel + // it mirrors. + let img = ramp(10, 8); + let out = run_at( + "ec.stylize.motiontile", + &[("tileCenter", Value::Vec2([5.0, 4.0])), ("tileWidth", num(50.0)), ("mirrorEdges", Value::Bool(true))], + img.clone(), + 0.0, + ); + assert_eq!((out.img.width, out.img.height), (10, 8)); + for (x, mirrored) in [(2u32, 0u32), (7, 9)] { + for y in 0..out.img.height { + let a = out.img.get(x, y); + assert!((a[3] - 1.0).abs() < 1e-5, "fold x={x},y={y} faded to alpha {}", a[3]); + let b = img.get(mirrored, y); + for c in 0..4 { + assert!((a[c] - b[c]).abs() < 1e-5, "fold x={x},y={y} channel {c}"); + } + } + } + // And no pixel anywhere in the tile faded either. + for px in out.img.data.iter() { + assert!((px[3] - 1.0).abs() < 1e-5, "tile pixel faded to alpha {}", px[3]); + } + } + #[test] fn strobe_by_time() { assert!(strobe_on(0.02, 0.05, 1.0, 0.0, 1)); diff --git a/crates/gpu/src/shaders/fx_stylize.wgsl b/crates/gpu/src/shaders/fx_stylize.wgsl index 97db06c52..7fe42df7a 100644 --- a/crates/gpu/src/shaders/fx_stylize.wgsl +++ b/crates/gpu/src/shaders/fx_stylize.wgsl @@ -611,7 +611,9 @@ fn fxs_motiontile(@builtin(global_invocation_id) gid: vec3) { fy = 1.0 - fy; } } - textureStore(out, p, sample_bilinear(src, fx * P.f[0].y + P.f[1].x, fy * P.f[0].z + P.f[1].y)); + // Clamped for the same reason as the CPU path: a fold on a pixel centre samples the + // source's geometric edge, where the plain sampler mixes in the transparent outside. + textureStore(out, p, sample_bilinear_clamped(src, fx * P.f[0].y + P.f[1].x, fy * P.f[0].z + P.f[1].y)); } // CC RepeTile (stylize2::repetile) into a new output size. data: per output column (tile index,