Skip to content

Fix HiDPI half-size regression: default 2d projection is logical points - #575

Open
pusewicz wants to merge 6 commits into
RandyGaul:masterfrom
pusewicz:hidpi-01-projection-fix
Open

Fix HiDPI half-size regression: default 2d projection is logical points#575
pusewicz wants to merge 6 commits into
RandyGaul:masterfrom
pusewicz:hidpi-01-projection-fix

Conversation

@pusewicz

Copy link
Copy Markdown
Contributor

3f5a828 wired cf_draw_on_app_canvas_resized to the canvas's pixel dimensions. The default canvas is window_points * pixel_scale, so on a 2x display the projection extent doubled and everything drawn in point space rendered at half size — from the second frame on, since the hook never refreshed mvp and reset_cam only latches it at end of frame. At 1x displays points and pixels coincide, which is why CI never saw it, and why it's invisible on most dev machines too.

s_canvas now hands the hook window points, and the hook refreshes mvp (mirroring cf_draw_projection) and the AA factor. When a refresh lands mid draw-list-recording or inside a push/pop pair it parks in pending_projection and reset_cam applies it at the frame boundary — applying immediately would bake the ortho into recorded draw lists (doubled again at replay) or be clobbered by the pop.

DPI bugs were untestable on 1x machines, so cf_app_force_pixel_scale (internal) pins pixel_scale through the real recreation path: the new test_hidpi suite readback-verifies the projection contract, the resize frame, one-shot cf_app_set_canvas_size behavior, and draw-list recording across a resize, at a forced 2x on any machine.

On this Retina Mac, master currently fails 20 test_draw_tiled cases; this branch is 346/346.

