Text Sharpness & HiDPI¶
WhatsCanvas rasterizes glyphs into a GPU glyph atlas and composites them as textured quads. This note explains how the engine keeps text crisp — including under zoom, scale, and high-DPI displays — and how to opt into HiDPI rendering.
Why text can look blurry (and what the engine does about it)¶
Glyphs are rasterized at an integer pixel size, so their atlas texels map 1:1 to screen pixels only when the glyph lands on an integer pixel boundary at its native resolution. Three independent effects break that mapping; the engine handles each automatically:
-
Fractional placement. If a glyph quad's device origin is fractional, the bilinear sampler averages neighbouring texels and a solid stem loses 20–49% of its coverage — the classic "washed / blurry" look. The engine snaps glyph quads to the device pixel grid whenever the current transform is a pure axis-aligned, unit-scale transform (identity / translation — the common UI case). Rotated or scaled text is left unsnapped.
-
Magnified low-resolution glyphs. If text is drawn under a scaled transform (for example a game that fits a design resolution to the window with
canvas.scale(s, s)), a logically-sized glyph would be bilinearly magnified. Instead, the engine reads the transform's effective device-space scale and rasterizes the glyph at that resolution, then draws it 1:1. Zoomed, scaled, and rotated text stay crisp. The effective pixel size is quantized (to bound atlas growth) and capped. -
High-DPI displays. See below —
setDevicePixelRatiofolds the display scale into the same effective-scale path.
HiDPI with setDevicePixelRatio¶
On a high-DPI display the OS reports a content scale (e.g. 2.0 on a "Retina" screen): the physical framebuffer is larger than the logical window. To render crisply, size the canvas to the physical framebuffer and tell it the ratio:
// After creating the window and loading GL:
int fbW = 0, fbH = 0;
glfwGetFramebufferSize(window, &fbW, &fbH);
float sx = 1.0f, sy = 1.0f;
glfwGetWindowContentScale(window, &sx, &sy);
const float dpr = sx > 0.0f ? sx : 1.0f;
auto canvas = wsc::Canvas::create(wsc::Canvas::Backend::OpenGL, 0, 0);
canvas->setSize(fbW, fbH); // physical framebuffer pixels
canvas->setDevicePixelRatio(dpr); // logical -> physical scale
canvas->initializeContext();
canvas->beginFrame();
// From here on, draw in LOGICAL coordinates:
canvas->drawText("Crisp on HiDPI", 24.0f, 40.0f, paint); // 24,40 are logical px
canvas->endFrame();
Semantics:
- The device pixel ratio is folded into the root transform, so
resetMatrix()restores aratio-scaled base. Draw in logical coordinates; everything — including text — renders at physical resolution. - Text automatically reuses the device-resolution rasterization path (effect 2 above) and stays sharp.
- The public matrix API stays logical:
setMatrix()composes onto the ratio base (so an absolutesetMatrixnever silently drops HiDPI),getMatrix()returns the logical matrix you built, andmapPoint()reports device pixels. - Default ratio is
1.0, a no-op — existing code is unaffected.
Handle window resizes by re-applying the size and ratio:
void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
glViewport(0, 0, width, height);
float sx = 1.0f, sy = 1.0f;
glfwGetWindowContentScale(window, &sx, &sy);
const float dpr = sx > 0.0f ? sx : 1.0f;
canvas->setSize(width, height); // physical
canvas->setDevicePixelRatio(dpr);
// lay out in logical units: width / dpr, height / dpr
}
The examples/game/* samples (Tetris and Racer) use exactly this
pattern: they lay the scene out in logical design units and set the device pixel
ratio from the window content scale.
Optional policy: gamma-correct text edges¶
Text anti-aliasing is currently blended in sRGB space, which makes AA edges a
little heavier than a gamma-correct blend would. Gamma-correct rendering is
available globally via Canvas::setGammaCorrect(true) but is off by default,
because turning it on changes all rendering (shapes, gradients, images) and
would regenerate every golden-image baseline. It is intentionally a separate
rendering-policy decision, not part of the text-sharpness fixes above.