From 2c96d5cbc699b8fd32f1a3ff57d3e7471259d75c Mon Sep 17 00:00:00 2001 From: Arignir Date: Wed, 16 Oct 2024 23:23:16 +0200 Subject: [PATCH] Improve how High DPI displays are handled. --- include/app/app.h | 20 +++-- source/app/main.c | 33 +++++++-- source/app/sdl/video.c | 133 ++++++++++++++++++++++++---------- source/app/windows/game.c | 1 - source/app/windows/menubar.c | 6 +- source/app/windows/settings.c | 9 +-- 6 files changed, 142 insertions(+), 60 deletions(-) diff --git a/include/app/app.h b/include/app/app.h index 440f2d6..8aefceb 100644 --- a/include/app/app.h +++ b/include/app/app.h @@ -26,6 +26,8 @@ #define POWER_SAVE_FRAME_DELAY 30 #define MAX_GFX_PROGRAMS 10 +#define DEFAULT_RESIZE_TIMER 3 + struct ImGuiIO; enum menubar_mode { @@ -394,7 +396,11 @@ struct app { // High resolution float dpi; - uint32_t scale; + float scale; + + // Default style of ImGui. + // Used when rescaling the application. + struct ImGuiStyle default_style; // Display refresh rate uint32_t refresh_rate; @@ -408,9 +414,6 @@ struct app { // Temporary value used to measure the time since the last mouse movement (in ms) float time_elapsed_since_last_mouse_motion_ms; - // Used to avoid a bug on Linux/Wayland that prevents us from resizing the window during the first frame. - bool first_frame; - struct { // 1.0 if the menubar is visible, 0.0 if not, and something in between if the // menubar is fading away @@ -459,8 +462,11 @@ struct app { uint32_t height; } win; - // Set when the window needs to be resized to fit a specific aspect ratio - bool request_resize; + // Timer, in frames, until the window needs to be resized to fit a specific aspect ratio + int resize_request_timer; + + // Set to the last time the scale was calculated. + uint64_t last_scale_calculation_ms; } display; // The error message to print, if any. @@ -531,6 +537,8 @@ void app_sdl_video_render_frame(struct app *app); void app_sdl_video_rebuild_pipeline(struct app *app); void app_sdl_video_resize_window(struct app *app); void app_sdl_video_update_display_mode(struct app *app); +void app_sdl_video_update_scale(struct app *app, float scale); +float app_sdl_video_calculate_scale(struct app *app); /* app/shaders/frag-color-correction.c */ extern char const *SHADER_FRAG_COLOR_CORRECTION; diff --git a/source/app/main.c b/source/app/main.c index 51e25ce..8173f4f 100644 --- a/source/app/main.c +++ b/source/app/main.c @@ -93,6 +93,7 @@ main( pthread_t dbg_thread; #endif uint64_t sdl_counters[2]; + uint64_t last_rescale_recalculation_ms; memset(&app, 0, sizeof(app)); app.emulation.gba = gba_create(); @@ -102,9 +103,7 @@ main( app.emulation.is_started = false; app.emulation.is_running = false; app.audio.resample_frequency = 48000; - app.ui.display.request_resize = true; app.ui.menubar.visibility = 1.0; - app.ui.first_frame = true; app_settings_default(&app.settings); app_bindings_setup_default(&app); @@ -122,7 +121,7 @@ main( logln(HS_INFO, "Opengl version: %s%s%s.", g_light_magenta, (char*)glGetString(GL_VERSION), g_reset); logln( HS_INFO, - "Dpi: %s%.1f%s, Scale factor: %s%u%s, Refresh Rate: %s%uHz%s.", + "Dpi: %s%.1f%s, Scale factor: %s%.2f%s, Refresh Rate: %s%uHz%s.", g_light_magenta, app.ui.dpi, g_reset, @@ -217,9 +216,31 @@ main( // Handle window resize request // There's a bug on Linux/Wayland that prevents us from resizing the window during // the first frame, hence why we wait in that case. - if (app.ui.display.request_resize && !app.ui.first_frame) { + if (app.ui.display.resize_request_timer && !--app.ui.display.resize_request_timer) { app_sdl_video_resize_window(&app); - app.ui.display.request_resize = false; + } + + // Recalculate the scale every 100ms + // + // We do this periodically to avoid glitching when the window is in-between two monitors. + // It might look like a weird idea, but overall this adds a lot of stability to the hidpi system. + // + // This also prevents us from calling `app_sdl_video_calculate_scale()` every frame, which + // would be quite costly. + last_rescale_recalculation_ms = (uint64_t)((float)sdl_counters[0] / (float)SDL_GetPerformanceFrequency() * 1000.f); + if (last_rescale_recalculation_ms - app.ui.display.last_scale_calculation_ms > 100) { + float scale; + + app.ui.display.last_scale_calculation_ms = last_rescale_recalculation_ms; + scale = app_sdl_video_calculate_scale(&app); + + // If the scale changed significantly + if (scale - app.ui.scale > 0.01 || scale - app.ui.scale < -0.01) { + app_sdl_video_update_scale(&app, scale); + + // Request a resize to ensure the window matches the new scale + app.ui.display.resize_request_timer = DEFAULT_RESIZE_TIMER; + } } elapsed_ms = ((float)(sdl_counters[1] - sdl_counters[0]) / (float)SDL_GetPerformanceFrequency()) * 1000.f; @@ -307,8 +328,6 @@ main( app.file.flush_qsaves_cache = false; } - - app.ui.first_frame = false; } app_emulator_exit(&app); diff --git a/source/app/sdl/video.c b/source/app/sdl/video.c index 58f0846..3d5dcab 100644 --- a/source/app/sdl/video.c +++ b/source/app/sdl/video.c @@ -24,7 +24,6 @@ app_sdl_video_init( ) { char const *glsl_version; SDL_DisplayMode mode; - ImFontConfig *cfg; uint32_t win_flags; int err; @@ -71,8 +70,8 @@ app_sdl_video_init( // // The size given here is merely a guess as to what the real size will be, hence the magical +19.f for the window's height. app->ui.menubar.size.y = app->settings.video.menubar_mode == MENUBAR_MODE_FIXED_ABOVE_GAME ? 19.f * app->ui.scale : 0.f; - app->ui.display.win.width = GBA_SCREEN_WIDTH * app->settings.video.display_size * app->ui.scale; - app->ui.display.win.height = (GBA_SCREEN_HEIGHT * app->settings.video.display_size * app->ui.scale) + app->ui.menubar.size.y; + app->ui.display.win.width = GBA_SCREEN_WIDTH * app->settings.video.display_size; + app->ui.display.win.height = GBA_SCREEN_HEIGHT * app->settings.video.display_size + app->ui.menubar.size.y; app_win_game_refresh_game_area(app); win_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; @@ -98,27 +97,6 @@ app_sdl_video_init( exit(EXIT_FAILURE); } - -#if defined(__APPLE__) - // On my MacBook (12.3.1) it looks like the system is already scaling the window in a nice, pixel-perfect way. - // - // If we use our scaling on top of it, the windows gets blurry and ugly very quick so we hard-code the scaling to 1 to - // avoid that. - app->ui.scale = 1; -#else - int screen_w; - int pixel_w; - - // Calculate the scale of the window - SDL_GetWindowSize(app->sdl.window, &screen_w, NULL); - SDL_GL_GetDrawableSize(app->sdl.window, &pixel_w, NULL); - app->ui.scale = (uint32_t)round((float)pixel_w / (float)screen_w); - app->ui.scale = app->ui.scale ?: 1; -#endif - - // Resize the window to match the newfound scale. - app_sdl_video_resize_window(app); - // Create the OpenGL context app->gfx.gl_context = SDL_GL_CreateContext(app->sdl.window); SDL_GL_MakeCurrent(app->sdl.window, app->gfx.gl_context); @@ -143,20 +121,17 @@ app_sdl_video_init( app->ui.ioptr->ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls app->ui.ioptr->IniFilename = NULL; - cfg = ImFontConfig_ImFontConfig(); - cfg->SizePixels = 13.f * round(app->ui.scale); - cfg->GlyphOffset.y = 13.f * round(app->ui.scale); - app->ui.fonts.normal = ImFontAtlas_AddFontDefault(app->ui.ioptr->Fonts, cfg); + ImGui_ImplSDL2_InitForOpenGL(app->sdl.window, app->gfx.gl_context); + ImGui_ImplOpenGL3_Init(glsl_version); - cfg = ImFontConfig_ImFontConfig(); - cfg->SizePixels = 13.f * round(app->ui.scale * 3.); - cfg->GlyphOffset.y = 13.f * round(app->ui.scale * 3.); - app->ui.fonts.big = ImFontAtlas_AddFontDefault(app->ui.ioptr->Fonts, cfg); + // Copy the default style so we can easily rescale ImGui to something different + memcpy(&app->ui.default_style, igGetStyle(), sizeof(*igGetStyle())); - ImGuiStyle_ScaleAllSizes(igGetStyle(), app->ui.scale); + // Update all scale-related objects, such as the ImGui fonts and style. + app_sdl_video_update_scale(app, app_sdl_video_calculate_scale(app)); - ImGui_ImplSDL2_InitForOpenGL(app->sdl.window, app->gfx.gl_context); - ImGui_ImplOpenGL3_Init(glsl_version); + // Request a resize to ensure the window matches the new scale + app->ui.display.resize_request_timer = DEFAULT_RESIZE_TIMER; // Build all the available shaders app->gfx.program_color_correction = build_shader_program("color_correction", SHADER_FRAG_COLOR_CORRECTION, SHADER_VERTEX_COMMON); @@ -203,6 +178,25 @@ app_sdl_video_init( NFD_Init(); } +/* +** Calculate the UI's scale by dividing the window's size in logical points by its size in pixels. +*/ +float +app_sdl_video_calculate_scale( + struct app *app +) { + int screen_w; + int pixel_w; + + SDL_GetWindowSize(app->sdl.window, &screen_w, NULL); + SDL_GL_GetDrawableSize(app->sdl.window, &pixel_w, NULL); + + return ((float)pixel_w / (float)screen_w); +} + +/* +** Resize the window to match the size choosen in the settings and the monitor's scale. +*/ void app_sdl_video_resize_window( struct app *app @@ -210,8 +204,8 @@ app_sdl_video_resize_window( uint32_t w; uint32_t h; - w = GBA_SCREEN_WIDTH * app->settings.video.display_size * app->ui.scale; - h = GBA_SCREEN_HEIGHT * app->settings.video.display_size * app->ui.scale; + w = round((float)(GBA_SCREEN_WIDTH * app->settings.video.display_size) / app->ui.scale); + h = round((float)(GBA_SCREEN_HEIGHT * app->settings.video.display_size) / app->ui.scale); // If relevant, expand the window by the size of the menubar h += app->settings.video.menubar_mode == MENUBAR_MODE_FIXED_ABOVE_GAME ? app->ui.menubar.size.y : 0; @@ -219,6 +213,69 @@ app_sdl_video_resize_window( SDL_SetWindowSize(app->sdl.window, w, h); } +/* +** Update thes scale and anything that hardcodes the scale somehow. +** +** Currently this includes the ImGui fonts and style. +*/ +void +app_sdl_video_update_scale( + struct app *app, + float scale +) { + ImFontConfig *cfg; + struct ImGuiStyle *style; + + app->ui.scale = scale; + + ImFontAtlas_Clear(app->ui.ioptr->Fonts); + + cfg = ImFontConfig_ImFontConfig(); + cfg->SizePixels = round(13.f * app->ui.scale); + cfg->GlyphOffset.y = round(13.f * app->ui.scale); + cfg->RasterizerDensity = app->ui.scale * 2.f; + app->ui.fonts.normal = ImFontAtlas_AddFontDefault(app->ui.ioptr->Fonts, cfg); + + cfg = ImFontConfig_ImFontConfig(); + cfg->SizePixels = round(13.f * app->ui.scale * 3.); + cfg->GlyphOffset.y = round(13.f * app->ui.scale * 3.); + cfg->RasterizerDensity = app->ui.scale * 6.f; + app->ui.fonts.big = ImFontAtlas_AddFontDefault(app->ui.ioptr->Fonts, cfg); + + ImFontAtlas_Build(app->ui.ioptr->Fonts); + ImGui_ImplOpenGL3_DestroyDeviceObjects(); + + style = igGetStyle(); + + // Restore default style size + style->WindowPadding = app->ui.default_style.WindowPadding; + style->WindowRounding = app->ui.default_style.WindowRounding; + style->WindowMinSize = app->ui.default_style.WindowMinSize; + style->ChildRounding = app->ui.default_style.ChildRounding; + style->PopupRounding = app->ui.default_style.PopupRounding; + style->FramePadding = app->ui.default_style.FramePadding; + style->FrameRounding = app->ui.default_style.FrameRounding; + style->ItemSpacing = app->ui.default_style.ItemSpacing; + style->ItemInnerSpacing = app->ui.default_style.ItemInnerSpacing; + style->CellPadding = app->ui.default_style.CellPadding; + style->TouchExtraPadding = app->ui.default_style.TouchExtraPadding; + style->IndentSpacing = app->ui.default_style.IndentSpacing; + style->ColumnsMinSpacing = app->ui.default_style.ColumnsMinSpacing; + style->ScrollbarSize = app->ui.default_style.ScrollbarSize; + style->ScrollbarRounding = app->ui.default_style.ScrollbarRounding; + style->GrabMinSize = app->ui.default_style.GrabMinSize; + style->GrabRounding = app->ui.default_style.GrabRounding; + style->LogSliderDeadzone = app->ui.default_style.LogSliderDeadzone; + style->TabRounding = app->ui.default_style.TabRounding; + style->TabMinWidthForCloseButton = (style->TabMinWidthForCloseButton != FLT_MAX) ? app->ui.default_style.TabMinWidthForCloseButton : FLT_MAX; + style->SeparatorTextPadding = app->ui.default_style.SeparatorTextPadding; + style->DisplayWindowPadding = app->ui.default_style.DisplayWindowPadding; + style->DisplaySafeAreaPadding = app->ui.default_style.DisplaySafeAreaPadding; + style->MouseCursorScale = app->ui.default_style.MouseCursorScale; + + ImGuiStyle_ScaleAllSizes(style, app->ui.scale); +} + void app_sdl_video_update_display_mode( struct app *app @@ -230,7 +287,7 @@ app_sdl_video_update_display_mode( case DISPLAY_MODE_BORDERLESS: win_flags = SDL_WINDOW_FULLSCREEN_DESKTOP; break; case DISPLAY_MODE_WINDOWED: { win_flags = 0; - app->ui.display.request_resize = true; + app->ui.display.resize_request_timer = true; break; }; default: { diff --git a/source/app/windows/game.c b/source/app/windows/game.c index f5f3089..5b849f5 100644 --- a/source/app/windows/game.c +++ b/source/app/windows/game.c @@ -20,7 +20,6 @@ void app_win_game_refresh_game_area( struct app *app ) { - app->ui.display.game.outer.x = 0; app->ui.display.game.outer.y = 0; diff --git a/source/app/windows/menubar.c b/source/app/windows/menubar.c index 8de5b40..dde7721 100644 --- a/source/app/windows/menubar.c +++ b/source/app/windows/menubar.c @@ -310,12 +310,12 @@ app_win_menubar_video( if (igMenuItem_Bool( display_sizes[x - 1], NULL, - app->ui.display.game.outer.width == GBA_SCREEN_WIDTH * x * app->ui.scale - && app->ui.display.game.outer.height == GBA_SCREEN_HEIGHT * x * app->ui.scale, + app->ui.display.game.outer.width == GBA_SCREEN_WIDTH * x + && app->ui.display.game.outer.height == GBA_SCREEN_HEIGHT * x, true )) { app->settings.video.display_size = x; - app->ui.display.request_resize = true; + app->ui.display.resize_request_timer = DEFAULT_RESIZE_TIMER; } } diff --git a/source/app/windows/settings.c b/source/app/windows/settings.c index 536d923..20b0bfd 100644 --- a/source/app/windows/settings.c +++ b/source/app/windows/settings.c @@ -422,7 +422,7 @@ app_win_settings_video( igTableNextColumn(); if (igCombo_Str_arr("##MenubarMode", (int *)&app->settings.video.menubar_mode, menubar_mode_names, array_length(menubar_mode_names), 0)) { - app->ui.display.request_resize = true; + app->ui.display.resize_request_timer = DEFAULT_RESIZE_TIMER; } // Display Mode @@ -444,10 +444,9 @@ app_win_settings_video( display_size = -1; for (i = 1; i < array_length(display_size_names) + 1; ++i) { - if ( - app->ui.display.game.outer.width == GBA_SCREEN_WIDTH * i * app->ui.scale - && app->ui.display.game.outer.height == GBA_SCREEN_HEIGHT * i * app->ui.scale + app->ui.display.game.outer.width == GBA_SCREEN_WIDTH * i + && app->ui.display.game.outer.height == GBA_SCREEN_HEIGHT * i ) { display_size = i; break; @@ -461,7 +460,7 @@ app_win_settings_video( is_selected = (display_size == i); if (igSelectable_Bool(display_size_names[i - 1], is_selected, ImGuiSelectableFlags_None, (ImVec2){ 0.f, 0.f })) { app->settings.video.display_size = i; - app->ui.display.request_resize = true; + app->ui.display.resize_request_timer = DEFAULT_RESIZE_TIMER; } if (is_selected) {