First of a 3-PR series (viewport/scissor scaling and 3d stroke thickness build on this one's test infra) — holding the rest until this lands so each diff stays reviewable on its own.

3f5a828 wired cf_draw_on_app_canvas_resized to the canvas's *pixel*
dimensions. The default canvas is window_points * pixel_scale, so on a
2x display the projection extent doubled and everything drawn in point
space rendered at half size -- from the second frame on, since the hook
never refreshed mvp and reset_cam only latches it at end of frame. At
1x displays points and pixels coincide, which is why CI never saw it.

s_canvas now hands the hook window points, and the hook refreshes mvp
(mirroring cf_draw_projection) and the AA factor. When a refresh lands
mid draw-list-recording or inside a push/pop pair it parks in
pending_projection and reset_cam applies it at the frame boundary --
applying immediately would bake the ortho into recorded draw lists
(doubled again at replay) or be clobbered by the pop.

DPI bugs were untestable on 1x machines, so cf_app_force_pixel_scale
(internal) pins pixel_scale through the real recreation path: the new
test_hidpi suite readback-verifies the projection contract, the resize
frame, one-shot cf_app_set_canvas_size behavior, and draw-list recording
across a resize, at a forced 2x on any machine. The hook validates NaN,
respects NO_HIGH_DPI and NO_GFX, and the shared test fixture sweeps a
leaked force. Canvas dimensions clamp to >= 1 against degenerate scales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019EzCXyMG8sBxLtDSTv2rFi
Copilot AI lite review requested due to automatic review settings August 10, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a HiDPI regression where the default 2D projection was refreshed using app-canvas pixel dimensions instead of window logical-point dimensions (causing point-space content to render at half size on 2x displays after the first frame). Adds deterministic HiDPI test coverage by introducing an internal pixel-scale override hook used by a new test_hidpi suite.

Changes:

  • Make default-projection refresh use window logical size (points), and refresh mvp + AA factor on canvas recreation (with safe deferral during draw-list recording / push-pop).
  • Add cf_app_force_pixel_scale (internal/test hook) and integrate it with the pixel-density refresh path.
  • Add a new test_hidpi suite and wire it into the test build and runner; update cf_app_set_size docs to clarify logical-point units.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/test_hidpi.cpp New suite that forces 2x pixel scale and readback-verifies projection/resize/draw-list behavior.
test/test_app_shared.cpp Sweeps any leaked forced pixel scale when reusing a test app instance.
test/main.cpp Registers and runs the new test_hidpi suite.
test/CMakeLists.txt Adds test_hidpi.cpp to the tests target sources.
src/internal/cute_draw_internal.h Adds pending-projection state and clarifies resized-hook contract (logical units).
src/internal/cute_app_internal.h Declares cf_app_force_pixel_scale and tracks pixel_scale_override in CF_App.
src/cute_input.cpp Skips real pixel-scale refresh while an override is active.
src/cute_draw.cpp Updates resized-hook behavior: logical extents, mid-frame safety, refresh mvp/AA, deferred apply.
src/cute_app.cpp Passes logical window size to draw resized hook; clamps recreated canvas pixel size; implements force-scale hook.
include/cute_app.h Updates cf_app_set_size docs to specify logical points and mention pixel-density behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/cute_draw.cpp
Comment on lines +5215 to +5234
// A custom cf_draw_projection is per-frame state and simply overrides this as usual.
if (!s_draw) return;
CF_ASSERT(w == app->w && h == app->h);
// Applying mid draw-list-recording would stomp the recording's identity space (baking
// the ortho into recorded geometry, doubled again at replay), and applying inside a
// cf_draw_push/pop pair would just be clobbered by the pop. Park the refresh; reset_cam
// applies it at the frame boundary.
if (s_draw->recording_list || s_draw->projection_stack.count() > 0) {
s_draw->pending_projection = true;
s_draw->pending_projection_w = w;
s_draw->pending_projection_h = h;
return;
}
s_draw->pending_projection = false;
s_draw->projection = ortho_2d(0, 0, (float)w, (float)h);
// Refresh mvp too, mirroring cf_draw_projection: reset_cam only re-latches it at end of
// frame, so without this the rest of the resize frame draws with the stale matrix. The
// AA factor divides by pixel_scale, so it is equally stale on a density change.
CF_MUL_M32_M32(s_draw->mvp, s_draw->projection, s_draw->cam_stack.last());
s_draw->set_aaf();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ecca88b: clamps only the value fed to ortho_2d (via a local ortho_w/ortho_h), so w/h still report the real logical size to callers and CF_ASSERT(w == app->w && h == app->h) is untouched.

Some platforms deliver a 0x0 logical window size mid-minimize/resize;
cf_ortho_2d divides by the extents, so a bare 0 poisons the projection
with inf. Only the value fed to ortho_2d is clamped -- callers still see
the real (possibly 0) logical size.

Addresses Copilot review comment on PR RandyGaul#575.
A WINDOW_RESIZED event reporting the size app->w/h already have (e.g. an
X11/Xvfb ConfigureNotify fired on window map with no actual geometry
change) was still treated as a recreation event, silently discarding an
active cf_app_set_canvas_size one-shot override. This is what failed
test_hidpi_one_shot_canvas_keeps_points_projection on Linux CI.

app->w/h and window_state.resized still update unconditionally --
cf_app_was_resized() must keep reporting true after a programmatic
cf_app_set_size, which relies on this same event arriving with matching
dimensions. Only the canvas recreate is gated on an actual size change.
CI's ubuntu/gcc and ubuntu/clang jobs still fail
test_hidpi_one_shot_canvas_keeps_points_projection after the
redundant-resize-event fix, so something else on Linux is still
recreating (or otherwise resizing) the one-shot canvas before the
readback. A bare size mismatch gives no way to tell what actually
happened; this dumps the app's canvas/window dims and pixel_scale at
the point of failure.
…t test

The diagnostic added in 9b391fb caught it: ubuntu/clang/static failed
with app_canvas=640x480, app_window=320x240, pixel_scale=2.0 -- the
canvas was rebuilt from a value that matches the CURRENT window/scale,
not a stale one, so af516a7's redundant-event guard doesn't apply here.

The shared test app's own cf_app_set_size (inside test_make_app) can
still have an SDL_SetWindowSize in flight when this test starts. On
X11 its ConfigureNotify confirmation arrives asynchronously, and when
a prior test resized the window more than once, it can arrive as a
stale-then-fresh burst -- each event looks like a real change relative
to the one just before it, so a same-value dedup guard can't tell it
apart from a genuine resize. Settling the window (sync + drain) before
establishing the one-shot override, instead of leaving it to land
wherever the test happens to poll events next, avoids the race
entirely without weakening what WINDOW_RESIZED does for a real,
externally-driven resize.
The Linux-only failure fixed in 190e1c8 was a timing race (inconsistent
across the prior 3 CI runs), so one green run isn't fully conclusive.
No admin rights on RandyGaul/cute_framework to use the Actions rerun
API, hence an empty commit to get a second independent run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants