Files
RetroArch/tools/mingw_syntax_check.sh
T
LibretroAdminandLibretroAdmin 4b460947b4 Companion UI: native Win32 and Cocoa desktop menus on a shared C core (Qt becomes one backend of three) (#19505)
* 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 (<Detect> 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 "<version> - <core or No Core>" 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
  (<assets>/xmb/dot-art/png/<playlist>.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 "<version> - <core>" 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 "<version> - <core or No Core>" 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 "<label> (C:)" from GetVolumeInformation (the core
does it, so Cocoa would too); Up / Start Directory / Downloads buttons
under the tabs (Qt's toolbar); the content view is the folder's whole
listing as a table with Name / Size / Type / Date Modified, drawn with
the shell's own small image list so drives, folders and file types
have Explorer's icons (system image list: nothing of ours to
composite); size and date from FindFirstFile (Windows 95); the browser
is always a table like Qt's, and the playlist view type returns on
leaving. The boxart pane is cleared in the browser - Qt shows none for
a file selection - which the screenshot showed still holding the
previous playlist entry's cover. companion_core_browse_up() is the
"Up" action for any backend.

Cocoa gets the same: the three buttons, the whole listing with a Type
column, boxart cleared, Up / Start / Downloads.

companion_parity.py: Qt's engine waiver is gone - all three backends
reference the engine's request and poll for real.

Verified: companion_thumbs_test.sh plain, TSan, ASan pass with the
new cases; companion_core.c, ui_win32_companion.c and the consumers of
companion_core.h / companion_thumbs.h pass tools/mingw_syntax_check.sh;
full Qt 6 retroarch builds with the model on the engine and starts
headless with the qt companion; companion_parity.py green;
apple_mrc_ownership_check.py unchanged. Not run on Windows / macOS.

* ui/companion: regression test for the companion core; two bugs it found

A second regression test, this one for the toolkit-free model every
desktop companion drives (companion_core.c). It links the real core
and playlist.c against libretro-common with RetroArch's state replaced
by fixtures (companion_core_stubs.c: settings, runloop, path slots, an
empty core-info world, recording stand-ins for the load / scan tasks),
writes real .lpl files, and checks:

- the playlist listing: All Playlists first with the token path, the
  configured specials next, then the files in order, with their paths
- selecting a playlist through the budgeted iterate and reading its
  entries; each row's own file and index within it
- All Playlists: every file merged, sorted by label case-
  insensitively, rows mapped back to their file and index
- the thumbnail path rule: the repository layout, label sanitising
  (& * / : ` < > ? \ | -> _), the extension probe, the default path
  for a missing file, image content as its own thumbnail
- the file browser: folders first with "..", the folder / file split,
  descending, going up to the root, up refused at the root
- Run: a DETECT entry with nothing loaded asks for a core with its
  path; an entry with a core pushes the load task with that core
- launch options: the entry's own core is offered, nothing duplicated

Run by tools/companion_core_test.sh, plain and under ASan+UBSan; wired
into Linux-libretro-common-tests.yml. On its first run it found two
bugs in the browser that no screenshot had shown:

- a use-after-free when descending into a folder: browse_open() was
  handed a pointer into the current listing, freed that listing, then
  copied the path from it. The path is copied before anything is
  freed.
- "Up" could never reach the root on Unix: path_parent_dir() turns "/"
  into "", which browse_open() took as "use the default directory", so
  going up from the content directory looped back into it. The parent
  step now maps "" to "/" and keeps browse_dir canonical (no trailing
  separator below the root), so it compares and terminates.

tools/mingw_syntax_check.sh --consumers-of skips */test/ harnesses:
they are Linux-only by design and have their own scripts.

Verified: companion_core_test.sh plain and ASan pass;
companion_thumbs_test.sh passes; companion_core.c passes
tools/mingw_syntax_check.sh (mingw -Wall -Wextra + linux c89 pedantic)
and its consumers pass; companion_parity.py green.

* ui: abortable decodes (no exit hang), browser previews, no disk I/O on paint

Three reports: a thumbnail preview in the File Browser can take a very
long time to show, RetroArch can still hang on exit, and the Win32 /
Cocoa companions never preview in the browser at all. Traced, and the
traversal audited while at it:

Directory traversal is not the problem: dir_list_new gets each entry's
type from the enumeration itself (FindFirstFile attributes on Windows,
d_type elsewhere) - no per-file stat. The bottlenecks were these:

1. A decode could not be interrupted. image_texture_load runs both of
   its stages incrementally (one chunk / one pass per step) but never
   looked up between steps, so a worker decoding a large image ran to
   completion whatever happened. On exit, free() joined that worker:
   the hang. In the browser, clicking through large images queued
   full decodes behind each other on the worker.

   libretro-common gains image_texture_load_ex() with an abort hook
   asked between chunks and between passes (image_texture_load is now
   that with no hook). The engine's workers decode through it and
   abort when the engine is shutting down or when their job's epoch is
   stale - i.e. a cancel() happened since it was queued. Aborted jobs
   are not delivered and their entries are dropped, so a later request
   decodes again. Two new tests: cancel() during 8 x 16 MiB decodes
   lets the re-requested key land first; free() with the same in
   flight returns in well under a second.

2. Win32 did disk I/O while painting the browser table: FindFirstFile
   per row per sub-item on every repaint, and SHGetFileInfo hitting
   the shell and disk for every directory. Metadata (size, date, icon)
   is now resolved once per row - one FindFirstFile, and the icon by
   attributes alone (SHGFI_USEFILEATTRIBUTES; a drive keeps its real
   icon) - and paints read the cache.

3. Qt's QFileSystemModel asks the shell for every folder's own icon
   (a disk lookup per folder) and starts a QFileSystemWatcher on every
   directory it has shown. Both are off now
   (DontUseCustomDirectoryIcons, DontWatchForChanges): the generic
   folder icon is what the natives show too, and the table is rebuilt
   on navigation. Selecting an image in Qt's browser abandons the
   decode of the previous one (abandonPending -> engine cancel).

4. Previews in the natives: Qt previews an image file selected in the
   browser in its sidebar; Win32 and Cocoa cleared the pane in browse
   mode. Both now show the selected image file through the engine
   (cw_boxart_update_path / refreshBoxart's browse branch), cleared for
   non-images. Since abandoned decodes leave grid rows marked
   "requested", both natives re-ask for empty on-screen cells when the
   engine goes idle.

Verified: companion_thumbs_test.sh plain, TSan and ASan pass with the
abort tests; companion_core_test.sh passes; image_texture.c and its
consumers, ui_win32_companion.c pass tools/mingw_syntax_check.sh;
full Qt 6 retroarch builds and starts headless; companion_parity.py
green; apple_mrc_ownership_check.py unchanged. Not run on Windows /
macOS.

* ui/companion: directory listings enumerate off the UI thread, for every backend

Going up into a large directory froze the Qt companion's File Browser
for seconds; the natives enumerated on the UI thread too. Nothing in
the browser may block, so the listing moves into the core as one
asynchronous, shared implementation, and each backend only paints it:

- companion_core_browse_open() now enumerates on a worker (rthreads;
  synchronous fallback without HAVE_THREADS): dir_list_new, the
  folders-first sort, and one stat per entry for size and mtime - all
  off the UI thread. The result lands in companion_core_iterate() and
  the new on_browse_changed callback fires (added last in the callback
  table, so existing positional tables stay valid). The previous
  listing stays readable until then. A newer open supersedes the one
  running: it stops between entries, its result is discarded, and no
  second thread is ever spawned alongside it. browse_busy() says when
  "Loading..." applies.
- Size / Type / Date Modified are formatted by the core
  (browse_size_str / _type_str / _date_str) from what it gathered, so
  the three backends show the same strings and none of them touches
  the disk to paint. The Win32 FindFirstFile-per-row cache is gone;
  only the shell icon index stays local (by attributes, no lookup).

Qt: the file table drops QFileSystemModel + the sort/filter proxy.
That pair was the stall: the model kept every visited directory
loaded, the gatherer inserted rows in batches with the proxy
re-filtering and re-sorting on each one (O(n^2/100) on the UI
thread), and header sorting re-sorted the whole directory on every
navigation. BrowseTableModel paints the core's listing (Name / Size /
Type / Date, three stock icons resolved once, the search filter as a
row map) and resets when the listing lands. The directory tree keeps
QFileSystemModel (folders only) with its watcher and custom icons off.

Win32 / Cocoa: open / up / descend show "Loading..." and rebuild when
the listing lands, instead of right after open() (which would show the
old listing now).

companion_core_test covers the async path: the listing lands through
iterate(), busy() clears, size / type / date strings, supersession
(open A then B: only B delivers, once), Up to the root; run plain,
ASan and now TSan. companion_parity.py requires on_browse_changed on
every backend (Qt's two-pane waiver becomes its BrowseTableModel).

Verified: companion_core_test.sh plain / ASan / TSan and
companion_thumbs_test.sh pass; companion_core.c, ui_win32_companion.c
and the consumers of companion_core.h pass tools/mingw_syntax_check.sh;
full Qt 6 retroarch builds and starts headless; companion_parity.py
green; apple_mrc_ownership_check.py unchanged. Not run on Windows /
macOS.

* ui/drivers: clear the boxart pane the moment the File Browser is entered

Switching to the File Browser tab still left the previous playlist
entry's cover in the boxart pane on Win32. Two reasons:

- the clear had moved to the listing's landing (cw_browse_rebuild),
  which is now asynchronous; until then the old cover stayed
- a static control does not repaint on STM_SETIMAGE with NULL: it
  keeps drawing the image it had until something invalidates it, so
  even once the clear ran, the cover remained on screen

Now cw_browse_enter clears the pane at once (and resets the core popup
and Core Info to the no-entry state, as Qt shows for the browser), and
the clear invalidates the control. Cocoa does the same on entry
(refreshBoxart sets nil, popup and info reset) so both natives match.

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. Not run on Windows / macOS.

* ui/companion: header-click sorting in the file browser, once, for every backend

Clicking Name / Size / Type / Date Modified in the browser did nothing
(Win32, Cocoa) or nothing useful (Qt: the core-backed table had sorting
off). The sort lives in the core, so the three companions order the
listing identically:

- companion_core_browse_sort(column, ascending): folders always first
  (".." first of all), then the files by name (case-insensitive), size,
  type (extension) or modification time, ties broken by name. One qsort
  over the already gathered arrays - no I/O. It applies to the current
  listing at once (on_browse_changed fires) and is remembered, so every
  listing that lands afterwards comes in the chosen order.
- Qt: BrowseTableModel::sort() forwards the header click; sorting is
  on again, with the indicator on Name.
- Win32: LVN_COLUMNCLICK forwards it; the same column again flips the
  direction, a new one starts ascending, as Qt's header does.
- Cocoa: the entries table gets sort descriptor prototypes (Name / Type
  / Size / Date; the Size and Date columns show only under the
  browser) and sortDescriptorsDidChange: forwards them.

companion_core_test covers each column both ways, the folders-first
invariant, tie-breaking by name, and the order persisting across a
re-enumeration. companion_parity.py requires the sort call on every
backend (24 features).

Verified: companion_core_test.sh plain and ASan pass; companion_core.c,
ui_win32_companion.c and the consumers of companion_core.h pass
tools/mingw_syntax_check.sh; full Qt 6 retroarch builds and starts
headless; companion_parity.py green; apple_mrc_ownership_check.py
unchanged. Not run on Windows / macOS.

* ui/companion: fix the crash on the drive list after a sort; scenario regression tests

Clicking a drive in the File Browser crashed on Windows once the
browser had been sorted by Date (or Size). The drive-list branch of the
listing worker returned without allocating the size / mtime arrays
(only the Windows path did that), and the sort comparator indexed
mtime[] for the Date column unconditionally: NULL dereference in
companion_core_browse_cmp the moment the drive list landed and the
remembered sort was applied to it.

Both ends fixed: the drive list now goes through the same metadata
allocation as every other listing (a drive root is simply not stat'ed),
so there is one job path; and the comparator never indexes an array it
does not have.

Tests, in the shape that catches this class before it reaches a
Windows box:

- test_sort_without_metadata builds the drive-list shape directly (a
  folders-only listing with no metadata arrays, via a COMPANION_CORE_
  TESTING hook) and sorts it by every column both ways. On the old
  comparator this SEGVs under ASan at the exact line; on the fix it
  passes - verified both ways.
- test_sort_then_folders_only runs the real job path: every sort
  setting, then open a folders-only directory, then sort the landed
  listing by every column again.
- test_backend_scenario replays the click sequences the backends send
  to the core in a user's order: enter the browser, click every header
  both ways, select every row (name / path / size / type / date /
  is-dir, as the panes and tables resolve them), descend, activate a
  file, Up, Start Directory, rapid navigation without waiting, sort
  again, back to a playlist. Any fault on the core side of any
  companion trips it. Runs plain, ASan and TSan.

Verified: companion_core_test.sh plain / ASan / TSan pass;
companion_core.c passes tools/mingw_syntax_check.sh (mingw -Wall
-Wextra + linux c89 pedantic) and builds; companion_parity.py green.

* ui/companion: stop the reset -> sort -> changed -> reset recursion (Qt stack overflow)

Clicking a drive in the Qt File Browser blew the stack: the backtrace
is ui_companion_qt_core_on_browse_changed -> Qt -> the same, twelve
thousand frames deep. The loop:

  the listing lands -> onBrowseChanged -> the model resets ->
  QTableView re-applies its header sort on a reset -> model sort() ->
  companion_core_browse_sort() -> on_browse_changed -> ...

companion_core_browse_sort() fired the callback even when the requested
order was the one already in place, so a view that re-applies its sort
on "changed" - which Qt's table does on every model reset - never
converged.

Fixed once in the core: a sort that changes nothing does nothing and
fires nothing. The Qt model's sort() is idempotent too, and
onBrowseChanged has a re-entrancy guard. companion_core_set_ud() is
added (a backend can create the core before the object its callbacks
act on).

Regression test: test_sort_callback_reentrancy registers a listener
that does what Qt's table does - re-applies the current sort on every
"changed" - with a depth counter that stops it at 3, so a loop is
reported rather than crashed in. On the previous core it reports depth
4 in every case; on this one, one callback per real change and none
for a repeat. Verified both ways.

Verified: companion_core_test.sh plain / ASan / TSan pass;
companion_core.c and the consumers of companion_core.h pass
tools/mingw_syntax_check.sh; full Qt 6 retroarch builds and starts
headless; companion_parity.py green.

* ui: browser header clicks sort, and stay sorted, on Qt / Win32; Date and Type proven

Header clicks in the Qt browser appeared to do nothing: every column
sorted the same. The core sorted correctly; the Qt window then undid
it. onBrowseChanged() restored the header state saved before the last
navigation - and that state carries a sort indicator (Name ascending),
so after each click's reload the view snapped its indicator back to
Name and re-sorted by it. Now the saved state (column widths) is
restored once, after a navigation, and the indicator is always set from
the core's actual order (signals blocked, so it never re-sorts by
itself).

Win32: the clicked column shows a sort arrow (HDF_SORTUP / SORTDOWN;
ignored by older comctl32), and a rebuild no longer resets the column
widths the user set (the browser rebuilds on every landing and every
sort). Cocoa's descriptor path re-read and left as is: it already
forwards the clicked column and direction.

The core test now proves each column orders differently, not just
Name and Size: Date with explicit, distinct mtimes (newest first
descending, oldest first ascending), Type across six extensions
(grouped and ordered by extension, names within a group, reversed when
descending), and an explicit check that the four sorts produce four
different first files.

Verified: companion_core_test.sh plain / ASan / TSan pass;
ui_win32_companion.c passes tools/mingw_syntax_check.sh; full Qt 6
retroarch builds and starts headless; companion_parity.py green;
apple_mrc_ownership_check.py unchanged. Not clicked on Windows / macOS.

* ui/drivers: browser header shows the sort direction on Win32 without comctl32 v6

The Win32 companion showed no arrow on the sorted column: it used
HDF_SORTUP / HDF_SORTDOWN, which only a themed (v6) comctl32 draws, and
this build carries no common-controls v6 manifest - the header is the
classic one, which ignores those bits.

The classic header shows a picture instead: the sorted column now gets
HDF_IMAGE with an image on the right of its caption, from a two-entry
image list (up, down) drawn with GDI on the button colour at creation.
The v6 bits are still set for a themed header, so either kind shows
the direction; leaving the browser clears the arrow off the playlist
columns. Qt explicitly turns its header's indicator on.

Verified: ui_win32_companion.c passes tools/mingw_syntax_check.sh
(mingw -Wall -Wextra clean); full Qt 6 retroarch builds;
companion_parity.py green. Not run on Windows.

* ui/drivers: Qt and Cocoa browser headers follow the core's sort like Win32's

One rule for the browser header on all three companions: a new column
starts ascending, the same column again flips, and the header's
highlighted column and arrow always show the order the core's listing
is actually in - whether it came from a click on this backend or was
remembered from before (a re-opened directory lands already sorted).

Cocoa: after every reload the table's sort descriptors, highlighted
column and indicator image are set from the core (syncSortIndicator;
the programmatic set is flagged so sortDescriptorsDidChange: does not
treat it as a click). Leaving the browser clears the highlight and
arrows off the playlist table. Qt: the initial indicator is taken from
the core rather than hard-coded to Name ascending; the per-landing sync
was already in place. Win32 already did both.

Verified: full Qt 6 retroarch builds; companion_parity.py green;
apple_mrc_ownership_check.py reports one new local autoreleased object
(the descriptor, retained by setSortDescriptors: within the call).
Not run on macOS.

* ui/drivers: Win32 browser header showed a document, not the sort arrow

A report-view list view hands its small image list to its header when
LVM_SETIMAGELIST(LVSIL_SMALL) is sent, replacing whatever the header
had. cw_browse_rebuild set the header's arrow image list first and the
rows' shell image list after, so the header's image 0 / 1 became the
shell's first two icons - a document. The arrow image list is now set
after the rows' one, every rebuild.

Verified: tools/mingw_syntax_check.sh (mingw -Wall -Wextra clean). Not
run on Windows.

* ui/companion: thumbnail engine use-after-free on a doubly-recorded key; test races

CI's TSan run of companion_thumbs_test failed at "re-requested decoded"
while the plain run passed. Two test defects and one engine bug behind
it:

- The waits were counting deliveries, not the delivery they meant. A
  job popped before a cancel() still lands with its old epoch; with one
  worker that stale delivery satisfied drain(1) before the re-requested
  key had landed, and the following get() saw nothing. TSan's
  scheduling made it hit. The tests now wait for the specific tag
  (drain_tag) wherever a re-request is checked.

- strdup was implicitly declared under the script's -ansi pass (the
  warning in the log); on a 64-bit host an implicit int return would
  truncate the pointer. Replaced with strldup(path, strlen(path) + 1) -
  and note the + 1: strldup(s, n) copies n - 1 characters, strlcpy
  style. My first attempt passed strlen(path) and silently truncated
  every path by one character, which made every decode fail and, with
  the next point, exposed the engine bug.

- Engine: an entry could be freed while another finished record still
  pointed at it. A failed decode's record freed the entry in poll()
  ("undecodable: forget it"); a second record for the same key - a job
  taken before a cancel() and a re-request after it - then dereferenced
  it (ct_find walked a freed chain link). Entries are now reference-
  counted by the jobs and records that point at them (request +1, ring
  drop / cancel pop / polled record -1) and freed only at zero.

Regression test: test_double_failure_no_uaf builds that shape twenty
times (request an undecodable file, cancel, request again, poll, look
it up). On the branch-tip engine it is a heap-use-after-free under
ASan five runs out of five; on this engine it passes, plain, TSan and
ASan, repeatedly.

Verified: companion_thumbs_test.sh plain / TSan / ASan pass (ASan x5);
companion_core_test.sh passes; companion_thumbs.c passes
tools/mingw_syntax_check.sh (mingw -Wall -Wextra + c89 pedantic) and
builds.

* ui/companion: animated previews (APNG, WEBP, WEBM, MP4) in every companion's pane

The File Browser preview showed only the first frame of an animated
file. RetroArch's own File Browser (gfx_thumbnail) plays the selected
thumbnail through libretro-common's animation stream API -
image_transfer_anim_stream_* over APNG (rpng_apng), animated WEBP, WEBM
and MP4, frames on the container's clock, no audio. The companions now
do the same, through the shared engine, once for all three:

- companion_thumbs_animate(path, w, h, tag, bg): one animation at a
  time (the selected file). Its own thread reads the file, probes it
  the way gfx_thumbnail does (a cheap type gate, then rpng_is_apng_ex
  over the head of the buffer for PNG - an ordinary PNG costs one
  4 KiB probe and nothing else), opens the stream, and loops: next
  frame (honouring the loop count, rewinding at the end of a pass),
  ARGB from the stream where it can supply it or a swizzle where it
  cannot, scaled and letterboxed exactly like a still, pushed as a
  delivery for (path, w x h, tag), then sleeps the frame's duration.
  A still produces nothing. animate_stop(), a newer animate(), or
  free() ends it within one frame duration; frames of a superseded
  animation are dropped at poll. Files over 256 MiB are not animated.
  Frames land through companion_thumbs_poll like any other delivery,
  so a backend's existing "blit these bits into the pane" path draws
  them; pending() and animating() keep its polling alive.

- Win32 / Cocoa: the boxart pane starts the animation for the selected
  file after showing its still, and stops it when the selection moves.
  Qt: the sidebar's four widgets play frames for a browser selection
  (frameReady from the model, QT_TAG_ANIM_FRAME deliveries), stopped on
  the next selection. Any previous animation stops first.

Regression test: test_animation writes a two-frame APNG by hand (IHDR,
acTL, fcTL, IDAT, fdAT - zlib + CRC), checks it decodes as a still
(frame 0, red), then that animate() delivers at least six frames in
four seconds with both colours seen, that animating() reports it, that
stop() ends it, that a still animates nothing, and that free() with an
animation running returns. Plain, TSan and ASan. The script's C89 pass
gains _GNU_SOURCE (as the real C89 gate has) for the POSIX types
libretro-common's headers use, and HAVE_RPNG for the APNG probe.

companion_parity.py requires the animate call on every backend (25
features).

Verified: companion_thumbs_test.sh plain / TSan / ASan and
companion_core_test.sh pass; companion_thumbs.c, ui_win32_companion.c
and the consumers of companion_thumbs.h pass tools/mingw_syntax_check.sh;
full Qt 6 retroarch builds and starts headless; companion_parity.py
green; apple_mrc_ownership_check.py unchanged. Not run on Windows /
macOS.

* ui/drivers: remove stale companion_preview references from the Win32 companion

The animated-previews commit left the Win32 companion with an include
of, and calls into, a companion_preview module from a direction I had
abandoned in the same session (the Qt and Cocoa files had been reset;
this one had not). The header does not exist: the Windows build failed
at the include. All of it is gone; the pane's animation goes through
companion_thumbs_animate like the other two backends.

The syntax gate should have caught it and did not: it forgave every
"No such file" error so that optional platform headers this box lacks
(d3dkmthk.h) do not fail unrelated files - which also forgave a missing
project header. It now forgives only a missing header named without a
path; one with a path component (../companion/x.h) fails the gate.
Probed both ways.

Verified: ui_win32_companion.c compiles to an object with mingw
(-Wall -Wextra, no warnings), and passes the fixed gate along with
companion_thumbs.c, companion_core.c and the 15 consumers of
ui_companion_driver.h; companion_parity.py green.

* gfx: share the File Browser's animated preview with the companions (gfx_anim_preview)

The companions' previews of an animated file were slow to start and
silent, while RetroArch's own File Browser thumbnail plays the same
file at once and with sound. The reason was that the companion engine
had its own playback path: read the whole file, open the stream, no
audio. The menu's path (gfx_thumbnail.c) does it right - a sliding-
window progressive open so frames start from the first resident bytes
and a tail-moov MP4 opens from a few MiB, memory admission that scales
with the heap, the container's audio track through the mixer over its
own window - so that is what everyone uses now.

gfx/gfx_anim_preview.{c,h}: the session that machinery becomes, moved
out of gfx_thumbnail.c with its reasoning comments - open (type gate,
APNG head probe, windowed progressive open with the moov-island jump
and the media-floor prime, admission), feed (keep the window
straddling the decoder's frontier under the per-tick I/O budget and
raise the demuxer's bound to what is resident), next / rewind with
the container's duration rules, and the preview audio (the second
window, moov head sizing, mixer stream, its feeder, stop). Nothing in
it knows about a thumbnail, a video driver or a toolkit.

gfx_thumbnail.c keeps its threaded upload path and now drives the
open, both feeders and the audio through a session it wraps around the
stream and mapping it owns (gfx_anim_preview_wrap / _release, non-
owning); ~430 lines of inline copies are gone. Both of its sample
suites pass on the result: gfx_thumbnail_anim (plain, ASan+UBSan,
TSan sweep) and gfx_thumbnail_preview (make check: windowed opens over
sparse tail-moov fixtures, audio-window islands, decode budgets).

The companion engine's animation thread is that session too: open,
feed, next, rewind, close; the UI thread starts the preview audio once
the session is published and feeds its window every poll (mixer calls
stay on the UI thread), stop() silences at once. A video's still is
now its first frame through the same windowed open instead of
image_texture_load reading the whole file - the other half of the
startup latency. So the companions get the menu's startup time and its
sound (honouring menu_thumbnail_preview_audio) from the same code.

One bug found by the engine test while doing this, in the shared code:
image_transfer_anim_stream_set_argb is honoured only before the first
frame, and APNG reports false to a repeat request afterwards although
the order stands. Asking per frame and trusting the answer swapped R
and B from the second frame on (a blue-tinted preview after the first
loop). The session decides the order once at open and remembers it.
The animation test now checks every frame is one of the fixture's two
colours over ten frames, which is what caught it.

Verified: companion_thumbs_test.sh plain / TSan / ASan pass (the module
linked in); companion_core_test.sh passes; both gfx_thumbnail sample
suites pass; gfx_anim_preview.c, gfx_thumbnail.c, companion_thumbs.c,
ui_win32_companion.c and the consumers of gfx_thumbnail.h pass
tools/mingw_syntax_check.sh; full Qt 6 retroarch builds and starts
headless; companion_parity.py green; apple_mrc_ownership_check.py
unchanged. Xcode and MSVC build through griffin, which includes the
new file. Not run on Windows / macOS.

* ui/companion: per-frame preview cost - fold the byte swap into the sampler, 4-tap downscale, no GDI churn

Measured per frame at pane size (320 x 400), -O2:

                         nearest  full-canvas swizzle  folded swizzle  4-tap (+swizzle)
   3840x2160 source      0.35 ms  11.8 ms              0.22 ms         0.98 ms
   1920x1080             0.13 ms   1.5 ms              0.16 ms         0.91 ms
    640x480              0.09 ms   0.29 ms             0.12 ms         0.93 ms

So the scale to pane size was never the cost. What was: for a stream
that will not emit ARGB words (animated WEBP), the whole canvas was
byte-swapped into a fresh malloc before sampling - 12 ms a frame at
4K, a third of a 30 fps budget - to feed a sampler that reads a few
hundred thousand of those pixels. companion_thumbs_scale_ex takes the
source order as a flag and swaps the sampled pixels only: 0.2 ms, no
allocation. The animation thread and the video-still path use it.
(Handing the raw canvas to the toolkit and letting it scale would be
worse, not better: every toolkit here would copy the full canvas into
its own image per frame - 33 MB at 4K - and scale on the CPU anyway.)

Quality, the actual weakness: nearest-neighbour downscaling a video to
a pane aliases and shimmers on motion. When shrinking by two or more
in both directions the sampler now averages four taps per output
pixel, placed a third in from each edge of the source cell so they
always fall on rows and columns of different parity (a 1-px
checkerboard, the worst case, averages to grey instead of picking one
colour - the test caught the quarter-point placement getting that
wrong). Under 1 ms at any source size. Enlarging stays nearest.

UI-thread side, per frame: Win32 created and destroyed a DIB section
and re-set the static control's image every frame; it now keeps one
DIB for the pane, copies the frame into its pixels (after GdiFlush)
and invalidates. Cocoa allocated an NSBitmapImageRep and an NSImage
per frame; it keeps one rep and writes into its bitmapData. Qt did
img.copy() before QPixmap::fromImage(), which copies again; the copy
is gone.

test_scaler covers the R,G,B,A-order path (red in, ARGB red out; the
same words read as ARGB are blue), the 4-tap average on a checkerboard
and nearest on enlargement.

Verified: companion_thumbs_test.sh plain / TSan / ASan pass;
companion_thumbs.c and ui_win32_companion.c pass
tools/mingw_syntax_check.sh (the Win32 TU also compiled to an object
with mingw, no warnings); consumers of companion_thumbs.h pass; full
Qt 6 retroarch builds and starts headless; companion_parity.py green;
apple_mrc_ownership_check.py unchanged. Not run on Windows / macOS.

* ui/drivers: Cocoa - folders pane under the File Browser tab, grid layout on attach, focus back on close

Three reports from running the Cocoa companion:

- The File Browser was empty. The playlists table was tab item 0's
  view, and tab item 1 (File Browser) had no view at all, so selecting
  that tab showed an empty pane and hid the very table that lists the
  folders in browse mode. The tab view is now only the strip; the table
  sits under it in both modes and layoutViews places it.

- The grid view showed nothing. RACompanionGrid computed its column
  count from its own bounds, which are zero until its first relayout
  inside the scroll view, and it only relaid out when explicitly told
  to; a zero-width layout stayed. It now takes the width from the clip
  view when it has one and relays out on viewDidMoveToSuperview, so
  becoming the document view lays it out on its own.

- Closing the companion (or loading content from it) left the keyboard
  with no window: RetroArch's window is now made key when the companion
  hides, on every path that hides it (close button, entry run, load
  with the popup's core, load through the core picker).

Verified: static checks (bracket balance by character scan, every
selector sent to self declared and implemented,
apple_mrc_ownership_check.py unchanged); companion_parity.py green.
Not compiled here (no Apple toolchain): the Xcode CI build is the gate.

* ui/drivers: Cocoa - keyboard back to RetroArch's real window; grid paints visible rows; diagnostics

Focus. Closing the companion still left RetroArch without keyboard
control. hideAndFocusRetroArch made key the window of [CocoaView get]
- but on a Metal build the render view is not a CocoaView and that
object sits in no window, so the call went to nil. It now asks the
platform: the window hosting the render view (RetroArch_OSX
-hostWindow), activates the app, makes that window key and makes the
render view its first responder - without which key events reach the
window and nothing in it.

Grid. drawRect walked every entry of the list on every paint (42k
rectForRow calls per repaint); it now paints only the rows the dirty
rect covers. And, since the grid still shows nothing on a real Mac and
I have no Mac, it logs its first paint - count, frame, dirty rect,
thumbnail edge, columns, owner and wimp pointers - so a --verbose run
says which of those is wrong.

Folder pane. Names are missing while icons show; the data-source path
reads correctly, so it logs the first name it hands the table and
whether it is valid UTF-8 (a non-UTF-8 name makes stringWithUTF8String
return nil, i.e. an empty cell).

Verified: static checks (bracket balance, MRC lint unchanged);
companion_parity.py green. Not compiled here: the Xcode CI build is
the gate, and the two log lines are what I need from the next run.

* ui/companion: Cocoa regression harness on Linux (GNUstep AppKit, headless); focus hand-back after AppKit's close

Until now the Cocoa companion had never been compiled, let alone run,
anywhere I could observe; every change to it was made blind and judged
by static checks. GNUstep provides a real Foundation + AppKit on
Linux, so it can be:

tools/companion_cocoa_test.sh compiles the real
ui/drivers/ui_cocoa_companion.m against GNUstep's AppKit (Apple-only
headers - AvailabilityMacros.h, TargetConditionals.h, QuartzCore.h -
shimmed in GNUstep's own version scale), links it with the real
companion core, the core test's stubs and fixtures and a stub platform
(a window with a render view that takes first responder, as CocoaView
does), and runs it under Xvfb. companion_cocoa_test.m drives the
driver the way RetroArch and a user do - init, iterate until the
playlist lands, select Icons in the view popup, click the File Browser
tab, close the window through the delegate and through AppKit's own
performClose - and asserts on the AppKit objects: the grid is the
scroll view's document with a frame and the playlist's count and
paints; the folder table stays visible under the File Browser tab and
hands back names; the content table has the listing's rows; after
closing, RetroArch's window is key and main with the render view first
responder. Wired into CI beside the other two suites (apt-get in the
job).

What the harness found while being brought up: the core-test stub's
msg_hash returned one placeholder for every message, and a popup drops
a second item with the same title - the stub now returns a distinct
string per message; the driver's 10.4 open-panel branch calls
objc_msgSend, which the GNU runtime does not have (guarded, since that
runtime never lacks -URL).

What it did not reproduce, stated plainly: the grid and the focus
hand-back both pass under GNUstep on the previous driver too, so the
empty grid and the lost keyboard seen on a real Mac are AppKit-on-Mac
behaviour this harness does not model. For focus, the difference most
likely to matter is that AppKit re-arbitrates the key window as part of
closing the one that was key, after windowShouldClose: has returned -
so the hand-back (key, main, first responder on the render view) is
now repeated from windowWillClose: and once more on the next run-loop
pass, to win over that arbitration. The grid's first-paint log from
the previous patch remains the way to pin the grid.

Verified: companion_cocoa_test.sh passes; companion_core_test.sh
passes; companion_parity.py green; apple_mrc_ownership_check.py
unchanged. The Xcode CI build remains the Mac compile gate.

* tools: companion gap audit vs Qt; Cocoa harness action audit mode

Two audits, to answer "what does the Cocoa (and Win32) companion still
lack against Qt's":

- COMPANION_AUDIT=1 tools/companion_cocoa_test.sh: after the regression
  checks, the harness fires every user action the Cocoa controller
  implements (view switches, zoom, thumbnail and boxart types, the
  three dock toggles, search focus / edit / clear, the browser's tab
  and three buttons, folder double-click, core popup, picker Load /
  Cancel) on the real AppKit objects under GNUstep and reports each as
  implemented / missing and whether it ran. Actions that launch
  content, open modal panels or delete are listed but not fired.
  Result on this tip: every action implemented, none faults.

- tools/companion_gap_audit.py: a feature-by-feature table of what
  Qt's window offers against evidence in the Cocoa and Win32 sources,
  and the resulting gap lists.

Verified: companion_cocoa_test.sh passes (with and without the audit).

* ui/drivers: Qt parity wiring - Stop, Unload Core, Exit, About, Contributors, Load Custom Core, search Enter

From the gap audit, the items that are pure wiring of native widgets
to core calls Qt already uses:

Cocoa: a Stop button beside Run (companion_core_unload_core, like
Qt's), File > Unload Core and File > Exit RetroArch (CMD_EVENT_QUIT),
Help > About (version alert) and Help > About Contributors (the
AUTHORS list in a scrolling text window).

Win32: the same Stop button and Unload Core item; Help > About
Contributors (the list in a read-only edit); File > Load Custom Core
(a file picker for a core library, into companion_core_load_core);
and Enter in the search box runs the focused entry as Qt's
onSearchEnterPressed does (the EDIT is subclassed for the key).

AUTHORS.h is a C++11 raw string, which Qt can read and C / Objective-C
cannot: tools/gen_authors_c.py writes AUTHORS_c.h, the same list as a
plain C literal, and CI checks the two agree.

companion_parity.py now requires unload core, exit and the
contributors list on every backend (28 features). The gap audit's
Win32 patterns are corrected (Load Core, Exit and About were already
there; they were regex misses) - Cocoa's remaining gaps are 10 and
Win32's 8, all dialogs, drag-and-drop, rename, progress and theme.

The Cocoa harness checks the new wiring on a real AppKit: Stop reaches
the core (command_event observed by the stub), the contributors
window opens with the text, and the remaining selectors exist; its
audit mode lists them.

Verified: companion_cocoa_test.sh passes; ui_win32_companion.c
compiles to an object with mingw (no warnings) and passes
tools/mingw_syntax_check.sh; full Qt 6 retroarch builds;
companion_parity.py green; apple_mrc_ownership_check.py unchanged.
Not run on Windows / macOS.

* ui/companion: playlist rename, add files / file drop, thumbnail drop - in the core, on every backend

Three more of Qt's features, implemented once in the C89 core and bound
on both natives (Qt's rename now goes through the core too):

- companion_core_playlist_rename(): Qt's rules - only inside the
  playlists directory (history / favorites refused), no path
  separators, never over an existing name - then the move and a list
  refresh.
- companion_core_playlist_add_files(): Qt's drop / "Add Files" for
  paths - a file is pushed with label = name without extension and
  db_name = the playlist, a directory is walked recursively, DETECT or
  a given core; All Playlists refused; the shown playlist reloads.
- companion_core_thumbnail_install(): Qt's changeThumbnail for a
  dropped image file - decoded, downscaled to
  desktop_menu_thumbnail_max_size, written as PNG at the repository
  path with the directory created.

Win32: Rename (in-place label edit on the playlist list, applied on
LVN_ENDLABELEDIT through the core), Add Files (multi-select picker),
WM_DROPFILES - an image over the boxart pane installs the thumbnail
of the pane's type, anything else goes into the selected playlist.
Cocoa: Rename (a sheet with a field), Add Files (open panel), file
drops on the entries table (data-source validate / accept) and an
image drop on the boxart pane (RACompanionBoxart). Both forget the
changed thumbnail in the engine and show it again.

Qt: renamePlaylistItem is the core's rename (72 lines -> 30); its Add
Files dialog (extension filter, names, core choice, progress) and its
QImage-based thumbnail drop stay, as waived in companion_parity.py.

Tests: companion_core_test covers the three (refusals, the move, the
list refresh; a file plus a walked directory with the label / db_name
/ DETECT of each entry; a PNG at the repository path downscaled to the
setting), plain and ASan. The Cocoa harness drives the controller: a
dropped file and directory add two entries, a dropped image installs
at the repository path, a rename shows in the reloaded table.

companion_parity.py: 31 features. The gap audit drops "status bar
progress": Qt's QProgressBar there is created, hidden and never driven.
Remaining gaps: the Core Options, Shader Params and Options dialogs,
and the dark theme on Cocoa.

Verified: companion_core_test.sh plain / ASan pass;
companion_cocoa_test.sh passes; ui_win32_companion.c compiles to an
object with mingw (no warnings) and passes tools/mingw_syntax_check.sh
with companion_core.c and the consumers of companion_core.h; full Qt 6
retroarch builds and starts headless; companion_parity.py green;
apple_mrc_ownership_check.py: one new local autoreleased object (the
rename field). Not run on Windows / macOS.

* ui/companion: Core Options, Shader Parameters and Options dialogs on both natives; Cocoa dark theme

The last of Qt's features the natives lacked, each as a flat table the
core exposes toolkit-free and a table window per backend:

- Core Options (companion_core_option_*): the running core's options
  with description, info, value labels, current value; set, reset,
  reset all - over runloop's core_option_manager, written when the
  core flushes as the menu's are. Win32 / Cocoa: option | value,
  double-click cycles, Reset / Reset All.
- Shader Parameters (companion_core_shader_param_*): the menu shader's
  parameters with description (id when there is none), range, step,
  current; set (clamped), reset to initial, apply
  (CMD_EVENT_SHADERS_APPLY_CHANGES). Win32: parameter | value | range
  with a field and Apply / Reset; Cocoa: the same with the Value column
  editable in place.
- Options (companion_core_setting_*): the desktop-menu settings Qt's
  View > Options edits, as typed rows (bool / uint / string / choice)
  read and set from text with type checks. Win32: setting | value,
  double-click toggles a bool or cycles a choice, a field and Apply
  for the rest; Cocoa: editable Value column, double-click likewise.
- Cocoa dark theme: the Theme setting is honoured on 10.14+ by giving
  the companion window the dark appearance (system default follows the
  system).

Tests: companion_core_test builds a real core_option_manager from v2
definitions and a stub menu shader and checks every accessor, set /
reset / clamp / out-of-range, apply, and the settings table's read-back
and type refusals - plain and ASan. The Cocoa harness opens all three
windows on a real AppKit and drives them: rows and values, cycling and
reset, editing a shader value in the cell and applying, toggling a bool
and editing a number and the theme in the Options table.

companion_parity.py: 34 features (Qt's own three dialogs, which predate
the core, are waived with reasons). tools/companion_gap_audit.py now
reports zero gaps vs Qt for both Cocoa and Win32.

Verified: companion_core_test.sh plain / ASan pass;
companion_cocoa_test.sh passes; ui_win32_companion.c compiles to an
object with mingw (no warnings) and passes tools/mingw_syntax_check.sh
with companion_core.c and the consumers of companion_core.h; full Qt 6
retroarch builds and starts headless; companion_parity.py green;
apple_mrc_ownership_check.py: one more local autoreleased object. Not
run on Windows / macOS.

* ui/companion: Win32 regression harness on Linux (mingw + Wine, headless)

The Win32 companion gets the same net the Cocoa one has: the real
ui/drivers/ui_win32_companion.c, built with mingw and linked with the
real companion core, the core test's stubs and fixtures and a stub for
RetroArch's main window and modal helpers, runs under Wine on Xvfb
(tools/companion_win32_test.sh). Wine's comctl32 is not Microsoft's,
but the object graph, the messages and their side effects are the same
- which is where every Win32 bug so far has been.

companion_win32_test.c drives the driver as RetroArch and a user do -
init, iterate until the playlist lands, then the messages the widgets
send (the View combo's CBN_SELCHANGE and the menu's WM_COMMAND ids, the
tab's TCN_SELCHANGE, the header's LVN_COLUMNCLICK, list selections,
WM_CLOSE) - and checks the real controls: the playlist list's rows and
text; the entries list's count, its view style after switching to
Icons and back; the search filter and Clear; the File Browser - the
async listing landing, the folder pane's rows and "..", the content
list's rows and its four columns and the Type text, the empty boxart
pane, a header click sorting by Type and the same click flipping it,
Up and Start Directory; Stop reaching the core; the Core Options,
Shader Parameters and Options windows opening with the right rows,
value labels, a double-click cycling an option; the window hiding on
close. 33 checks, all green; with the browse-landed rebuild removed it
reports the four panes that go stale, so it catches the class of bug
it exists for.

The driver's control and menu ids move to ui_win32_companion_ids.h so
the harness sends exactly the commands the widgets send (its first
draft carried a copy of the enum, which had drifted). The core test's
fixtures gain the two Windows spellings they needed (mkdir, utime,
sleeping). Wired into CI beside the other suites.

Verified: companion_win32_test.sh passes; companion_cocoa_test.sh and
companion_core_test.sh pass; ui_win32_companion.c and the consumers of
the new header pass tools/mingw_syntax_check.sh; companion_parity.py
green.

* ui/drivers: fix the Xcode (ARC) build and the overlap-copy finding

Xcode, which builds the Cocoa companion with -fobjc-arc: the grid's
first-paint log cast an object pointer to void* without a bridge
(BRIDGE, as the rest of the file does), and the three table windows
passed the address of an ivar as the makeTableWindow out-parameter,
which ARC treats as an __autoreleasing write-back and refuses for a
non-local; each now goes through a local and assigns the ivar.

tools/overlap_copy_check.py: the Win32 Options list read buf[0] in the
source expression of a strlcpy into buf; the value is now read into a
bool before either literal is copied.

Verified: overlap_copy_check.py clean; companion_cocoa_test.sh and
companion_win32_test.sh pass; ui_win32_companion.c passes
tools/mingw_syntax_check.sh; apple_mrc_ownership_check.py unchanged.
The deprecated NSTitledWindowMask names in the log are the pre-10.12
spellings the file uses throughout for the 10.4 SDK; warnings only.

* ui/drivers: the Mac main loop never iterated the companion - "Loading playlist..." for ever

The screenshot says it: "Loading playlist..." in the status bar, the
entries pane empty in both views, the File Browser empty. Every one of
those is an asynchronous result that lands through
ui_companion_driver_wimp_iterate() - the playlist parse, the browser's
listing, decoded thumbnails, animation frames, status - and on a real
Mac that hook was never called.

The only call was in the Qt-era -rarch_main loop in ui_cocoa.m, which
is under #ifdef HAVE_QT. The Xcode build has no Qt: its whole main loop
is the CFRunLoop observer rarch_draw_observer in cocoa_common.m, which
called runloop_iterate() and task_queue_check() and nothing else. So
the Cocoa companion built its window, kicked off its work, and nothing
ever arrived. The observer now calls the hook each frame, as the
generic loop in retroarch.c does for the other platforms.

Why the harness did not catch it: it calls the driver's iterate itself,
standing in for the platform loop. companion_parity.py now also checks
that the platforms' real main loops call ui_companion_driver_wimp_
iterate() (rarch_draw_observer today; the generic loop covers Win32 and
Qt) - it fails on the previous cocoa_common.m and passes on this one.

This is also the most likely cause of the earlier "grid shows nothing"
and "browser is empty" reports, which the GNUstep harness could never
reproduce: it iterates.

Verified: companion_parity.py green (with the new check);
cocoa_common.m compiles under GNUstep's syntax check. Not run on macOS:
the Xcode CI build and your next run are the gate.

* ui/drivers: Cocoa - hand the keyboard back in every run-loop mode, and when the window resigns key

After closing the companion the mouse controls RetroArch again but the
keyboard does not. On macOS the mouse goes to the window under it,
while key events go to the key window through RAWindow -sendEvent:, so
this is the picture of RetroArch's window being main but not key after
the close.

Two things about the previous hand-back could let that happen. Its
deferred re-assert was a default-mode timer, and the close button's
click is dispatched in the event-tracking mode, so the re-assert could
wait until the mode changed; and it ran only from the delegate's close
methods, whereas AppKit's own choice of the next key window - made
while the close is in progress - is what has to be beaten.

Now: the re-assert is scheduled in the default, event-tracking and
modal-panel modes, three times over 40 ms (the arbitration can run
late); it also runs when the companion window resigns key while not
visible (the moment the close takes the key status away); and it does
nothing if the window has been shown again meanwhile. Each attempt
logs whether the host ended up key and main and what NSApp's key
window is, so a --verbose run says exactly where it lands if this is
still not it.

Verified: GNUstep compile; companion_cocoa_test.sh passes;
companion_parity.py green; MRC lint unchanged. Not run on macOS.

* ui/drivers: Cocoa - let the companion window actually close; the keyboard comes back

The keyboard stayed with a window that no longer existed on screen.
windowShouldClose: hid the companion itself and returned NO - so AppKit
was told the window was not closing. It stayed open in AppKit's eyes,
hidden but still the key window, and key events kept going to it while
mouse events went to the window under the pointer: exactly "mouse works,
keyboard does not". And since no close ever happened, windowWillClose:
and windowDidResignKey: - where every hand-back attempt of the last
three patches lived - never fired.

windowShouldClose: now returns YES. AppKit closes the window (kept
alive by setReleasedWhenClosed:NO; the driver's toggle orders it back),
resigns its key status, and the hand-back runs from the delegate
methods that now fire. The programmatic hide on loading content keeps
its own hand-back.

The harness asserts the delegate returns YES and closes the window
through AppKit, then shows it again through the driver's toggle and
checks it still lists the playlists.

Verified: companion_cocoa_test.sh passes; MRC lint unchanged. Not run
on macOS.

* ui/companion: run the Cocoa harness on Apple's AppKit in CI; assert a keystroke after the close

The keyboard still does not return to RetroArch after the companion
closes on a real Mac, and the GNUstep harness keeps passing: whatever
this is, it is in Apple's AppKit - key-window arbitration, activation -
which GNUstep only approximates. There is no Mac here to debug on; the
macOS CI runner has a window server and can run AppKit, so the harness
runs there now, built with Apple's clang against the real Cocoa
framework (tools/companion_cocoa_test_macos.sh, no shims), as a step of
the macOS workflow ahead of the Xcode build.

The harness also asks the actual question now. RetroArch reads the
keyboard in RAWindow -sendEvent:, which AppKit calls on the key window
for key events; the harness's stand-in for RetroArch's window counts
the key events reaching its -sendEvent:. After closing the companion
through AppKit (performClose:), it posts a keystroke through NSApp and
asserts it arrived - and prints which window NSApp holds as key if not.
The harness also becomes a regular, activatable app first, since
Apple's AppKit gives no key window to a background process.

Under GNUstep the new checks pass (NSApp's key window is RetroArch's;
the posted keystroke reaches -sendEvent:). The macOS job's log is the
answer for Apple's AppKit.

* ui/companion: Cocoa harness - report faults and exceptions with a backtrace

The first run on Apple's AppKit got through init, the playlist, the
grid (a real frame, painted), the File Browser, Stop, Contributors and
the menu items, then died with "Trace/BPT trap: 5" right after
"addPaths: implemented" - a SIGTRAP with no message. The harness now
installs handlers for SIGTRAP / SIGSEGV / SIGBUS / SIGABRT / SIGILL that
print a backtrace, and an uncaught-exception handler that prints the
exception's name, reason and call stack, so the next log names the
place. Methods returning size_t / BOOL are invoked through NSInvocation
rather than performSelector: (which is for id returns and undefined for
these on Apple's runtime), in case that was the trap.

Under GNUstep the harness still passes.

* ui/companion: overlapping strlcpy on a playlist reload (trapped on macOS); test builds trap it too

The harness on Apple's AppKit got through everything up to adding files
to the shown playlist, then trapped in __strlcpy_chk -> __chk_fail_
overlap inside companion_core_begin_playlist. The path: a backend adds
files to the playlist it is showing and hands the core the pointer it
got from companion_core_selected_playlist_path() - the core's own
selected_path buffer. add_files reloads that playlist through
select_playlist_path -> begin_playlist, which strlcpy'd the path into
selected_path: source and destination the same buffer. Undefined, and
macOS's fortified libc aborts on it; Linux's strlcpy (libretro-common's
own) silently did the copy, which is why every Linux suite passed.

begin_playlist now takes a copy of @path before it clears state or
writes selected_path.

So the Linux suites catch this class from now on: under
LIBRETRO_STRL_CHECK_OVERLAP, libretro-common's strlcpy aborts on an
overlapping copy exactly as Apple's does, and the four companion test
scripts build with it. companion_core_test adds the aliasing call the
backends make (add files through the selected-path pointer, reload,
count) - on the previous core it aborts with "strlcpy: overlapping copy";
on this one it passes. All four suites pass with the check on.

Verified: companion_core_test.sh plain / ASan, companion_thumbs_test.sh,
companion_cocoa_test.sh, companion_win32_test.sh pass;
companion_core.c and compat_strl.c pass tools/mingw_syntax_check.sh
and build; overlap_copy_check.py clean.

* ui/drivers: Cocoa - forget held keys when another window takes the keyboard (the dead keyboard)

The harness on Apple's AppKit settled the key-window question: after
the companion closes, RetroArch's window is key and main and a posted
keystroke reaches RAWindow -sendEvent:. So the keyboard was arriving,
and RetroArch was ignoring it.

The mechanism is RetroArch's own held-key state. The companion is
opened with a key (its hotkey, or Enter in the menu). The moment its
window becomes key, that key is still down; its release is delivered to
the companion window, so RetroArch's window never sees the key-up and
apple_key_state keeps the key "held". The menu's flush-and-wait-for-
release (menu_st->input_driver_flushing_input ->
INP_FLAG_WAIT_INPUT_RELEASE) then waits for that release - for ever.
Keyboard dead, mouse (which does not go through that gate) fine, and
closing the companion changes nothing because the key is still "down".
Deactivating the app clears it, which is why RetroArch already calls
apple_input_keyboard_reset() from applicationWillResignActive.

Same treatment for losing the keyboard to another window of our own:
RetroArch_OSX (RAWindow's delegate) resets the keyboard state in
windowDidResignKey:, and the companion's toggle resets it right after
taking the keyboard, covering a show from a non-key state. The harness
stubs apple_input_keyboard_reset and asserts the companion's show calls
it.

Verified: companion_cocoa_test.sh passes under GNUstep; MRC lint
unchanged. Apple's AppKit run of the harness (previous commit) is what
established the key-window half. Not run in the real app on macOS.

* ui/companion: keep the playlist config off the stack; guard the selected_path self-copy by pointer

companion_core_begin_playlist() no longer takes a PATH_MAX_LENGTH copy
of @path to survive being handed core->selected_path: the copy is
skipped when the pointer already is that buffer, the way path_set()
does it, and the buffer already holds the path. The playlist_config_t
that begin_playlist, all_next, playlist_open and playlist_open_private
each built on the stack now lives in companion_core_t; playlist_init()
and playlist_parse_begin() copy it, and all four run on the UI thread,
so one is enough. begin_playlist drops from 5184 to 32 bytes of frame
and the other three from ~3 KiB to under 64 bytes, within the 4 KiB
ceiling CODING-GUIDELINES sets.

* ui/drivers: Cocoa - the icon grid's prefetch read past the thumbnail marks (the crash)

Guard Malloc named it exactly: a byte read one past the end of a
MALLOC_TINY block in -[RACompanionController thumbWant:urgent:]. (The
earlier crash in MoltenVK was the same corruption, noticed later by an
unrelated allocation - the usual way an overrun surfaces.)

iconTick prefetches a screen either side of the visible rows, so it
calls thumbWant: with rows that do not exist. hasImageForRow: answers
NO for those - it means "no image", not "no row" - so thumbWant: went
on to read thumbNone[row], and thumbNone is only as long as the list.
Reading one byte past a heap block: harmless on glibc, fatal under
Apple's allocator.

thumbNone now carries its length (thumbNoneCount) and every access is
bounded by it: thumbWant: returns for a row outside the list, the idle
re-scan clamps, the delivery callback checks the array's length as well
as the current entry count (a delivery for the previous list), and the
two memsets use the array's own length rather than a count that may
have moved on since it was allocated.

Regression test, in the harness: drive iconTick repeatedly and call
thumbWant: with rows either side of the range, and run the whole
harness under ASan (tools/companion_cocoa_test.sh asan, wired into CI
beside the plain run). On the previous driver ASan reports
heap-buffer-overflow at ui_cocoa_companion.m:976 in thumbWant: and the
script exits 5; on this one both passes are clean. The script also
propagates the run's exit status now - the pipe through the log filter
was swallowing it, so a sanitizer abort would have looked like a pass.

Verified: companion_cocoa_test.sh plain and ASan pass (and fail as
described on the previous driver); companion_core_test.sh,
companion_thumbs_test.sh, companion_win32_test.sh pass;
companion_parity.py green; MRC lint unchanged.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-09-07 23:12:20 +02:00

98 lines
4.6 KiB
Bash
Executable File

#!/bin/sh
# Syntax-check every C translation unit a Win32 MSYS2 build compiles,
# with the defines that build actually uses, on a mingw cross compiler.
#
# Why this exists: a header edit checked by compiling only the file that
# was edited will pass while another TU that includes the same header
# fails. The include order differs per TU, so a typedef visible in one
# is absent in another (vulkan_win32.h behind an include guard set
# before VK_USE_PLATFORM_WIN32_KHR was defined, for instance). The only
# check that catches that is compiling all of them.
#
# Usage:
# tools/mingw_syntax_check.sh --consumers-of gfx/common/foo.h [...]
# compile every .c that directly includes any of the named headers.
# THIS is the check to run after editing a header.
# tools/mingw_syntax_check.sh file.c [...]
# just those files
# tools/mingw_syntax_check.sh
# every .c under gfx/ audio/ input/ menu/ and the top level
#
# Prints one line per failing TU and exits non-zero if any failed.
CC="${CC:-x86_64-w64-mingw32-gcc}"
command -v "$CC" >/dev/null 2>&1 || { echo "no $CC" >&2; exit 2; }
FLAGS="-fsyntax-only -std=gnu99 -I. -Ilibretro-common/include -Ideps \
-Ideps/stb -Igfx/include \
-DRARCH_INTERNAL -DHAVE_THREADS -DHAVE_CONFIGFILE -DHAVE_MENU \
-DHAVE_VULKAN -DHAVE_SLANG -DHAVE_SPIRV_CROSS -DHAVE_D3D11 -DHAVE_D3D12 \
-DHAVE_D3DKMT -DHAVE_DINPUT -DHAVE_XINPUT -DHAVE_WASAPI -DHAVE_XAUDIO \
-DHAVE_NETWORKING -DHAVE_CHEEVOS -DHAVE_RUNAHEAD -DHAVE_REWIND \
-DHAVE_OVERLAY -DHAVE_RGUI -DHAVE_XMB -DHAVE_OZONE -DHAVE_MATERIALUI \
-DHAVE_GFX_WIDGETS -DHAVE_SHADERPIPELINE -DHAVE_CG -DHAVE_GLSL \
-DHAVE_OPENGL -DHAVE_OPENGL_CORE -DHAVE_DSOUND -DHAVE_AUDIOMIXER \
-DHAVE_TRANSLATE -DHAVE_SCREENSHOTS -DHAVE_PATCH -DHAVE_BSV_MOVIE"
if [ "$1" = "--consumers-of" ]; then
shift
FILES=""
for h in "$@"; do
base=$(basename "$h")
# Linux-only test harnesses under */test/ are not Win32 / C89
# consumers (they have their own scripts under tools/).
FILES="$FILES $(grep -rl --include='*.c' "#include.*[\"/]$base\"" . \
| grep -v '^./deps/' | grep -v '/test/' | sed 's#^\./##')"
done
FILES=$(echo $FILES | tr ' ' '\n' | sort -u)
[ -z "$FILES" ] && { echo "no consumers found for: $*" >&2; exit 2; }
echo "checking $(echo "$FILES" | wc -l) consumers of: $*"
elif [ $# -gt 0 ]; then
FILES="$*"
else
FILES=$(find gfx audio input menu -name '*.c' \
-not -path '*/deps/*' -not -path '*/include/*' 2>/dev/null; \
ls *.c 2>/dev/null)
fi
# Pass 2: the linux-c89 CI job's flags, verbatim from the Makefile's
# C89_BUILD block, on the native compiler. -Werror=pedantic and
# -Werror=declaration-after-statement are what MSVC-era C89 compliance
# actually means here, and neither is implied by -std=gnu99 above. A
# file that passes pass 1 and fails this one is exactly what has broken
# the build before: a statement placed between declarations.
C89CC="${C89CC:-gcc}"
C89FLAGS="-fsyntax-only -std=c89 -ansi -pedantic -Werror=pedantic \
-Wno-long-long -Werror=declaration-after-statement -Wno-variadic-macros \
-D_GNU_SOURCE -I. -Ilibretro-common/include -Ideps -Ideps/stb -Igfx/include \
-DRARCH_INTERNAL -DHAVE_THREADS -DHAVE_CONFIGFILE -DHAVE_MENU \
-DHAVE_NETWORKING -DHAVE_CHEEVOS -DHAVE_RUNAHEAD -DHAVE_REWIND \
-DHAVE_AUDIOMIXER -DHAVE_OVERLAY -DHAVE_RGUI -DHAVE_XMB -DHAVE_OZONE"
fail=0; n=0
for f in $FILES; do
n=$((n+1))
# Only real errors, not warnings. A missing header named without a
# path (d3dkmthk.h: an optional platform header this box lacks) is
# forgiven; a missing header with a path component (../companion/x.h:
# a project header) is not - that once let a stale include of a
# deleted header through as "ok".
err=$($CC $FLAGS "$f" 2>&1 | grep -E ' error: ' \
| grep -vE 'error: [A-Za-z0-9_.-]+: No such file|error: [A-Za-z0-9_.-]+: file not found' | head -3)
if [ -n "$err" ]; then
echo "FAIL [win32] $f"; echo "$err" | sed 's/^/ /'; fail=1
fi
# Skip pass 2 for files that are Windows-only by path; they are not
# in the linux-c89 job and -ansi breaks the Windows headers.
case "$f" in
*win32*|*dinput*|*xinput*|*wasapi*|*xaudio*|*asio*|*dsound*|*d3d*|*dxgi*|*wgl*|*uwp*|*winraw*|*_w.c|*/w_*) continue;;
esac
err=$($C89CC $C89FLAGS "$f" 2>&1 | grep -E ' error: ' \
| grep -vE 'error: [A-Za-z0-9_.-]+: No such file|error: [A-Za-z0-9_.-]+: file not found' | head -3)
if [ -n "$err" ]; then
echo "FAIL [c89] $f"; echo "$err" | sed 's/^/ /'; fail=1
fi
done
[ $fail = 0 ] && echo "ok: $n translation units clean (win32 gnu99 + linux c89 pedantic)"
exit $fail