From 36b461122169808775e90a66ea8d2b8da696b5ba Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 11 May 2026 20:34:10 +0300 Subject: [PATCH 1/3] hellocodenameone: screenshot test for clip under scale+translate (#4907) Repro for issue #4907 (glitchy metal drawing). The issue reports that a magnifier-style render path -- set a small clip rect, then apply g.scale(scale, scale) + g.translate(-ax, -ay) and draw a much larger surface -- correctly clips on the legacy iOS pipeline / JavaSE / Android but leaks outside the clip rect under iOS Metal. The new ClipUnderScaleTranslate test runs the AbstractGraphicsScreenshotTest 2x2 (AA off/on, form-graphics vs mutable-image) and renders the exact sequence from ddyer0's snippet: g.pushClip(); g.clipRect(centred small rect); g.scale(2.0f, 2.0f); g.translate(-ax, -ay); g.fillRect(); // should clip to rect g.translate(ax, ay); g.scale(0.5f, 0.5f); g.popClip(); A navy outline of the expected clip rect is drawn before the clipped fill so the screenshot diff has a stable anchor; a tiny green sentinel is drawn after popClip so a broken pop/transform-reset is also caught. On a working pipeline the cell shows a centred red square inside the navy outline; on the buggy pipeline the red leaks outside (worst case: the entire cell is red). Splitting the form-graphics path from the mutable-image path lets the diff narrow the suspect target without extra instrumentation. Wired into Cn1ssDeviceRunner after Clip and into the HTML5 skip list to match the other graphics screenshot tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/Cn1ssDeviceRunner.java | 3 + .../graphics/ClipUnderScaleTranslate.java | 91 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java index a62561c108..401057ba6a 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/Cn1ssDeviceRunner.java @@ -9,6 +9,7 @@ import com.codenameone.examples.hellocodenameone.NativeInterfaceLanguageValidator; import com.codenameone.examples.hellocodenameone.tests.graphics.AffineScale; import com.codenameone.examples.hellocodenameone.tests.graphics.Clip; +import com.codenameone.examples.hellocodenameone.tests.graphics.ClipUnderScaleTranslate; import com.codenameone.examples.hellocodenameone.tests.graphics.DrawArc; import com.codenameone.examples.hellocodenameone.tests.graphics.DrawGradient; import com.codenameone.examples.hellocodenameone.tests.graphics.DrawImage; @@ -128,6 +129,7 @@ private static int testTimeoutMs() { new FillShape(), new StrokeTest(), new Clip(), + new ClipUnderScaleTranslate(), new TileImage(), new Rotate(), new TransformTranslation(), @@ -361,6 +363,7 @@ private static boolean isJsSkippedScreenshotTest(String testName) { // graphics tests || "AffineScale".equals(testName) || "Clip".equals(testName) + || "ClipUnderScaleTranslate".equals(testName) || "DrawArc".equals(testName) || "DrawGradient".equals(testName) || "DrawImage".equals(testName) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java new file mode 100644 index 0000000000..ed1dd35fd5 --- /dev/null +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java @@ -0,0 +1,91 @@ +package com.codenameone.examples.hellocodenameone.tests.graphics; + +import com.codename1.ui.Graphics; +import com.codename1.ui.geom.Rectangle; +import com.codenameone.examples.hellocodenameone.tests.AbstractGraphicsScreenshotTest; + +// Repro for issue #4907 (glitchy metal drawing): a magnifier-style render path +// sets a small clip rect, then applies g.scale() + g.translate() to draw a +// scaled and panned copy of a much larger surface into that clip. Under the +// pre-metal iOS pipeline (and on JavaSE/Android) the large fillRect emitted +// after the transform is correctly clipped to the inner rect; under metal the +// clip is reportedly not respected and the red fill leaks outside the inner +// rect. +// +// Each cell is gray with a black frame and a small centred inner rect that is +// the only region the red fill should reach. If the clip is honoured the cell +// shows a gray cell with a centred red square; if the clip is dropped the +// entire cell turns red (or shows red bleed outside the centred square). +public class ClipUnderScaleTranslate extends AbstractGraphicsScreenshotTest { + + @Override + protected void drawContent(Graphics g, Rectangle bounds) { + int x = bounds.getX(); + int y = bounds.getY(); + int w = bounds.getWidth(); + int h = bounds.getHeight(); + + // Cell background + frame so a fully-red cell (clip dropped) is + // visually unambiguous and a centred red square (clip honoured) has a + // reference frame to compare against. + g.setColor(0xcccccc); + g.fillRect(x, y, w, h); + g.setColor(0x000000); + g.drawRect(x, y, w - 1, h - 1); + + // Centred inner rect roughly 1/3 of the cell in each dimension -- + // small enough that a leak outside it is obvious, large enough to + // dominate the cell when correctly filled. + int clipW = Math.max(8, w / 3); + int clipH = Math.max(8, h / 3); + int clipX = x + (w - clipW) / 2; + int clipY = y + (h - clipH) / 2; + + // Reference outline of the expected clip region. Drawn before the + // clip + transform so it survives regardless of how the clipped fill + // behaves, giving the diff a stable anchor. + g.setColor(0x000080); + g.drawRect(clipX, clipY, clipW - 1, clipH - 1); + + g.pushClip(); + g.clipRect(clipX, clipY, clipW, clipH); + + // Mirror the magnifier code in issue #4907: + // gc.scale(scale, scale); + // gc.translate(-ax, -ay); + // + // gc.translate(ax, ay); + // gc.scale(1/scale, 1/scale); + // The clip was set in pre-transform coordinates; after scale+translate + // a fillRect over the full untransformed cell should still land only + // inside the original clipRect. + float scale = 2.0f; + int ax = clipX + clipW / 2; + int ay = clipY + clipH / 2; + g.scale(scale, scale); + g.translate(-ax, -ay); + + // Massive fill: cover the whole cell and then some, in the + // post-transform coordinate space. If the clip was honoured this + // paints only the inner clip rect red; if it was dropped this paints + // the entire cell red. + g.setColor(0xff0000); + g.fillRect(x - w, y - h, w * 4, h * 4); + + g.translate(ax, ay); + g.scale(1f / scale, 1f / scale); + + g.popClip(); + + // Sentinel outside the clip rect drawn after popClip with the + // identity transform. Should always render -- if it doesn't, the + // pop/transform-reset path is also broken. + g.setColor(0x008000); + g.fillRect(x + 2, y + 2, 6, 6); + } + + @Override + protected String screenshotName() { + return "graphics-clip-under-scale-translate"; + } +} From 784ccb147385beb6a3df7d6caf509aaa5696ed99 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 11 May 2026 20:52:05 +0300 Subject: [PATCH 2/3] ClipUnderScaleTranslate: 5-patch pattern for clearer bleed detection (#4907) The previous version drew a solid red fillRect covering the entire post-transform area, with a navy outline of the expected clip rect. When the clip is honoured the red square sits exactly on the navy outline, so a small bleed at the edges is hard to distinguish from normal antialiasing -- on the JavaSE simulator the result looked correct but it was hard to tell whether the test would catch a subtle leak. Switch to a 5-patch pattern sized in source (pre-scale) coordinates: - Central red patch sized 1/scale * clipW by 1/scale * clipH so that, under the 2x scale, it exactly fills the clip rect. - Four side patches (green N, blue S, orange W, magenta E) placed one patch-width / height away from the centre. With the 2x scale they end up one full clipW / clipH off-centre, entirely outside the clip rect under a correct clip and invisible. A correct render shows only a red square inside a black clip-rect outline (drawn last, on top of the fill so its position is stable). A bleed shows a coloured patch outside the outline, and the colour identifies which post-transform direction leaked. The clip rect is now 1/5 of the cell (was 1/3) so there is plenty of gray buffer around it to make even a thin bleed visible. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../graphics/ClipUnderScaleTranslate.java | 104 ++++++++++++------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java index ed1dd35fd5..9954a6744f 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java @@ -7,15 +7,26 @@ // Repro for issue #4907 (glitchy metal drawing): a magnifier-style render path // sets a small clip rect, then applies g.scale() + g.translate() to draw a // scaled and panned copy of a much larger surface into that clip. Under the -// pre-metal iOS pipeline (and on JavaSE/Android) the large fillRect emitted -// after the transform is correctly clipped to the inner rect; under metal the -// clip is reportedly not respected and the red fill leaks outside the inner -// rect. +// pre-metal iOS pipeline (and on JavaSE/Android) drawing emitted after the +// transform is correctly clipped to the inner rect; under metal the clip is +// reportedly not respected and content leaks outside the inner rect. // -// Each cell is gray with a black frame and a small centred inner rect that is -// the only region the red fill should reach. If the clip is honoured the cell -// shows a gray cell with a centred red square; if the clip is dropped the -// entire cell turns red (or shows red bleed outside the centred square). +// The cell is light gray with a small centred clip rect (1/5 of the cell so +// there is plenty of "outside" area to make bleed obvious). Inside the +// transform we paint a recognisable 5-patch pattern: a central red patch +// (sized so that, under the 2x magnification, it exactly fills the clip rect +// in screen space) plus four surrounding patches in distinct colours +// (green / blue / orange / magenta). The surrounding patches sit far enough +// from the magnifier centre that, when the clip is honoured, they are +// entirely outside the clip rect and invisible -- so the cell shows only a +// red square inside the black clip-rect outline drawn last on top. +// +// If the clip is dropped or partially dropped on iOS Metal, one or more of +// the coloured side patches becomes visible OUTSIDE the black outline. The +// colour of the leaking patch identifies which post-transform direction +// leaked. A full clip drop paints most of the cell red plus visible side +// patches; a sub-pixel bleed shows a thin coloured ring just outside the +// black outline. public class ClipUnderScaleTranslate extends AbstractGraphicsScreenshotTest { @Override @@ -25,28 +36,17 @@ protected void drawContent(Graphics g, Rectangle bounds) { int w = bounds.getWidth(); int h = bounds.getHeight(); - // Cell background + frame so a fully-red cell (clip dropped) is - // visually unambiguous and a centred red square (clip honoured) has a - // reference frame to compare against. - g.setColor(0xcccccc); + // Light-gray cell so any leaked patch colour is unmistakable. + g.setColor(0xeeeeee); g.fillRect(x, y, w, h); - g.setColor(0x000000); - g.drawRect(x, y, w - 1, h - 1); - // Centred inner rect roughly 1/3 of the cell in each dimension -- - // small enough that a leak outside it is obvious, large enough to - // dominate the cell when correctly filled. - int clipW = Math.max(8, w / 3); - int clipH = Math.max(8, h / 3); + // Small centred clip rect -- 1/5 of the cell in each dimension. Plenty + // of gray buffer around it so even a thin bleed is visible. + int clipW = Math.max(16, w / 5); + int clipH = Math.max(16, h / 5); int clipX = x + (w - clipW) / 2; int clipY = y + (h - clipH) / 2; - // Reference outline of the expected clip region. Drawn before the - // clip + transform so it survives regardless of how the clipped fill - // behaves, giving the diff a stable anchor. - g.setColor(0x000080); - g.drawRect(clipX, clipY, clipW - 1, clipH - 1); - g.pushClip(); g.clipRect(clipX, clipY, clipW, clipH); @@ -56,30 +56,62 @@ protected void drawContent(Graphics g, Rectangle bounds) { // // gc.translate(ax, ay); // gc.scale(1/scale, 1/scale); - // The clip was set in pre-transform coordinates; after scale+translate - // a fillRect over the full untransformed cell should still land only - // inside the original clipRect. float scale = 2.0f; int ax = clipX + clipW / 2; int ay = clipY + clipH / 2; g.scale(scale, scale); g.translate(-ax, -ay); - // Massive fill: cover the whole cell and then some, in the - // post-transform coordinate space. If the clip was honoured this - // paints only the inner clip rect red; if it was dropped this paints - // the entire cell red. + // 5-patch test pattern, sized in source (pre-scale) coordinates. + // Central patch is clipW/scale by clipH/scale in source space, so it + // exactly fills the clip rect after the 2x scale. The four side + // patches are placed clipW/scale and clipH/scale away from the centre + // -- with the 2x scale that is one full clipW / clipH off centre, so + // under a correct clip the side patches end up entirely outside the + // clip rect and contribute no pixels. If the clip is dropped they + // become visible at known cardinal positions outside the outline, + // identifying the leak direction by colour. + int patchW = clipW / 2; // 1/scale * clipW + int patchH = clipH / 2; // 1/scale * clipH + int halfPW = patchW / 2; + int halfPH = patchH / 2; + int offX = patchW; + int offY = patchH; + + // Centre (red) -- the only patch that should be visible. g.setColor(0xff0000); - g.fillRect(x - w, y - h, w * 4, h * 4); + g.fillRect(ax - halfPW, ay - halfPH, patchW, patchH); + + // North (green) -- one patch-height above centre. + g.setColor(0x00aa00); + g.fillRect(ax - halfPW, ay - halfPH - offY, patchW, patchH); + + // South (blue). + g.setColor(0x0000ff); + g.fillRect(ax - halfPW, ay - halfPH + offY, patchW, patchH); + + // West (orange). + g.setColor(0xff8800); + g.fillRect(ax - halfPW - offX, ay - halfPH, patchW, patchH); + + // East (magenta). + g.setColor(0xff00ff); + g.fillRect(ax - halfPW + offX, ay - halfPH, patchW, patchH); g.translate(ax, ay); g.scale(1f / scale, 1f / scale); g.popClip(); - // Sentinel outside the clip rect drawn after popClip with the - // identity transform. Should always render -- if it doesn't, the - // pop/transform-reset path is also broken. + // Black outline of the expected clip region drawn after popClip on + // top of the rendered fill, so its position is unaffected by any leak + // and the diff has a stable visual anchor. + g.setColor(0x000000); + g.drawRect(clipX, clipY, clipW - 1, clipH - 1); + + // Sentinel outside the clip rect drawn last with the identity + // transform. Should always render -- if it doesn't, the pop / + // transform-reset path is also broken. g.setColor(0x008000); g.fillRect(x + 2, y + 2, 6, 6); } From 74d77b733e23d75b1fc6e20a3afff4ac3c3afde9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 12 May 2026 04:00:30 +0300 Subject: [PATCH 3/3] ClipUnderScaleTranslate: fix transform math so patches land on screen (#4907) The previous version picked ax = clipCenterX and ay = clipCenterY, which made the magnifier-origin the fixed point of the transform rather than the clip-rect centre. After g.scale(s); g.translate(-ax, -ay) a source point p ends up at native screen coord s * (p - (ax, ay)) (Graphics adds xTranslate/yTranslate before the impl applies the scale; every active port has isTranslationSupported() == false), so the old math sent the centre patch and all four side patches to screen positions outside the cell on every port. The CI output for the previous PR showed empty cells on iOS GL and iOS Metal -- no red, no leak signal, just the clip outline and the corner sentinel. Set ax = clipCenterX / scale and ay = clipCenterY / scale instead. With that choice the transform's fixed point is the clip-rect centre, and a source patch centred at (clipCenterX, clipCenterY) of size (clipW / scale, clipH / scale) maps exactly onto the clip rect on screen. The four side patches placed one source-patch-size off-centre land in the cell rows / columns immediately adjacent to the clip rect. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../graphics/ClipUnderScaleTranslate.java | 99 ++++++++++--------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java index 9954a6744f..5b41478abc 100644 --- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java +++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/graphics/ClipUnderScaleTranslate.java @@ -11,22 +11,34 @@ // transform is correctly clipped to the inner rect; under metal the clip is // reportedly not respected and content leaks outside the inner rect. // +// The transform math: +// +// After g.scale(s, s); g.translate(-ax, -ay); +// a source point p ends up at native screen coord s * (p - (ax, ay)). +// (Graphics adds xTranslate/yTranslate to coords before passing them to +// the native impl on every active port -- isTranslationSupported() is +// false everywhere -- and the native impl then applies the scale.) +// +// To make the clip-rect centre a fixed point of the transform, pick +// ax = clipCenterX / s and ay = clipCenterY / s. Then a source patch +// centred at (clipCenterX, clipCenterY) of size (clipW/s, clipH/s) +// maps exactly onto the clip rect on screen. +// // The cell is light gray with a small centred clip rect (1/5 of the cell so // there is plenty of "outside" area to make bleed obvious). Inside the -// transform we paint a recognisable 5-patch pattern: a central red patch -// (sized so that, under the 2x magnification, it exactly fills the clip rect -// in screen space) plus four surrounding patches in distinct colours -// (green / blue / orange / magenta). The surrounding patches sit far enough -// from the magnifier centre that, when the clip is honoured, they are -// entirely outside the clip rect and invisible -- so the cell shows only a -// red square inside the black clip-rect outline drawn last on top. +// transform we paint a 5-patch pattern in source space: a central red patch +// sized so it exactly fills the clip rect on screen, plus four surrounding +// patches in distinct colours (green N, blue S, orange W, magenta E). Each +// side patch is one full source-patch-size off-centre, so under the 2x +// scale they map to the cell rows/columns immediately adjacent to the clip +// rect on screen -- well outside the clip rect, entirely invisible under a +// correct clip. // -// If the clip is dropped or partially dropped on iOS Metal, one or more of -// the coloured side patches becomes visible OUTSIDE the black outline. The -// colour of the leaking patch identifies which post-transform direction -// leaked. A full clip drop paints most of the cell red plus visible side -// patches; a sub-pixel bleed shows a thin coloured ring just outside the -// black outline. +// Correct render: red square inside a black clip-rect outline (drawn last +// on top), light gray everywhere else, small green sentinel dot in the +// corner. A bleed shows one or more coloured patches outside the outline; +// the colour identifies the leak direction. A full clip drop shows all +// four side patches. public class ClipUnderScaleTranslate extends AbstractGraphicsScreenshotTest { @Override @@ -36,76 +48,65 @@ protected void drawContent(Graphics g, Rectangle bounds) { int w = bounds.getWidth(); int h = bounds.getHeight(); - // Light-gray cell so any leaked patch colour is unmistakable. g.setColor(0xeeeeee); g.fillRect(x, y, w, h); - // Small centred clip rect -- 1/5 of the cell in each dimension. Plenty - // of gray buffer around it so even a thin bleed is visible. - int clipW = Math.max(16, w / 5); - int clipH = Math.max(16, h / 5); + // Small centred clip rect -- 1/5 of the cell. + int clipW = Math.max(20, w / 5); + int clipH = Math.max(20, h / 5); int clipX = x + (w - clipW) / 2; int clipY = y + (h - clipH) / 2; + int clipCenterX = clipX + clipW / 2; + int clipCenterY = clipY + clipH / 2; g.pushClip(); g.clipRect(clipX, clipY, clipW, clipH); - // Mirror the magnifier code in issue #4907: - // gc.scale(scale, scale); - // gc.translate(-ax, -ay); - // - // gc.translate(ax, ay); - // gc.scale(1/scale, 1/scale); + // Mirror the magnifier code in issue #4907. float scale = 2.0f; - int ax = clipX + clipW / 2; - int ay = clipY + clipH / 2; + int ax = (int) (clipCenterX / scale); + int ay = (int) (clipCenterY / scale); g.scale(scale, scale); g.translate(-ax, -ay); - // 5-patch test pattern, sized in source (pre-scale) coordinates. - // Central patch is clipW/scale by clipH/scale in source space, so it - // exactly fills the clip rect after the 2x scale. The four side - // patches are placed clipW/scale and clipH/scale away from the centre - // -- with the 2x scale that is one full clipW / clipH off centre, so - // under a correct clip the side patches end up entirely outside the - // clip rect and contribute no pixels. If the clip is dropped they - // become visible at known cardinal positions outside the outline, - // identifying the leak direction by colour. - int patchW = clipW / 2; // 1/scale * clipW - int patchH = clipH / 2; // 1/scale * clipH + // Source-space patch size: clipW/s by clipH/s. Under the 2x scale + // each patch maps to a clipW x clipH rectangle on screen. The + // central patch centred at (clipCenterX, clipCenterY) maps onto + // the clip rect; each side patch maps onto the adjacent cell row / + // column and should be entirely clipped out. + int patchW = (int) (clipW / scale); + int patchH = (int) (clipH / scale); int halfPW = patchW / 2; int halfPH = patchH / 2; - int offX = patchW; - int offY = patchH; - // Centre (red) -- the only patch that should be visible. + // Centre (red) -- fills the clip rect under a correct clip. g.setColor(0xff0000); - g.fillRect(ax - halfPW, ay - halfPH, patchW, patchH); + g.fillRect(clipCenterX - halfPW, clipCenterY - halfPH, patchW, patchH); - // North (green) -- one patch-height above centre. + // North (green). g.setColor(0x00aa00); - g.fillRect(ax - halfPW, ay - halfPH - offY, patchW, patchH); + g.fillRect(clipCenterX - halfPW, clipCenterY - halfPH - patchH, patchW, patchH); // South (blue). g.setColor(0x0000ff); - g.fillRect(ax - halfPW, ay - halfPH + offY, patchW, patchH); + g.fillRect(clipCenterX - halfPW, clipCenterY - halfPH + patchH, patchW, patchH); // West (orange). g.setColor(0xff8800); - g.fillRect(ax - halfPW - offX, ay - halfPH, patchW, patchH); + g.fillRect(clipCenterX - halfPW - patchW, clipCenterY - halfPH, patchW, patchH); // East (magenta). g.setColor(0xff00ff); - g.fillRect(ax - halfPW + offX, ay - halfPH, patchW, patchH); + g.fillRect(clipCenterX - halfPW + patchW, clipCenterY - halfPH, patchW, patchH); g.translate(ax, ay); g.scale(1f / scale, 1f / scale); g.popClip(); - // Black outline of the expected clip region drawn after popClip on - // top of the rendered fill, so its position is unaffected by any leak - // and the diff has a stable visual anchor. + // Black clip-rect outline drawn AFTER popClip on top of the fill so + // its position is unaffected by any leak and the diff has a stable + // visual anchor for spotting bleed. g.setColor(0x000000); g.drawRect(clipX, clipY, clipW - 1, clipH - 1);