[OpenJDK 2D-Dev] HiDPI support issues on Windows

Jim Graham james.graham at oracle.com
Thu Oct 6 18:31:43 UTC 2016

Ah, I see.

There are a lot of mistaken assumptions in the rendering there.  It's not just line thickness, it is assumptions about 
stroke control and positioning of strokes.

The biggest issue seems to be that developers (including our own internal Swing developers who heard this lecture from 
me decades ago, but ignored it because it was inconvenient and not yet relevant at the time) who use the integer-based 
draw commands assume that their lines are centered on the pixel that they are naming.  In other words "if I draw a line 
along x=5 then I am telling the system to fill all the pixels with an X coordinate of 5", but that is not what that 
drawing request is asking for.  The coordinates are at pixel edges and it is an artifact of our fill/stroke rules and 
stroke control hinting that has made this work out at 1:1 scaling.  As soon as you scale you can see the issues.  We 
used to only scale for printing, but now we are starting to scale for the screen.

g.drawRect(0,0,w-1,h-1) is a completely disfunctional way to outline a component with any settings other than 1:1 
coordinates and STROKE_CONTROL on.  I've been mentioning this for years (going on decades now), but Swing was addicted 
to that boilerplate for drawing a border.  Thankfully, FX and its CSS-focused skinning has gone with a different 
mechanism (primarily most FX components are outlined using successive fills - optimized in implementation to avoid 
overdrawing - rather than using strokes based on mistaken assumptions about where the line is drawn).

In particular, the line in that example g.drawRect(0,0,w-1,h-1) technically occurs at the following coordinates:

outer part of outline goes from 0.0-0.5,0.0-0.5 to w-1+0.5,h-1+0.5
    (i.e. -0.5,-0.5 to w-0.5,h-0.5)
inner part of outline goes from 0.0+0.5,0.0+0.5 to w-1-0.5,h-1-0.5
    (i.e. +0.5,+0.5 to w-1.5,h-1.5)

At a high enough scale you can see the stroke starting to separate from the fill on the right and bottom edges where the 
closest it gets to the edge is 0.5 scaled coordiantes.  That rounds to 0 for 1:1 and with the biasing from 
STROKE_CONTROL, but at higher resolutions it becomes a non-zero number of pixels.

Even at 1:1 scale if you follow our filling rules explicitly (which would require setting STROKE_CONTROL to PURE) then 
that would fill the rows at y=-1 and y=h-2 and the columns at x=-1 and x=w-2, which is not what you want at all. 
Setting STROKE_CONTROL on allows us to bias the location of the stroke a little and it ends up filling the correct 
pixels as a side effect (though STROKE_CONTROL is meant to achieve consistency in strokes, it also ends up shifting the 
line by enough that the fill rules choose different pixels in this case).  The STROKE_CONTROL=on version of the path is 
assumed to be (0.25,0.25,w-0.75,h-0.75) because we snap all coordinates in a stroke-controlled non-AA path to the 
nearest location that is 0.25,0.25 within a pixel.  This snapping to a consistent sub-pixel location biasing at 0.25 was 
chosen because line widths grow more evenly at that offset, but it offsets the outline enough so that the outline 
considered for filling becomes:

outer part of outline goes from 0.25-0.5,0.25-0.5 to 0.25+w-1+0.5,0.25+h-1+0.5
    (i.e. -0.25,-0.25 to w-0.25,h-0.25)
inner part of outline goes from 0.25+0.5,0.25+0.5 to 0.25+w-1-0.5,0.25+h-1-0.5
    (i.e. +0.75,+0.75 to w-1.25,h-1.25)

which renders the rows columns at 0 and wh-1 at a 1:1 scale using our fill rules.  Note that if you scale high enough 
you can still see separation between fill and outline, but the gap is only 0.25 scaled coordinates so it would take a 
scale of at least 4x to see it.

The technically accurate way to render the first/last pixel/1-unit-coordinate boundary of a component would be to 
drawRect(0.5,0.5,w-1,h-1) (with no stroke control set) which would place the rectangle at the following coordinates:

outer part of outline goes from 0.5-0.5,0.5-0.5 to 0.5+w-1+0.5,0.5+h-1+0.5
    (i.e. 0,0, to w,h)
inner part of outline goes form 0.5+0.5,0.5+0.5 to 0.5+w-1-0.5,0.5+h-1-0.5
    (i.e. 1,1 to w-1,h-1)

which completely encloses the first/last row/column of pixels on a 1:1 coordinate system and accurately covers the 
first/last N pixels in any arbitrary N-scaled coordinate system.  The rounding for scales like 1.5 still might not work 
out the way you wanted, but at least the exact geometry is consistent with respect to the placement of pixels.  With AA 
you will get a consistent border all around if w,h are snapped to a pixel size, but with non-AA then rounding error 
might lead to an extra pixel on one pair of sides.  I haven't done the analysis to see how the above technique would be 
affected by STROKE_CONTROL because really what you are looking for is to render the a consistent edge around the 
component and so successive fills as is done with most of our CSS skinning files in FX is a better solution overall. 
There are just too many considerations in filling to make it worthwhile for simple rectangular regions...


On 10/4/16 1:46 PM, Anton Tarasov wrote:
> On 10/4/2016 11:30 PM, Jim Graham wrote:
>> I wasn't familiar with the test code for this particular case.  Is it in a bug report somewhere?
> Jim, I'm re-attaching it (it was in my first e-mail).
> Thanks,
> Anton.
>>             ...jim
>> On 10/4/16 1:01 PM, Anton Tarasov wrote:
>>> Also, the problem with primitives rendering (http://cr.openjdk.java.net/%7Eant/hidpi_pics/Scaling-150-percent.png) is
>>> still there. But it seems to relate to line-thikness (border-thickness) rounding inaccuracy. What we can do with
>>> that?...

More information about the 2d-dev mailing list