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

Anton Tarasov anton.tarasov at jetbrains.com
Mon Oct 3 21:43:08 UTC 2016


Hi Jim,

Thank you for the details! This gives a clue.

Anton.

On 10/2/2016 10:10 PM, Jim Graham wrote:
> After looking into the code in RepaintManager and re-reading 
> Alexander's message again I can see how it describes what is going on 
> more clearly.
>
> Fixing the rounding errors doesn't necessarily require avoiding use of 
> the intermediate image for damage repair, you just have to make sure 
> that you use the incoming xywh as suggestions for what needs to be 
> redrawn, but instead determine exact pixels that you will repaint 
> (usually floor,floor,ceil,ceil to "round out" the area), and then use 
> those pixel-precise locations instead of passing along the integers 
> that came from the repaint requests and hoping for the right 
> rounding.  The problem is that a number of the interfaces used by the 
> RepaintManager take integers and hide a scale from the caller so we 
> need to either work around their implicit scale, or possible create 
> internal variants that let us work in pixels.
>
> In other words, the typical boilerplate for intermediate image damage 
> repair would be:
>
> // repainting x,y,w,h
> img = make image (w,h)
> g = img.getGraphics()
> g.setClip(x,y,w,h)
> g.translate(-x,-y)
> component.paint(g)
> destination.drawImage(img, x,y)
>
> but that boilerplate only works if x,y are exact pixel coordinates, 
> but since it is all being doing on a scaled graphics then x,y will 
> transform to arbitrary not-necessarily-integer locations and then all 
> bets are off.
>
> Fixing this could either rely on using float interfaces wherever 
> available, or by undoing all of the implicit scales and working in 
> pixels, but being aware of the scale that is required for the 
> destination.  Something like one of these boilerplates instead:
>
> // repainting x,y,w,h integers using floats
> float pixelx1 = floor(x * scaleX)
> float pixely1 = floor(y * scaleY)
> float pixelx2 = ceil((x+w) * scaleX)
> float pixely2 = ceil((y+h) * scaleY)
> int pixelw = (int) (pixelx2 - pixelx1)
> int pixelh = (int) (pixely2 - pixely1)
> // Note that the code currently asks the destination to make
> // a compatible image of a virtual pixel size that is then
> // scaled to match.  A "make me an image of this many pixels"
> // might be less cumbersome.
> img = make image (ceil(pixelw / scaleX),
>                   ceil(pixelh / scaleY))
> g = img.getGraphics() // will be scaled already
> // The following will use the translate(double, double) method
> g.setClip(new Rectangle2D.Double(pixel* / scale*))
> g.translate(-pixelx1 / scaleX, -pixely1 / scaleY)
> component.paint(g)
> // Since there is no drawImage(img, float x, float y)...
> destination.translate(pixelx1 / scaleX, pixely1 / scaleY)
> destination.drawImage(img, 0, 0)
> // (restore transforms where needed)
>
> That version uses floating point interfaces in a number of key places 
> (notably translate() calls are available as either int or double in 
> the Graphics and have to use the setClip(Shape) method to specify a 
> floating point rectangle), but a down side is that using those 
> interfaces means that you have a value that you know is at a pixel 
> boundary and you pass it in as "number / scale" only to have the code 
> in the Graphics immediately apply that scale and you end up with the 
> final result of "number / scale * scale" which might incur round-off 
> errors and end up being slightly off of a pixel.
>
> In another approach, you could also kill all of the transforms and do 
> it more directly in pixels as in the following:
>
> // repainting x,y,w,h integers using unscaled operations
> // Some parts more cumbersome to undo the implicit scaling
> // but it won't suffer from round-off errors when constantly
> // scaling and unscaling through the various interfaces
> // that have transforms built in
> int pixelx1 = (int) floor(x * scaleX)
> int pixely1 = (int) floor(y * scaleY)
> int pixelx2 = (int) ceil((x+w) * scaleX)
> int pixely2 = (int) ceil((y+h) * scaleY)
> int pixelw = pixelx2 - pixelx1;
> int pixelh = pixely2 - pixely1;
> // Not sure if there is a mechanism for this since I think
> // all of the interfaces to get a compatible image are
> // designed to assume that the caller is not scale-aware.
> img = make pixel-sized image (pixelw, pixelh)
> g = img.getGraphics()
> // assuming that g would be unscaled in this case, but
> // if g is scaled, then g.setTransform(IDENTITY)
> // translate by an integer amount, and then scale
> g.setClip(pixelx1, pixely1, pixelw, pixelh)
> g.translate(pixelx1, pixely1)
> g.scale(scaleX, scaleY);
> component.paint(g)
> destinationg.setTransform(IDENTITY)
> destinationg.drawImage(img, pixelx1, pixely1)
> // (restore transforms where needed)
>
>             ...jim
>
> On 9/30/2016 1:30 PM, Jim Graham wrote:
>>
>>
>> On 9/30/16 3:22 AM, Alexandr Scherbatiy wrote:
>>> The problem is that the RepaintManager draws a region to a buffered
>>> image at first and draws the image after that to the
>>> window.
>>> Suppose the image has int coordinates and size (x, y, w, h) in the
>>> user space. It should be drawn into the region with
>>> coordinates (x, y, x+width, y+height) = (x1, y1, x2, y2).
>>> If floating point UI scale is used (like 1.5) the region coordinates
>>> are converted to values (1.5 * x1, 1.5 * y1, 1.5 *
>>> x2, 1.5 * y2) in the dev space.
>>> Now these coordinates need to be rounded and the process really
>>> depends on the evenness or oddness of the start and end
>>> coordinates. They both can be rounded to one side or to opposite.
>>> Depending on this some lines near the drawn image
>>> region can be not filled or just wrongly filled.
>>
>> The repaint manager should compute the nearest pixel bounds outside of
>> the scaled repaint area, and then adjust the rendering to repaint all of
>> those pixels.  You don't "round" here, you "floor,floor,ceil,ceil" (and
>> then worry how to present the clip region to the app so it can do the
>> right thing - probably by clipping to a Rect2D.Float() and letting the
>> integer g.getClipBounds() further round out the coordinates which means
>> extra paint calls, but at least you'll repaint all the dirty pixels and
>> they will be blitted to the right destination pixels if the code that
>> sends them to the screen is aware of the full resolution...)
>>
>>                 ...jim




More information about the 2d-dev mailing list