diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
index 978c3f612b..aab166f869 100644
--- a/src/org/openstreetmap/josm/gui/MapView.java
+++ b/src/org/openstreetmap/josm/gui/MapView.java
@@ -3,17 +3,19 @@
import static java.util.function.Predicate.not;
+import java.awt.AWTException;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
-import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
+import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
+import java.awt.ImageCapabilities;
import java.awt.Point;
import java.awt.Rectangle;
-import java.awt.Shape;
import java.awt.Stroke;
import java.awt.Transparency;
import java.awt.event.ComponentAdapter;
@@ -24,7 +26,7 @@
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
-import java.awt.image.BufferedImage;
+import java.awt.image.VolatileImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
@@ -86,12 +88,12 @@
import org.openstreetmap.josm.tools.bugreport.BugReport;
/**
- * This is a component used in the {@link MapFrame} for browsing the map. It use is to
+ * This is a component used in the {@link MapFrame} for browsing the map. It’s used to
* provide the MapMode's enough capabilities to operate.
*
* {@code MapView} holds meta-data about the data set currently displayed, as scale level,
* center point viewed, what scrolling mode or editing mode is selected or with
- * what projection the map is viewed etc..
+ * what projection the map is viewed, etc.
*
* {@code MapView} is able to administrate several layers.
*
@@ -101,6 +103,9 @@ public class MapView extends NavigatableComponent
implements PropertyChangeListener, PreferenceChangedListener,
LayerManager.LayerChangeListener, MainLayerManager.ActiveLayerChangeListener {
+ private static final boolean IS_HEADLESS = GraphicsEnvironment.isHeadless();
+ private static Rectangle virtualDesktopBounds = getVirtualDesktopBounds();
+
static {
MapPaintStyles.addMapPaintStylesUpdateListener(new MapPaintStylesUpdateListener() {
@Override
@@ -234,10 +239,10 @@ public void detachFromMapView(MapViewEvent event) {
*/
private final transient Set temporaryLayers = new LinkedHashSet<>();
- private transient BufferedImage nonChangedLayersBuffer;
- private transient BufferedImage offscreenBuffer;
+ private transient VolatileImage unchangedLayersBuffer;
+ private transient VolatileImage offscreenBuffer;
// Layers that wasn't changed since last paint
- private final transient List nonChangedLayers = new ArrayList<>();
+ private final transient List unchangedLayers = new ArrayList<>();
private int lastViewID;
private final AtomicBoolean paintPreferencesChanged = new AtomicBoolean(true);
private Rectangle lastClipBounds = new Rectangle();
@@ -277,7 +282,18 @@ public void componentResized(ComponentEvent e) {
mapMover = new MapMover(MapView.this);
}
});
-
+ addPropertyChangeListener("graphicsConfiguration", event -> {
+ GraphicsConfiguration gc = (GraphicsConfiguration) event.getNewValue();
+ if (null == gc) {
+ return;
+ }
+ Rectangle currentVirtualDesktopBounds = getVirtualDesktopBounds();
+ if (!virtualDesktopBounds.equals(currentVirtualDesktopBounds)) {
+ virtualDesktopBounds = currentVirtualDesktopBounds;
+ }
+ offscreenBuffer = getAcceleratedBuffer(gc);
+ unchangedLayersBuffer = getAcceleratedBuffer(gc);
+ });
// listens to selection changes to redraw the map
SelectionEventManager.getInstance().addSelectionListenerForEdt(repaintSelectionChangedListener);
@@ -311,6 +327,14 @@ public void mousePressed(MouseEvent me) {
AutoFilterManager.getInstance().enableAutoFilterRule(AutoFilterManager.PROP_AUTO_FILTER_RULE.get());
}
setTransferHandler(new OsmTransferHandler());
+ setOpaque(true);
+
+ if (!IS_HEADLESS) {
+ GraphicsDevice defaultSD = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
+ GraphicsConfiguration defaultGC = defaultSD.getDefaultConfiguration();
+ offscreenBuffer = getAcceleratedBuffer(defaultGC);
+ unchangedLayersBuffer = getAcceleratedBuffer(defaultGC);
+ }
}
/**
@@ -333,14 +357,55 @@ public static List extends JComponent> getMapNavigationComponents(MapView forM
return Arrays.asList(zoomSlider, scaler);
}
- private static BufferedImage getAcceleratedImage(Component mv, int width, int height) {
- if (GraphicsEnvironment.isHeadless()) {
- return new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
+ private static Rectangle getVirtualDesktopBounds() {
+ GraphicsEnvironment localGE = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ GraphicsDevice[] localSDs = localGE.getScreenDevices();
+ Rectangle virtualDesktopBounds = new Rectangle();
+ for (GraphicsDevice screenDevice : localSDs) {
+ Rectangle sdBounds = getHiDPIDeviceBounds(screenDevice);
+ virtualDesktopBounds = virtualDesktopBounds.union(sdBounds);
+ }
+ virtualDesktopBounds.setLocation(0, 0);
+ return virtualDesktopBounds;
+ }
+
+ private static Rectangle getHiDPIDeviceBounds(GraphicsDevice screenDevice) {
+ GraphicsConfiguration defaultGC = screenDevice.getDefaultConfiguration();
+ // bounds for a DPI-aware GC return bounds with correctly scaled dimensions but unscaled screen-space
+ // coordinates, for whatever reason
+ // in fact, *everything* that returns screen-space coordinates does so without scaling 😭
+ AffineTransform defaultTransform = defaultGC.getDefaultTransform();
+ double scaleX = defaultTransform.getScaleX();
+ double scaleY = defaultTransform.getScaleY();
+ Rectangle gcBounds = defaultGC.getBounds();
+ gcBounds.setLocation((int) Math.round(gcBounds.x / scaleX), (int) Math.round(gcBounds.y / scaleY));
+ return gcBounds;
+ }
+
+ private VolatileImage getAcceleratedBuffer(GraphicsConfiguration graphicsConfiguration) {
+ // hardware-accelerated pipelines typically initialize on at least the default device, so we use that and hope
+ // for the best 😭
+ int width = virtualDesktopBounds.width;
+ int height = virtualDesktopBounds.height;
+ ImageCapabilities bufferCaps = new ImageCapabilities(true);
+ VolatileImage buffer;
+ try {
+ buffer = graphicsConfiguration.createCompatibleVolatileImage(width, height, bufferCaps,
+ Transparency.OPAQUE);
+ } catch (AWTException e) {
+ buffer = graphicsConfiguration.createCompatibleVolatileImage(width, height, Transparency.OPAQUE);
+ }
+ boolean isBufferAccelerated = buffer.getCapabilities().isAccelerated();
+ if (isBufferAccelerated) {
+ buffer.setAccelerationPriority(1);
+ } else {
+ Logging.info("HW acceleration unavailable on screen device {0}",
+ graphicsConfiguration.getDevice().getIDstring());
}
- return mv.getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE);
+ return buffer;
}
- // remebered geometry of the component
+ // Remembered geometry of the component
private Dimension oldSize;
private Point oldLoc;
@@ -439,7 +504,7 @@ public void setVirtualNodesEnabled(boolean enabled) {
/**
* Checks if virtual nodes should be drawn. Default is false
- * @return The virtual nodes property.
+ * @return The virtual node’s property.
* @see Rendering#render
*/
public boolean isVirtualNodesEnabled() {
@@ -505,162 +570,111 @@ public void paint(Graphics g) {
return;
}
- try {
- drawMapContent((Graphics2D) g);
- } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
- throw BugReport.intercept(e).put("visibleLayers", layerManager::getVisibleLayersInZOrder)
- .put("temporaryLayers", temporaryLayers);
+ if (!IS_HEADLESS) {
+ try {
+ drawMapContent((Graphics2D) g);
+ } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
+ throw BugReport.intercept(e).put("visibleLayers", layerManager::getVisibleLayersInZOrder)
+ .put("temporaryLayers", temporaryLayers);
+ }
}
super.paint(g);
}
private void drawMapContent(Graphics2D g) {
- // In HiDPI-mode, the Graphics g will have a transform that scales
- // everything by a factor of 2.0 or so. At the same time, the value returned
- // by getWidth()/getHeight will be reduced by that factor.
- //
- // This would work as intended, if we were to draw directly on g. But
- // with a temporary buffer image, we need to move the scale transform to
- // the Graphics of the buffer image and (in the end) transfer the content
- // of the temporary buffer pixel by pixel onto g, without scaling.
- // (Otherwise, we would upscale a small buffer image and the result would be
- // blurry, with 2x2 pixel blocks.)
- AffineTransform trOrig = g.getTransform();
- double uiScaleX = g.getTransform().getScaleX();
- double uiScaleY = g.getTransform().getScaleY();
- // width/height in full-resolution screen pixels
- int width = (int) Math.round(getWidth() * uiScaleX);
- int height = (int) Math.round(getHeight() * uiScaleY);
- // This transformation corresponds to the original transformation of g,
- // except for the translation part. It will be applied to the temporary
- // buffer images.
- AffineTransform trDef = AffineTransform.getScaleInstance(uiScaleX, uiScaleY);
- // The goal is to create the temporary image at full pixel resolution,
- // so scale up the clip shape
- Shape scaledClip = trDef.createTransformedShape(g.getClip());
-
List visibleLayers = layerManager.getVisibleLayersInZOrder();
+ int unchangedLayersCount = getUnchangedLayersCount(visibleLayers);
+ do {
+ if (VolatileImage.IMAGE_INCOMPATIBLE == offscreenBuffer.validate(getGraphicsConfiguration())) {
+ offscreenBuffer = getAcceleratedBuffer(getGraphicsConfiguration());
+ }
+ Graphics2D g2 = offscreenBuffer.createGraphics();
+ // TODO: clip to content visible in screen-space *only*
+ g2.setClip(getVisibleRect());
+ renderUnchangedLayersBuffer(g, visibleLayers, unchangedLayersCount);
+ g2.drawImage(unchangedLayersBuffer, 0, 0, null);
+
+ for (Layer layer : visibleLayers.subList(unchangedLayersCount, visibleLayers.size())) {
+ paintLayer(layer, g2);
+ }
- int nonChangedLayersCount = 0;
- Set invalidated = invalidatedListener.collectInvalidatedLayers();
- for (Layer l: visibleLayers) {
- if (invalidated.contains(l)) {
- break;
- } else {
- nonChangedLayersCount++;
+ try {
+ drawTemporaryLayers(g2, getLatLonBounds(g.getClipBounds()));
+ } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
+ BugReport.intercept(e).put("temporaryLayers", temporaryLayers).warn();
}
- }
- boolean canUseBuffer = !paintPreferencesChanged.getAndSet(false)
- && nonChangedLayers.size() <= nonChangedLayersCount
- && lastViewID == getViewID()
- && lastClipBounds.contains(g.getClipBounds())
- && nonChangedLayers.equals(visibleLayers.subList(0, nonChangedLayers.size()));
+ // draw world borders
+ try {
+ drawWorldBorders(g2);
+ } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
+ // getProjection() needs to be inside lambda to catch errors.
+ BugReport.intercept(e).put("bounds", () -> getProjection().getWorldBoundsLatLon()).warn();
+ }
- if (null == offscreenBuffer || offscreenBuffer.getWidth() != width || offscreenBuffer.getHeight() != height) {
- offscreenBuffer = getAcceleratedImage(this, width, height);
- }
+ MapFrame map = MainApplication.getMap();
+ if (AutoFilterManager.getInstance().getCurrentAutoFilter() != null) {
+ AutoFilterManager.getInstance().drawOSDText(g2);
+ } else if (MainApplication.isDisplayingMapView() && map.filterDialog != null) {
+ map.filterDialog.drawOSDText(g2);
+ }
- if (!canUseBuffer || nonChangedLayersBuffer == null) {
- if (null == nonChangedLayersBuffer
- || nonChangedLayersBuffer.getWidth() != width || nonChangedLayersBuffer.getHeight() != height) {
- nonChangedLayersBuffer = getAcceleratedImage(this, width, height);
+ if (playHeadMarker != null) {
+ playHeadMarker.paint(g2, this);
}
- Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
- g2.setClip(scaledClip);
- g2.setTransform(trDef);
- g2.setColor(PaintColors.getBackgroundColor());
- g2.fillRect(0, 0, width, height);
-
- for (int i = 0; i < nonChangedLayersCount; i++) {
- paintLayer(visibleLayers.get(i), g2);
+
+ g2.dispose();
+ g.drawImage(offscreenBuffer, 0, 0, null);
+ } while (offscreenBuffer.contentsLost() || unchangedLayersBuffer.contentsLost());
+ offscreenBuffer.flush();
+ }
+
+ private void renderUnchangedLayersBuffer(Graphics2D g, List visibleLayers, int unchangedLayersCount) {
+ boolean canUseBuffer = !paintPreferencesChanged.getAndSet(false)
+ && lastViewID == getViewID()
+ && lastClipBounds.contains(g.getClipBounds())
+ && unchangedLayers.size() <= unchangedLayersCount
+ && unchangedLayers.equals(visibleLayers.subList(0, unchangedLayers.size()));
+ do {
+ if (VolatileImage.IMAGE_INCOMPATIBLE == unchangedLayersBuffer.validate(getGraphicsConfiguration())) {
+ unchangedLayersBuffer = getAcceleratedBuffer(getGraphicsConfiguration());
+ canUseBuffer = false;
}
- } else {
- // Maybe there were more unchanged layers then last time - draw them to buffer
- if (nonChangedLayers.size() != nonChangedLayersCount) {
- Graphics2D g2 = nonChangedLayersBuffer.createGraphics();
- g2.setClip(scaledClip);
- g2.setTransform(trDef);
- for (int i = nonChangedLayers.size(); i < nonChangedLayersCount; i++) {
- paintLayer(visibleLayers.get(i), g2);
+ if (!canUseBuffer || (unchangedLayers.size() != unchangedLayersCount)) {
+ Graphics2D g2 = unchangedLayersBuffer.createGraphics();
+ g2.setClip(getVisibleRect());
+ if (!canUseBuffer) {
+ g2.setColor(PaintColors.getBackgroundColor());
+ g2.fillRect(0, 0, getWidth(), getHeight());
+ for (Layer layer : visibleLayers.subList(0, unchangedLayersCount)) {
+ paintLayer(layer, g2);
+ }
+ } else {
+ // If there were more unchanged layers than last time, draw them to the buffer
+ for (Layer layer : visibleLayers.subList(unchangedLayers.size(), unchangedLayersCount)) {
+ paintLayer(layer, g2);
+ }
}
+ g2.dispose();
}
- }
-
- nonChangedLayers.clear();
- if (nonChangedLayersCount > 0)
- nonChangedLayers.addAll(visibleLayers.subList(0, nonChangedLayersCount));
+ } while (unchangedLayersBuffer.contentsLost());
+ unchangedLayers.clear();
+ unchangedLayers.addAll(visibleLayers.subList(0, unchangedLayersCount));
lastViewID = getViewID();
lastClipBounds = g.getClipBounds();
+ }
- Graphics2D tempG = offscreenBuffer.createGraphics();
- tempG.setClip(scaledClip);
- tempG.setTransform(new AffineTransform());
- tempG.drawImage(nonChangedLayersBuffer, 0, 0, null);
- tempG.setTransform(trDef);
-
- for (int i = nonChangedLayersCount; i < visibleLayers.size(); i++) {
- paintLayer(visibleLayers.get(i), tempG);
- }
-
- try {
- drawTemporaryLayers(tempG, getLatLonBounds(new Rectangle(
- (int) Math.round(g.getClipBounds().x * uiScaleX),
- (int) Math.round(g.getClipBounds().y * uiScaleY))));
- } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
- BugReport.intercept(e).put("temporaryLayers", temporaryLayers).warn();
- }
-
- // draw world borders
- try {
- drawWorldBorders(tempG);
- } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
- // getProjection() needs to be inside lambda to catch errors.
- BugReport.intercept(e).put("bounds", () -> getProjection().getWorldBoundsLatLon()).warn();
- }
-
- MapFrame map = MainApplication.getMap();
- if (AutoFilterManager.getInstance().getCurrentAutoFilter() != null) {
- AutoFilterManager.getInstance().drawOSDText(tempG);
- } else if (MainApplication.isDisplayingMapView() && map.filterDialog != null) {
- map.filterDialog.drawOSDText(tempG);
- }
-
- if (playHeadMarker != null) {
- playHeadMarker.paint(tempG, this);
- }
-
- try {
- g.setTransform(new AffineTransform(1, 0, 0, 1, trOrig.getTranslateX(), trOrig.getTranslateY()));
- g.drawImage(offscreenBuffer, 0, 0, null);
- } catch (ClassCastException e) {
- // See #11002 and duplicate tickets. On Linux with Java >= 8 Many users face this error here:
- //
- // java.lang.ClassCastException: sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData
- // at sun.java2d.xr.XRPMBlitLoops.cacheToTmpSurface(XRPMBlitLoops.java:145)
- // at sun.java2d.xr.XrSwToPMBlit.Blit(XRPMBlitLoops.java:353)
- // at sun.java2d.pipe.DrawImage.blitSurfaceData(DrawImage.java:959)
- // at sun.java2d.pipe.DrawImage.renderImageCopy(DrawImage.java:577)
- // at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:67)
- // at sun.java2d.pipe.DrawImage.copyImage(DrawImage.java:1014)
- // at sun.java2d.pipe.ValidatePipe.copyImage(ValidatePipe.java:186)
- // at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3318)
- // at sun.java2d.SunGraphics2D.drawImage(SunGraphics2D.java:3296)
- // at org.openstreetmap.josm.gui.MapView.paint(MapView.java:834)
- //
- // It seems to be this JDK bug, but Oracle does not seem to be fixing it:
- // https://bugs.openjdk.java.net/browse/JDK-7172749
- //
- // According to bug reports it can happen for a variety of reasons such as:
- // - long period of time
- // - change of screen resolution
- // - addition/removal of a secondary monitor
- //
- // But the application seems to work fine after, so let's just log the error
- Logging.error(e);
- } finally {
- g.setTransform(trOrig);
+ private int getUnchangedLayersCount(List visibleLayers) {
+ int unchangedLayersCount = 0;
+ Set invalidated = invalidatedListener.collectInvalidatedLayers();
+ for (Layer l: visibleLayers) {
+ if (invalidated.contains(l)) {
+ break;
+ } else {
+ unchangedLayersCount++;
+ }
}
+ return unchangedLayersCount;
}
private void drawTemporaryLayers(Graphics2D tempG, Bounds box) {
@@ -823,11 +837,11 @@ public void destroy() {
if (mapMover != null) {
mapMover.destroy();
}
- nonChangedLayers.clear();
+ unchangedLayers.clear();
synchronized (temporaryLayers) {
temporaryLayers.clear();
}
- nonChangedLayersBuffer = null;
+ unchangedLayersBuffer = null;
offscreenBuffer = null;
setTransferHandler(null);
GuiHelper.destroyComponents(this, false);
diff --git a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
index faaaf50268..610056b725 100644
--- a/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
+++ b/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java
@@ -1507,12 +1507,14 @@ public void highlightUpdated(HighlightUpdateEvent e) {
@Override
public void primitiveHovered(PrimitiveHoverEvent e) {
- List primitives = new ArrayList<>(2);
- primitives.add(e.getHoveredPrimitive());
- primitives.add(e.getPreviousPrimitive());
- primitives.removeIf(Objects::isNull);
- resetTiles(primitives);
- this.invalidate();
+ if (MapRendererFactory.getInstance().isMapRendererActive(StyledTiledMapRenderer.class)) {
+ List primitives = new ArrayList<>(2);
+ primitives.add(e.getHoveredPrimitive());
+ primitives.add(e.getPreviousPrimitive());
+ primitives.removeIf(Objects::isNull);
+ resetTiles(primitives);
+ this.invalidate();
+ }
}
@Override