[OpenJDK 2D-Dev] HiDPI support issues on Windows
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).
>> 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
More information about the 2d-dev