From 4b460947b452f03c03b806c3615984386a256fb3 Mon Sep 17 00:00:00 2001 From: LibretroAdmin <105389611+LibretroAdmin@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:12:20 +0200 Subject: [PATCH] Companion UI: native Win32 and Cocoa desktop menus on a shared C core (Qt becomes one backend of three) (#19505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ui/companion: add shared toolkit-agnostic companion core (C89) First step of the companion UI native-backend refactor: a pure-C89 model/command layer with no toolkit types, to be shared by the Qt, Win32 and Cocoa desktop companions. - opaque companion_core_t + callback table for model changes, status, log and refresh notifications - playlist file enumeration and accessors - selected-playlist loading driven through playlist_parse_step() under a microsecond budget from companion_core_iterate(), so parsing a large playlist never stalls the runloop - load requests routed through the existing task_push_load_content_*_from_companion_ui paths Passes tools/mingw_syntax_check.sh (mingw gnu99 + linux c89 pedantic). Not yet wired into any build; the next commits add the consumers. * ui: make the desktop companion a selectable driver (Settings -> Drivers -> Companion UI) The companion layer now has two slots. The platform driver (win32 / cocoa / cocoatouch) is unchanged and always active: it keeps owning the message pump, message boxes, file browser and window glue, so the Qt companion on Windows and macOS behaves exactly as before. The desktop ("WIMP") companion - the playlist / content browser window that used to be hard-wired to Qt via qt_data - is now chosen from a driver table by the new ui_companion_driver string setting, exposed as Settings -> Drivers -> Companion UI. When a build carries more than one desktop companion the user can switch between them. - uico_driver_state_t: qt_data -> wimp + wimp_data, UICO_ST_FLAG_QT_IS_INITED -> UICO_ST_FLAG_WIMP_IS_INITED - HAVE_COMPANION_WIMP (ui_companion_driver.h): any desktop companion is available (Qt, Cocoa, or desktop Win32); replaces the HAVE_QT / HAVE_QT||HAVE_COCOA gates in retroarch.c, menu_displaylist.c, menu_setting.c, menu_cbs_sublabel.c and settings_def_desktop_menu.h - new iterate slot in ui_companion_driver_t (all initialiser tables updated) and ui_companion_driver_wimp_iterate(), called from the main loop in retroarch.c and from rarch_main in ui_cocoa.m, so native companions that ride the platform pump still get per-frame time - ui_companion_wimp_find_driver/_find_ident, config_get_ui_companion_driver_options, config_get_default_ui_companion, STRING_LIST_UI_COMPANION_DRIVERS, find_driver_nonempty case - companion core wired into Makefile.common (once, for Qt / Win32 / Cocoa) and griffin.c - tools/settings_table.reference regenerated (no row changes; verified with --check, unity and settings_migrate_selftest) Verified: tools/mingw_syntax_check.sh --consumers-of ui/ui_companion_driver.h ui/companion/companion_core.h (16 TUs clean). * ui/drivers/qt: route status and log messages through the shared companion core Phase 1 plumbing proof for the companion refactor: ui_companion_qt_t now owns a companion_core_t; msg_queue_push and log_msg go core -> callback -> MainWindow::showStatusMessage / appendLogMessage. No behaviour change for Qt users; further subsystems (playlists, scan, load requests, thumbnails) follow the same route one at a time. * ui/drivers: add native Win32 desktop companion ("win32" companion UI driver) Pure C89 (MSVC 2003 / Win9x baseline) desktop companion built on the shared companion core and classic common controls only: LISTBOX for playlists, SysListView32 report view for entries, msctls_statusbar32, ANSI entry points, no static imports of post-9x APIs. Load Core / Load Content reuse the platform driver's existing dialog flow through win32_menu_loop(); Run / double-click loads through the core's task path. The window is dispatched by ui_win32.c's existing thread pump; the only per-frame work is a 2 ms-budgeted companion_core_iterate() from the iterate slot, so the runloop is never blocked. When this driver is selected the companion is Win32-only: Qt is not touched. Wired into Makefile.common (Win32 targets) and griffin.c (MSVC projects). Playlist browser / load only for now; scan, core association, thumbnails and the parity checklist follow. Verified with x86_64-w64-mingw32-gcc -fsyntax-only (gnu99, -Wall -Wextra clean); no MSVC 2003 toolchain on this box. * ui/drivers: add native Cocoa desktop companion ("cocoa" companion UI driver) OS X 10.4-baseline AppKit shell over the shared companion core: NSWindow, NSSplitView, two NSTableViews using the classic dataSource / delegate protocols (no blocks, no view-based cells, no properties), NSTextField status line and a "Companion" menu appended to the main menu bar. Load Core / Load Content forward to the app delegate's existing openCore: / openDocument: actions; Run / double-click loads through the core's task path. Pumped by ui_cocoa.m's existing run loop; per-frame work is a 2 ms-budgeted companion_core_iterate(). Objective-C is confined to the RACompanionController; all AppKit objects are its ivars so both the MRC per-file Makefile build and the ARC griffin_objc.m Xcode build own them correctly. The controller is held from the C driver struct through CFBridgingRetain under ARC and plain +1 under MRC. Uses the NSWindowStyleMask* compat names from defines/cocoa_defines.h and a 10.4-SDK NSInteger fallback. When this driver is selected the companion is Cocoa-only: Qt is not touched. Wired into Makefile.common (HAVE_COCOA_COMMON) and griffin_objc.m (TARGET_OS_OSX). Not compiled here (no Apple toolchain on this box); tools/apple_mrc_ownership_check.py reports nothing for the new file. * ui: fix Win32 link (comctl32) and Xcode Metal link (HAVE_COCOA_METAL) of the native companions Two CI link failures on the uicompanion branch: - MSYS2 (CLANG64/MINGW32/MINGW64): undefined InitCommonControls. The native Win32 companion uses comctl32 (list view, status bar) and the Makefile never linked it - ui_win32.c only carried the MSVC #pragma comment. Add -lcomctl32 to LIBS in the Companion UI block, and the same pragma to ui_win32_companion.c for MSVC per-file builds. - Xcode RetroArch_Metal (arm64): companion_core_* undefined. Xcode defines HAVE_COCOA_METAL, not HAVE_COCOA, so HAVE_COMPANION_WIMP was never set for griffin.c and companion_core.c was not compiled, while griffin_objc.m (TARGET_OS_OSX) pulled in ui_cocoa_companion.m and its references. HAVE_COMPANION_WIMP, the wimp driver table and the desktop-menu settings_def guard now all include HAVE_COCOA_METAL, so the three stay in sync as the header comment requires. tools/settings_table.reference regenerated (unchanged). * ui/companion: move load-request and core lifecycle logic out of the Qt companion Phase 2, first subsystem. The tail of MainWindow::loadContent (core path sanitising via core_info_find, caching it across CMD_EVENT_UNLOAD_CORE, .lpl suffixing of db_name, the load task push and the pending quick menu), LoadCoreWindow::loadCore's core-load sequence, the unload-core paths and getPlaylistDefaultCore now live in companion_core: - companion_core_request_load_content() - companion_core_load_core() (HAVE_DYNAMIC) - companion_core_unload_core() - companion_core_current_core_path() - companion_core_playlist_default_core() companion_core_request_load_entry() now routes through request_load_content too, so the native Win32/Cocoa companions get the same core-path sanitising and quick-menu behaviour as Qt. Qt keeps only the widget logic and the failure message boxes; it reaches the core via ui_companion_qt_core() (ui_qt_widgets.cpp is a separate TU). Behaviour is unchanged for Qt users. playlist_default_core still parses the playlist synchronously when it is not the menu's cached one, exactly as the Qt code did; moving that onto the budgeted parse is a follow-up. Verified: ui_qt.cpp / ui_qt_widgets.cpp compile against Qt 6; tools/mingw_syntax_check.sh on companion_core.c and the consumers of companion_core.h (mingw gnu99 + linux c89 pedantic clean). * ui/companion: move playlist editing out of the Qt companion Phase 2, second subsystem. Every Qt playlist edit repeated the same sequence: borrow the menu's cached playlist when it is the same file, otherwise parse; mutate; write; free if owned. That now lives once in the shared core: - companion_core_playlist_open() / _release() (cache borrow, parse, write, free) - companion_core_playlist_update_entry() - companion_core_playlist_delete_entry() - companion_core_playlist_set_default_core() (core_info lookup, "DETECT" fallback) Qt callers reduced to string marshalling + one call: MainWindow::updateCurrentPlaylistEntry, deleteCurrentPlaylistItem, the "Associate core" context-menu action, and PlaylistModel::getPlaylistItems (read path, still populating the Qt model itself). companion_core_playlist_default_core() uses the same open/release. The per-call playlist_config_t setup disappears from four Qt functions; the config is built in one place in the core. Behaviour unchanged for Qt users; the native Win32/Cocoa companions can now offer entry editing and core association through the same calls. Verified: ui_qt.cpp / ui_qt_widgets.cpp compile against Qt 6 (only the two pre-existing warnings); companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. * ui/drivers: playlist editing in the native Win32 and Cocoa companions Wire the shared core's editing surface into both native companions so they match the Qt companion's context menus: - entries: right-click -> Run / Delete Entry (with confirmation); the playlist is re-selected through the core's budgeted parse afterwards - playlists: right-click -> Associate Core submenu ( plus every installed core, rebuilt on open) / Refresh Playlists companion_core gains read-only accessors for the installed core list (companion_core_installed_core_count/_path/_name) so backends do not include core_info.h. Win32: WM_CONTEXTMENU on both controls, LB_ITEMFROMPOINT so the action applies to the right-clicked playlist (a list box does not move its selection on right-click), menu IDs 51000+ reserved for core entries. Cocoa: per-table -setMenu:, NSMenuDelegate -menuNeedsUpdate: fills the core submenu, actions use -clickedRow; NSAlert confirm (10.3+). Verified: mingw gnu99 -Wall -Wextra and linux c89 pedantic clean on the Win32 file and the core; tools/apple_mrc_ownership_check.py reports nothing for the Cocoa file (no Apple toolchain here). * ui/drivers/qt: populate the playlist model through the companion core's budgeted parse Phase 2, third subsystem, and the first non-blocking win for Qt users: PlaylistModel::getPlaylistItems parsed the whole playlist on the UI thread (the "first touch of a large playlist is the grid view's hitch" the code itself documented). It now asks the core to companion_core_select_playlist_path(); the parse advances in 2 ms slices from the runloop via the new ui_companion_qt_iterate() hook, and on_playlist_changed lands in PlaylistModel::onCorePlaylistChanged(), which deep-copies the entries and starts the next pending file. The "all playlists" view chains through its list one completion at a time. Entries are committed to the model in a single reset when the last playlist is in, then playlistsLoaded() fires. MainWindow::initContentTableWidget() no longer sorts / counts / scrolls right after asking for the data; that moved to the new onPlaylistModelLoaded() slot, which also refreshes the launch-with actions (they read the current entry, which did not exist yet). Restarting a load supersedes the one in flight: the core aborts the pending parse on the new selection and partial entries are dropped. Core additions: companion_core_select_playlist_path() (any .lpl path, not just the playlist directory) and companion_core_selected_playlist_path(). Verified: ui_qt.cpp, ui_qt_widgets.cpp and both moc TUs compile against Qt 6 (only the two pre-existing warnings); companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. Not run here (no display); the grid, list, history and all-playlists paths need a manual pass on a desktop. * ui/companion: move bulk playlist add out of the Qt companion Phase 2, fourth subsystem. MainWindow::addFilesToPlaylist was the last Qt code constructing a playlist_config_t and calling playlist_init / playlist_push / playlist_write_file directly. The core now provides the bulk-add surface: - companion_core_playlist_open_private(): a fresh instance, never the menu's cached playlist, so a cancelled add is discarded unwritten via companion_core_playlist_release(core, pl, true, false) and committed with (…, true, true) - companion_core_resolve_content_path(): the "archive with exactly one file -> archive#file" rule, previously duplicated in the add and the update-entry paths - companion_core_playlist_push(): DETECT defaults for core path / name, the unknown-crc marker Qt keeps the dialog, the file/directory gathering, the progress dialog and the extension filter (which now applies to the resolved path from the core). updateCurrentPlaylistEntry uses the same resolver. After this no Qt source touches playlist_config_t, playlist_init(), playlist_get_cached() or playlist_push(); all playlist model logic the plan lists for extraction goes through ui/companion. Verified: ui_qt_widgets.cpp compiles against Qt 6 (only the pre-existing warning); companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. * ui/companion: move directory scan out of the Qt companion; add it to the native companions Phase 2, fifth subsystem. The scan was already task-based; the request plumbing and the completion handling move into the core: - companion_core_request_scan(path, directory, show_hidden): pushes task_push_dbscan against the configured playlist directory and content database; false when the build has no libretrodb or the push failed - on_scan_finished callback: fired after the menu's horizontal list has been reset (that reset was Qt's scan_finished_handler; it belongs to RetroArch state, so it is the core's now). task_push_dbscan carries no user data, so the requesting core is remembered in a static and cleared in companion_core_free (one desktop companion is active at a time). Qt: onFileBrowserTreeContextMenuRequested calls the core; the "scan finished" confirmation dialog stays in Qt as the callback. Native companions gain File -> Scan Directory...: Win32 through SHBrowseForFolderA (BIF_RETURNONLYFSDIRS, no new-style flag; shell32 / ole32 added to LIBS and the MSVC pragmas), Cocoa through NSOpenPanel using the same URL-or-objc_msgSend split as ui_cocoa.m so the 10.4 selectors do not appear as declarations to a modern SDK. Both refresh the playlist list on completion. Verified: ui_qt.cpp compiles against Qt 6; companion_core.c with and without HAVE_LIBRETRODB, ui_win32_companion.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean). apple_mrc_ownership_check.py reports the local [NSOpenPanel openPanel], used within the pool exactly as ui_cocoa.m does - a read-and-dismiss hit. * ui/companion: move thumbnail path resolution out of the Qt companion Phase 2, sixth subsystem. The thumbnail repository layout, the label sanitising rule (& * / : ` < > ? \ | -> '_'), the extension probe order (.png .jpg .jpeg .bmp .tga, .png as the save target) and the "image content is its own thumbnail" rule were all Qt-side. They are now C89 in the core: - COMPANION_THUMB_BOXART / _SCREENSHOT / _TITLE / _LOGO subdirectory names (ui_qt.h's THUMBNAIL_* macros alias them) - companion_core_thumbnail_dir(db_name, subdir) - companion_core_thumbnail_path(db_name, subdir, label, content_path) PlaylistModel keeps three thin QString wrappers (getPlaylistThumbnailsDir, getThumbnailPath, getRepositoryThumbnailPath - the last for the save target and the sidebar images, where the content path must not short- circuit the lookup); getSanitizedThumbnailName and the sanitiser QRegularExpression are gone. changeThumbnail and the current-item thumbnail loader use the wrappers. The same calls are what the native companions' grid / list thumbnails will resolve through. Verified: ui_qt.cpp, ui_qt_widgets.cpp and moc_ui_qt compile against Qt 6 (only the two pre-existing warnings); companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. * ui/drivers/qt: define MainWindow::playlistModel() The getter was declared in ui_qt.h but never defined - nothing used it until the companion core's on_playlist_changed callback did, so the MSYS2 Qt builds failed to link: undefined reference to `MainWindow::playlistModel()' Define it alongside the other trivial accessors. This box only ever compiled ui_qt.o, never linked a Qt retroarch, which is how it slipped. * ui/companion: move "launch with" core selection out of the Qt companion Phase 2, seventh subsystem. MainWindow::setCoreActions built the "Launch with" list itself: the running core, the entry's own core, the playlist's default core, each de-duplicated against the others by path, core name, display name and core_file_id prefix, spread over ~150 lines of QVariantMap juggling (including a per-row loop that re-resolved the same default core for the all-playlists view). The selection logic is now C89: - companion_core_launch_options(entry core, playlist name, suggest_loaded_first, out[], max): the de-duplicated candidates in menu order, each tagged companion_launch_selection - companion_core_current_core_name() / _version() / _supports_no_content(): the runloop system info Qt read directly Qt's core_selection enum now aliases companion_launch_selection so the combo box stores what the core hands back; setCoreActions fills the combo from the array and appends its Ask / Load Core... entries; setCurrentCoreLabel uses the accessors instead of runloop_state. Verified: full Qt 6 retroarch links on Linux (the miss behind the playlistModel() CI failure is now covered on this box) and starts headless with desktop_menu_enable + the qt companion under QT_QPA_PLATFORM=offscreen without faulting; companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. * ui/companion: move thumbnail downloads out of the Qt companion Phase 2, eighth subsystem. The single-thumbnail and thumbnail-pack downloads - server URLs and per-component percent-encoding, the label scrub the server expects, the .partial write + rename, the pack's "delete or rename aside with .tmp" replacement pass, the decompress task and its source cleanup, plus three retro_task callbacks and a userdata struct - were ~430 lines of Qt. They are C89 in the core now: - companion_core_thumbnail_download(db_name, label, subdir) - companion_core_thumbnail_pack_download(db_name) (download + extract) - companion_core_download_cancel() / _active() - on_thumbnail_downloaded / on_thumbnail_pack_finished callbacks, the latter carrying a companion_download_result the UI maps to its messages (network / rename / delete / empty archive / extract) One transfer is in flight at a time, as before. The task user data is owned by the task chain; the core only points at it and clears the back-pointer when the transfer is cancelled or the core is freed, so a late callback frees the data and notifies nobody. Qt keeps its queues (per-type list for one entry, per-playlist list), counters and the three progress dialogs; downloadThumbnail, downloadAllThumbnails and downloadNextPlaylistThumbnail are now a core call plus the dialog. onExtractArchive, the cb_* callbacks, qt_download_userdata_t, m_currentHttpTask, the extraction progress dialog and scrub_qstring are gone. A cancelled transfer no longer touches the UI at all. Verified: full Qt 6 retroarch links on Linux and starts headless with the qt companion; companion_core.c with and without HAVE_NETWORKING and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. The actual transfers are not exercised here (no thumbnail server access from this box). * ui/drivers: Win32 companion keeps RetroArch running while dragged, gets a splitter; Qt gains Scan Directory / Quit Win32 companion: - Dragging or sizing the companion window (or browsing its menu bar) froze RetroArch's video: DefWindowProc runs those loops modally on the main thread, and only the main window's WndProc clocked the run loop through them. win32_common.c's modal clock (win32_modal_enter / _exit / _tick, the WM_RA_MODAL_TICK message and WIN32_MODAL_TIMER_ID) is now exported, and the companion window routes WM_ENTERSIZEMOVE / WM_ENTERMENULOOP / WM_EXITSIZEMOVE / WM_EXITMENULOOP and the ticks to it exactly as the main window does; win32_modal_window_destroyed() ends a session if the clocked window goes away mid-drag. - The playlist pane was fixed at 200 px. A 5 px splitter between the pane and the entry list now drags (IDC_SIZEWE, capture, clamped to 100 px from either edge, re-clamped on window resize). Qt companion: File menu gains "Scan Directory" (QFileDialog directory picker -> companion_core_request_scan) and "Quit RetroArch" (CMD_EVENT_QUIT through the core), matching the native companions' menus. Both use existing menu strings. The two native callback tables spell out the new thumbnail callbacks as NULL (they were relying on zero-initialisation; -Wextra flagged it). Verified: ui_win32_companion.c and win32_common.c pass tools/mingw_syntax_check.sh (the one consumer of win32_common.h that fails, uwp_egl_ctx.c, fails identically on the branch tip); full Qt 6 retroarch links and starts headless with the qt companion. * verbosity: mirror the log into any desktop companion, not just Qt The companion log path in verbosity.c was Qt-specific twice over: it was gated on HAVE_QT, and it replaced the platform sink - on a Qt build the line was formatted into a buffer, written to the log file from there and handed to ui_companion_driver_log_msg(), while the macOS terminal/asl path and the plain vfprintf path were skipped entirely. Now: - verbosity.c includes ui_companion_driver.h (RARCH_INTERNAL) and gates on HAVE_COMPANION_WIMP, so the native Win32 and Cocoa companions get the log too - the companion copy is orthogonal to the platform sink: it is formatted from a va_copy of the arguments and the normal output below still happens unchanged (macOS Qt builds regain their Terminal/asl output). A va_copy fallback (__va_copy, or plain assignment) keeps the file C89-clean; the linux-c89 job compiles it - nothing is formatted unless someone is listening: ui_companion_driver_log_active() (setting + inited + is_active + log_msg present) is asked once per line, and ui_companion_driver_log_msg() then delivers without repeating the setting lookup and is_active() round trip - the previous code paid both on every RARCH_LOG whenever Qt was compiled in, even with the desktop menu disabled - the __WINRT__ buffered path is unchanged, just no longer shared with Qt The native companions gain the log view the plan asks for (View > Log on Win32: a read-only multiline EDIT appended via EM_REPLACESEL at the end, trimmed by half in one cut past 256 KiB so appends stay O(line); Companion > Log on Cocoa: an NSTextView added under the split while shown, same trim rule). The 10.4 NSInteger fallback grows NSUInteger. Verified: verbosity.c and ui_companion_driver.c compile with the linux-c89 flags; all 15 consumers of ui_companion_driver.h pass tools/mingw_syntax_check.sh; ui_win32_companion.c is mingw -Wall -Wextra clean; full Qt 6 retroarch links and runs headless with --verbose (the mirror path exercised into Qt's log widget). * ui/companion: move the core picker filter and the core-information rows out of the Qt companion Phase 2, ninth subsystem: everything the Qt companion still computed from core_info itself. - LoadCoreWindow::initCoreList filtered the core list by gathering the content's extension plus every archive member's extension into a QStringList and matching it against each core's supported_extensions string. That was a Qt re-implementation of core_info's own matcher; companion_core_installed_cores_supporting(content_path) now calls core_info_list_get_supported_cores() - the exact rule the menu uses, directories included - and reports how many cores lead the list. The table is built from the installed-core accessors (a _version accessor is added; _name falls back to the core file's base name, as the table did) and hides the rows past that count. MainWindow::loadContent's CORE_SELECTION_ASK path no longer walks archives; it passes the path. - The core-information collector (~240 lines: name / version / system / authors / permissions / licences / extensions / firmware status with per-row colouring / notes) was already toolkit-free C living in ui_qt.cpp under a qt_ prefix. It moves verbatim into the core as companion_core_core_info_rows() with companion_core_info_row_status; MainWindow::getCoreInfo keeps only the HTML/CSS decoration of the firmware rows. Both are what the native companions' Load Core dialog and core-info panel will consume. Verified: full Qt 6 retroarch links and starts headless with the qt companion; companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean). * ui/companion: one window hand-off and one start-core path for all three companions Two pieces of RetroArch-facing logic were still written three times, once per backend, and once in Qt only: - Raising the companion window meant "release the mouse grab, show the cursor, leave fullscreen" in ui_qt.cpp, ui_win32_companion.c and ui_cocoa_companion.m, each reading input_state / video_state / settings itself. companion_core_prepare_show_window() is that sequence; companion_core_video_started_fullscreen() answers Qt's "stay behind the video window" question. The two native files drop their video_driver.h / input_driver.h includes. - Starting a contentless core: Qt cleared RARCH_PATH_BASENAME and pushed task_push_start_current_core; the native companions had been wired to companion_core_request_load(NULL, NULL), which is a different task. companion_core_start_core() is the Qt sequence, and all three use it (native ones report failure in the status bar). getSelectedCorePath's CORE_SELECTION_CURRENT reads the core path from the core accessor; the Win32 Quit item goes through companion_core_event_command like the Qt one. ui_qt.cpp no longer calls command_event, input_state_get_ptr or video_state_get_ptr. Verified: full Qt 6 retroarch links and starts headless with the qt companion; companion_core.c, ui_win32_companion.c (mingw -Wall -Wextra) and the consumers of companion_core.h pass tools/mingw_syntax_check.sh. * ui/drivers: Load Core window for the native Win32 and Cocoa companions Parity with Qt's LoadCoreWindow: File -> Load Core... in both native companions now opens the companion's own picker - a Name / Version list of the installed cores from companion_core_installed_core_* - with Load / Cancel buttons and double-click, and loads through companion_core_load_core(). Previously the Win32 item forwarded to the main window's file-open dialog and the Cocoa item to the app delegate's openCore:, i.e. a raw .dll / .dylib chooser. Win32: a second top-level window owned by the companion (stays above it, hides with it, WS_EX_TOOLWINDOW), SysListView32 report view sorted by name with the core index in lParam so a sorted row still maps back, and the same modal-loop clocking as the companion window so dragging it does not stall RetroArch. Cocoa: an NSWindow with an NSTableView fed by the same controller (keyed on the table object), created on first use. Load Content keeps using the platform driver's dialog. Verified: ui_win32_companion.c passes tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); apple_mrc_ownership_check.py reports only the known local NSOpenPanel hit. Not run here (no Windows / macOS). * ui/drivers: core-information pane for the native Win32 and Cocoa companions Parity with Qt's core-info dock: View -> Core Information (Win32) / Companion -> Core Information (Cocoa) shows the rows companion_core_core_info_rows() produces for the running core - name, version, system, authors, permissions, licences, extensions, firmware status, notes - as a two-column key / value list beside the entries. Win32: a SysListView32 pane on the right of the entry list; it yields when the window is too narrow for three panes. Cocoa: a third pane that joins the NSSplitView while shown, rows cached in two string_lists. Only the shader commands are forwarded to companions, so a core load or unload is not an event a companion sees; both panes instead follow the running core from the per-frame iterate hook with one short strcmp while shown, refilling only when the path changed. Verified: ui_win32_companion.c passes tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); apple_mrc_ownership_check.py reports only the known local NSOpenPanel hit. Not run here (no Windows / macOS). * ui/drivers: icon view with thumbnails for the native Win32 companion Parity with Qt's grid view. View -> Icons switches the entry list to LVS_ICON with 128 px boxart thumbnails; View -> List returns to the report view. Thumbnails resolve through companion_core_thumbnail_path() (the same rule as Qt: repository layout, sanitised label, extension probe, image content as its own thumbnail) and decode through image_texture_load(), whose ARGB8888 output is exactly the 32-bit BGRA DIB order Windows wants, so a thumbnail is a nearest-neighbour letterbox into a CreateDIBSection and an ImageList_Add. Never blocks: the image list starts with a placeholder at index 0 for every item, and the iterate hook decodes one pending thumbnail per frame while the icon view is showing - one file decode per frame however long the playlist. Decoding is sequential from the top for now; prioritising the visible range is a follow-up. Everything used exists on Windows 95 with comctl32 4.70 (ImageList_*, LVS_ICON, LVM_SETICONSPACING); ILC_COLOR32 degrades to what the installed comctl32 supports. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean, linux c89 pass skipped for the Win32 path as the script does). Not run here. * ui/companion: destroy the desktop companion window at quit, before the drivers go away Quitting from RetroArch's own menu with the native Win32 companion open left the process hung ("Not Responding") with an undestroyed window: the companion window is a top-level window on the main thread, and the only thing that destroyed it was ui_companion_driver_deinit(), which main_exit() runs after driver_uninit(DRIVERS_CMD_ALL) has already torn down the video driver - i.e. after the message pump that services the window (ui_application_win32_process_events on the main thread, or the video thread under threaded video) is gone. DestroyWindow() then has no pump to run the window's WM_DESTROY through, and shutdown stalls. Fix: close the companion window while its pump and the drivers are still alive. ui_companion_driver_wimp_deinit() is split out of ui_companion_driver_deinit() (which now calls it), made idempotent, and invoked right after the run loop breaks - in retroarch.c's retroarch_main_loop and in ui_cocoa.m's rarch_main - before main_exit(). main_exit()'s existing ui_companion_driver_deinit() still runs and is now a harmless second call for the WIMP part while still tearing down the platform driver. This is the Qt companion's behaviour too (it lived inside the same deinit), so no change there; it fixes the native Win32 and Cocoa companions, whose windows previously outlived the pump. Verified: retroarch.c, ui/ui_companion_driver.c and all 15 consumers of ui_companion_driver.h pass tools/mingw_syntax_check.sh; full Linux build links. The hang itself is Windows-only and not reproducible here. * ui/drivers: icon (grid) view for the native Cocoa companion Parity with Qt's grid and the Win32 icon view. Companion -> Icons swaps the entries table in its scroll view for a thumbnail grid; Companion -> List swaps it back. NSCollectionView is 10.5+ and the baseline is 10.4, so the grid is a hand-laid flipped NSView (RACompanionGrid): fixed 128 px cells flow-laid to the width, each drawing a letterboxed boxart above a truncated label, with mouse and arrow / Return selection and double-click to load. The scroll view is reused, so no second data path. Thumbnails resolve through companion_core_thumbnail_path() - the same rule as everywhere else - and load as NSImage; like the Win32 icon view, one is decoded per frame from the iterate hook while the grid is showing, so a playlist of any size never costs more than one decode per frame. One NSImage per row is cached and drawn as it arrives. Verified: apple_mrc_ownership_check.py reports only local autoreleased objects used within their pool (the drawRect: attributes dictionary and the existing NSOpenPanel), neither stored past the pool. Not compiled here (no Apple toolchain). * ui: pick a core on Run when a playlist entry has none (native companions) When a playlist entry's core is empty or "DETECT" and no core is loaded, Run silently failed on the native companions (Qt shows its core-selection combo / Load Core window). companion_core_entry_needs_core() reports that case and hands back the content path; both native companions then open their Load Core window filtered to the cores that can run that content, and the pick launches the content with the chosen core through companion_core_request_load_content(). Win32: cw_run_selected checks first and calls cw_cores_show(content); the Load Core window carries the pending content, filters its list with companion_core_installed_cores_supporting(), and its OK path runs the content instead of just loading the core. Cocoa: runSelected and gridRun do the same via -showCoresForContent:; the window build is split into -buildCoresWindow so it is populated and raised once. Plain File -> Load Core is unchanged (no content, full list, loads the core only). Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); the consumers of companion_core.h pass; apple_mrc_ownership_check.py reports only the two known local-autorelease hits. Not run here (no Windows / macOS). * ui: file-system browser for the native Win32 and Cocoa companions Parity with Qt's File Browser tab: File -> Browse Files (Win32) / Companion -> Browse Files (Cocoa) turns the entry list into a filesystem browser - directories first with a ".." parent link, a directory descends, a file loads through the current core (or, when the file has no usable core, opens the Load Core picker filtered to it). Picking a playlist returns to playlist mode. The listing and navigation are in the core, toolkit-free: companion_core_browse_open() (dir_list_new + dir-first sort, defaulting to the content directory), _browse_count/_name/_path/_is_dir with a synthesised ".." entry, and _browse_activate() which descends, loads, or reports needs-core with the content path for the picker - reusing companion_core_request_load_content(), so the same core-path sanitising and quick-menu behaviour as everywhere else. Backends only present it: Win32 reuses the entries list view (trailing slash marks directories), Cocoa the entries table; both branch their rebuild / run on a browse_mode flag and leave it on playlist select. Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean, linux c89 pass); the consumers of companion_core.h pass; apple_mrc_ownership_check.py reports only the two known local-autorelease hits. Not run here. * tools: companion backend feature-parity check (Phase 6) The refactor's final phase requires the three desktop companions - Qt, native Win32, native Cocoa - to stay interchangeable: "same actions produce the same results". Nothing enforced that. A backend can omit a whole feature (file browser, scan, core picker, core-info panel) and still compile and run; the gap only surfaces when a user switches the Companion UI driver and finds a menu entry gone. companion_parity.py defines the feature set by the companion_core_* entry point each feature needs and checks every backend references it - the same source-level, zero-cost, Linux-only shape as vfs_backend_parity.py. Qt's two presentation TUs are checked together; where Qt implements a feature a toolkit-native way (playlist listing and the file browser through its own QDir / QFileSystemModel, Run and the core picker through the launch-with combo) the equivalent call is recorded in QT_EQUIVALENT with a reason. A backend that genuinely should not carry a feature goes in WAIVERS, so dropping one is a reviewed choice, not an oversight. --list prints the matrix. Wired into Linux-libretro-common-tests.yml beside the VFS parity check. Confirmed it passes on the current three backends (19 features) and fails when a feature call is removed from one. This is Phase 6's parity gate as an enforced check rather than a static checklist that would drift; the remaining visual / behavioural parity pass stays manual. * ui/drivers: Win32 companion opens at the Qt companion's startup size The Qt companion opens at 1280x720 centred on the primary monitor, clamped to it; the native Win32 companion opened at CW_USEDEFAULT with a fixed 800x520 fallback - a much smaller window in a different place, so the two look nothing alike on first F5. Match Qt: 1280x720, centred on the work area (screen minus taskbar), clamped to it. Made scalable for small displays, which the 9x baseline can hit (640x480 / 800x600 CRTs): the size is clamped to the work area and only floored to the window's own minimum when the work area is large enough for it, so it never opens larger than the screen. SPI_GETWORKAREA falls back to the full screen metrics when it fails, as it can on 9x. The default view stays the report list, which matches the Qt companion's own default (VIEW_TYPE_LIST); the grid in the reference screenshot was a persisted user choice, not the startup state. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean). Not run here (no Windows). * ui/drivers: search box and Edit menu in the native Win32 companion Visual/behavioural parity with the Qt companion, which has a Search box and an Edit -> Search action. The Win32 companion gains an EDIT strip above the entry list and an Edit menu with Search (Ctrl+F focuses the box, matching Qt's shortcut). Typing filters the current content view - playlist entries or the file browser - by case-insensitive substring, the same field Qt's proxy model filters on. Filtering makes list-view rows non-contiguous with entry indices, so the entry index now rides in each row's lParam and the readers use it: a new cw_selected_entry() resolves the selection through lParam (browse rows are never filtered, so they stay row == index), and the lazy thumbnail decode walks visible rows and reads each row's lParam. The image list is sized to the full count and installed before items reference it; the per-frame decode budget is unchanged. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean, linux c89 pass for the shared paths). Not run here (no Windows). * ui/drivers: boxart preview pane for the native Win32 and Cocoa companions Parity with the Qt companion's Boxart dock. View -> Boxart (Win32) / Companion -> Boxart (Cocoa) shows the selected playlist entry's boxart, updated as the selection moves. It resolves through the same companion_core_thumbnail_path() rule as the grid, so image content and the repository layout behave identically. Win32: a STATIC (SS_BITMAP) in the right column below the core-info pane - the column appears when either is shown and splits between them. The image is decoded on LVN_ITEMCHANGED, scaled to the pane preserving aspect into a 32-bit DIB (image_texture_load's ARGB8888 is Windows' BGRA order), and the previous HBITMAP is freed. Cocoa: an NSImageView on the right edge (proportional scaling), refreshed from the entries table's selection-changed delegate. Nothing decodes while the pane is hidden or in file-browser mode, and only the selected entry is decoded, so there is no per-frame cost. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); companion_parity.py green; apple_mrc_ownership_check.py reports only the two known local-autorelease hits. Not run here (no Windows / macOS). * ui/companion: lead the playlist list with the special playlists The Qt companion's left pane starts with Favorites, History, Images, Music and Videos above the system playlists; the native companions, which build their list from companion_core_playlist_count/_name/_path, only saw the .lpl files in the playlist directory - so History and Favorites were unreachable there. The core now returns those five as leading playlist entries, in the Qt order, each a real .lpl at its configured path (path_content_favorites / _history / _image_history / _music_history / _video_history); a path left empty in the config is skipped, so the lead count is not always five. Names come from the existing *_TAB label strings. Selection, delete and core-association already key off the public index / path, which now map through the specials, so they follow without change; select_playlist_path matches against the public paths too. Qt is unaffected: it builds its own special-playlist items (including the synthetic "All Playlists", which is not a file and stays a presentation concern) and does not enumerate through these accessors. Verified: companion_core.c and the consumers of companion_core.h pass tools/mingw_syntax_check.sh; companion_parity.py green. * ui/drivers: Win32 companion puts the search box in the left column The Qt companion's search field sits at the top of the left content column, above the playlist list; the native Win32 companion had it above the entry list on the right. Move it to match: the left column is now the search box on top and the playlist list below it, and the entry list spans the full height on the right, so the two windows line up. Layout only - the EDIT still filters the current content view the same way, and the splitter, browse, info and boxart panes are unchanged. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); companion_parity.py green. Not run here (no Windows). * ui/drivers: lay the Win32 companion out like the Qt companion The Qt companion is the reference look; the native Win32 window had the same features behind a different shape. This makes its structure match the Qt startup layout, using classic controls only (all on Windows 95 with comctl32 4.70) and the same msg_hash strings Qt shows: Left column, as Qt's docks: Search [edit][Clear] Content Browser [Playlists | File Browser] (a tab control; the second tab enters the file browser, the first returns) [playlist list] (a list view with the shell folder icon per row, like Qt's, replacing the LISTBOX) Core [launch-with combo][Info][Run] The Core section is Qt's launch-with combo: companion_core_launch_options for the selected entry (running / entry's own / playlist default core), plus "Ask" and "Load Core..."; Run launches with the chosen core, Ask and Load Core open the picker. This was the last Qt behaviour the natives lacked - they previously always asked. Centre: the entry list over Qt's footer ("N items" from QT_ITEMS_COUNT on the left, a View: List / Icons combo on the right). Right column: "Core Info" and "Boxart" captions over their panes, both shown by default as Qt does. Menubar: File / Edit / View / Help with Qt's titles; Help gains Documentation (docs.libretro.com) and About. Status bar defaults to " - " and follows the running core. Every control is set to DEFAULT_GUI_FONT - the bold bitmapped System font the controls default to was most of why the window looked nothing like Qt. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); companion_parity.py green. Not run here (no Windows) - the pixel pass against the Qt screenshot needs a real box. * ui/drivers: Win32 companion scales with the display DPI like Qt The side-by-side on a 200% display showed the Win32 companion at half the Qt companion's size with captions overlapping their controls and text truncated everywhere. Cause: Qt sizes in logical (96-dpi) pixels and renders them at the monitor's DPI - its 1280x720 fills a 2560x1440 screen - while the native window used the same numbers as raw pixels, and its DEFAULT_GUI_FONT is a fixed 8pt bitmap that does not scale, so the rows sized for it could not hold the text the display wanted. Now everything scales the way Qt does: - LOGPIXELSY is read once and every logical size (startup 1280x720, minimums, pane and column widths, splitter, boxart height, icon size, icon-view spacing, padding) goes through MulDiv against 96 - the font is the system message font from NONCLIENTMETRICS, which is the DPI-scaled UI font (Segoe UI at the display's size on modern Windows, MS Sans Serif on 9x), created once and freed on deinit; the stock font remains the fallback - row heights derive from that font's TEXTMETRIC (caption = one line, control = line + chrome) instead of fixed 18/24 - list columns are sized to their panes on every layout (playlist column to the pane, Name/Core 2:1 across the entry list, info key column narrow with the value taking the rest), so nothing truncates; the info list drops its column header as Qt has none - the left pane defaults to 280 logical px, Qt's dock width All APIs used date from Windows 95 (GetDeviceCaps, CreateFontIndirect, SPI_GETNONCLIENTMETRICS, MulDiv). Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); companion_parity.py green. Not run here (no Windows). * ui/drivers: fix the zero-size Win32 companion window and the Xcode build Win32: the DPI-scaling commit read w->dpi before it was set. The DPI / font block sat after the main window's size was computed and the window created, so CW_S() ran MulDiv(x, 0, 96) = 0 for the startup size and the WM_GETMINMAXINFO minimums - a zero-size window, the near-invisible strip in the report. Move the DPI / font setup to the top of cw_create_window, before anything is sized. Cocoa: the Xcode (ARC) build failed on the core-info pane's construction - "passing address of non-local object to __autoreleasing parameter for write-back": an ivar (infoScroll) was used as the scroll-view out-parameter of -makeTable:scroll:twoColumns:. Under ARC such a parameter is __autoreleasing and must be a local; go through one and assign it to the ivar (ARC retains the strong ivar, KEEP_IVAR adds the retain under MRC). The other three callers already used locals. Also declare the controller's protocols (NSTableViewDataSource / Delegate, NSWindowDelegate, NSMenuDelegate) when the SDK is 10.6+, so the four setDataSource: / setDelegate:self "incompatible type" warnings in that build go away; older SDKs use the informal protocols and keep the plain declaration. Verified: ui_win32_companion.c passes tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); companion_parity.py green; apple_mrc_ownership_check.py unchanged (the two known local hits). * ui: playlist icons, single-column core info, 50/50 right column on Win32 (Qt parity) Second side-by-side pass at 200% DPI, Qt leading. The window now opens at the same size with legible controls; the remaining differences were: - Playlist icons. Qt shows the XMB dot-art asset for each system (/xmb/dot-art/png/.png, folder.png as fallback) at a large row height; Win32 showed a 16 px shell folder on every row. The asset rule moves into the core (companion_core_playlist_icon_path, so Cocoa gets the same icons) and the Win32 list decodes each icon into a 32 px image list through the existing scaler, which also gives the rows Qt's height. The shell folder stays at index 0 as the fallback when the asset set is not installed. Icons are rebuilt with the list, i.e. on playlist-directory changes only, not per frame. - Core Info. It was a two-column key / value list, so the no-core text (a key with no value) truncated at the key column, and long firmware lines were cut. Qt renders "Key: value" as one text line per row; do the same in a single wide column that scrolls horizontally instead. - The right column splits 50/50 between Core Info and Boxart, as Qt's two docks do, rather than a fixed boxart height. - Entry columns titled "Name" / "Core" (QT_NAME / QT_CORE) rather than the entry-dialog labels with their colons; the footer drops the separate "View" caption Qt does not have. Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); consumers of companion_core.h pass; companion_parity.py green. Not run here. * ui: the main loops and the Win32 window stop hard-coding Qt as "the" companion Sweep of HAVE_QT / HAVE_QT6 outside the Qt driver itself. Most sites are legitimately Qt-specific (griffin_cpp's TU list, the feature-list strings, the griffin HAVE_MAIN hack, config.features.h, Qt's own entry in the companion driver table, and ui_cocoa.m choosing rarch_main over the draw observer because of Qt's event loop) and stay. Five were companion-generic behaviour wired to Qt alone: - retroarch.c main loop: `ui_companion_qt.application->exiting` and `->quit()` under HAVE_QT. That is "the active desktop companion asked to exit / tell it to quit", whichever driver is selected. New ui_companion_driver_wimp_exiting() / _wimp_quit() route it through uico_st->wimp; ui_cocoa.m's rarch_main and cocoa_common.m's draw observer use the same two calls (the observer path also gains the wimp deinit before main_exit, the ordering fix the Win32 quit hang needed, which it had missed). - ui_win32.c: the main window's "Toggle Desktop Menu" item (and its label lookup) existed only #ifdef HAVE_QT, so a native-companion build had no menu entry to open its companion. Now HAVE_COMPANION_WIMP. - configuration.c: the ui_companion_toggle SETTING_BOOL was gated on HAVE_QT || HAVE_COCOA - the pre-refactor condition, missing desktop Win32 and HAVE_COCOA_METAL. Now HAVE_COMPANION_WIMP. - settings_def_menu_main_lists_2.h: the SHOW_WIMP ("Show Desktop Menu") action row was HAVE_QT only, so the main menu had no way to open a native companion. Guard widened to the HAVE_COMPANION_WIMP condition, spelled out as settings_def cannot include the header. tools/settings_table.reference regenerated (unchanged - the desktop profile already defines HAVE_QT). Behaviour for Qt builds is identical (same calls, through the driver slot). Verified: settings table --check, unity (3 profiles) and migrate selftest pass; all 15 consumers of ui_companion_driver.h, configuration.c, ui_win32.c and retroarch.c pass tools/mingw_syntax_check.sh; full Qt 6 retroarch links and starts headless; companion_parity.py green. * ui/companion: native companions read and write retroarch_qt.cfg The Qt companion keeps its own settings in retroarch_qt.cfg beside retroarch.cfg (QSettings, INI form); the native Win32 and Cocoa companions ignored it, so a view type, thumbnail type, start playlist or "suggest loaded core first" set in one did not carry to the others. All three now share that file. RetroArch's config_file cannot read it: its key scan takes every graph character as part of the key, so QSettings' unspaced `key=value` becomes a key with no '=' and the line is dropped - confirmed with a harness, it parsed zero entries from a real retroarch_qt.cfg. Rather than touch that hot parser, the core gets a small dedicated reader/writer for the dialect: "[section]" and comment lines skipped, split at the first '=', values verbatim, written back in QSettings' own shape so Qt reads it. Unknown keys (Qt's @ByteArray geometry, theme...) survive a rewrite. Verified with the harness: every key read, a set-and-reread round trip, and the geometry blob preserved. companion_core_setting_get / _get_bool / _get_int / _set / _set_bool Both native companions honour the keys Qt writes: view_type list / icons at startup; persisted on switch icon_view_thumbnail_type boxart / screenshot / title / logo for the icon view and the boxart pane initial_playlist opened at startup, History as the fallback (Qt's default), applied once suggest_loaded_core_first launch-with combo ordering show_hidden_files directory scan save_last_tab / last_tab restore the Playlists / File Browser tab; persisted on change when save_last_tab is on Reads are a file load per call: only at startup and on user actions, never per frame. Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); consumers of companion_core.h pass; full Qt 6 retroarch links; companion_parity.py green; apple_mrc_ownership_check.py unchanged. Not run here. * ui: companion settings move from retroarch_qt.cfg into retroarch.cfg; Qt-isms dropped The Qt companion kept its presentation settings in a private QSettings INI, retroarch_qt.cfg, and the previous commit taught the natives to read that file. One config is cleaner: the values become real RetroArch settings in retroarch.cfg, saved with everything else, visible in the Settings menu, and read by all three companions through settings_t. The retroarch_qt.cfg reader/writer added last commit is removed again. New settings (settings_def_desktop_menu.h, same HAVE_COMPANION_WIMP guard; menu placement beside desktop_menu_enable; tools/ settings_table.reference regenerated, 19 rows): desktop_menu_view_type uint 0 list / 1 icons desktop_menu_thumbnail_type uint boxart/screenshot/title/logo desktop_menu_suggest_loaded_core_first bool desktop_menu_save_last_tab / _last_tab bool / uint desktop_menu_save_geometry bool desktop_menu_window_x/_y/_width/_height uint - the window geometry as plain ints, replacing Qt's QByteArray saveGeometry() blob desktop_menu_show_welcome_screen bool desktop_menu_scan_finish_confirm bool desktop_menu_thumbnail_cache_limit / _max_size / _quality uint desktop_menu_icon_view_zoom uint desktop_menu_all_playlists_list_max_count / _grid_max_count uint desktop_menu_theme uint system/dark/custom desktop_menu_initial_playlist, desktop_menu_custom_theme (SETTING_PATH) desktop_menu_hidden_playlists (comma list), desktop_menu_highlight_color ("#rrggbb") (SETTING_ARRAY) Qt's private copy of show_hidden_files is dropped for RetroArch's own show_hidden_files. Qt-isms removed rather than carried: the QByteArray blobs for dock positions (and its View Options checkbox), the file-browser table header state and the options-dialog geometry are gone; window geometry restores from the four ints. QSettings is no longer used anywhere: the member, its construction, MainWindow::settings() and the two widget copies are deleted, and every read/write goes through settings_t (the View Options dialog included). Writes take effect immediately and reach disk when RetroArch saves its config. The companion core exposes the shared values to the backends as typed accessors (companion_core_pref_*), including the thumbnail-type -> repository-subdir mapping in one place; both native companions use them. Verified: settings table --check, unity (3 profiles) and migrate selftest pass; label strings regenerated (gen_lbl_str.py); consumers of companion_core.h, ui_win32_companion.c, configuration.c, menu_displaylist.c and menu_setting.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); full Qt 6 retroarch links and starts headless; companion_parity.py green; apple_mrc_ownership_check.py unchanged. * ui/companion: retroarch_qt.cfg is gone - one-time import into retroarch.cfg, then deleted The previous commit moved the companion settings into retroarch.cfg; this one makes sure the old file actually disappears. On the first companion start that still finds retroarch_qt.cfg beside retroarch.cfg, the core imports the values that have a home into settings_t and deletes the file, so it does not linger as dead configuration after an upgrade. Runs once per process at most. Imported (with the same value mapping the Qt companion used): view_type, icon_view_thumbnail_type, initial_playlist, suggest_loaded_core_first, show_hidden_files, save_last_tab / last_tab, save_geometry, show_welcome_screen, scan_finish_confirm, thumbnail_cache_limit / _max_size / _quality, icon_view_zoom, all_playlists_list/grid_max_count, theme, custom_theme, highlight_color, hidden_playlists (QStringList's ", " separators normalised to ","). Dropped with the file: the Qt blobs (geometry, dock_positions, options_dialog_geometry, file_browser_table_headers). Stale comments naming retroarch_qt.cfg in the natives, config.def.h, settings_def_desktop_menu.h and menu_displaylist.c are updated; no source mentions the file any more except this migration. Verified with a harness against a real QSettings-shaped file: every key lands as the right typed value, blobs are skipped, the file is removed (a whole-process run confirmed the import log line and the deletion; the exit-time config save could not be exercised headless). companion_core.c passes tools/mingw_syntax_check.sh; consumers of companion_core.h pass; full Qt 6 retroarch links; companion_parity.py green. * ui/drivers: Win32 companion - core combo and Core Info work, icon alpha, zoom, Boxart tabs Five defects from the side-by-side, all in the Win32 companion: - The Core combo dropped down to nothing. A COMBOBOX's window height is the height of its *dropped list*, and the layout sized both combos to one closed row, so the list had no room. They are now given list room (the closed control stays one row tall regardless). The combo was in fact populated; it just could not show its items. - Core Info always read "No Core Information Available". The pane followed the running core; Qt shows the core picked in the Core combo (the entry's own or the playlist default). It now does the same: cw_combo_core_path() resolves the combo's selection and the pane refreshes on entry selection, on combo change, and when that core changes. - Playlist icons sat on black. The scaler forced alpha to opaque, so transparent XMB PNGs kept whatever was under them. It now composites the source alpha over the control colour, giving an opaque result every comctl32 draws identically (v6 alpha and 9x mask-less alike). - No thumbnail zoom and a fixed thumbnail size. The icon-view edge now comes from desktop_menu_icon_view_zoom (0..100 -> 64..320 logical px, DPI-scaled), with Qt's Zoom slider (msctls_trackbar32) and Thumbnail type combo in the footer; a change rebuilds the image list and redecodes. New companion_core_pref_icon_view_zoom / _thumbnail_type accessors (and setters) back both. - No way to switch the boxart pane's type. It gets Qt's four tabs (Boxart / Title Screen / Screenshot / Logo) under its caption; the pane keeps its own type, independent of the grid's Thumbnail combo, as Qt's dock does. Also: the status bar returns to " - " once a playlist has loaded instead of staying on "Loading playlist...". Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); consumers of companion_core.h pass; full Qt 6 retroarch links; companion_parity.py green. Not run here (no Windows). * ui/drivers: fix the Win32 icon view's cell spacing (grid came up scrambled) After the zoom change the icon view laid items out in cells a few pixels wide: labels wrapped to three characters and items piled on top of each other. The cell spacing was computed from w->thumb_px, which is only set when the thumbnail image list is (re)built - but at startup the view type is applied before the first playlist builds it, so the spacing was 0 + 24 by 0 + 40 px. A zoom or thumbnail-type rebuild then resized the image list without ever reapplying the spacing either. The spacing now comes from the live thumbnail edge whenever it is applied (cw_icon_spacing_apply), is reapplied every time the image list is rebuilt at a new size, and the grid is re-arranged (LVM_ARRANGE) after a rebuild or a view switch so existing items take the new cells. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); companion_parity.py green. Not run here (no Windows). * ui: "All Playlists" for every companion, and it is where all three start Qt's playlist list begins with All Playlists - the union of every playlist - and that is the view it opens on. The native companions had neither, so the two windows came up on different pages and the natives could not show the combined library at all. The aggregate is in the core so every backend shares it. Public playlist index 0 is now All Playlists (name from QT_ALL_PLAYLISTS, path the token "|||ALL|||" Qt has always used, so desktop_menu_initial_playlist can name it and the setting means the same thing everywhere); the specials and files follow. Selecting it parses every playlist file in turn under the same per-frame budget as a single playlist - one file's parse finishes, the next begins, the view updates once at the end - and the entries are referenced through a (list, index) table rather than copied, then sorted by label (strcasecmp) as Qt sorts, and capped by the larger of desktop_menu_all_playlists_list/grid_max_count (0 = no cap). Edits under the aggregate go to the entry's own file: new companion_core_entry_playlist_path() / _entry_index_in_playlist() resolve an aggregate row to the file it came from and its index there; the Win32 and Cocoa delete actions use them, and playlist_open() rejects the token so nothing can ever try to write "|||ALL|||.lpl". Startup: with desktop_menu_initial_playlist empty, Qt, Win32 and Cocoa all open All Playlists (Qt's History fallback goes too, so the three agree). The All slot shows the folder icon like the specials. Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); consumers of companion_core.h pass; full Qt 6 retroarch links; companion_parity.py green; apple_mrc_ownership_check.py unchanged. The aggregate itself needs a run with real playlists (it cannot be driven headless here). * ui: select the first entry on load, and All Playlists is uncapped by default Two more from the side-by-side, Qt leading: - Qt selects the first entry of a freshly loaded playlist, so the boxart pane and the Core section have content immediately; the natives left nothing selected and the panes empty. Win32 now selects row 0 after the entry list is rebuilt (LVN_ITEMCHANGED drives boxart, core combo and core info from there); Cocoa selects row 0 in the table and the grid. - All Playlists showed "1000 items" against Qt's 42116. The cap I gave it took the larger of the list / grid max counts, and I had invented a grid default of 1000. Qt in fact never applies either count (the setters are set-only and their spin boxes are commented out), which is why it shows everything. The grid default is now 0, and a cap applies only when both counts are set (0 = no cap on that side), then the larger - so the default is uncapped like Qt, while a user who sets both still gets a bound. tools/settings_table.reference updated for the default. Verified: settings table --check, unity (3 profiles) and migrate selftest pass; companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); full Qt 6 retroarch links; companion_parity.py green; apple_mrc_ownership_check.py unchanged. * ui: align companion guards with master's HAVE_COCOA; unretained grid back-pointer * ui/drivers: bring the Cocoa companion up to the Qt / Win32 layout and feature set The native Cocoa companion had fallen well behind: two bare tables in a split view, no launch-with core, no zoom, no thumbnail or boxart type switches, no search, no playlist icons, Core Info following the running core, and its own status text. This puts it on the same footing as the Win32 companion, Qt leading: Layout (explicit frames recomputed in -layoutViews on resize; 10.4-safe, no autolayout; points, so the display scales it as it does Qt): Left column, as Qt's docks: Search [field][Clear]; Content Browser with a Playlists | File Browser NSTabView (the second tab enters the file browser, the first returns); the playlist table with an icon column (XMB dot-art per system via companion_core_playlist_icon_path, the Finder folder as fallback) at Qt's row height; Core: launch-with NSPopUpButton + Info + Run. Centre: the entries table or grid over Qt's footer - "N items", Zoom slider, Thumbnail type popup, View (List / Icons) popup. Right column: Core Info caption + single-column "Key: value" list, Boxart caption + NSSegmentedControl for the four types + image view; both shown by default, 50/50, like Qt's docks. Window opens at 1280x720 centred, clamped to the screen; status bar reads " - " and follows the core. Behaviour, all through the shared core and its pref accessors: - launch-with popup from companion_core_launch_options plus Ask and Load Core...; Run launches with the choice, Ask / Load Core open the picker; Core Info follows the popup's core as Qt's does, refreshed on entry / grid selection and popup change - zoom slider drives the grid's thumbnail edge (64..320 pt) and persists desktop_menu_icon_view_zoom; thumbnail popup switches the grid's type and redecodes; boxart segments switch the pane's own type - search field filters the entry table (case-insensitive substring) through a row -> entry map, so run / delete / boxart / popup resolve the right entry under a filter; Clear resets it; Cmd-F focuses it - view popup and menu items stay in sync; last tab persisted when desktop_menu_save_last_tab is on; shared settings applied at start - grid selection now drives boxart / popup / info like table selection - Companion menu uses Qt's strings and gains Documentation The split view and the toggles that added panes to it are gone; Core Info / Boxart toggles show or hide within the fixed layout. Verified: apple_mrc_ownership_check.py unchanged (the two known local autoreleased objects); bracket / brace / paren balance checked by a character-level scan; every declared selector has an implementation; companion_parity.py green. Not compiled here (no Apple toolchain) - the Xcode build and a run are the real test for this one. * ui/drivers: fix the Cocoa companion build (undeclared -wimp, Carbon icon constant) CI on the Cocoa layout commit (95dee5f) fails the three Apple jobs - macOS (Xcode, clang/ARC) and macOS PowerPC cross 10.4 / 10.5 (GCC 14, 10.5 SDK, MRC) - while every Windows, MSVC, MXE, c89 and Linux job is green, so the breakage is confined to ui_cocoa_companion.m. Two errors, found by auditing the file against those toolchains (the job logs are not readable without a login from here): - RACompanionGrid's -drawRect: calls [owner wimp] to reach the core, but the controller never declared or defined a -wimp accessor. GCC rejects the unknown selector on a typed receiver; clang under ARC rejects the resulting id -> ui_companion_cocoa_wimp_t* conversion outright ("implicit conversion of an Objective-C pointer to a C pointer is disallowed with ARC"). Declared and defined. - The playlist folder fallback used kGenericFolderIcon, a Carbon constant from Icons.h that AppKit alone does not declare. The generic folder's HFS type is 'fldr'; pass the literal to NSFileTypeForHFSTypeCode (10.0), which the 10.4u import audit accepts. Re-checked: every selector sent to self / owner is now declared and implemented; all top-level C functions are static (the build carries -Werror=missing-declarations); bracket balance by character scan; apple_mrc_ownership_check.py unchanged; companion_parity.py green. * ui/drivers: fix the Cocoa companion build (Xcode log: shadowed grid edge, BOOL receiver) From the CI Xcode log on c2e0e39: - error: passing 'NSRect' to parameter of incompatible type 'CGFloat' (and five "invalid operands" errors following it) in RACompanionGrid -drawRect:. When the grid's fixed CC_GRID_THUMB became the zoomable `thumb` ivar, the rename collided with the method's existing local `NSRect thumb` (the thumbnail's box), so the edge and the box became one identifier. The box is now `trect`; the edge stays `thumb`. - error: bad receiver type 'BOOL': [[window makeFirstResponder:grid] self] messaged the BOOL that -makeFirstResponder: returns. Dropped the outer message. Also cleared the warnings the log shows in the same method: -setFlipped: (deprecated 10.6) is now only used below the 10.6 SDK; on 10.6+ the draw call takes respectFlipped:YES instead, which is the replacement Apple's notes describe. NSCompositeSourceOver goes through the compat name the defines header already provides, and the two NSTextAlignment* names are mapped to their pre-10.12 spellings under an SDK check so the 10.5-SDK PowerPC build sees them too. Re-checked: bracket balance by character scan; every selector sent to self / owner declared; apple_mrc_ownership_check.py unchanged. * ui/drivers: Win32 companion entry list goes virtual - grid builds instantly Building the icon view took minutes on a large library where Qt is instant. Three costs, all structural: the entry list was a retained list view, so populating it was one LVM_INSERTITEM (plus a sub-item set) per entry - 42k round trips through the control; LVM_ARRANGE then laid out 42k icon cells; and thumbnails were decoded top-to-bottom at one per frame regardless of what was on screen, so the visible screen only filled in once everything above it had (42k entries: twelve minutes). Qt's view is virtual: its model reports a count, cells are drawn on demand, thumbnails load for the cells actually shown. The entry list is now the Win32 equivalent, LVS_OWNERDATA (comctl32 4.70, Windows 95): - populating is a filtered row map (rows[]: visible row -> entry index) and one LVM_SETITEMCOUNT, whatever the size; no inserts, no arrange. The search filter builds the same map, so run / delete / boxart / core combo resolve through rows[] instead of a per-item lParam. - the control asks for each row's text and image as it draws (LVN_GETDISPINFOA - the ANSI notification, matching the ANSI control even under a UNICODE build); the browse view is served the same way, with the trailing backslash on directories. - thumbnails are decoded only for rows the control has asked to draw: GETDISPINFO queues a row that has no image yet, and the iterate hook drains the queue newest-first (the rows most likely still on screen) within a 4 ms budget, redrawing each row as its image lands. Nothing is decoded for rows never scrolled to. - memory is bounded: decoded thumbnails live in a ring of 512 image-list slots, recycled round-robin; an evicted row falls back to the placeholder and re-queues if it comes into view again. Previously the image list grew by one bitmap per entry (42k x 128 x 128 x 4 = 2.7 GB for a full pass). Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean), plus a -DUNICODE syntax pass; companion_parity.py green. Not run here. * ui/drivers: Win32 companion decodes thumbnails on a thread pool Still slower than Qt after the virtual list. The remaining gap is where the decode runs: Qt's ThumbnailLoader is a dedicated thread that runs image_texture_load (the same decoder) with no per-frame budget, and its UI thread only receives finished images. The Win32 companion decoded on the UI thread under a 4 ms/frame budget - a boxart PNG takes 5-20 ms, so about one thumbnail per frame, sharing that frame with emulation. Now the decode is off the UI thread, and arranged to come out ahead of Qt rather than level with it: - a pool of decode threads (cores - 1, at least one, at most four; libretro-common rthreads, so it builds wherever HAVE_THREADS does, 9x included) takes jobs newest-first - the rows most likely still on screen - and decodes in parallel. Qt has one thread. - each worker scales to thumbnail size itself and hands the UI thread a T x T ARGB buffer; the UI thread's per-frame work is a DIB and an image-list replace per finished thumbnail (~0.2 ms), never a decode. Qt caches the full-size pixmap and rescales it on every paint. - the request path resolves the file on the UI thread (the companion core is single-threaded), so a job needs nothing from the core. - a generation counter, bumped when the list, filter, zoom or thumbnail type changes, makes in-flight results stale; they are dropped when they land rather than blitted into the wrong row or size. - all cross-thread state (queues, row map, thumb_idx, thumb_px) is read and written under one lock; workers park on a condition and are woken by requests and by ring space; shutdown joins them. - without HAVE_THREADS the UI thread decodes under the old budget, so the driver still builds and works. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean) with and without HAVE_THREADS; companion_parity.py green. The pipeline itself (queue, ready ring, worker loop, generation handling, shutdown) was extracted into a Linux harness against the real rthreads and run under ThreadSanitizer: no races, every decode installed exactly once, stale results dropped, nothing leaked. The decode / GDI halves need a Windows run. * ui/drivers: Win32 companion - fix the quit hang; thumbnails cached across playlists Two reports after the thread-pool commit: RetroArch hangs at close, and the grid, while faster, is still not "instant" the way Qt's is. The hang. ui_companion_win32_wimp_deinit freed the row map, thumb_idx and the image list and only then called DestroyWindow. A virtual (LVS_OWNERDATA) list view can ask for rows (LVN_GETDISPINFO) while it is being torn down, so the handler read freed memory and a destroyed image list during DestroyWindow - a fault inside the modal-clocked teardown, which presents as a hang. Deinit now empties the list (LVM_SETITEMCOUNT 0, image list detached) and destroys the windows first, and frees the data after; the hwnd/entries pointers are cleared so a late iterate or log call is a no-op. The pool shutdown is also made hang-proof on its own: workers wait with a 100 ms timeout so a lost wake-up on the Windows condition variable can never keep one parked past quit, and stop broadcasts rather than signalling per worker. (A TSan harness of the real pipeline code with four workers, mid-decode and ring-blocked at quit, stops in 0 ms with no races.) The "instant". The Qt companion keeps decoded pixmaps in a QCache keyed by file path that survives switching playlists, so revisiting one draws thumbnails with no decode at all. The Win32 slots were per row and reset on every rebuild. They are now keyed by thumbnail path in an open-addressed hash table and kept across list changes: only a new thumbnail size or type drops them. A row whose file is already a slot gets it on the spot, no decode. The slot budget follows the size to stay near 64 MiB (a few hundred at 320 px, thousands at 64 px). Rows whose thumbnail file does not exist are marked once (-2) so the filesystem is not probed for them on every paint. Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean) with and without HAVE_THREADS; companion_parity.py green; the pipeline harness under ThreadSanitizer (shutdown stress, 5 trials). GDI / window teardown needs the Windows run. * ui: Win32 File Browser laid out like Qt's; drive list on Windows The File Browser tab on Win32 came up empty and did not work the way Qt's does. Empty: with no content directory configured, the core refused to open a top level on Windows (I had ruled out a "bogus root"), so there was nothing to list. On Windows the top level is the drive list: the core now lists GetLogicalDrives() as directory entries ("C:\") for an empty browse_dir, a drive root's ".." goes back to it, and the status bar calls that level "Computer". Layout: Qt's File Browser keeps the folder tree in the left pane and shows the selected folder's files in the content view. Win32 had turned the content view into a combined folder + file listing and left the playlists in the left pane. Now, in the browser: the left list shows the current folder's subfolders (".." first, folder icon), the content view - list or grid - shows its files, a folder descends on double- click / Enter, and a file loads (or opens the core picker) as before. Leaving the tab, or picking a playlist, puts the playlists back in the left pane; a playlist refresh that lands while browsing is deferred to then. The core exposes the split (companion_core_browse_dir_count: the listing is sorted folders first, so [0, dc) are folders and [dc, n) files) for any backend that wants the same two-pane shape. Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); consumers of companion_core.h pass; companion_core.o builds; companion_parity.py green. Not run here (no Windows). * ui: Win32 file browser - drive names shown, folder icon opaque Two defects in the drive list the previous commit added: - No names. companion_core_browse_name() took path_basename() of each entry; a drive root "C:\" has no basename, so every row was blank. When the basename is empty the name is the path itself. - Black squares behind the folder icons. The fallback icon went into the playlist image list raw (ImageList_AddIcon) and the list was created with ILC_MASK; a masked list draws an alpha icon's transparent pixels as black. The list is now unmasked and every image in it is an opaque bitmap composited on the window colour: index 0 is the XMB folder.png asset (what Qt shows) through the same compositing scaler the playlist icons use, and only if the asset set is missing is the shell folder icon used - drawn with DrawIconEx onto a window-coloured bitmap, so it too is opaque. companion_core_folder_icon_path() gives backends the asset's path. Verified: companion_core.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean); consumers of companion_core.h pass; companion_parity.py green. Not run here. * ui/companion: shared thumbnail engine for every companion, with a regression test Thumbnails in the Win32 grid only appeared for entries that were clicked. Reading the Qt companion's pipeline end to end (ThumbnailLoader, PlaylistModel::loadThumbnail, MainWindow:: updateVisibleItems, GridView::visibleIndexes) gives the model and, by contrast, the bug: Qt requests decodes for the rows its grid reports as visible, on a 50 ms scroll debounce, and never for anything else. Win32 requested from LVN_GETDISPINFO - and comctl32 sends that for every item of a virtual icon view while laying it out, so a 42k-entry list flooded the 512-job queue, the oldest (the visible top) were dropped, the workers decoded the bottom of the list, and a visible row was only requested again when something repainted it: a click. Rather than patch that in place, the whole pipeline moves into the core as one engine every companion uses, ui/companion/companion_thumbs: - cache of decoded, already-scaled thumbnails keyed by (path, edge), byte budget (64 MiB default) with LRU eviction; survives switching playlists, so a revisit draws with no decode (Qt's QCache behaviour) - decode thread pool (cores - 1, 1..4; Qt has one), scaling on the worker so the UI thread only ever copies finished pixels; a budgeted UI-thread fallback without HAVE_THREADS - two-lane request queue: urgent jobs newest-first (the rows on screen, topmost first), then a prefetch lane oldest-first; keys are de-duplicated against the cache and the queues - cancel() empties the queues and keeps the cache; results in flight still land and are cached; delivery carries the backend's tag, which encodes its own row generation, so a stale result is dropped by the backend, never blitted into the wrong list - pixels are opaque ARGB8888: Windows' DIB order, a byte swap for Cocoa Both natives now drive it the way Qt drives its loader: Win32: each frame, the rows on screen are found from the list view's own item rects (monotonic in row order, so a binary search), requested urgently topmost-first, a screen either side prefetched; finished pixels go into a recycled image-list slot ring; LVN_GETDISPINFO only reads a row's slot. The local thread pool, hash table and rings are gone. Cocoa: the grid reports its visible rows from its visibleRect; the same request / prefetch / poll cycle runs from iconTick; finished pixels become an NSImage through an NSBitmapImageRep. The per-frame synchronous NSImage load is gone. Regression test: ui/companion/test/companion_thumbs_test.c, run by tools/companion_thumbs_test.sh plain, under ThreadSanitizer and under ASan+UBSan, against the real decoder (image_texture_load) with TGA fixtures it writes itself. It checks a decode lands with the right scaled / letterboxed pixels, cache hits without decode, LRU eviction under budget, cancel keeping the cache and allowing re-requests, an undecodable file delivered as NULL and forgotten, 300 keys over four threads delivered exactly once, and free() with work in flight. The engine is also compiled once as C89 (-ansi -pedantic, no threads). Wired into Linux-libretro-common-tests.yml beside the parity check. The test found one engine bug before it ever reached a backend (re-request of a key whose decode was in flight across cancel()). Verified: the test passes plain, TSan and ASan; companion_thumbs.c, ui_win32_companion.c and the consumers of companion_thumbs.h pass tools/mingw_syntax_check.sh (also with HAVE_THREADS off); full Qt 6 retroarch builds with the engine in and starts headless; companion_parity.py green; apple_mrc_ownership_check.py unchanged. Not run on Windows / macOS here. * ui/companion: boxart panes on the engine; Cocoa two-pane browser; engine takes w x h Follow-ups to the shared thumbnail engine, so both natives are on it for everything and nothing decodes on a UI thread any more: - The engine keys and scales by (path, w, h) rather than a square edge, so the boxart pane can ask for its own rectangle and get an aspect-fit, letterboxed image instead of a square. Grid cells pass edge x edge. The test gains a rectangular case (20x80 into 60x30: centre column image, corners letterbox). - Boxart panes (Win32 and Cocoa) come from the engine: cached at once, otherwise an urgent request tagged with the entry and a boxart bit, delivered through the same per-frame poll, which now also runs in list view so the pane fills in there too. The pane is cleared while it waits, so a stale cover never sits under a new selection. Both panes decoded synchronously per click before; Cocoa's per-frame NSImage loader (cc_thumb_image) is gone. - Cocoa File Browser is now two-pane like Qt's and Win32's: the left table shows the folders (companion_core_browse_dir_count, ".." first, XMB folder.png or the Finder folder) and the content table the files; double-click a folder to descend, a file loads; leaving the tab or picking a playlist restores the playlist pane. The row map serves the browser too, so run / boxart / popup resolve files correctly. - companion_parity.py requires the engine's request and poll calls and the folder/file split on every backend (Qt waived: its own ThumbnailLoader / QFileSystemModel), so a backend cannot quietly go back to synchronous loading. 22 features, all three agree. Verified: companion_thumbs_test.sh plain, TSan and ASan pass (ASan caught a test-side index bug in the new rectangular case, fixed); companion_thumbs.c and ui_win32_companion.c pass tools/mingw_syntax_check.sh (also with HAVE_THREADS off); consumers of companion_thumbs.h pass; full Qt 6 retroarch builds and starts headless; apple_mrc_ownership_check.py unchanged. Not run on Windows / macOS here. * ui: Qt on the shared thumbnail engine; Win32 / Cocoa file browser like Qt's Qt joins the engine, so all three companions decode through one path: - PlaylistModel drops ThumbnailLoader (its own QThread) and the QCache of full-size pixmaps; it owns a companion_thumbs instance instead. loadThumbnail() requests at the grid's cell size (fed from the zoom slider), data(THUMBNAIL) converts the engine's pixels to a QPixmap once per (path, size) into a small conversion cache, and a 16 ms timer polls while anything is pending; thumbnailReady(path) is emitted for the row's dataChanged and for other consumers. The thumbnail-cache-limit setting becomes the engine's byte budget. - The sidebar's four thumbnail widgets and the file-browser image preview go through the same engine (imageAt / requestImage at the widget's size), replacing the second loader thread and two synchronous full-size decodes on the UI thread. ThumbnailLoader and pixmapFromPathRA are gone; the previewLoader / m_thumbnailPixmaps state with them. - The engine gains set_budget(), forget(path) (a downloaded thumbnail replaces its cached sizes) and pending() (queued + decoding + not yet polled, so a poll timer never stops early). All three are in the regression test. The File Browser on Win32 now matches Qt's (screenshots side by side): drive entries named "