Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 19 additions & 14 deletions include/cute_app.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ typedef enum CF_AppOptionFlagBits
* @param display_index The index of the display to spawn upon. Set this to zero for the primary display. See `cf_get_display_list`.
* @param x The x position of the window.
* @param y The y position of the window.
* @param w The width of the window in pixels.
* @param h The height of the window in pixels.
* @param w The width of the window in points (see `cf_app_get_pixel_scale`; a point is a pixel unless the OS clusters pixels).
* @param h The height of the window in points.
* @param options 0 by default; a bitmask of `app_options` flags.
* @param argv0 The first argument passed to your main function in the `argv` parameter.
* @return Returns any errors on failure as a `CF_Result`.
Expand Down Expand Up @@ -400,12 +400,11 @@ 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.
* @brief Returns the OS's suggested UI scale for the window's current display, e.g. 1.5f on a Windows desktop at 150%.
* @remarks This is a hint about how large the user would like UI to appear. It is informational only: CF never applies it.
* On devices that cluster pixels into points (Apple Retina, iOS) it equals `cf_app_get_pixel_scale`, which CF
* does honor automatically. On Windows/Linux desktops a pixel stays a pixel regardless of this value; honor it
* yourself (e.g. by picking a larger window size or UI scale at startup) if your app should follow the OS setting.
* @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
*/
CF_API float CF_CALL cf_app_get_display_scale(void);
Expand Down Expand Up @@ -433,9 +432,11 @@ CF_API float CF_CALL cf_app_get_pixel_scale(void);
/**
* @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.
* @brief Sets the size of the window in points.
* @param w The width of the window in points (a point is a pixel unless the OS clusters pixels; see `cf_app_get_pixel_scale`).
* @param h The height of the window in points.
* @remarks The app canvas and the default 2d projection follow automatically: the canvas is recreated at
* `w * cf_app_get_pixel_scale()` by `h * cf_app_get_pixel_scale()` pixels and the projection spans `w` by `h` points.
* @related cf_app_get_size cf_app_get_position cf_app_set_position
*/
CF_API void CF_CALL cf_app_set_size(int w, int h);
Expand Down Expand Up @@ -729,11 +730,15 @@ 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`.
*
* The default 2d projection is rebuilt to span `w` by `h`, so the draw API works in the canvas's own pixels:
* a 320x180 retro target draws as a 320x180 world and is blitted up to the window (see `cf_app_set_canvas_blit_filter`).
*
* 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.
* different pixel density, `cf_app_set_size`, or `cf_app_set_msaa` -- and the projection goes back to spanning
* the window in points, 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`, or re-apply the size when `cf_app_was_resized`; see the canvas_modes sample.
* @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
3 changes: 2 additions & 1 deletion src/cute_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,14 @@ 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_draw_on_app_canvas_resized(app->w, app->h); // The draw API stays in window points.
}

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 @@ -787,6 +787,7 @@ CF_Canvas cf_app_get_canvas()
void cf_app_set_canvas_size(int w, int h)
{
s_canvas(w, h);
cf_draw_on_app_canvas_resized(w, h); // An explicit canvas size is drawn in its own pixels (a 320x180 retro target).
}

