Fix HiDPI half-size regression: default 2d projection is logical points - #575
Fix HiDPI half-size regression: default 2d projection is logical points#575pusewicz wants to merge 6 commits into
Conversation
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
There was a problem hiding this comment.
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_hidpisuite and wire it into the test build and runner; updatecf_app_set_sizedocs 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.
| // 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(); |
There was a problem hiding this comment.
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.
3f5a828 wired
cf_draw_on_app_canvas_resizedto the canvas's pixel dimensions. The default canvas iswindow_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 refreshedmvpandreset_camonly 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_canvasnow hands the hook window points, and the hook refreshesmvp(mirroringcf_draw_projection) and the AA factor. When a refresh lands mid draw-list-recording or inside a push/pop pair it parks inpending_projectionandreset_camapplies 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) pinspixel_scalethrough the real recreation path: the newtest_hidpisuite readback-verifies the projection contract, the resize frame, one-shotcf_app_set_canvas_sizebehavior, and draw-list recording across a resize, at a forced 2x on any machine.On this Retina Mac, master currently fails 20
test_draw_tiledcases; 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.