Resizing and Scaling
If you draw with cl-gfx, through cl-gfx/windowing, its surfaces do
everything on this page for you. Read on to understand what they do, to
size your own render targets, or to draw with Vulkan directly.
Units and pixels
A window has a size in units and a scale.
- Units are pixels at scale 1.
window-sizeand pointer positions are in units. A window of 800 by 600 units covers the same part of the screen on every display, whatever its density. - The scale is how many pixels a frame has per unit on the display the
window is on: 1 on an ordinary display, 2 on a high-density one, 3/2 on a
display scaled to 150%. It is an exact rational, of type
scale, so sizes in pixels follow from it without rounding errors creeping in. - The pixel size,
window-pixel-size, is the size times the scale: the size of the frames to draw. 800 by 600 units at scale 3/2 is 1200 by 900 pixels. Before the window system has given the window its first size, nothing may be drawn, and the pixel size is 0 by 0.
Pixel sizes are rounded to the nearest whole pixel, halves up, as Wayland rounds them.
To look the same size on every display, draw in units and multiply by the
scale when you turn them into pixels. A 12-unit font is drawn at
(* 12 (win:window-scale window)) pixels: 12 at scale 1, 18 at scale 3/2,
24 at scale 2. Its edges stay sharp, because it is drawn at the display's
own pixels instead of being stretched.
The resize handshake
The window system may propose a new size at any time: when the user drags an edge, maximises the window, or tiles it. It may also propose a new scale, when the window moves to another display or the user changes the display's scaling.
The window does not take a proposal at once. It goes through a handshake with the code that renders:
- The window system proposes a size, a scale, or both.
- The proposal becomes the window's pending size. A
:resizeevent announces it, withevent-width,event-heightandevent-scale. - The code that renders sees the pending size, and recreates its swapchain and its other render targets at the pending pixel size.
- Just before it presents the first frame drawn at the new size, it calls
adopt-window-size. - The window takes the size, and the window system is told that the next
frame follows its proposal. On Wayland, this is when
xdg_surface.ack_configureis sent.
So the window system never sees a frame whose size disagrees with what it was told, and a window being resized does not flicker or stretch.
Some proposals need no handshake, and are taken at once:
- the first size, which arrives before anything has been drawn. It
still sends a
:resizeevent; - a proposal that keeps the window's size and scale.
If a proposal leaves out a dimension, as Wayland does with 0, the window keeps its own.
If nothing calls adopt-window-size, the window keeps its old size and
scale, and its frames stay at the old size. Nothing breaks, but the window
does not follow the user.
The functions
| Function | Returns |
|---|---|
(window-size window) |
The width and height in units, as last adopted. |
(window-scale window) |
The scale, as last adopted. |
(window-pixel-size window) |
The width and height in pixels: size times scale; 0 and 0 before the first size. |
(window-pending-size window) |
The proposed width and height in units and scale, or nil, nil, nil when nothing is pending. |
(window-pending-pixel-size window) |
The proposed width and height in pixels, or nil, nil. |
(adopt-window-size window) |
Takes the pending size and scale, if any, and returns the width and height in units. |
The pending functions read the size and the scale together under the window system's lock, so they always describe one proposal. They are cheap and allocate nothing: call them every frame.
adopt-window-size with nothing pending does nothing and returns the
size, so it is safe to call before every present.
In a render loop
Each frame, on the render thread:
(multiple-value-bind (pending-width pending-height) (win:window-pending-pixel-size window)
(let ((adopt (and pending-width t)))
(when adopt
(recreate-swapchain pending-width pending-height))
(draw-frame)
(when adopt
;; The frame is drawn at the new size: let the window system know.
(win:adopt-window-size window))
(present)))
Only recreate the swapchain when the pixel size changed: a proposal may change the size in units and the scale while the pixel size stays the same. Recreate it too when Vulkan reports it out of date.
While a size is pending, draw at the pending size and scale. Code that lays things out in units should use the pending scale for that frame:
(or (nth-value 2 (win:window-pending-size window))
(win:window-scale window))
Your own render targets
cl-gfx recreates its swapchain for you, but not your depth buffers, offscreen targets or anything else sized to the window. Compare the size of the surface texture with theirs every frame and recreate them when they differ. See cl-gfx's Surfaces and Windows.
Asking for a size
The program asks for a size with set-window-size, in units, and the size
comes the same way as one the window system proposes, with a :resize
event, through the handshake:
(win:set-window-size window 1280 720)
;; => T
window-size stays the old size until the new one is adopted. The window
system may give the window another size, as one that fits the screen.
While it sizes the window itself, nothing is asked and set-window-size
returns nil:
- on Windows, while the window is maximised, minimised or fullscreen;
- on Wayland, while it is maximised, fullscreen or tiled, or the user is resizing it.
Until the window system has proposed the window's first size, the size
asked for replaces create-window's. On Wayland a window sizes itself by
drawing at the size, which xdg-shell allows, so a configure that comes
before the compositor has seen a frame at it, and proposes the old size,
does not undo it. A popup keeps its placement, as reposition-popup with
only a size places it.
A fullscreen window is sized by the window system; see Window Systems and Windows.
The :resize event
The :resize event is a notification: use it to schedule a redraw when you
draw only on demand, or to relayout. Do not use its fields as the window's
size. They describe one proposal, and the window system may send another
before you redraw. The window's pending size always holds the latest.
Scaling on Wayland
Where the scale comes from depends on what the compositor offers:
| The compositor offers | Scales | Notes |
|---|---|---|
wp_fractional_scale_v1 and wp_viewporter |
Any multiple of 1/120, such as 3/2 or 5/4 | :fractional-scale is in window-system-capabilities. Frames are drawn at the pixel size and shown at the window's size through a viewport. |
wl_surface version 6 or later |
Whole numbers | The compositor's preferred buffer scale for the window. |
| Neither | Whole numbers | The largest scale of the outputs the window is on, displays plugged in later included. Weston 16 is such a compositor. |
Which compositors offer what is on Wayland Backend.
On a compositor of the last kind, a window is on an output only once it has
been drawn. Its first frame goes out at scale 1, and the right scale
follows as a :resize event.
Scaling on Windows
A window's scale is the DPI of the display it is on over 96: 3/2 at 150%.
Windows sizes windows in pixels, so a window's size in its coordinates is
its pixel size over its scale, rounded. At a fractional scale,
window-pixel-size can then differ from the window's real pixel size by a
pixel, which the swapchain takes from Windows. A window moved to a display
of another scale keeps its size in its coordinates, and the new scale comes
as a :resize event. :fractional-scale is always in
window-system-capabilities.
In the Pyssel engine
Pyssel works in units throughout. engine-size is in units,
engine-scale and engine-pixel-size say the rest, and the 2D renderer
turns units into pixels with the display's scale, so its anti-aliasing
works in pixels and edges stay sharp.