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:

Lua
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:

Lua
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:

TEXT
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:

TEXT
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.

Lua
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:

Lua
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:

Lua
-- 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.