Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 59 additions & 25 deletions include/cute_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ CF_API CF_DisplayOrientation CF_CALL cf_display_orientation(CF_DisplayID display
CF_ENUM(APP_OPTIONS_GFX_OPENGL_BIT, 1 << 11) \
/* @entry Starts the application with a debug mode graphics context. */ \
CF_ENUM(APP_OPTIONS_GFX_DEBUG_BIT, 1 << 12) \
/* @entry Disables the OS's high-pixel-density (Retina/HiDPI) backbuffer, forcing 1:1 logical-to-physical rendering. `cf_app_get_pixel_scale` will always return 1.0f. */ \
/* @entry Disables the OS's high-pixel-density (Retina/HiDPI) backbuffer, so the window starts with a 1:1 logical-to-physical backbuffer and an initial pixel scale of 1.0f. */ \
CF_ENUM(APP_OPTIONS_NO_HIGH_DPI_BIT, 1 << 13) \
/* @end */

Expand Down Expand Up @@ -400,43 +400,75 @@ CF_API void CF_CALL cf_app_show_window(void);
/**
* @function cf_app_get_display_scale
* @category app
* @brief Returns the OS's display scale for the window's current display.
* @remarks On some devices (e.g. Apple Retina or iOS) pixels are clustered in 4x4 packs and abstracted as a single pixel
* called a "point". The intent is for applications to work in points, and scale their UI elements by a factor of 2x
* to aid in readability. These devices have very small pixels. Most of the time you should ignore dpi and let the OS
* handle this. CF enables DPI settings by default, but, you can see if this function returns 2.0f to let you know if
* pixels are clustered for you under the hood.
* @related cf_app_set_size cf_app_get_position cf_app_set_position cf_app_get_width cf_app_get_height cf_app_get_display_scale cf_app_display_scale_was_changed
* @brief Returns the display scale the OS wants for the app's window: the factor converting logical points to physical pixels.
* @remarks E.g. 2.0f on a 2x Retina display, or 1.5f on a Windows desktop at 150% scaling -- consistent across platforms.
* Only the OS changes this value (moving the window to a different display, or the user changing display settings),
* and a change raises `cf_app_display_scale_was_changed`. It is informational: CF never applies it for you. Pass it
* to `cf_app_update_display` to render at the display's native crispness, or ignore it to keep a fixed scale.
* @related cf_app_display_scale_was_changed cf_app_update_display cf_app_get_pixel_scale cf_app_get_size
*/
CF_API float CF_CALL cf_app_get_display_scale(void);

/**
* @function cf_app_display_scale_was_changed
* @category app
* @brief Returns true if the display scale changed, such as moving from one screen to another.
* @related cf_app_get_display_scale cf_app_display_scale_was_changed
* @brief Returns true if the display scale changed, such as the window moving to a screen with a different scale.
* @remarks The standard reaction is `cf_app_update_display(cf_app_get_display_scale())`; see the hidpi sample.
* @related cf_app_get_display_scale cf_app_update_display
*/
CF_API bool CF_CALL cf_app_display_scale_was_changed(void);

/**
* @function cf_app_get_pixel_scale
* @category app
* @brief Returns the number of physical pixels per logical point for the app's window.
* @remarks This is the ratio CF actually renders at internally -- e.g. 2.0f on a 2x Retina display. Unlike
* `cf_app_get_display_scale` (the OS's suggested UI content scale, which is informational only), this value
* directly reflects the backbuffer/canvas pixel density and is what you'd multiply a logical size by to
* get physical pixels. Returns 1.0f if `CF_APP_OPTIONS_NO_HIGH_DPI_BIT` was passed to `cf_make_app`.
* @related cf_app_get_display_scale cf_app_get_size cf_app_get_canvas_width cf_app_get_canvas_height
* @brief Returns the pixel scale used for rendering: physical pixels per logical point.
* @remarks This is how CF scales fonts and shapes: it drives antialiasing width and glyph rasterization density,
* and is what you'd multiply a logical size by to size a pixel-perfect render target. It is a
* user-controlled value, like the window or canvas size: it starts at the display scale the OS wants
* (e.g. 2.0f on a 2x Retina display, or 1.0f if `CF_APP_OPTIONS_NO_HIGH_DPI_BIT` was passed to
* `cf_make_app`) and afterwards changes only through `cf_app_set_pixel_scale` or `cf_app_update_display`.
* @related cf_app_set_pixel_scale cf_app_update_display cf_app_get_display_scale cf_app_get_size cf_app_get_canvas_width cf_app_get_canvas_height
*/
CF_API float CF_CALL cf_app_get_pixel_scale(void);

/**
* @function cf_app_set_pixel_scale
* @category app
* @brief Sets the pixel scale (physical pixels per logical point) used for rendering.
* @param scale The new pixel scale. Must be greater than zero; other values are ignored.
* @remarks Only the scale value itself changes: antialiasing width adjusts immediately and text re-rasterizes at
* the new density, but the app canvas keeps its current size -- `cf_app_set_canvas_size` is a separate
* call (or use `cf_app_update_display` to do both plus the projection). The scale can be arbitrary, or
* follow the reported `cf_app_get_display_scale`. The draw API stays in logical points throughout.
* @related cf_app_get_pixel_scale cf_app_update_display cf_app_set_canvas_size cf_app_get_display_scale
*/
CF_API void CF_CALL cf_app_set_pixel_scale(float scale);

/**
* @function cf_app_update_display
* @category app
* @brief The all-in-one display update: sets the pixel scale, resizes the app canvas to window size times scale, and rebuilds the default 2d projection from the logical window size.
* @param scale The pixel scale to apply. Must be greater than zero; other values are ignored.
* @remarks Just a helper bringing `cf_app_set_pixel_scale`, `cf_app_set_canvas_size(window_w * scale, window_h * scale)`,
* and `cf_draw_projection` (spanning the logical window size) together. Use it on both resize and scale change:
* when `cf_app_was_resized` fires pass `cf_app_get_pixel_scale` to keep the current scale, and when
* `cf_app_display_scale_was_changed` fires pass `cf_app_get_display_scale` to follow the display. The scale can
* also be arbitrary -- e.g. a forced 2.0f to test HiDPI rendering on a normal monitor. It overwrites a custom
* `cf_draw_projection` -- re-apply yours after, if you use one. See the hidpi sample.
* @related cf_app_set_pixel_scale cf_app_get_pixel_scale cf_app_get_display_scale cf_app_set_canvas_size cf_app_display_scale_was_changed
*/
CF_API void CF_CALL cf_app_update_display(float scale);

/**
* @function cf_app_set_size
* @category app
* @brief Sets the size of the window in pixels.
* @param w The width of the window in pixels.
* @param h The height of the window in pixels.
* @related cf_app_get_size cf_app_get_position cf_app_set_position
* @brief Sets the size of the window in logical points.
* @param w The width of the window in logical points.
* @param h The height of the window in logical points.
* @remarks Only the window changes. The app canvas and the default 2d projection keep their current size --
* update them alongside if desired, e.g. `cf_app_set_canvas_size` and `cf_draw_projection`; see the
* hidpi sample for the recipe.
* @related cf_app_get_size cf_app_get_position cf_app_set_position cf_app_set_canvas_size
Comment on lines 463 to +471
*/
CF_API void CF_CALL cf_app_set_size(int w, int h);

Expand Down Expand Up @@ -729,11 +761,11 @@ CF_API CF_Canvas CF_CALL cf_app_get_canvas(void);
* @param h The height in pixels to resize the canvas to.
* @remarks Be careful about calling this function, as it will invalidate any old references from `cf_app_get_canvas`.
*
* This is a one-shot override. The app's canvas is automatically recreated at window size (in points) times
* `cf_app_get_pixel_scale` on every canvas recreation event -- a window resize, moving to a display with a
* different pixel density, `cf_app_set_size`, or `cf_app_set_msaa` -- so a custom size lasts only until the
* next such event. For a persistent fixed-resolution render target (e.g. a retro/pixel-art look) make your
* own canvas with `cf_make_canvas` and draw it scaled-up with `cf_draw_canvas`; see the canvas_modes sample.
* The canvas keeps this size until the next `cf_app_set_canvas_size` call -- nothing resizes it behind
* your back. It is created once at startup at window size (in points) times the display's pixel density;
* after that, window resizes and display density changes only raise `cf_app_was_resized` /
* `cf_app_display_scale_was_changed`, and resizing the canvas in response is up to you (see the hidpi sample
* for the recipe).
Comment on lines +764 to +768

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update stale HiDPI docs for the new resize contract

With this new persistent-canvas contract, the published docs still tell users the opposite: docs/topics/hidpi.md:24-26 says CF recreates the app canvas on resize/scale changes and cf_app_set_canvas_size is one-shot, docs/topics/application_window.md:100 says the same, and include/cute_draw.h:2214-2215 still says CF resets the projection on resize. Users following those pages won't call cf_app_update_display() after cf_app_was_resized(), so their resizable windows keep the old canvas/projection and render stretched or clipped.

Useful? React with 👍 / 👎.

* @related cf_app_get_canvas cf_app_get_canvas_width cf_app_get_canvas_height cf_app_get_pixel_scale cf_app_set_canvas_blit_filter cf_make_canvas cf_draw_canvas
*/
CF_API void CF_CALL cf_app_set_canvas_size(int w, int h);
Expand Down Expand Up @@ -973,6 +1005,8 @@ CF_INLINE int app_get_height() { return cf_app_get_height(); }
CF_INLINE float app_get_display_scale() { return cf_app_get_display_scale(); }
CF_INLINE bool app_display_scale_was_changed() { return cf_app_display_scale_was_changed(); }
CF_INLINE float app_get_pixel_scale() { return cf_app_get_pixel_scale(); }
CF_INLINE void app_set_pixel_scale(float scale) { cf_app_set_pixel_scale(scale); }
CF_INLINE void app_update_display(float scale) { cf_app_update_display(scale); }
CF_INLINE void app_center_window() { cf_app_center_window(); }
CF_INLINE bool app_was_resized() { return cf_app_was_resized(); }
CF_INLINE bool app_was_moved() { return cf_app_was_moved(); }
Expand Down
80 changes: 60 additions & 20 deletions samples/hidpi.c
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
/*
hidpi.c -- HiDPI / Retina rendering visual verification.
hidpi.c -- HiDPI / Retina rendering, the manual way.
Comment thread
pusewicz marked this conversation as resolved.

Cute Framework renders its default canvas at physical resolution (logical
size scaled by `cf_app_get_pixel_scale()`), so text and shapes stay crisp
on Retina/HiDPI displays without any extra work from the user. This sample
is a quick visual check of that: run it on a HiDPI display and glyph edges
and shape antialiasing should look sharp, not soft/blurry.
The pixel scale (physical pixels per logical point) is a plain user-controlled
value, like the window or canvas size. At startup CF creates the app canvas
once at window_points * the display scale the OS wants and sets the default 2d
projection once from the logical window size -- and never touches either again.
Reacting to window resizes and display-density changes is YOUR code, and
cf_app_update_display (= set the scale, resize the canvas to match the
window, rebuild the projection) is the whole recipe:

See docs/topics/hidpi.md for the full point/pixel model, and
samples/canvas_modes.c for an interactive tour of app-canvas sizing
(custom scale, forced 1x, and fixed-resolution retro canvases).
if (cf_app_was_resized()) cf_app_update_display(cf_app_get_pixel_scale());
if (cf_app_display_scale_was_changed() && tracking_the_display) {
cf_app_update_display(cf_app_get_display_scale());
}

A fixed-size, non-resizable window on one display needs NONE of this -- the
startup defaults are already correct.

Interactivity: press N to follow the OS display scale (the default),
or 1 / 2 / 4 to force a 1x / 2x / 4x pixel scale -- forcing a value is also
how you test HiDPI behavior on a non-HiDPI monitor. Resize the window to
watch the recipe keep everything crisp.

What it draws:
- Text at three sizes (12px / 24px / 48px) to eyeball glyph
crispness at different scales.
- A row of basic SDF shapes (filled circle, outlined circle, lines
of varying thickness including a thin ~1px line, a filled rounded
box, and an outlined triangle) to eyeball shape edge antialiasing.
- A live readout of `cf_app_get_pixel_scale()` alongside the physical
canvas size, so the current display's HiDPI scale factor is visible
at a glance.

No interactivity beyond closing the window; no external assets needed.
- A live readout of the applied pixel scale and the OS display scale alongside
the physical canvas size.
*/

#include <cute.h>
Expand Down Expand Up @@ -54,13 +62,27 @@ int main(int argc, char* argv[])
cf_sprite_play(&sprite, "idle");
sprite.scale = cf_v2(3.0f, 3.0f);

// true = follow the OS display scale; false = a forced 1x/2x/4x scale.
bool track_display = true;

while (cf_app_is_running()) {
cf_app_update(NULL);

// Scale mode switching. Forcing a scale on purpose is exactly the same call the
// engine-side recipe uses -- there is no separate "override" concept.
if (cf_key_just_pressed(CF_KEY_N)) { track_display = true; cf_app_update_display(cf_app_get_display_scale()); }
if (cf_key_just_pressed(CF_KEY_1)) { track_display = false; cf_app_update_display(1.0f); }
if (cf_key_just_pressed(CF_KEY_2)) { track_display = false; cf_app_update_display(2.0f); }
if (cf_key_just_pressed(CF_KEY_4)) { track_display = false; cf_app_update_display(4.0f); }

// The manual-model recipe: window resized -> rebuild canvas + projection at the
// current scale. Display scale changed (moved to another monitor) -> update to
// the new scale, but only in follow mode.
if (cf_app_was_resized()) {
// Nothing special to handle here -- part of the point of this
// sample is to observe how resizing/HiDPI scaling affects
// rendering crispness.
cf_app_update_display(cf_app_get_pixel_scale());
}
if (cf_app_display_scale_was_changed() && track_display) {
cf_app_update_display(cf_app_get_display_scale());
}

cf_push_font("Calibri");
Expand All @@ -79,19 +101,37 @@ int main(int argc, char* argv[])
cf_pop_font_size();

// -- Live pixel-scale readout --
char pixel_scale_buf[128];
char pixel_scale_buf[192];
float pixel_scale = cf_app_get_pixel_scale();
int physical_w = cf_app_get_canvas_width();
int physical_h = cf_app_get_canvas_height();
snprintf(
pixel_scale_buf, sizeof(pixel_scale_buf),
"pixel_scale: %.2fx (physical canvas: %dx%d)",
pixel_scale, physical_w, physical_h
"pixel_scale: %.2fx %s (display: %.2fx, physical canvas: %dx%d) -- press N/1/2/4",
pixel_scale, track_display ? "[display]" : "[forced]",
cf_app_get_display_scale(), physical_w, physical_h
);
cf_push_font_size(12);
draw_text_centered(pixel_scale_buf, cf_v2(0, 300));
cf_pop_font_size();

// -- Live mouse-coordinate readout -- screen space is raw, top-left-origin,
// y-down input; world space is what cf_screen_to_world hands back, matching
// the space cf_draw_text etc. draw into. If HiDPI translation is correct, the
// world coordinate should track the cursor 1:1 over the canvas regardless of
// pixel_scale.
char mouse_buf[128];
CF_V2 mouse_screen = cf_v2(cf_mouse_x(), cf_mouse_y());
CF_V2 mouse_world = cf_screen_to_world(mouse_screen);
snprintf(
mouse_buf, sizeof(mouse_buf),
"mouse: screen (%.0f, %.0f) -> world (%.0f, %.0f)",
mouse_screen.x, mouse_screen.y, mouse_world.x, mouse_world.y
);
cf_push_font_size(12);
draw_text_centered(mouse_buf, cf_v2(0, 300 - 18));
cf_pop_font_size();

cf_draw_pop_color();

cf_draw_push_color(cf_make_color_rgb(120, 130, 150));
Expand Down
55 changes: 36 additions & 19 deletions src/cute_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,6 @@ static void s_canvas(int w, int h)
app->offscreen_canvas = cf_make_canvas(params);
app->canvas_w = w;
app->canvas_h = h;
cf_draw_on_app_canvas_resized(w, h);
}

void cf_app_recreate_default_canvas_if_needed()
{
int w = (int)CF_ROUNDF(app->w * app->pixel_scale);
int h = (int)CF_ROUNDF(app->h * app->pixel_scale);
s_canvas(w, h);
}

CF_Result cf_make_app(const char* window_title, CF_DisplayID display_id, int x, int y, int w, int h, CF_AppOptionFlags options, const char* argv0)
Expand Down Expand Up @@ -305,10 +297,12 @@ CF_Result cf_make_app(const char* window_title, CF_DisplayID display_id, int x,
if (options & CF_APP_OPTIONS_RESIZABLE_BIT) flags |= SDL_WINDOW_RESIZABLE;
if (options & CF_APP_OPTIONS_HIDDEN_BIT) flags |= (SDL_WINDOW_HIDDEN | SDL_WINDOW_MINIMIZED);

float creation_content_scale = SDL_GetDisplayContentScale(display_id ? display_id : SDL_GetPrimaryDisplay());
if (creation_content_scale <= 0) creation_content_scale = 1.0f;
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING, window_title);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, w);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, h);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, (int)CF_ROUNDF(w * creation_content_scale));
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, (int)CF_ROUNDF(h * creation_content_scale));
Comment on lines +304 to +305

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align cf_make_app docs with logical-point sizing

