From af3d754a277190b122b67990659f698c99292915 Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Wed, 22 Jul 2026 14:06:33 +0200 Subject: [PATCH] [Win32] Extend GC.drawImage() handle selection with exact-zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GC.drawImage() operations use a temporary image handle mechanism that selects the best-fitting native handle for each pixel size. A recent change introduced the concept of "nearest available zoom" so that handles can be reused when consecutive draw calls at different sizes map to the same underlying image data, and eagerly persists handles when the nearest available zoom is 100%. This change extends the handle-selection logic in two ways. Exact imageZoom lookup before nearestAvailableZoom: Before consulting the nearestAvailableZoom, the image's persistent handle manager is now queried at the exact imageZoom first. This ensures that a handle explicitly created for a zoom — even one the provider would not normally supply on its own — is found and reused. For example, when win32_getHandle() has been called at 200% for a 100%-only image, drawing at the 200%-equivalent pixel size now returns the pre-existing 200% handle rather than the 100% handle from nearestAvailableZoom. Monitor-zoom persistence: The condition for eagerly persisting a handle is extended from "nearestAvailableZoom == 100%" to also include any zoom that matches a current monitor. Monitor zooms are obtained from the zoom reported by each open Shell via Display.getShells(). When imageZoom or nearestAvailableZoom matches a monitor zoom, the image is likely being drawn repeatedly at that screen's native resolution, so persisting the handle avoids repeated allocations across consecutive draw calls. As a minor cleanup, getAvailableMonitorZooms() is now called once and its result reused within getExistingHandle() instead of being called twice. Two regression tests are added to ImagesWin32Tests to cover the new behaviors: one verifying that an existing handle at the exact imageZoom is preferred over a nearestAvailableZoom handle, and one verifying that drawing at the monitor zoom creates a persistent handle that is then reused via win32_getHandle() without a second native allocation. See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419 Contributes to https://github.com/eclipse-platform/eclipse.platform.swt/issues/3454 --- .../swt/graphics/ImagesWin32Tests.java | 79 +++++++++++++++++++ .../win32/org/eclipse/swt/graphics/Image.java | 36 ++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java index cf48800de3..33f7e3fac2 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java @@ -134,6 +134,85 @@ public void testHandlesAreDifferentForTwoZoomImageAtDifferentNearestZooms() { } } + /** + * Tests that a GC.drawImage() handle at the exact imageZoom is returned in + * preference to a handle at nearestAvailableZoom when both are present in the + * image's handle manager. + *