int cf_app_get_canvas_width()
Expand Down
22 changes: 16 additions & 6 deletions src/cute_draw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ void cf_make_draw()
{
s_draw = CF_NEW(CF_Draw);
s_draw->path_image_id_gen = CF_PATH_ID_RANGE_LO;
s_draw->projection = ortho_2d(0, 0, (float)app->w, (float)app->h);
s_draw->projection = s_draw->default_projection = ortho_2d(0, 0, (float)app->w, (float)app->h);
s_draw->reset_cam();
s_draw->uniform_arena = cf_make_arena(32, CF_MB);

Expand Down Expand Up @@ -5319,11 +5319,21 @@ static void s_process_command(CF_Canvas canvas, CF_Command* cmd, CF_Command* nex
// one frame of extra draw calls for brand-new content, instead of N defrags every frame.
void cf_draw_on_app_canvas_resized(int w, int h)
{
// The default 2d projection tracks the app canvas 1:1. It used to be computed once at
// startup and never again, so any resize (cf_app_set_size or a user dragging a resizable
// window) silently rescaled every world-space 2d draw. Refresh it with the canvas; a
// custom cf_draw_projection is per-frame state and simply overrides this as usual.
if (s_draw) s_draw->projection = ortho_2d(0, 0, (float)w, (float)h);
if (!s_draw) return;
CF_M3x2 old = s_draw->default_projection;
CF_M3x2 next = ortho_2d(0, 0, (float)w, (float)h);
s_draw->default_projection = next;
s_draw->projection = next;
// A resize from inside a cf_draw_push scope (a settings menu applying a resolution) must
// survive the matching pop: the saved copies of the old default become the new one, while
// a user's own saved projection is left alone.
for (int i = 0; i < s_draw->projection_stack.size(); ++i) {
if (!CF_MEMCMP(&s_draw->projection_stack[i], &old, sizeof(old))) s_draw->projection_stack[i] = next;
}
// Mirrors cf_draw_projection so a mid-frame cf_app_set_size takes effect immediately, and
// re-derives the AA factor since pixel_scale may be what changed.
CF_MUL_M32_M32(s_draw->mvp, next, s_draw->cam_stack.last());
s_draw->set_aaf();
}

void cf_atlas_defrag_once()
Expand Down
9 changes: 7 additions & 2 deletions src/internal/cute_draw_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ struct CF_Draw
Cute::Array<CF_M3x2> projection_stack;
float aaf = 0;
CF_M3x2 projection;
CF_M3x2 default_projection; // The last projection CF set itself; cf_draw_on_app_canvas_resized tells it apart from a user's.
CF_M3x2 mvp;
void reset_cam();
void set_aaf();
Expand Down Expand Up @@ -469,8 +470,12 @@ void cf_draw3d_free_cmd(CF_Command* cmd);
// Runs the atlas defrag at most once per frame (see CF_Draw::defragged_this_frame).
void cf_atlas_defrag_once();

// Called when the app's offscreen canvas is recreated (window resize / cf_app_set_size):
// refreshes the default 2d projection, which tracks the app canvas 1:1.
// Called when the app's offscreen canvas is recreated: rebuilds the default 2d projection to span
// w by h, refreshes the mvp + AA factor so the very next draw sees it, and swaps the new default
// into any cf_draw_push-saved copies of the old one so a pop cannot restore a stale size. The
// automatic path (window resize, density change, cf_app_set_size, cf_app_set_msaa) passes the
// window size in LOGICAL POINTS -- never the canvas's pixel size, which halves everything on a 2x
// display -- and cf_app_set_canvas_size passes its own size so a retro canvas draws in its pixels.
void cf_draw_on_app_canvas_resized(int w, int h);

// Called by cf_render_layers_to before the canvas (and its render pass) is applied: stages
Expand Down
76 changes: 76 additions & 0 deletions test/test_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,80 @@ TEST_CASE(test_app_set_canvas_size_is_one_shot)
return true;
}

TEST_CASE(test_app_default_projection_is_logical_points_at_2x)
{
REQUIRE(!cf_is_error(cf_make_app(NULL, 0, 0, 0, 200, 100, CF_APP_OPTIONS_HIDDEN_BIT | CF_APP_OPTIONS_NO_AUDIO_BIT, NULL)));
OwnedAppGuard guard;

// Pretend this is a 2x (Retina-class) display: the OS clusters 2x2 pixels into one point.
// Forcing it here means a 1x CI machine exercises the same path a real 2x display takes.
app->pixel_scale = 2.0f;

// A recreation event rebuilds the canvas at window * pixel_scale...
cf_app_set_size(256, 128);
REQUIRE(cf_app_get_canvas_width() == 512);
REQUIRE(cf_app_get_canvas_height() == 256);

// ...but the default 2d projection must keep spanning the window in POINTS, not canvas
// pixels. The regression: it was rebuilt from the canvas size, so on a 2x display the
// top-left of the screen mapped to (-256, 128) and every draw rendered at half size.
CF_V2 top_left = cf_screen_to_world(cf_v2(0, 0));
REQUIRE(CF_FABSF(top_left.x - -128.0f) < 0.01f);
REQUIRE(CF_FABSF(top_left.y - 64.0f) < 0.01f);
CF_V2 bottom_right = cf_screen_to_world(cf_v2(256, 128));
REQUIRE(CF_FABSF(bottom_right.x - 128.0f) < 0.01f);
REQUIRE(CF_FABSF(bottom_right.y - -64.0f) < 0.01f);

// And it takes effect immediately, mid-frame, under a pushed camera: the mvp is refreshed
// like cf_draw_projection does, so a translate still composes on top of the new projection.
cf_draw_push();
cf_draw_translate(10, 0);
cf_app_set_size(300, 150);
CF_V2 origin = cf_world_to_screen(cf_v2(0, 0));
REQUIRE(CF_FABSF(origin.x - 160.0f) < 0.01f);
REQUIRE(CF_FABSF(origin.y - 75.0f) < 0.01f);
cf_draw_pop();

// The pop restores the projection saved at push time -- which must be the NEW default, not
// the stale pre-resize one (a settings menu applying a resolution from inside a push scope).
top_left = cf_screen_to_world(cf_v2(0, 0));
REQUIRE(CF_FABSF(top_left.x - -150.0f) < 0.01f);
REQUIRE(CF_FABSF(top_left.y - 75.0f) < 0.01f);

// A user's own saved projection is not touched by a resize.
cf_draw_projection(cf_ortho_2d(0, 0, 40, 20));
cf_draw_push();
cf_app_set_size(400, 200);
cf_draw_pop();
top_left = cf_screen_to_world(cf_v2(0, 0));
REQUIRE(CF_FABSF(top_left.x - -20.0f) < 0.01f);
REQUIRE(CF_FABSF(top_left.y - 10.0f) < 0.01f);

return true;
}

TEST_CASE(test_app_set_canvas_size_projection_spans_the_canvas)
{
REQUIRE(!cf_is_error(cf_make_app(NULL, 0, 0, 0, 1280, 720, CF_APP_OPTIONS_HIDDEN_BIT | CF_APP_OPTIONS_NO_AUDIO_BIT, NULL)));
OwnedAppGuard guard;

// A retro target: the game draws in 320x180 canvas pixels and the canvas is blitted up. The
// default projection follows the explicit canvas size, so the top-left of the screen is the
// top-left of the 320x180 world -- no cf_draw_projection call needed.
cf_app_set_canvas_size(320, 180);
CF_V2 top_left = cf_screen_to_world(cf_v2(0, 0));
REQUIRE(CF_FABSF(top_left.x - -160.0f) < 0.01f);
REQUIRE(CF_FABSF(top_left.y - 90.0f) < 0.01f);

// The next recreation event snaps the canvas back to the window, and the projection with it.
cf_app_set_size(640, 360);
top_left = cf_screen_to_world(cf_v2(0, 0));
REQUIRE(CF_FABSF(top_left.x - -320.0f) < 0.01f);
REQUIRE(CF_FABSF(top_left.y - 180.0f) < 0.01f);

return true;
}

TEST_CASE(test_app_msaa_change_resets_canvas_size)
{
REQUIRE(!cf_is_error(cf_make_app(NULL, 0, 0, 0, 200, 100, CF_APP_OPTIONS_HIDDEN_BIT | CF_APP_OPTIONS_NO_AUDIO_BIT, NULL)));
Expand Down Expand Up @@ -223,6 +297,8 @@ TEST_SUITE(test_app)
// https://github.com/RandyGaul/cute_framework/pull/517
RUN_TEST_CASE(test_app_set_canvas_size_is_one_shot);
RUN_TEST_CASE(test_app_msaa_change_resets_canvas_size);
RUN_TEST_CASE(test_app_default_projection_is_logical_points_at_2x);
RUN_TEST_CASE(test_app_set_canvas_size_projection_spans_the_canvas);
RUN_TEST_CASE(test_app_present_mode_vsync_always_supported);
RUN_TEST_CASE(test_app_present_mode_off_round_trip);
RUN_TEST_CASE(test_app_present_mode_mailbox_failure_does_not_corrupt_state);
Expand Down
Loading