After this change, cf_make_app treats w/h as logical points and multiplies by the content scale before creating the SDL window. On Windows/X11 at 150% scaling, callers following include/cute_app.h:224-225 (pixels) who request 640x480 now get a 960x720 physical window; either the constructor docs/examples need to move to logical points along with cf_app_set_size, or this scaling should not happen here.

Useful? React with 👍 / 👎.

SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER, flags);
if (options & CF_APP_OPTIONS_WINDOW_POS_CENTERED_BIT) {
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED_DISPLAY(display_id));
Expand Down Expand Up @@ -341,8 +335,8 @@ CF_Result cf_make_app(const char* window_title, CF_DisplayID display_id, int x,
if (window) {
SDL_GetWindowPosition(app->window, &app->x, &app->y);
app->display_scale = SDL_GetWindowDisplayScale(app->window);
app->pixel_scale = window ? SDL_GetWindowPixelDensity(app->window) : 1.0f;
if (app->pixel_scale <= 0.0f) app->pixel_scale = 1.0f;
if (app->display_scale <= 0.0f) app->display_scale = 1.0f;
app->pixel_scale = app->display_scale;
if (options & CF_APP_OPTIONS_NO_HIGH_DPI_BIT) app->pixel_scale = 1.0f;
}
::app = app;
Expand All @@ -361,7 +355,7 @@ CF_Result cf_make_app(const char* window_title, CF_DisplayID display_id, int x,
cf_load_internal_shaders();
cf_make_draw();

cf_app_recreate_default_canvas_if_needed();
s_canvas((int)CF_ROUNDF(app->w * app->pixel_scale), (int)CF_ROUNDF(app->h * app->pixel_scale));

// Create the default font.
make_font_from_memory(calibri_data, calibri_sz, "Calibri");
Expand Down Expand Up @@ -635,6 +629,12 @@ void cf_app_show_window()
SDL_ShowWindow(app->window);
}

float cf_app_get_content_scale()
{
float scale = app->window ? SDL_GetDisplayContentScale(SDL_GetDisplayForWindow(app->window)) : 1.0f;
return scale > 0 ? scale : 1.0f;
}

float cf_app_get_display_scale()
{
return app->display_scale;
Expand All @@ -650,16 +650,33 @@ float cf_app_get_pixel_scale()
return app->pixel_scale;
}

void cf_app_set_pixel_scale(float scale)
{
if (!(scale > 0)) return;
if (scale == app->pixel_scale) return;
app->pixel_scale = scale;
cf_draw_on_pixel_scale_changed();
}

void cf_app_update_display(float scale)
{
if (!(scale > 0)) return;
cf_app_set_pixel_scale(scale);
if (app->gfx_enabled) {
cf_app_set_canvas_size((int)CF_ROUNDF(app->w * scale), (int)CF_ROUNDF(app->h * scale));
cf_draw_projection(cf_ortho_2d(0, 0, (float)app->w, (float)app->h));
}
}

void cf_app_set_size(int w, int h)
{
SDL_SetWindowSize(app->window, w, h);
// Public sizes are logical points; SDL_SetWindowSize wants raw window coordinates
// (identical on macOS, points * content scale on Windows/X11).
float cs = cf_app_get_content_scale();
SDL_SetWindowSize(app->window, (int)CF_ROUNDF(w * cs), (int)CF_ROUNDF(h * cs));
app->w = w;
app->h = h;
app->sync_window = true;
// Recreate the app canvas now rather than waiting for the resize event: hidden windows
// don't reliably deliver one, and a caller who set the size expects the canvas (and the
// default 2d projection that tracks it) to match immediately.
cf_app_recreate_default_canvas_if_needed();
}

void cf_app_get_position(int* x, int* y)
Expand Down Expand Up @@ -773,7 +790,7 @@ bool cf_app_set_msaa(int sample_count)

if (supported && app->sample_count != sample_count) {
app->sample_count = sample_count;
cf_app_recreate_default_canvas_if_needed();
s_canvas(app->canvas_w, app->canvas_h);
}

return supported;
Expand Down
Loading
Loading