+ * Explicitly creating a persistent handle at 200% via + * {@link Image#win32_getHandle(Image, int)} places it in the handle manager. + * When GC.drawImage() then targets a pixel size that maps to imageZoom=200 + * (while nearestAvailableZoom stays 100% because the provider only has 100% + * data), the 200% handle must be found first via the imageZoom lookup and + * returned instead of the 100% one. + *

+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419 + */ + @Test + public void testDrawImagePrefersExistingHandleAtExactImageZoom() { + PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF); + ImageData imageData = new ImageData(10, 10, 24, palette); + Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null); + try { + long handle100 = Image.win32_getHandle(image, 100); + long handle200 = Image.win32_getHandle(image, 200); + assertNotEquals(0L, handle100, "100% handle should be non-zero"); + assertNotEquals(0L, handle200, "200% handle should be non-zero"); + assertNotEquals(handle100, handle200, "Handles for different zooms should be distinct native objects"); + long[] drawHandle = {0}; + // 20x20 pixels → imageZoom=200 for a 10x10 base; nearestAvailableZoom=100 + // The 200% handle already in the handle manager must be found and preferred + image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), 20, 20); + assertEquals(handle200, drawHandle[0], + "GC.drawImage() should prefer the existing handle at imageZoom=200 " + + "over the nearestAvailableZoom=100 handle"); + } finally { + image.dispose(); + } + } + + /** + * Tests that when the pixel size requested by GC.drawImage() maps to a zoom + * equal to a current monitor's zoom, a persistent handle is created for that + * zoom level and is subsequently reusable via + * {@link Image#win32_getHandle(Image, int)}. + *

+ * A shell is created so that {@code Display.getShells()} is non-empty and + * the monitor zoom is visible to the handle-selection logic. Drawing at a + * pixel size whose imageZoom equals the shell's zoom triggers the + * monitor-zoom persistence path: the handle is stored in the image's handle + * manager and can be retrieved without allocating a second native object. + * On 100%-DPI machines the new {@code imageZoom == monitorZoom} branch + * overlaps with the existing {@code nearestAvailableZoom == 100} fallback; + * on HiDPI machines the new branch is exercised in isolation. + *

+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419 + */ + @Test + public void testDrawImageCreatesAndReusesPersistentHandleForMonitorZoomImageZoom() { + PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF); + ImageData imageData = new ImageData(10, 10, 24, palette); + Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null); + Shell shell = new Shell(Display.getDefault()); + try { + int shellZoom = shell.getZoom(); + int pixelSize = 10 * shellZoom / 100; + long[] drawHandle = {0}; + // Drawing at shellZoom's pixel size → imageZoom == shellZoom == monitorZoom + // A persistent handle must be created and stored in the handle manager + image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), pixelSize, pixelSize); + // If persisted, win32_getHandle returns the same native handle without a new allocation + long persistedHandle = Image.win32_getHandle(image, shellZoom); + assertNotEquals(0L, drawHandle[0], "Draw handle should be non-zero"); + assertEquals(persistedHandle, drawHandle[0], + "GC.drawImage() at the monitor zoom should create a persistent handle " + + "so that win32_getHandle returns the same native object"); + } finally { + image.dispose(); + shell.dispose(); + } + } + /** * Tests that a persistent native handle already created via * {@link Image#win32_getHandle(Image, int)} is found and reused by diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java index 29e6fc94ae..962da6ee71 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java @@ -18,6 +18,7 @@ import java.io.*; import java.util.*; +import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.function.*; @@ -29,6 +30,7 @@ import org.eclipse.swt.internal.gdip.*; import org.eclipse.swt.internal.image.*; import org.eclipse.swt.internal.win32.*; +import org.eclipse.swt.widgets.*; /** * Instances of this class are graphics which have been prepared @@ -261,13 +263,10 @@ private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, i int imageZoomForHeight = 100 * heightHint / bounds.height; int imageZoom = DPIUtil.getZoomForAutoscaleProperty(Math.max(imageZoomForWidth, imageZoomForHeight)); int nearestAvailableZoom = imageProvider.nearestAvailableZoom(imageZoom); - InternalImageHandle bestFittingHandle = imageHandleManager.get(nearestAvailableZoom); + InternalImageHandle bestFittingHandle = getPersistentHandle(imageZoom, nearestAvailableZoom); if (bestFittingHandle != null) { return bestFittingHandle; } - if (nearestAvailableZoom == 100) { - return getHandleInternal(100, 100); - } if (previousHandle != null && previousHandle.zoom() == nearestAvailableZoom) { temporaryHandleContainer = previousHandle; return previousHandle.handle(); @@ -277,6 +276,35 @@ private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, i return temporaryHandleContainer.handle(); } + private InternalImageHandle getPersistentHandle(int imageZoom, int nearestAvailableZoom) { + InternalImageHandle bestFittingHandle = imageHandleManager.get(imageZoom); + if (bestFittingHandle != null) { + return bestFittingHandle; + } + if (getShellZooms().contains(imageZoom)) { + return getHandleInternal(imageZoom, imageZoom); + } + bestFittingHandle = imageHandleManager.get(nearestAvailableZoom); + if (bestFittingHandle != null) { + return bestFittingHandle; + } + if (nearestAvailableZoom == 100) { + return getHandleInternal(100, 100); + } + return null; + } + + private Set getShellZooms() { + if (getDevice() instanceof Display display) { + try { + return Arrays.stream(display.getShells()).map(Shell::getZoom).collect(Collectors.toSet()); + } catch (SWTException e) { + return Collections.emptySet(); + } + } + return Collections.emptySet(); + } + } private final HandleAtSize lastRequestedHandle = new HandleAtSize();