Current state of things:
- I have set up a VASSAL repo on github: github.com/uckelman/vassal
I had not been intending to do this for V3, on the assumption that by now we’d be working on V4; however, the work for V3.3 has reminded me of just how painful Subversion is in comparison with Git and I can’t stand it anymore.
I’m not abandoning our svn repo, but the rest of the work I do for V3.3 is going to happen using git and then be pushed back to svn later.
The two branches in the git repo are master, which is the same as master in svn at present, and hdpi, which is where I’md going the HiDPI work.
- Traditionally, one screen pixel has equaled one “user space” pixel. This changed when Apple introduced their “Retina” displays some years ago. On a Retina screen, each user-space pixel is twice as wide and twice as tall as a screen pixel—i.e., each user-space pixel corresponds to four screen pixels. Screens where user-space pixels correspond to multiple screen pixels are HiDPI screens.
Through Java 8, Java treated HiDPI displays as regular displays, which is why Java applications running in those versions of Java looked very small on HiDPI displays. Java 9 has HiDPI support for Swing components—a Swing application (as VASSAL is) running on Java 9 or later on a HiDPI display will look the right size, since the Swing components in Java 9 are drawn to allow for a difference betwen user and screen pixels. In order to accomplish this, how all the standard Swing components are drawn had to be modified. (Specifically, each component has to be aware that its user-space dimensions differ from its screen dimensions by a constant factor.) We get all this for free with the standard components when moving from Java 8 to Java 9—but we’re stuck undertaking that work for any custom components we have which draw themselves. One such component is the map. There are no standard Swing components which are even remotely close to being able to handle displaying game maps.
- With Java 8 on a HiDPI display, Swing applications are drawn at half scale. With Java 9 (or later) on a HiDPI display, Swing applications which haven’t been updated for HiDPI have their custom components drawn at user-space size but then upscaled to fill the pixels, with predictably crappy looking results. This has the virtue of keeping the applications nominally functional, but makes anything your application draws quite blurry. The correct solution is to paint on the basis of how many screen pixels you have to fill, so that no upscaling occurs. To take a concrete example: If a map is being displayed at 25% zoom on a HiDPI display where each user-space pixel is four screen pixels, then we need to treat the map’s zoom as 50% when drawing (25% map zoom * screen scale factor of 2).
That sounds simple, but in practice it is not—the reason being that drawing and component interaction had previously always taken place in the same coordinate space, so there are no indicators in the code for which are which, and coordinates originating from component interaction necesasrily get fed into drawing when doing things like dragging pieces.
- So, if you look at Map.java, you’ll find Map.View, which is the Swing component which displays the map. Map.View.paint(Graphics g) is the function which paints the map, and that’s where we have to start.
That Graphics is actually a Graphics2D, so you can cast it to one and get an AffineTransform from it:
final Graphics2D g2d = (Graphics2D) g;
final AffineTransform orig_t = g2d.getTransform();
If you’re running Java 8 or using a display which is not HiDPI and you check the scale elements of the AffineTransform (using getScaleX(), getScaleY()), you’ll find that they’re 1.0. If you’re using Java 9 or later on a HiDPI system, however (or setting the sun.java2d.uiScale property to simulate a HiDPI system—more on that later), you’ll find that your scale elements are 2.0. Now we see how exactly Java 9 handles upscaling custom Swing components which haven’t been modified for HiDPI support—it’s the scale factor on the AffineTransform which does it.
So, how do we adjust this to avoid upscaling and get crisp drawing back? What we need to do is save the AffineTransform we’re given and replace it with an indentity transform but then draw everything with an effective zoom that’s our actual map zoom multiplied by the scale factor the original AffineTransform had.
In service of this, I’ve expanded the coordinate space transformation functions in Map to handle conversions to and from drawing coordinates, because we have to deal with three coordinate spaces now—map, component, AND drawing, whereas previously component and drawing coordinates were always identical. These functions all have names of the form “aToB”, where “a” and “B” are coordinate spaces. E.g., componentToDrawing() converts component coordinates to drawing coordinates, while drawingToMap() converts drawing coordinates to map coordinates.)
Map.View.paint() doesn’t do much itself, but there we convert the visible rectangle from component space to drawing space before passing it on to Map.paintRegion():
final Rectangle r = map.componentToDrawing(getVisibleRect());
Map.paintRegion() calls in succession the functions which clear the map border, paint the boards, and draw the pieces. What I’ve done in each of these is similar, so I’ll take Map.drawBoardsInRegion() as an exmaple and omit discussion of the others.
public void drawBoardsInRegion(Graphics g,
Rectangle visibleRect,
Component c) {
final double dzoom = getZoom() * os_scale;
for (Board b : boards) {
b.drawRegion(g, getLocation(b, dzoom), visibleRect, dzoom, c);
}
}
The change I’ve made is simply to the scale factor which gets passed on: It used to be Map.getZoom(), but is now getZoom() * os_scale (which is the HiDPI scale factor). That’s it. Everything further down this call tree is expecting to be told a scale factor and to draw at that scale factor, so by adjusting the scale factor here, it does the right thing. (*This is not 100% true, I had to make a small modification in Board.java as well, but for most things we draw it is true.)
- I’ve made a bunch of changes as in #4 which get us most of the way back to correct rendering. However, there are a few difficult components which involve both rendering and user interaction which are not yet done: GlobalMap, MenuDisplayer, PieceMover for sure; possibly also LOS_Thread, FreeRotator, MapCenterer.
In order to troubleshoot these, you need to run with Java 9 or later, and either use a HiDPI display OR set the sun.java2d.uiScale property to force Java to upscale your UI. The following has been my test setup:
java -classpath lib/Vengine.jar --add-exports java.desktop/sun.java2d.cmm=ALL-UNNAMED -Dsun.java2d.uiScale=2 VASSAL.launch.Player --standalone ../mods/The_caucasus_campaign_1_2_1.vmod
(Note that 2 is likely the only value which makese sense for uiScale. 1 is just normal, so you won’t be able to troubleshoot that way. Non-integers seem not to work, and anyhow would be awful for aliasing if they did. 3 is way too huge.)
Particular problems:
- GlobalMap is very broken. It needs a complete read-through.
- MenuDisplayer handles showing context menus when you right-click. The problem to solve here is that the map jumps when you right-click under HiDPI.
- PieceMover handles piece drags, among other things. The problem here is incorrect repainting behind drags.
There are likely to be other problems I have yet to find. I’d appreciate being made aware of those and shown how to reproduce them.
- If you’d like to help with this, clone the git repo and start reading through the relevant code.