Window scaling
A game window can have more pixels than its size suggests. A window that is 800 units wide may contain 1600 pixels on a display set to 200% scaling. If a game treats both numbers as the same thing, its interface becomes tiny or its finished image becomes blurry.
Natiny lets you choose which result you want. In this guide, we will keep 2D coordinates comfortable to use and make text and shapes stay sharp on a high-density display.
Why one window has two sizes
The operating system describes an on-screen size in units that remain useful at different display densities. The graphics device, however, must produce an actual colour for every pixel in the window.
The display scale is the relationship between those sizes. A scale of 1
means 100%. A scale of 2 means 200%: one unit of on-screen size covers two
pixels across and two pixels down.
natiny.window.get_scale reports this factor:
local scale = natiny.window.get_scale(window)
print("Display scale: " .. scale)
Natiny calls the numbers passed to 2D drawing commands drawing units. The window's scale mode decides how those units reach its pixels.
Choose how Natiny produces the frame
Set the mode after creating the window and before drawing a frame:
local window = natiny.window.create("Scaled 2D", 800, 600)
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
natiny.window.set_scale_mode provides
three modes. Their names describe the result:
SCALE_MODE_PIXELS draws pixel for pixel
One drawing unit is one window pixel. Coordinate 100 reaches pixel 100,
even when the display scale is 2.
This is the default because it preserves the behaviour of applications written before Natiny had scale modes. It is also useful when code deliberately works in exact pixel coordinates. On a dense display, the same 16-pixel text occupies less physical space and can appear smaller.
SCALE_MODE_STRETCH enlarges the finished frame
Natiny draws the 2D frame into a smaller off-screen image, then stretches that
image over the window. At scale 2, coordinate 100 reaches pixel 200, but
the pixels in the smaller image are enlarged too. Text and diagonal edges can
look blurry.
This is useful when enlarging the whole finished image is the intended visual style. The path looks like this:
2D drawing -> smaller frame -> stretch to window pixels
SCALE_MODE_HIDPI keeps the full detail
HiDPI means high pixel density: the display has extra pixels available for the same on-screen size. In this mode, Natiny enlarges 2D coordinates but draws directly into every window pixel. It also rasterizes fonts at the display scale, so their outlines remain sharp.
At scale 2, coordinate 100 still reaches pixel 200, but there is no small
finished image to stretch:
2D drawing coordinates x display scale -> full-size window frame
Use this mode for a normally sized, sharp 2D interface.
Draw a sharp interface
Set the mode before creating fonts. A later mode change is supported, but it may rebuild live font atlases.
local natiny = require("natiny")
assert(natiny.backend.init())
local window = natiny.window.create("Sharp interface", 800, 600)
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
local font_data = natiny.resource.load("data/fonts/roboto.ttf")
local font = natiny.font.create(font_data, 24)
natiny.backend.loop(function(dt)
natiny.window.bind(window, function()
natiny.render.clear(0.05, 0.07, 0.10, 1)
natiny.render.set_color(0.18, 0.48, 0.82, 1)
natiny.render.rectangle(40, 40, 320, 72)
natiny.render.set_color(1, 1, 1, 1)
natiny.render.text(font, 60, 64, "Sharp at the same size")
end)
end)
Run the program on displays using 100% and 200% scaling. The rectangle still uses the same drawing coordinates and occupies the expected on-screen size. On the denser display, Natiny uses the extra pixels to produce smoother edges and text.
The window size and mouse coordinates follow the selected drawing units too. This keeps layout and input in agreement:
local width = natiny.window.get_width(window)
local mouse_x = natiny.input.get_mouse_x()
-- Both values use the same horizontal coordinate system as drawing.
In SCALE_MODE_PIXELS, width is the number of window pixels. In
SCALE_MODE_STRETCH and SCALE_MODE_HIDPI, it is the width used by the scaled
2D drawing commands.
Change the mode outside the frame
The drawing target and coordinate system are fixed when the window is bound.
For that reason, Natiny rejects a mode change made from inside
natiny.window.bind:
-- Correct: the window is not bound yet.
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_HIDPI)
natiny.window.bind(window, function()
-- Draw the frame here. Do not change its scale mode here.
end)
Choose the mode during setup rather than setting it every frame. Switching can
rebuild font atlases and allocate or release the off-screen image used by
SCALE_MODE_STRETCH.
The selected mode affects 2D window drawing, its viewport and scissor coordinates, window dimensions, and mouse coordinates. It does not scale 3D drawing or user-created surfaces. Surface dimensions always remain pixels.
The browser is a display like any other
A web build answers the same question the same way. A browser lays the page
out in CSS pixels and reports how many device pixels stand behind each of them;
Natiny reads that as the display scale, so a Retina screen reports 2, a
Windows display at 125% reports 1.25, and a page zoomed with Ctrl and +
reports whatever the zoom makes it. The canvas is given one drawing buffer
pixel per device pixel, and the scale, the window size and the font atlases
follow the zoom while the program runs.
Pick the mode for the result, exactly as on the desktop: HIDPI for a
normally sized, sharp interface, STRETCH for the same coordinates at the
cost of sharpness when the GPU is the constraint, and PIXELS when one
drawing unit must be one device pixel.
Pick the mode by the result you need
Use SCALE_MODE_PIXELS when exact window pixels are part of the design. Use
SCALE_MODE_STRETCH when you intentionally want to enlarge a completed
lower-resolution frame. Use SCALE_MODE_HIDPI for sharp 2D graphics that keep
the same useful coordinate sizes across display densities.
All three modes answer the same underlying question: whether display scaling should be ignored, applied after the frame is complete, or applied while the full-resolution frame is drawn. Once that choice is explicit, drawing, window-size queries, and mouse input stay in one consistent coordinate system.