Custom Classes and Vassal 3.3

Hi Folks,

So far my custom classes seem to be running pretty well under Vassal 3.3, which is of course a relief.

However two relevant questions:
(1) When I make full on 3.3 versions of my modules, what version of Java should I be compiling the classes with? 8 like before? 9 because it’s the minimum requirement? 13 because it’s what comes bundled?

(2) Are there any notes (here, or new comments in the Vassal code) about the changes needed to provide HiDPI support? I have a custom class (LookAt, which I didn’t make but someone asked me to enable) which apparently does draw at an offset on HiDPI. But I don’t have a HiDPI monitor, etc, etc. SO… if there are any useful details for this that you guys picked up while getting 3.3 running, a quick summary would be most helpful. I’m pretty good at following instructions of the form “go look at what I did to ThisJavaClass.java and you’ll get the key idea”, if needed.

Any advice on either issue most appreciated!

Best,

Brian

Thus spake Cattlesquat:

Hi Folks,

So far my custom classes seem to be running pretty well under Vassal
3.3, which is of course a relief.

However two relevant questions:
(1) When I make full on 3.3 versions of my modules, what version of Java
should I be compiling the classes with? 8 like before? 9 because it’s
the minimum requirement? 13 because it’s what comes bundled?

As of 3.3.0-beta2, 9 is the minimum requirment. However, I’m not sure
we should keep it at 9. We’re bundling 13 for Windows and Macs. I have
yet to see anyone on Linux running 9 or 10; 11 seems to be by far the
most common. It might be a good opportunity to jump ahead to 11. I’m
open to suggestsions.

I believe we have some documentation about which version of Java to
target; I intend to update that before we release 3.3.0.

(2) Are there any notes (here, or new comments in the Vassal code) about
the changes needed to provide HiDPI support? I have a custom class
(LookAt, which I didn’t make but someone asked me to enable) which
apparently does draw at an offset on HiDPI. But I don’t have a HiDPI
monitor, etc, etc. SO… if there are any useful details for this that
you guys picked up while getting 3.3 running, a quick summary would be
most helpful. I’m pretty good at following instructions of the form “go
look at what I did to ThisJavaClass.java and you’ll get the key idea”,
if needed.

It’s too late tonight, but I can write up something about it tomorrow.


J.

Perfect, thanks!

Brian

I was able to reproduce the HiDPI issues on a Windows machine with a standard display by going into Display settings, Scale and layout and changing the ‘Change the size of text, apps and other items’ from 100% to a different value.

I was able to fix the issues in my GTS code using the info Joel sent me. I’ll send that on to you by email.

Thus spake Brent Easton:

I was able to reproduce the HiDPI issues on a Windows machine with a
standard display by going into Display settings, Scale and layout and
changing the ‘Change the size of text, apps and other items’ from 100%
to a different value.

I was able to fix the issues in my GTS code using the info Joel sent me.
I’ll send that on to you by email.

I see a few things you should do differently in that VSQL commit, some
of which are things which chaged after I sent you that email. Let me
put together my current recommendations later today.


J.

Another reason to make java 11 the required version is that it’s an “LTS” release with support through 2024 (at least via adoptopenjdk). Java 9 hasn’t been supported since March 2018.

Here’s my description of the changes necessary for HiDPI support, as promised. This is cobbled together from a few different things I wrote during the process, so may be a bit disjointed. If you find it can be improved in any regard, please do so:

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.

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. These functions are also overloaded to handle Rectangles.)

Updating module code for HiDPI support is exactly the same process I went through for the core VASSAL classes. There are two situations:

  1. You’re in a paint() method of a cutsom Swing component. If you’re not drawing
    any text or displaying any images, there’s nothing to do. If you are, then you need to descale the AffineTransform on the Graphics2D and then scale everything by the device scale factor yourself. (This lets images and text be drawn directly without being upscaled.) There are a bunch of examples of this; Map.View.paint() is one.
  public void paint(Graphics g) {
    final Graphics2D g2d = (Graphics2D) g;
    final double os_scale = g2d.getDeviceConfiguration().getDefaultTransform().getScaleX();
    final AffineTransform orig_t = g2d.getTransform();
    g2d.setTransform(SwingUtils.descaleTransform(orig_t));

    //
    // paint whatever you're going to paint
    //
  
    g2d.setTransform(orig_t);
  }

Note that you MUST set the transform on the Graphics2D back to the orignal one when you finish painting. Failure to do this will result in subseqent painting being completely screwed up.

  1. You’re in some drawing method which is called via Map.View.paint(), such as for a GamePiece or a Drawable. In this case, the zoom you’ve had passed in has already had the device scale factor applied to it, so there might not be anything you need to do—unless you’re using coordinates which are component coordinates and were expecting them to also be drawing coordinates, like it was before 3.3.0. In that case, you have to scale them appropriately, possibly using one of the scaling functions from Map.

Things to look for:

  • Map.getZoom(): If you’re calling Map.getZoom() for something which will not be involved in painting, it’s likely fine as-is. If you’re calling Map.getZoom() to compute something which will be used for painting, it’s likely you will also need to get the device scale factor and multiply that by Map.getZoom(). (You should get the device scale factor from the Graphics2D that you’re going to use to paint, not from the default graphics device, as it’s possible that they’re not the same.)

  • Map.componentCoordinates(), Map.componentRectangle(), Map.mapCoordinates(), map.mapRectangle(): These are all deprecated in favor of the new “aToB” coordinate translation functions. You need to update all of these. When any of these are used in a paint function, you probably need to swap occurances of translating to or from component coordinates to occurances of translating to or from drawing coordinates. (There may be some cases where you don’t, though—it really requires reading the code and understanding where the particular coordinates come from.) Conversely, drawing coordinates probably don’t occur in code which is outside of paint functions or is not closely associated with them.

  • Fonts: If you have text painted by Graphics.drawString(), and the size of that text depends on the zoom, then you’ll need to make sure that zoom is multiplied by the device scale factor.

  • ImageIcons: In order to have images rendered at the best resolution, you may want to use OwningOpMultiResolutionImage to supply the image. (Check SpecialDiceButton for an example of this.)

Generally, the HiDPI changes occur in 9829498a to e113a330 on the master branch on github:

github.com/vassalengine/vassal/ … …e113a330

Have a look at the “Files changed” tab for a diff. There’s a lot there, but it’s fairly formulaic—you’ll start to see a pattern to the changes as you read through them.

On Linux, I’m able to simulate a device scale factor other than unity by setting sun.java2d.uiScale. E.g., this gives me everything double-sized:

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

On Windows, there’s somewhere that you can set your device scaling, I think in the Control Panel.

On a Mac, I’m not sure, but if you have a Retina display, then you’re already running with a device scale factor of 2 (probably).

Woot! Thanks so much. I probably won’t even have to say any curse words to get it done. :smiley:

This. With the new release schedule, it only really makes sense to declare an LTS release as required.