Skip to content

Commit

Permalink
Improve how High DPI displays are handled.
Browse files Browse the repository at this point in the history
  • Loading branch information
Arignir committed Oct 16, 2024
1 parent d3ff74b commit 2c96d5c
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 60 deletions.
20 changes: 14 additions & 6 deletions include/app/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
33 changes: 26 additions & 7 deletions source/app/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);

Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -307,8 +328,6 @@ main(

app.file.flush_qsaves_cache = false;
}

app.ui.first_frame = false;
}

app_emulator_exit(&app);
Expand Down
133 changes: 95 additions & 38 deletions source/app/sdl/video.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ app_sdl_video_init(
) {
char const *glsl_version;
SDL_DisplayMode mode;
ImFontConfig *cfg;
uint32_t win_flags;
int err;

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -203,22 +178,104 @@ 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
) {
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;

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
Expand All @@ -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: {
Expand Down
1 change: 0 additions & 1 deletion source/app/windows/game.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions source/app/windows/menubar.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
9 changes: 4 additions & 5 deletions source/app/windows/settings.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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) {
Expand Down

0 comments on commit 2c96d5c

Please sign in to comment.