[pull] master from libretro:master#937
Merged
pull[bot] merged 14 commits intoAlexandre1er:masterfrom Apr 18, 2026
Merged
Conversation
Date: (date)
Subject: win32: apply DPI awareness before any window is created
Move the SetProcessDpiAwareness call from win32_resources_init()
(invoked deep inside frontend_win32_init) to the very top of
rarch_main(), above CoInitialize and any other code that may
create an HWND directly or transitively.
Background
----------
SetProcessDpiAwareness returns E_ACCESSDENIED if called after the
process has a top-level window. When it fails, the process silently
stays DPI-Unaware — meaning GetDeviceCaps reports a fixed 96 DPI
regardless of the actual monitor DPI or Windows scaling setting,
and the compositor bitmap-stretches the output instead of letting
RetroArch render natively.
The previous call site was too late on several Windows entry paths:
- frontend_driver_attach_console() can run before
frontend_driver_init_first() (confirmed by the existing
FIXME at frontend/frontend_driver.c:515). AllocConsole
creates a console window, locking DPI awareness to Unaware.
- CoInitialize() at the top of rarch_main() can instantiate
a hidden OLE message window in some STA configurations,
also locking DPI awareness.
- The Qt UI companion (HAVE_QT build) defers QApplication
construction until the user presses F5 on Qt 5 builds with
ui_companion_toggle=false, so Qt's own HighDpiScaling
attribute is set far too late to matter.
The new call site runs before all three of the above. It is
reached for every Windows entry path: the plain main() in
retroarch.c and the Qt main() in ui/drivers/ui_qt.cpp both
delegate straight to rarch_main(argc, argv, NULL).
No behavioural change on non-Windows builds; the call is
guarded by `#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)`
matching the existing CoInitialize guard.
Verification
------------
Confirmed via Task Manager's "DPI Awareness" column (which reads
the OS-side GetProcessDpiAwareness value, not a self-report):
retroarch.exe shows "System" after the patch, both on a normal
startup and with verbose logging enabled (which forces the
attach_console / AllocConsole path that previously raced this call).
Visual demonstration of the downstream effect requires a display
in the <32" range at non-100% Windows scaling, which was not
available during development. The API-level confirmation is
considered sufficient; a reviewer on a DPI-scaled PC monitor can
verify Ozone/MaterialUI menu scaling directly.
Future work (not part of this commit)
-------------------------------------
This applies PROCESS_SYSTEM_DPI_AWARE (level 1) to match the
behaviour the old media/rarch.manifest declared. Upgrading to
PerMonitorV2 (SetProcessDpiAwarenessContext with
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) would allow the DPI
reported to RetroArch to follow the current monitor when the
window is moved across displays with different scaling, instead
of staying frozen to the primary monitor's startup DPI. That
upgrade additionally requires handling WM_DPICHANGED in the
WndProc to re-query metrics and invalidate cached scale factors,
and should land as a separate commit with its own testing.
--- a/gfx/common/win32_common.c
+++ b/gfx/common/win32_common.c
@@ -1991,17 +1991,26 @@
* IDR_MENU → win32_resources_create_menu() [in ui_win32.c]
* IDR_ACCELERATOR1 → win32_resources_get_accelerator()
* IDD_PICKCORE → win32_resources_pick_core_dialog() [in ui_win32.c]
- * rarch.manifest → apply_dpi_awareness() (called from _init)
+ * rarch.manifest → win32_apply_dpi_awareness()
+ * (called from the top of rarch_main, before
+ * any window is created)
* ---------------------------------------------------------------- */
static HACCEL s_accel_table = NULL;
/* DPI AWARENESS (replaces media/rarch.manifest)
* The manifest contained <dpiAware>true</dpiAware>.
- * We call the equivalent API at runtime. */
+ * We call the equivalent API at runtime.
+ *
+ * Must be called before the process creates any HWND (direct or
+ * transitive, e.g. via CoInitialize or AllocConsole). Once any
+ * top-level window exists, SetProcessDpiAwareness returns
+ * E_ACCESSDENIED and the process stays Unaware — meaning GetDeviceCaps
+ * reports a fixed 96 DPI regardless of monitor or scaling settings.
+ * See call site in retroarch.c (top of rarch_main). */
typedef HRESULT (WINAPI *pfn_SetProcessDpiAwareness)(int);
-static void apply_dpi_awareness(void)
+void win32_apply_dpi_awareness(void)
{
HMODULE shcore = LoadLibraryW(L"shcore.dll");
if (shcore)
@@ -2055,7 +2064,10 @@
void win32_resources_init(void)
{
- apply_dpi_awareness();
+ /* NOTE: DPI awareness is applied separately, at the very top of
+ * rarch_main(), to guarantee it runs before any window is created
+ * (including the hidden OLE window CoInitialize may create).
+ * See win32_apply_dpi_awareness(). */
s_accel_table = create_accelerator_table();
}
--- a/gfx/common/win32_common.h
+++ b/gfx/common/win32_common.h
@@ -175,8 +175,18 @@
* so that the .exe has an embedded icon visible in Explorer.
* Everything else is created at runtime via Win32 API calls. */
+/* Mark the process as DPI-aware (replaces the <dpiAware>true</dpiAware>
+ * entry that used to live in media/rarch.manifest).
+ *
+ * MUST be called before any HWND is created — directly or transitively
+ * (e.g. via CoInitialize or AllocConsole). Call it at the very top of
+ * rarch_main(), before anything else. Safe to call on any Windows
+ * version: falls back from SetProcessDpiAwareness (Win8.1+) to
+ * SetProcessDPIAware (Vista/7), and is a no-op on older systems. */
+void win32_apply_dpi_awareness(void);
+
/* Call once before creating any windows.
- * Applies DPI awareness and creates the accelerator table. */
+ * Creates the accelerator table and other programmatic resources. */
void win32_resources_init(void);
/* Release resources created by win32_resources_init(). */
--- a/retroarch.c
+++ b/retroarch.c
@@ -6141,6 +6141,14 @@
);
#endif
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
+ /* Mark the process DPI-aware BEFORE anything creates a window.
+ * CoInitialize below can create a hidden OLE message window on
+ * some STA configurations; once any HWND exists, the DPI setting
+ * is locked and GetDeviceCaps will report a fixed 96 DPI. */
+ {
+ extern void win32_apply_dpi_awareness(void);
+ win32_apply_dpi_awareness();
+ }
if (FAILED(CoInitialize(NULL)))
{
RARCH_ERR("FATAL: Failed to initialize the COM interface.\n");
…with shcore path Minor followup to cc16a0d: the user32-based fallback uses GetModuleHandleW, which only succeeds if user32.dll is already loaded into the process. That is true for any GUI subsystem build (the CRT links user32 implicitly) but not guaranteed for a pure console-subsystem build on Vista/Win7 — exactly the OS range this fallback is there to cover. Switch to LoadLibraryW to match the shcore path above, and pair it with a matching FreeLibrary so the reference count is balanced. No behavioural change on Win8.1+ (the shcore path is taken and this block never runs) or on XP/Server 2003 (neither DLL exports the symbol and the block is a no-op either way). --- a/gfx/common/win32_common.c +++ b/gfx/common/win32_common.c @@ -2029,9 +2029,12 @@ FreeLibrary(shcore); } /* Fallback for Vista / Win7 without shcore. - * Load dynamically so we still link on XP / MSVC 2005. */ + * Use LoadLibraryW (not GetModuleHandleW) for parity with the + * shcore path above: GetModuleHandleW only succeeds if user32 + * is already loaded, which is true for any GUI subsystem build + * but not guaranteed for a pure console build on those OSes. */ { - HMODULE user32 = GetModuleHandleW(L"user32.dll"); + HMODULE user32 = LoadLibraryW(L"user32.dll"); if (user32) { typedef BOOL (WINAPI *pfn_SetProcessDPIAware)(void); @@ -2042,6 +2045,7 @@ u.proc = GetProcAddress(user32, "SetProcessDPIAware"); if (u.func) u.func(); + FreeLibrary(user32); } } }
…nges Several settings pages leave dependent entries visible when the toggle that controls them is turned off, and in one case the entries fail to re-hide at all without leaving and re-entering the page. Fix three such cases: Settings -> Video -> Scaling: Hide 'Integer Scale Axis' and 'Integer Scale Scaling' when 'Integer Scale' is off. Wrap both entries in a settings->bools.video_scale_integer guard in DISPLAYLIST_VIDEO_SCALING_SETTINGS_LIST. The master toggle already uses setting_bool_action_left/right_with_refresh, so the page rebuilds on toggle and the entries appear/disappear instantly. This mirrors the conditional rendering already used a few lines below for the Aspect Ratio index's custom viewport fields. Settings -> Audio -> Output: Hide 'WASAPI Exclusive Mode', 'WASAPI Float Format' and 'WASAPI Shared Buffer Length' unless audio_driver is "wasapi", mirroring the existing HAVE_ASIO block directly below that gates the ASIO Control Panel on audio_driver == "asio". To make this apply on the fly, add audio_driver_write_handler next to record_driver_write_handler; it delegates to general_write_handler and sets MENU_ST_FLAG_PREVENT_POPULATE | MENU_ST_FLAG_ENTRIES_NEED_REFRESH. Install it via the change_handler slot on the AUDIO_DRIVER entry in the drivers registration loop. Because setting_string_action_left/right_driver and the dropdown OK path (generic_action_ok_dropdown_setting) both call change_handler after writing, a single hook covers the scroll and dropdown-select paths with no action_left/right wrappers. As a side benefit, switching into or out of "asio" now also shows/hides the ASIO Control Panel entry on the fly, where previously it required navigating out of and back into the page. Settings -> Achievements: Fix sub-entries (Username, Password, Appearance, Visibility, Hardcore Mode, etc.) not hiding when 'Achievements' is toggled off. The refresh itself was firing correctly - CHEEVOS_ENABLE uses setting_bool_action_left/right_with_refresh - but the visibility loop in DISPLAYLIST_RETRO_ACHIEVEMENTS_SETTINGS_LIST only ever set build_list[i].checked = true and never reset it. Because build_list is static, 'checked' persisted across rebuilds, so once Achievements had been enabled all sub-entries stayed permanently visible. Reset 'checked' on every pass: CHEEVOS_ENABLE itself is always shown, and every other entry mirrors cheevos_enable. This matches the reset pattern already in use in the sibling DISPLAYLIST_CHEEVOS_APPEARANCE_SETTINGS_LIST block. Note: the same static-build_list-without-full-reset pattern exists in several other displaylist cases in this file and is worth an audit as a follow-up.
Follow-up to dbc9a08 ("MENU: Hide dependent settings on the fly when their master toggle changes"). That commit fixed the Achievements submenu, which suffered from a class of bug that recurs throughout menu_displaylist.c: static menu_displaylist_build_info_selective_t build_list[] = { ... }; for (i = 0; i < ARRAY_SIZE(build_list); i++) { switch (build_list[i].enum_idx) { case MENU_ENUM_LABEL_X: if (some_condition) build_list[i].checked = true; /* only ever turns on */ break; ... } } Because build_list is static, the 'checked' field is reused across menu rebuilds. The `if (cond) checked = true;` idiom only ever sets the field; it never clears it. Once a conditionally-visible entry is shown, it stays visible forever within the process lifetime even if its gating condition later flips to false. The Achievements fix used a reset branch; this commit applies the equivalent fix to every other place in the file that has the same pattern. The mechanical transformation is: if (cond) ==> build_list[i].checked = cond; build_list[i].checked = true; which fully resets 'checked' on every pass and is both shorter and clearer. Affected cases (by DISPLAYLIST_ enum): - PLAYLIST_SETTINGS (playlist_show_sublabels, history_list_enable, truncate_playlist) [*] - ACCESSIBILITY_SETTINGS (accessibility_enable) - AI_SERVICE_SETTINGS (ai_service_enable, 6 entries) - NETWORK_SETTINGS (netplay_allow_slaves, netplay_use_mitm_server) - RECORDING_SETTINGS (UDP_STREAM_PORT / is_ffmpeg+streaming_mode) - USER_INTERFACE_SETTINGS (kiosk_mode_enable, menu_screensaver_supported, desktop_menu_enable) - VIDEO_WINDOWED_MODE_SETTINGS (window_custom_size_enable, 5 entries) - ONSCREEN_OVERLAY_SETTINGS (input_overlay_enable and derivatives, ~28 entries) - ONSCREEN_NOTIFICATIONS_SETTINGS (video_font_enable, widgets_active, video_msg_bgcolor_enable) - ONSCREEN_NOTIFICATIONS_VIEWS_SETTINGS (notifications_active, video_fps_show, video_memory_show, widgets_active, and others) - SAVING_SETTINGS (VIDEO_GPU_SCREENSHOT / video driver caps) - CLOUD_SYNC_SETTINGS (cloud_sync_driver, 3 groups) - STEAM_SETTINGS (steam_rich_presence_enable) - LOGGING_SETTINGS (verbosity_is_enabled, log_to_file) - FRAME_THROTTLE_SETTINGS (rewind_supported, vrr_runloop_enable) - MENU_SETTINGS (menu_horizontal_animation, rgui color theme, rgui particle effect, materialui icons, ozone theme/font scale, hdr mode; 12 entries) - MENU_BOTTOM_SETTINGS (video_3ds_lcd_bottom) - SMB_CLIENT_SETTINGS (smb_enable, 9 entries) - NETWORK_HOSTING_SETTINGS (netplay_allow_slaves, netplay_use_mitm_server) All of these had observable stale-state bugs of varying severity. The ones most likely to have hit users in practice involve masters whose toggles already install refresh-aware action handlers (they force an immediate menu rebuild, making the stale state visible on-screen the moment the toggle flips): accessibility, AI service, audio overlay, netplay, screensaver, VRR runloop, window custom size, and the various WASAPI / ASIO cases already covered in dbc9a08. No functional behavior changes outside of the visibility correction: the condition evaluated is the same one the old code checked, and the initializer defaults in each build_list[] array remain the correct "first-time" value (the first rebuild under typical startup conditions will compute the same visibility the old code did). [*] PLAYLIST_SETTINGS was the one build_list in this file that was not already declared static. It is now, matching the other 33 arrays in the file. Because the visibility loop now fully resets every conditional entry (per the fix above), making the array static is safe and eliminates per-call stack initialization of the ~25-entry literal. No other build_list arrays were non-static.
On Windows, using File -> Load Core... in the native Win32 menubar picks a core via the file dialog and calls task_push_load_new_core, but the in-app main menu (Ozone / XMB / MaterialUI / RGUI) does not rebuild. Entries that become available once a core is loaded - most notably 'Start Core' for contentless cores like 2048, and 'Unload Core' - do not appear until the user performs some other action that triggers a menu refresh. A user loading 2048 through the File menu sees the same main-menu contents as before, with no way to actually start the core. This is specific to the Win32 menubar integration. The in-menu 'Load Core' flow (menu/cbs/menu_cbs_ok.c, ACTION_OK_LOAD_CORE) routes through generic_action_ok which ends by calling menu_entries_flush_stack, and the PENDING_RELOAD_CORE path in menu/menu_driver.c sets MENU_ST_FLAG_ENTRIES_NEED_REFRESH | MENU_ST_FLAG_PREVENT_POPULATE immediately after a successful task_push_load_new_core. Both paths force a rebuild. The Win32 menubar handler skips this step. Fix both Win32 entry points into task_push_load_new_core: - gfx/common/win32_common.c: WM_BROWSER_OPEN_RESULT handler (WIN32_BROWSER_MODE_LOAD_CORE branch) - the threaded / async file-dialog path, used when HAVE_THREADS is defined. - ui/drivers/ui_win32.c: win32_menu_loop ID_M_LOAD_CORE handler, #else (!HAVE_THREADS) branch - the synchronous file-dialog path. In both places, if the task_push_load_new_core call succeeds, set MENU_ST_FLAG_ENTRIES_NEED_REFRESH | MENU_ST_FLAG_PREVENT_POPULATE on the menu state. This mirrors menu_driver.c:8038-8039 exactly. Guarded by #ifdef HAVE_MENU; both translation units already pull in menu/menu_driver.h under the same guard, so no new includes. Load Content path is untouched: it takes a different code path (win32_load_content_from_gui) that loads content with a core, which typically transitions out of the menu entirely rather than staying in it. Why not set the flags inside task_push_load_new_core itself: that function is also called from the network-command handler (command.c:975) and from netplay find-content paths where the menu is not necessarily visible or active. Keeping the flag-set local to the Win32 GUI integration mirrors what menu_driver.c already does for PENDING_RELOAD_CORE - GUI-adjacent code sets the flags; the task itself does not.
On Windows, File -> Load Content... in the native menubar passes settings->paths.directory_menu_content to GetOpenFileName via lpstrInitialDir. That setting is empty by default (see configuration.c, "*directory_menu_content = '\0'"), so the dialog inherits whatever Windows picks as a fallback. On Vista+, that fallback is a per-app MRU keyed by the lpstrInitialDir string. Both File -> Load Core and File -> Load Content pass "" when their respective content / core directories haven't been configured, so they share a single MRU entry. The consequence: after the user picks a core, the next Load Content dialog opens inside the libretro core directory, which is not where anyone's content lives. Fix by synthesising an explicit initial directory when the content path setting is empty: take the system drive letter from GetWindowsDirectoryA, for example "C:\". Opening Load Content at the drive root gives the user a clean top-level starting point (effectively "This PC" at the drive level) and breaks the MRU collision with Load Core because the two calls now pass distinct, non-empty lpstrInitialDir strings. Design notes: - GetWindowsDirectoryA is used rather than SHGetKnownFolderPath or IFileOpenDialog / shell PIDLs because the project targets Windows 9x as well. GetWindowsDirectory has been in kernel32 since Windows 3.1 / 95 and is the broadest-compatibility way to get at the system drive letter. - A literal "C:\" is avoided; some installs are on other drives. - Only Load Content is touched. Load Core already passes settings->paths.directory_libretro, which is always populated with a real core directory by default, so it does not hit the empty-string MRU case. - If GetWindowsDirectoryA fails (returns 0) or returns something that doesn't look drive-letter-prefixed (e.g. some unusual boot layouts), initial_dir is left as the empty string and behavior falls back to the current broken behavior rather than to a bad path. This is strictly better than the status quo. - The fallback logic is placed after all variable declarations in the case block so it stays C89-legal (per CODING-GUIDELINES).
Route the following Win32 menubar entries through msg_hash_to_str
so the menubar honors the active user language, replacing the
handful of entries that were hardcoded to English:
File -> Load Core... MENU_ENUM_LABEL_VALUE_CORE_LIST + "..."
File -> Load Content... MENU_ENUM_LABEL_VALUE_LOAD_CONTENT_LIST + "..."
Command -> Audio Options MENU_ENUM_LABEL_VALUE_AUDIO_SETTINGS
Command -> Disk Options MENU_ENUM_LABEL_VALUE_DISK_OPTIONS
Command -> Save State Opts. MENU_ENUM_LABEL_VALUE_SAVESTATE_LIST
Command -> Reset MENU_ENUM_LABEL_VALUE_RESTART_CONTENT
Command -> Pause Toggle MENU_ENUM_LABEL_VALUE_INPUT_META_PAUSE_TOGGLE
Command -> Menu Toggle MENU_ENUM_LABEL_VALUE_INPUT_META_MENU_TOGGLE
Command -> Take Screenshot MENU_ENUM_LABEL_VALUE_INPUT_META_SCREENSHOT
Command -> Mouse Grab Toggle MENU_ENUM_LABEL_VALUE_INPUT_META_GRAB_MOUSE_TOGGLE
Command -> Save States ->
State Index MENU_ENUM_LABEL_VALUE_STATE_SLOT
(renders as "State Slot")
Window -> Toggle Desktop MENU_ENUM_LABEL_VALUE_INPUT_META_UI_COMPANION_TOGGLE
Window -> Fullscreen Toggle MENU_ENUM_LABEL_VALUE_INPUT_META_FULLSCREEN_TOGGLE_KEY
Window -> Windowed Scale MENU_ENUM_LABEL_VALUE_VIDEO_SCALE
Most items have a Win32 resource ID and were already eligible for
runtime re-localization by win32_localize_menu() via the
menu_id_to_label_enum() lookup table. Three improvements round
this out so localization is correct for all languages:
1. UTF-8 encoding safety for popup submenu headers
Audio Options, Disc Control and Save State Options are popup
submenu entries and therefore have no wID. win32_localize_menu()
never walks them, so the strings passed in at AppendMenu time
are final.
AppendMenuA interprets bytes via the active code page, not UTF-8,
so passing msg_hash_to_str() output directly produced mojibake
for every non-ASCII translation (Japanese "リセット" rendering
as "リセ..." etc.).
New static helper win32_append_popup_utf8() converts UTF-8 to
UTF-16 and calls AppendMenuW on modern Windows, with a
LEGACY_WIN32 fallback via utf8_to_local_string_alloc +
AppendMenuA that matches the encoding path win32_localize_menu
uses for old targets. The three popup call sites route through
this helper.
2. ID_M_TOGGLE_DESKTOP added to menu_id_to_label_enum
Previously missing from the lookup table, so it stayed English
regardless of user language. Added under #ifdef HAVE_QT,
matching the existing guard on the menu item's AppendMenu call.
3. Ellipsis preservation across language changes
"Load Core..." and "Load Content..." use a trailing ellipsis
per Windows UI convention for dialog-opening items. The ellipsis
is appended inside win32_localize_menu() for those two enums
rather than folded into intl/msg_hash_*.h, so the convention is
applied uniformly on every localize pass (startup and any later
language changes).
On-the-fly language change
--------------------------
The native menubar now rebuilds when the user changes
Settings -> User Interface -> Language. A new helper
void win32_menubar_rebuild(void)
is added to ui_win32.c (declared in ui_win32.h under HAVE_MENU).
It grabs the current menubar via GetMenu(main_window.hwnd), early-
exits if none is attached (fullscreen / menubar disabled), builds
a fresh one via win32_resources_create_menu() + win32_localize_menu(),
swaps it in with SetMenu(), destroys the old handle, and calls
DrawMenuBar() to force a repaint.
menu_setting.c invokes this helper from general_write_handler()'s
MENU_ENUM_LABEL_USER_LANGUAGE case. Both the left/right-arrow and
combo-box dropdown paths fire general_write_handler via
setting->change_handler (see generic_action_ok_dropdown_setting in
menu_cbs_ok.c), so both UX paths trigger the menubar rebuild.
menu_setting.c does not include ui_win32.h; a narrow forward
declaration guarded by
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
&& defined(HAVE_MENU)
keeps <windows.h> out of the menu subsystem.
Implementation details
----------------------
- String building uses strlcpy from compat/strl.h (already included
in ui_win32.c). No snprintf, no strlcat. The ellipsis append uses
a second strlcpy at the buffer offset returned by the first, per
the idiom already used in menu_displaylist.c.
- Label buffers are 128-byte stack arrays, sized for any plausible
translation of the two ellipsis items plus the trailing 3 dots.
- C89-compliant declaration ordering preserved throughout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )