mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
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]>
This commit is contained in:
1 parent
dc82a34bba
commit
4b460947b4
65 files changed
+20245
-3179
No files matched your search
@@ -94,6 +94,79 @@ jobs:
|
||||
# free, on Linux.
|
||||
python3 tools/vfs_backend_parity.py
|
||||
|
||||
- name: Check companion backend feature parity
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -eu
|
||||
# The Qt, Win32 and Cocoa desktop companions share one C core
|
||||
# but each has its own presentation; a backend can quietly omit
|
||||
# a feature (no file browser, no scan, no core picker) and still
|
||||
# build and run, the gap only showing when a user switches
|
||||
# drivers. Source-level check, for free, on Linux.
|
||||
python3 tools/companion_parity.py
|
||||
|
||||
- name: Companion thumbnail engine regression test (plain, TSan, ASan)
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -eu
|
||||
# The shared thumbnail engine every desktop companion draws
|
||||
# through: real decoder, decode threads, cache, priority,
|
||||
# cancel, shutdown. Under both sanitizers, since it is the
|
||||
# one piece of threaded code in the companions.
|
||||
tools/companion_thumbs_test.sh
|
||||
tools/companion_thumbs_test.sh tsan
|
||||
tools/companion_thumbs_test.sh asan
|
||||
|
||||
- name: Companion core regression test (plain, ASan)
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -eu
|
||||
# The toolkit-free model every desktop companion drives:
|
||||
# playlist listing and All Playlists, the thumbnail path rule,
|
||||
# the file browser, the run / needs-core paths, launch
|
||||
# options - against real .lpl fixtures, with RetroArch's
|
||||
# state replaced by stubs.
|
||||
tools/companion_core_test.sh
|
||||
tools/companion_core_test.sh asan
|
||||
tools/companion_core_test.sh tsan
|
||||
|
||||
- name: AUTHORS_c.h in sync with AUTHORS.h
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: python3 tools/gen_authors_c.py --check
|
||||
|
||||
- name: Cocoa companion regression harness (GNUstep AppKit, headless)
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -eu
|
||||
# The real ui/drivers/ui_cocoa_companion.m, compiled and linked
|
||||
# against GNUstep's Foundation + AppKit with the real companion
|
||||
# core and the core test's fixtures, driven under Xvfb the way a
|
||||
# user drives it (view popup, File Browser tab, close) and
|
||||
# checked on the AppKit objects themselves.
|
||||
sudo apt-get install -y -qq gobjc gnustep-devel gnustep-back-common xvfb > /dev/null
|
||||
tools/companion_cocoa_test.sh
|
||||
# ASan too: the grid's prefetch reaching past the thumbnail
|
||||
# marks only shows under a checking allocator (it crashed the
|
||||
# app on macOS, where Apple's is one).
|
||||
tools/companion_cocoa_test.sh asan
|
||||
|
||||
- name: Win32 companion regression harness (mingw + Wine, headless)
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
set -eu
|
||||
# The real ui/drivers/ui_win32_companion.c, built with mingw and
|
||||
# linked with the real companion core and the core test's
|
||||
# fixtures, run under Wine on Xvfb and driven with the messages
|
||||
# the widgets send; checked on the real controls.
|
||||
sudo apt-get install -y -qq gcc-mingw-w64-x86-64 wine xvfb > /dev/null
|
||||
tools/companion_win32_test.sh
|
||||
|
||||
- name: Check stack frame budget
|
||||
shell: bash
|
||||
working-directory: ${{ github.workspace }}
|
||||
|
||||
@@ -17,6 +17,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Cocoa companion harness on Apple's AppKit
|
||||
run: |
|
||||
set -o pipefail
|
||||
# The same harness the Linux job runs under GNUstep, built with
|
||||
# Apple's clang against the real Cocoa framework and run on the
|
||||
# runner's window server: key-window arbitration after closing the
|
||||
# companion, the grid, the tables, the dialogs - as AppKit does them.
|
||||
tools/companion_cocoa_test_macos.sh 2>&1 | grep -v "^$"
|
||||
|
||||
- name: Compile RA
|
||||
run: |
|
||||
set -o pipefail
|
||||
|
||||
+519
@@ -0,0 +1,519 @@
|
||||
/* Generated from AUTHORS.h by tools/gen_authors_c.py - do not edit. */
|
||||
#ifndef __RETROARCH_AUTHORS_C_H
|
||||
#define __RETROARCH_AUTHORS_C_H
|
||||
static const char *retroarch_contributors_list =
|
||||
"\n"
|
||||
"RetroArch and the libretro team would like to\n"
|
||||
"acknowledge the following contributors:\n"
|
||||
"\n"
|
||||
"1ch (L0sted)\n"
|
||||
"23rd\n"
|
||||
"6lackmag3\n"
|
||||
"A Schain (aschain)\n"
|
||||
"Aaron Oneal (aarononeal)\n"
|
||||
"Aaron Ramirez (aaronbytes)\n"
|
||||
"Abadede\n"
|
||||
"Adam Londero (alondero)\n"
|
||||
"Adam Mechtley (amechtley)\n"
|
||||
"Adriano de Moura Leite (amlib)\n"
|
||||
"Adrien Moulin (elyosh)\n"
|
||||
"Aethyrion\n"
|
||||
"ajefr\n"
|
||||
"AKuHAK\n"
|
||||
"Alberto Alonso Ruibal (albertoruibal)\n"
|
||||
"Alberto Fustinoni (albertofustinoni)\n"
|
||||
"Alberto Simon (asimonf)\n"
|
||||
"Alcaro\n"
|
||||
"aldebaran27\n"
|
||||
"Aleksey Samoilov (Sunderland93)\n"
|
||||
"Alessandro Mangone (Vektor) (zevektor)\n"
|
||||
"Alex Folland (AlexFolland)\n"
|
||||
"Alex Kornitzer (alexkornitzer)\n"
|
||||
"Alexander Spady (alkaseltzerspadt)\n"
|
||||
"Alexander Trufanov (trufanov-nok)\n"
|
||||
"Alexandre Garcia (Alexandre-Garcia)\n"
|
||||
"Alfredo Monclus (alfrix)\n"
|
||||
"aliaspider\n"
|
||||
"alphanu1\n"
|
||||
"altiereslima\n"
|
||||
"Alwin Garside (Yogarine)\n"
|
||||
"amtz\n"
|
||||
"Anders Storhaug (andersstorhaug)\n"
|
||||
"Andre Leiradella (leiradel)\n"
|
||||
"Andrés (fr500)\n"
|
||||
"Andy Nguyen (TheOfficialFloW)\n"
|
||||
"Anthony J. Bentley (bentley)\n"
|
||||
"Antonio Jose Ramos Marquez (psxdev)\n"
|
||||
"antonioginer\n"
|
||||
"Apaczer\n"
|
||||
"AridRayne\n"
|
||||
"ArnaudFaucher\n"
|
||||
"arpruss\n"
|
||||
"arstn\n"
|
||||
"Artem Polishchuk (tim77)\n"
|
||||
"Arto Vainiolehto (arakerlu)\n"
|
||||
"asavah\n"
|
||||
"Ash (ashquarky)\n"
|
||||
"Ashura (CoalaJoe)\n"
|
||||
"Avton1\n"
|
||||
"ayssia\n"
|
||||
"BananaRama203\n"
|
||||
"barbudreadmon\n"
|
||||
"Barry Rowe (BarryJRowe)\n"
|
||||
"Bartłomiej Burdukiewicz (dev-0x7C6)\n"
|
||||
"Bashar Astifan (basharast)\n"
|
||||
"bearoso\n"
|
||||
"Ben Hamilton (Ben Gertzfield) (bhamiltoncx)\n"
|
||||
"benklop\n"
|
||||
"benutzer193\n"
|
||||
"Bernhard Schelling (schellingb)\n"
|
||||
"blackgur\n"
|
||||
"bmaupin\n"
|
||||
"Bobby Smith (bslenul)\n"
|
||||
"Boris Timofeev (btimofeev)\n"
|
||||
"Brad (SpiralBrad)\n"
|
||||
"Braden Farmer (farmerbb)\n"
|
||||
"Brandon Johnson (BinaryFissionGames)\n"
|
||||
"Breno Dantas Mendonça (brenodantas10)\n"
|
||||
"Brian Hoem (ZornTaov)\n"
|
||||
"Brian Koropoff (bkoropoff)\n"
|
||||
"Brian S. Stephan (bsstephan)\n"
|
||||
"BringerethOfLight\n"
|
||||
"Brunnis\n"
|
||||
"bsmith (DogParty)\n"
|
||||
"bubuleur\n"
|
||||
"bulzipke\n"
|
||||
"C.W. Betts (MaddTheSane)\n"
|
||||
"Casey Borders (CaseyB)\n"
|
||||
"CatGreen90\n"
|
||||
"ceb33\n"
|
||||
"cedorg\n"
|
||||
"celerizer\n"
|
||||
"Charlène (julianaito)\n"
|
||||
"CheezeCake\n"
|
||||
"Chris Merrett (chrisfu)\n"
|
||||
"Christian Murphy (grimpunch)\n"
|
||||
"christianhaitian\n"
|
||||
"Christoffer Tibell (Brottweiler)\n"
|
||||
"Christopher Snowhill (kode54)\n"
|
||||
"chu (9chu)\n"
|
||||
"Chuck Smith (chucksmith)\n"
|
||||
"CidVonHighwind\n"
|
||||
"clienthax\n"
|
||||
"cold-brewed\n"
|
||||
"Colin Kinloch (ColinKinloch)\n"
|
||||
"CompCom\n"
|
||||
"ComradeEcho\n"
|
||||
"confusedguy9\n"
|
||||
"Conn O'Griofa (psyke83)\n"
|
||||
"Connor McLaughlin (stenzek)\n"
|
||||
"Conrad Kreyling (konistehrad)\n"
|
||||
"CozmoP\n"
|
||||
"Cpasjuste\n"
|
||||
"cpsw\n"
|
||||
"Cray Elliott (MP2E)\n"
|
||||
"Crestwave\n"
|
||||
"CrewMdk\n"
|
||||
"Cristi Mitrana (cmitu)\n"
|
||||
"Cristian Sandu (crazyquark)\n"
|
||||
"crystalct\n"
|
||||
"CTCaer\n"
|
||||
"cucholix\n"
|
||||
"cudencuden\n"
|
||||
"CyberLabSystems\n"
|
||||
"Daehan Kim (iAmGhost)\n"
|
||||
"dalter\n"
|
||||
"Dan Weiss (Dwedit)\n"
|
||||
"danieljg\n"
|
||||
"Daniil Zhilin (LaserMoai)\n"
|
||||
"dankcushions\n"
|
||||
"Darren Thompson (DiscoStarslayer)\n"
|
||||
"Dave (freakdave)\n"
|
||||
"Dave Leaver (danzel)\n"
|
||||
"David CARLIER (devnexen)\n"
|
||||
"David Demelier (markand)\n"
|
||||
"David Erickson (daviderickson)\n"
|
||||
"David G. F. (davidgfnet)\n"
|
||||
"David Lawson (dmrlawson)\n"
|
||||
"David Walters (hiddenasbestos)\n"
|
||||
"dbaronica\n"
|
||||
"DEX357\n"
|
||||
"diablodiab\n"
|
||||
"dibas\n"
|
||||
"Diego A. (SuperrSonic)\n"
|
||||
"Diego Viola (diegoviola)\n"
|
||||
"digitalisland-nino\n"
|
||||
"Diogo Barros (BlueKore)\n"
|
||||
"Dmitry Sergeenkov (DuMOHsmol)\n"
|
||||
"dor3k\n"
|
||||
"Doug Christman (dobyrch)\n"
|
||||
"driver1998\n"
|
||||
"Dudu Akiva (duduke)\n"
|
||||
"duganchen\n"
|
||||
"dukemiller\n"
|
||||
"duskgao\n"
|
||||
"Dylan Scott (dyscott)\n"
|
||||
"Dystopian\n"
|
||||
"eadmaster\n"
|
||||
"Eduard Strehlau (Herdinger)\n"
|
||||
"ekipan\n"
|
||||
"Elvira Khabirova (parport0)\n"
|
||||
"embercold\n"
|
||||
"Emmanuel Martin (daliaetnano)\n"
|
||||
"Ethan Lee (flibitijibibo)\n"
|
||||
"extrowerk\n"
|
||||
"Ezio-PS\n"
|
||||
"F (erbridge)\n"
|
||||
"Fabio (Oibaf66)\n"
|
||||
"Fabio Ritrovato (Sephiroth87)\n"
|
||||
"Fayne Aldan (FayneAldan)\n"
|
||||
"fff (felinben)\n"
|
||||
"Filipe Azevedo (pasnox)\n"
|
||||
"FIX94\n"
|
||||
"Flame Sage (flamesage)\n"
|
||||
"flyinghead\n"
|
||||
"FmT0\n"
|
||||
"francescotintori\n"
|
||||
"Francisco Javier Trujillo Mata (fjtrujy)\n"
|
||||
"Francisco José García García (frangarcj)\n"
|
||||
"FreeShippingParty\n"
|
||||
"fuzzydunlops732 (JKoppel93)\n"
|
||||
"GABO1423\n"
|
||||
"Gabriel (Ghabry)\n"
|
||||
"GameDragon2k\n"
|
||||
"Garrett Brown (garbear)\n"
|
||||
"gatecat (daveshah1)\n"
|
||||
"GavinDarkglider\n"
|
||||
"gblues\n"
|
||||
"Geoffrey Plitt (GeoffreyPlitt)\n"
|
||||
"Giovanni Cascione (spleen1981)\n"
|
||||
"Glenn Hevey (hevey)\n"
|
||||
"Googer\n"
|
||||
"gordon-fish\n"
|
||||
"gouchi\n"
|
||||
"Gregor Richards (GregorR)\n"
|
||||
"Grzegorz Raciborski (graciborski)\n"
|
||||
"Gui Andrade (archshift)\n"
|
||||
"Guillaume Thiolliere (thiolliere)\n"
|
||||
"Guo Yunhe (guoyunhe)\n"
|
||||
"Gustavo Maciel Dias Vieira (gmdvieira)\n"
|
||||
"gvbr\n"
|
||||
"hadess\n"
|
||||
"Hans-Kristian Arntzen (Themaister)\n"
|
||||
"Henri Gomez (hgomez)\n"
|
||||
"Henri Gomez (hgomez-sonarsource)\n"
|
||||
"Henry Castro (hcvcastro)\n"
|
||||
"Higor E. (heuripedes)\n"
|
||||
"hizzlekizzle\n"
|
||||
"Hugo B. (hug0b)\n"
|
||||
"Hugo Hromic (hhromic)\n"
|
||||
"hunterk\n"
|
||||
"Hyllian\n"
|
||||
"HyperspaceMadness\n"
|
||||
"i30817\n"
|
||||
"im4potato\n"
|
||||
"ImJezze\n"
|
||||
"İbrahim Güngör (igungor)\n"
|
||||
"Jack (jakcron)\n"
|
||||
"Jacob (cubtekki)\n"
|
||||
"Jacob (RegenJacob)\n"
|
||||
"Jacob Patterson (jacobpa)\n"
|
||||
"jacobsson (Roet-Ivar)\n"
|
||||
"Jahed\n"
|
||||
"Jake Stine (jstine35)\n"
|
||||
"James Le Cuirot (chewi)\n"
|
||||
"James Sexton (Bezier89)\n"
|
||||
"Jamiras\n"
|
||||
"Jan Holthuis (Holzhaus)\n"
|
||||
"Jason Tomsic (plaidman)\n"
|
||||
"Javier Martinez Canillas (martinezjavier)\n"
|
||||
"Jay McCarthy (jeapostrophe)\n"
|
||||
"jdgleaver\n"
|
||||
"Jean-André Santoni (kivutar)\n"
|
||||
"Jean-Paul Mari (djipi)\n"
|
||||
"Jean-Sébastien Guay (Skylark13)\n"
|
||||
"Jeff (jeffbdavenport)\n"
|
||||
"Jeff Sousa (LordeIlluminati)\n"
|
||||
"jess (winneon)\n"
|
||||
"Jesse Talavera-Greenberg (JesseTG)\n"
|
||||
"Joan Coll Cerdán (johanbcn)\n"
|
||||
"Job Adrian Salinas Gonzalez (efylan)\n"
|
||||
"Joe Osborn (JoeOsborn)\n"
|
||||
"Joel (joel16)\n"
|
||||
"Joel Puig Rubio (XXLuigiMario)\n"
|
||||
"Joerg Sonnenberger (jsonn)\n"
|
||||
"Johannes Schickel (lordhoto)\n"
|
||||
"John Parton (john-parton)\n"
|
||||
"John Regan (jprjr)\n"
|
||||
"Jon Maddox (maddox)\n"
|
||||
"Jonathan (JonnyMe)\n"
|
||||
"Jonathan Relf (jonathanrelf)\n"
|
||||
"Joohan Lee (losernator)\n"
|
||||
"Jools Wills (joolswills)\n"
|
||||
"Jose Lage Miguez (Joselagem)\n"
|
||||
"Joseph Carter (knghtbrd)\n"
|
||||
"Joseph Conan Montgomery (Grotke)\n"
|
||||
"jsd1982 (JamesDunne)\n"
|
||||
"jSTE0\n"
|
||||
"JuanVCS\n"
|
||||
"Justin Jacobs (dorkster)\n"
|
||||
"Justin Weiss (justinweiss)\n"
|
||||
"JustMeDaFaq\n"
|
||||
"Kangmo-Ahn\n"
|
||||
"Karl Forshaw (karlforshaw)\n"
|
||||
"Kate Eckhart (katepango)\n"
|
||||
"Keith Bowes (keithbowes)\n"
|
||||
"Ken Rossato (rossato)\n"
|
||||
"kikmon\n"
|
||||
"Kopert\n"
|
||||
"Krzysztof Haładyn (krzys_h) (krzys-h)\n"
|
||||
"kurumushi\n"
|
||||
"kwyxz\n"
|
||||
"l3iggs\n"
|
||||
"lambolighting\n"
|
||||
"lasers\n"
|
||||
"Lawrence Kesteloot (lkesteloot)\n"
|
||||
"LazyBumHorse\n"
|
||||
"LazyFunker\n"
|
||||
"Lennard Kittner (Lennard599)\n"
|
||||
"LibretroAdmin\n"
|
||||
"Lionel Flandrin (simias)\n"
|
||||
"littleguy77\n"
|
||||
"liushuyu\n"
|
||||
"Logan McNaughton (loganmc10)\n"
|
||||
"lordashram\n"
|
||||
"Lothar Serra Mari (lotharsm)\n"
|
||||
"Lubosz Sarnecki (lubosz)\n"
|
||||
"lucasmr\n"
|
||||
"lucianposton\n"
|
||||
"lxerandrew\n"
|
||||
"m4xw\n"
|
||||
"mahoneyt944\n"
|
||||
"Mahyar Koshkouei (deltabeard)\n"
|
||||
"Mai (lioncash)\n"
|
||||
"MajorPainTheCactus\n"
|
||||
"maksun\n"
|
||||
"Manuel Alfayate Corchete (vanfanel)\n"
|
||||
"Marcelo Munhoz Pélos (mpelos)\n"
|
||||
"Marcus Crisostomo (mcrisostomo)\n"
|
||||
"markwkidd\n"
|
||||
"Maschell\n"
|
||||
"Mats (Myaats)\n"
|
||||
"Matt Sephton (gingerbeardman)\n"
|
||||
"Matthew Bauer (matthewbauer)\n"
|
||||
"Matthew Wang (asakous)\n"
|
||||
"Max K. (langerhans)\n"
|
||||
"Maxime Gauduin (alucryd)\n"
|
||||
"meancoot\n"
|
||||
"meepingsnesroms\n"
|
||||
"meleu\n"
|
||||
"Merry Mello (merryMellody)\n"
|
||||
"micechal\n"
|
||||
"Michael Burgardt (DisasterMo)\n"
|
||||
"Michael K. (kuxii2016)\n"
|
||||
"Michael Lelli (ToadKing)\n"
|
||||
"mickski56\n"
|
||||
"Mico Chopitea (micochops)\n"
|
||||
"miguelpinheiro\n"
|
||||
"Mike Swanson (chungy)\n"
|
||||
"mikeOSX\n"
|
||||
"minucce\n"
|
||||
"misson20000\n"
|
||||
"esoptron\n"
|
||||
"Monroe88\n"
|
||||
"Morgane (MorganeAD)\n"
|
||||
"mprobinson\n"
|
||||
"MrHuu\n"
|
||||
"MrJs (mrjschulte)\n"
|
||||
"Muhamed Hobi (mmhobi7)\n"
|
||||
"muzuiget\n"
|
||||
"N/A (inactive123)\n"
|
||||
"Nargash\n"
|
||||
"Nathan S. (natinusala)\n"
|
||||
"NathanStrong-Tripwire\n"
|
||||
"nattycleopatra\n"
|
||||
"Naveen (naveensrinivasan)\n"
|
||||
"nayslayer\n"
|
||||
"negativeExponent\n"
|
||||
"Neil Barkhina (nbarkhina)\n"
|
||||
"neil4\n"
|
||||
"netux79\n"
|
||||
"nfnty\n"
|
||||
"nfp0\n"
|
||||
"nia (alarixnia)\n"
|
||||
"Nicolas Adenis-Lamarre (nadenislamarre)\n"
|
||||
"Nicolas Guillaumin (nguillaumin)\n"
|
||||
"Nicolas Roehm (NicolasRoehm)\n"
|
||||
"Nik (WerWolv)\n"
|
||||
"Nikita Vakhrushev (XF10C)\n"
|
||||
"Nikola Kocić (nikola-kocic)\n"
|
||||
"Nikos Chantziaras (realnc)\n"
|
||||
"Nils Hasenbanck (hasenbanck)\n"
|
||||
"njordan64\n"
|
||||
"nosh01\n"
|
||||
"not6 (r-type)\n"
|
||||
"notaz\n"
|
||||
"nstCactus\n"
|
||||
"nurupo\n"
|
||||
"Oggom\n"
|
||||
"Oh Won Sik (dnjstlr555)\n"
|
||||
"Oleg Shevchenko (olsh)\n"
|
||||
"Omar Saleem (ofsaleem)\n"
|
||||
"opendata26\n"
|
||||
"Ophidon\n"
|
||||
"orbea\n"
|
||||
"Oschowa\n"
|
||||
"OsirizX\n"
|
||||
"OV2\n"
|
||||
"Palmic (mpalomas)\n"
|
||||
"pamapa\n"
|
||||
"Paolo Bonzini (bonzini)\n"
|
||||
"Paul McCarty (notabeatle)\n"
|
||||
"pcjco\n"
|
||||
"Pedro Ferraz (ferrazpedro)\n"
|
||||
"Pedro Ribeiro (pedrib)\n"
|
||||
"Peter J Kos (pkos)\n"
|
||||
"Philippe Simons (loki666)\n"
|
||||
"pinumbernumber\n"
|
||||
"Piotr Dobrowolski (Informatic)\n"
|
||||
"pjft\n"
|
||||
"Poligraf\n"
|
||||
"pponso1\n"
|
||||
"pyroesp\n"
|
||||
"R-YaTian\n"
|
||||
"Rafael Toledo (rafaeltoledo)\n"
|
||||
"Ramiro Morales (ramiro)\n"
|
||||
"Raphaël Zumer (rzumer)\n"
|
||||
"redenvelope2000\n"
|
||||
"Reid Kleckner (rnk)\n"
|
||||
"repojohnray\n"
|
||||
"retrotails\n"
|
||||
"revvv\n"
|
||||
"reztek\n"
|
||||
"Richard Howell (rmaz)\n"
|
||||
"Rinnegatamante\n"
|
||||
"rlnilsen\n"
|
||||
"Rob Loach (RobLoach)\n"
|
||||
"Robin de Rooij (rrooij)\n"
|
||||
"Romain Gay (vikbez)\n"
|
||||
"Romain Graillot (notnotme)\n"
|
||||
"Romain TISSERAND (rtissera)\n"
|
||||
"Roman Fomin (rfomin)\n"
|
||||
"Roman Kalashnikov (lunixoid)\n"
|
||||
"Royerson\n"
|
||||
"rsn8887\n"
|
||||
"Ruben Nine (rnine)\n"
|
||||
"Ryan C. Gordon (icculus)\n"
|
||||
"Ryan Kuba (thelamer)\n"
|
||||
"Ryunam\n"
|
||||
"rz5\n"
|
||||
"Sage (neonfuz)\n"
|
||||
"Saggi Mizrahi (ficoos)\n"
|
||||
"SAKUJ0\n"
|
||||
"Salvador (salvacam)\n"
|
||||
"Sam (44100hertz)\n"
|
||||
"Sam Lantinga (slouken)\n"
|
||||
"Sam Stenvall (Jalle19)\n"
|
||||
"Samuel P. (p-sam)\n"
|
||||
"Sanaki\n"
|
||||
"Scheiker\n"
|
||||
"Sean O'Neil (seanocali)\n"
|
||||
"Sebastien Ronsse (sronsse)\n"
|
||||
"SecularSteve\n"
|
||||
"Sefa Eyeoglu (Scrumplex)\n"
|
||||
"Sergei Nikitin (Kert)\n"
|
||||
"Sergei Trofimovich (trofi)\n"
|
||||
"Sergi Granell (xerpi)\n"
|
||||
"Sergio Padrino (sergiou87)\n"
|
||||
"sergio-br2 (sergiobenrocha2)\n"
|
||||
"Seth Kingsley (sethk)\n"
|
||||
"Shadow (ShadowOne333)\n"
|
||||
"Shane Mouton (ismouton)\n"
|
||||
"shantigilbert\n"
|
||||
"Shfty\n"
|
||||
"Shizeeg Unadequatov (shizeeg)\n"
|
||||
"SimpleTease\n"
|
||||
"SindenLightgun\n"
|
||||
"Sirrempat\n"
|
||||
"slotek\n"
|
||||
"Soar Qin (soarqin)\n"
|
||||
"sonninnos\n"
|
||||
"Sou Bunnbu (iyzsong)\n"
|
||||
"sparklewind\n"
|
||||
"spec-chum\n"
|
||||
"Stefan (gizmo98)\n"
|
||||
"stefan-gr\n"
|
||||
"stellarporter\n"
|
||||
"Stephanie Gawroriski (XerTheSquirrel)\n"
|
||||
"Steve Cox (stevenjamescox)\n"
|
||||
"Stevoisiak\n"
|
||||
"stoofin\n"
|
||||
"StormedBubbles\n"
|
||||
"strotlog\n"
|
||||
"Stuart Carnie (stuartcarnie)\n"
|
||||
"Sunguk Lee (d3m3vilurr)\n"
|
||||
"Supernature2k\n"
|
||||
"Sven (RetroSven)\n"
|
||||
"Swizzy\n"
|
||||
"Sylvain Colinet (Skarsnik)\n"
|
||||
"Szunti\n"
|
||||
"Tadek Kijkowski (monkeyman79)\n"
|
||||
"Tatsuya79\n"
|
||||
"Terry Lewis (terry-au)\n"
|
||||
"thedax\n"
|
||||
"theheroGAC\n"
|
||||
"TheMrIron2\n"
|
||||
"ThePhxRises\n"
|
||||
"TheRhysWyrill\n"
|
||||
"Thomas Lorblanchès (Zlika)\n"
|
||||
"Tilman M. Jaeschke (asciibeats)\n"
|
||||
"Tim Van den Langenbergh (tmtvl)\n"
|
||||
"Tobias Gruetzmacher (TobiX)\n"
|
||||
"Tobias Jakobi (tobiasjakobi)\n"
|
||||
"Todd Laney (ToddLa)\n"
|
||||
"Tomáš Kelemen (ToKe79)\n"
|
||||
"Tor Arne Vestbø (torarnv)\n"
|
||||
"Torsten Paul (t-paul)\n"
|
||||
"TotalCaesar659\n"
|
||||
"tr12345\n"
|
||||
"TroggleMonkey\n"
|
||||
"tunip3\n"
|
||||
"txorion\n"
|
||||
"Tyler Loch (TylerLoch)\n"
|
||||
"URBANsUNITED\n"
|
||||
"vaguerant\n"
|
||||
"Val Packett (valpackett)\n"
|
||||
"Valerio Proietti (kamicane)\n"
|
||||
"vgmoose\n"
|
||||
"Viachaslau Khalikin (viachaslavic)\n"
|
||||
"Vicki Pfau (endrift)\n"
|
||||
"Vicky C Lau (vickychenglau)\n"
|
||||
"vin (suseme)\n"
|
||||
"Vincent Pelletier (vpelletier)\n"
|
||||
"Vladimir Panteleev (CyberShadow)\n"
|
||||
"Vladimir Serbinenko (phcoder)\n"
|
||||
"Víctor \"IlDucci\" (IlDucci)\n"
|
||||
"waitingmoon\n"
|
||||
"Warning: Non-Potable Water (warningnonpotablewater)\n"
|
||||
"webgeek1234\n"
|
||||
"Weedy Weed Smoker (WeedyWeedSmoker)\n"
|
||||
"whorsemaster\n"
|
||||
"Wiimpathy\n"
|
||||
"Wiktor Strzębała (wiktorek140)\n"
|
||||
"Woohyun Shin (sinusinu)\n"
|
||||
"xenomorph-us\n"
|
||||
"xhp-creations\n"
|
||||
"Xitee (Xitee1)\n"
|
||||
"Yari (hyarsan)\n"
|
||||
"yesfish (huwpascoe)\n"
|
||||
"Yongwoon Cho (ssangkong)\n"
|
||||
"yoshisuga\n"
|
||||
"Zer0xFF\n"
|
||||
"zeromus\n"
|
||||
"zlice\n"
|
||||
"Érico Nogueira Rolim (ericonr)\n"
|
||||
"Ömercan Kömür (fpscan)\n"
|
||||
"シーはディー四を取る (cxd4)\n"
|
||||
"\n"
|
||||
;
|
||||
#endif
|
||||
+22
-1
@@ -326,6 +326,7 @@ OBJ += \
|
||||
gfx/gfx_display.o \
|
||||
gfx/gfx_animation.o \
|
||||
gfx/gfx_thumbnail.o \
|
||||
gfx/gfx_anim_preview.o \
|
||||
configuration.o \
|
||||
$(LIBRETRO_COMM_DIR)/dynamic/dylib.o \
|
||||
cores/dynamic_dummy.o \
|
||||
@@ -1460,8 +1461,21 @@ endif
|
||||
|
||||
# Companion UI
|
||||
|
||||
# Shared companion core (ui/companion/) is needed by every desktop
|
||||
# companion driver: Qt, native Win32, native Cocoa. Added once.
|
||||
ifneq ($(findstring Win32,$(OS)),)
|
||||
OBJ += ui/drivers/ui_win32.o
|
||||
OBJ += ui/drivers/ui_win32.o \
|
||||
ui/drivers/ui_win32_companion.o
|
||||
# comctl32: InitCommonControls / list-view / status bar used by the
|
||||
# native Win32 companion (MSVC builds pull it in via #pragma comment).
|
||||
LIBS += -lcomctl32 -lshell32 -lole32
|
||||
HAVE_COMPANION_CORE = 1
|
||||
endif
|
||||
ifeq ($(HAVE_QT), 1)
|
||||
HAVE_COMPANION_CORE = 1
|
||||
endif
|
||||
ifeq ($(HAVE_COMPANION_CORE), 1)
|
||||
OBJ += ui/companion/companion_core.o ui/companion/companion_thumbs.o
|
||||
endif
|
||||
|
||||
# Video
|
||||
@@ -2738,7 +2752,14 @@ ifeq ($(HAVE_COCOA), 1)
|
||||
DEFINES += -DHAVE_MAIN -DTARGET_OS_OSX=1 -DHAVE_COCOA
|
||||
OBJ += input/drivers/cocoa_input.o \
|
||||
ui/drivers/ui_cocoa.o \
|
||||
ui/drivers/ui_cocoa_companion.o \
|
||||
ui/drivers/cocoa/cocoa_common.o
|
||||
# HAVE_COCOA_COMMON is only known here, after the companion-core
|
||||
# block above ran; add the shared core if Qt/Win32 did not already.
|
||||
ifneq ($(HAVE_COMPANION_CORE), 1)
|
||||
OBJ += ui/companion/companion_core.o ui/companion/companion_thumbs.o
|
||||
HAVE_COMPANION_CORE = 1
|
||||
endif
|
||||
# RetroArchPlaylistManager uses Obj-C lightweight generics and
|
||||
# nullability macros (Xcode 7+, 2015). It's fine on iOS/tvOS and
|
||||
# modern macOS (10.11+), but GCC 4.0 / old clang on pre-10.11
|
||||
|
||||
@@ -1852,6 +1852,24 @@
|
||||
/* Only init the WIMP UI for this session if this is enabled */
|
||||
#define DEFAULT_DESKTOP_MENU_ENABLE true
|
||||
|
||||
/* Desktop companion presentation settings, shared by the Qt, Win32 and
|
||||
* Cocoa companions. */
|
||||
#define DEFAULT_DESKTOP_MENU_VIEW_TYPE 0 /* 0 list, 1 icons */
|
||||
#define DEFAULT_DESKTOP_MENU_THUMBNAIL_TYPE 0 /* 0 boxart, 1 screenshot, 2 title, 3 logo */
|
||||
#define DEFAULT_DESKTOP_MENU_SUGGEST_LOADED_CORE_FIRST false
|
||||
#define DEFAULT_DESKTOP_MENU_SAVE_LAST_TAB false
|
||||
#define DEFAULT_DESKTOP_MENU_LAST_TAB 0
|
||||
#define DEFAULT_DESKTOP_MENU_SAVE_GEOMETRY false
|
||||
#define DEFAULT_DESKTOP_MENU_SHOW_WELCOME_SCREEN true
|
||||
#define DEFAULT_DESKTOP_MENU_SCAN_FINISH_CONFIRM true
|
||||
#define DEFAULT_DESKTOP_MENU_THUMBNAIL_CACHE_LIMIT 500
|
||||
#define DEFAULT_DESKTOP_MENU_THUMBNAIL_MAX_SIZE 0 /* 0 = unlimited */
|
||||
#define DEFAULT_DESKTOP_MENU_THUMBNAIL_QUALITY 0 /* 0 = default */
|
||||
#define DEFAULT_DESKTOP_MENU_ICON_VIEW_ZOOM 50
|
||||
#define DEFAULT_DESKTOP_MENU_ALL_PLAYLISTS_LIST_MAX_COUNT 0
|
||||
#define DEFAULT_DESKTOP_MENU_ALL_PLAYLISTS_GRID_MAX_COUNT 0
|
||||
#define DEFAULT_DESKTOP_MENU_THEME 0 /* 0 system, 1 dark, 2 custom */
|
||||
|
||||
/* Keep track of how long each core+content has been running for over time */
|
||||
|
||||
#ifdef __WINRT__
|
||||
|
||||
+12
-1
@@ -82,6 +82,7 @@ void android_app_set_window_settings(bool notch_write_over,
|
||||
#if __APPLE__
|
||||
#include "ui/drivers/cocoa/apple_platform.h"
|
||||
#endif
|
||||
#include "ui/ui_companion_driver.h"
|
||||
|
||||
#ifdef HAVE_LAKKA
|
||||
#include <time.h>
|
||||
@@ -1601,6 +1602,9 @@ static struct config_array_setting *populate_settings_array(
|
||||
SETTING_ARRAY("microphone_resampler", settings->arrays.microphone_resampler, false, NULL, true);
|
||||
#endif
|
||||
SETTING_ARRAY("midi_driver", settings->arrays.midi_driver, false, NULL, true);
|
||||
SETTING_ARRAY("ui_companion_driver", settings->arrays.ui_companion_driver, false, NULL, true);
|
||||
SETTING_ARRAY("desktop_menu_hidden_playlists", settings->arrays.desktop_menu_hidden_playlists, false, NULL, true);
|
||||
SETTING_ARRAY("desktop_menu_highlight_color", settings->arrays.desktop_menu_highlight_color, false, NULL, true);
|
||||
SETTING_ARRAY("midi_input", settings->arrays.midi_input, true, DEFAULT_MIDI_INPUT, true);
|
||||
SETTING_ARRAY("midi_output", settings->arrays.midi_output, true, DEFAULT_MIDI_OUTPUT, true);
|
||||
SETTING_ARRAY("ai_service_backend", settings->arrays.ai_service_backend, false, NULL, true);
|
||||
@@ -1741,6 +1745,8 @@ static struct config_path_setting *populate_settings_path(
|
||||
SETTING_PATH("content_database_path", settings->paths.path_content_database, false, NULL, true);
|
||||
SETTING_PATH("content_favorites_path", settings->paths.path_content_favorites, false, NULL, true);
|
||||
SETTING_PATH("content_history_path", settings->paths.path_content_history, false, NULL, true);
|
||||
SETTING_PATH("desktop_menu_initial_playlist", settings->paths.desktop_menu_initial_playlist, false, NULL, true);
|
||||
SETTING_PATH("desktop_menu_custom_theme", settings->paths.desktop_menu_custom_theme, false, NULL, true);
|
||||
SETTING_PATH("content_image_history_path", settings->paths.path_content_image_history, false, NULL, true);
|
||||
SETTING_PATH("content_music_history_path", settings->paths.path_content_music_history, false, NULL, true);
|
||||
SETTING_PATH("content_video_history_path", settings->paths.path_content_video_history, false, NULL, true);
|
||||
@@ -1919,7 +1925,7 @@ static struct config_bool_setting *populate_settings_bool(
|
||||
#ifdef HAVE_NETWORKGAMEPAD
|
||||
SETTING_BOOL("network_remote_enable", &settings->bools.network_remote_enable, false, false, false);
|
||||
#endif
|
||||
#if defined(HAVE_QT) || defined(HAVE_COCOA)
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
SETTING_BOOL("ui_companion_toggle", &settings->bools.ui_companion_toggle, false, DEFAULT_UI_COMPANION_TOGGLE, false);
|
||||
#endif
|
||||
/* GENERATED: single-source setting rows (bool kind emits here) */
|
||||
@@ -5303,6 +5309,7 @@ void config_set_defaults(void *data)
|
||||
const char *def_menu = config_get_default_menu();
|
||||
#endif
|
||||
const char *def_camera = config_get_default_camera();
|
||||
const char *def_ui_companion = config_get_default_ui_companion();
|
||||
const char *def_bluetooth = config_get_default_bluetooth();
|
||||
const char *def_wifi = config_get_default_wifi();
|
||||
const char *def_led = config_get_default_led();
|
||||
@@ -5380,6 +5387,10 @@ void config_set_defaults(void *data)
|
||||
configuration_set_string(settings,
|
||||
settings->arrays.camera_driver,
|
||||
def_camera);
|
||||
if (def_ui_companion)
|
||||
configuration_set_string(settings,
|
||||
settings->arrays.ui_companion_driver,
|
||||
def_ui_companion);
|
||||
if (def_bluetooth)
|
||||
configuration_set_string(settings,
|
||||
settings->arrays.bluetooth_driver,
|
||||
|
||||
@@ -317,6 +317,21 @@ typedef struct settings
|
||||
unsigned menu_rgui_particle_effect;
|
||||
unsigned menu_ticker_type;
|
||||
unsigned menu_scroll_delay;
|
||||
unsigned desktop_menu_view_type;
|
||||
unsigned desktop_menu_thumbnail_type;
|
||||
unsigned desktop_menu_last_tab;
|
||||
unsigned desktop_menu_thumbnail_cache_limit;
|
||||
unsigned desktop_menu_thumbnail_max_size;
|
||||
unsigned desktop_menu_thumbnail_quality;
|
||||
unsigned desktop_menu_icon_view_zoom;
|
||||
unsigned desktop_menu_all_playlists_list_max_count;
|
||||
unsigned desktop_menu_all_playlists_grid_max_count;
|
||||
unsigned desktop_menu_theme;
|
||||
/* Window geometry, plain ints (was a Qt QByteArray blob). 0 = unset. */
|
||||
unsigned desktop_menu_window_x;
|
||||
unsigned desktop_menu_window_y;
|
||||
unsigned desktop_menu_window_width;
|
||||
unsigned desktop_menu_window_height;
|
||||
unsigned menu_content_show_add_entry;
|
||||
unsigned menu_content_show_contentless_cores;
|
||||
unsigned menu_content_show_netplay;
|
||||
@@ -887,6 +902,11 @@ typedef struct settings
|
||||
bool ui_companion_enable;
|
||||
bool ui_companion_toggle;
|
||||
bool desktop_menu_enable;
|
||||
bool desktop_menu_suggest_loaded_core_first;
|
||||
bool desktop_menu_save_last_tab;
|
||||
bool desktop_menu_save_geometry;
|
||||
bool desktop_menu_show_welcome_screen;
|
||||
bool desktop_menu_scan_finish_confirm;
|
||||
|
||||
/* Cheevos */
|
||||
bool cheevos_enable;
|
||||
@@ -1117,6 +1137,9 @@ typedef struct settings
|
||||
char input_driver[32];
|
||||
char input_joypad_driver[32];
|
||||
char midi_driver[32];
|
||||
char ui_companion_driver[32];
|
||||
char desktop_menu_hidden_playlists[PATH_MAX_LENGTH]; /* comma-separated .lpl names */
|
||||
char desktop_menu_highlight_color[32]; /* "#rrggbb" */
|
||||
char midi_input[32];
|
||||
char midi_output[32];
|
||||
char ai_service_backend[32];
|
||||
@@ -1247,6 +1270,8 @@ typedef struct settings
|
||||
char path_softfilter_plugin[PATH_MAX_LENGTH];
|
||||
char path_core_options[PATH_MAX_LENGTH];
|
||||
char path_content_favorites[PATH_MAX_LENGTH];
|
||||
char desktop_menu_initial_playlist[PATH_MAX_LENGTH];
|
||||
char desktop_menu_custom_theme[PATH_MAX_LENGTH];
|
||||
char path_content_history[PATH_MAX_LENGTH];
|
||||
char path_content_image_history[PATH_MAX_LENGTH];
|
||||
char path_content_music_history[PATH_MAX_LENGTH];
|
||||
|
||||
+12
-11
@@ -636,14 +636,6 @@ uint16_t win32_get_keyboard_mods(void)
|
||||
* Every API here dates from Windows 95; the constants an older SDK
|
||||
* lacks are defined below. */
|
||||
|
||||
/* Chosen not to collide with anything else that might use timers or
|
||||
* WM_APP messages on the main window. */
|
||||
#define WIN32_MODAL_TIMER_ID 0x5241
|
||||
#define WM_RA_MODAL_TICK (WM_APP + 0x52)
|
||||
|
||||
#ifndef WM_APP
|
||||
#define WM_APP 0x8000
|
||||
#endif
|
||||
#ifndef USER_TIMER_MINIMUM
|
||||
#define USER_TIMER_MINIMUM 0x0000000A
|
||||
#endif
|
||||
@@ -740,7 +732,7 @@ static sthread_t *win32_modal_thread_get(win32_modal_t *m)
|
||||
}
|
||||
#endif
|
||||
|
||||
static void win32_modal_enter(HWND hwnd)
|
||||
void win32_modal_enter(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
|
||||
@@ -797,7 +789,7 @@ static void win32_modal_stop(HWND hwnd)
|
||||
runloop_state_get_ptr()->pace_external = false;
|
||||
}
|
||||
|
||||
static void win32_modal_exit(HWND hwnd)
|
||||
void win32_modal_exit(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
|
||||
@@ -808,6 +800,15 @@ static void win32_modal_exit(HWND hwnd)
|
||||
|
||||
/* The window is going away. Any session is ended and the thread, if
|
||||
* one was ever started, is joined. */
|
||||
void win32_modal_window_destroyed(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
/* Only the window whose loop is being clocked ends the session;
|
||||
* another window closing during a drag is none of our business. */
|
||||
if (m->hwnd == hwnd)
|
||||
win32_modal_stop(hwnd);
|
||||
}
|
||||
|
||||
static void win32_modal_deinit(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
@@ -834,7 +835,7 @@ static void win32_modal_deinit(HWND hwnd)
|
||||
#endif
|
||||
}
|
||||
|
||||
static void win32_modal_tick(HWND hwnd)
|
||||
void win32_modal_tick(HWND hwnd)
|
||||
{
|
||||
win32_modal_t *m = &win32_modal;
|
||||
int ret;
|
||||
|
||||
@@ -81,6 +81,29 @@ enum win32_common_flags
|
||||
|
||||
extern uint8_t g_win32_flags;
|
||||
|
||||
#if !defined(_XBOX)
|
||||
/* Modal size/move and menu loops (DefWindowProc runs them on this
|
||||
* thread and returns only when they end) are clocked so the run loop
|
||||
* keeps iterating: WM_ENTERSIZEMOVE / WM_ENTERMENULOOP -> _enter,
|
||||
* WM_EXITSIZEMOVE / WM_EXITMENULOOP -> _exit, and the ticks arrive as
|
||||
* WM_RA_MODAL_TICK or as WM_TIMER with WIN32_MODAL_TIMER_ID -> _tick.
|
||||
* Any top-level window on the main thread - the companion window
|
||||
* included - must route these, or dragging it freezes RetroArch.
|
||||
* Chosen not to collide with anything else that might use timers or
|
||||
* WM_APP messages on those windows. */
|
||||
#ifndef WM_APP
|
||||
#define WM_APP 0x8000
|
||||
#endif
|
||||
#define WIN32_MODAL_TIMER_ID 0x5241
|
||||
#define WM_RA_MODAL_TICK (WM_APP + 0x52)
|
||||
|
||||
void win32_modal_enter(HWND hwnd);
|
||||
void win32_modal_exit(HWND hwnd);
|
||||
void win32_modal_tick(HWND hwnd);
|
||||
/* Ends the session if @hwnd is the window being clocked. */
|
||||
void win32_modal_window_destroyed(HWND hwnd);
|
||||
#endif
|
||||
|
||||
#if !defined(_XBOX)
|
||||
extern unsigned g_win32_resize_width;
|
||||
extern unsigned g_win32_resize_height;
|
||||
|
||||
@@ -0,0 +1,681 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Moved out of gfx/gfx_thumbnail.c so the desktop companions play a
|
||||
* preview exactly the way the menu's File Browser does. The bodies
|
||||
* below are gfx_thumbnail's, with its thumbnail-specific state replaced
|
||||
* by the session struct; the reasoning comments travel with them. */
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <compat/strl.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <file/file_path.h>
|
||||
#include <streams/file_stream.h>
|
||||
#include <formats/image.h>
|
||||
#include <formats/data_transfer.h>
|
||||
#include <memory/mem_stats.h>
|
||||
#ifdef HAVE_RPNG
|
||||
#include <formats/rpng.h>
|
||||
#endif
|
||||
|
||||
#include "gfx_anim_preview.h"
|
||||
#include "../configuration.h"
|
||||
|
||||
/* Preview audio: decode the animated thumbnail's audio track and loop
|
||||
* it through the audio mixer while the animation is shown. */
|
||||
#if (defined(HAVE_RWEBM) || defined(HAVE_RMP4)) && \
|
||||
defined(HAVE_AUDIOMIXER) && \
|
||||
(defined(HAVE_ROPUS) || defined(HAVE_RVORBIS) || defined(HAVE_RAAC))
|
||||
#define GFX_ANIM_PREVIEW_AUDIO 1
|
||||
#include <audio/audio_mixer.h>
|
||||
#include "../audio/audio_driver.h"
|
||||
/* The mixer streams the audio track and decodes on the flush, so no
|
||||
* PCM is buffered up front and the clip is not length-capped. */
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_NAME "__gfx_thumb_preview"
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_FEED_BUDGET (256 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP (2 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_HEAD_SLACK (1 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_LOOKAHEAD (2 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_MARGIN (1 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_AUDIO_HEAD_MAX (32 * 1024 * 1024)
|
||||
#endif
|
||||
|
||||
/* --- memory admission ---------------------------------------------------- */
|
||||
|
||||
/* Memory an active animation pins: the file buffer for its lifetime,
|
||||
* two XRGB canvases, and decoder working state - for MP4/H.264 a
|
||||
* decoded-picture buffer of reference and reorder pictures, bounded
|
||||
* generously at 24 I420 frames. px may be 0 before the stream has
|
||||
* been opened (file buffer only). */
|
||||
static uint64_t gfx_anim_preview_mem_need(uint64_t file_len, uint64_t px)
|
||||
{
|
||||
return file_len + px * 4 * 3 + (px * 3 / 2) * 24 + (1 << 20);
|
||||
}
|
||||
|
||||
/* Admission: scale with the heap when the platform reports free
|
||||
* memory, keep the static caps when it cannot (they return 0). */
|
||||
static bool gfx_anim_preview_whole_ok(uint64_t file_len, uint64_t px)
|
||||
{
|
||||
uint64_t free_mem = mem_stats_free();
|
||||
if (free_mem)
|
||||
return (file_len <= GFX_ANIM_PREVIEW_ABS_MAX_FILE)
|
||||
&& (gfx_anim_preview_mem_need(file_len, px) <= free_mem / 4);
|
||||
return (file_len <= GFX_ANIM_PREVIEW_MAX_FILE)
|
||||
&& (px == 0 || px <= GFX_ANIM_PREVIEW_MAX_PIXELS);
|
||||
}
|
||||
|
||||
/* Admission for the windowed path: a windowed open commits only the
|
||||
* head plus the sliding window, never the whole file, so charge that
|
||||
* and the decoder's own buffers. */
|
||||
static bool gfx_anim_preview_window_ok(uint64_t px)
|
||||
{
|
||||
uint64_t free_mem = mem_stats_free();
|
||||
uint64_t win = (uint64_t)GFX_ANIM_PREVIEW_WINDOW_KEEP
|
||||
+ GFX_ANIM_PREVIEW_WINDOW_AHEAD
|
||||
+ GFX_ANIM_PREVIEW_WINDOW_BACK;
|
||||
if (free_mem)
|
||||
return gfx_anim_preview_mem_need(win, px) <= free_mem / 4;
|
||||
return (px == 0 || px <= GFX_ANIM_PREVIEW_MAX_PIXELS);
|
||||
}
|
||||
|
||||
bool gfx_anim_preview_admit(uint64_t charge, uint64_t px)
|
||||
{
|
||||
return gfx_anim_preview_whole_ok(charge, px);
|
||||
}
|
||||
|
||||
bool gfx_anim_preview_window_admit(uint64_t px)
|
||||
{
|
||||
return gfx_anim_preview_window_ok(px);
|
||||
}
|
||||
|
||||
bool gfx_anim_preview_mem_ok(gfx_anim_preview_t *p, uint64_t px)
|
||||
{
|
||||
if (!p)
|
||||
return false;
|
||||
return p->windowed ? gfx_anim_preview_window_ok(px)
|
||||
: gfx_anim_preview_whole_ok((uint64_t)p->len, px);
|
||||
}
|
||||
|
||||
/* --- open ------------------------------------------------------------------ */
|
||||
|
||||
gfx_anim_preview_t *gfx_anim_preview_open(const char *path, int png_probe)
|
||||
{
|
||||
enum image_type_enum type;
|
||||
gfx_anim_preview_t *p;
|
||||
data_transfer_t *dt;
|
||||
const uint8_t *base;
|
||||
size_t blen = 0;
|
||||
bool reserved;
|
||||
void *stream = NULL;
|
||||
|
||||
if (string_is_empty(path))
|
||||
return NULL;
|
||||
|
||||
/* Cheap gate: only container types with an animation decoder.
|
||||
* PNG is included for APNG. */
|
||||
type = image_texture_get_type(path);
|
||||
if ( (type != IMAGE_TYPE_PNG)
|
||||
&& (type != IMAGE_TYPE_WEBP)
|
||||
&& (type != IMAGE_TYPE_WEBM)
|
||||
&& (type != IMAGE_TYPE_MP4))
|
||||
return NULL;
|
||||
|
||||
#ifdef HAVE_RPNG
|
||||
/* PNG is the dominant thumbnail format and almost all of them are
|
||||
* still images, so deciding "animated?" only after reading the whole
|
||||
* file would add a full extra read to the common path. APNG puts
|
||||
* its acTL control chunk before the first IDAT, i.e. within the
|
||||
* first few hundred bytes, so probe a small header window first and
|
||||
* bail out early for ordinary PNGs. A caller with a verdict from a
|
||||
* still decode that held the whole file passes it and skips the
|
||||
* probe. */
|
||||
if (type == IMAGE_TYPE_PNG && png_probe == 0)
|
||||
return NULL;
|
||||
if (type == IMAGE_TYPE_PNG && png_probe < 0)
|
||||
{
|
||||
uint8_t *probe;
|
||||
int64_t got = 0;
|
||||
int more = 0;
|
||||
RFILE *fp = filestream_open(path,
|
||||
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
||||
if (!fp)
|
||||
return NULL;
|
||||
if (!(probe = (uint8_t*)malloc(4096)))
|
||||
{
|
||||
filestream_close(fp);
|
||||
return NULL;
|
||||
}
|
||||
got = filestream_read(fp, probe, 4096);
|
||||
filestream_close(fp);
|
||||
if (got <= 0)
|
||||
{
|
||||
free(probe);
|
||||
return NULL;
|
||||
}
|
||||
if (!rpng_is_apng_ex(probe, (size_t)got, &more))
|
||||
{
|
||||
(void)more;
|
||||
free(probe);
|
||||
return NULL;
|
||||
}
|
||||
free(probe);
|
||||
}
|
||||
#else
|
||||
(void)png_probe;
|
||||
#endif
|
||||
|
||||
/* Open the file as a sliding window: address space for the whole
|
||||
* file is reserved, but only [tell - margin, tell + lookahead) is
|
||||
* ever committed, so a long video costs its window rather than its
|
||||
* length. On a platform with no address-space reservation
|
||||
* data_transfer_open_window fills the whole file before returning,
|
||||
* so the absolute bound is applied BEFORE the open there. */
|
||||
if (!data_transfer_reserve_supported())
|
||||
{
|
||||
int64_t fsz = path_get_size(path);
|
||||
if (fsz <= 0 || fsz > GFX_ANIM_PREVIEW_ABS_MAX_FILE)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!(dt = data_transfer_open_window(path, GFX_ANIM_PREVIEW_WINDOW_KEEP)))
|
||||
return NULL;
|
||||
reserved = data_transfer_window_is_reserved(dt);
|
||||
if (!(base = data_transfer_window_base(dt, &blen)) || blen == 0)
|
||||
{
|
||||
data_transfer_free(dt);
|
||||
return NULL;
|
||||
}
|
||||
if ( (!reserved && blen > GFX_ANIM_PREVIEW_ABS_MAX_FILE)
|
||||
|| !(reserved ? gfx_anim_preview_window_ok(0)
|
||||
: gfx_anim_preview_whole_ok((uint64_t)blen, 0)))
|
||||
{
|
||||
data_transfer_free(dt);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The demuxer must see the whole logical length; only the head is
|
||||
* resident at this point, so open progressively and grow the window
|
||||
* until the header/index is covered. */
|
||||
{
|
||||
int need_more = 0;
|
||||
size_t need_lo = 0, need_hi = 0;
|
||||
size_t avail = GFX_ANIM_PREVIEW_WINDOW_KEEP;
|
||||
int jumps = 0;
|
||||
if (avail > blen)
|
||||
avail = blen;
|
||||
for (;;)
|
||||
{
|
||||
stream = image_transfer_anim_stream_new_avail(
|
||||
(void*)base, blen, avail, type, &need_more,
|
||||
&need_lo, &need_hi);
|
||||
if (stream || !need_more || avail >= blen)
|
||||
break;
|
||||
if (need_hi > need_lo && need_hi > avail && need_hi <= blen)
|
||||
{
|
||||
/* The demuxer named the exact bytes that unblock it (a box
|
||||
* header past the wall, or a trailing moov body): commit
|
||||
* just that island. */
|
||||
if (++jumps > 64 ||
|
||||
!data_transfer_window_ensure(dt, need_lo, need_hi))
|
||||
break;
|
||||
avail = need_hi;
|
||||
continue;
|
||||
}
|
||||
avail += GFX_ANIM_PREVIEW_WINDOW_KEEP;
|
||||
if (avail > blen)
|
||||
avail = blen;
|
||||
if (!data_transfer_window_extend(dt, avail))
|
||||
break;
|
||||
}
|
||||
if (stream)
|
||||
{
|
||||
/* Restart the read frontier at the media floor and prime one
|
||||
* window so the first frames decode from resident bytes. */
|
||||
size_t fl = image_transfer_anim_stream_media_floor(stream, type);
|
||||
size_t hi = fl + GFX_ANIM_PREVIEW_WINDOW_KEEP
|
||||
+ GFX_ANIM_PREVIEW_WINDOW_AHEAD;
|
||||
if (hi > blen)
|
||||
hi = blen;
|
||||
data_transfer_window_rebase(dt, fl);
|
||||
if (!data_transfer_window_extend(dt, hi))
|
||||
{
|
||||
image_transfer_anim_stream_free(stream, type);
|
||||
stream = NULL;
|
||||
}
|
||||
else
|
||||
image_transfer_anim_stream_set_avail(stream, type, hi);
|
||||
}
|
||||
/* Types without a progressive open (animated WEBP) return NULL
|
||||
* with need_more clear: fall back to the whole buffer. */
|
||||
if (!stream && !need_more)
|
||||
{
|
||||
if (data_transfer_window_extend(dt, blen))
|
||||
stream = image_transfer_anim_stream_new((void*)base, blen, type);
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream)
|
||||
{
|
||||
data_transfer_free(dt);
|
||||
return NULL; /* still image or malformed */
|
||||
}
|
||||
|
||||
if (!(p = (gfx_anim_preview_t*)calloc(1, sizeof(*p))))
|
||||
{
|
||||
image_transfer_anim_stream_free(stream, type);
|
||||
data_transfer_free(dt);
|
||||
return NULL;
|
||||
}
|
||||
p->stream = stream;
|
||||
p->dt = dt;
|
||||
p->base = base;
|
||||
p->len = blen;
|
||||
p->type = type;
|
||||
p->windowed = reserved;
|
||||
p->audio_slot = -1;
|
||||
p->path = strldup(path, strlen(path) + 1);
|
||||
image_transfer_anim_stream_get_info(stream, type, &p->width, &p->height,
|
||||
&p->num_frames, &p->loop_count);
|
||||
/* Ask for ARGB once, before any frame: the answer holds for the
|
||||
* animation's life (a repeat ask after the first frame is refused by
|
||||
* APNG even though the order stands - deciding per frame here once
|
||||
* swapped R and B from the second frame on). */
|
||||
p->native_argb = image_transfer_anim_stream_set_argb(stream, type, 1);
|
||||
if (p->num_frames < 2 && type != IMAGE_TYPE_WEBM && type != IMAGE_TYPE_MP4)
|
||||
{
|
||||
/* one frame is a still */
|
||||
gfx_anim_preview_close(p);
|
||||
return NULL;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* --- feeding --------------------------------------------------------------- */
|
||||
|
||||
bool gfx_anim_preview_feed(gfx_anim_preview_t *p)
|
||||
{
|
||||
size_t tell, floor_off, anchor;
|
||||
if (!p || !p->stream)
|
||||
return false;
|
||||
if (!p->windowed || !p->dt)
|
||||
return true;
|
||||
|
||||
/* Keep the committed range straddling the decoder's byte frontier.
|
||||
* The demuxers report a monotonic consumed offset and a fixed media
|
||||
* floor, so the feeder can decommit behind and commit ahead without
|
||||
* touching the range the decoder is reading; the margin exceeds the
|
||||
* largest single frame's packet by orders of magnitude. Anchor at
|
||||
* the media floor until the decoder has consumed anything. */
|
||||
tell = image_transfer_anim_stream_consumed(p->stream, p->type);
|
||||
floor_off = image_transfer_anim_stream_media_floor(p->stream, p->type);
|
||||
anchor = (tell > 0) ? tell : floor_off;
|
||||
if (anchor > 0)
|
||||
{
|
||||
size_t margin = GFX_ANIM_PREVIEW_WINDOW_BACK;
|
||||
size_t hi = anchor + GFX_ANIM_PREVIEW_WINDOW_AHEAD;
|
||||
size_t res_hi = 0;
|
||||
if (anchor > floor_off && anchor - floor_off < margin)
|
||||
margin = anchor - floor_off;
|
||||
if (!data_transfer_window_feed_budget(p->dt, anchor,
|
||||
GFX_ANIM_PREVIEW_WINDOW_AHEAD, margin,
|
||||
GFX_ANIM_PREVIEW_FEED_BUDGET, &res_hi))
|
||||
return false;
|
||||
/* The demuxer's bound follows what the feed made resident, both
|
||||
* ways (a loop's rewind drops the frontier back to the head). */
|
||||
if (hi > res_hi)
|
||||
hi = res_hi;
|
||||
if (hi > p->len)
|
||||
hi = p->len;
|
||||
image_transfer_anim_stream_set_avail(p->stream, p->type, hi);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t *gfx_anim_preview_next(gfx_anim_preview_t *p,
|
||||
int *duration_ms, bool *native_argb)
|
||||
{
|
||||
const uint32_t *frame;
|
||||
int d = 0;
|
||||
if (!p || !p->stream)
|
||||
return NULL;
|
||||
frame = image_transfer_anim_stream_next(p->stream, p->type, &d);
|
||||
if (native_argb)
|
||||
*native_argb = p->native_argb;
|
||||
/* <= 0 is undefined by the container spec (browsers substitute
|
||||
* 100 ms); very small durations are floored so a hostile file cannot
|
||||
* request thousands of decodes per second. */
|
||||
if (d <= 0)
|
||||
d = GFX_ANIM_PREVIEW_DUR_DEFAULT;
|
||||
if (d < GFX_ANIM_PREVIEW_DUR_MIN)
|
||||
d = GFX_ANIM_PREVIEW_DUR_MIN;
|
||||
if (duration_ms)
|
||||
*duration_ms = d;
|
||||
return frame;
|
||||
}
|
||||
|
||||
void gfx_anim_preview_rewind(gfx_anim_preview_t *p)
|
||||
{
|
||||
if (p && p->stream)
|
||||
image_transfer_anim_stream_rewind(p->stream, p->type);
|
||||
}
|
||||
|
||||
/* --- audio ------------------------------------------------------------------ */
|
||||
|
||||
#if defined(GFX_ANIM_PREVIEW_AUDIO)
|
||||
typedef struct
|
||||
{
|
||||
struct data_transfer *dt;
|
||||
} gfx_anim_preview_audio_win_t;
|
||||
|
||||
static void gfx_anim_preview_audio_win_release(void *owner)
|
||||
{
|
||||
gfx_anim_preview_audio_win_t *w = (gfx_anim_preview_audio_win_t*)owner;
|
||||
if (!w)
|
||||
return;
|
||||
if (w->dt)
|
||||
data_transfer_free(w->dt);
|
||||
free(w);
|
||||
}
|
||||
|
||||
/* End offset of the MP4 moov box within the head, or 0. */
|
||||
static size_t gfx_anim_preview_audio_meta_end(struct data_transfer *dt,
|
||||
size_t blen, size_t *lo_out)
|
||||
{
|
||||
size_t pos = 0;
|
||||
int n = 0;
|
||||
while (pos + 8 <= blen && n++ < 64)
|
||||
{
|
||||
uint8_t h[16];
|
||||
uint64_t sz;
|
||||
size_t hs = 8;
|
||||
if (!data_transfer_window_peek(dt, pos, h, 8))
|
||||
return 0;
|
||||
sz = ((uint64_t)h[0] << 24) | ((uint64_t)h[1] << 16)
|
||||
| ((uint64_t)h[2] << 8) | (uint64_t)h[3];
|
||||
if (sz == 1)
|
||||
{
|
||||
int i;
|
||||
if (!data_transfer_window_peek(dt, pos + 8, h + 8, 8))
|
||||
return 0;
|
||||
sz = 0;
|
||||
for (i = 0; i < 8; i++)
|
||||
sz = (sz << 8) | (uint64_t)h[8 + i];
|
||||
hs = 16;
|
||||
}
|
||||
else if (sz == 0)
|
||||
sz = (uint64_t)blen - (uint64_t)pos;
|
||||
if (sz < hs || sz > (uint64_t)blen - (uint64_t)pos)
|
||||
return 0;
|
||||
if (h[4] == 'm' && h[5] == 'o' && h[6] == 'o' && h[7] == 'v')
|
||||
{
|
||||
if (lo_out)
|
||||
*lo_out = pos;
|
||||
return (size_t)((uint64_t)pos + sz);
|
||||
}
|
||||
pos += (size_t)sz;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void gfx_anim_preview_audio_stop_slot(gfx_anim_preview_t *p)
|
||||
{
|
||||
if (p->audio_slot >= 0)
|
||||
{
|
||||
/* Only touch the slot if it still holds our stream. */
|
||||
const char *name = audio_driver_mixer_get_stream_name((unsigned)p->audio_slot);
|
||||
if (name && string_is_equal(name, GFX_ANIM_PREVIEW_AUDIO_NAME))
|
||||
audio_driver_mixer_remove_stream((unsigned)p->audio_slot);
|
||||
}
|
||||
/* the mixer owns (and frees) the audio window through buf_owner */
|
||||
p->audio_dt = NULL;
|
||||
p->audio_hi = 0;
|
||||
p->audio_slot = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
void gfx_anim_preview_audio_begin(gfx_anim_preview_t *p)
|
||||
{
|
||||
#if defined(GFX_ANIM_PREVIEW_AUDIO)
|
||||
gfx_anim_preview_audio_win_t *w;
|
||||
const uint8_t *base = NULL;
|
||||
size_t blen = 0, floor_off, keep, island_hi = 0;
|
||||
|
||||
if (!p || !p->stream || !p->path || !*p->path)
|
||||
return;
|
||||
if ( (p->type != IMAGE_TYPE_WEBM && p->type != IMAGE_TYPE_MP4)
|
||||
|| !config_get_ptr()->bools.menu_thumbnail_preview_audio)
|
||||
return;
|
||||
if (p->audio_dt)
|
||||
return; /* already streaming */
|
||||
|
||||
/* A second window over the same file, independent of the video one:
|
||||
* the two decoders run on different threads and each needs its own
|
||||
* committed range. The head must permanently cover the container's
|
||||
* metadata (the AAC arm borrows codec_private out of the moov for
|
||||
* the decoder's whole life); the video open already found where the
|
||||
* media starts. */
|
||||
floor_off = image_transfer_anim_stream_media_floor(p->stream, p->type);
|
||||
keep = floor_off + GFX_ANIM_PREVIEW_AUDIO_HEAD_SLACK;
|
||||
if (keep < GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP)
|
||||
keep = GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP;
|
||||
if (keep > GFX_ANIM_PREVIEW_AUDIO_HEAD_MAX)
|
||||
keep = GFX_ANIM_PREVIEW_AUDIO_HEAD_MAX;
|
||||
|
||||
if (!(w = (gfx_anim_preview_audio_win_t*)calloc(1, sizeof(*w))))
|
||||
return;
|
||||
if (!(w->dt = data_transfer_open_window(p->path, keep)))
|
||||
{
|
||||
free(w);
|
||||
return;
|
||||
}
|
||||
if (p->type == IMAGE_TYPE_MP4)
|
||||
{
|
||||
size_t probe_len = 0;
|
||||
if (data_transfer_window_base(w->dt, &probe_len) && probe_len)
|
||||
{
|
||||
size_t meta_lo = 0;
|
||||
size_t meta_end = gfx_anim_preview_audio_meta_end(w->dt, probe_len, &meta_lo);
|
||||
if (!meta_end)
|
||||
{ /* no moov found: leave the floor-derived head */ }
|
||||
else if (meta_end <= GFX_ANIM_PREVIEW_AUDIO_HEAD_MAX)
|
||||
{
|
||||
size_t need = meta_end;
|
||||
if (floor_off + GFX_ANIM_PREVIEW_AUDIO_HEAD_SLACK > need)
|
||||
need = floor_off + GFX_ANIM_PREVIEW_AUDIO_HEAD_SLACK;
|
||||
if (need < GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP)
|
||||
need = GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP;
|
||||
if (need > probe_len)
|
||||
need = probe_len;
|
||||
if (need > GFX_ANIM_PREVIEW_AUDIO_HEAD_MAX)
|
||||
{
|
||||
need = meta_end;
|
||||
island_hi = meta_end;
|
||||
if (need < GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP)
|
||||
need = GFX_ANIM_PREVIEW_AUDIO_WINDOW_KEEP;
|
||||
}
|
||||
if (need > keep)
|
||||
{
|
||||
if (!data_transfer_window_grow_keep(w->dt, need))
|
||||
{
|
||||
gfx_anim_preview_audio_win_release(w);
|
||||
return;
|
||||
}
|
||||
keep = need;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!data_transfer_window_ensure(w->dt, meta_lo, meta_end))
|
||||
{
|
||||
gfx_anim_preview_audio_win_release(w);
|
||||
return;
|
||||
}
|
||||
island_hi = meta_end;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!(base = data_transfer_window_base(w->dt, &blen)) || !blen)
|
||||
{
|
||||
gfx_anim_preview_audio_win_release(w);
|
||||
return;
|
||||
}
|
||||
p->audio_hi = island_hi ? blen : ((keep < blen) ? keep : blen);
|
||||
|
||||
{
|
||||
audio_mixer_stream_params_t params;
|
||||
int out_slot = -1;
|
||||
params.buf = (void*)base;
|
||||
params.bufsize = blen;
|
||||
params.basename = strldup(GFX_ANIM_PREVIEW_AUDIO_NAME,
|
||||
strlen(GFX_ANIM_PREVIEW_AUDIO_NAME) + 1);
|
||||
params.cb = NULL;
|
||||
params.buf_owner = w;
|
||||
params.buf_owner_free = gfx_anim_preview_audio_win_release;
|
||||
params.out_slot = &out_slot;
|
||||
params.slot_selection_idx = 0;
|
||||
params.volume = 1.0f;
|
||||
params.slot_selection_type = AUDIO_MIXER_SLOT_SELECTION_AUTOMATIC;
|
||||
params.stream_type = AUDIO_STREAM_TYPE_SYSTEM;
|
||||
params.type = (p->type == IMAGE_TYPE_WEBM)
|
||||
? AUDIO_MIXER_TYPE_WEBA
|
||||
: AUDIO_MIXER_TYPE_M4A;
|
||||
params.state = AUDIO_STREAM_STATE_PLAYING_LOOPED;
|
||||
params.end_granule = 0;
|
||||
params.avail = p->audio_hi;
|
||||
if (!audio_driver_mixer_add_stream(¶ms))
|
||||
{
|
||||
free(params.basename);
|
||||
p->audio_hi = 0;
|
||||
/* the mixer did not take the window: release it ourselves */
|
||||
gfx_anim_preview_audio_win_release(w);
|
||||
return;
|
||||
}
|
||||
free(params.basename);
|
||||
p->audio_dt = w->dt;
|
||||
p->audio_slot = out_slot;
|
||||
if (island_hi)
|
||||
{
|
||||
p->audio_hi = (keep < blen) ? keep : blen;
|
||||
audio_driver_mixer_stream_set_avail((unsigned)out_slot, p->audio_hi);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)p;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool gfx_anim_preview_audio_feed(gfx_anim_preview_t *p)
|
||||
{
|
||||
#if defined(GFX_ANIM_PREVIEW_AUDIO)
|
||||
int64_t tell;
|
||||
if (!p || !p->audio_dt || p->audio_slot < 0)
|
||||
return true;
|
||||
/* The decoder runs on the audio thread out of this mapping: feed
|
||||
* first, then raise the bound to what the feed actually committed. */
|
||||
tell = audio_driver_mixer_stream_byte_tell((unsigned)p->audio_slot);
|
||||
if (tell >= 0)
|
||||
{
|
||||
size_t anchor = (size_t)tell;
|
||||
size_t hi = anchor + GFX_ANIM_PREVIEW_AUDIO_LOOKAHEAD;
|
||||
size_t res_hi = 0;
|
||||
if (hi > p->len)
|
||||
hi = p->len;
|
||||
if (!data_transfer_window_feed_budget(p->audio_dt, anchor,
|
||||
GFX_ANIM_PREVIEW_AUDIO_LOOKAHEAD, GFX_ANIM_PREVIEW_AUDIO_MARGIN,
|
||||
GFX_ANIM_PREVIEW_AUDIO_FEED_BUDGET, &res_hi))
|
||||
{
|
||||
/* the window cannot be maintained: stop rather than play
|
||||
* against a frozen one */
|
||||
gfx_anim_preview_audio_stop_slot(p);
|
||||
return false;
|
||||
}
|
||||
if (hi > res_hi)
|
||||
hi = res_hi;
|
||||
if (hi > p->audio_hi)
|
||||
{
|
||||
p->audio_hi = hi;
|
||||
audio_driver_mixer_stream_set_avail((unsigned)p->audio_slot, hi);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
(void)p;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void gfx_anim_preview_audio_stop(gfx_anim_preview_t *p)
|
||||
{
|
||||
#if defined(GFX_ANIM_PREVIEW_AUDIO)
|
||||
if (p)
|
||||
gfx_anim_preview_audio_stop_slot(p);
|
||||
#else
|
||||
(void)p;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --- wrap / release (non-owning) -------------------------------------------- */
|
||||
|
||||
gfx_anim_preview_t *gfx_anim_preview_wrap(void *stream,
|
||||
enum image_type_enum type, struct data_transfer *dt,
|
||||
const uint8_t *base, size_t len, bool windowed, const char *path)
|
||||
{
|
||||
gfx_anim_preview_t *p;
|
||||
if (!stream)
|
||||
return NULL;
|
||||
if (!(p = (gfx_anim_preview_t*)calloc(1, sizeof(*p))))
|
||||
return NULL;
|
||||
p->stream = stream;
|
||||
p->dt = dt;
|
||||
p->base = base;
|
||||
p->len = len;
|
||||
p->type = type;
|
||||
p->windowed = windowed;
|
||||
p->audio_slot = -1;
|
||||
p->path = path ? strldup(path, strlen(path) + 1) : NULL;
|
||||
image_transfer_anim_stream_get_info(stream, type, &p->width, &p->height,
|
||||
&p->num_frames, &p->loop_count);
|
||||
/* gfx_thumbnail's worker asks the stream itself per job; the answer
|
||||
* recorded here is for callers that draw through the session. */
|
||||
p->native_argb = image_transfer_anim_stream_set_argb(stream, type, 1);
|
||||
return p;
|
||||
}
|
||||
|
||||
void gfx_anim_preview_release(gfx_anim_preview_t *p)
|
||||
{
|
||||
if (!p)
|
||||
return;
|
||||
gfx_anim_preview_audio_stop(p);
|
||||
free(p->path);
|
||||
free(p);
|
||||
}
|
||||
|
||||
/* --- close ------------------------------------------------------------------ */
|
||||
|
||||
void gfx_anim_preview_close(gfx_anim_preview_t *p)
|
||||
{
|
||||
if (!p)
|
||||
return;
|
||||
gfx_anim_preview_audio_stop(p);
|
||||
if (p->stream)
|
||||
image_transfer_anim_stream_free(p->stream, p->type);
|
||||
if (p->dt)
|
||||
data_transfer_free(p->dt);
|
||||
free(p->path);
|
||||
free(p);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef __GFX_ANIM_PREVIEW_H
|
||||
#define __GFX_ANIM_PREVIEW_H
|
||||
|
||||
/* The animated-thumbnail preview machinery of RetroArch's File Browser
|
||||
* (gfx_thumbnail), factored out so every consumer plays a file the same
|
||||
* way: the menu's thumbnail, and the desktop companions' preview panes.
|
||||
*
|
||||
* - open: the sliding-window progressive open over the file
|
||||
* (data_transfer_open_window + image_transfer_anim_stream_new_
|
||||
* avail): playback starts from the first resident bytes, a
|
||||
* tail-moov MP4 opens from a few MiB, memory admission scales
|
||||
* with the heap and never slurps a multi-GB file
|
||||
* - feed: per tick, keep the committed window straddling the decoder's
|
||||
* byte frontier under a per-tick I/O budget, and hand the
|
||||
* demuxer what is actually resident
|
||||
* - audio: the container's audio track (WEBM / MP4) through the audio
|
||||
* mixer, looped, over its own window, with its own feeder
|
||||
* - close: everything down, in the right order
|
||||
*
|
||||
* Frames themselves come from image_transfer_anim_stream_next() on the
|
||||
* session's stream; the caller owns the clock (each frame's duration).
|
||||
*
|
||||
* Threading: open / feed / next / rewind may run on any one thread at a
|
||||
* time (gfx_thumbnail runs them on its worker; a companion on its
|
||||
* animation thread). audio_* run on the main thread (the mixer). */
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <boolean.h>
|
||||
#include <formats/image.h>
|
||||
#include <retro_common_api.h>
|
||||
|
||||
RETRO_BEGIN_DECLS
|
||||
|
||||
struct data_transfer;
|
||||
|
||||
/* Window sizing shared by every consumer (see gfx_thumbnail.c history
|
||||
* for how these were arrived at). */
|
||||
#define GFX_ANIM_PREVIEW_WINDOW_KEEP (4 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_WINDOW_AHEAD (8 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_WINDOW_BACK (8 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_FEED_BUDGET (512 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_ABS_MAX_FILE (1024 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_MAX_FILE (256 * 1024 * 1024)
|
||||
#define GFX_ANIM_PREVIEW_MAX_PIXELS (3840 * 2160)
|
||||
#define GFX_ANIM_PREVIEW_DUR_DEFAULT 100
|
||||
#define GFX_ANIM_PREVIEW_DUR_MIN 16
|
||||
|
||||
typedef struct gfx_anim_preview
|
||||
{
|
||||
void *stream; /* image_transfer anim stream */
|
||||
struct data_transfer *dt; /* owns the mapping the stream borrows */
|
||||
const uint8_t *base; /* mapping base (dt) */
|
||||
size_t len; /* full logical file length */
|
||||
enum image_type_enum type;
|
||||
unsigned width, height;
|
||||
int num_frames, loop_count;
|
||||
bool windowed; /* dt is a sliding window (reserved) */
|
||||
/* Whether the stream emits ARGB words (asked once at open; a stream
|
||||
* that honours it keeps that order for the whole animation, and
|
||||
* some - APNG - report "no" to a repeat request after the first
|
||||
* frame even though the order stands). */
|
||||
bool native_argb;
|
||||
char *path;
|
||||
|
||||
/* preview audio (WEBM / MP4), main thread */
|
||||
struct data_transfer *audio_dt;
|
||||
size_t audio_hi;
|
||||
int audio_slot;
|
||||
} gfx_anim_preview_t;
|
||||
|
||||
/* Open @path for animated playback. @png_probe: what a still-load task
|
||||
* already learned about a PNG (< 0 unknown - probe here; 0 still; 1
|
||||
* APNG); pass -1 when there is no verdict. Returns NULL for a still, a
|
||||
* type without an animation decoder, a malformed file, or one that
|
||||
* fails admission - the caller keeps whatever still it has. */
|
||||
gfx_anim_preview_t *gfx_anim_preview_open(const char *path, int png_probe);
|
||||
|
||||
/* Per tick, before decoding: keep the window straddling the decoder's
|
||||
* frontier and raise the demuxer's bound to what is resident. false on
|
||||
* an I/O failure (the caller should close: the decoder would loop
|
||||
* early on an end-of-data wall). No-op for a non-windowed session. */
|
||||
bool gfx_anim_preview_feed(gfx_anim_preview_t *p);
|
||||
|
||||
/* Next displayed frame: the stream's canvas (valid until the next call).
|
||||
* *native_argb tells whether it is ARGB words (decided once at open) or
|
||||
* R,G,B,A memory order the caller must swizzle. NULL at the end of a
|
||||
* pass - the caller handles the loop count and rewinds. @duration_ms is
|
||||
* clamped per the container rules (default 100, min 16). */
|
||||
const uint32_t *gfx_anim_preview_next(gfx_anim_preview_t *p,
|
||||
int *duration_ms, bool *native_argb);
|
||||
void gfx_anim_preview_rewind(gfx_anim_preview_t *p);
|
||||
|
||||
/* Memory admission, exposed for a caller that knows its canvas size
|
||||
* after open (gfx_thumbnail charges the upload buffers too). */
|
||||
bool gfx_anim_preview_mem_ok(gfx_anim_preview_t *p, uint64_t px);
|
||||
/* The same admission on raw figures: @charge is what playback pins
|
||||
* (the file length for a resident buffer, the window for a windowed
|
||||
* one), @px the canvas. @windowed selects the test. */
|
||||
bool gfx_anim_preview_admit(uint64_t charge, uint64_t px);
|
||||
bool gfx_anim_preview_window_admit(uint64_t px);
|
||||
|
||||
/* Wrap an already-open stream / mapping (gfx_thumbnail installs those
|
||||
* itself, including ones adopted from a still decode) in a session that
|
||||
* only feeds and plays audio; it does NOT own them. Release with
|
||||
* gfx_anim_preview_release(), which frees the session and stops its
|
||||
* audio but leaves stream and mapping to the owner. */
|
||||
gfx_anim_preview_t *gfx_anim_preview_wrap(void *stream,
|
||||
enum image_type_enum type, struct data_transfer *dt,
|
||||
const uint8_t *base, size_t len, bool windowed, const char *path);
|
||||
void gfx_anim_preview_release(gfx_anim_preview_t *p);
|
||||
|
||||
/* Preview audio: start the container's audio track looping through the
|
||||
* mixer (WEBM / MP4 only; honours the menu_thumbnail_preview_audio
|
||||
* setting), feed its window per tick, stop it. Main thread. */
|
||||
void gfx_anim_preview_audio_begin(gfx_anim_preview_t *p);
|
||||
bool gfx_anim_preview_audio_feed(gfx_anim_preview_t *p);
|
||||
void gfx_anim_preview_audio_stop(gfx_anim_preview_t *p);
|
||||
|
||||
/* Close: audio, stream, mapping. */
|
||||
void gfx_anim_preview_close(gfx_anim_preview_t *p);
|
||||
|
||||
RETRO_END_DECLS
|
||||
|
||||
#endif
|
||||
+57
-869
File diff suppressed because it is too large.
Load diff
+4
-24
@@ -226,29 +226,12 @@ typedef struct
|
||||
* data_transfer_free(anim_dt); anim_buf itself must not be
|
||||
* freed). */
|
||||
void *anim;
|
||||
/* Shared preview session (gfx_anim_preview_t*) over anim / anim_dt:
|
||||
* the window feeder and the preview audio. Non-owning. */
|
||||
void *anim_sess;
|
||||
void *anim_buf;
|
||||
struct data_transfer *anim_dt; /* transfer owning anim_buf (and the
|
||||
adopted nbio handle beneath it) */
|
||||
/* Preview audio on a WINDOWED handle. anim_buf is a sliding
|
||||
* mapping there, only partly resident, so the mixer cannot be
|
||||
* handed a copy of it - it needs the whole container. This is a
|
||||
* second, independent read of the same file, pumped a frame
|
||||
* budget at a time by gfx_thumbnail_animate and handed over when
|
||||
* complete. NULL on every other path, where anim_buf is already
|
||||
* the whole file and the hand-off is immediate. */
|
||||
struct data_transfer *anim_audio_dt;
|
||||
/* Windowed preview audio: the mixer borrows this window's mapping
|
||||
* for the container and is told, through params.avail and
|
||||
* audio_driver_mixer_stream_set_avail, how much of it is resident.
|
||||
* anim_audio_hi is that figure - never above the committed
|
||||
* frontier, which is what keeps a stale feeder a stall rather than
|
||||
* a read of reserved pages. anim_audio_slot is the mixer slot the
|
||||
* feeder follows with audio_driver_mixer_stream_byte_tell. */
|
||||
size_t anim_audio_hi;
|
||||
int anim_audio_slot;
|
||||
char *anim_audio_path; /* strdup'd source for the read above; only
|
||||
set on windowed handles, freed by
|
||||
gfx_thumbnail_anim_close */
|
||||
/* Decode-worker ping-pong job pair (HAVE_THREADS builds): while
|
||||
* the frame held in one job waits for its due time, the other is
|
||||
* already decoding its successor. anim_job_upload selects which
|
||||
@@ -306,10 +289,7 @@ static INLINE void gfx_thumbnail_init_blank(gfx_thumbnail_t *t)
|
||||
t->anim = NULL;
|
||||
t->anim_buf = NULL;
|
||||
t->anim_dt = NULL;
|
||||
t->anim_audio_dt = NULL;
|
||||
t->anim_audio_hi = 0;
|
||||
t->anim_audio_slot = -1;
|
||||
t->anim_audio_path = NULL;
|
||||
t->anim_sess = NULL;
|
||||
t->anim_job = NULL;
|
||||
t->anim_job2 = NULL;
|
||||
t->anim_buf_len = 0;
|
||||
|
||||
@@ -1093,6 +1093,7 @@ DRIVERS
|
||||
#include "../gfx/gfx_animation.c"
|
||||
#include "../gfx/gfx_display.c"
|
||||
#include "../gfx/gfx_thumbnail.c"
|
||||
#include "../gfx/gfx_anim_preview.c"
|
||||
|
||||
/* rflac is used by the audio mixer (HAVE_RFLAC) and by the CHD FLAC
|
||||
* decoder in libchdr (HAVE_CHD). Include its implementation once, ahead
|
||||
@@ -1326,6 +1327,7 @@ UI
|
||||
============================================================ */
|
||||
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
|
||||
#include "../ui/drivers/ui_win32.c"
|
||||
#include "../ui/drivers/ui_win32_companion.c"
|
||||
#endif
|
||||
|
||||
/*============================================================
|
||||
@@ -1346,6 +1348,10 @@ RETROARCH
|
||||
#endif
|
||||
#include "../command.c"
|
||||
#include "../ui/ui_companion_driver.c"
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
#include "../ui/companion/companion_core.c"
|
||||
#include "../ui/companion/companion_thumbs.c"
|
||||
#endif
|
||||
#include "../libretro-common/queues/task_queue.c"
|
||||
|
||||
#include "../msg_hash.c"
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
|
||||
#if TARGET_OS_OSX
|
||||
#include "../ui/drivers/ui_cocoa.m"
|
||||
#include "../ui/drivers/ui_cocoa_companion.m"
|
||||
#else
|
||||
#include "../ui/drivers/ui_cocoatouch.m"
|
||||
#endif
|
||||
|
||||
@@ -2096,6 +2096,10 @@ MSG_HASH(
|
||||
MENU_ENUM_LABEL_MIDI_DRIVER,
|
||||
MENU_ENUM_LABEL_MIDI_DRIVER_STR
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_LABEL_UI_COMPANION_DRIVER,
|
||||
MENU_ENUM_LABEL_UI_COMPANION_DRIVER_STR
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_LABEL_DEFERRED_REMAP_FILE_MANAGER_LIST,
|
||||
MENU_ENUM_LABEL_DEFERRED_REMAP_FILE_MANAGER_LIST_STR
|
||||
|
||||
@@ -3219,6 +3219,14 @@ MSG_HASH(
|
||||
MENU_ENUM_SUBLABEL_MIDI_DRIVER,
|
||||
"MIDI driver to use. (Restart required)"
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_LABEL_VALUE_UI_COMPANION_DRIVER,
|
||||
"Companion UI"
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_SUBLABEL_UI_COMPANION_DRIVER,
|
||||
"Desktop companion UI driver to use when the Desktop Menu is enabled. (Restart required)"
|
||||
)
|
||||
|
||||
/* Settings > Video */
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <compat/strl.h>
|
||||
|
||||
/* Implementation of strlcpy()/strlcat() based on OpenBSD. */
|
||||
@@ -28,6 +31,22 @@
|
||||
size_t strlcpy(char *s, const char *in, size_t len)
|
||||
{
|
||||
size_t src_len = strlen(in);
|
||||
#ifdef LIBRETRO_STRL_CHECK_OVERLAP
|
||||
/* Test builds: fail the way macOS's fortified libc does on an
|
||||
* overlapping copy (__chk_fail_overlap), so the Linux test suites
|
||||
* catch what only Apple's runtime would otherwise catch. */
|
||||
if (len && src_len)
|
||||
{
|
||||
const char *s_end = s + (src_len < len - 1 ? src_len : len - 1) + 1;
|
||||
const char *in_end = in + src_len + 1;
|
||||
if (!(s_end <= in || in_end <= s))
|
||||
{
|
||||
fprintf(stderr, "strlcpy: overlapping copy (dst %p, src %p, %u bytes)\n",
|
||||
(void*)s, (const void*)in, (unsigned)src_len);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (len)
|
||||
{
|
||||
size_t cpy_len = src_len < len - 1 ? src_len : len - 1;
|
||||
|
||||
@@ -129,7 +129,8 @@ static bool image_texture_load_internal(
|
||||
enum image_type_enum type,
|
||||
void *ptr,
|
||||
size_t len,
|
||||
struct texture_image *out_img)
|
||||
struct texture_image *out_img,
|
||||
bool (*should_abort)(void *ud), void *abort_ud)
|
||||
{
|
||||
int ret;
|
||||
void *img;
|
||||
@@ -148,7 +149,13 @@ static bool image_texture_load_internal(
|
||||
return false;
|
||||
}
|
||||
|
||||
while (image_transfer_iterate(img, type));
|
||||
/* Chunk by chunk, with the abort hook between chunks. */
|
||||
while (image_transfer_iterate(img, type))
|
||||
if (should_abort && should_abort(abort_ud))
|
||||
{
|
||||
image_transfer_free(img, type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!image_transfer_is_valid(img, type))
|
||||
{
|
||||
@@ -206,6 +213,14 @@ static bool image_texture_load_internal(
|
||||
ret = image_transfer_process(img, type,
|
||||
(uint32_t**)&out_img->pixels, len, &out_img->width,
|
||||
&out_img->height, out_img->supports_rgba);
|
||||
/* Pass by pass (inflate + unfilter), the abort hook between. */
|
||||
if (ret == IMAGE_PROCESS_NEXT && should_abort && should_abort(abort_ud))
|
||||
{
|
||||
free(out_img->pixels);
|
||||
out_img->pixels = NULL;
|
||||
image_transfer_free(img, type);
|
||||
return false;
|
||||
}
|
||||
} while (ret == IMAGE_PROCESS_NEXT);
|
||||
|
||||
if (ret == IMAGE_PROCESS_ERROR || ret == IMAGE_PROCESS_ERROR_END)
|
||||
@@ -218,6 +233,11 @@ static bool image_texture_load_internal(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool image_texture_load(struct texture_image *out_img, const char *path)
|
||||
{
|
||||
return image_texture_load_ex(out_img, path, NULL, NULL);
|
||||
}
|
||||
|
||||
void image_texture_free(struct texture_image *img)
|
||||
{
|
||||
if (!img)
|
||||
@@ -320,7 +340,7 @@ bool image_texture_load_buffer(struct texture_image *out_img,
|
||||
if (type != IMAGE_TYPE_NONE)
|
||||
{
|
||||
if (image_texture_load_internal(
|
||||
type, buffer, buffer_len, out_img))
|
||||
type, buffer, buffer_len, out_img, NULL, NULL))
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -333,8 +353,8 @@ bool image_texture_load_buffer(struct texture_image *out_img,
|
||||
return false;
|
||||
}
|
||||
|
||||
bool image_texture_load(struct texture_image *out_img,
|
||||
const char *path)
|
||||
bool image_texture_load_ex(struct texture_image *out_img,
|
||||
const char *path, bool (*should_abort)(void *ud), void *ud)
|
||||
{
|
||||
enum image_type_enum type = image_texture_get_type(path);
|
||||
|
||||
@@ -358,7 +378,7 @@ bool image_texture_load(struct texture_image *out_img,
|
||||
if (data_transfer_complete(dt) && ptr && file_len
|
||||
&& image_texture_load_internal(
|
||||
type,
|
||||
(void*)ptr, file_len, out_img))
|
||||
(void*)ptr, file_len, out_img, should_abort, ud))
|
||||
{
|
||||
data_transfer_free(dt);
|
||||
return true;
|
||||
|
||||
@@ -146,6 +146,15 @@ bool image_texture_load_buffer(struct texture_image *img,
|
||||
enum image_type_enum type, void *s, size_t len);
|
||||
|
||||
bool image_texture_load(struct texture_image *img, const char *path);
|
||||
|
||||
/* image_texture_load with an abort hook: both decode stages are
|
||||
* incremental (one chunk / one pass per step) and @should_abort is
|
||||
* asked between steps; returning true abandons the decode, which then
|
||||
* fails cleanly. For decodes on worker threads that must stop promptly
|
||||
* at shutdown or when their result is no longer wanted. @should_abort
|
||||
* may be NULL. */
|
||||
bool image_texture_load_ex(struct texture_image *img, const char *path,
|
||||
bool (*should_abort)(void *ud), void *ud);
|
||||
void image_texture_free(struct texture_image *img);
|
||||
|
||||
/* Force a CPU decode of a compressed texture_image into ->pixels (RGBA8).
|
||||
|
||||
@@ -46,6 +46,7 @@ enum string_list_type
|
||||
STRING_LIST_CAMERA_DRIVERS,
|
||||
STRING_LIST_BLUETOOTH_DRIVERS,
|
||||
STRING_LIST_WIFI_DRIVERS,
|
||||
STRING_LIST_UI_COMPANION_DRIVERS,
|
||||
STRING_LIST_LOCATION_DRIVERS,
|
||||
STRING_LIST_AUDIO_DRIVERS,
|
||||
#ifdef HAVE_MICROPHONE
|
||||
|
||||
@@ -1942,6 +1942,7 @@ static int menu_cbs_init_bind_get_string_representation_compare_label(
|
||||
case MENU_ENUM_LABEL_AUDIO_RESAMPLER_DRIVER:
|
||||
case MENU_ENUM_LABEL_RECORD_DRIVER:
|
||||
case MENU_ENUM_LABEL_MIDI_DRIVER:
|
||||
case MENU_ENUM_LABEL_UI_COMPANION_DRIVER:
|
||||
case MENU_ENUM_LABEL_LOCATION_DRIVER:
|
||||
case MENU_ENUM_LABEL_CAMERA_DRIVER:
|
||||
case MENU_ENUM_LABEL_BLUETOOTH_DRIVER:
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "../../input/input_remapping.h"
|
||||
|
||||
#include "../../retroarch.h"
|
||||
#include "../../ui/ui_companion_driver.h"
|
||||
#include "../../core_option_manager.h"
|
||||
|
||||
#ifdef HAVE_CHEEVOS
|
||||
@@ -512,6 +513,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_menu_screensaver_animation_speed, ME
|
||||
#ifdef HAVE_BLUETOOTH
|
||||
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_bluetooth_driver, MENU_ENUM_SUBLABEL_BLUETOOTH_DRIVER)
|
||||
#endif
|
||||
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_ui_companion_driver, MENU_ENUM_SUBLABEL_UI_COMPANION_DRIVER)
|
||||
|
||||
#ifdef HAVE_MICROPHONE
|
||||
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_microphone_driver, MENU_ENUM_SUBLABEL_MICROPHONE_DRIVER)
|
||||
@@ -613,7 +615,7 @@ DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_video_dingux_rs90_softfilter_type,
|
||||
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_video_vp_bias_portrait_x, MENU_ENUM_SUBLABEL_VIDEO_VIEWPORT_BIAS_PORTRAIT_X)
|
||||
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_video_vp_bias_portrait_y, MENU_ENUM_SUBLABEL_VIDEO_VIEWPORT_BIAS_PORTRAIT_Y)
|
||||
#endif
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
DEFAULT_SUBLABEL_MACRO(action_bind_sublabel_show_wimp, MENU_ENUM_SUBLABEL_SHOW_WIMP)
|
||||
#endif
|
||||
|
||||
@@ -3043,6 +3045,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs,
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_bluetooth_driver);
|
||||
#endif
|
||||
break;
|
||||
case MENU_ENUM_LABEL_UI_COMPANION_DRIVER:
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_ui_companion_driver);
|
||||
break;
|
||||
#if defined(HAVE_MATERIALUI) || defined(HAVE_XMB) || defined(HAVE_OZONE)
|
||||
case MENU_ENUM_LABEL_MENU_SCREENSAVER_ANIMATION:
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_screensaver_animation);
|
||||
@@ -3541,7 +3546,7 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs,
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_video_filter_enable);
|
||||
break;
|
||||
#endif
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
case MENU_ENUM_LABEL_SHOW_WIMP:
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_show_wimp);
|
||||
break;
|
||||
|
||||
+22
-4
@@ -10455,7 +10455,7 @@ unsigned menu_displaylist_build_list(
|
||||
case DISPLAYLIST_USER_INTERFACE_SETTINGS_LIST:
|
||||
{
|
||||
bool kiosk_mode_enable = settings->bools.kiosk_mode_enable;
|
||||
#if defined(HAVE_QT) || defined(HAVE_COCOA)
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
bool desktop_menu_enable = settings->bools.desktop_menu_enable;
|
||||
#endif
|
||||
bool menu_screensaver_supported = ((menu_st->flags & MENU_ST_FLAG_SCREENSAVER_SUPPORTED) > 0);
|
||||
@@ -10498,11 +10498,26 @@ unsigned menu_displaylist_build_list(
|
||||
#if !TARGET_OS_OSX
|
||||
{MENU_ENUM_LABEL_VIDEO_DISABLE_COMPOSITION, PARSE_ONLY_BOOL, true},
|
||||
#endif
|
||||
#if defined(HAVE_QT) || defined(HAVE_COCOA)
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
{MENU_ENUM_LABEL_UI_COMPANION_ENABLE, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_UI_COMPANION_START_ON_BOOT, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_UI_COMPANION_TOGGLE, PARSE_ONLY_BOOL, false},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_ENABLE, PARSE_ONLY_BOOL, true},
|
||||
/* Presentation settings shared by every desktop companion. */
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_VIEW_TYPE, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_TYPE, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_SUGGEST_LOADED_CORE_FIRST, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_SAVE_LAST_TAB, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_SAVE_GEOMETRY, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_SHOW_WELCOME_SCREEN, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_SCAN_FINISH_CONFIRM, PARSE_ONLY_BOOL, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_CACHE_LIMIT, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_MAX_SIZE, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_QUALITY, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_ICON_VIEW_ZOOM, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_ALL_PLAYLISTS_LIST_MAX_COUNT, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_ALL_PLAYLISTS_GRID_MAX_COUNT, PARSE_ONLY_UINT, true},
|
||||
{MENU_ENUM_LABEL_DESKTOP_MENU_THEME, PARSE_ONLY_UINT, true},
|
||||
#endif
|
||||
#ifdef _3DS
|
||||
{MENU_ENUM_LABEL_VIDEO_3DS_DISPLAY_MODE, PARSE_ONLY_UINT, true},
|
||||
@@ -10547,7 +10562,7 @@ unsigned menu_displaylist_build_list(
|
||||
&& (menu_screensaver_animation != MENU_SCREENSAVER_BLANK);
|
||||
break;
|
||||
#endif
|
||||
#if defined(HAVE_QT) || defined(HAVE_COCOA)
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
case MENU_ENUM_LABEL_UI_COMPANION_TOGGLE:
|
||||
build_list[i].checked = desktop_menu_enable;
|
||||
break;
|
||||
@@ -12440,6 +12455,9 @@ unsigned menu_displaylist_build_list(
|
||||
{MENU_ENUM_LABEL_JOYPAD_DRIVER, PARSE_ONLY_STRING_OPTIONS},
|
||||
{MENU_ENUM_LABEL_RECORD_DRIVER, PARSE_ONLY_STRING_OPTIONS},
|
||||
{MENU_ENUM_LABEL_MIDI_DRIVER, PARSE_ONLY_STRING_OPTIONS},
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
{MENU_ENUM_LABEL_UI_COMPANION_DRIVER, PARSE_ONLY_STRING_OPTIONS},
|
||||
#endif
|
||||
#ifdef HAVE_BLUETOOTH
|
||||
{MENU_ENUM_LABEL_BLUETOOTH_DRIVER, PARSE_ONLY_STRING_OPTIONS},
|
||||
#endif
|
||||
@@ -16137,7 +16155,7 @@ static bool menu_displaylist_ctl_internal(
|
||||
count++;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
if (settings->bools.desktop_menu_enable)
|
||||
if (MENU_DISPLAYLIST_PARSE_SETTINGS_ENUM(
|
||||
info->list,
|
||||
|
||||
+13
-2
@@ -13152,7 +13152,7 @@ static void settings_build_drivers(
|
||||
{
|
||||
|
||||
unsigned i, j = 0;
|
||||
struct string_options_entry string_options_entries[14] = {{0}};
|
||||
struct string_options_entry string_options_entries[15] = {{0}};
|
||||
|
||||
START_GROUP(list, list_info, &group_info, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_DRIVER_SETTINGS), parent_group);
|
||||
MENU_SETTINGS_LIST_CURRENT_ADD_ENUM_IDX_PTR(list, list_info, MENU_ENUM_LABEL_DRIVER_SETTINGS);
|
||||
@@ -13296,6 +13296,17 @@ static void settings_build_drivers(
|
||||
|
||||
j++;
|
||||
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
string_options_entries[j].target = settings->arrays.ui_companion_driver;
|
||||
string_options_entries[j].len = sizeof(settings->arrays.ui_companion_driver);
|
||||
string_options_entries[j].name_enum_idx = MENU_ENUM_LABEL_UI_COMPANION_DRIVER;
|
||||
string_options_entries[j].SHORT_enum_idx = MENU_ENUM_LABEL_VALUE_UI_COMPANION_DRIVER;
|
||||
string_options_entries[j].default_value = config_get_default_ui_companion();
|
||||
string_options_entries[j].values = config_get_ui_companion_driver_options();
|
||||
|
||||
j++;
|
||||
#endif
|
||||
|
||||
for (i = 0; i < j; i++)
|
||||
{
|
||||
CONFIG_STRING_OPTIONS(
|
||||
@@ -15269,7 +15280,7 @@ static void settings_build_input_hotkey(
|
||||
{
|
||||
if (!input_config_bind_map_get_meta(i))
|
||||
continue;
|
||||
#ifndef HAVE_QT
|
||||
#ifndef HAVE_COMPANION_WIMP
|
||||
if (i == RARCH_UI_COMPANION_TOGGLE)
|
||||
continue;
|
||||
#endif
|
||||
|
||||
+21
@@ -16075,6 +16075,26 @@ enum msg_hash_enums
|
||||
MENU_LBL_H(UI_COMPANION_START_ON_BOOT),
|
||||
MENU_LABEL(UI_COMPANION_TOGGLE),
|
||||
MENU_LABEL(DESKTOP_MENU_ENABLE),
|
||||
/* Desktop companion presentation settings (settings_def_desktop_menu.h) */
|
||||
MENU_LABEL(DESKTOP_MENU_SUGGEST_LOADED_CORE_FIRST),
|
||||
MENU_LABEL(DESKTOP_MENU_SAVE_LAST_TAB),
|
||||
MENU_LABEL(DESKTOP_MENU_SAVE_GEOMETRY),
|
||||
MENU_LABEL(DESKTOP_MENU_SHOW_WELCOME_SCREEN),
|
||||
MENU_LABEL(DESKTOP_MENU_SCAN_FINISH_CONFIRM),
|
||||
MENU_LABEL(DESKTOP_MENU_VIEW_TYPE),
|
||||
MENU_LABEL(DESKTOP_MENU_THUMBNAIL_TYPE),
|
||||
MENU_LABEL(DESKTOP_MENU_LAST_TAB),
|
||||
MENU_LABEL(DESKTOP_MENU_THUMBNAIL_CACHE_LIMIT),
|
||||
MENU_LABEL(DESKTOP_MENU_THUMBNAIL_MAX_SIZE),
|
||||
MENU_LABEL(DESKTOP_MENU_THUMBNAIL_QUALITY),
|
||||
MENU_LABEL(DESKTOP_MENU_ICON_VIEW_ZOOM),
|
||||
MENU_LABEL(DESKTOP_MENU_ALL_PLAYLISTS_LIST_MAX_COUNT),
|
||||
MENU_LABEL(DESKTOP_MENU_ALL_PLAYLISTS_GRID_MAX_COUNT),
|
||||
MENU_LABEL(DESKTOP_MENU_THEME),
|
||||
MENU_LABEL(DESKTOP_MENU_WINDOW_X),
|
||||
MENU_LABEL(DESKTOP_MENU_WINDOW_Y),
|
||||
MENU_LABEL(DESKTOP_MENU_WINDOW_WIDTH),
|
||||
MENU_LABEL(DESKTOP_MENU_WINDOW_HEIGHT),
|
||||
/* GENERATED REGION: menubar setting enum rows (see settings/settings_def_ui_menubar.h). */
|
||||
#define SETTINGS_DEF_ENUM_PASS
|
||||
#define SETTINGS_DEF_STRINGS_PASS
|
||||
@@ -23585,6 +23605,7 @@ enum msg_hash_enums
|
||||
MENU_LABEL(VIDEO_DRIVER),
|
||||
MENU_LABEL(INPUT_DRIVER),
|
||||
MENU_LABEL(MIDI_DRIVER),
|
||||
MENU_LABEL(UI_COMPANION_DRIVER),
|
||||
|
||||
MENU_ENUM_LABEL_AUDIO_DRIVER_RSOUND,
|
||||
MENU_ENUM_LABEL_AUDIO_DRIVER_AUDIOIO,
|
||||
|
||||
@@ -530,6 +530,7 @@
|
||||
#define MENU_ENUM_LABEL_RECORDING_OUTPUT_DIRECTORY_STR "recording_output_directory"
|
||||
#define MENU_ENUM_LABEL_RECORD_DRIVER_STR "record_driver"
|
||||
#define MENU_ENUM_LABEL_MIDI_DRIVER_STR "midi_driver"
|
||||
#define MENU_ENUM_LABEL_UI_COMPANION_DRIVER_STR "ui_companion_driver"
|
||||
#define MENU_ENUM_LABEL_DEFERRED_REMAP_FILE_MANAGER_LIST_STR "deferred_remap_file_manager_list"
|
||||
#define MENU_ENUM_LABEL_REMAP_FILE_MANAGER_LIST_STR "remap_file_manager_list"
|
||||
#define MENU_ENUM_LABEL_REMAP_FILE_INFO_STR "remap_file_info"
|
||||
@@ -1082,8 +1083,28 @@
|
||||
#define MENU_ENUM_LABEL_CRT_SWITCH_RESOLUTION_SUPER_STR "crt_switch_resolution_super"
|
||||
#define MENU_ENUM_LABEL_CRT_SWITCH_RESOLUTION_USE_CUSTOM_REFRESH_RATE_STR "crt_switch_resolution_use_custom_refresh_rate"
|
||||
#define MENU_ENUM_LABEL_CRT_SWITCH_VERTICAL_ADJUST_STR "crt_switch_vertical_size"
|
||||
#define MENU_ENUM_LABEL_CRT_SWITCH_WRITE_EDID_STR "crt_switch_write_edid"
|
||||
#define MENU_ENUM_LABEL_CRT_SWITCH_X_AXIS_CENTERING_STR "crt_switch_horizontal_shift"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_ALL_PLAYLISTS_GRID_MAX_COUNT_STR "desktop_menu_all_playlists_grid_max_count"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_ALL_PLAYLISTS_LIST_MAX_COUNT_STR "desktop_menu_all_playlists_list_max_count"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_ENABLE_STR "desktop_menu_enable"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_ICON_VIEW_ZOOM_STR "desktop_menu_icon_view_zoom"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_LAST_TAB_STR "desktop_menu_last_tab"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_SAVE_GEOMETRY_STR "desktop_menu_save_geometry"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_SAVE_LAST_TAB_STR "desktop_menu_save_last_tab"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_SCAN_FINISH_CONFIRM_STR "desktop_menu_scan_finish_confirm"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_SHOW_WELCOME_SCREEN_STR "desktop_menu_show_welcome_screen"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_SUGGEST_LOADED_CORE_FIRST_STR "desktop_menu_suggest_loaded_core_first"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_THEME_STR "desktop_menu_theme"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_CACHE_LIMIT_STR "desktop_menu_thumbnail_cache_limit"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_MAX_SIZE_STR "desktop_menu_thumbnail_max_size"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_QUALITY_STR "desktop_menu_thumbnail_quality"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_THUMBNAIL_TYPE_STR "desktop_menu_thumbnail_type"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_VIEW_TYPE_STR "desktop_menu_view_type"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_WINDOW_HEIGHT_STR "desktop_menu_window_height"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_WINDOW_WIDTH_STR "desktop_menu_window_width"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_WINDOW_X_STR "desktop_menu_window_x"
|
||||
#define MENU_ENUM_LABEL_DESKTOP_MENU_WINDOW_Y_STR "desktop_menu_window_y"
|
||||
#define MENU_ENUM_LABEL_DIRECTORY_SETTINGS_STR "directory_settings"
|
||||
#define MENU_ENUM_LABEL_DISCORD_ALLOW_STR "discord_allow"
|
||||
#define MENU_ENUM_LABEL_DRIVER_SETTINGS_STR "driver_settings"
|
||||
@@ -1639,6 +1660,7 @@
|
||||
#define MENU_ENUM_LABEL_VIDEO_SCALE_INTEGER_SCALING_STR "video_scale_integer_scaling"
|
||||
#define MENU_ENUM_LABEL_VIDEO_SCALING_SETTINGS_STR "video_scaling_settings"
|
||||
#define MENU_ENUM_LABEL_VIDEO_SCAN_SUBFRAMES_STR "video_scan_subframes"
|
||||
#define MENU_ENUM_LABEL_VIDEO_SDL_DISPLAY_SERVER_STR "video_sdl_display_server"
|
||||
#define MENU_ENUM_LABEL_VIDEO_SETTINGS_STR "video_settings"
|
||||
#define MENU_ENUM_LABEL_VIDEO_SHADERS_ENABLE_STR "video_shader_enable"
|
||||
#define MENU_ENUM_LABEL_VIDEO_SHADER_DELAY_STR "video_shader_delay"
|
||||
|
||||
+29
-12
@@ -1303,6 +1303,12 @@ static size_t find_driver_nonempty(
|
||||
return strlcpy(s, cloud_sync_drivers[i]->ident, len);
|
||||
}
|
||||
#endif
|
||||
else if (!strcmp(label, "ui_companion_driver"))
|
||||
{
|
||||
const char *ident = ui_companion_wimp_find_ident(i);
|
||||
if (ident)
|
||||
return strlcpy(s, ident, len);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2349,6 +2355,17 @@ static struct string_list *string_list_new_special(
|
||||
string_list_append(s, opt, attr);
|
||||
}
|
||||
break;
|
||||
case STRING_LIST_UI_COMPANION_DRIVERS:
|
||||
for (i = 0; ; i++)
|
||||
{
|
||||
const char *opt = ui_companion_wimp_find_ident(i);
|
||||
if (!opt)
|
||||
break;
|
||||
*len += strlen(opt) + 1;
|
||||
|
||||
string_list_append(s, opt, attr);
|
||||
}
|
||||
break;
|
||||
case STRING_LIST_BLUETOOTH_DRIVERS:
|
||||
#ifdef HAVE_BLUETOOTH
|
||||
for (i = 0; bluetooth_drivers[i]; i++)
|
||||
@@ -4930,7 +4947,7 @@ bool command_event(enum event_command cmd, void *data)
|
||||
#endif
|
||||
if (uico_st->flags & UICO_ST_FLAG_IS_ON_FOREGROUND)
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
bool desktop_menu_enable = settings->bools.desktop_menu_enable;
|
||||
bool ui_companion_toggle = settings->bools.ui_companion_toggle;
|
||||
#else
|
||||
@@ -5787,7 +5804,7 @@ bool command_event(enum event_command cmd, void *data)
|
||||
break;
|
||||
case CMD_EVENT_UI_COMPANION_TOGGLE:
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
bool desktop_menu_enable = settings->bools.desktop_menu_enable;
|
||||
bool ui_companion_toggle = settings->bools.ui_companion_toggle;
|
||||
#else
|
||||
@@ -6624,7 +6641,7 @@ int rarch_main(int argc, char *argv[], void *data)
|
||||
settings = config_get_ptr();
|
||||
|
||||
ui_companion_driver_init_first(
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
settings->bools.desktop_menu_enable,
|
||||
settings->bools.ui_companion_toggle,
|
||||
#else
|
||||
@@ -6645,9 +6662,7 @@ int rarch_main(int argc, char *argv[], void *data)
|
||||
{
|
||||
int ret;
|
||||
bool app_exit = false;
|
||||
#ifdef HAVE_QT
|
||||
ui_companion_qt.application->process_events();
|
||||
#endif
|
||||
ui_companion_driver_wimp_iterate();
|
||||
ret = runloop_iterate();
|
||||
|
||||
task_queue_check();
|
||||
@@ -6656,19 +6671,21 @@ int rarch_main(int argc, char *argv[], void *data)
|
||||
steam_poll();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_QT
|
||||
app_exit = ui_companion_qt.application->exiting;
|
||||
#endif
|
||||
/* Whichever desktop companion is active, not Qt specifically. */
|
||||
app_exit = ui_companion_driver_wimp_exiting();
|
||||
|
||||
if (ret == -1 || app_exit)
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
ui_companion_qt.application->quit();
|
||||
#endif
|
||||
ui_companion_driver_wimp_quit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Close the desktop companion window while its message pump and the
|
||||
* drivers are still alive; leaving it for main_exit's teardown lets a
|
||||
* native (Win32) companion window outlive the pump and hang. */
|
||||
ui_companion_driver_wimp_deinit();
|
||||
|
||||
main_exit(data);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ LIBRETRO_COMM_DIR := $(REPO_ROOT)/libretro-common
|
||||
# lets the test drive the animation open directly - upstream of the
|
||||
# task queue, downstream of the request plumbing.
|
||||
UUT_SRC := $(REPO_ROOT)/gfx/gfx_thumbnail.c
|
||||
ANIM_PREVIEW_SRC := $(REPO_ROOT)/gfx/gfx_anim_preview.c
|
||||
UUT_OBJ := gfx_thumbnail_uut.o
|
||||
|
||||
SOURCES := gfx_thumbnail_anim_test.c \
|
||||
@@ -75,7 +76,13 @@ $(UUT_OBJ): $(UUT_SRC)
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
|
||||
$(TARGET): $(OBJS) $(UUT_OBJ)
|
||||
# The preview session gfx_thumbnail now drives its window feeder and
|
||||
# audio through; built with the UUT's flags (same stubs surface).
|
||||
ANIM_PREVIEW_OBJ := gfx_anim_preview_uut.o
|
||||
$(ANIM_PREVIEW_OBJ): $(ANIM_PREVIEW_SRC)
|
||||
$(CC) -c -o $@ $< $(UUT_CFLAGS)
|
||||
|
||||
$(TARGET): $(OBJS) $(UUT_OBJ) $(ANIM_PREVIEW_OBJ)
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
# ASan and TSan are mutually exclusive, hence separate passes.
|
||||
@@ -89,6 +96,6 @@ sweep:
|
||||
@echo "sweep clean"
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) $(OBJS) $(UUT_OBJ)
|
||||
rm -f $(TARGET) $(OBJS) $(UUT_OBJ) $(ANIM_PREVIEW_OBJ)
|
||||
|
||||
.PHONY: all sweep clean
|
||||
@@ -13,6 +13,9 @@ LIBRETRO_COMM_DIR := $(REPO_ROOT)/libretro-common
|
||||
|
||||
UUT_SRC := $(REPO_ROOT)/gfx/gfx_thumbnail.c
|
||||
UUT_OBJ := gfx_thumbnail_uut.o
|
||||
# The preview session gfx_thumbnail drives its feeder and audio through.
|
||||
ANIM_PREVIEW_SRC := $(REPO_ROOT)/gfx/gfx_anim_preview.c
|
||||
ANIM_PREVIEW_OBJ := gfx_anim_preview_uut.o
|
||||
|
||||
# Layout facts (settings_t offsets/size) for the stubs, compiled with
|
||||
# UUT_CFLAGS so they match what the UUT's configuration.h produces.
|
||||
@@ -128,13 +131,16 @@ $(DECODE_TARGET_NORWEBM): $(DECODE_SOURCES) preview_audio_decode_test.c
|
||||
$(UUT_OBJ): $(UUT_SRC)
|
||||
$(CC) -c -o $@ $< $(UUT_CFLAGS)
|
||||
|
||||
$(ANIM_PREVIEW_OBJ): $(ANIM_PREVIEW_SRC)
|
||||
$(CC) -c -o $@ $< $(UUT_CFLAGS)
|
||||
|
||||
$(LAYOUT_OBJ): settings_layout_uut.c
|
||||
$(CC) -c -o $@ $< $(UUT_CFLAGS)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
|
||||
$(TARGET): $(OBJS) $(UUT_OBJ) $(LAYOUT_OBJ)
|
||||
$(TARGET): $(OBJS) $(UUT_OBJ) $(LAYOUT_OBJ) $(ANIM_PREVIEW_OBJ)
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
# The fixtures are container layouts, not clips: see mkfixtures.py.
|
||||
@@ -154,7 +160,7 @@ check: $(TARGET) $(DECODE_TARGET) $(DECODE_TARGET_NORWEBM) fixtures
|
||||
# left the following run globbing nothing. Use distclean to drop them.
|
||||
clean:
|
||||
rm -f $(TARGET) $(DECODE_TARGET) $(DECODE_TARGET_NORWEBM) \
|
||||
$(OBJS) $(UUT_OBJ) $(LAYOUT_OBJ)
|
||||
$(OBJS) $(UUT_OBJ) $(LAYOUT_OBJ) $(ANIM_PREVIEW_OBJ)
|
||||
|
||||
distclean: clean
|
||||
rm -rf fixtures
|
||||
|
||||
@@ -20,33 +20,41 @@ S_UINT_EX(menu_scroll_delay, MENU_SCROLL_DELAY,
|
||||
"Scroll Delay",
|
||||
"Initial delay in milliseconds when holding a direction to scroll.")
|
||||
#endif
|
||||
/* Descriptor and configuration rows are #if defined(HAVE_QT) || defined(HAVE_COCOA); the string
|
||||
* tables always carry this row via the strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA)) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
/* Descriptor and configuration rows are guarded by the same condition
|
||||
* as HAVE_COMPANION_WIMP in ui/ui_companion_driver.h (Qt, Cocoa, or
|
||||
* desktop Win32); the string tables always carry this row via the
|
||||
* strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS(ui_companion_enable, UI_COMPANION_ENABLE,
|
||||
"ui_companion_enable",
|
||||
DEFAULT_UI_COMPANION_ENABLE, SD_FLAG_ADVANCED, 0, 0,
|
||||
"UI Companion")
|
||||
#endif
|
||||
/* Descriptor and configuration rows are #if defined(HAVE_QT) || defined(HAVE_COCOA); the string
|
||||
* tables always carry this row via the strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA)) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
/* Descriptor and configuration rows are guarded by the same condition
|
||||
* as HAVE_COMPANION_WIMP in ui/ui_companion_driver.h (Qt, Cocoa, or
|
||||
* desktop Win32); the string tables always carry this row via the
|
||||
* strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS_H(ui_companion_start_on_boot, UI_COMPANION_START_ON_BOOT,
|
||||
"ui_companion_start_on_boot",
|
||||
DEFAULT_UI_COMPANION_START_ON_BOOT, SD_FLAG_ADVANCED, 0, 0,
|
||||
"Start UI Companion on Boot")
|
||||
#endif
|
||||
/* Descriptor and configuration rows are #if defined(HAVE_QT) || defined(HAVE_COCOA); the string
|
||||
* tables always carry this row via the strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA)) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
/* Descriptor and configuration rows are guarded by the same condition
|
||||
* as HAVE_COMPANION_WIMP in ui/ui_companion_driver.h (Qt, Cocoa, or
|
||||
* desktop Win32); the string tables always carry this row via the
|
||||
* strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_EX_NS(desktop_menu_enable, DESKTOP_MENU_ENABLE,
|
||||
"desktop_menu_enable",
|
||||
DEFAULT_DESKTOP_MENU_ENABLE, SD_FLAG_NONE, 0, 0, setting_bool_action_left_with_refresh, NULL, NULL, NULL, setting_bool_action_left_with_refresh, setting_bool_action_right_with_refresh, 0,
|
||||
"Desktop Menu (Restart required)")
|
||||
#endif
|
||||
/* Descriptor and configuration rows are #if defined(HAVE_QT) || defined(HAVE_COCOA); the string
|
||||
* tables always carry this row via the strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA)) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
/* Descriptor and configuration rows are guarded by the same condition
|
||||
* as HAVE_COMPANION_WIMP in ui/ui_companion_driver.h (Qt, Cocoa, or
|
||||
* desktop Win32); the string tables always carry this row via the
|
||||
* strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
/* The configuration table registers this row by hand in
|
||||
* configuration.c because it carries no default there; the
|
||||
* generated row is for the other passes. */
|
||||
@@ -57,3 +65,134 @@ S_BOOL_NS(ui_companion_toggle, UI_COMPANION_TOGGLE,
|
||||
"Open Desktop Menu on Startup")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Desktop companion presentation settings shared by every companion
|
||||
* backend. Same guard as HAVE_COMPANION_WIMP. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS(desktop_menu_suggest_loaded_core_first, DESKTOP_MENU_SUGGEST_LOADED_CORE_FIRST,
|
||||
"desktop_menu_suggest_loaded_core_first",
|
||||
DEFAULT_DESKTOP_MENU_SUGGEST_LOADED_CORE_FIRST, SD_FLAG_ADVANCED, 0, 0,
|
||||
"Desktop Menu: Suggest Loaded Core First")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS(desktop_menu_save_last_tab, DESKTOP_MENU_SAVE_LAST_TAB,
|
||||
"desktop_menu_save_last_tab",
|
||||
DEFAULT_DESKTOP_MENU_SAVE_LAST_TAB, SD_FLAG_ADVANCED, 0, 0,
|
||||
"Desktop Menu: Remember Last Tab")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS(desktop_menu_save_geometry, DESKTOP_MENU_SAVE_GEOMETRY,
|
||||
"desktop_menu_save_geometry",
|
||||
DEFAULT_DESKTOP_MENU_SAVE_GEOMETRY, SD_FLAG_ADVANCED, 0, 0,
|
||||
"Desktop Menu: Remember Window Geometry")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS(desktop_menu_show_welcome_screen, DESKTOP_MENU_SHOW_WELCOME_SCREEN,
|
||||
"desktop_menu_show_welcome_screen",
|
||||
DEFAULT_DESKTOP_MENU_SHOW_WELCOME_SCREEN, SD_FLAG_ADVANCED, 0, 0,
|
||||
"Desktop Menu: Show Welcome Screen")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_BOOL_NS(desktop_menu_scan_finish_confirm, DESKTOP_MENU_SCAN_FINISH_CONFIRM,
|
||||
"desktop_menu_scan_finish_confirm",
|
||||
DEFAULT_DESKTOP_MENU_SCAN_FINISH_CONFIRM, SD_FLAG_ADVANCED, 0, 0,
|
||||
"Desktop Menu: Confirm When Scan Finishes")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_view_type, DESKTOP_MENU_VIEW_TYPE,
|
||||
"desktop_menu_view_type",
|
||||
DEFAULT_DESKTOP_MENU_VIEW_TYPE, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 1, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: View Type",
|
||||
"0 = list, 1 = icons.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_thumbnail_type, DESKTOP_MENU_THUMBNAIL_TYPE,
|
||||
"desktop_menu_thumbnail_type",
|
||||
DEFAULT_DESKTOP_MENU_THUMBNAIL_TYPE, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 3, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Thumbnail Type",
|
||||
"0 = boxart, 1 = screenshot, 2 = title screen, 3 = logo.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_last_tab, DESKTOP_MENU_LAST_TAB,
|
||||
"desktop_menu_last_tab",
|
||||
DEFAULT_DESKTOP_MENU_LAST_TAB, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 1, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Last Tab",
|
||||
"0 = playlists, 1 = file browser.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_thumbnail_cache_limit, DESKTOP_MENU_THUMBNAIL_CACHE_LIMIT,
|
||||
"desktop_menu_thumbnail_cache_limit",
|
||||
DEFAULT_DESKTOP_MENU_THUMBNAIL_CACHE_LIMIT, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 100000, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Thumbnail Cache Limit",
|
||||
"Number of thumbnails kept in memory.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_thumbnail_max_size, DESKTOP_MENU_THUMBNAIL_MAX_SIZE,
|
||||
"desktop_menu_thumbnail_max_size",
|
||||
DEFAULT_DESKTOP_MENU_THUMBNAIL_MAX_SIZE, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 4096, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Thumbnail Max Size",
|
||||
"Longest edge in pixels; 0 = unlimited.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_thumbnail_quality, DESKTOP_MENU_THUMBNAIL_QUALITY,
|
||||
"desktop_menu_thumbnail_quality",
|
||||
DEFAULT_DESKTOP_MENU_THUMBNAIL_QUALITY, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 100, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Thumbnail Quality",
|
||||
"0 = default.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_icon_view_zoom, DESKTOP_MENU_ICON_VIEW_ZOOM,
|
||||
"desktop_menu_icon_view_zoom",
|
||||
DEFAULT_DESKTOP_MENU_ICON_VIEW_ZOOM, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 100, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Icon View Zoom",
|
||||
"Zoom level of the icon view.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_all_playlists_list_max_count, DESKTOP_MENU_ALL_PLAYLISTS_LIST_MAX_COUNT,
|
||||
"desktop_menu_all_playlists_list_max_count",
|
||||
DEFAULT_DESKTOP_MENU_ALL_PLAYLISTS_LIST_MAX_COUNT, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 100000, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: All Playlists List Max Count",
|
||||
"Entries shown for All Playlists in list view; 0 = unlimited.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_all_playlists_grid_max_count, DESKTOP_MENU_ALL_PLAYLISTS_GRID_MAX_COUNT,
|
||||
"desktop_menu_all_playlists_grid_max_count",
|
||||
DEFAULT_DESKTOP_MENU_ALL_PLAYLISTS_GRID_MAX_COUNT, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 100000, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: All Playlists Grid Max Count",
|
||||
"Entries shown for All Playlists in icon view; 0 = unlimited.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_theme, DESKTOP_MENU_THEME,
|
||||
"desktop_menu_theme",
|
||||
DEFAULT_DESKTOP_MENU_THEME, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 2, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Theme",
|
||||
"0 = system default, 1 = dark, 2 = custom stylesheet.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_window_x, DESKTOP_MENU_WINDOW_X,
|
||||
"desktop_menu_window_x",
|
||||
0, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 32767, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Window X",
|
||||
"Saved window position; used when Remember Window Geometry is on.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_window_y, DESKTOP_MENU_WINDOW_Y,
|
||||
"desktop_menu_window_y",
|
||||
0, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 32767, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Window Y",
|
||||
"Saved window position; used when Remember Window Geometry is on.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_window_width, DESKTOP_MENU_WINDOW_WIDTH,
|
||||
"desktop_menu_window_width",
|
||||
0, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 32767, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Window Width",
|
||||
"Saved window size; used when Remember Window Geometry is on.")
|
||||
#endif
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_UINT_EX(desktop_menu_window_height, DESKTOP_MENU_WINDOW_HEIGHT,
|
||||
"desktop_menu_window_height",
|
||||
0, SD_FLAG_ADVANCED, SDESC_RANGE_MINMAX, 0, 0, 32767, 1, 0, setting_action_ok_uint, NULL, NULL, NULL, NULL, NULL, 0,
|
||||
"Desktop Menu: Window Height",
|
||||
"Saved window size; used when Remember Window Geometry is on.")
|
||||
#endif
|
||||
@@ -76,9 +76,11 @@ S_ACTION_EX(HELP_LIST,
|
||||
"help_list", SD_FLAG_LAKKA_ADVANCED, NULL, NULL, 0,
|
||||
"Help",
|
||||
"Learn more about how the program works.")
|
||||
/* Descriptor and configuration rows are #ifdef HAVE_QT; the string
|
||||
* tables always carry this row via the strings pass. */
|
||||
#if defined(HAVE_QT) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
/* Descriptor and configuration rows are guarded by the same condition
|
||||
* as HAVE_COMPANION_WIMP in ui/ui_companion_driver.h (Qt, Cocoa, or
|
||||
* desktop Win32); the string tables always carry this row via the
|
||||
* strings pass. */
|
||||
#if (defined(HAVE_QT) || defined(HAVE_COCOA) || (defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))) || defined(SETTINGS_DEF_STRINGS_PASS)
|
||||
S_ACTION_EX(SHOW_WIMP,
|
||||
"show_wimp", SD_FLAG_NONE, NULL, NULL, CMD_EVENT_UI_COMPANION_TOGGLE,
|
||||
"Show Desktop Menu",
|
||||
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/bin/sh
|
||||
# Regression harness for the Cocoa companion, on Linux: the real
|
||||
# ui/drivers/ui_cocoa_companion.m compiled and linked against GNUstep's
|
||||
# Foundation + AppKit, with the real companion core, the core test's
|
||||
# stubs and fixtures, and a stub platform (a window with a render view)
|
||||
# - run headless under Xvfb. See companion_cocoa_test.m for what it
|
||||
# asserts.
|
||||
#
|
||||
# Needs: gobjc, gnustep-devel, gnustep-back-common, xvfb (Debian/Ubuntu:
|
||||
# apt-get install gobjc gnustep-devel gnustep-back-common xvfb)
|
||||
#
|
||||
# tools/companion_cocoa_test.sh
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CC=${CC:-gcc}
|
||||
if ! command -v gnustep-config >/dev/null 2>&1; then
|
||||
echo "gnustep-config not found: install gnustep-devel" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
# Apple-only headers and macros the driver includes, shimmed for GNUstep.
|
||||
SHIM="$OUT/shim"
|
||||
mkdir -p "$SHIM/objc" "$SHIM/QuartzCore"
|
||||
printf '#include <objc/runtime.h>\n#include <objc/message.h>\n' > "$SHIM/objc/objc-runtime.h"
|
||||
printf '#include <Foundation/Foundation.h>\n' > "$SHIM/QuartzCore/QuartzCore.h"
|
||||
cat > "$SHIM/TargetConditionals.h" <<'EOF'
|
||||
#define TARGET_OS_MAC 1
|
||||
#define TARGET_OS_OSX 1
|
||||
#define TARGET_OS_IPHONE 0
|
||||
#define TARGET_OS_IOS 0
|
||||
#define TARGET_OS_TV 0
|
||||
#define TARGET_OS_SIMULATOR 0
|
||||
EOF
|
||||
cat > "$SHIM/AvailabilityMacros.h" <<'EOF'
|
||||
/* GNUstep defines the MAC_OS_X_VERSION_10_x constants itself (its own
|
||||
* scale: 10.12 is 1120); take its newest API level. */
|
||||
#include <GNUstepBase/GSVersionMacros.h>
|
||||
#ifndef MAC_OS_X_VERSION_MAX_ALLOWED
|
||||
#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_14
|
||||
#endif
|
||||
#ifndef MAC_OS_X_VERSION_MIN_REQUIRED
|
||||
#define MAC_OS_X_VERSION_MIN_REQUIRED MAC_OS_X_VERSION_10_14
|
||||
#endif
|
||||
#define API_DEPRECATED(...)
|
||||
#define API_AVAILABLE(...)
|
||||
#include <AppKit/NSGraphics.h>
|
||||
#ifndef NSCompositingOperationSourceOver
|
||||
#define NSCompositingOperationSourceOver NSCompositeSourceOver
|
||||
#endif
|
||||
EOF
|
||||
|
||||
LC=libretro-common
|
||||
INC="-I$SHIM -I. -I$LC/include -Ideps -Igfx/include"
|
||||
DEFS="-DRARCH_INTERNAL -DLIBRETRO_STRL_CHECK_OVERLAP -DHAVE_MENU -DHAVE_CONFIGFILE -DHAVE_THREADS -DHAVE_COCOA -DTARGET_OS_OSX=1 -DHAVE_RPNG -DHAVE_RTGA -DHAVE_COMPANION_WIMP -DCOMPANION_TEST_NO_MAIN -DCOMPANION_CORE_TESTING"
|
||||
SAN=""
|
||||
case "${1:-}" in
|
||||
asan) SAN="-fsanitize=address -fno-omit-frame-pointer" ;;
|
||||
"") ;;
|
||||
*) echo "usage: $0 [asan]" >&2; exit 2 ;;
|
||||
esac
|
||||
OBJCFLAGS="$(gnustep-config --objc-flags) -Wall -Wno-unused-parameter -Wno-multichar $SAN"
|
||||
LDFLAGS="$(gnustep-config --gui-libs) -lpthread -lm -lz"
|
||||
|
||||
# C sources shared with the core test (its stubs and fixtures included)
|
||||
CSRCS="ui/companion/companion_core.c \
|
||||
ui/companion/companion_thumbs.c \
|
||||
gfx/gfx_anim_preview.c \
|
||||
ui/companion/test/companion_core_stubs.c \
|
||||
ui/companion/test/companion_core_test.c \
|
||||
playlist.c core_option_manager.c $LC/lists/nested_list.c $LC/file/config_file_io.c \
|
||||
$LC/formats/json/rjson.c $LC/formats/image_texture.c $LC/formats/image_transfer.c \
|
||||
$LC/formats/data_transfer.c $LC/formats/png/rpng.c $LC/formats/png/rpng_apng.c \
|
||||
$LC/formats/png/rpng_encode.c $LC/file/rpng_file.c $LC/formats/tga/rtga.c $LC/memmap/memmap.c $LC/memory/mem_stats.c \
|
||||
$LC/streams/file_stream.c $LC/streams/file_stream_transforms.c $LC/streams/interface_stream.c \
|
||||
$LC/streams/memory_stream.c $LC/streams/rzip_stream.c $LC/streams/trans_stream.c \
|
||||
$LC/streams/trans_stream_pipe.c $LC/streams/trans_stream_zlib.c $LC/streams/trans_stream_deflate.c \
|
||||
$LC/vfs/vfs_implementation.c $LC/file/file_path.c $LC/file/file_path_io.c $LC/file/config_file.c \
|
||||
$LC/file/archive_file.c $LC/file/archive_file_zlib.c $LC/file/retro_dirent.c \
|
||||
$LC/lists/dir_list.c $LC/lists/string_list.c $LC/lists/file_list.c \
|
||||
$LC/string/stdstring.c $LC/string/rstrtod.c $LC/compat/compat_strl.c $LC/compat/compat_strldup.c \
|
||||
$LC/compat/compat_posix_string.c $LC/compat/compat_strcasestr.c $LC/compat/fopen_utf8.c \
|
||||
$LC/encodings/encoding_utf.c $LC/encodings/encoding_crc32.c $LC/encodings/encoding_deflate.c \
|
||||
$LC/hash/lrc_hash.c $LC/time/rtime.c $LC/features/features_cpu.c $LC/rthreads/rthreads.c"
|
||||
|
||||
OBJS=""
|
||||
for f in $CSRCS; do
|
||||
o="$OUT/$(echo "$f" | tr '/' '_').o"
|
||||
$CC -std=gnu99 -O1 -g -w $SAN $INC $DEFS -c "$f" -o "$o"
|
||||
OBJS="$OBJS $o"
|
||||
done
|
||||
$CC -x objective-c -O1 -g $OBJCFLAGS $INC $DEFS -c ui/drivers/ui_cocoa_companion.m -o "$OUT/driver.o"
|
||||
$CC -x objective-c -O1 -g $OBJCFLAGS $INC $DEFS -c ui/companion/test/companion_cocoa_test.m -o "$OUT/test.o"
|
||||
$CC $SAN -o "$OUT/companion_cocoa_test" "$OUT/driver.o" "$OUT/test.o" $OBJS $LDFLAGS -lobjc -lgnustep-base -lgnustep-gui
|
||||
|
||||
# Headless: GNUstep needs a display for AppKit; Xvfb provides one.
|
||||
# GDB=1 runs under gdb and prints a backtrace on a fault.
|
||||
RUN="$OUT/companion_cocoa_test"
|
||||
if [ "${GDB:-0}" = 1 ]; then
|
||||
RUN="gdb -q -batch -ex run -ex bt -ex quit --args $OUT/companion_cocoa_test"
|
||||
fi
|
||||
if command -v xvfb-run >/dev/null 2>&1; then
|
||||
# The status must survive the filter, or a sanitizer abort would
|
||||
# look like a pass (it did: the run that first caught the macOS
|
||||
# heap-buffer-overflow still exited 0 through the pipe).
|
||||
set +e
|
||||
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 \
|
||||
xvfb-run -a -s "-screen 0 1280x800x24" $RUN > "$OUT/run.log" 2>&1
|
||||
st=$?
|
||||
set -e
|
||||
grep -av "autorelease called without pool" "$OUT/run.log"
|
||||
[ "$st" = 0 ] || echo "(harness exited $st)"
|
||||
exit $st
|
||||
|
||||
else
|
||||
$RUN
|
||||
fi
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
# The Cocoa companion harness on a real Mac: the same
|
||||
# ui/companion/test/companion_cocoa_test.m as tools/companion_cocoa_test.sh
|
||||
# runs under GNUstep on Linux, built here with Apple's clang against the
|
||||
# Cocoa framework and run on the machine's window server (GitHub's macOS
|
||||
# runners have one). Apple's AppKit is what decides key-window
|
||||
# arbitration, appearance and the rest; GNUstep only approximates it.
|
||||
#
|
||||
# tools/companion_cocoa_test_macos.sh (on macOS)
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CC=${CC:-clang}
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
LC=libretro-common
|
||||
INC="-I. -I$LC/include -Ideps -Igfx/include"
|
||||
DEFS="-DRARCH_INTERNAL -DHAVE_MENU -DHAVE_CONFIGFILE -DHAVE_THREADS -DHAVE_COCOA -DTARGET_OS_OSX=1 -DHAVE_RPNG -DHAVE_RTGA -DHAVE_COMPANION_WIMP -DCOMPANION_TEST_NO_MAIN -DCOMPANION_CORE_TESTING"
|
||||
|
||||
CSRCS="ui/companion/companion_core.c \
|
||||
ui/companion/companion_thumbs.c \
|
||||
gfx/gfx_anim_preview.c \
|
||||
ui/companion/test/companion_core_stubs.c \
|
||||
ui/companion/test/companion_core_test.c \
|
||||
playlist.c core_option_manager.c $LC/lists/nested_list.c $LC/file/config_file_io.c \
|
||||
$LC/formats/json/rjson.c $LC/formats/image_texture.c $LC/formats/image_transfer.c \
|
||||
$LC/formats/data_transfer.c $LC/formats/png/rpng.c $LC/formats/png/rpng_apng.c \
|
||||
$LC/formats/png/rpng_encode.c $LC/file/rpng_file.c $LC/formats/tga/rtga.c $LC/memmap/memmap.c $LC/memory/mem_stats.c \
|
||||
$LC/streams/file_stream.c $LC/streams/file_stream_transforms.c $LC/streams/interface_stream.c \
|
||||
$LC/streams/memory_stream.c $LC/streams/rzip_stream.c $LC/streams/trans_stream.c \
|
||||
$LC/streams/trans_stream_pipe.c $LC/streams/trans_stream_zlib.c $LC/streams/trans_stream_deflate.c \
|
||||
$LC/vfs/vfs_implementation.c $LC/file/file_path.c $LC/file/file_path_io.c $LC/file/config_file.c \
|
||||
$LC/file/archive_file.c $LC/file/archive_file_zlib.c $LC/file/retro_dirent.c \
|
||||
$LC/lists/dir_list.c $LC/lists/string_list.c $LC/lists/file_list.c \
|
||||
$LC/string/stdstring.c $LC/string/rstrtod.c $LC/compat/compat_strl.c $LC/compat/compat_strldup.c \
|
||||
$LC/compat/compat_posix_string.c $LC/compat/compat_strcasestr.c $LC/compat/fopen_utf8.c \
|
||||
$LC/encodings/encoding_utf.c $LC/encodings/encoding_crc32.c $LC/encodings/encoding_deflate.c \
|
||||
$LC/hash/lrc_hash.c $LC/time/rtime.c $LC/features/features_cpu.c $LC/rthreads/rthreads.c"
|
||||
|
||||
OBJS=""
|
||||
for f in $CSRCS; do
|
||||
o="$OUT/$(echo "$f" | tr '/' '_').o"
|
||||
$CC -std=gnu99 -O1 -g -w $INC $DEFS -c "$f" -o "$o"
|
||||
OBJS="$OBJS $o"
|
||||
done
|
||||
# MRC (no -fobjc-arc): the file supports both; the GNUstep run is MRC too.
|
||||
$CC -x objective-c -O1 -g -w $INC $DEFS -c ui/drivers/ui_cocoa_companion.m -o "$OUT/driver.o"
|
||||
$CC -x objective-c -O1 -g -w $INC $DEFS -c ui/companion/test/companion_cocoa_test.m -o "$OUT/test.o"
|
||||
$CC -o "$OUT/companion_cocoa_test" "$OUT/driver.o" "$OUT/test.o" $OBJS \
|
||||
-framework Cocoa -framework QuartzCore -lz -lpthread -lobjc
|
||||
|
||||
"$OUT/companion_cocoa_test"
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/sh
|
||||
# Build and run the regression test for the desktop companions' shared
|
||||
# core (ui/companion/companion_core.c), on Linux. Links the real core
|
||||
# and playlist.c against libretro-common, with RetroArch's state
|
||||
# replaced by fixtures. Pass "asan" to run under Address+UB sanitizer.
|
||||
#
|
||||
# tools/companion_core_test.sh
|
||||
# tools/companion_core_test.sh asan
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CC=${CC:-gcc}
|
||||
SAN=""
|
||||
case "${1:-}" in
|
||||
asan) SAN="-fsanitize=address,undefined -fno-omit-frame-pointer" ;;
|
||||
tsan) SAN="-fsanitize=thread" ;;
|
||||
"") ;;
|
||||
*) echo "usage: $0 [asan|tsan]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
LC=libretro-common
|
||||
INC="-I. -I$LC/include -Ideps -Igfx/include"
|
||||
DEFS="-DRARCH_INTERNAL -DLIBRETRO_STRL_CHECK_OVERLAP -DHAVE_MENU -DHAVE_CONFIGFILE -DHAVE_THREADS -DHAVE_COMPANION_WIMP -DHAVE_RPNG -DHAVE_RTGA -DCOMPANION_CORE_TESTING"
|
||||
SRCS="ui/companion/companion_core.c \
|
||||
ui/companion/companion_thumbs.c \
|
||||
gfx/gfx_anim_preview.c \
|
||||
$LC/memory/mem_stats.c \
|
||||
$LC/formats/tga/rtga.c \
|
||||
ui/companion/test/companion_core_stubs.c \
|
||||
ui/companion/test/companion_core_test.c \
|
||||
playlist.c \
|
||||
core_option_manager.c \
|
||||
$LC/lists/nested_list.c \
|
||||
$LC/formats/json/rjson.c \
|
||||
$LC/formats/image_texture.c \
|
||||
$LC/formats/image_transfer.c \
|
||||
$LC/formats/data_transfer.c \
|
||||
$LC/formats/png/rpng.c \
|
||||
$LC/formats/png/rpng_apng.c \
|
||||
$LC/formats/png/rpng_encode.c \
|
||||
$LC/file/rpng_file.c \
|
||||
$LC/memmap/memmap.c \
|
||||
$LC/streams/file_stream.c \
|
||||
$LC/streams/file_stream_transforms.c \
|
||||
$LC/streams/interface_stream.c \
|
||||
$LC/streams/memory_stream.c \
|
||||
$LC/streams/rzip_stream.c \
|
||||
$LC/streams/trans_stream.c \
|
||||
$LC/streams/trans_stream_pipe.c \
|
||||
$LC/streams/trans_stream_zlib.c \
|
||||
$LC/streams/trans_stream_deflate.c \
|
||||
$LC/vfs/vfs_implementation.c \
|
||||
$LC/file/file_path.c \
|
||||
$LC/file/file_path_io.c \
|
||||
$LC/file/config_file.c \
|
||||
$LC/file/config_file_io.c \
|
||||
$LC/file/archive_file.c \
|
||||
$LC/file/archive_file_zlib.c \
|
||||
$LC/file/retro_dirent.c \
|
||||
$LC/lists/dir_list.c \
|
||||
$LC/lists/string_list.c \
|
||||
$LC/lists/file_list.c \
|
||||
$LC/string/stdstring.c \
|
||||
$LC/string/rstrtod.c \
|
||||
$LC/compat/compat_strl.c \
|
||||
$LC/compat/compat_strldup.c \
|
||||
$LC/compat/compat_posix_string.c \
|
||||
$LC/compat/compat_strcasestr.c \
|
||||
$LC/compat/fopen_utf8.c \
|
||||
$LC/encodings/encoding_utf.c \
|
||||
$LC/encodings/encoding_crc32.c \
|
||||
$LC/encodings/encoding_deflate.c \
|
||||
$LC/hash/lrc_hash.c \
|
||||
$LC/time/rtime.c \
|
||||
$LC/features/features_cpu.c \
|
||||
$LC/rthreads/rthreads.c"
|
||||
|
||||
$CC -std=gnu99 -O1 -g $SAN -Wall -Wno-unused-parameter -Wno-unused-function \
|
||||
$INC $DEFS $SRCS -o "$OUT/companion_core_test" -lpthread -lm -lz
|
||||
|
||||
"$OUT/companion_core_test"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Feature gap audit: what Qt's companion window offers vs what the
|
||||
Cocoa and Win32 companions implement. Static (regex evidence per
|
||||
feature); pair it with COMPANION_AUDIT=1 tools/companion_cocoa_test.sh,
|
||||
which fires every action the Cocoa controller implements on a real
|
||||
AppKit under GNUstep. Prints the table and the gap lists; exit 0."""
|
||||
import re, sys, os
|
||||
root = os.path.join(os.path.dirname(__file__), '..')
|
||||
qt = open(os.path.join(root, 'ui/drivers/ui_qt.cpp')).read()
|
||||
cc = open(os.path.join(root, 'ui/drivers/ui_cocoa_companion.m')).read()
|
||||
w32 = open(os.path.join(root, 'ui/drivers/ui_win32_companion.c')).read()
|
||||
FEATS = [
|
||||
("File menu: Load Core", r"MENU_FILE_LOAD_CORE", r"MENU_FILE_LOAD_CORE|loadCore:", r"IDM_CW_LOAD_CORE\b"),
|
||||
("File menu: Unload Core", r"MENU_FILE_UNLOAD_CORE", r"unloadCore|UNLOAD_CORE", r"IDM_CW_UNLOAD_CORE"),
|
||||
("File menu: Exit RetroArch", r"MENU_FILE_EXIT", r"quitRetroArch|MENU_FILE_EXIT", r"IDM_CW_QUIT"),
|
||||
("View: Shader Params", r"onShaderParamsClicked", r"showShaderParams", r"IDM_CW_SHADER_PARAMS"),
|
||||
("View: Core Options", r"onCoreOptionsClicked", r"showCoreOptions", r"IDM_CW_CORE_OPTIONS"),
|
||||
("View: Options dialog", r"MENU_VIEW_OPTIONS\b", r"showOptions", r"IDM_CW_OPTIONS\b"),
|
||||
("Help: About", r"MENU_HELP_ABOUT\b", r"aboutRetroArch|orderFrontStandardAboutPanel", r"IDM_CW_HELP_ABOUT\b"),
|
||||
("Help: About contributors", r"CONTRIBUTORS", r"aboutContributors", r"IDM_CW_HELP_CONTRIBUTORS"),
|
||||
("Playlist context: rename", r"renamePlaylist|PLAYLIST_RENAME", r"renamePlaylist", r"IDM_CW_RENAME_PLAYLIST"),
|
||||
("Playlist context: hide/show", r"hidePlaylist|HIDE_PLAYLIST", r"hidePlaylist", r"hide_playlist|HIDE"),
|
||||
("Playlist context: download thumbnails", r"thumbnailPack|DOWNLOAD_THUMBNAILS", r"thumbnail_pack|downloadThumbnails", r"thumbnail_pack|DOWNLOAD_THUMB"),
|
||||
("Entry context: set core association", r"SET_CORE_ASSOCIATION|associat", r"associateCore", r"associate|ASSOCIATION"),
|
||||
("Entry context: delete entry", r"deletePlaylistItem|DELETE_ENTRY|deleteEntry", r"deleteEntry", r"delete_entry|DELETE_ENTRY"),
|
||||
("Drag & drop files onto playlist", r"onPlaylistFilesDropped", r"acceptDrop", r"WM_DROPFILES"),
|
||||
("Drag & drop thumbnail onto boxart", r"onThumbnailDropped", r"installThumbnailFromPath", r"companion_core_thumbnail_install"),
|
||||
("Scan directory", r"onScanDirectoryClicked", r"scanDirectory", r"scan_dir|SCAN"),
|
||||
("Load custom core (file picker)", r"onLoadCustomCoreClicked", r"loadCustomCore|NSOpenPanel", r"cw_load_custom_core"),
|
||||
("Stop content", r"onStopClicked", r"stopContent", r"IDC_CW_STOP_BTN"),
|
||||
("Log dock", r"LogWidget", r"toggleLog", r"IDC_CW_LOG|cw_log"),
|
||||
("Theme (dark)", r"CUSTOM_THEME|setTheme", r"applyTheme", r"theme"),
|
||||
("Search filters entries", r"onSearchLineEditEdited", r"searchChanged", r"IDC_CW_SEARCH"),
|
||||
("Search Enter selects+runs", r"onSearchEnterPressed", r"searchEnter|runSelected", r"cw_search_proc"),
|
||||
("Core Options dialog", r"CoreOptionsDialog", r"optsTable", r"cw_opts_show"),
|
||||
("Shader params dialog", r"ShaderParamsDialog", r"shpTable", r"cw_shp_show"),
|
||||
]
|
||||
print("%-40s %-6s %-6s %s" % ("feature", "qt", "cocoa", "win32"))
|
||||
gc, gw = [], []
|
||||
for f, q, c, w in FEATS:
|
||||
hq, hc, hw = (bool(re.search(x, y)) for x, y in ((q, qt), (c, cc), (w, w32)))
|
||||
print("%-40s %-6s %-6s %s" % (f, "yes" if hq else "-", "yes" if hc else "NO", "yes" if hw else "NO"))
|
||||
if hq and not hc: gc.append(f)
|
||||
if hq and not hw: gw.append(f)
|
||||
print("\nCocoa gaps vs Qt (%d):" % len(gc)); [print(" -", g) for g in gc]
|
||||
print("Win32 gaps vs Qt (%d):" % len(gw)); [print(" -", g) for g in gw]
|
||||
Executable
+239
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that every desktop companion backend exposes the same features.
|
||||
|
||||
The companion UI has three presentation backends over one shared C core
|
||||
(ui/companion/companion_core.*): Qt, native Win32, native Cocoa. The
|
||||
plan for the refactor requires them to be interchangeable - "same
|
||||
actions produce the same results" - but nothing enforces it: a backend
|
||||
can quietly omit a feature (no file browser, no scan, no core picker)
|
||||
and still compile and run, and the omission only surfaces when a user
|
||||
switches drivers and finds a menu entry gone. That is exactly the drift
|
||||
this check exists to catch, the same way vfs_backend_parity.py catches a
|
||||
VFS backend that skips a call.
|
||||
|
||||
Each feature below is defined by the companion-core entry point a
|
||||
backend must call to implement it. A backend "has" the feature when it
|
||||
references that symbol. Qt's presentation is split across two
|
||||
translation units (ui_qt.cpp + ui_qt_widgets.cpp), so they are checked
|
||||
together as one backend.
|
||||
|
||||
This is a source-level grep, so it costs nothing and runs on Linux. It
|
||||
is deliberately loose - it proves a backend wired the feature's core
|
||||
call in at all, not that the UI is pixel-identical; visual parity is a
|
||||
manual pass. What it reliably catches is a whole feature missing from
|
||||
one backend.
|
||||
|
||||
A backend that genuinely should not carry a feature must be listed in
|
||||
WAIVERS with a reason, so dropping one is a conscious, reviewed choice
|
||||
rather than an oversight.
|
||||
|
||||
Usage:
|
||||
tools/companion_parity.py # check, exit 1 on any gap
|
||||
tools/companion_parity.py --list # print the feature matrix
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
# backend name -> the translation unit(s) that make up its presentation
|
||||
BACKENDS = {
|
||||
'qt': ['ui/drivers/ui_qt.cpp', 'ui/drivers/ui_qt_widgets.cpp'],
|
||||
'win32': ['ui/drivers/ui_win32_companion.c'],
|
||||
'cocoa': ['ui/drivers/ui_cocoa_companion.m'],
|
||||
}
|
||||
|
||||
# feature -> the companion_core_* symbol whose use implements it.
|
||||
# One representative call per feature: the one a backend cannot skip and
|
||||
# still offer it.
|
||||
FEATURES = {
|
||||
'lifecycle': 'companion_core_new',
|
||||
'iterate': 'companion_core_iterate',
|
||||
'playlist list': 'companion_core_playlist_count',
|
||||
'playlist entries': 'companion_core_entry_count',
|
||||
'select playlist': 'companion_core_select_playlist',
|
||||
'run entry': 'companion_core_request_load_entry',
|
||||
'load content': 'companion_core_request_load_content',
|
||||
'start core': 'companion_core_start_core',
|
||||
'load core': 'companion_core_load_core',
|
||||
'installed cores': 'companion_core_installed_core_count',
|
||||
'core picker filter': 'companion_core_installed_cores_supporting',
|
||||
'core association': 'companion_core_playlist_set_default_core',
|
||||
'delete entry': 'companion_core_playlist_delete_entry',
|
||||
'core info panel': 'companion_core_core_info_rows',
|
||||
'directory scan': 'companion_core_request_scan',
|
||||
'thumbnails': 'companion_core_thumbnail_path',
|
||||
# Decoding goes through the shared engine (threads, cache, visible-
|
||||
# first requests); a backend decoding on its own UI thread regresses
|
||||
# the grid to the "shows on click" behaviour this replaced.
|
||||
'thumbnail engine': 'companion_thumbs_request',
|
||||
'thumbnail poll': 'companion_thumbs_poll',
|
||||
'two-pane browser': 'companion_core_browse_dir_count',
|
||||
# The listing is enumerated off the UI thread; a backend that asks
|
||||
# whether it is still busy is one that waits for the callback rather
|
||||
# than rebuilding straight after open() (or enumerating itself).
|
||||
'async browser': 'companion_core_browse_busy',
|
||||
'browser sorting': 'companion_core_browse_sort',
|
||||
# The selected file's animation (APNG / WEBP / WEBM / MP4) plays in
|
||||
# the pane on every backend, as in RetroArch's own File Browser.
|
||||
'animated preview': 'companion_thumbs_animate',
|
||||
# Qt's Stop / File > Unload Core, and File > Exit
|
||||
'unload core': 'companion_core_unload_core',
|
||||
'exit retroarch': 'CMD_EVENT_QUIT',
|
||||
# Help > About Contributors: the AUTHORS list in a window
|
||||
'about contributors': 'retroarch_contributors_list',
|
||||
# Qt's playlist rename, "Add Files" / file drop, thumbnail drop
|
||||
'rename playlist': 'companion_core_playlist_rename',
|
||||
'add files': 'companion_core_playlist_add_files',
|
||||
'thumbnail drop': 'companion_core_thumbnail_install',
|
||||
# Qt's View > Core Options, View > Shader Parameters, View > Options
|
||||
'core options': 'companion_core_option_count',
|
||||
'shader parameters': 'companion_core_shader_param_count',
|
||||
'options dialog': 'companion_core_setting_count',
|
||||
'file browser': 'companion_core_browse_open',
|
||||
'pick core on run': 'companion_core_entry_needs_core',
|
||||
'window hand-off': 'companion_core_prepare_show_window',
|
||||
}
|
||||
|
||||
# (backend, feature) pairs that are intentionally absent, with a reason.
|
||||
# Empty for now: all three backends implement every feature above.
|
||||
WAIVERS = {
|
||||
# Qt's "Add Files" is its own dialog (name, extension filter, core
|
||||
# choice, archive filter, progress) over companion_core_playlist_push;
|
||||
# the natives' plain add-files call is the drop semantics only.
|
||||
('qt', 'add files'): 'Qt: PlaylistEntryDialog over companion_core_playlist_push',
|
||||
# Qt's thumbnail drop receives image data (a QImage from any drag
|
||||
# source), not a file path, and saves through QImage; the core call
|
||||
# takes a path.
|
||||
('qt', 'thumbnail drop'): 'Qt: QImage drop saved via changeThumbnail',
|
||||
# Qt's three dialogs predate the core and read RetroArch directly
|
||||
# (CoreOptionsDialog, ShaderParamsDialog, ViewOptionsWidget).
|
||||
('qt', 'core options'): 'Qt: CoreOptionsDialog reads runloop core_options directly',
|
||||
('qt', 'shader parameters'): 'Qt: ShaderParamsDialog reads menu_shader_get directly',
|
||||
('qt', 'options dialog'): 'Qt: ViewOptionsWidget edits settings directly',
|
||||
# ('qt', 'window hand-off'): 'Qt calls it under its own guard',
|
||||
}
|
||||
|
||||
# Qt reaches the window hand-off through the same call from ui_qt.cpp;
|
||||
# the playlist-file browser accessors it does not use because its model
|
||||
# owns the QFileSystemModel. Rather than hard-code such exceptions, a
|
||||
# feature a backend implements a different but equivalent way is waived
|
||||
# above with a reason; keep that list short and reviewed.
|
||||
QT_EQUIVALENT = {
|
||||
# Qt lists playlist files through its own QDir model (getPlaylistFiles)
|
||||
# and selects by path, not by the core's playlist-file index.
|
||||
'playlist list': 'companion_core_select_playlist_path',
|
||||
'select playlist': 'companion_core_select_playlist_path',
|
||||
# Qt's "Run" and its core picker go through the launch-with combo:
|
||||
# it resolves the core (companion_core_launch_options) and loads the
|
||||
# content directly, rather than the natives' request_load_entry /
|
||||
# entry_needs_core path.
|
||||
'run entry': 'companion_core_launch_options',
|
||||
'pick core on run': 'companion_core_launch_options',
|
||||
# Qt runs the file browser through QFileSystemModel; it has no
|
||||
# in-core browse listing, and so no folder / file split either.
|
||||
'file browser': None,
|
||||
'two-pane browser': 'BrowseTableModel',
|
||||
# (Qt now draws through the same engine as the natives: no waiver.)
|
||||
}
|
||||
|
||||
|
||||
def uses(paths, symbol):
|
||||
pat = re.compile(r'\b' + re.escape(symbol) + r'\b')
|
||||
for p in paths:
|
||||
try:
|
||||
with open(p, encoding='utf-8', errors='replace') as f:
|
||||
if pat.search(f.read()):
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def has_feature(backend, feature):
|
||||
sym = FEATURES[feature]
|
||||
if uses(BACKENDS[backend], sym):
|
||||
return True
|
||||
if backend == 'qt' and feature in QT_EQUIVALENT:
|
||||
alt = QT_EQUIVALENT[feature]
|
||||
if alt is None:
|
||||
return True # implemented a toolkit-native way
|
||||
return uses(BACKENDS['qt'], alt)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
want_list = '--list' in sys.argv[1:]
|
||||
|
||||
if want_list:
|
||||
w = max(len(f) for f in FEATURES)
|
||||
print('%-*s %s' % (w, 'feature', ' '.join(BACKENDS)))
|
||||
for feat in FEATURES:
|
||||
cells = []
|
||||
for b in BACKENDS:
|
||||
if (b, feat) in WAIVERS:
|
||||
cells.append('waived')
|
||||
else:
|
||||
cells.append('yes' if has_feature(b, feat) else 'NO')
|
||||
print('%-*s %s' % (w, feat, ' '.join(
|
||||
'%-*s' % (len(b), c) for b, c in zip(BACKENDS, cells))))
|
||||
return 0
|
||||
|
||||
gaps = []
|
||||
for feat in FEATURES:
|
||||
for b in BACKENDS:
|
||||
if (b, feat) in WAIVERS:
|
||||
continue
|
||||
if not has_feature(b, feat):
|
||||
gaps.append((b, feat))
|
||||
|
||||
if gaps:
|
||||
print('error: companion backends disagree on features:',
|
||||
file=sys.stderr)
|
||||
for b, feat in gaps:
|
||||
print(' %-6s is missing "%s" (expected a call to %s)'
|
||||
% (b, feat, FEATURES[feat]), file=sys.stderr)
|
||||
print('\nEither wire the feature into that backend, or - if it '
|
||||
'genuinely should not carry it - add the (backend, feature) '
|
||||
'pair to WAIVERS in this script with a reason.',
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if check_main_loops():
|
||||
return 1
|
||||
print('companion parity: %d features, all %d backends agree'
|
||||
% (len(FEATURES), len(BACKENDS)))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
|
||||
# --- the platforms' real main loops call the companion's iterate -----
|
||||
# Every companion is asynchronous on the far side of
|
||||
# ui_companion_driver_wimp_iterate(): the playlist parse, the browser
|
||||
# listing, thumbnails, animation frames, status. A platform main loop
|
||||
# that does not call it shows "Loading playlist..." for ever. The
|
||||
# generic loop (retroarch.c) covers Win32 and Qt; the non-Qt Mac build's
|
||||
# main loop is the CFRunLoop observer in cocoa_common.m, which once
|
||||
# lacked the call (the Qt-era rarch_main had it, under #ifdef HAVE_QT).
|
||||
def _loop_calls_iterate(path, func):
|
||||
src = open(path).read()
|
||||
m = re.search(r'static void %s\b.*?\n\}\n' % re.escape(func), src, re.S)
|
||||
body = m.group(0) if m else ''
|
||||
return 'ui_companion_driver_wimp_iterate()' in body
|
||||
|
||||
|
||||
def check_main_loops():
|
||||
_loops = [
|
||||
('ui/drivers/cocoa/cocoa_common.m', 'rarch_draw_observer'),
|
||||
]
|
||||
_missing = [f for p, f in _loops if not _loop_calls_iterate(p, f)]
|
||||
if _missing:
|
||||
print("error: main loop(s) that never call ui_companion_driver_wimp_iterate(): %s" % ', '.join(_missing), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("main loops iterate the companion: ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
# Build and run the regression test for the desktop companions' shared
|
||||
# thumbnail engine (ui/companion/companion_thumbs.c) against the real
|
||||
# decoder, on Linux. Pass "tsan" or "asan" to run under that sanitizer.
|
||||
#
|
||||
# tools/companion_thumbs_test.sh # plain
|
||||
# tools/companion_thumbs_test.sh tsan # ThreadSanitizer
|
||||
# tools/companion_thumbs_test.sh asan # Address+UB sanitizer
|
||||
#
|
||||
# Also compiles the engine once as C89 (-ansi -pedantic, no threads) to
|
||||
# keep it honest for the MSVC / 9x builds.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CC=${CC:-gcc}
|
||||
SAN=""
|
||||
case "${1:-}" in
|
||||
tsan) SAN="-fsanitize=thread" ;;
|
||||
asan) SAN="-fsanitize=address,undefined -fno-omit-frame-pointer" ;;
|
||||
"") ;;
|
||||
*) echo "usage: $0 [tsan|asan]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
LC=libretro-common
|
||||
INC="-I. -I$LC/include"
|
||||
DEFS="-DRARCH_INTERNAL -DLIBRETRO_STRL_CHECK_OVERLAP -DHAVE_THREADS -DHAVE_RTGA -DHAVE_RPNG"
|
||||
SRCS="ui/companion/companion_thumbs.c \
|
||||
gfx/gfx_anim_preview.c \
|
||||
$LC/memory/mem_stats.c \
|
||||
ui/companion/test/companion_thumbs_test.c \
|
||||
$LC/formats/image_texture.c \
|
||||
$LC/formats/image_transfer.c \
|
||||
$LC/formats/data_transfer.c \
|
||||
$LC/memmap/memmap.c \
|
||||
$LC/formats/tga/rtga.c \
|
||||
$LC/formats/png/rpng.c \
|
||||
$LC/formats/png/rpng_apng.c \
|
||||
$LC/streams/trans_stream.c \
|
||||
$LC/streams/trans_stream_pipe.c \
|
||||
$LC/streams/trans_stream_zlib.c \
|
||||
$LC/streams/trans_stream_deflate.c \
|
||||
$LC/encodings/encoding_deflate.c \
|
||||
$LC/encodings/encoding_crc32.c \
|
||||
$LC/streams/file_stream.c \
|
||||
$LC/streams/interface_stream.c \
|
||||
$LC/streams/memory_stream.c \
|
||||
$LC/streams/rzip_stream.c \
|
||||
$LC/vfs/vfs_implementation.c \
|
||||
$LC/file/file_path.c \
|
||||
$LC/file/file_path_io.c \
|
||||
$LC/string/stdstring.c \
|
||||
$LC/string/rstrtod.c \
|
||||
$LC/compat/compat_strl.c \
|
||||
$LC/compat/compat_strldup.c \
|
||||
$LC/compat/compat_posix_string.c \
|
||||
$LC/compat/fopen_utf8.c \
|
||||
$LC/encodings/encoding_utf.c \
|
||||
$LC/time/rtime.c \
|
||||
$LC/features/features_cpu.c \
|
||||
$LC/rthreads/rthreads.c"
|
||||
|
||||
# C89 honesty pass on the engine itself (no threads).
|
||||
# (_GNU_SOURCE as in the real C89 gate: strict -ansi hides POSIX types
|
||||
# such as struct timespec that libretro-common's headers rely on.)
|
||||
$CC -std=c89 -ansi -pedantic -Werror=pedantic -Wno-long-long -Wall -Wextra \
|
||||
-Wno-unused-parameter -Werror=declaration-after-statement -D_GNU_SOURCE \
|
||||
$INC -DRARCH_INTERNAL -DHAVE_RPNG -c ui/companion/companion_thumbs.c -o "$OUT/c89.o"
|
||||
|
||||
$CC -std=gnu99 -O1 -g $SAN -Wall -Wextra -Wno-unused-parameter \
|
||||
$INC $DEFS $SRCS -o "$OUT/companion_thumbs_test" -lpthread -lm -lz
|
||||
|
||||
"$OUT/companion_thumbs_test" "$OUT"
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/sh
|
||||
# Regression harness for the Win32 companion, on Linux: the real
|
||||
# ui/drivers/ui_win32_companion.c compiled with mingw and linked with
|
||||
# the real companion core, the core test's stubs and fixtures and a stub
|
||||
# for RetroArch's main window - run under Wine on Xvfb. See
|
||||
# companion_win32_test.c for what it asserts.
|
||||
#
|
||||
# Needs: gcc-mingw-w64-x86-64, wine, xvfb (Debian/Ubuntu:
|
||||
# apt-get install gcc-mingw-w64-x86-64 wine xvfb)
|
||||
#
|
||||
# tools/companion_win32_test.sh
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
CC=${CC:-x86_64-w64-mingw32-gcc}
|
||||
WINE=${WINE:-wine}
|
||||
command -v "$CC" >/dev/null 2>&1 || { echo "$CC not found" >&2; exit 2; }
|
||||
command -v "$WINE" >/dev/null 2>&1 || { echo "$WINE not found" >&2; exit 2; }
|
||||
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$OUT"' EXIT
|
||||
|
||||
LC=libretro-common
|
||||
INC="-I. -I$LC/include -Ideps -Igfx/include"
|
||||
DEFS="-DRARCH_INTERNAL -DLIBRETRO_STRL_CHECK_OVERLAP -DHAVE_MENU -DHAVE_CONFIGFILE -DHAVE_THREADS -DHAVE_RPNG -DHAVE_RTGA -DHAVE_COMPANION_WIMP -DCOMPANION_TEST_NO_MAIN -DCOMPANION_CORE_TESTING"
|
||||
|
||||
CSRCS="ui/companion/companion_core.c \
|
||||
ui/companion/companion_thumbs.c \
|
||||
gfx/gfx_anim_preview.c \
|
||||
ui/companion/test/companion_core_stubs.c \
|
||||
ui/companion/test/companion_core_test.c \
|
||||
playlist.c core_option_manager.c $LC/lists/nested_list.c $LC/file/config_file_io.c \
|
||||
$LC/formats/json/rjson.c $LC/formats/image_texture.c $LC/formats/image_transfer.c \
|
||||
$LC/formats/data_transfer.c $LC/formats/png/rpng.c $LC/formats/png/rpng_apng.c \
|
||||
$LC/formats/png/rpng_encode.c $LC/file/rpng_file.c $LC/formats/tga/rtga.c $LC/memmap/memmap.c $LC/memory/mem_stats.c \
|
||||
$LC/streams/file_stream.c $LC/streams/file_stream_transforms.c $LC/streams/interface_stream.c \
|
||||
$LC/streams/memory_stream.c $LC/streams/rzip_stream.c $LC/streams/trans_stream.c \
|
||||
$LC/streams/trans_stream_pipe.c $LC/streams/trans_stream_deflate.c \
|
||||
$LC/vfs/vfs_implementation.c $LC/file/file_path.c $LC/file/file_path_io.c $LC/file/config_file.c \
|
||||
$LC/file/archive_file.c $LC/file/archive_file_zlib.c $LC/file/retro_dirent.c \
|
||||
$LC/lists/dir_list.c $LC/lists/string_list.c $LC/lists/file_list.c \
|
||||
$LC/string/stdstring.c $LC/string/rstrtod.c $LC/compat/compat_strl.c $LC/compat/compat_strldup.c \
|
||||
$LC/compat/compat_posix_string.c $LC/compat/compat_strcasestr.c $LC/compat/fopen_utf8.c \
|
||||
$LC/encodings/encoding_utf.c $LC/encodings/encoding_crc32.c $LC/encodings/encoding_deflate.c \
|
||||
$LC/hash/lrc_hash.c $LC/time/rtime.c $LC/features/features_cpu.c $LC/rthreads/rthreads.c"
|
||||
|
||||
OBJS=""
|
||||
for f in $CSRCS ui/drivers/ui_win32_companion.c ui/companion/test/companion_win32_test.c; do
|
||||
o="$OUT/$(echo "$f" | tr '/' '_').o"
|
||||
$CC -std=gnu99 -O1 -g -w $INC $DEFS -c "$f" -o "$o"
|
||||
OBJS="$OBJS $o"
|
||||
done
|
||||
$CC -o "$OUT/companion_win32_test.exe" $OBJS -lcomctl32 -lcomdlg32 -lshell32 -lole32 -lgdi32 -luser32 -lws2_32 -lpthread
|
||||
|
||||
# Headless: Wine needs a display; Xvfb provides one. Wine's own noise
|
||||
# on stderr is dropped; the harness prints to stdout.
|
||||
export WINEDEBUG=-all
|
||||
if command -v xvfb-run >/dev/null 2>&1; then
|
||||
set +e
|
||||
xvfb-run -a -s "-screen 0 1280x800x24" "$WINE" "$OUT/companion_win32_test.exe" > "$OUT/run.log" 2>"$OUT/err.log"
|
||||
st=$?
|
||||
set -e
|
||||
tr -d '\r' < "$OUT/run.log"
|
||||
[ "$st" = 0 ] || { echo "(wine exit $st)"; grep -av "^wine:\|^00[0-9a-f]*:" "$OUT/err.log" | tail -5; }
|
||||
exit $st
|
||||
else
|
||||
"$WINE" "$OUT/companion_win32_test.exe"
|
||||
fi
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AUTHORS.h is a C++11 raw string (Qt reads it). The native companions
|
||||
are C / Objective-C, so this writes AUTHORS_c.h: the same list as a
|
||||
plain C string literal. Run after editing AUTHORS.h; CI checks the two
|
||||
agree."""
|
||||
import os, sys
|
||||
root = os.path.join(os.path.dirname(__file__), '..')
|
||||
src = open(os.path.join(root, 'AUTHORS.h'), encoding='utf-8').read()
|
||||
start = src.index('R"(') + 3
|
||||
end = src.rindex(')"')
|
||||
body = src[start:end]
|
||||
lines = body.split('\n')
|
||||
out = ['/* Generated from AUTHORS.h by tools/gen_authors_c.py - do not edit. */',
|
||||
'#ifndef __RETROARCH_AUTHORS_C_H', '#define __RETROARCH_AUTHORS_C_H',
|
||||
'static const char *retroarch_contributors_list =']
|
||||
for l in lines:
|
||||
esc = l.replace('\\', '\\\\').replace('"', '\\"')
|
||||
out.append(' "%s\\n"' % esc)
|
||||
out.append(' ;')
|
||||
out.append('#endif')
|
||||
text = '\n'.join(out) + '\n'
|
||||
dst = os.path.join(root, 'AUTHORS_c.h')
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--check':
|
||||
cur = open(dst, encoding='utf-8').read() if os.path.exists(dst) else ''
|
||||
if cur != text:
|
||||
print('AUTHORS_c.h is out of date: run tools/gen_authors_c.py'); sys.exit(1)
|
||||
print('AUTHORS_c.h up to date'); sys.exit(0)
|
||||
open(dst, 'w', encoding='utf-8').write(text)
|
||||
print('wrote AUTHORS_c.h (%d lines)' % len(lines))
|
||||
@@ -39,8 +39,10 @@ if [ "$1" = "--consumers-of" ]; then
|
||||
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/' | sed 's#^\./##')"
|
||||
| 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; }
|
||||
@@ -70,10 +72,13 @@ C89FLAGS="-fsyntax-only -std=c89 -ansi -pedantic -Werror=pedantic \
|
||||
fail=0; n=0
|
||||
for f in $FILES; do
|
||||
n=$((n+1))
|
||||
# Only real errors, not warnings; and not "file not found" for
|
||||
# optional platform headers this box does not have.
|
||||
# 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 'No such file|file not found' | head -3)
|
||||
| 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
|
||||
@@ -83,7 +88,7 @@ for f in $FILES; do
|
||||
*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 'No such file|file not found' | head -3)
|
||||
| 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
|
||||
|
||||
@@ -1444,6 +1444,11 @@ bool input_sensors_enable 1 1
|
||||
bool ui_companion_enable 1 0
|
||||
bool ui_companion_start_on_boot 1 1
|
||||
bool desktop_menu_enable 1 1
|
||||
bool desktop_menu_suggest_loaded_core_first 1 0
|
||||
bool desktop_menu_save_last_tab 1 0
|
||||
bool desktop_menu_save_geometry 1 0
|
||||
bool desktop_menu_show_welcome_screen 1 1
|
||||
bool desktop_menu_scan_finish_confirm 1 1
|
||||
bool ai_service_enable 1 0
|
||||
bool ai_service_pause 1 0
|
||||
bool video_scale_integer 1 0
|
||||
@@ -1784,6 +1789,20 @@ uint crt_switch_resolution 1 CRT_SWITCH_NONE
|
||||
uint crt_switch_resolution_super 1 2560
|
||||
uint video_sdl_display_server 1 VIDEO_SDL_DISPLAY_SERVER_AUTO
|
||||
uint menu_scroll_delay 1 256
|
||||
uint desktop_menu_view_type 1 0
|
||||
uint desktop_menu_thumbnail_type 1 0
|
||||
uint desktop_menu_last_tab 1 0
|
||||
uint desktop_menu_thumbnail_cache_limit 1 500
|
||||
uint desktop_menu_thumbnail_max_size 1 0
|
||||
uint desktop_menu_thumbnail_quality 1 0
|
||||
uint desktop_menu_icon_view_zoom 1 50
|
||||
uint desktop_menu_all_playlists_list_max_count 1 0
|
||||
uint desktop_menu_all_playlists_grid_max_count 1 0
|
||||
uint desktop_menu_theme 1 0
|
||||
uint desktop_menu_window_x 1 0
|
||||
uint desktop_menu_window_y 1 0
|
||||
uint desktop_menu_window_width 1 0
|
||||
uint desktop_menu_window_height 1 0
|
||||
uint ai_service_source_lang 1 0
|
||||
uint ai_service_target_lang 1 0
|
||||
uint video_scale_integer_axis 1 0
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,582 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2011-2017 - Daniel De Matteis
|
||||
* Copyright (C) 2026 - libretro team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Shared companion (desktop / WIMP) UI core.
|
||||
*
|
||||
* Toolkit-agnostic model and command layer shared by the Qt, Win32 and
|
||||
* Cocoa companion drivers. Contains no toolkit types and never blocks
|
||||
* the caller: anything that can take more than a couple of milliseconds
|
||||
* is either driven in budgeted steps from companion_core_iterate() or
|
||||
* posted to the RetroArch task queue, with completion reported through
|
||||
* the callback table supplied at creation time.
|
||||
*
|
||||
* Presentation backends own only windows, controls, layout and event
|
||||
* wiring. They read model state through the accessors below and issue
|
||||
* requests through the request functions; they never own the data. */
|
||||
|
||||
#ifndef __UI_COMPANION_CORE_H
|
||||
#define __UI_COMPANION_CORE_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <retro_common_api.h>
|
||||
|
||||
#include "../../command.h"
|
||||
#include "../../playlist.h"
|
||||
|
||||
RETRO_BEGIN_DECLS
|
||||
|
||||
typedef struct companion_core companion_core_t;
|
||||
|
||||
enum companion_download_result
|
||||
{
|
||||
COMPANION_DL_OK = 0,
|
||||
COMPANION_DL_ERR_NETWORK, /* HTTP failed / non-200 */
|
||||
COMPANION_DL_ERR_WRITE, /* could not write the file */
|
||||
COMPANION_DL_ERR_RENAME, /* could not rename .partial */
|
||||
COMPANION_DL_ERR_ARCHIVE_EMPTY, /* pack held no files */
|
||||
COMPANION_DL_ERR_DELETE, /* could not replace an existing file */
|
||||
COMPANION_DL_ERR_EXTRACT /* decompression failed */
|
||||
};
|
||||
|
||||
/* All callbacks are invoked on the main (UI) thread, from within the
|
||||
* request function that caused them or from companion_core_iterate().
|
||||
* Every member may be NULL. */
|
||||
typedef struct companion_callbacks
|
||||
{
|
||||
/* The set of available playlist files changed
|
||||
* (companion_core_playlist_count / _name / _path). */
|
||||
void (*on_playlists_changed)(void *ud);
|
||||
/* The selected playlist finished (re)loading, or was cleared
|
||||
* (companion_core_entry_count / _entry). */
|
||||
void (*on_playlist_changed)(void *ud);
|
||||
/* A status-bar message from RetroArch's message queue. */
|
||||
void (*on_status_message)(void *ud, const char *msg,
|
||||
unsigned prio, unsigned duration, bool flush);
|
||||
/* A log line RetroArch wants shown in the companion's log view. */
|
||||
void (*on_log_message)(void *ud, const char *msg);
|
||||
/* RetroArch asked the companion to refresh whatever it shows. */
|
||||
void (*on_notify_refresh)(void *ud);
|
||||
/* A scan started with companion_core_request_scan() finished; the
|
||||
* playlist files may have changed. */
|
||||
void (*on_scan_finished)(void *ud);
|
||||
/* A single thumbnail download finished. @path is the file written
|
||||
* (NULL on failure). */
|
||||
void (*on_thumbnail_downloaded)(void *ud, const char *db_name,
|
||||
const char *label, const char *subdir, const char *path,
|
||||
bool success);
|
||||
/* A thumbnail pack download + extraction finished. */
|
||||
void (*on_thumbnail_pack_finished)(void *ud,
|
||||
enum companion_download_result result);
|
||||
/* A companion_core_browse_open() finished: the listing is in place.
|
||||
* Fired from companion_core_iterate() on the UI thread. (Last, so
|
||||
* existing positional tables stay valid; NULL if unused.) */
|
||||
void (*on_browse_changed)(void *ud);
|
||||
} companion_callbacks_t;
|
||||
|
||||
/* Lifecycle */
|
||||
companion_core_t *companion_core_new(const companion_callbacks_t *cb,
|
||||
void *ud);
|
||||
/* Change the user pointer handed to the callbacks (a backend that
|
||||
* creates the core before the object its callbacks act on). */
|
||||
void companion_core_set_ud(companion_core_t *core, void *ud);
|
||||
void companion_core_free(companion_core_t *core);
|
||||
|
||||
/* Advance pending budgeted work (playlist parse, ...) for at most
|
||||
* @budget_us microseconds. Call from the backend's iterate / idle
|
||||
* path. Never blocks past the budget. */
|
||||
void companion_core_iterate(companion_core_t *core, unsigned budget_us);
|
||||
|
||||
/* "All Playlists": the first playlist entry every companion lists. Not
|
||||
* a file - selecting it loads every playlist file in turn (under the
|
||||
* same per-frame budget) and presents the union, sorted by label, capped
|
||||
* by desktop_menu_all_playlists_list/grid_max_count (0 = no cap). Its
|
||||
* companion_core_playlist_path() is this token, as the Qt companion has
|
||||
* always used, so desktop_menu_initial_playlist can name it. */
|
||||
#define COMPANION_ALL_PLAYLISTS_TOKEN "|||ALL|||"
|
||||
|
||||
/* The playlist file entry @i of the current view belongs to: the selected
|
||||
* playlist normally, or, under All Playlists, the file that entry came
|
||||
* from. Edits (delete, associate core) must target this, never the All
|
||||
* token. NULL when unknown. */
|
||||
const char *companion_core_entry_playlist_path(companion_core_t *core,
|
||||
size_t i);
|
||||
/* Entry @i's index within that file (== @i except under All Playlists);
|
||||
* (size_t)-1 when unknown. Pair with the path above for edits. */
|
||||
size_t companion_core_entry_index_in_playlist(companion_core_t *core, size_t i);
|
||||
|
||||
/* --- Playlist files ------------------------------------------------ */
|
||||
|
||||
/* Rescan the playlist directory. Cheap (directory listing only),
|
||||
* completes synchronously and fires on_playlists_changed. */
|
||||
void companion_core_refresh_playlists(companion_core_t *core);
|
||||
size_t companion_core_playlist_count(companion_core_t *core);
|
||||
/* Display name: file name without the .lpl extension. */
|
||||
const char *companion_core_playlist_name(companion_core_t *core, size_t i);
|
||||
const char *companion_core_playlist_path(companion_core_t *core, size_t i);
|
||||
|
||||
/* --- Selected playlist --------------------------------------------- */
|
||||
|
||||
/* Start loading playlist @i. Parsing is spread over
|
||||
* companion_core_iterate() calls; on_playlist_changed fires when the
|
||||
* entries are available. Selecting while a load is pending aborts the
|
||||
* pending load. Returns false if @i is out of range. */
|
||||
bool companion_core_select_playlist(companion_core_t *core, size_t i);
|
||||
/* Same as companion_core_select_playlist() for an arbitrary playlist
|
||||
* file @path (it need not be in the playlist directory). The selected
|
||||
* index becomes the matching entry of the file list, or (size_t)-1. */
|
||||
bool companion_core_select_playlist_path(companion_core_t *core,
|
||||
const char *path);
|
||||
/* Index of the selected playlist, or (size_t)-1 if none. */
|
||||
size_t companion_core_selected_playlist(companion_core_t *core);
|
||||
/* Path of the playlist currently selected / loading ("" if none). */
|
||||
const char *companion_core_selected_playlist_path(companion_core_t *core);
|
||||
bool companion_core_playlist_loading(companion_core_t *core);
|
||||
size_t companion_core_entry_count(companion_core_t *core);
|
||||
const struct playlist_entry *companion_core_entry(companion_core_t *core,
|
||||
size_t i);
|
||||
|
||||
/* --- File-system browser ------------------------------------------- */
|
||||
|
||||
/* Open @path as the browse directory (NULL / empty -> the configured
|
||||
* default content directory, else the filesystem root). Lists it
|
||||
* (directories first, then files), replacing the previous listing.
|
||||
* Cheap - a single directory read. Returns false only on a listing
|
||||
* error, in which case the previous listing is kept. */
|
||||
bool companion_core_browse_open(companion_core_t *core, const char *path);
|
||||
/* The directory currently listed ("" before the first open). */
|
||||
const char *companion_core_browse_dir(companion_core_t *core);
|
||||
size_t companion_core_browse_count(companion_core_t *core);
|
||||
/* Whether a browse_open() is still enumerating (show "Loading..."). The
|
||||
* previous listing stays readable until the new one lands. */
|
||||
bool companion_core_browse_busy(companion_core_t *core);
|
||||
/* Size (bytes; 0 for directories) and modification time (seconds since
|
||||
* the epoch; 0 unknown) of entry @i, gathered with the listing off the
|
||||
* UI thread so a view never touches the disk to paint. */
|
||||
uint64_t companion_core_browse_size(companion_core_t *core, size_t i);
|
||||
int64_t companion_core_browse_mtime(companion_core_t *core, size_t i);
|
||||
/* The browser table's columns as the Qt companion shows them, formatted
|
||||
* once here for every backend: Size ("12 KB", "1.5 MB", "" for a
|
||||
* folder), Type ("Drive", "File Folder", "ZIP File"), Date Modified
|
||||
* (local time, "YYYY-MM-DD HH:MM"). Each writes into @s and returns it. */
|
||||
const char *companion_core_browse_size_str(companion_core_t *core, size_t i,
|
||||
char *s, size_t len);
|
||||
const char *companion_core_browse_type_str(companion_core_t *core, size_t i,
|
||||
char *s, size_t len);
|
||||
const char *companion_core_browse_date_str(companion_core_t *core, size_t i,
|
||||
char *s, size_t len);
|
||||
/* The listing is directories first (".." included), then files: indices
|
||||
* [0, dir_count) are directories, [dir_count, count) files. A Qt-style
|
||||
* browser shows the first range in its folder pane and the second as
|
||||
* the selected folder's content. */
|
||||
size_t companion_core_browse_dir_count(companion_core_t *core);
|
||||
/* Go to the parent directory (Qt's "Up"); false at the top. */
|
||||
bool companion_core_browse_up(companion_core_t *core);
|
||||
|
||||
/* Sort the listing by a column, as clicking the browser's header does
|
||||
* in every companion: folders always first (and ".." first of all),
|
||||
* then the files by name (case-insensitive), size, type (extension) or
|
||||
* modification time, ascending or not. Applies to the current listing
|
||||
* at once (on_browse_changed fires) and to every listing after it. */
|
||||
enum companion_browse_column
|
||||
{
|
||||
COMPANION_BROWSE_SORT_NAME = 0,
|
||||
COMPANION_BROWSE_SORT_SIZE,
|
||||
COMPANION_BROWSE_SORT_TYPE,
|
||||
COMPANION_BROWSE_SORT_DATE
|
||||
};
|
||||
void companion_core_browse_sort(companion_core_t *core,
|
||||
enum companion_browse_column column, bool ascending);
|
||||
enum companion_browse_column companion_core_browse_sort_column(companion_core_t *core);
|
||||
bool companion_core_browse_sort_ascending(companion_core_t *core);
|
||||
/* Display name of entry @i (base name; ".." for the parent link). */
|
||||
const char *companion_core_browse_name(companion_core_t *core, size_t i);
|
||||
/* Full path of entry @i. */
|
||||
const char *companion_core_browse_path(companion_core_t *core, size_t i);
|
||||
bool companion_core_browse_is_dir(companion_core_t *core, size_t i);
|
||||
/* Activate entry @i: descend into a directory (re-opens it, fires
|
||||
* nothing) or, for a file, load it - through the current core, or, when
|
||||
* @pick_core_path is given, that core. Returns 1 when content was
|
||||
* requested (the caller may hide its window), 0 when a directory was
|
||||
* entered, -1 on error. A file with no usable core and no
|
||||
* @pick_core_path returns -1 with *needs_core set and the path in
|
||||
* @content, so the caller can open a core picker. */
|
||||
int companion_core_browse_activate(companion_core_t *core, size_t i,
|
||||
const char *pick_core_path, bool *needs_core,
|
||||
char *content, size_t content_len);
|
||||
|
||||
/* --- Commands (never block; work goes to the task queue) ----------- */
|
||||
|
||||
/* Load playlist entry @i with its associated core (or the current core
|
||||
* when the entry has none). Returns false when it cannot proceed - a
|
||||
* common reason is that the entry needs a core chosen (see
|
||||
* companion_core_entry_needs_core); the caller then opens a picker. */
|
||||
bool companion_core_request_load_entry(companion_core_t *core, size_t i);
|
||||
/* True when entry @i has no usable core: its own core is empty or
|
||||
* "DETECT" and no core is currently loaded, so the entry cannot run
|
||||
* until the user picks one. Copies the entry's content path into @s
|
||||
* (a picker filters its core list by it). */
|
||||
bool companion_core_entry_needs_core(companion_core_t *core, size_t i,
|
||||
char *s, size_t len);
|
||||
/* Load arbitrary content. @core_path may be NULL to use the current
|
||||
* core; @content_path may be NULL to start the core without content. */
|
||||
bool companion_core_request_load(companion_core_t *core,
|
||||
const char *core_path, const char *content_path);
|
||||
/* Playlist-style load request, as issued by a companion's Run action:
|
||||
* @core_path is sanitised against core_info, @db_name gets its .lpl
|
||||
* extension, the running core is unloaded and the load is pushed to
|
||||
* the task queue with the menu parked on the quick menu. Any argument
|
||||
* but @core_path and @content_path may be NULL. Returns false when
|
||||
* the task could not be pushed (the caller reports the failure). */
|
||||
bool companion_core_request_load_content(companion_core_t *core,
|
||||
const char *core_path, const char *content_path,
|
||||
const char *label, const char *db_name, const char *crc32);
|
||||
/* Start the running core without content (contentless cores). */
|
||||
bool companion_core_start_core(companion_core_t *core);
|
||||
/* Load a core with no content (HAVE_DYNAMIC): sets the core path,
|
||||
* rebuilds core_info and issues CMD_EVENT_LOAD_CORE. Returns false if
|
||||
* the core could not be loaded (or the build is not HAVE_DYNAMIC). */
|
||||
bool companion_core_load_core(companion_core_t *core, const char *path);
|
||||
/* Unload the running core (CMD_EVENT_UNLOAD_CORE), resetting the menu
|
||||
* selection as the companions do. */
|
||||
bool companion_core_unload_core(companion_core_t *core);
|
||||
/* Path of the currently loaded core ("" if none). */
|
||||
const char *companion_core_current_core_path(companion_core_t *core);
|
||||
/* Copy the default core path of playlist @name (file name without
|
||||
* .lpl) into @s. Empty result when the playlist has none or "DETECT".
|
||||
* Uses the menu's cached playlist when it is the same file; otherwise
|
||||
* parses the file (synchronously - behaviour inherited from the Qt
|
||||
* companion, to be moved to a budgeted parse). */
|
||||
size_t companion_core_playlist_default_core(companion_core_t *core,
|
||||
const char *name, char *s, size_t len);
|
||||
void companion_core_event_command(companion_core_t *core,
|
||||
enum event_command cmd);
|
||||
/* Scan @path (a directory when @directory, else a single file) against
|
||||
* the content databases into the playlist directory, on the task
|
||||
* queue. on_scan_finished fires when done, after the menu's horizontal
|
||||
* list has been reset. Returns false when no scan could be started
|
||||
* (no libretrodb in this build, or the task could not be pushed). */
|
||||
bool companion_core_request_scan(companion_core_t *core, const char *path,
|
||||
bool directory, bool show_hidden_files);
|
||||
|
||||
/* --- Companion settings (retroarch.cfg) ------------------------------- */
|
||||
|
||||
/* The desktop companion's presentation settings live in retroarch.cfg
|
||||
* (settings->*.desktop_menu_*), shared by the Qt, Win32 and Cocoa
|
||||
* companions and saved with the main config. These accessors are the
|
||||
* shape the backends use; values are plain (no toolkit blobs). */
|
||||
|
||||
/* View: false = list, true = icons. */
|
||||
bool companion_core_pref_icon_view(companion_core_t *core);
|
||||
void companion_core_pref_set_icon_view(companion_core_t *core, bool icons);
|
||||
/* Thumbnail repository subdir for the icon view / boxart pane
|
||||
* (COMPANION_THUMB_BOXART etc.) from desktop_menu_thumbnail_type. */
|
||||
const char *companion_core_pref_thumbnail_subdir(companion_core_t *core);
|
||||
/* Playlist file to open at startup ("" = use History). */
|
||||
const char *companion_core_pref_initial_playlist(companion_core_t *core);
|
||||
bool companion_core_pref_suggest_loaded_core_first(companion_core_t *core);
|
||||
bool companion_core_pref_show_hidden_files(companion_core_t *core);
|
||||
/* Content-browser tab to restore: -1 when not remembering, else 0/1. */
|
||||
int companion_core_pref_last_tab(companion_core_t *core);
|
||||
void companion_core_pref_set_last_tab(companion_core_t *core, int tab);
|
||||
/* Icon-view zoom, 0..100 (desktop_menu_icon_view_zoom). */
|
||||
unsigned companion_core_pref_icon_view_zoom(companion_core_t *core);
|
||||
void companion_core_pref_set_icon_view_zoom(companion_core_t *core, unsigned z);
|
||||
/* Thumbnail type, 0 boxart / 1 screenshot / 2 title / 3 logo. */
|
||||
unsigned companion_core_pref_thumbnail_type(companion_core_t *core);
|
||||
void companion_core_pref_set_thumbnail_type(companion_core_t *core, unsigned t);
|
||||
|
||||
/* --- Playlist icons ------------------------------------------------ */
|
||||
|
||||
/* The icon a playlist list shows for playlist @i: the XMB dot-art asset
|
||||
* <assets>/xmb/dot-art/png/<playlist name>.png when it exists, else the
|
||||
* generic folder.png from the same set (what the Qt companion shows).
|
||||
* Copies the path into @s; returns its length, 0 if neither exists. */
|
||||
size_t companion_core_playlist_icon_path(companion_core_t *core, size_t i,
|
||||
char *s, size_t len);
|
||||
/* The generic folder icon of the same asset set (folder.png), for
|
||||
* playlists without their own and for the file browser. */
|
||||
size_t companion_core_folder_icon_path(companion_core_t *core,
|
||||
char *s, size_t len);
|
||||
|
||||
/* --- Thumbnails ---------------------------------------------------- */
|
||||
|
||||
/* Subdirectory names of the thumbnail repository layout. */
|
||||
#define COMPANION_THUMB_BOXART "Named_Boxarts"
|
||||
#define COMPANION_THUMB_SCREENSHOT "Named_Snaps"
|
||||
#define COMPANION_THUMB_TITLE "Named_Titles"
|
||||
#define COMPANION_THUMB_LOGO "Named_Logos"
|
||||
|
||||
/* <thumbnails dir>/<db_name>/<subdir>. Returns the length written. */
|
||||
size_t companion_core_thumbnail_dir(companion_core_t *core,
|
||||
const char *db_name, const char *subdir, char *s, size_t len);
|
||||
/* Thumbnail file for an entry: the label with the characters the
|
||||
* thumbnail repository forbids replaced by '_', under
|
||||
* companion_core_thumbnail_dir(); the first of .png .jpg .jpeg .bmp .tga
|
||||
* that exists, else the .png name (the download / save target). When
|
||||
* @content_path is itself an image file it is returned as-is, so image
|
||||
* content shows as its own thumbnail. @db_name is the playlist name
|
||||
* without .lpl; @label has no extension. Returns the length written. */
|
||||
size_t companion_core_thumbnail_path(companion_core_t *core,
|
||||
const char *db_name, const char *subdir, const char *label,
|
||||
const char *content_path, char *s, size_t len);
|
||||
|
||||
/* --- Running core -------------------------------------------------- */
|
||||
|
||||
/* Library name of the running core ("" if none). */
|
||||
const char *companion_core_current_core_name(companion_core_t *core);
|
||||
/* Version string of the running core ("" if none). */
|
||||
const char *companion_core_current_core_version(companion_core_t *core);
|
||||
/* True when the running core can start without content. */
|
||||
bool companion_core_current_core_supports_no_content(companion_core_t *core);
|
||||
|
||||
/* --- "Launch with" candidates -------------------------------------- */
|
||||
|
||||
enum companion_launch_selection
|
||||
{
|
||||
COMPANION_LAUNCH_CURRENT = 0, /* the running core */
|
||||
COMPANION_LAUNCH_PLAYLIST_SAVED, /* the entry's own core */
|
||||
COMPANION_LAUNCH_PLAYLIST_DEFAULT, /* the playlist's default core */
|
||||
COMPANION_LAUNCH_ASK, /* presentation: "Ask" */
|
||||
COMPANION_LAUNCH_LOAD_CORE /* presentation: "Load Core..." */
|
||||
};
|
||||
|
||||
typedef struct companion_launch_option
|
||||
{
|
||||
char name[NAME_MAX_LENGTH];
|
||||
char path[PATH_MAX_LENGTH];
|
||||
enum companion_launch_selection selection;
|
||||
} companion_launch_option_t;
|
||||
|
||||
/* Cores worth offering to launch a content entry with, in menu order
|
||||
* and de-duplicated: the running core (when @suggest_loaded_first and
|
||||
* one is loaded), the entry's own core (@entry_core_path /
|
||||
* @entry_core_name, skipped when empty or "DETECT"), and the default
|
||||
* core of playlist @playlist_name (name without .lpl; for a file
|
||||
* browser pass the directory name) resolved through core_info.
|
||||
* The presentation appends its own ASK / LOAD_CORE items.
|
||||
* Returns the number of options written to @out (at most @max). */
|
||||
size_t companion_core_launch_options(companion_core_t *core,
|
||||
const char *entry_core_path, const char *entry_core_name,
|
||||
const char *playlist_name, bool suggest_loaded_first,
|
||||
companion_launch_option_t *out, size_t max);
|
||||
|
||||
/* --- Thumbnail downloads (HAVE_NETWORKING) -------------------------- */
|
||||
|
||||
/* Fetch <db_name>/<subdir>/<label>.png from the thumbnail server into
|
||||
* the repository (written via a .partial file, then renamed). One
|
||||
* download is in flight at a time; a caller wanting several queues
|
||||
* them off on_thumbnail_downloaded. Returns false if the transfer could
|
||||
* not be started (no networking, or one is already running). */
|
||||
bool companion_core_thumbnail_download(companion_core_t *core,
|
||||
const char *db_name, const char *label, const char *subdir);
|
||||
/* Fetch the <db_name>.zip thumbnail pack and extract it over the
|
||||
* repository (existing files are replaced; ones that cannot be deleted
|
||||
* are renamed aside with .tmp). on_thumbnail_pack_finished reports. */
|
||||
bool companion_core_thumbnail_pack_download(companion_core_t *core,
|
||||
const char *db_name);
|
||||
/* Cancel the transfer in flight, if any. */
|
||||
void companion_core_download_cancel(companion_core_t *core);
|
||||
bool companion_core_download_active(companion_core_t *core);
|
||||
|
||||
/* --- Core information panel ---------------------------------------- */
|
||||
|
||||
/* Status codes carried in string_list_elem_attr.i for the
|
||||
* core-info value list returned by companion_core_core_info_rows(). */
|
||||
enum companion_core_info_row_status
|
||||
{
|
||||
COMPANION_CORE_INFO_ROW_NORMAL = 0,
|
||||
/* Firmware section: header rows with a key but empty value. */
|
||||
COMPANION_CORE_INFO_ROW_FIRMWARE_NOTE,
|
||||
/* Firmware status rows that should render in green. */
|
||||
COMPANION_CORE_INFO_ROW_FIRMWARE_PRESENT,
|
||||
/* Firmware status rows that should render in red. */
|
||||
COMPANION_CORE_INFO_ROW_FIRMWARE_MISSING,
|
||||
/* Notes: no key, free-form value. */
|
||||
COMPANION_CORE_INFO_ROW_NOTE_NO_KEY
|
||||
};
|
||||
|
||||
|
||||
/* Fill @keys / @values (parallel, caller-created string_lists) with the
|
||||
* rows a core-information panel shows for @core_path: name, label,
|
||||
* version, system, authors, permissions, licences, extensions, then the
|
||||
* firmware section and free-form notes. Each value's attr.i carries a
|
||||
* companion_core_info_row_status. Returns false when there is no
|
||||
* information (one "no information" row is still emitted). */
|
||||
bool companion_core_core_info_rows(const char *core_path,
|
||||
struct string_list *keys, struct string_list *values);
|
||||
|
||||
/* --- Installed cores (for "associate core" style pickers) ---------- */
|
||||
|
||||
size_t companion_core_installed_core_count(companion_core_t *core);
|
||||
const char *companion_core_installed_core_path(companion_core_t *core,
|
||||
size_t i);
|
||||
/* Display name, falling back to the core file's base name. */
|
||||
const char *companion_core_installed_core_name(companion_core_t *core,
|
||||
size_t i);
|
||||
const char *companion_core_installed_core_version(companion_core_t *core,
|
||||
size_t i);
|
||||
/* Reorder the installed-core list so the cores that can run
|
||||
* @content_path - by its extension, or any member's when it is an
|
||||
* archive, using core_info's matcher - come first, and return how many
|
||||
* there are. Indices 0..n-1 of the accessors above then name them.
|
||||
* NULL / empty @content_path leaves the order and returns the count. */
|
||||
size_t companion_core_installed_cores_supporting(companion_core_t *core,
|
||||
const char *content_path);
|
||||
|
||||
/* --- Playlist editing ---------------------------------------------- */
|
||||
|
||||
/* Open playlist file @path for reading or editing. When it is the
|
||||
* playlist the menu currently has cached, that object is returned and
|
||||
* *owned is false (edits then go through the object the menu reads);
|
||||
* otherwise the file is parsed (synchronously) and *owned is true.
|
||||
* Release with companion_core_playlist_release(); NULL on failure. */
|
||||
playlist_t *companion_core_playlist_open(companion_core_t *core,
|
||||
const char *path, bool *owned);
|
||||
/* Write @playlist to disk if @write, then free it if @owned. */
|
||||
void companion_core_playlist_release(companion_core_t *core,
|
||||
playlist_t *playlist, bool owned, bool write);
|
||||
/* Rename playlist file @path to @new_name (no directory, no extension)
|
||||
* within the playlists directory, as Qt's rename does: special
|
||||
* playlists (history, favorites - outside that directory) cannot be
|
||||
* renamed. Writes @out_path (the new file) on success. */
|
||||
bool companion_core_playlist_rename(companion_core_t *core,
|
||||
const char *path, const char *new_name, char *out_path, size_t len);
|
||||
|
||||
/* Add content to playlist file @playlist_path, as Qt's drop / "Add
|
||||
* files" does: each of @paths that is a file is pushed (label = file
|
||||
* name without extension, db_name = the playlist's name); a directory
|
||||
* is walked recursively. @core_path / @core_name may be NULL (DETECT).
|
||||
* Returns the number added; the playlist is written once at the end
|
||||
* and on_playlist_changed fires if it is the one shown. */
|
||||
size_t companion_core_playlist_add_files(companion_core_t *core,
|
||||
const char *playlist_path, const char *const *paths, size_t n,
|
||||
const char *core_path, const char *core_name);
|
||||
|
||||
/* Install an image dropped on a thumbnail pane as entry's thumbnail of
|
||||
* @type (COMPANION_THUMB_*), as Qt's changeThumbnail does: decoded,
|
||||
* downscaled to desktop_menu_thumbnail_max_size if set, written as
|
||||
* PNG under the repository path (directories created). Writes the
|
||||
* file path to @out_path on success. @image_path is a file; the
|
||||
* caller's engine should forget() that path afterwards. */
|
||||
bool companion_core_thumbnail_install(companion_core_t *core,
|
||||
const char *db_name, const char *type, const char *label,
|
||||
const char *image_path, char *out_path, size_t len);
|
||||
|
||||
/* --- Options (Qt's View > Options dialog) --------------------------------
|
||||
* The desktop-menu settings that dialog edits, as a typed table every
|
||||
* backend can render as rows: a label, a kind, the current value as a
|
||||
* string, and a setter from a string. Settings are written to the
|
||||
* config by RetroArch as usual. */
|
||||
enum companion_setting_kind
|
||||
{
|
||||
COMPANION_SETTING_BOOL = 0, /* "0" / "1" */
|
||||
COMPANION_SETTING_UINT, /* decimal */
|
||||
COMPANION_SETTING_STRING, /* free text */
|
||||
COMPANION_SETTING_CHOICE /* one of companion_core_setting_choice() */
|
||||
};
|
||||
size_t companion_core_setting_count(companion_core_t *core);
|
||||
const char *companion_core_setting_label(companion_core_t *core, size_t i);
|
||||
enum companion_setting_kind companion_core_setting_kind(companion_core_t *core, size_t i);
|
||||
/* Current value as text into @s; returns @s. */
|
||||
const char *companion_core_setting_get(companion_core_t *core, size_t i, char *s, size_t len);
|
||||
/* Set from text (a bool takes 0/1/true/false; a choice its label or
|
||||
* index); false when the text is not acceptable. */
|
||||
bool companion_core_setting_set(companion_core_t *core, size_t i, const char *text);
|
||||
size_t companion_core_setting_choice_count(companion_core_t *core, size_t i);
|
||||
const char *companion_core_setting_choice(companion_core_t *core, size_t i, size_t c);
|
||||
|
||||
/* --- Core Options (Qt's Core Options dialog) ---------------------------
|
||||
* The running core's options as a flat table: one row per visible
|
||||
* option with a description, its possible values (labels) and the
|
||||
* current one. Indices are into that table for this snapshot; refresh
|
||||
* after a core change. Nothing is toolkit-specific. */
|
||||
size_t companion_core_option_count(companion_core_t *core);
|
||||
const char *companion_core_option_desc(companion_core_t *core, size_t i);
|
||||
const char *companion_core_option_info(companion_core_t *core, size_t i);
|
||||
size_t companion_core_option_value_count(companion_core_t *core, size_t i);
|
||||
const char *companion_core_option_value_label(companion_core_t *core, size_t i, size_t v);
|
||||
size_t companion_core_option_current(companion_core_t *core, size_t i); /* value index */
|
||||
/* Set option @i to value index @v (written when the core flushes, as
|
||||
* the menu does); reset to its default. */
|
||||
void companion_core_option_set(companion_core_t *core, size_t i, size_t v);
|
||||
void companion_core_option_reset(companion_core_t *core, size_t i);
|
||||
void companion_core_option_reset_all(companion_core_t *core);
|
||||
|
||||
/* --- Shader parameters (Qt's Shader Parameters dialog) -----------------
|
||||
* The menu shader's parameters: description, range, step, current.
|
||||
* Edits apply through CMD_EVENT_SHADERS_APPLY_CHANGES. */
|
||||
size_t companion_core_shader_param_count(companion_core_t *core);
|
||||
const char *companion_core_shader_param_desc(companion_core_t *core, size_t i);
|
||||
bool companion_core_shader_param_range(companion_core_t *core, size_t i,
|
||||
float *min, float *max, float *step, float *initial);
|
||||
float companion_core_shader_param_current(companion_core_t *core, size_t i);
|
||||
void companion_core_shader_param_set(companion_core_t *core, size_t i, float v);
|
||||
void companion_core_shader_param_reset(companion_core_t *core, size_t i);
|
||||
/* The shader preset's own path ("" when none), for the dialog's title. */
|
||||
const char *companion_core_shader_path(companion_core_t *core);
|
||||
void companion_core_shader_apply(companion_core_t *core);
|
||||
|
||||
/* Replace entry @index of playlist file @path with @entry and write. */
|
||||
bool companion_core_playlist_update_entry(companion_core_t *core,
|
||||
const char *path, size_t index, const struct playlist_entry *entry);
|
||||
/* Delete entry @index of playlist file @path and write. */
|
||||
bool companion_core_playlist_delete_entry(companion_core_t *core,
|
||||
const char *path, size_t index);
|
||||
/* Associate playlist file @path with @core_path (resolved through
|
||||
* core_info; unknown or NULL -> "DETECT") and write. */
|
||||
bool companion_core_playlist_set_default_core(companion_core_t *core,
|
||||
const char *path, const char *core_path);
|
||||
|
||||
/* Bulk add. Opens playlist @path as a private instance (never the
|
||||
* menu's cached one), so a cancelled add can be discarded unwritten:
|
||||
* commit with companion_core_playlist_release(core, pl, true, true),
|
||||
* abort with companion_core_playlist_release(core, pl, true, false). */
|
||||
playlist_t *companion_core_playlist_open_private(companion_core_t *core,
|
||||
const char *path);
|
||||
/* Content path a playlist entry should carry for @path: an archive
|
||||
* holding exactly one file resolves to "archive#file" (not to every
|
||||
* member: for MAME/FBA-style content the archive itself is the entry).
|
||||
* Copies into @s; returns its length. */
|
||||
size_t companion_core_resolve_content_path(companion_core_t *core,
|
||||
const char *path, char *s, size_t len);
|
||||
/* Append an entry. NULL / empty @core_path or @core_name become
|
||||
* "DETECT"; the crc is the unknown marker. */
|
||||
bool companion_core_playlist_push(companion_core_t *core,
|
||||
playlist_t *playlist, const char *content_path, const char *label,
|
||||
const char *core_path, const char *core_name, const char *db_name);
|
||||
|
||||
/* --- Window hand-off ------------------------------------------------ */
|
||||
|
||||
/* Before a companion raises its window: release the mouse grab, show
|
||||
* the cursor and leave fullscreen so the window can be reached. Every
|
||||
* backend's toggle does exactly this. */
|
||||
void companion_core_prepare_show_window(companion_core_t *core);
|
||||
/* True when RetroArch started in fullscreen (a companion may prefer to
|
||||
* stay behind the video window then). */
|
||||
bool companion_core_video_started_fullscreen(companion_core_t *core);
|
||||
|
||||
/* --- Inbound notifications from RetroArch (called by the driver glue) */
|
||||
|
||||
void companion_core_status_message(companion_core_t *core,
|
||||
const char *msg, unsigned prio, unsigned duration, bool flush);
|
||||
void companion_core_log_message(companion_core_t *core, const char *msg);
|
||||
void companion_core_notify_refresh(companion_core_t *core);
|
||||
|
||||
RETRO_END_DECLS
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,151 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef __COMPANION_THUMBS_H
|
||||
#define __COMPANION_THUMBS_H
|
||||
|
||||
/* Shared thumbnail engine for the desktop companions (C89, no toolkit
|
||||
* types). One instance per companion window.
|
||||
*
|
||||
* What the Qt companion does, made common and faster:
|
||||
* - a cache of decoded thumbnails keyed by (file path, edge), already
|
||||
* scaled to the size the view draws, under a byte budget with LRU
|
||||
* eviction: a thumbnail decoded once is drawn from the cache for as
|
||||
* long as it fits, across playlist switches
|
||||
* - a pool of decode threads (Qt: one) that decode and scale off the
|
||||
* UI thread; the UI thread only ever copies finished pixels into a
|
||||
* toolkit image
|
||||
* - a request queue served most-recent-first (the rows still on
|
||||
* screen), de-duplicated by key, with a low-priority lane for
|
||||
* prefetching what is about to scroll in
|
||||
*
|
||||
* The backend's job is only to say which rows are visible (the view
|
||||
* knows; Qt asks its grid for visibleIndexes() on a 50 ms scroll
|
||||
* debounce), turn a row into a file path, and poll once per frame for
|
||||
* finished thumbnails to hand to its image control.
|
||||
*
|
||||
* Pixels are ARGB8888 (0xAARRGGBB, opaque), w x h, row-major -
|
||||
* Windows' 32-bit DIB order, and what Cocoa/Qt take with a byte swap.
|
||||
*
|
||||
* Thread-safe where stated; everything else is UI-thread only. Builds
|
||||
* without HAVE_THREADS, decoding on the UI thread inside poll() under a
|
||||
* time budget. */
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <boolean.h>
|
||||
#include <retro_common_api.h>
|
||||
|
||||
RETRO_BEGIN_DECLS
|
||||
|
||||
typedef struct companion_thumbs companion_thumbs_t;
|
||||
|
||||
/* Delivered from companion_thumbs_poll() for each thumbnail that has
|
||||
* finished decoding since the last poll. @tag is what the request
|
||||
* passed (a backend row id); @bits is valid for the duration of the
|
||||
* callback only - copy it into the toolkit image there. @bits is NULL
|
||||
* when the file could not be decoded (the backend marks the row as
|
||||
* having no thumbnail). */
|
||||
typedef void (*companion_thumbs_done_cb)(void *ud, const char *path,
|
||||
int w, int h, uintptr_t tag, const uint32_t *bits);
|
||||
|
||||
/* @budget_bytes: cache size in bytes of decoded pixels (0 = a default of
|
||||
* 64 MiB). @threads: decode threads to start, 0 = cores - 1 clamped to
|
||||
* [1, 4]; ignored without HAVE_THREADS. */
|
||||
companion_thumbs_t *companion_thumbs_new(size_t budget_bytes,
|
||||
unsigned threads);
|
||||
void companion_thumbs_free(companion_thumbs_t *t);
|
||||
|
||||
/* Cached thumbnail for (@path, @w x @h), or NULL. The pointer is valid
|
||||
* until the next companion_thumbs_* call on this instance. UI thread.
|
||||
* Touches the entry (LRU). Grid cells pass w == h; the boxart pane its
|
||||
* own size. */
|
||||
const uint32_t *companion_thumbs_get(companion_thumbs_t *t,
|
||||
const char *path, int w, int h);
|
||||
|
||||
/* Ask for (@path, @edge) to be decoded. @urgent requests are served
|
||||
* most-recent-first ahead of every non-urgent one; non-urgent ones
|
||||
* (prefetch) are served oldest-first after them. A key already cached
|
||||
* or already queued is ignored (returns false). @bg is the ARGB colour
|
||||
* the letterbox is filled with. UI thread. */
|
||||
bool companion_thumbs_request(companion_thumbs_t *t, const char *path,
|
||||
int w, int h, uintptr_t tag, bool urgent, uint32_t bg);
|
||||
|
||||
/* Drop every queued request (the view changed); cached thumbnails stay.
|
||||
* Decodes already in flight still land, are cached, and are delivered -
|
||||
* the backend validates the tag against its own view state, e.g. a row
|
||||
* generation it bumps on rebuild. UI thread. */
|
||||
void companion_thumbs_cancel(companion_thumbs_t *t);
|
||||
|
||||
/* Deliver finished decodes (at most @max, 0 = all) through @cb; without
|
||||
* HAVE_THREADS this is also where decoding happens, for at most
|
||||
* @budget_us. Returns how many were delivered. UI thread. */
|
||||
size_t companion_thumbs_poll(companion_thumbs_t *t,
|
||||
companion_thumbs_done_cb cb, void *ud, size_t max,
|
||||
unsigned budget_us);
|
||||
|
||||
/* Animation: what RetroArch's own File Browser does for the selected
|
||||
* thumbnail - an APNG, animated WEBP, WEBM or MP4 plays its frames on
|
||||
* the container's clock (no audio, as in the menu). One animation at a
|
||||
* time (the pane showing the selection). Frames arrive through
|
||||
* companion_thumbs_poll() as ordinary deliveries for (@path, @w x @h,
|
||||
* @tag), each frame already scaled and letterboxed; the backend blits
|
||||
* them into its pane exactly as it does a still. A still image, or a
|
||||
* type without an animation decoder, produces nothing (the still that
|
||||
* was requested normally stays). Decoding runs on its own thread and
|
||||
* stops on _animate_stop(), a new _animate(), or free(). UI thread. */
|
||||
void companion_thumbs_animate(companion_thumbs_t *t, const char *path,
|
||||
int w, int h, uintptr_t tag, uint32_t bg);
|
||||
void companion_thumbs_animate_stop(companion_thumbs_t *t);
|
||||
/* True while an animation is playing (a backend may keep polling). */
|
||||
bool companion_thumbs_animating(companion_thumbs_t *t);
|
||||
|
||||
/* Change the cache budget (bytes; 0 = the default); evicts down to it
|
||||
* at once. UI thread. */
|
||||
void companion_thumbs_set_budget(companion_thumbs_t *t, size_t budget_bytes);
|
||||
|
||||
/* Forget every cached size of @path (the file changed - a download
|
||||
* replaced it, say) so the next request decodes it again. A decode in
|
||||
* flight for it still lands with the old pixels; the backend can call
|
||||
* this again afterwards. Returns how many entries were dropped. */
|
||||
size_t companion_thumbs_forget(companion_thumbs_t *t, const char *path);
|
||||
|
||||
/* Diagnostics / tests. */
|
||||
size_t companion_thumbs_cached_count(companion_thumbs_t *t);
|
||||
size_t companion_thumbs_cached_bytes(companion_thumbs_t *t);
|
||||
size_t companion_thumbs_queued(companion_thumbs_t *t);
|
||||
/* Queued + decoding + finished-but-not-yet-polled: while non-zero a
|
||||
* backend must keep polling. */
|
||||
size_t companion_thumbs_pending(companion_thumbs_t *t);
|
||||
|
||||
/* Letterbox @src (sw x sh ARGB) into a freshly allocated dw x dh ARGB
|
||||
* buffer filled with @bg. Pure; exposed for tests and for backends that
|
||||
* scale their own images (the boxart pane). Nearest-neighbour: plenty at
|
||||
* thumbnail size, and the same on every backend. */
|
||||
uint32_t *companion_thumbs_scale(const uint32_t *src, unsigned sw,
|
||||
unsigned sh, int dw, int dh, uint32_t bg);
|
||||
/* Same, with the source in R,G,B,A memory order (what an animation
|
||||
* stream emits when it will not emit ARGB words): the byte swap happens
|
||||
* on the pixels sampled, never over the whole canvas - at 4K that pass
|
||||
* cost 12 ms a frame against 0.2 ms here. When shrinking by two or
|
||||
* more in both directions the samplers average four taps per output
|
||||
* pixel instead of one (under 1 ms at 4K), which takes most of the
|
||||
* shimmer out of a downscaled video. */
|
||||
uint32_t *companion_thumbs_scale_ex(const uint32_t *src, unsigned sw,
|
||||
unsigned sh, int dw, int dh, uint32_t bg, bool src_rgba_order);
|
||||
|
||||
RETRO_END_DECLS
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,770 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Regression harness for the Cocoa companion (ui/drivers/
|
||||
* ui_cocoa_companion.m), run on Linux against GNUstep's AppKit under
|
||||
* Xvfb by tools/companion_cocoa_test.sh. The real driver file is
|
||||
* compiled and linked - the same code that runs on macOS, minus what
|
||||
* differs between the two AppKits - together with the real companion
|
||||
* core and the core test's stubs and fixtures.
|
||||
*
|
||||
* The harness drives the driver the way RetroArch does: init, iterate
|
||||
* until the playlist lands, then acts on the controller exactly as a
|
||||
* user would through the widgets (the view popup, the File Browser
|
||||
* tab, closing the window) and asserts on the AppKit objects
|
||||
* themselves: is the grid the scroll view's document, does it have a
|
||||
* frame and a count, does the folder table hand back names, does
|
||||
* closing make RetroArch's window key with the render view as first
|
||||
* responder.
|
||||
*
|
||||
* What GNUstep cannot stand in for: Metal, the real WindowServer's
|
||||
* key-window arbitration, fonts on a Mac. What it can: the whole
|
||||
* AppKit object graph, layout, table data sources, first responder
|
||||
* plumbing - which is where every bug so far has been. */
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <compat/strl.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <retro_miscellaneous.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../ui/ui_companion_driver.h"
|
||||
#include "../../../ui/companion/companion_core.h"
|
||||
#include "../../../runloop.h"
|
||||
#include "../../../core_option_manager.h"
|
||||
#include <file/file_path.h>
|
||||
extern runloop_state_t test_runloop;
|
||||
#include "../../../ui/drivers/cocoa/cocoa_common.h"
|
||||
#include "../../../ui/drivers/cocoa/apple_platform.h"
|
||||
|
||||
/* --- the platform, stubbed ----------------------------------------------- */
|
||||
|
||||
/* The harness's stand-in for RetroArch_OSX: a window with a render view,
|
||||
* so the companion's "hand the keyboard back" path has a real target
|
||||
* to make key and first responder. */
|
||||
/* RetroArch's RAWindow reads the keyboard in -sendEvent:, which AppKit
|
||||
* calls on the KEY window for key events. This stand-in counts the key
|
||||
* events that reach it: after the companion closes, a posted keystroke
|
||||
* must arrive here or RetroArch has no keyboard. */
|
||||
@interface HarnessWindow : NSWindow
|
||||
{
|
||||
@public
|
||||
int keyEvents;
|
||||
}
|
||||
@end
|
||||
@implementation HarnessWindow
|
||||
- (void)sendEvent:(NSEvent*)event
|
||||
{
|
||||
if ([event type] == NSKeyDown || [event type] == NSKeyUp)
|
||||
keyEvents++;
|
||||
[super sendEvent:event];
|
||||
}
|
||||
@end
|
||||
|
||||
/* The render view, as CocoaView / the Metal view: takes first responder. */
|
||||
@interface HarnessRenderView : NSView
|
||||
@end
|
||||
@implementation HarnessRenderView
|
||||
- (BOOL)acceptsFirstResponder { return YES; }
|
||||
@end
|
||||
|
||||
@interface HarnessPlatform : NSObject
|
||||
{
|
||||
HarnessWindow *win;
|
||||
NSView *rv;
|
||||
}
|
||||
- (NSWindow *)hostWindow;
|
||||
- (id)renderView;
|
||||
@end
|
||||
|
||||
@implementation HarnessPlatform
|
||||
- (id)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
win = [[HarnessWindow alloc] initWithContentRect:NSMakeRect(50, 50, 640, 480)
|
||||
styleMask:(NSTitledWindowMask | NSClosableWindowMask)
|
||||
backing:NSBackingStoreBuffered defer:NO];
|
||||
rv = [[HarnessRenderView alloc] initWithFrame:NSMakeRect(0, 0, 640, 480)];
|
||||
[win setContentView:rv];
|
||||
[win setTitle:@"RetroArch (harness)"];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
- (NSWindow *)hostWindow { return win; }
|
||||
- (id)renderView { return rv; }
|
||||
@end
|
||||
|
||||
id<ApplePlatform> apple_platform = nil;
|
||||
|
||||
/* RetroArch's keyboard state (input/drivers/cocoa_input.m): showing the
|
||||
* companion must forget held keys, or a key released into the
|
||||
* companion stays "down" for RetroArch's menu. */
|
||||
int stub_keyboard_resets;
|
||||
void apple_input_keyboard_reset(void) { stub_keyboard_resets++; }
|
||||
|
||||
/* CocoaView: the driver reaches for +get; return a view in no window,
|
||||
* exactly the situation on a Metal build that broke focus before. */
|
||||
@implementation CocoaView
|
||||
+ (CocoaView*)get
|
||||
{
|
||||
static CocoaView *v = nil;
|
||||
if (!v)
|
||||
v = [[CocoaView alloc] initWithFrame:NSMakeRect(0, 0, 8, 8)];
|
||||
return v;
|
||||
}
|
||||
@end
|
||||
|
||||
extern settings_t test_settings;
|
||||
|
||||
/* The driver under test (its global driver struct). */
|
||||
extern ui_companion_driver_t ui_companion_wimp_cocoa;
|
||||
|
||||
/* Peek into the driver's private state: the struct is private to the
|
||||
* .m, but its first two fields are stable and documented there. */
|
||||
struct wimp_peek { companion_core_t *core; void *controller; };
|
||||
|
||||
static int fails;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { fails++; printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } else { printf("[ok] "); printf(__VA_ARGS__); printf("\n"); } fflush(stdout); } while (0)
|
||||
|
||||
/* Pump AppKit and the driver for a while (the core's async work lands
|
||||
* through iterate; AppKit's deferred layout through the run loop). */
|
||||
static void pump(void *data, int ms)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < ms / 5; i++)
|
||||
{
|
||||
NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init];
|
||||
NSEvent *ev;
|
||||
ui_companion_wimp_cocoa.iterate(data);
|
||||
while ((ev = [NSApp nextEventMatchingMask:NSAnyEventMask
|
||||
untilDate:[NSDate dateWithTimeIntervalSinceNow:0.001]
|
||||
inMode:NSDefaultRunLoopMode dequeue:YES]))
|
||||
[NSApp sendEvent:ev];
|
||||
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.004]];
|
||||
[p drain];
|
||||
}
|
||||
}
|
||||
|
||||
/* Find a subview of a class, depth-first. */
|
||||
static id find_view(NSView *root, Class cls)
|
||||
{
|
||||
NSUInteger i;
|
||||
if ([root isKindOfClass:cls])
|
||||
return root;
|
||||
for (i = 0; i < [[root subviews] count]; i++)
|
||||
{
|
||||
id r = find_view([[root subviews] objectAtIndex:i], cls);
|
||||
if (r)
|
||||
return r;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/* The controller's scroll views, by which document they hold. */
|
||||
static NSScrollView *scroll_holding(NSView *root, Class docCls)
|
||||
{
|
||||
NSUInteger i;
|
||||
if ([root isKindOfClass:[NSScrollView class]]
|
||||
&& [[(NSScrollView*)root documentView] isKindOfClass:docCls])
|
||||
return (NSScrollView*)root;
|
||||
for (i = 0; i < [[root subviews] count]; i++)
|
||||
{
|
||||
NSScrollView *r = scroll_holding([[root subviews] objectAtIndex:i], docCls);
|
||||
if (r)
|
||||
return r;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
extern void companion_test_setup_fixtures(char *root, size_t len);
|
||||
extern void companion_test_teardown_fixtures(const char *root);
|
||||
|
||||
#include <signal.h>
|
||||
static void on_alarm(int sig) { (void)sig; printf("FAIL: harness timed out (hang)\n"); fflush(stdout); _exit(3); }
|
||||
|
||||
/* A fault (SIGTRAP is what a libdispatch or runtime assertion raises on
|
||||
* macOS; SIGSEGV / SIGBUS / SIGABRT the rest): print a backtrace so the
|
||||
* CI log says where, then fail. */
|
||||
#include <execinfo.h>
|
||||
static void on_fault(int sig)
|
||||
{
|
||||
void *frames[40];
|
||||
int n = backtrace(frames, 40);
|
||||
printf("FAIL: fault (signal %d); backtrace:\n", sig);
|
||||
fflush(stdout);
|
||||
backtrace_symbols_fd(frames, n, 1);
|
||||
_exit(5);
|
||||
}
|
||||
|
||||
/* An uncaught Objective-C exception is a trap with no message on macOS
|
||||
* ("Trace/BPT trap"): print what it was and where, then fail. */
|
||||
static void on_exception(NSException *e)
|
||||
{
|
||||
printf("FAIL: uncaught exception %s: %s\n",
|
||||
[[e name] UTF8String], [[e reason] UTF8String]);
|
||||
if ([e respondsToSelector:@selector(callStackSymbols)])
|
||||
{
|
||||
NSArray *syms = [e performSelector:@selector(callStackSymbols)];
|
||||
NSUInteger i;
|
||||
for (i = 0; i < [syms count] && i < 25; i++)
|
||||
printf(" %s\n", [[syms objectAtIndex:i] UTF8String]);
|
||||
}
|
||||
fflush(stdout);
|
||||
_exit(4);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char root[512];
|
||||
void *data;
|
||||
struct wimp_peek *peek;
|
||||
id ctrl;
|
||||
NSWindow *win;
|
||||
NSView *content;
|
||||
NSPopUpButton *viewPopup = nil;
|
||||
NSTabView *tabs = nil;
|
||||
NSTableView *leftTable = nil;
|
||||
NSScrollView *entriesScroll = nil;
|
||||
Class gridCls;
|
||||
(void)argc; (void)argv;
|
||||
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
signal(SIGALRM, on_alarm);
|
||||
alarm(90);
|
||||
NSSetUncaughtExceptionHandler(on_exception);
|
||||
signal(SIGTRAP, on_fault);
|
||||
signal(SIGSEGV, on_fault);
|
||||
signal(SIGBUS, on_fault);
|
||||
signal(SIGABRT, on_fault);
|
||||
signal(SIGILL, on_fault);
|
||||
[NSApplication sharedApplication];
|
||||
/* A plain executable: become a regular, activatable app (Apple's
|
||||
* AppKit will not give a background process a key window). */
|
||||
if ([NSApp respondsToSelector:@selector(setActivationPolicy:)])
|
||||
[NSApp setActivationPolicy:0 /* NSApplicationActivationPolicyRegular */];
|
||||
[NSApp finishLaunching];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
apple_platform = (id<ApplePlatform>)[[HarnessPlatform alloc] init];
|
||||
[[(id)apple_platform hostWindow] makeKeyAndOrderFront:nil];
|
||||
|
||||
companion_test_setup_fixtures(root, sizeof(root));
|
||||
test_settings.bools.ui_companion_toggle = true;
|
||||
test_settings.uints.desktop_menu_view_type = 0; /* start in list view */
|
||||
|
||||
data = ui_companion_wimp_cocoa.init();
|
||||
CHECK(data != NULL, "driver init (window built)");
|
||||
if (!data)
|
||||
return 1;
|
||||
peek = (struct wimp_peek*)data;
|
||||
ctrl = (id)peek->controller;
|
||||
win = [ctrl performSelector:@selector(window)];
|
||||
CHECK(win != nil, "controller has a window");
|
||||
content = [win contentView];
|
||||
{
|
||||
/* showing the companion takes the keyboard: RetroArch's held keys
|
||||
* must be forgotten, or a release into this window leaves the
|
||||
* menu waiting on a key that is "still down" */
|
||||
int before = stub_keyboard_resets;
|
||||
ui_companion_wimp_cocoa.toggle(data, true);
|
||||
pump(data, 200);
|
||||
CHECK(stub_keyboard_resets > before, "showing the companion resets RetroArch's keyboard state (apple_input_keyboard_reset)");
|
||||
}
|
||||
|
||||
/* Playlist lands through iterate */
|
||||
pump(data, 400);
|
||||
CHECK(companion_core_playlist_count(peek->core) >= 3, "playlists listed (%u)", (unsigned)companion_core_playlist_count(peek->core));
|
||||
companion_core_select_playlist(peek->core, 2); /* NES */
|
||||
pump(data, 600);
|
||||
CHECK(companion_core_entry_count(peek->core) == 3, "playlist entries landed (%u)", (unsigned)companion_core_entry_count(peek->core));
|
||||
|
||||
/* --- the widgets the user drives --- */
|
||||
{
|
||||
NSUInteger i;
|
||||
for (i = 0; i < [[content subviews] count]; i++)
|
||||
{
|
||||
id v = [[content subviews] objectAtIndex:i];
|
||||
/* by action, not item count: the core popup also has two items
|
||||
* (Ask, Load Core...) when no core is installed */
|
||||
if ([v isKindOfClass:[NSPopUpButton class]]
|
||||
&& [(NSPopUpButton*)v action] == NSSelectorFromString(@"viewChanged:"))
|
||||
viewPopup = v; /* List / Icons */
|
||||
if ([v isKindOfClass:[NSTabView class]])
|
||||
tabs = v;
|
||||
}
|
||||
}
|
||||
CHECK(viewPopup != nil, "view popup found");
|
||||
CHECK(tabs != nil && [tabs numberOfTabViewItems] == 2, "Playlists | File Browser tabs");
|
||||
gridCls = NSClassFromString(@"RACompanionGrid");
|
||||
CHECK(gridCls != Nil, "grid class present");
|
||||
entriesScroll = nil;
|
||||
{
|
||||
NSUInteger i;
|
||||
for (i = 0; i < [[content subviews] count]; i++)
|
||||
{
|
||||
id v = [[content subviews] objectAtIndex:i];
|
||||
if ([v isKindOfClass:[NSScrollView class]])
|
||||
{
|
||||
id doc = [(NSScrollView*)v documentView];
|
||||
if ([doc isKindOfClass:[NSTableView class]]
|
||||
&& [[(NSTableView*)doc tableColumns] count] >= 2
|
||||
&& [[[[(NSTableView*)doc tableColumns] objectAtIndex:0] identifier] isEqualToString:@"name"]
|
||||
&& [[[[(NSTableView*)doc tableColumns] objectAtIndex:1] identifier] isEqualToString:@"core"])
|
||||
entriesScroll = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
leftTable = nil;
|
||||
{
|
||||
/* the left table: a table whose first column is the icon column */
|
||||
NSUInteger i;
|
||||
for (i = 0; i < [[content subviews] count]; i++)
|
||||
{
|
||||
id v = [[content subviews] objectAtIndex:i];
|
||||
if ([v isKindOfClass:[NSScrollView class]])
|
||||
{
|
||||
id doc = [(NSScrollView*)v documentView];
|
||||
if ([doc isKindOfClass:[NSTableView class]]
|
||||
&& [[[[(NSTableView*)doc tableColumns] objectAtIndex:0] identifier] isEqualToString:@"icon"])
|
||||
leftTable = doc;
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK(leftTable != nil, "playlist / folder table found");
|
||||
CHECK([leftTable numberOfRows] >= 3, "playlist table has rows (%ld)", (long)[leftTable numberOfRows]);
|
||||
CHECK([[leftTable superview] superview] != nil && ![[[leftTable superview] superview] isHidden], "playlist table is in the window and visible");
|
||||
|
||||
/* --- Icons view --- */
|
||||
[viewPopup selectItemAtIndex:1];
|
||||
printf(" popup: items=%ld selected=%ld\n", (long)[viewPopup numberOfItems], (long)[viewPopup indexOfSelectedItem]); fflush(stdout);
|
||||
[viewPopup sendAction:[viewPopup action] to:[viewPopup target]];
|
||||
printf(" action returned\n"); fflush(stdout);
|
||||
pump(data, 300);
|
||||
printf(" pumped\n"); fflush(stdout);
|
||||
printf(" entries scroll document class: %s\n", [[[[entriesScroll documentView] class] description] UTF8String]);
|
||||
{
|
||||
NSScrollView *gs = scroll_holding(content, gridCls);
|
||||
CHECK(gs != nil, "Icons: the grid is a scroll view's document");
|
||||
if (gs)
|
||||
{
|
||||
NSView *grid = [gs documentView];
|
||||
NSRect f = [grid frame];
|
||||
NSInteger n = (NSInteger)[[grid valueForKey:@"count"] integerValue];
|
||||
CHECK(f.size.width > 100 && f.size.height > 50, "grid has a frame (%.0f x %.0f)", f.size.width, f.size.height);
|
||||
CHECK([grid isKindOfClass:gridCls], "document is the grid");
|
||||
CHECK(n == 3, "grid count is the playlist's 3 entries (got %ld)", (long)n);
|
||||
/* paint it: the draw path must run without faulting */
|
||||
[grid setNeedsDisplay:YES];
|
||||
[grid displayIfNeeded];
|
||||
pump(data, 200);
|
||||
CHECK(1, "grid painted");
|
||||
}
|
||||
}
|
||||
|
||||
/* --- the icon grid's prefetch must not read past the list ---
|
||||
* iconTick asks for a screen either side of the visible rows, so it
|
||||
* reaches rows that do not exist; thumbNone is only as long as the
|
||||
* list. Under ASan (and Apple's Guard Malloc) reading past it is a
|
||||
* crash - this is what took the app down on macOS. Drive iconTick
|
||||
* repeatedly with a short list, and hit thumbWant: directly with
|
||||
* rows either side of the range. */
|
||||
{
|
||||
SEL sTick = NSSelectorFromString(@"iconTick");
|
||||
SEL sWant = NSSelectorFromString(@"thumbWant:urgent:");
|
||||
NSInteger n = (NSInteger)companion_core_entry_count(peek->core);
|
||||
NSInteger k;
|
||||
CHECK([ctrl respondsToSelector:sTick] && [ctrl respondsToSelector:sWant], "iconTick / thumbWant: present");
|
||||
for (k = 0; k < 8; k++)
|
||||
{
|
||||
[ctrl performSelector:sTick];
|
||||
pump(data, 20);
|
||||
}
|
||||
for (k = -3; k <= n + 3; k++)
|
||||
{
|
||||
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[ctrl methodSignatureForSelector:sWant]];
|
||||
BOOL urgent = YES;
|
||||
[inv setSelector:sWant]; [inv setTarget:ctrl];
|
||||
[inv setArgument:&k atIndex:2];
|
||||
[inv setArgument:&urgent atIndex:3];
|
||||
[inv invoke];
|
||||
}
|
||||
pump(data, 100);
|
||||
CHECK(1, "prefetch and out-of-range rows leave the thumbnail marks alone (%ld entries)", (long)n);
|
||||
}
|
||||
|
||||
/* --- File Browser tab --- */
|
||||
[tabs selectTabViewItemAtIndex:1];
|
||||
pump(data, 800);
|
||||
{
|
||||
const char *dir = companion_core_browse_dir(peek->core);
|
||||
CHECK(dir && *dir, "browse listing landed (dir=%s)", dir ? dir : "-");
|
||||
CHECK(companion_core_browse_count(peek->core) >= 2, "browse count %u", (unsigned)companion_core_browse_count(peek->core));
|
||||
CHECK([leftTable numberOfRows] == (NSInteger)companion_core_browse_dir_count(peek->core), "folder pane rows = dir_count (%ld vs %u)", (long)[leftTable numberOfRows], (unsigned)companion_core_browse_dir_count(peek->core));
|
||||
CHECK(![[[leftTable superview] superview] isHidden] && [[leftTable superview] superview] != nil, "folder table still visible under the File Browser tab");
|
||||
if ([leftTable numberOfRows] > 1)
|
||||
{
|
||||
id ds = [leftTable dataSource];
|
||||
NSTableColumn *nameCol = [[leftTable tableColumns] objectAtIndex:1];
|
||||
id v0 = [ds tableView:leftTable objectValueForTableColumn:nameCol row:0];
|
||||
id v1 = [ds tableView:leftTable objectValueForTableColumn:nameCol row:1];
|
||||
CHECK([v0 isKindOfClass:[NSString class]] && [(NSString*)v0 length] > 0, "folder row 0 has text: '%s'", [[v0 description] UTF8String]);
|
||||
CHECK([v1 isKindOfClass:[NSString class]] && [(NSString*)v1 length] > 0, "folder row 1 has text: '%s'", [[v1 description] UTF8String]);
|
||||
CHECK([[v0 description] isEqualToString:@".."], "row 0 is '..' (got '%s')", [[v0 description] UTF8String]);
|
||||
}
|
||||
{
|
||||
NSTableView *files = (NSTableView*)[entriesScroll documentView];
|
||||
CHECK([files isKindOfClass:[NSTableView class]], "browser content is the table");
|
||||
CHECK([files numberOfRows] == (NSInteger)companion_core_browse_count(peek->core), "content rows = browse count (%ld)", (long)[files numberOfRows]);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- audit: every user action the controller implements, fired in
|
||||
* the state a user would fire it from, on the real objects ---
|
||||
* Reported, not asserted: the point is the gap list. */
|
||||
if (getenv("COMPANION_AUDIT"))
|
||||
{
|
||||
struct { const char *sel; const char *note; } acts[] = {
|
||||
{ "refreshPlaylists:", "F5 / reload playlists" },
|
||||
{ "viewList:", "View > List" },
|
||||
{ "viewIcons:", "View > Icons" },
|
||||
{ "zoomChanged:", "zoom slider" },
|
||||
{ "thumbTypeChanged:", "thumbnail type popup" },
|
||||
{ "boxartTypeChanged:","boxart segment" },
|
||||
{ "toggleInfo:", "View > Core Info dock" },
|
||||
{ "toggleBoxart:", "View > Boxart dock" },
|
||||
{ "toggleLog:", "View > Log dock" },
|
||||
{ "focusSearch:", "Edit > Search" },
|
||||
{ "searchChanged:", "search field edited" },
|
||||
{ "clearSearch:", "search Clear" },
|
||||
{ "browseFiles:", "File Browser tab" },
|
||||
{ "browseUp:", "browser Up" },
|
||||
{ "browseStart:", "browser Start Directory" },
|
||||
{ "browseDownloads:", "browser Downloads" },
|
||||
{ "playlistsDoubleClick:", "double-click playlist / folder" },
|
||||
{ "corePopupChanged:", "core popup changed" },
|
||||
{ "runSelected:", "Run" },
|
||||
{ "runWithPopup:", "run with the popup's core" },
|
||||
{ "startCore:", "Start Core (no content)" },
|
||||
{ "loadCore:", "File > Load Core (picker)" },
|
||||
{ "loadSelectedCore:", "picker: Load" },
|
||||
{ "cancelLoadCore:", "picker: Cancel" },
|
||||
{ "scanDirectory:", "Scan Directory" },
|
||||
{ "deleteEntry:", "context: delete entry" },
|
||||
{ "associateCore:", "context: associate core" },
|
||||
{ "openDocs:", "Help > Documentation" },
|
||||
{ "aboutRetroArch:", "Help > About (modal alert)" },
|
||||
{ "aboutContributors:","Help > About Contributors (window)" },
|
||||
{ "stopContent:", "Stop button" },
|
||||
{ "unloadCore:", "File > Unload Core" },
|
||||
{ "quitRetroArch:", "File > Exit RetroArch" },
|
||||
{ "loadContent:", "load content (file)" },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
int k;
|
||||
printf("\n=== Cocoa companion action audit ===\n");
|
||||
for (k = 0; acts[k].sel; k++)
|
||||
{
|
||||
SEL s = NSSelectorFromString([NSString stringWithUTF8String:acts[k].sel]);
|
||||
BOOL has = [ctrl respondsToSelector:s];
|
||||
const char *dlg = strstr(acts[k].sel, "loadCore") || strstr(acts[k].sel, "scanDirectory")
|
||||
|| strstr(acts[k].sel, "aboutRetroArch") || strstr(acts[k].sel, "quitRetroArch")
|
||||
|| strstr(acts[k].sel, "openDoc") || strstr(acts[k].sel, "loadContent")
|
||||
|| strstr(acts[k].sel, "startCore") || strstr(acts[k].sel, "run")
|
||||
|| strstr(acts[k].sel, "deleteEntry") || strstr(acts[k].sel, "associateCore")
|
||||
? " (skipped: modal / launches / destructive)" : "";
|
||||
printf(" %-24s %-36s %s%s\n", acts[k].sel, acts[k].note,
|
||||
has ? "implemented" : "MISSING", has ? dlg : "");
|
||||
if (has && !*dlg)
|
||||
{
|
||||
[ctrl performSelector:s withObject:nil];
|
||||
pump(data, 60);
|
||||
}
|
||||
}
|
||||
printf(" (state after audit: iconView doc=%s, browseMode=%s, entries rows=%ld)\n",
|
||||
[[[[entriesScroll documentView] class] description] UTF8String],
|
||||
[[ctrl valueForKey:@"browseMode"] boolValue] ? "yes" : "no",
|
||||
(long)[(NSTableView*)[entriesScroll documentView] numberOfRows]);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/* --- the menu / button wiring added for Qt parity --- */
|
||||
{
|
||||
extern int stub_calls_command;
|
||||
int before = stub_calls_command;
|
||||
SEL s;
|
||||
s = NSSelectorFromString(@"stopContent:");
|
||||
CHECK([ctrl respondsToSelector:s], "Stop implemented");
|
||||
[ctrl performSelector:s withObject:nil];
|
||||
pump(data, 50);
|
||||
CHECK(stub_calls_command > before, "Stop reaches the core (command_event called)");
|
||||
s = NSSelectorFromString(@"aboutContributors:");
|
||||
CHECK([ctrl respondsToSelector:s], "About Contributors implemented");
|
||||
[ctrl performSelector:s withObject:nil];
|
||||
pump(data, 100);
|
||||
{
|
||||
NSWindow *cw = [ctrl valueForKey:@"contributorsWindow"];
|
||||
CHECK(cw != nil && [cw isVisible], "contributors window shown");
|
||||
if (cw)
|
||||
{
|
||||
NSTextView *tv = find_view([cw contentView], [NSTextView class]);
|
||||
CHECK(tv && [[tv string] length] > 1000, "contributors text present (%lu chars)", tv ? (unsigned long)[[tv string] length] : 0UL);
|
||||
[cw orderOut:nil];
|
||||
}
|
||||
}
|
||||
CHECK([ctrl respondsToSelector:NSSelectorFromString(@"unloadCore:")], "Unload Core implemented");
|
||||
CHECK([ctrl respondsToSelector:NSSelectorFromString(@"quitRetroArch:")], "Exit RetroArch implemented");
|
||||
CHECK([ctrl respondsToSelector:NSSelectorFromString(@"aboutRetroArch:")], "About implemented");
|
||||
}
|
||||
|
||||
/* --- rename / add files / thumbnail drop, through the controller --- */
|
||||
{
|
||||
char p1[600], p2[600], img[600], out[600];
|
||||
NSMutableArray *paths = [NSMutableArray array];
|
||||
size_t before, after;
|
||||
SEL s;
|
||||
/* back to the playlists tab and the Genesis playlist (row 3) */
|
||||
[tabs selectTabViewItemAtIndex:0];
|
||||
pump(data, 200);
|
||||
companion_core_select_playlist(peek->core, 3);
|
||||
pump(data, 500);
|
||||
before = companion_core_entry_count(peek->core);
|
||||
CHECK(before == 2, "Genesis has 2 entries (got %u)", (unsigned)before);
|
||||
snprintf(p1, sizeof(p1), "%s/content/a.nes", root);
|
||||
snprintf(p2, sizeof(p2), "%s/content/sub", root);
|
||||
[paths addObject:[NSString stringWithUTF8String:p1]];
|
||||
[paths addObject:[NSString stringWithUTF8String:p2]];
|
||||
s = NSSelectorFromString(@"addPaths:");
|
||||
CHECK([ctrl respondsToSelector:s], "addPaths: implemented");
|
||||
{
|
||||
/* size_t return: NSInvocation, not performSelector (which is
|
||||
* for id returns) */
|
||||
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[ctrl methodSignatureForSelector:s]];
|
||||
size_t added = 0;
|
||||
[inv setSelector:s]; [inv setTarget:ctrl];
|
||||
[inv setArgument:&paths atIndex:2];
|
||||
[inv invoke];
|
||||
[inv getReturnValue:&added];
|
||||
printf(" addPaths: returned %u\n", (unsigned)added); fflush(stdout);
|
||||
}
|
||||
pump(data, 600);
|
||||
after = companion_core_entry_count(peek->core);
|
||||
CHECK(after == before + 2, "drop of a file and a directory added 2 (got %u)", (unsigned)after);
|
||||
|
||||
/* thumbnail drop onto the pane for the selected entry */
|
||||
snprintf(img, sizeof(img), "%s/drop2.tga", root);
|
||||
{
|
||||
FILE *f = fopen(img, "wb");
|
||||
uint8_t hdr[18]; int i;
|
||||
memset(hdr, 0, 18); hdr[2] = 2; hdr[12] = 8; hdr[14] = 8; hdr[16] = 32; hdr[17] = 0x28;
|
||||
fwrite(hdr, 1, 18, f);
|
||||
for (i = 0; i < 64; i++) { uint8_t px[4] = { 0x33, 0x66, 0x99, 0xff }; fwrite(px, 1, 4, f); }
|
||||
fclose(f);
|
||||
}
|
||||
[(NSTableView*)[entriesScroll documentView] selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
|
||||
pump(data, 100);
|
||||
s = NSSelectorFromString(@"installThumbnailFromPath:");
|
||||
CHECK([ctrl respondsToSelector:s], "installThumbnailFromPath: implemented");
|
||||
{
|
||||
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[ctrl methodSignatureForSelector:s]];
|
||||
const char *ip = img;
|
||||
BOOL ok = NO;
|
||||
[inv setSelector:s]; [inv setTarget:ctrl];
|
||||
[inv setArgument:&ip atIndex:2];
|
||||
[inv invoke];
|
||||
[inv getReturnValue:&ok];
|
||||
CHECK(ok, "thumbnail installed from a dropped image");
|
||||
}
|
||||
{
|
||||
const struct playlist_entry *e = companion_core_entry(peek->core, 0);
|
||||
char db[128];
|
||||
strlcpy(db, e->db_name, sizeof(db)); path_remove_extension(db);
|
||||
companion_core_thumbnail_path(peek->core, db, COMPANION_THUMB_BOXART, e->label, e->path, out, sizeof(out));
|
||||
CHECK(path_is_valid(out), "png exists at the repository path: %s", out);
|
||||
}
|
||||
|
||||
/* rename through the core-backed method */
|
||||
s = NSSelectorFromString(@"renamePlaylistAtRow:to:");
|
||||
CHECK([ctrl respondsToSelector:s], "rename implemented");
|
||||
{
|
||||
NSMethodSignature *sig = [ctrl methodSignatureForSelector:s];
|
||||
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
||||
NSInteger row = 3; const char *nm = "Sega - Genesis Renamed"; BOOL ok = NO;
|
||||
[inv setSelector:s]; [inv setTarget:ctrl];
|
||||
[inv setArgument:&row atIndex:2]; [inv setArgument:&nm atIndex:3];
|
||||
[inv invoke]; [inv getReturnValue:&ok];
|
||||
CHECK(ok, "rename accepted");
|
||||
}
|
||||
pump(data, 300);
|
||||
CHECK(string_is_equal(companion_core_playlist_name(peek->core, 3), "Sega - Genesis Renamed"), "list shows the new name (got %s)", companion_core_playlist_name(peek->core, 3));
|
||||
CHECK([leftTable numberOfRows] == 4, "playlist table reloaded (%ld rows)", (long)[leftTable numberOfRows]);
|
||||
}
|
||||
|
||||
/* --- Core Options and Shader Parameters windows --- */
|
||||
{
|
||||
static struct retro_core_option_v2_definition defs[3];
|
||||
struct retro_core_options_v2 v2;
|
||||
char cfg[600];
|
||||
NSTableView *ot, *st;
|
||||
NSWindow *ow, *sw;
|
||||
id ds;
|
||||
extern int stub_calls_shader_apply;
|
||||
memset(defs, 0, sizeof(defs));
|
||||
defs[0].key = "test_speed"; defs[0].desc = "Speed";
|
||||
defs[0].values[0].value = "slow"; defs[0].values[0].label = "Slow";
|
||||
defs[0].values[1].value = "fast"; defs[0].values[1].label = "Fast";
|
||||
defs[0].default_value = "fast";
|
||||
defs[1].key = "test_color"; defs[1].desc = "Colour";
|
||||
defs[1].values[0].value = "rgb"; defs[1].values[1].value = "mono";
|
||||
defs[1].default_value = "rgb";
|
||||
v2.categories = NULL; v2.definitions = defs;
|
||||
snprintf(cfg, sizeof(cfg), "%s/core.opt", root);
|
||||
test_runloop.core_options = core_option_manager_new(cfg, NULL, &v2, false);
|
||||
|
||||
[ctrl performSelector:NSSelectorFromString(@"showCoreOptions:") withObject:nil];
|
||||
pump(data, 200);
|
||||
ow = [ctrl valueForKey:@"optsWindow"];
|
||||
ot = [ctrl valueForKey:@"optsTable"];
|
||||
CHECK(ow && [ow isVisible] && ot, "Core Options window shown");
|
||||
CHECK([ot numberOfRows] == 2, "2 options listed (%ld)", (long)[ot numberOfRows]);
|
||||
ds = [ot dataSource];
|
||||
{
|
||||
id v = [ds tableView:ot objectValueForTableColumn:[ot tableColumnWithIdentifier:@"val"] row:0];
|
||||
CHECK([[v description] isEqualToString:@"Fast"], "value shows the default label (got %s)", [[v description] UTF8String]);
|
||||
}
|
||||
[ot selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
|
||||
[ctrl performSelector:NSSelectorFromString(@"optionCycle:") withObject:nil];
|
||||
{
|
||||
id v = [ds tableView:ot objectValueForTableColumn:[ot tableColumnWithIdentifier:@"val"] row:0];
|
||||
CHECK([[v description] isEqualToString:@"Slow"], "double-click cycles the value (got %s)", [[v description] UTF8String]);
|
||||
}
|
||||
[ctrl performSelector:NSSelectorFromString(@"optionReset:") withObject:nil];
|
||||
CHECK(companion_core_option_current(peek->core, 0) == 1, "Reset restores the default");
|
||||
[ow orderOut:nil];
|
||||
|
||||
[ctrl performSelector:NSSelectorFromString(@"showShaderParams:") withObject:nil];
|
||||
pump(data, 200);
|
||||
sw = [ctrl valueForKey:@"shpWindow"];
|
||||
st = [ctrl valueForKey:@"shpTable"];
|
||||
CHECK(sw && [sw isVisible] && st, "Shader Parameters window shown");
|
||||
CHECK([st numberOfRows] == 2, "2 parameters listed (%ld)", (long)[st numberOfRows]);
|
||||
ds = [st dataSource];
|
||||
{
|
||||
id v = [ds tableView:st objectValueForTableColumn:[st tableColumnWithIdentifier:@"range"] row:0];
|
||||
CHECK([[v description] isEqualToString:@"0 .. 1 (step 0.05)"], "range column (got %s)", [[v description] UTF8String]);
|
||||
}
|
||||
/* edit the Value cell as the table would */
|
||||
[ds tableView:st setObjectValue:@"0.75" forTableColumn:[st tableColumnWithIdentifier:@"pval"] row:0];
|
||||
CHECK(companion_core_shader_param_current(peek->core, 0) == 0.75f, "editing the cell sets the parameter");
|
||||
{
|
||||
int before = stub_calls_shader_apply;
|
||||
[ctrl performSelector:NSSelectorFromString(@"shaderApply:") withObject:nil];
|
||||
CHECK(stub_calls_shader_apply == before + 1, "Apply fires the shader-apply command");
|
||||
}
|
||||
[st selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
|
||||
[ctrl performSelector:NSSelectorFromString(@"shaderReset:") withObject:nil];
|
||||
CHECK(companion_core_shader_param_current(peek->core, 0) == 0.5f, "Reset restores the initial value");
|
||||
[sw orderOut:nil];
|
||||
core_option_manager_free(test_runloop.core_options);
|
||||
test_runloop.core_options = NULL;
|
||||
}
|
||||
|
||||
/* --- Options window --- */
|
||||
{
|
||||
NSWindow *ow; NSTableView *ot; id ds;
|
||||
[ctrl performSelector:NSSelectorFromString(@"showOptions:") withObject:nil];
|
||||
pump(data, 200);
|
||||
ow = [ctrl valueForKey:@"setWindow"]; ot = [ctrl valueForKey:@"setTable"];
|
||||
CHECK(ow && [ow isVisible] && ot, "Options window shown");
|
||||
CHECK([ot numberOfRows] == 13, "13 settings listed (%ld)", (long)[ot numberOfRows]);
|
||||
ds = [ot dataSource];
|
||||
test_settings.bools.desktop_menu_save_geometry = false;
|
||||
[ot selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
|
||||
[ctrl performSelector:NSSelectorFromString(@"settingActivate:") withObject:nil];
|
||||
CHECK(test_settings.bools.desktop_menu_save_geometry, "double-click toggles a bool");
|
||||
{
|
||||
id v = [ds tableView:ot objectValueForTableColumn:[ot tableColumnWithIdentifier:@"sval"] row:0];
|
||||
CHECK([[v description] isEqualToString:@"Yes"], "bool shows Yes (got %s)", [[v description] UTF8String]);
|
||||
}
|
||||
[ds tableView:ot setObjectValue:@"512" forTableColumn:[ot tableColumnWithIdentifier:@"sval"] row:7];
|
||||
CHECK(test_settings.uints.desktop_menu_thumbnail_cache_limit == 512, "editing a number sets it");
|
||||
[ds tableView:ot setObjectValue:@"Dark" forTableColumn:[ot tableColumnWithIdentifier:@"sval"] row:2];
|
||||
CHECK(test_settings.uints.desktop_menu_theme == 1, "theme set by label");
|
||||
[ow orderOut:nil];
|
||||
}
|
||||
|
||||
/* --- closing hands the keyboard back --- */
|
||||
{
|
||||
NSWindow *host = [(id)apple_platform hostWindow];
|
||||
[win makeKeyAndOrderFront:nil];
|
||||
pump(data, 100);
|
||||
CHECK([[win delegate] windowShouldClose:win] == YES, "windowShouldClose: lets AppKit close (YES)");
|
||||
[win close];
|
||||
pump(data, 200);
|
||||
CHECK(![win isVisible], "companion window hidden after close");
|
||||
CHECK([host isKeyWindow], "RetroArch's window is key again (keyboard goes to RAWindow -sendEvent:)");
|
||||
CHECK([host isMainWindow], "RetroArch's window is main again");
|
||||
CHECK([host firstResponder] == (NSResponder*)[(id)apple_platform renderView], "RetroArch's render view is first responder again");
|
||||
/* the real close: AppKit's own close after windowShouldClose: YES */
|
||||
[win makeKeyAndOrderFront:nil];
|
||||
pump(data, 50);
|
||||
[win performClose:nil];
|
||||
pump(data, 200);
|
||||
CHECK(![win isVisible], "companion window closed via performClose");
|
||||
CHECK([host isKeyWindow] && [host isMainWindow], "after AppKit's close: RetroArch's window key and main");
|
||||
CHECK([NSApp keyWindow] == host, "NSApp's key window is RetroArch's (got %s)",
|
||||
[NSApp keyWindow] == host ? "host" : ([NSApp keyWindow] ? [[[NSApp keyWindow] title] UTF8String] : "none"));
|
||||
/* The real question: does a keystroke reach RetroArch's window's
|
||||
* -sendEvent: now? AppKit routes key events to the key window. */
|
||||
{
|
||||
HarnessWindow *hw = (HarnessWindow*)host;
|
||||
NSEvent *kd, *ku;
|
||||
int before = hw->keyEvents;
|
||||
kd = [NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(10, 10) modifierFlags:0
|
||||
timestamp:0 windowNumber:[host windowNumber] context:nil
|
||||
characters:@"x" charactersIgnoringModifiers:@"x" isARepeat:NO keyCode:7];
|
||||
ku = [NSEvent keyEventWithType:NSKeyUp location:NSMakePoint(10, 10) modifierFlags:0
|
||||
timestamp:0 windowNumber:[host windowNumber] context:nil
|
||||
characters:@"x" charactersIgnoringModifiers:@"x" isARepeat:NO keyCode:7];
|
||||
[NSApp postEvent:kd atStart:NO];
|
||||
[NSApp postEvent:ku atStart:NO];
|
||||
pump(data, 300);
|
||||
CHECK(hw->keyEvents > before, "a keystroke after the close reaches RetroArch's window (-sendEvent: saw %d key events)", hw->keyEvents - before);
|
||||
}
|
||||
/* and the driver can show it again after a real close */
|
||||
ui_companion_wimp_cocoa.toggle(data, true);
|
||||
pump(data, 200);
|
||||
CHECK([win isVisible], "toggle shows the closed window again");
|
||||
CHECK([[ctrl valueForKey:@"playlists"] numberOfRows] == 4, "and it still lists the playlists");
|
||||
[win close];
|
||||
pump(data, 100);
|
||||
}
|
||||
|
||||
ui_companion_wimp_cocoa.deinit(data);
|
||||
companion_test_teardown_fixtures(root);
|
||||
[pool drain];
|
||||
if (fails)
|
||||
{
|
||||
printf("companion_cocoa_test: %d failure(s)\n", fails);
|
||||
return 1;
|
||||
}
|
||||
printf("companion_cocoa_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* The RetroArch state the companion core reaches for, replaced by
|
||||
* fixtures so companion_core.c + playlist.c link against libretro-common
|
||||
* alone. The test sets up @test_settings and @test_runloop. Everything
|
||||
* that would launch content, run a task or talk to the menu records the
|
||||
* call instead. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <retro_miscellaneous.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../runloop.h"
|
||||
#include "../../../core_info.h"
|
||||
#include "../../../msg_hash.h"
|
||||
#include "../../../command.h"
|
||||
#include "../../../content.h"
|
||||
#include "../../../tasks/task_content.h"
|
||||
#include "../../../tasks/tasks_internal.h"
|
||||
#include "../../../menu/menu_driver.h"
|
||||
#include "../../../input/input_driver.h"
|
||||
#include "../../../gfx/video_driver.h"
|
||||
#include "../../../paths.h"
|
||||
|
||||
settings_t test_settings;
|
||||
runloop_state_t test_runloop;
|
||||
static input_driver_state_t test_input;
|
||||
static video_driver_state_t test_video;
|
||||
static struct menu_state test_menu;
|
||||
|
||||
/* What the core asked RetroArch to do. */
|
||||
int stub_calls_load_with_new_core;
|
||||
int stub_calls_load_with_current_core;
|
||||
int stub_calls_start_core;
|
||||
int stub_calls_dbscan;
|
||||
int stub_calls_command;
|
||||
char stub_last_content[PATH_MAX_LENGTH];
|
||||
char stub_last_core[PATH_MAX_LENGTH];
|
||||
char stub_last_scan_dir[PATH_MAX_LENGTH];
|
||||
|
||||
settings_t *config_get_ptr(void) { return &test_settings; }
|
||||
runloop_state_t *runloop_state_get_ptr(void) { return &test_runloop; }
|
||||
input_driver_state_t *input_state_get_ptr(void) { return &test_input; }
|
||||
video_driver_state_t *video_state_get_ptr(void) { return &test_video; }
|
||||
struct menu_state *menu_state_get_ptr(void) { return &test_menu; }
|
||||
bool menu_driver_ctl(enum rarch_menu_ctl_state state, void *data) { (void)state; (void)data; return true; }
|
||||
uint8_t content_get_flags(void) { return 0; }
|
||||
|
||||
/* RetroArch's path slots: one string per type, enough for the core. */
|
||||
static char test_paths[16][PATH_MAX_LENGTH];
|
||||
const char *path_get(enum rarch_path_type type) { return (unsigned)type < 16 ? test_paths[type] : ""; }
|
||||
bool path_set(enum rarch_path_type type, const char *path)
|
||||
{
|
||||
if ((unsigned)type >= 16) return false;
|
||||
strlcpy(test_paths[type], path ? path : "", PATH_MAX_LENGTH);
|
||||
return true;
|
||||
}
|
||||
void path_clear(enum rarch_path_type type) { if ((unsigned)type < 16) test_paths[type][0] = '\0'; }
|
||||
|
||||
void RARCH_LOG(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_WARN(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_ERR(const char *fmt, ...) { (void)fmt; }
|
||||
void RARCH_DBG(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
/* No cores installed: the core-info world is empty. */
|
||||
bool core_info_find(const char *core_path, core_info_t **core_info) { (void)core_path; if (core_info) *core_info = NULL; return false; }
|
||||
bool core_info_get_list(core_info_list_t **list) { if (list) *list = NULL; return false; }
|
||||
bool core_info_init_current_core(void) { return false; }
|
||||
void core_info_list_get_supported_cores(core_info_list_t *l, const char *p,
|
||||
const core_info_t **infos, size_t *num) { (void)l; (void)p; if (infos) *infos = NULL; if (num) *num = 0; }
|
||||
bool core_info_list_update_missing_firmware(core_info_ctx_firmware_t *info) { (void)info; return false; }
|
||||
bool core_info_core_file_id_is_equal(const char *a, const char *b) { (void)a; (void)b; return false; }
|
||||
|
||||
const char *msg_hash_to_str(enum msg_hash_enums msg)
|
||||
{
|
||||
/* Only what the tests compare against by name. */
|
||||
switch (msg)
|
||||
{
|
||||
case MENU_ENUM_LABEL_VALUE_QT_ALL_PLAYLISTS: return "All Playlists";
|
||||
case MENU_ENUM_LABEL_VALUE_HISTORY_TAB: return "History";
|
||||
case MENU_ENUM_LABEL_VALUE_FAVORITES_TAB: return "Favorites";
|
||||
case MENU_ENUM_LABEL_VALUE_IMAGES_TAB: return "Images";
|
||||
case MENU_ENUM_LABEL_VALUE_MUSIC_TAB: return "Music";
|
||||
case MENU_ENUM_LABEL_VALUE_VIDEO_TAB: return "Videos";
|
||||
case MENU_ENUM_LABEL_VALUE_QT_VIEW_TYPE_LIST: return "List";
|
||||
case MENU_ENUM_LABEL_VALUE_QT_VIEW_TYPE_ICONS: return "Icons";
|
||||
default:
|
||||
{
|
||||
/* Distinct per message: a popup drops a second item with the
|
||||
* same title, so identical placeholders would collapse menus. */
|
||||
static char buf[8][32];
|
||||
static unsigned k;
|
||||
char *b = buf[k++ & 7];
|
||||
snprintf(b, 32, "msg#%u", (unsigned)msg);
|
||||
return b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* retroarch_ctl (core_option_manager asks whether a core is running). */
|
||||
bool retroarch_ctl(enum rarch_ctl_state state, void *data)
|
||||
{
|
||||
(void)state; (void)data;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* The menu shader, fabricated: two parameters. */
|
||||
#include "../../../gfx/video_shader_parse.h"
|
||||
static struct video_shader test_shader;
|
||||
struct video_shader *menu_shader_get(void)
|
||||
{
|
||||
if (!test_shader.num_parameters)
|
||||
{
|
||||
strlcpy(test_shader.path, "/shaders/crt.slangp", sizeof(test_shader.path));
|
||||
test_shader.num_parameters = 2;
|
||||
strlcpy(test_shader.parameters[0].id, "SCANLINE", 64);
|
||||
strlcpy(test_shader.parameters[0].desc, "Scanline strength", 64);
|
||||
test_shader.parameters[0].minimum = 0.0f; test_shader.parameters[0].maximum = 1.0f;
|
||||
test_shader.parameters[0].step = 0.05f; test_shader.parameters[0].initial = 0.5f;
|
||||
test_shader.parameters[0].current = 0.5f;
|
||||
strlcpy(test_shader.parameters[1].id, "CURV", 64);
|
||||
test_shader.parameters[1].desc[0] = '\0'; /* no desc: id shows */
|
||||
test_shader.parameters[1].minimum = 0.0f; test_shader.parameters[1].maximum = 10.0f;
|
||||
test_shader.parameters[1].step = 1.0f; test_shader.parameters[1].initial = 2.0f;
|
||||
test_shader.parameters[1].current = 2.0f;
|
||||
}
|
||||
return &test_shader;
|
||||
}
|
||||
|
||||
int stub_calls_shader_apply;
|
||||
|
||||
bool command_event(enum event_command cmd, void *data)
|
||||
{
|
||||
(void)data;
|
||||
stub_calls_command++;
|
||||
if (cmd == CMD_EVENT_SHADERS_APPLY_CHANGES)
|
||||
stub_calls_shader_apply++;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool task_push_load_content_with_new_core_from_companion_ui(
|
||||
const char *core_path, const char *fullpath, const char *label,
|
||||
const char *db_name, const char *crc32, content_ctx_info_t *ci,
|
||||
retro_task_callback_t cb, void *ud)
|
||||
{
|
||||
(void)label; (void)db_name; (void)crc32; (void)ci; (void)cb; (void)ud;
|
||||
stub_calls_load_with_new_core++;
|
||||
strlcpy(stub_last_core, core_path ? core_path : "", sizeof(stub_last_core));
|
||||
strlcpy(stub_last_content, fullpath ? fullpath : "", sizeof(stub_last_content));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool task_push_load_content_with_current_core_from_companion_ui(
|
||||
const char *fullpath, content_ctx_info_t *ci, enum rarch_core_type type,
|
||||
retro_task_callback_t cb, void *ud)
|
||||
{
|
||||
(void)ci; (void)type; (void)cb; (void)ud;
|
||||
stub_calls_load_with_current_core++;
|
||||
strlcpy(stub_last_content, fullpath ? fullpath : "", sizeof(stub_last_content));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool task_push_start_current_core(content_ctx_info_t *ci) { (void)ci; stub_calls_start_core++; return true; }
|
||||
|
||||
bool task_push_dbscan(const char *playlist_dir, const char *content_db,
|
||||
const char *path, bool directory, bool show_hidden, retro_task_callback_t cb)
|
||||
{
|
||||
(void)playlist_dir; (void)content_db; (void)directory; (void)show_hidden; (void)cb;
|
||||
stub_calls_dbscan++;
|
||||
strlcpy(stub_last_scan_dir, path ? path : "", sizeof(stub_last_scan_dir));
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,921 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Regression test for ui/companion/companion_core: the toolkit-free
|
||||
* model every desktop companion (Qt, Win32, Cocoa) drives. Links the
|
||||
* real companion_core.c and playlist.c against libretro-common, with
|
||||
* RetroArch's state replaced by fixtures (companion_core_stubs.c), and
|
||||
* runs on Linux from tools/companion_core_test.sh.
|
||||
*
|
||||
* Covered, against real .lpl files it writes itself:
|
||||
* - playlist listing: "All Playlists" first (token path), then the
|
||||
* special playlists that are configured, then the files, in order
|
||||
* - selecting a playlist parses it through the budgeted iterate and
|
||||
* exposes its entries
|
||||
* - "All Playlists" merges every file, sorted by label
|
||||
* case-insensitively, and maps each row back to its file and index
|
||||
* - the thumbnail path rule: repository layout, label sanitising,
|
||||
* extension probe, image content as its own thumbnail
|
||||
* - the file browser: folders first with "..", the folder / file
|
||||
* split, descending, going up, the root
|
||||
* - Run on an entry without a core reports needs-core with the path;
|
||||
* with a core path it pushes the load task with that core
|
||||
* - launch options with no cores installed: the entry's own core
|
||||
* (by path) is offered, nothing is duplicated */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <sys/utime.h>
|
||||
#else
|
||||
#include <utime.h>
|
||||
#endif
|
||||
|
||||
#include <boolean.h>
|
||||
#include <compat/strl.h>
|
||||
#include <retro_miscellaneous.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <file/file_path.h>
|
||||
#include <lists/string_list.h>
|
||||
#include <lists/dir_list.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../runloop.h"
|
||||
#include "../../../core_option_manager.h"
|
||||
#include "../companion_core.h"
|
||||
|
||||
extern settings_t test_settings;
|
||||
extern runloop_state_t test_runloop;
|
||||
extern int stub_calls_load_with_new_core;
|
||||
extern int stub_calls_load_with_current_core;
|
||||
extern char stub_last_content[PATH_MAX_LENGTH];
|
||||
extern char stub_last_core[PATH_MAX_LENGTH];
|
||||
|
||||
static int fails;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { fails++; printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static char root[512];
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
static void mkdirp(const char *p) { _mkdir(p); }
|
||||
static void sleep_briefly_ms(long ns) { Sleep((DWORD)(ns / 1000000 + 1)); }
|
||||
#else
|
||||
static void mkdirp(const char *p) { mkdir(p, 0755); }
|
||||
static void sleep_briefly_ms(long ns) { struct timespec ts; ts.tv_sec = 0; ts.tv_nsec = ns; nanosleep(&ts, NULL); }
|
||||
#endif
|
||||
|
||||
static int playlist_changed, playlists_changed, browse_changed;
|
||||
|
||||
static void on_playlists_changed(void *ud) { (void)ud; playlists_changed++; }
|
||||
static void on_playlist_changed(void *ud) { (void)ud; playlist_changed++; }
|
||||
static void on_browse_changed(void *ud) { (void)ud; browse_changed++; }
|
||||
|
||||
/* The listing is enumerated off the UI thread and lands in iterate():
|
||||
* spin it until the browse callback fires (or a timeout). */
|
||||
static bool wait_browse(companion_core_t *core)
|
||||
{
|
||||
int i;
|
||||
int before = browse_changed;
|
||||
for (i = 0; i < 200000 && browse_changed == before; i++)
|
||||
{
|
||||
companion_core_iterate(core, 1000);
|
||||
if (browse_changed == before)
|
||||
{
|
||||
sleep_briefly_ms(200000);
|
||||
}
|
||||
}
|
||||
return browse_changed != before;
|
||||
}
|
||||
|
||||
|
||||
static void writef(const char *path, const char *text)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f)
|
||||
{
|
||||
printf("cannot write %s\n", path);
|
||||
exit(2);
|
||||
}
|
||||
fputs(text, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
#include <formats/image.h>
|
||||
static void write_tga_rgba(const char *path, unsigned w, unsigned h, uint32_t argb)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
uint8_t hdr[18];
|
||||
unsigned i;
|
||||
if (!f) return;
|
||||
memset(hdr, 0, sizeof(hdr));
|
||||
hdr[2] = 2; hdr[12] = (uint8_t)w; hdr[13] = (uint8_t)(w >> 8);
|
||||
hdr[14] = (uint8_t)h; hdr[15] = (uint8_t)(h >> 8); hdr[16] = 32; hdr[17] = 0x28;
|
||||
fwrite(hdr, 1, 18, f);
|
||||
for (i = 0; i < w * h; i++)
|
||||
{
|
||||
uint8_t px[4];
|
||||
px[0] = (uint8_t)argb; px[1] = (uint8_t)(argb >> 8); px[2] = (uint8_t)(argb >> 16); px[3] = (uint8_t)(argb >> 24);
|
||||
fwrite(px, 1, 4, f);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void fixture(char *out, size_t len, const char *rel)
|
||||
{
|
||||
snprintf(out, len, "%s/%s", root, rel);
|
||||
}
|
||||
|
||||
/* A RetroArch JSON playlist with the given entries. */
|
||||
static void write_lpl(const char *rel, const char *default_core,
|
||||
const char *const *labels, const char *const *paths,
|
||||
const char *const *cores, int n)
|
||||
{
|
||||
char path[512];
|
||||
char buf[8192];
|
||||
int i, off = 0;
|
||||
fixture(path, sizeof(path), rel);
|
||||
off += snprintf(buf + off, sizeof(buf) - off,
|
||||
"{\n \"version\": \"1.5\",\n \"default_core_path\": \"%s\",\n"
|
||||
" \"default_core_name\": \"\",\n \"items\": [\n", default_core ? default_core : "");
|
||||
for (i = 0; i < n; i++)
|
||||
off += snprintf(buf + off, sizeof(buf) - off,
|
||||
" {\n \"path\": \"%s\",\n \"label\": \"%s\",\n"
|
||||
" \"core_path\": \"%s\",\n \"core_name\": \"DETECT\",\n"
|
||||
" \"crc32\": \"00000000|crc\",\n \"db_name\": \"%s\"\n }%s\n",
|
||||
paths[i], labels[i], cores ? cores[i] : "DETECT",
|
||||
rel, i + 1 < n ? "," : "");
|
||||
snprintf(buf + off, sizeof(buf) - off, " ]\n}\n");
|
||||
writef(path, buf);
|
||||
}
|
||||
|
||||
/* Run the budgeted iterate until the playlist callback fires. */
|
||||
static bool iterate_until_loaded(companion_core_t *core)
|
||||
{
|
||||
int i;
|
||||
int before = playlist_changed;
|
||||
for (i = 0; i < 100000 && playlist_changed == before; i++)
|
||||
companion_core_iterate(core, 2000);
|
||||
return playlist_changed != before;
|
||||
}
|
||||
|
||||
/* --- fixtures ------------------------------------------------------------- */
|
||||
|
||||
static const char *nes_labels[] = { "Zelda II - The Adventure of Link (USA)", "Metroid (USA)", "Alpha Mission (USA)" };
|
||||
static const char *nes_paths[] = { "/roms/nes/zelda2.nes", "/roms/nes/metroid.nes", "/roms/nes/alpha.nes" };
|
||||
static const char *nes_cores[] = { "DETECT", "/cores/fceumm_libretro.so", "DETECT" };
|
||||
static const char *gen_labels[] = { "sonic the hedgehog (USA)", "'89 Dennou Kyuusei Uranai (Japan)" };
|
||||
static const char *gen_paths[] = { "/roms/gen/sonic.md", "/roms/gen/dennou.md" };
|
||||
|
||||
static void setup(void)
|
||||
{
|
||||
char p[512];
|
||||
snprintf(root, sizeof(root), "/tmp/companion_core_test_%ld", (long)time(NULL));
|
||||
mkdirp(root);
|
||||
fixture(p, sizeof(p), "playlists"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "thumbnails"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "thumbnails/Nintendo - Nintendo Entertainment System"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "thumbnails/Nintendo - Nintendo Entertainment System/Named_Boxarts"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "content"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "content/sub"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "content/sub/deeper"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "content/a.nes"); writef(p, "x");
|
||||
fixture(p, sizeof(p), "content/b.sfc"); writef(p, "x");
|
||||
fixture(p, sizeof(p), "content/sub/c.gb"); writef(p, "x");
|
||||
fixture(p, sizeof(p), "content/cover.png"); writef(p, "x");
|
||||
|
||||
write_lpl("playlists/Nintendo - Nintendo Entertainment System.lpl",
|
||||
"/cores/nestopia_libretro.so", nes_labels, nes_paths, nes_cores, 3);
|
||||
write_lpl("playlists/Sega - Mega Drive - Genesis.lpl", NULL,
|
||||
gen_labels, gen_paths, NULL, 2);
|
||||
/* history is a special: configured path outside the playlist dir */
|
||||
write_lpl("history.lpl", NULL, gen_labels, gen_paths, NULL, 1);
|
||||
|
||||
/* a thumbnail whose file name is the sanitised label */
|
||||
fixture(p, sizeof(p),
|
||||
"thumbnails/Nintendo - Nintendo Entertainment System/Named_Boxarts/Zelda II - The Adventure of Link (USA).png");
|
||||
writef(p, "png");
|
||||
|
||||
memset(&test_settings, 0, sizeof(test_settings));
|
||||
memset(&test_runloop, 0, sizeof(test_runloop));
|
||||
fixture(test_settings.paths.directory_playlist, sizeof(test_settings.paths.directory_playlist), "playlists");
|
||||
fixture(test_settings.paths.directory_thumbnails, sizeof(test_settings.paths.directory_thumbnails), "thumbnails");
|
||||
fixture(test_settings.paths.directory_menu_content, sizeof(test_settings.paths.directory_menu_content), "content");
|
||||
fixture(test_settings.paths.path_content_history, sizeof(test_settings.paths.path_content_history), "history.lpl");
|
||||
/* favorites / images / music / video left empty: skipped in the list */
|
||||
}
|
||||
|
||||
/* Exported for other harnesses (companion_cocoa_test): the same
|
||||
* fixtures and stub settings. */
|
||||
void companion_test_setup_fixtures(char *out_root, size_t len)
|
||||
{
|
||||
setup();
|
||||
if (out_root && len)
|
||||
strlcpy(out_root, root, len);
|
||||
}
|
||||
|
||||
static void teardown(void);
|
||||
void companion_test_teardown_fixtures(const char *r)
|
||||
{
|
||||
(void)r;
|
||||
teardown();
|
||||
}
|
||||
|
||||
static void teardown(void)
|
||||
{
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", root);
|
||||
if (system(cmd) != 0)
|
||||
printf("(could not remove %s)\n", root);
|
||||
}
|
||||
|
||||
/* --- tests ---------------------------------------------------------------- */
|
||||
|
||||
static companion_core_t *make_core(void)
|
||||
{
|
||||
companion_callbacks_t cb;
|
||||
memset(&cb, 0, sizeof(cb));
|
||||
cb.on_playlists_changed = on_playlists_changed;
|
||||
cb.on_playlist_changed = on_playlist_changed;
|
||||
cb.on_browse_changed = on_browse_changed;
|
||||
return companion_core_new(&cb, NULL);
|
||||
}
|
||||
|
||||
static void test_playlist_listing(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
size_t n;
|
||||
char nes[512];
|
||||
companion_core_refresh_playlists(c);
|
||||
n = companion_core_playlist_count(c);
|
||||
/* All + History (the one special configured) + 2 files */
|
||||
CHECK(n == 4, "playlist count 4, got %u", (unsigned)n);
|
||||
CHECK(string_is_equal(companion_core_playlist_name(c, 0), "All Playlists"), "slot 0 is All Playlists");
|
||||
CHECK(string_is_equal(companion_core_playlist_path(c, 0), COMPANION_ALL_PLAYLISTS_TOKEN), "slot 0 path is the token");
|
||||
CHECK(string_is_equal(companion_core_playlist_name(c, 1), "History"), "slot 1 is History (got %s)", companion_core_playlist_name(c, 1));
|
||||
CHECK(string_is_equal(companion_core_playlist_name(c, 2), "Nintendo - Nintendo Entertainment System"), "files sorted: NES first (got %s)", companion_core_playlist_name(c, 2));
|
||||
CHECK(string_is_equal(companion_core_playlist_name(c, 3), "Sega - Mega Drive - Genesis"), "then Genesis");
|
||||
fixture(nes, sizeof(nes), "playlists/Nintendo - Nintendo Entertainment System.lpl");
|
||||
CHECK(string_is_equal(companion_core_playlist_path(c, 2), nes), "file path");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_select_and_entries(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
const struct playlist_entry *e;
|
||||
companion_core_refresh_playlists(c);
|
||||
CHECK(companion_core_select_playlist(c, 2), "select NES");
|
||||
CHECK(iterate_until_loaded(c), "parse finished through iterate");
|
||||
CHECK(companion_core_entry_count(c) == 3, "3 entries, got %u", (unsigned)companion_core_entry_count(c));
|
||||
e = companion_core_entry(c, 1);
|
||||
CHECK(e && string_is_equal(e->label, "Metroid (USA)"), "entry 1 label");
|
||||
CHECK(e && string_is_equal(e->core_path, "/cores/fceumm_libretro.so"), "entry 1 core path");
|
||||
CHECK(string_is_equal(companion_core_entry_playlist_path(c, 1), companion_core_playlist_path(c, 2)), "entry's playlist is the selected file");
|
||||
CHECK(companion_core_entry_index_in_playlist(c, 1) == 1, "index within file");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_all_playlists(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
const struct playlist_entry *e;
|
||||
char gen[512];
|
||||
companion_core_refresh_playlists(c);
|
||||
CHECK(companion_core_select_playlist(c, 0), "select All");
|
||||
CHECK(iterate_until_loaded(c), "aggregate finished");
|
||||
/* NES 3 + Genesis 2 = 5 (History is a special, not a file: not merged) */
|
||||
CHECK(companion_core_entry_count(c) == 5, "5 merged entries, got %u", (unsigned)companion_core_entry_count(c));
|
||||
e = companion_core_entry(c, 0);
|
||||
CHECK(e && string_is_equal(e->label, "'89 Dennou Kyuusei Uranai (Japan)"), "sorted: '89 first (got %s)", e ? e->label : "-");
|
||||
e = companion_core_entry(c, 1);
|
||||
CHECK(e && string_is_equal(e->label, "Alpha Mission (USA)"), "then Alpha (got %s)", e ? e->label : "-");
|
||||
e = companion_core_entry(c, 3);
|
||||
CHECK(e && string_is_equal(e->label, "sonic the hedgehog (USA)"), "case-insensitive: sonic before Zelda (got %s)", e ? e->label : "-");
|
||||
fixture(gen, sizeof(gen), "playlists/Sega - Mega Drive - Genesis.lpl");
|
||||
CHECK(string_is_equal(companion_core_entry_playlist_path(c, 0), gen), "row 0 came from the Genesis file");
|
||||
CHECK(companion_core_entry_index_in_playlist(c, 0) == 1, "and is its second entry");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_thumbnail_path(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char out[PATH_MAX_LENGTH], want[PATH_MAX_LENGTH], img[512];
|
||||
size_t n;
|
||||
/* existing file: found through the extension probe */
|
||||
n = companion_core_thumbnail_path(c, "Nintendo - Nintendo Entertainment System",
|
||||
COMPANION_THUMB_BOXART, "Zelda II - The Adventure of Link (USA)",
|
||||
"/roms/nes/zelda2.nes", out, sizeof(out));
|
||||
fixture(want, sizeof(want),
|
||||
"thumbnails/Nintendo - Nintendo Entertainment System/Named_Boxarts/Zelda II - The Adventure of Link (USA).png");
|
||||
CHECK(n > 0 && string_is_equal(out, want), "existing thumbnail found: %s", out);
|
||||
/* missing: the default .png path (the save target), which does not
|
||||
* exist - backends decide by path_is_valid, and a decode of it
|
||||
* fails cleanly */
|
||||
n = companion_core_thumbnail_path(c, "Nintendo - Nintendo Entertainment System",
|
||||
COMPANION_THUMB_BOXART, "Metroid (USA)", "/roms/nes/metroid.nes", out, sizeof(out));
|
||||
CHECK(n > 0 && string_ends_with(out, "Named_Boxarts/Metroid (USA).png"), "missing thumbnail gives the default path: %s", out);
|
||||
CHECK(!path_is_valid(out), "which does not exist");
|
||||
/* sanitising: a label with the characters the repository replaces */
|
||||
fixture(want, sizeof(want),
|
||||
"thumbnails/Nintendo - Nintendo Entertainment System/Named_Boxarts/A_B_C_D_E_F_G_H_I.png");
|
||||
writef(want, "png");
|
||||
n = companion_core_thumbnail_path(c, "Nintendo - Nintendo Entertainment System",
|
||||
COMPANION_THUMB_BOXART, "A&B*C/D:E`F<G>H?I", "/x", out, sizeof(out));
|
||||
CHECK(n > 0 && string_is_equal(out, want), "&*/:`<>? sanitised to _: %s", out);
|
||||
/* image content is its own thumbnail */
|
||||
fixture(img, sizeof(img), "content/cover.png");
|
||||
n = companion_core_thumbnail_path(c, "Whatever", COMPANION_THUMB_BOXART,
|
||||
"cover", img, out, sizeof(out));
|
||||
CHECK(n > 0 && string_is_equal(out, img), "image content is its own thumbnail");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_browser(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char content[512], sub[512];
|
||||
char buf[64];
|
||||
size_t n, dc;
|
||||
bool needs_core = false;
|
||||
char pick[PATH_MAX_LENGTH];
|
||||
fixture(content, sizeof(content), "content");
|
||||
fixture(sub, sizeof(sub), "content/sub");
|
||||
CHECK(companion_core_browse_open(c, NULL), "open defaults to the content directory");
|
||||
CHECK(wait_browse(c), "listing lands through iterate()");
|
||||
CHECK(!companion_core_browse_busy(c), "not busy once landed");
|
||||
CHECK(string_is_equal(companion_core_browse_dir(c), content), "browse dir");
|
||||
n = companion_core_browse_count(c);
|
||||
dc = companion_core_browse_dir_count(c);
|
||||
/* "..", sub | a.nes, b.sfc, cover.png */
|
||||
CHECK(n == 5, "5 entries (.. + 1 dir + 3 files), got %u", (unsigned)n);
|
||||
CHECK(dc == 2, "2 folder rows (.. and sub), got %u", (unsigned)dc);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 0), ".."), "first is ..");
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 1), "sub"), "then the folder");
|
||||
CHECK(companion_core_browse_is_dir(c, 1) && !companion_core_browse_is_dir(c, 2), "dir / file flags");
|
||||
/* metadata gathered with the listing, formatted by the core */
|
||||
CHECK(companion_core_browse_size(c, 2) == 1, "a.nes is 1 byte (got %u)", (unsigned)companion_core_browse_size(c, 2));
|
||||
CHECK(string_is_equal(companion_core_browse_size_str(c, 2, buf, sizeof(buf)), "1 KB"), "size string: %s", buf);
|
||||
CHECK(string_is_equal(companion_core_browse_size_str(c, 1, buf, sizeof(buf)), ""), "folders have no size");
|
||||
CHECK(string_is_equal(companion_core_browse_type_str(c, 2, buf, sizeof(buf)), "NES File"), "type string: %s", buf);
|
||||
CHECK(string_is_equal(companion_core_browse_type_str(c, 1, buf, sizeof(buf)), "File Folder"), "folder type: %s", buf);
|
||||
CHECK(string_is_equal(companion_core_browse_type_str(c, 0, buf, sizeof(buf)), ""), ".. has no type");
|
||||
CHECK(companion_core_browse_mtime(c, 2) > 1000000000, "mtime is a real time");
|
||||
CHECK(strlen(companion_core_browse_date_str(c, 2, buf, sizeof(buf))) == 16, "date string YYYY-MM-DD HH:MM: %s", buf);
|
||||
/* descend */
|
||||
CHECK(companion_core_browse_activate(c, 1, NULL, &needs_core, pick, sizeof(pick)) == 0, "activate a folder descends");
|
||||
CHECK(wait_browse(c), "sub lands");
|
||||
CHECK(string_is_equal(companion_core_browse_dir(c), sub), "in sub");
|
||||
CHECK(companion_core_browse_dir_count(c) == 2, "sub: .. and deeper");
|
||||
CHECK(companion_core_browse_count(c) == 3, "sub: .. deeper c.gb");
|
||||
/* a file with no core: needs-core with its path */
|
||||
needs_core = false;
|
||||
CHECK(companion_core_browse_activate(c, 2, NULL, &needs_core, pick, sizeof(pick)) < 0 && needs_core, "file without a core asks for one");
|
||||
CHECK(strstr(pick, "c.gb") != NULL, "and hands back its path: %s", pick);
|
||||
/* up */
|
||||
CHECK(companion_core_browse_up(c), "up");
|
||||
CHECK(wait_browse(c), "parent lands");
|
||||
CHECK(string_is_equal(companion_core_browse_dir(c), content), "back at content (dir is [%s])", companion_core_browse_dir(c));
|
||||
/* supersession: open two directories back to back; only the second
|
||||
* lands, and nothing blocks meanwhile */
|
||||
browse_changed = 0;
|
||||
CHECK(companion_core_browse_open(c, sub), "open sub");
|
||||
CHECK(companion_core_browse_open(c, content), "open content right after");
|
||||
CHECK(wait_browse(c), "the later open lands");
|
||||
CHECK(string_is_equal(companion_core_browse_dir(c), content), "and it is content, not sub");
|
||||
{
|
||||
sleep_briefly_ms(20000000);
|
||||
companion_core_iterate(c, 1000);
|
||||
}
|
||||
CHECK(browse_changed == 1, "the superseded open never delivered (callbacks: %d)", browse_changed);
|
||||
/* keep going up to the root: eventually no parent */
|
||||
{
|
||||
int guard = 0;
|
||||
while (companion_core_browse_up(c) && guard++ < 64)
|
||||
wait_browse(c);
|
||||
CHECK(guard < 64, "reaches a top (dir is [%s])", companion_core_browse_dir(c));
|
||||
CHECK(!companion_core_browse_up(c), "up at the top is refused");
|
||||
}
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_browser_sort(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char big[512];
|
||||
size_t n;
|
||||
/* a bigger file so size order differs from name order */
|
||||
fixture(big, sizeof(big), "content/zz_big.sfc");
|
||||
writef(big, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
|
||||
browse_changed = 0;
|
||||
companion_core_browse_open(c, NULL);
|
||||
wait_browse(c);
|
||||
n = companion_core_browse_count(c);
|
||||
/* .., sub | a.nes b.sfc cover.png zz_big.sfc */
|
||||
CHECK(n == 6, "6 entries, got %u", (unsigned)n);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "a.nes"), "default: a.nes first file");
|
||||
|
||||
browse_changed = 0;
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_NAME, false);
|
||||
CHECK(browse_changed == 1, "sort fires on_browse_changed");
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 0), ".."), "name desc: .. still first");
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 1), "sub"), "name desc: folders still before files");
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "zz_big.sfc"), "name desc: zz_big first file (got %s)", companion_core_browse_name(c, 2));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 5), "a.nes"), "name desc: a.nes last");
|
||||
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_SIZE, false);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "zz_big.sfc"), "size desc: biggest first (got %s)", companion_core_browse_name(c, 2));
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_SIZE, true);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 5), "zz_big.sfc"), "size asc: biggest last (got %s)", companion_core_browse_name(c, 5));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "a.nes"), "size asc ties broken by name: a.nes (got %s)", companion_core_browse_name(c, 2));
|
||||
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_TYPE, true);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "a.nes"), "type asc: nes < png < sfc (got %s)", companion_core_browse_name(c, 2));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 3), "cover.png"), "then png (got %s)", companion_core_browse_name(c, 3));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 4), "b.sfc"), "then sfc by name: b before zz (got %s)", companion_core_browse_name(c, 4));
|
||||
|
||||
/* Date: distinct mtimes set explicitly (the files were written within
|
||||
* the same second), newest first when descending. */
|
||||
{
|
||||
char pa[512], pb[512], pc[512];
|
||||
#ifdef _WIN32
|
||||
struct _utimbuf ut;
|
||||
#define utime _utime
|
||||
#else
|
||||
struct utimbuf ut;
|
||||
#endif
|
||||
fixture(pa, sizeof(pa), "content/a.nes");
|
||||
fixture(pb, sizeof(pb), "content/b.sfc");
|
||||
fixture(pc, sizeof(pc), "content/cover.png");
|
||||
ut.actime = ut.modtime = 1000000000; utime(pa, &ut); /* oldest */
|
||||
ut.actime = ut.modtime = 1300000000; utime(pb, &ut);
|
||||
ut.actime = ut.modtime = 1200000000; utime(pc, &ut);
|
||||
companion_core_browse_open(c, NULL);
|
||||
wait_browse(c);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_DATE, false);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "zz_big.sfc"), "date desc: the just-written file newest (got %s)", companion_core_browse_name(c, 2));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 3), "b.sfc"), "then b (1.3e9) (got %s)", companion_core_browse_name(c, 3));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 4), "cover.png"), "then cover (1.2e9) (got %s)", companion_core_browse_name(c, 4));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 5), "a.nes"), "a.nes oldest last (got %s)", companion_core_browse_name(c, 5));
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_DATE, true);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "a.nes"), "date asc: oldest first (got %s)", companion_core_browse_name(c, 2));
|
||||
}
|
||||
/* Type: many extensions, grouped and ordered by extension, names
|
||||
* within a group. */
|
||||
{
|
||||
static const char *files[] = { "content/m.zip", "content/k.iso", "content/z.bin", "content/l.iso" };
|
||||
char p[512];
|
||||
int i;
|
||||
for (i = 0; i < 4; i++) { fixture(p, sizeof(p), files[i]); writef(p, "x"); }
|
||||
companion_core_browse_open(c, NULL);
|
||||
wait_browse(c);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_TYPE, true);
|
||||
/* bin < iso < nes < png < sfc < zip */
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "z.bin"), "type asc: bin first (got %s)", companion_core_browse_name(c, 2));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 3), "k.iso"), "iso: k before l (got %s)", companion_core_browse_name(c, 3));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 4), "l.iso"), "iso: then l (got %s)", companion_core_browse_name(c, 4));
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 9), "m.zip"), "zip last (got %s)", companion_core_browse_name(c, 9));
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_TYPE, false);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "m.zip"), "type desc: zip first (got %s)", companion_core_browse_name(c, 2));
|
||||
/* the four sorts give four different first files */
|
||||
{
|
||||
char first[4][64];
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_NAME, true);
|
||||
strlcpy(first[0], companion_core_browse_name(c, 2), 64);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_SIZE, false);
|
||||
strlcpy(first[1], companion_core_browse_name(c, 2), 64);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_TYPE, true);
|
||||
strlcpy(first[2], companion_core_browse_name(c, 2), 64);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_DATE, true);
|
||||
strlcpy(first[3], companion_core_browse_name(c, 2), 64);
|
||||
CHECK(!string_is_equal(first[0], first[1]) && !string_is_equal(first[1], first[2])
|
||||
&& !string_is_equal(first[2], first[3]) && !string_is_equal(first[0], first[2]),
|
||||
"each column orders differently: name=%s size=%s type=%s date=%s",
|
||||
first[0], first[1], first[2], first[3]);
|
||||
}
|
||||
/* clean up the extras for the tests after this one */
|
||||
for (i = 0; i < 4; i++) { fixture(p, sizeof(p), files[i]); remove(p); }
|
||||
}
|
||||
|
||||
/* the order persists across a re-enumeration */
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_NAME, false);
|
||||
companion_core_browse_open(c, NULL);
|
||||
wait_browse(c);
|
||||
CHECK(companion_core_browse_sort_column(c) == COMPANION_BROWSE_SORT_NAME && !companion_core_browse_sort_ascending(c), "sort setting kept");
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 2), "zz_big.sfc"), "re-opened listing is in the chosen order (got %s)", companion_core_browse_name(c, 2));
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
void companion_core_test_sort_listing(companion_core_t *core,
|
||||
struct string_list *list, uint64_t *size, int64_t *mtime,
|
||||
enum companion_browse_column column, bool ascending);
|
||||
|
||||
/* The crash: a listing with no metadata arrays (the Windows drive list
|
||||
* was built that way) sorted by Date or Size dereferenced NULL. Built
|
||||
* here directly, on every platform, so the comparator's NULL-safety is
|
||||
* tested where the drive list cannot be. */
|
||||
static void test_sort_without_metadata(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
struct string_list *l = string_list_new();
|
||||
union string_list_elem_attr attr;
|
||||
int col, asc;
|
||||
attr.i = RARCH_DIRECTORY;
|
||||
string_list_append(l, "C:\\", attr);
|
||||
string_list_append(l, "A:\\", attr);
|
||||
string_list_append(l, "D:\\", attr);
|
||||
for (col = COMPANION_BROWSE_SORT_NAME; col <= COMPANION_BROWSE_SORT_DATE; col++)
|
||||
for (asc = 0; asc < 2; asc++)
|
||||
companion_core_test_sort_listing(c, l, NULL, NULL,
|
||||
(enum companion_browse_column)col, asc != 0);
|
||||
companion_core_test_sort_listing(c, l, NULL, NULL, COMPANION_BROWSE_SORT_NAME, true);
|
||||
CHECK(string_is_equal(l->elems[0].data, "A:\\"), "sorted by name with no metadata: A first (got %s)", l->elems[0].data);
|
||||
string_list_free(l);
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
/* Then the same shape through the real job path: sort by Date (or
|
||||
* Size), then open a listing made only of folders. */
|
||||
static void test_sort_then_folders_only(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char p[512];
|
||||
int col;
|
||||
fixture(p, sizeof(p), "onlydirs"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "onlydirs/b"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "onlydirs/a"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "onlydirs/c"); mkdirp(p);
|
||||
fixture(p, sizeof(p), "onlydirs");
|
||||
for (col = COMPANION_BROWSE_SORT_NAME; col <= COMPANION_BROWSE_SORT_DATE; col++)
|
||||
{
|
||||
int asc;
|
||||
for (asc = 0; asc < 2; asc++)
|
||||
{
|
||||
companion_core_browse_sort(c, (enum companion_browse_column)col, asc != 0);
|
||||
browse_changed = 0;
|
||||
CHECK(companion_core_browse_open(c, p), "open folders-only (col %d asc %d)", col, asc);
|
||||
CHECK(wait_browse(c), "lands (col %d asc %d)", col, asc);
|
||||
CHECK(companion_core_browse_count(c) == 4, "4 entries (.. a b c)");
|
||||
CHECK(companion_core_browse_dir_count(c) == 4, "all folders");
|
||||
/* and sorting the landed listing again, every column */
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_DATE, false);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_SIZE, true);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_TYPE, false);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_NAME, true);
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 0), ".."), ".. first");
|
||||
CHECK(string_is_equal(companion_core_browse_name(c, 1), "a"), "then a (got %s)", companion_core_browse_name(c, 1));
|
||||
}
|
||||
}
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
/* A view that, on every "listing changed", re-applies the sort it is
|
||||
* showing (Qt's table does this on a model reset). With a sort that
|
||||
* fired the callback even when nothing changed, that recursed until the
|
||||
* stack ran out. The listener counts its depth and stops itself at 3,
|
||||
* so the test reports the loop instead of crashing in it. */
|
||||
static int reapply_depth, reapply_max;
|
||||
static void on_browse_changed_reapply(void *ud)
|
||||
{
|
||||
companion_core_t *c = (companion_core_t*)ud;
|
||||
reapply_depth++;
|
||||
if (reapply_depth > reapply_max)
|
||||
reapply_max = reapply_depth;
|
||||
if (reapply_depth <= 3)
|
||||
companion_core_browse_sort(c, companion_core_browse_sort_column(c),
|
||||
companion_core_browse_sort_ascending(c));
|
||||
reapply_depth--;
|
||||
}
|
||||
|
||||
static void test_sort_callback_reentrancy(void)
|
||||
{
|
||||
companion_callbacks_t cb;
|
||||
companion_core_t *c;
|
||||
char content[512];
|
||||
fixture(content, sizeof(content), "content");
|
||||
memset(&cb, 0, sizeof(cb));
|
||||
cb.on_browse_changed = on_browse_changed_reapply;
|
||||
c = companion_core_new(&cb, NULL);
|
||||
companion_core_set_ud(c, c); /* the listener sorts this core */
|
||||
reapply_depth = reapply_max = 0;
|
||||
companion_core_browse_open(c, content);
|
||||
wait_browse(c);
|
||||
CHECK(reapply_max <= 1, "listing landed: callback depth %d (a loop would be 3+)", reapply_max);
|
||||
reapply_max = 0;
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_SIZE, false);
|
||||
CHECK(reapply_max == 1, "one change -> one callback, no recursion (depth %d)", reapply_max);
|
||||
reapply_max = 0;
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_SIZE, false);
|
||||
CHECK(reapply_max == 0, "same sort again -> no callback at all (depth %d)", reapply_max);
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
/* Scenario: the click sequences the backends send, in order, the way
|
||||
* a user drives the browser - every step must complete and nothing may
|
||||
* fault. Run under ASan / TSan this is the regression net for the
|
||||
* core side of every companion. */
|
||||
static void test_backend_scenario(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char content[512], pick[PATH_MAX_LENGTH], buf[64];
|
||||
bool needs_core = false;
|
||||
size_t i, n;
|
||||
fixture(content, sizeof(content), "content");
|
||||
|
||||
/* tab -> File Browser: open default, listing lands */
|
||||
CHECK(companion_core_browse_open(c, NULL) && wait_browse(c), "enter browser");
|
||||
/* header clicks: every column, both ways, on the live listing */
|
||||
for (i = 0; i < 8; i++)
|
||||
companion_core_browse_sort(c, (enum companion_browse_column)(i % 4), (i & 1) != 0);
|
||||
/* select each row (the backends resolve name / path / type / size /
|
||||
* date / is-dir for the boxart pane and the table) */
|
||||
n = companion_core_browse_count(c);
|
||||
for (i = 0; i < n; i++)
|
||||
{
|
||||
CHECK(companion_core_browse_name(c, i) != NULL, "name %u", (unsigned)i);
|
||||
CHECK(companion_core_browse_path(c, i) != NULL, "path %u", (unsigned)i);
|
||||
companion_core_browse_size_str(c, i, buf, sizeof(buf));
|
||||
companion_core_browse_type_str(c, i, buf, sizeof(buf));
|
||||
companion_core_browse_date_str(c, i, buf, sizeof(buf));
|
||||
(void)companion_core_browse_is_dir(c, i);
|
||||
(void)companion_core_browse_size(c, i);
|
||||
(void)companion_core_browse_mtime(c, i);
|
||||
}
|
||||
/* double-click the folder, then a file, then Up, then the buttons */
|
||||
CHECK(companion_core_browse_activate(c, 1, NULL, &needs_core, pick, sizeof(pick)) == 0 && wait_browse(c), "descend");
|
||||
CHECK(companion_core_browse_activate(c, 2, NULL, &needs_core, pick, sizeof(pick)) < 0, "activate a file");
|
||||
CHECK(companion_core_browse_up(c) && wait_browse(c), "Up");
|
||||
CHECK(companion_core_browse_open(c, content) && wait_browse(c), "Start Directory");
|
||||
/* rapid navigation: several opens without waiting */
|
||||
companion_core_browse_open(c, content);
|
||||
companion_core_browse_up(c);
|
||||
companion_core_browse_open(c, content);
|
||||
companion_core_browse_up(c);
|
||||
CHECK(wait_browse(c), "rapid navigation settles");
|
||||
/* and the sort again on whatever landed */
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_DATE, false);
|
||||
companion_core_browse_sort(c, COMPANION_BROWSE_SORT_NAME, true);
|
||||
/* out of the browser and back into a playlist */
|
||||
companion_core_refresh_playlists(c);
|
||||
CHECK(companion_core_select_playlist(c, 2) && iterate_until_loaded(c), "back to a playlist");
|
||||
CHECK(companion_core_entry_count(c) == 3, "entries intact");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
/* Rename: only within the playlists directory; not over an existing
|
||||
* name; the list refreshes. Add files: files and directories (walked),
|
||||
* label from the file name, db_name from the playlist. Thumbnail
|
||||
* install: a PNG lands at the repository path, downscaled to the
|
||||
* setting. */
|
||||
static void test_rename_add_install(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char gen[512], out[PATH_MAX_LENGTH], hist[512], p[512], sub[512];
|
||||
const char *paths[2];
|
||||
size_t n;
|
||||
companion_core_refresh_playlists(c);
|
||||
fixture(gen, sizeof(gen), "playlists/Sega - Mega Drive - Genesis.lpl");
|
||||
fixture(hist, sizeof(hist), "history.lpl");
|
||||
CHECK(!companion_core_playlist_rename(c, hist, "Other", out, sizeof(out)), "history (outside the dir) cannot be renamed");
|
||||
CHECK(!companion_core_playlist_rename(c, gen, "Nintendo - Nintendo Entertainment System", out, sizeof(out)), "not over an existing playlist");
|
||||
CHECK(!companion_core_playlist_rename(c, gen, "a/b", out, sizeof(out)), "no path separators");
|
||||
CHECK(companion_core_playlist_rename(c, gen, "Sega - Genesis", out, sizeof(out)), "rename");
|
||||
CHECK(string_ends_with(out, "playlists/Sega - Genesis.lpl"), "new path %s", out);
|
||||
CHECK(path_is_valid(out) && !path_is_valid(gen), "file moved");
|
||||
CHECK(string_is_equal(companion_core_playlist_name(c, 3), "Sega - Genesis"), "list refreshed (got %s)", companion_core_playlist_name(c, 3));
|
||||
/* add: one file and one directory (content/sub has c.gb) */
|
||||
fixture(p, sizeof(p), "content/a.nes");
|
||||
fixture(sub, sizeof(sub), "content/sub");
|
||||
paths[0] = p; paths[1] = sub;
|
||||
n = companion_core_playlist_add_files(c, out, paths, 2, NULL, NULL);
|
||||
CHECK(n == 2, "added a.nes and sub/c.gb (got %u)", (unsigned)n);
|
||||
CHECK(companion_core_select_playlist_path(c, out) && iterate_until_loaded(c), "reload the renamed playlist");
|
||||
CHECK(companion_core_entry_count(c) == 4, "2 + 2 entries (got %u)", (unsigned)companion_core_entry_count(c));
|
||||
{
|
||||
/* entries are shown in the file's order; the two added are last */
|
||||
const struct playlist_entry *e = NULL;
|
||||
size_t k;
|
||||
for (k = 0; k < companion_core_entry_count(c); k++)
|
||||
if (string_is_equal(companion_core_entry(c, k)->label, "a"))
|
||||
e = companion_core_entry(c, k);
|
||||
CHECK(e != NULL, "added entry 'a' present");
|
||||
CHECK(e && string_is_equal(e->label, "a"), "label is the file name without extension (got %s)", e ? e->label : "-");
|
||||
CHECK(e && string_is_equal(e->db_name, "Sega - Genesis.lpl"), "db_name is the playlist (got %s)", e ? e->db_name : "-");
|
||||
CHECK(e && string_is_equal(e->core_path, "DETECT"), "core DETECT");
|
||||
}
|
||||
CHECK(companion_core_playlist_add_files(c, COMPANION_ALL_PLAYLISTS_TOKEN, paths, 1, NULL, NULL) == 0, "All Playlists takes no files");
|
||||
/* The backends pass the core's own selected path back in: the
|
||||
* reload must not copy that buffer onto itself (an overlapping
|
||||
* strlcpy traps on macOS's fortified libc; this is what the harness
|
||||
* hit on Apple's runner). */
|
||||
{
|
||||
const char *own = companion_core_selected_playlist_path(c);
|
||||
size_t before = companion_core_entry_count(c);
|
||||
char bsfc[512];
|
||||
const char *one[1];
|
||||
fixture(bsfc, sizeof(bsfc), "content/b.sfc"); /* not in the playlist yet */
|
||||
one[0] = bsfc;
|
||||
CHECK(own && *own, "a playlist is selected");
|
||||
n = companion_core_playlist_add_files(c, own, one, 1, NULL, NULL);
|
||||
CHECK(n == 1, "add through the selected-path alias (got %u)", (unsigned)n);
|
||||
CHECK(iterate_until_loaded(c), "reload landed");
|
||||
CHECK(companion_core_entry_count(c) == before + 1, "entries %u -> %u", (unsigned)before, (unsigned)companion_core_entry_count(c));
|
||||
CHECK(string_is_equal(companion_core_selected_playlist_path(c), own), "selected path intact");
|
||||
}
|
||||
/* thumbnail install from a TGA dropped on the boxart pane */
|
||||
{
|
||||
char img[512];
|
||||
fixture(img, sizeof(img), "drop.tga");
|
||||
write_tga_rgba(img, 64, 32, 0xff336699u);
|
||||
test_settings.uints.desktop_menu_thumbnail_max_size = 16;
|
||||
CHECK(companion_core_thumbnail_install(c, "Sega - Genesis", COMPANION_THUMB_BOXART,
|
||||
"a", img, out, sizeof(out)), "install");
|
||||
CHECK(string_ends_with(out, "Sega - Genesis/Named_Boxarts/a.png"), "at the repository path: %s", out);
|
||||
CHECK(path_is_valid(out), "png written");
|
||||
{
|
||||
struct texture_image ti;
|
||||
memset(&ti, 0, sizeof(ti));
|
||||
CHECK(image_texture_load(&ti, out) && ti.width == 16 && ti.height == 8, "downscaled to max size 16 (got %ux%u)", ti.width, ti.height);
|
||||
image_texture_free(&ti);
|
||||
}
|
||||
test_settings.uints.desktop_menu_thumbnail_max_size = 0;
|
||||
}
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
/* Core options and shader parameters as the dialogs see them: from a
|
||||
* real core_option_manager built from v2 definitions, and the stub
|
||||
* menu shader. */
|
||||
static void test_options_and_shader_params(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
static struct retro_core_option_v2_definition defs[3];
|
||||
static const struct retro_core_option_v2_definition none = { 0 };
|
||||
struct retro_core_options_v2 v2;
|
||||
char cfg[512];
|
||||
float mn, mx, stp, ini;
|
||||
memset(defs, 0, sizeof(defs));
|
||||
defs[0].key = "test_speed"; defs[0].desc = "Speed"; defs[0].info = "How fast";
|
||||
defs[0].values[0].value = "slow"; defs[0].values[0].label = "Slow";
|
||||
defs[0].values[1].value = "fast"; defs[0].values[1].label = "Fast";
|
||||
defs[0].default_value = "fast";
|
||||
defs[1].key = "test_color"; defs[1].desc = "Colour";
|
||||
defs[1].values[0].value = "rgb"; defs[1].values[1].value = "mono";
|
||||
defs[1].default_value = "rgb";
|
||||
defs[2] = none;
|
||||
v2.categories = NULL;
|
||||
v2.definitions = defs;
|
||||
fixture(cfg, sizeof(cfg), "core.opt");
|
||||
test_runloop.core_options = core_option_manager_new(cfg, NULL, &v2, false);
|
||||
CHECK(test_runloop.core_options != NULL, "option manager built");
|
||||
|
||||
CHECK(companion_core_option_count(c) == 2, "2 options (got %u)", (unsigned)companion_core_option_count(c));
|
||||
CHECK(string_is_equal(companion_core_option_desc(c, 0), "Speed"), "desc (got %s)", companion_core_option_desc(c, 0));
|
||||
CHECK(string_is_equal(companion_core_option_info(c, 0), "How fast"), "info");
|
||||
CHECK(companion_core_option_value_count(c, 0) == 2, "2 values");
|
||||
CHECK(string_is_equal(companion_core_option_value_label(c, 0, 1), "Fast"), "value label (got %s)", companion_core_option_value_label(c, 0, 1));
|
||||
CHECK(string_is_equal(companion_core_option_value_label(c, 1, 1), "mono"), "no label: the value (got %s)", companion_core_option_value_label(c, 1, 1));
|
||||
CHECK(companion_core_option_current(c, 0) == 1, "current is the default (fast)");
|
||||
companion_core_option_set(c, 0, 0);
|
||||
CHECK(companion_core_option_current(c, 0) == 0, "set to slow");
|
||||
companion_core_option_reset(c, 0);
|
||||
CHECK(companion_core_option_current(c, 0) == 1, "reset to the default");
|
||||
companion_core_option_set(c, 0, 5);
|
||||
CHECK(companion_core_option_current(c, 0) == 1, "out-of-range value ignored");
|
||||
companion_core_option_set(c, 1, 1);
|
||||
companion_core_option_reset_all(c);
|
||||
CHECK(companion_core_option_current(c, 1) == 0, "reset all");
|
||||
|
||||
CHECK(companion_core_shader_param_count(c) == 2, "2 shader params");
|
||||
CHECK(string_is_equal(companion_core_shader_param_desc(c, 0), "Scanline strength"), "param desc");
|
||||
CHECK(string_is_equal(companion_core_shader_param_desc(c, 1), "CURV"), "no desc: the id (got %s)", companion_core_shader_param_desc(c, 1));
|
||||
CHECK(companion_core_shader_param_range(c, 0, &mn, &mx, &stp, &ini) && mn == 0.0f && mx == 1.0f && ini == 0.5f, "range");
|
||||
companion_core_shader_param_set(c, 0, 0.75f);
|
||||
CHECK(companion_core_shader_param_current(c, 0) == 0.75f, "set");
|
||||
companion_core_shader_param_set(c, 0, 9.0f);
|
||||
CHECK(companion_core_shader_param_current(c, 0) == 1.0f, "clamped to max");
|
||||
companion_core_shader_param_reset(c, 0);
|
||||
CHECK(companion_core_shader_param_current(c, 0) == 0.5f, "reset to initial");
|
||||
CHECK(string_is_equal(companion_core_shader_path(c), "/shaders/crt.slangp"), "shader path");
|
||||
{
|
||||
extern int stub_calls_shader_apply;
|
||||
int before = stub_calls_shader_apply;
|
||||
companion_core_shader_apply(c);
|
||||
CHECK(stub_calls_shader_apply == before + 1, "apply fires CMD_EVENT_SHADERS_APPLY_CHANGES");
|
||||
}
|
||||
core_option_manager_free(test_runloop.core_options);
|
||||
test_runloop.core_options = NULL;
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
/* The Options table: every row reads back what it says, sets from
|
||||
* text with type checks, and lands in the settings. */
|
||||
static void test_settings_table(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char buf[64];
|
||||
size_t i, n = companion_core_setting_count(c);
|
||||
CHECK(n == 13, "13 rows (got %u)", (unsigned)n);
|
||||
for (i = 0; i < n; i++)
|
||||
CHECK(*companion_core_setting_label(c, i) && *companion_core_setting_get(c, i, buf, sizeof(buf)) != 2, "row %u has a label", (unsigned)i);
|
||||
CHECK(companion_core_setting_kind(c, 2) == COMPANION_SETTING_CHOICE && companion_core_setting_choice_count(c, 2) == 3, "theme is a 3-way choice");
|
||||
CHECK(companion_core_setting_set(c, 2, "Dark") && test_settings.uints.desktop_menu_theme == 1, "theme by label");
|
||||
CHECK(companion_core_setting_set(c, 2, "2") && test_settings.uints.desktop_menu_theme == 2, "theme by index");
|
||||
CHECK(!companion_core_setting_set(c, 2, "purple"), "unknown theme refused");
|
||||
CHECK(string_is_equal(companion_core_setting_get(c, 2, buf, sizeof(buf)), "Custom"), "theme reads back (got %s)", buf);
|
||||
CHECK(companion_core_setting_set(c, 0, "true") && test_settings.bools.desktop_menu_save_geometry, "bool from 'true'");
|
||||
CHECK(!companion_core_setting_set(c, 0, "maybe"), "bad bool refused");
|
||||
CHECK(companion_core_setting_set(c, 7, "256") && test_settings.uints.desktop_menu_thumbnail_cache_limit == 256, "uint");
|
||||
CHECK(!companion_core_setting_set(c, 7, "12x"), "bad uint refused");
|
||||
CHECK(companion_core_setting_set(c, 4, "#ff8800") && string_is_equal(test_settings.arrays.desktop_menu_highlight_color, "#ff8800"), "string");
|
||||
CHECK(string_is_equal(companion_core_setting_get(c, 4, buf, sizeof(buf)), "#ff8800"), "string reads back");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_run_paths(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
char content[PATH_MAX_LENGTH];
|
||||
companion_core_refresh_playlists(c);
|
||||
companion_core_select_playlist(c, 2);
|
||||
iterate_until_loaded(c);
|
||||
/* entry 0: DETECT and nothing loaded -> pick a core */
|
||||
CHECK(companion_core_entry_needs_core(c, 0, content, sizeof(content)), "DETECT entry needs a core");
|
||||
CHECK(string_is_equal(content, "/roms/nes/zelda2.nes"), "with its content path");
|
||||
/* entry 1: has a core path -> loads with it */
|
||||
CHECK(!companion_core_entry_needs_core(c, 1, content, sizeof(content)), "entry with a core does not ask");
|
||||
stub_calls_load_with_new_core = 0;
|
||||
CHECK(companion_core_request_load_entry(c, 1), "load entry 1");
|
||||
CHECK(stub_calls_load_with_new_core == 1, "pushed the load task once");
|
||||
CHECK(string_is_equal(stub_last_core, "/cores/fceumm_libretro.so"), "with the entry's core (%s)", stub_last_core);
|
||||
CHECK(string_is_equal(stub_last_content, "/roms/nes/metroid.nes"), "and its content");
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
static void test_launch_options(void)
|
||||
{
|
||||
companion_core_t *c = make_core();
|
||||
companion_launch_option_t opts[6];
|
||||
size_t n;
|
||||
/* No core loaded, no cores installed: the entry's own path is the
|
||||
* one candidate; the playlist default (same path) is not repeated. */
|
||||
n = companion_core_launch_options(c, "/cores/fceumm_libretro.so", "FCEUmm",
|
||||
"Nintendo - Nintendo Entertainment System", false, opts, 6);
|
||||
CHECK(n >= 1, "at least the entry's core, got %u", (unsigned)n);
|
||||
CHECK(n >= 1 && string_is_equal(opts[0].path, "/cores/fceumm_libretro.so"), "entry core first");
|
||||
{
|
||||
size_t i, j, dup = 0;
|
||||
for (i = 0; i < n; i++)
|
||||
for (j = i + 1; j < n; j++)
|
||||
if (string_is_equal(opts[i].path, opts[j].path))
|
||||
dup++;
|
||||
CHECK(dup == 0, "no duplicate paths");
|
||||
}
|
||||
companion_core_free(c);
|
||||
}
|
||||
|
||||
#ifndef COMPANION_TEST_NO_MAIN
|
||||
int main(void)
|
||||
{
|
||||
setup();
|
||||
test_playlist_listing();
|
||||
test_select_and_entries();
|
||||
test_all_playlists();
|
||||
test_thumbnail_path();
|
||||
test_browser();
|
||||
test_browser_sort();
|
||||
test_sort_without_metadata();
|
||||
test_sort_then_folders_only();
|
||||
test_backend_scenario();
|
||||
test_sort_callback_reentrancy();
|
||||
test_rename_add_install();
|
||||
test_options_and_shader_params();
|
||||
test_settings_table();
|
||||
test_run_paths();
|
||||
test_launch_options();
|
||||
teardown();
|
||||
if (fails)
|
||||
{
|
||||
printf("companion_core_test: %d failure(s)\n", fails);
|
||||
return 1;
|
||||
}
|
||||
printf("companion_core_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
#endif /* COMPANION_TEST_NO_MAIN */
|
||||
@@ -0,0 +1,659 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Regression test for ui/companion/companion_thumbs: the thumbnail
|
||||
* engine every desktop companion draws through. Runs on Linux with the
|
||||
* real decoder (image_texture_load) against TGA fixtures it writes
|
||||
* itself, so it exercises the decode threads for real. Build and run
|
||||
* with tools/companion_thumbs_test.sh (optionally under TSan / ASan).
|
||||
*
|
||||
* Covered:
|
||||
* - a decode lands, scaled and letterboxed, with the right pixels
|
||||
* - the cache serves a second request for the same key without a
|
||||
* decode and touches it as most-recently-used
|
||||
* - the byte budget evicts least-recently-used entries first
|
||||
* - urgent requests are served newest-first, before prefetch ones
|
||||
* - cancel() empties the queues but keeps the cache
|
||||
* - an undecodable file is delivered with NULL pixels and forgotten
|
||||
* - many requests across several threads: every one delivered exactly
|
||||
* once; free() with work in flight returns (no hang, no leak) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <compat/strl.h>
|
||||
#include <retro_miscellaneous.h>
|
||||
|
||||
#include "../companion_thumbs.h"
|
||||
#include <encodings/crc32.h>
|
||||
#include <zlib.h>
|
||||
|
||||
static int fails;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { fails++; printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static char tmpdir[512];
|
||||
|
||||
/* Uncompressed 32-bit TGA, top-left origin, filled with @argb; a @w x @h
|
||||
* image. TGA is the simplest format image_texture_load decodes. */
|
||||
static bool write_tga(const char *path, unsigned w, unsigned h, uint32_t argb)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
unsigned char hdr[18];
|
||||
unsigned i;
|
||||
if (!f)
|
||||
return false;
|
||||
memset(hdr, 0, sizeof(hdr));
|
||||
hdr[2] = 2; /* uncompressed true-colour */
|
||||
hdr[12] = (unsigned char)(w & 0xff);
|
||||
hdr[13] = (unsigned char)(w >> 8);
|
||||
hdr[14] = (unsigned char)(h & 0xff);
|
||||
hdr[15] = (unsigned char)(h >> 8);
|
||||
hdr[16] = 32; /* bpp */
|
||||
hdr[17] = 0x28; /* 8 alpha bits, top-left origin */
|
||||
fwrite(hdr, 1, sizeof(hdr), f);
|
||||
for (i = 0; i < w * h; i++)
|
||||
{
|
||||
unsigned char px[4];
|
||||
px[0] = (unsigned char)(argb & 0xff); /* B */
|
||||
px[1] = (unsigned char)((argb >> 8) & 0xff); /* G */
|
||||
px[2] = (unsigned char)((argb >> 16) & 0xff); /* R */
|
||||
px[3] = (unsigned char)((argb >> 24) & 0xff); /* A */
|
||||
fwrite(px, 1, 4, f);
|
||||
}
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --- a hand-made two-frame APNG ------------------------------------------ */
|
||||
|
||||
static void png_chunk(FILE *f, const char *type, const uint8_t *data, size_t len)
|
||||
{
|
||||
uint8_t hdr[8];
|
||||
uint32_t crc;
|
||||
uint32_t n = (uint32_t)len;
|
||||
hdr[0] = (uint8_t)(n >> 24); hdr[1] = (uint8_t)(n >> 16);
|
||||
hdr[2] = (uint8_t)(n >> 8); hdr[3] = (uint8_t)n;
|
||||
memcpy(hdr + 4, type, 4);
|
||||
fwrite(hdr, 1, 8, f);
|
||||
if (len)
|
||||
fwrite(data, 1, len, f);
|
||||
crc = encoding_crc32(0, (const uint8_t*)type, 4);
|
||||
if (len)
|
||||
crc = encoding_crc32(crc, data, len);
|
||||
hdr[0] = (uint8_t)(crc >> 24); hdr[1] = (uint8_t)(crc >> 16);
|
||||
hdr[2] = (uint8_t)(crc >> 8); hdr[3] = (uint8_t)crc;
|
||||
fwrite(hdr, 1, 4, f);
|
||||
}
|
||||
|
||||
static void be32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; }
|
||||
static void be16(uint8_t *p, uint16_t v) { p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; }
|
||||
|
||||
/* Solid-colour w x h RGBA rows with filter byte 0, zlib-compressed. */
|
||||
static size_t apng_frame_data(uint8_t *out, size_t outcap, unsigned w,
|
||||
unsigned h, uint32_t rgba)
|
||||
{
|
||||
size_t raw_len = (size_t)h * (1 + (size_t)w * 4), i, x;
|
||||
uint8_t *raw = (uint8_t*)malloc(raw_len);
|
||||
uLongf dl = (uLongf)outcap;
|
||||
for (i = 0; i < h; i++)
|
||||
{
|
||||
uint8_t *row = raw + i * (1 + (size_t)w * 4);
|
||||
row[0] = 0;
|
||||
for (x = 0; x < w; x++)
|
||||
{
|
||||
row[1 + x * 4 + 0] = (uint8_t)(rgba >> 24);
|
||||
row[1 + x * 4 + 1] = (uint8_t)(rgba >> 16);
|
||||
row[1 + x * 4 + 2] = (uint8_t)(rgba >> 8);
|
||||
row[1 + x * 4 + 3] = (uint8_t)rgba;
|
||||
}
|
||||
}
|
||||
if (compress2(out, &dl, raw, (uLong)raw_len, 6) != Z_OK)
|
||||
dl = 0;
|
||||
free(raw);
|
||||
return (size_t)dl;
|
||||
}
|
||||
|
||||
/* Two frames, each @delay_ms, looping forever: frame 0 red, frame 1
|
||||
* green (R,G,B,A in the file). */
|
||||
static bool write_apng(const char *path, unsigned w, unsigned h, int delay_ms)
|
||||
{
|
||||
static const uint8_t sig[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 };
|
||||
FILE *f = fopen(path, "wb");
|
||||
uint8_t ihdr[13], actl[8], fctl[26], comp[4096], fdat[4100];
|
||||
size_t n;
|
||||
if (!f)
|
||||
return false;
|
||||
fwrite(sig, 1, 8, f);
|
||||
be32(ihdr, w); be32(ihdr + 4, h);
|
||||
ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
||||
png_chunk(f, "IHDR", ihdr, 13);
|
||||
be32(actl, 2); be32(actl + 4, 0); /* 2 frames, loop forever */
|
||||
png_chunk(f, "acTL", actl, 8);
|
||||
/* frame 0: fcTL seq 0, then IDAT */
|
||||
memset(fctl, 0, sizeof(fctl));
|
||||
be32(fctl, 0); be32(fctl + 4, w); be32(fctl + 8, h);
|
||||
be16(fctl + 20, (uint16_t)delay_ms); be16(fctl + 22, 1000);
|
||||
png_chunk(f, "fcTL", fctl, 26);
|
||||
n = apng_frame_data(comp, sizeof(comp), w, h, 0xff0000ffu); /* red */
|
||||
png_chunk(f, "IDAT", comp, n);
|
||||
/* frame 1: fcTL seq 1, fdAT seq 2 */
|
||||
be32(fctl, 1);
|
||||
png_chunk(f, "fcTL", fctl, 26);
|
||||
n = apng_frame_data(comp, sizeof(comp), w, h, 0x00ff00ffu); /* green */
|
||||
be32(fdat, 2);
|
||||
memcpy(fdat + 4, comp, n);
|
||||
png_chunk(f, "fdAT", fdat, n + 4);
|
||||
png_chunk(f, "IEND", NULL, 0);
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void fixture(char *out, size_t len, const char *name)
|
||||
{
|
||||
snprintf(out, len, "%s/%s", tmpdir, name);
|
||||
}
|
||||
|
||||
/* --- delivery capture ---------------------------------------------------- */
|
||||
|
||||
struct got
|
||||
{
|
||||
uintptr_t tag;
|
||||
int edge;
|
||||
bool null;
|
||||
uint32_t centre, corner;
|
||||
};
|
||||
static struct got gots[4096];
|
||||
static size_t ngot;
|
||||
|
||||
static void on_done(void *ud, const char *path, int w, int h, uintptr_t tag,
|
||||
const uint32_t *bits)
|
||||
{
|
||||
struct got *g;
|
||||
(void)ud; (void)path;
|
||||
if (ngot >= sizeof(gots) / sizeof(gots[0]))
|
||||
return;
|
||||
g = &gots[ngot++];
|
||||
g->tag = tag;
|
||||
g->edge = w;
|
||||
g->null = (bits == NULL);
|
||||
/* centre of a w x h buffer */
|
||||
g->centre = bits ? bits[(size_t)(h / 2) * w + w / 2] : 0;
|
||||
g->corner = bits ? bits[0] : 0;
|
||||
}
|
||||
|
||||
static void sleep_ms(int ms)
|
||||
{
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000L;
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
/* Poll until a delivery with @tag arrives (true) or a timeout. Other
|
||||
* deliveries - a job popped before a cancel() still lands with its old
|
||||
* epoch - do not count. */
|
||||
static bool drain_tag(companion_thumbs_t *t, uintptr_t tag, int timeout_ms)
|
||||
{
|
||||
int waited = 0;
|
||||
size_t i, from = ngot;
|
||||
for (;;)
|
||||
{
|
||||
companion_thumbs_poll(t, on_done, NULL, 0, 20000);
|
||||
for (i = from; i < ngot; i++)
|
||||
if (gots[i].tag == tag)
|
||||
return true;
|
||||
if (waited >= timeout_ms)
|
||||
return false;
|
||||
sleep_ms(2);
|
||||
waited += 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Poll until @want deliveries or a timeout. */
|
||||
static size_t drain(companion_thumbs_t *t, size_t want, int timeout_ms)
|
||||
{
|
||||
int waited = 0;
|
||||
size_t before = ngot;
|
||||
while (ngot - before < want && waited < timeout_ms)
|
||||
{
|
||||
companion_thumbs_poll(t, on_done, NULL, 0, 20000);
|
||||
if (ngot - before >= want)
|
||||
break;
|
||||
sleep_ms(2);
|
||||
waited += 2;
|
||||
}
|
||||
return ngot - before;
|
||||
}
|
||||
|
||||
/* --- tests ---------------------------------------------------------------- */
|
||||
|
||||
static void test_decode_and_scale(void)
|
||||
{
|
||||
char red[512], tall[512];
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 2);
|
||||
const uint32_t *bits;
|
||||
|
||||
fixture(red, sizeof(red), "red_64x64.tga");
|
||||
fixture(tall, sizeof(tall), "green_20x80.tga");
|
||||
write_tga(red, 64, 64, 0xffff0000u);
|
||||
write_tga(tall, 20, 80, 0xff00ff00u);
|
||||
|
||||
ngot = 0;
|
||||
CHECK(companion_thumbs_request(t, red, 32, 32, 1, true, 0xff000000u), "request accepted");
|
||||
/* (queued() is 0 or 1 here depending on whether a worker already
|
||||
* took the job - both are correct, so it is not asserted.) */
|
||||
CHECK(drain(t, 1, 2000) == 1, "red delivered");
|
||||
CHECK(gots[0].tag == 1 && gots[0].edge == 32 && !gots[0].null, "red tag/edge");
|
||||
CHECK(gots[0].centre == 0xffff0000u, "red centre pixel 0x%08x", gots[0].centre);
|
||||
CHECK(gots[0].corner == 0xffff0000u, "square fills the whole thumb");
|
||||
|
||||
/* Tall image: letterboxed left/right with the bg colour. */
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, tall, 40, 40, 2, true, 0xff123456u);
|
||||
CHECK(drain(t, 1, 2000) == 1, "tall delivered");
|
||||
CHECK(gots[0].centre == 0xff00ff00u, "tall centre is the image");
|
||||
CHECK(gots[0].corner == 0xff123456u, "tall corner is the letterbox bg 0x%08x", gots[0].corner);
|
||||
|
||||
/* Rectangular box (a boxart pane): fit inside, letterboxed. */
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, tall, 60, 30, 5, true, 0xff0000ffu);
|
||||
CHECK(drain(t, 1, 2000) == 1, "rect delivered");
|
||||
CHECK(gots[0].tag == 5 && !gots[0].null, "rect tag");
|
||||
{
|
||||
/* 20x80 into 60x30 fits by height: a 7x30 image around x = 30;
|
||||
* the corner is letterbox, the centre column is image. */
|
||||
const uint32_t *r = companion_thumbs_get(t, tall, 60, 30);
|
||||
CHECK(r != NULL, "rect cached");
|
||||
CHECK(r && r[0] == 0xff0000ffu, "rect corner is bg");
|
||||
CHECK(r && r[15 * 60 + 30] == 0xff00ff00u, "rect centre is image");
|
||||
}
|
||||
|
||||
/* Cached now: get() serves it, request() declines. */
|
||||
bits = companion_thumbs_get(t, red, 32, 32);
|
||||
CHECK(bits && bits[0] == 0xffff0000u, "cache get");
|
||||
CHECK(!companion_thumbs_request(t, red, 32, 32, 3, true, 0), "cached key not re-queued");
|
||||
CHECK(companion_thumbs_get(t, red, 33, 33) == NULL, "other edge is a different key");
|
||||
CHECK(companion_thumbs_cached_count(t) == 3, "three cached");
|
||||
CHECK(companion_thumbs_cached_bytes(t) == 32u * 32 * 4 + 40u * 40 * 4 + 60u * 30 * 4, "cached bytes");
|
||||
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
static void test_lru_budget(void)
|
||||
{
|
||||
char p[8][512];
|
||||
int i;
|
||||
/* budget for exactly three 16x16 thumbs */
|
||||
companion_thumbs_t *t = companion_thumbs_new(3 * 16 * 16 * 4, 1);
|
||||
for (i = 0; i < 4; i++)
|
||||
{
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "lru%d.tga", i);
|
||||
fixture(p[i], sizeof(p[i]), name);
|
||||
write_tga(p[i], 8, 8, 0xff000000u | (uint32_t)(i * 40));
|
||||
}
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, p[0], 16, 16, 0, true, 0);
|
||||
drain(t, 1, 2000);
|
||||
companion_thumbs_request(t, p[1], 16, 16, 1, true, 0);
|
||||
drain(t, 1, 2000);
|
||||
companion_thumbs_request(t, p[2], 16, 16, 2, true, 0);
|
||||
drain(t, 1, 2000);
|
||||
CHECK(companion_thumbs_cached_count(t) == 3, "three fit");
|
||||
/* touch p[0] so p[1] is the least recently used */
|
||||
CHECK(companion_thumbs_get(t, p[0], 16, 16) != NULL, "touch p0");
|
||||
companion_thumbs_request(t, p[3], 16, 16, 3, true, 0);
|
||||
drain(t, 1, 2000);
|
||||
CHECK(companion_thumbs_cached_count(t) == 3, "still three after eviction");
|
||||
CHECK(companion_thumbs_get(t, p[1], 16, 16) == NULL, "LRU p1 evicted");
|
||||
CHECK(companion_thumbs_get(t, p[0], 16, 16) != NULL, "touched p0 kept");
|
||||
CHECK(companion_thumbs_get(t, p[3], 16, 16) != NULL, "new p3 kept");
|
||||
CHECK(companion_thumbs_cached_bytes(t) <= 3u * 16 * 16 * 4, "within budget");
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
static void test_priority_and_cancel(void)
|
||||
{
|
||||
char p[6][512];
|
||||
int i;
|
||||
/* No threads started: poll() decodes in order, so the order is
|
||||
* observable. (companion_thumbs_new with threads=1 still starts a
|
||||
* worker under HAVE_THREADS; use the queue state instead.) */
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 1);
|
||||
for (i = 0; i < 6; i++)
|
||||
{
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "pri%d.tga", i);
|
||||
fixture(p[i], sizeof(p[i]), name);
|
||||
write_tga(p[i], 4, 4, 0xffffffffu);
|
||||
}
|
||||
/* Cancel: queued requests vanish, cache stays. */
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, p[0], 8, 8, 0, true, 0);
|
||||
drain(t, 1, 2000);
|
||||
for (i = 1; i < 6; i++)
|
||||
companion_thumbs_request(t, p[i], 8, 8, (uintptr_t)i, i < 3, 0);
|
||||
companion_thumbs_cancel(t);
|
||||
CHECK(companion_thumbs_queued(t) == 0, "cancel empties queues");
|
||||
CHECK(companion_thumbs_get(t, p[0], 8, 8) != NULL, "cancel keeps the cache");
|
||||
/* A cancelled key can be requested again. Wait for that request's
|
||||
* own delivery (tag 1): a job popped before the cancel may land
|
||||
* first with its old epoch, and must not be mistaken for it. */
|
||||
CHECK(companion_thumbs_request(t, p[1], 8, 8, 1, true, 0), "re-request after cancel");
|
||||
CHECK(drain_tag(t, 1, 5000), "re-requested key delivered");
|
||||
CHECK(companion_thumbs_get(t, p[1], 8, 8) != NULL, "re-requested decoded");
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
static void test_forget_and_budget(void)
|
||||
{
|
||||
char p[512];
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 1);
|
||||
fixture(p, sizeof(p), "forget.tga");
|
||||
write_tga(p, 8, 8, 0xff112233u);
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, p, 16, 16, 0, true, 0);
|
||||
companion_thumbs_request(t, p, 24, 24, 1, true, 0);
|
||||
drain(t, 2, 2000);
|
||||
CHECK(companion_thumbs_cached_count(t) == 2, "two sizes cached");
|
||||
CHECK(companion_thumbs_forget(t, p) == 2, "forget drops every size");
|
||||
CHECK(companion_thumbs_get(t, p, 16, 16) == NULL, "forgotten");
|
||||
CHECK(companion_thumbs_request(t, p, 16, 16, 2, true, 0), "re-request after forget");
|
||||
CHECK(drain_tag(t, 2, 5000), "re-request after forget delivered");
|
||||
CHECK(companion_thumbs_get(t, p, 16, 16) != NULL, "decoded again");
|
||||
/* shrinking the budget evicts at once */
|
||||
companion_thumbs_request(t, p, 32, 32, 3, true, 0);
|
||||
drain(t, 1, 2000);
|
||||
CHECK(companion_thumbs_cached_count(t) == 2, "two again");
|
||||
companion_thumbs_set_budget(t, 32u * 32 * 4);
|
||||
CHECK(companion_thumbs_cached_count(t) == 1, "budget cut evicts LRU (got %u)", (unsigned)companion_thumbs_cached_count(t));
|
||||
CHECK(companion_thumbs_get(t, p, 32, 32) != NULL, "most recent kept");
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
static long ms_since(const struct timespec *t0)
|
||||
{
|
||||
struct timespec t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
return (long)((t1.tv_sec - t0->tv_sec) * 1000 + (t1.tv_nsec - t0->tv_nsec) / 1000000);
|
||||
}
|
||||
|
||||
/* Big images must not hold up shutdown or a view that moved on: the
|
||||
* decode is abandoned between steps. */
|
||||
static void test_abort(void)
|
||||
{
|
||||
char big[8][512];
|
||||
int i;
|
||||
struct timespec t0;
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 1);
|
||||
/* 8 x (2048 x 2048 x 4 = 16 MiB) TGA: each decode is a real amount
|
||||
* of work on one worker. */
|
||||
for (i = 0; i < 8; i++)
|
||||
{
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "big%d.tga", i);
|
||||
fixture(big[i], sizeof(big[i]), name);
|
||||
write_tga(big[i], 2048, 2048, 0xff808080u);
|
||||
}
|
||||
/* cancel() while decoding: the stale job is abandoned and its key
|
||||
* can be requested again */
|
||||
ngot = 0;
|
||||
for (i = 0; i < 8; i++)
|
||||
companion_thumbs_request(t, big[i], 64, 64, (uintptr_t)i, true, 0);
|
||||
sleep_ms(5);
|
||||
companion_thumbs_cancel(t);
|
||||
drain(t, 1, 300);
|
||||
CHECK(companion_thumbs_request(t, big[0], 64, 64, 100, true, 0), "re-request after cancel mid-decode");
|
||||
CHECK(drain_tag(t, 100, 10000), "re-requested big image lands (its own delivery, tag 100)");
|
||||
|
||||
/* free() while decoding: returns promptly */
|
||||
for (i = 0; i < 8; i++)
|
||||
companion_thumbs_request(t, big[i], 96, 96, (uintptr_t)i, true, 0);
|
||||
sleep_ms(5);
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
companion_thumbs_free(t);
|
||||
CHECK(ms_since(&t0) < 1000, "free with 8 x 16 MiB decodes queued returned in %ld ms", ms_since(&t0));
|
||||
}
|
||||
|
||||
/* Two records for one key in flight at once, both failing: the first
|
||||
* polled must not free the entry the second still points at. Built by
|
||||
* requesting an undecodable file, cancelling (the in-flight job keeps
|
||||
* its record), and requesting it again at once. */
|
||||
static void test_double_failure_no_uaf(void)
|
||||
{
|
||||
char bad[512];
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 1);
|
||||
FILE *f;
|
||||
int i;
|
||||
fixture(bad, sizeof(bad), "bad2.tga");
|
||||
f = fopen(bad, "wb");
|
||||
fputs("not a tga", f);
|
||||
fclose(f);
|
||||
ngot = 0;
|
||||
for (i = 0; i < 20; i++)
|
||||
{
|
||||
companion_thumbs_request(t, bad, 16, 16, 1, true, 0);
|
||||
companion_thumbs_cancel(t);
|
||||
companion_thumbs_request(t, bad, 16, 16, 2, true, 0);
|
||||
drain(t, 1, 1000);
|
||||
companion_thumbs_get(t, bad, 16, 16); /* walks the hash chain */
|
||||
}
|
||||
drain(t, 1, 1000);
|
||||
companion_thumbs_get(t, bad, 16, 16);
|
||||
CHECK(companion_thumbs_cached_count(t) == 0, "nothing cached");
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
/* Animated preview: an APNG plays through poll() as frame deliveries,
|
||||
* on its clock, scaled like a still; a still PNG produces none; stop
|
||||
* stops. */
|
||||
static void test_animation(void)
|
||||
{
|
||||
char apng[512], still[512];
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 1);
|
||||
int reds = 0, greens = 0;
|
||||
size_t i;
|
||||
fixture(apng, sizeof(apng), "anim.png");
|
||||
fixture(still, sizeof(still), "still.png");
|
||||
CHECK(write_apng(apng, 8, 8, 30), "wrote the APNG");
|
||||
/* a plain PNG through the same writer minus animation chunks is
|
||||
* more code than it is worth: a TGA is a still for this purpose */
|
||||
fixture(still, sizeof(still), "still.tga");
|
||||
write_tga(still, 8, 8, 0xff112233u);
|
||||
|
||||
/* the still request works on it as on any PNG */
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, apng, 16, 16, 1, true, 0);
|
||||
CHECK(drain_tag(t, 1, 3000), "APNG decodes as a still first (frame 0)");
|
||||
CHECK(!gots[0].null && gots[0].centre == 0xffff0000u, "still is frame 0 (red): 0x%08x", gots[0].centre);
|
||||
|
||||
/* animate: frames alternate red / green at ~30 ms */
|
||||
ngot = 0;
|
||||
companion_thumbs_animate(t, apng, 16, 16, 7, 0);
|
||||
CHECK(drain(t, 10, 4000) >= 10, "at least 10 frames in 4 s (got %u)", (unsigned)ngot);
|
||||
for (i = 0; i < ngot; i++)
|
||||
{
|
||||
CHECK(gots[i].tag == 7 && gots[i].edge == 16, "frame tag / size");
|
||||
if (gots[i].centre == 0xffff0000u) reds++;
|
||||
else if (gots[i].centre == 0xff00ff00u) greens++;
|
||||
}
|
||||
CHECK(reds >= 2 && greens >= 2, "both frames seen, more than once each (red %d, green %d)", reds, greens);
|
||||
CHECK(reds + greens == (int)ngot, "every frame is one of the two (red %d, green %d, total %u)", reds, greens, (unsigned)ngot);
|
||||
CHECK(companion_thumbs_animating(t), "reports animating");
|
||||
|
||||
/* stop: no more frames after the one in flight */
|
||||
companion_thumbs_animate_stop(t);
|
||||
sleep_ms(120);
|
||||
companion_thumbs_poll(t, on_done, NULL, 0, 20000);
|
||||
ngot = 0;
|
||||
sleep_ms(150);
|
||||
companion_thumbs_poll(t, on_done, NULL, 0, 20000);
|
||||
CHECK(ngot == 0, "stopped: no frames (got %u)", (unsigned)ngot);
|
||||
|
||||
/* a still produces no frames */
|
||||
companion_thumbs_animate(t, still, 16, 16, 8, 0);
|
||||
sleep_ms(200);
|
||||
ngot = 0;
|
||||
companion_thumbs_poll(t, on_done, NULL, 0, 20000);
|
||||
CHECK(ngot == 0, "a still animates nothing (got %u)", (unsigned)ngot);
|
||||
|
||||
/* free with an animation running returns */
|
||||
companion_thumbs_animate(t, apng, 16, 16, 9, 0);
|
||||
sleep_ms(50);
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
/* The scaler: R,G,B,A memory-order input comes out as ARGB words with
|
||||
* no whole-canvas pass; a 4x downscale of a 2-colour checkerboard
|
||||
* averages the four taps (nearest would pick one colour per pixel). */
|
||||
static void test_scaler(void)
|
||||
{
|
||||
unsigned sw = 64, sh = 64, x, y;
|
||||
uint32_t *src = (uint32_t*)malloc(sw * sh * 4), *out;
|
||||
/* memory order R,G,B,A of pure red: bytes FF 00 00 FF -> LE word 0xFF0000FF */
|
||||
for (y = 0; y < sh; y++)
|
||||
for (x = 0; x < sw; x++)
|
||||
src[y * sw + x] = 0xFF0000FFu;
|
||||
out = companion_thumbs_scale_ex(src, sw, sh, 16, 16, 0, true);
|
||||
CHECK(out && out[8 * 16 + 8] == 0xffff0000u, "RGBA-order red -> ARGB red (got 0x%08x)", out ? out[8 * 16 + 8] : 0);
|
||||
free(out);
|
||||
/* the same words read as ARGB are blue */
|
||||
out = companion_thumbs_scale_ex(src, sw, sh, 16, 16, 0, false);
|
||||
CHECK(out && out[8 * 16 + 8] == 0xff0000ffu, "ARGB-order 0xFF0000FF is blue (got 0x%08x)", out ? out[8 * 16 + 8] : 0);
|
||||
free(out);
|
||||
/* checkerboard of white and black, 1-px squares, 4x down: grey */
|
||||
for (y = 0; y < sh; y++)
|
||||
for (x = 0; x < sw; x++)
|
||||
src[y * sw + x] = ((x + y) & 1) ? 0xffffffffu : 0xff000000u;
|
||||
out = companion_thumbs_scale_ex(src, sw, sh, 16, 16, 0, false);
|
||||
{
|
||||
uint32_t p = out ? out[8 * 16 + 8] : 0;
|
||||
unsigned r = (p >> 16) & 0xff;
|
||||
CHECK(out && r > 0x30 && r < 0xd0, "4-tap downscale averages the checkerboard to grey (got 0x%08x)", p);
|
||||
}
|
||||
free(out);
|
||||
/* enlarging keeps nearest: a 2x2 source to 8x8 has hard edges */
|
||||
src[0] = 0xffff0000u; src[1] = 0xff00ff00u; src[sw] = 0xff0000ffu; src[sw + 1] = 0xffffffffu;
|
||||
out = companion_thumbs_scale_ex(src, 2, 2, 8, 8, 0, false);
|
||||
CHECK(out && out[0] == 0xffff0000u && out[7] == 0xff00ff00u, "enlarging is nearest (corners 0x%08x 0x%08x)", out ? out[0] : 0, out ? out[7] : 0);
|
||||
free(out);
|
||||
free(src);
|
||||
}
|
||||
|
||||
static void test_undecodable(void)
|
||||
{
|
||||
char bad[512];
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 1);
|
||||
FILE *f;
|
||||
fixture(bad, sizeof(bad), "bad.tga");
|
||||
f = fopen(bad, "wb");
|
||||
fputs("not a tga", f);
|
||||
fclose(f);
|
||||
ngot = 0;
|
||||
companion_thumbs_request(t, bad, 16, 16, 9, true, 0);
|
||||
CHECK(drain(t, 1, 2000) == 1, "undecodable delivered");
|
||||
CHECK(gots[0].null && gots[0].tag == 9, "delivered with NULL bits");
|
||||
CHECK(companion_thumbs_cached_count(t) == 0, "not cached");
|
||||
/* forgotten: can be requested again (e.g. after a download fixes it) */
|
||||
CHECK(companion_thumbs_request(t, bad, 16, 16, 9, true, 0), "retry allowed");
|
||||
drain(t, 1, 2000);
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
static void test_many_and_shutdown(void)
|
||||
{
|
||||
enum { N = 300 };
|
||||
static char paths[N][512];
|
||||
int i;
|
||||
companion_thumbs_t *t = companion_thumbs_new(0, 4);
|
||||
for (i = 0; i < N; i++)
|
||||
{
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "many%d.tga", i);
|
||||
fixture(paths[i], sizeof(paths[i]), name);
|
||||
write_tga(paths[i], 32, 24, 0xff000000u | (uint32_t)i);
|
||||
}
|
||||
ngot = 0;
|
||||
for (i = 0; i < N; i++)
|
||||
companion_thumbs_request(t, paths[i], 48, 48, (uintptr_t)i, (i & 1) != 0, 0);
|
||||
CHECK(drain(t, N, 10000) == N, "all %d delivered (got %u)", N, (unsigned)ngot);
|
||||
{
|
||||
/* each tag exactly once */
|
||||
static unsigned char seen[N];
|
||||
size_t k;
|
||||
int dup = 0, missing = 0;
|
||||
memset(seen, 0, sizeof(seen));
|
||||
for (k = 0; k < ngot; k++)
|
||||
{
|
||||
if (gots[k].tag < N)
|
||||
{
|
||||
if (seen[gots[k].tag]) dup++;
|
||||
seen[gots[k].tag] = 1;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < N; i++)
|
||||
if (!seen[i]) missing++;
|
||||
CHECK(!dup && !missing, "exactly once: dup=%d missing=%d", dup, missing);
|
||||
}
|
||||
CHECK(companion_thumbs_cached_count(t) == N, "all cached");
|
||||
|
||||
/* Shutdown with work in flight must return. */
|
||||
for (i = 0; i < N; i++)
|
||||
companion_thumbs_request(t, paths[i], 64, 64, (uintptr_t)i, true, 0);
|
||||
companion_thumbs_free(t);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *dir = (argc > 1) ? argv[1] : "/tmp";
|
||||
snprintf(tmpdir, sizeof(tmpdir), "%s/companion_thumbs_test_%ld", dir, (long)time(NULL));
|
||||
{
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof(cmd), "mkdir -p '%s'", tmpdir);
|
||||
if (system(cmd) != 0)
|
||||
{
|
||||
printf("cannot create %s\n", tmpdir);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
test_decode_and_scale();
|
||||
test_lru_budget();
|
||||
test_priority_and_cancel();
|
||||
test_forget_and_budget();
|
||||
test_abort();
|
||||
test_double_failure_no_uaf();
|
||||
test_animation();
|
||||
test_scaler();
|
||||
test_undecodable();
|
||||
test_many_and_shutdown();
|
||||
|
||||
{
|
||||
char cmd[600];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", tmpdir);
|
||||
if (system(cmd) != 0)
|
||||
printf("(could not remove %s)\n", tmpdir);
|
||||
}
|
||||
if (fails)
|
||||
{
|
||||
printf("companion_thumbs_test: %d failure(s)\n", fails);
|
||||
return 1;
|
||||
}
|
||||
printf("companion_thumbs_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Regression harness for the Win32 companion (ui/drivers/
|
||||
* ui_win32_companion.c), built with mingw and run under Wine + Xvfb by
|
||||
* tools/companion_win32_test.sh. The real driver, the real companion
|
||||
* core, the core test's stubs and fixtures; RetroArch's window and the
|
||||
* modal helpers are stubbed here.
|
||||
*
|
||||
* It drives the driver the way RetroArch and a user do - init, iterate
|
||||
* until the playlist lands, then the messages the widgets produce
|
||||
* (the View combo's CBN_SELCHANGE, the tab's TCN_SELCHANGE, the
|
||||
* menu's WM_COMMAND ids, list selections) - and asserts on the real
|
||||
* controls: the entries list's count and view style, the playlist
|
||||
* list's items and text, the boxart pane's image, the secondary
|
||||
* windows' rows, the status bar. Wine's comctl32 is not Microsoft's,
|
||||
* but the object graph, the messages and their side effects are the
|
||||
* same, which is where the bugs have been. */
|
||||
|
||||
#include <windows.h>
|
||||
#include <commctrl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <compat/strl.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <retro_miscellaneous.h>
|
||||
|
||||
#include "../../../configuration.h"
|
||||
#include "../../../runloop.h"
|
||||
#include "../../../core_option_manager.h"
|
||||
#include "../../../ui/ui_companion_driver.h"
|
||||
#include "../../../ui/companion/companion_core.h"
|
||||
#include "../../../ui/drivers/ui_win32.h"
|
||||
|
||||
/* --- stubs for what the driver reaches into RetroArch for --------------- */
|
||||
ui_window_win32_t main_window;
|
||||
static int stub_menu_loop_calls;
|
||||
LRESULT win32_menu_loop(HWND hwnd, WPARAM wparam) { (void)hwnd; (void)wparam; stub_menu_loop_calls++; return 0; }
|
||||
void win32_modal_enter(HWND h) { (void)h; }
|
||||
void win32_modal_exit(HWND h) { (void)h; }
|
||||
void win32_modal_tick(HWND h) { (void)h; }
|
||||
void win32_modal_window_destroyed(HWND h) { (void)h; }
|
||||
|
||||
extern settings_t test_settings;
|
||||
extern runloop_state_t test_runloop;
|
||||
extern int stub_calls_command;
|
||||
extern int stub_calls_shader_apply;
|
||||
extern ui_companion_driver_t ui_companion_wimp_win32;
|
||||
extern void companion_test_setup_fixtures(char *root, size_t len);
|
||||
extern void companion_test_teardown_fixtures(const char *root);
|
||||
|
||||
/* The driver's private struct: its first two fields are stable. */
|
||||
struct wimp_peek { companion_core_t *core; HWND hwnd; };
|
||||
|
||||
#include "../../../ui/drivers/ui_win32_companion_ids.h"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { fails++; printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } else { printf("[ok] "); printf(__VA_ARGS__); printf("\n"); } fflush(stdout); } while (0)
|
||||
|
||||
/* Pump messages and the driver for a while. */
|
||||
static void pump(void *data, int ms)
|
||||
{
|
||||
DWORD end = GetTickCount() + (DWORD)ms;
|
||||
do
|
||||
{
|
||||
MSG msg;
|
||||
ui_companion_wimp_win32.iterate(data);
|
||||
while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
Sleep(5);
|
||||
} while (GetTickCount() < end);
|
||||
}
|
||||
|
||||
static void lv_text(HWND lv, int item, int sub, char *buf, int len)
|
||||
{
|
||||
LVITEMA it;
|
||||
memset(&it, 0, sizeof(it));
|
||||
it.iSubItem = sub;
|
||||
it.pszText = buf;
|
||||
it.cchTextMax = len;
|
||||
buf[0] = '\0';
|
||||
SendMessageA(lv, LVM_GETITEMTEXTA, (WPARAM)item, (LPARAM)&it);
|
||||
}
|
||||
|
||||
static void send_command(HWND hwnd, int id) { SendMessageA(hwnd, WM_COMMAND, MAKEWPARAM(id, 0), 0); }
|
||||
|
||||
/* Find a top-level window created by the driver with the given class. */
|
||||
static HWND find_class_window(const char *cls)
|
||||
{
|
||||
return FindWindowA(cls, NULL);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char root[512];
|
||||
void *data;
|
||||
struct wimp_peek *peek;
|
||||
HWND hwnd, entries, playlists, tabs, view, status;
|
||||
char buf[512];
|
||||
INITCOMMONCONTROLSEX ic = { sizeof(ic), ICC_LISTVIEW_CLASSES | ICC_TAB_CLASSES | ICC_BAR_CLASSES };
|
||||
InitCommonControlsEx(&ic);
|
||||
|
||||
companion_test_setup_fixtures(root, sizeof(root));
|
||||
test_settings.bools.ui_companion_toggle = true;
|
||||
test_settings.uints.desktop_menu_view_type = 0;
|
||||
|
||||
data = ui_companion_wimp_win32.init();
|
||||
CHECK(data != NULL, "driver init (window built)");
|
||||
if (!data)
|
||||
return 1;
|
||||
peek = (struct wimp_peek*)data;
|
||||
hwnd = peek->hwnd;
|
||||
entries = GetDlgItem(hwnd, IDC_CW_ENTRIES);
|
||||
playlists = GetDlgItem(hwnd, IDC_CW_PLAYLISTS);
|
||||
tabs = GetDlgItem(hwnd, IDC_CW_TABS);
|
||||
view = GetDlgItem(hwnd, IDC_CW_VIEW_COMBO);
|
||||
status = GetDlgItem(hwnd, IDC_CW_STATUS);
|
||||
CHECK(hwnd && entries && playlists && tabs && view, "controls found (hwnd=%p entries=%p playlists=%p tabs=%p view=%p)", (void*)hwnd, (void*)entries, (void*)playlists, (void*)tabs, (void*)view);
|
||||
ui_companion_wimp_win32.toggle(data, true);
|
||||
pump(data, 400);
|
||||
|
||||
/* playlists listed, one selected, its entries land through iterate */
|
||||
CHECK(SendMessageA(playlists, LVM_GETITEMCOUNT, 0, 0) == 4, "playlist list has 4 rows (%d)", (int)SendMessageA(playlists, LVM_GETITEMCOUNT, 0, 0));
|
||||
lv_text(playlists, 2, 0, buf, sizeof(buf));
|
||||
CHECK(string_is_equal(buf, "Nintendo - Nintendo Entertainment System"), "row 2 text (got %s)", buf);
|
||||
ListView_SetItemState(playlists, 2, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
pump(data, 600);
|
||||
CHECK(companion_core_entry_count(peek->core) == 3, "NES entries landed (%u)", (unsigned)companion_core_entry_count(peek->core));
|
||||
CHECK(SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0) == 3, "entries list shows 3 (%d)", (int)SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0));
|
||||
lv_text(entries, 0, 0, buf, sizeof(buf));
|
||||
CHECK(strlen(buf) > 0, "entry 0 has text (%s)", buf);
|
||||
|
||||
/* Icons view through the combo (what a click sends) */
|
||||
SendMessageA(view, CB_SETCURSEL, 1, 0);
|
||||
SendMessageA(hwnd, WM_COMMAND, MAKEWPARAM(IDC_CW_VIEW_COMBO, CBN_SELCHANGE), (LPARAM)view);
|
||||
pump(data, 300);
|
||||
{
|
||||
LONG_PTR st = GetWindowLongPtrA(entries, GWL_STYLE);
|
||||
CHECK((st & LVS_TYPEMASK) == LVS_ICON, "entries list is in icon view (style 0x%lx)", (long)(st & LVS_TYPEMASK));
|
||||
CHECK(SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0) == 3, "icon view keeps the 3 items");
|
||||
}
|
||||
send_command(hwnd, IDM_CW_VIEW_LIST);
|
||||
pump(data, 200);
|
||||
CHECK((GetWindowLongPtrA(entries, GWL_STYLE) & LVS_TYPEMASK) == LVS_REPORT, "back to list view");
|
||||
|
||||
/* search filters */
|
||||
SetWindowTextA(GetDlgItem(hwnd, IDC_CW_SEARCH), "metroid");
|
||||
pump(data, 200);
|
||||
CHECK(SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0) == 1, "search 'metroid' leaves 1 (%d)", (int)SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0));
|
||||
send_command(hwnd, IDC_CW_CLEAR);
|
||||
pump(data, 200);
|
||||
CHECK(SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0) == 3, "clear restores 3");
|
||||
|
||||
/* File Browser tab: the listing lands async; folders in the left list,
|
||||
* the whole listing in the content list with 4 columns */
|
||||
{
|
||||
NMHDR nm;
|
||||
TabCtrl_SetCurSel(tabs, 1);
|
||||
nm.hwndFrom = tabs; nm.idFrom = IDC_CW_TABS; nm.code = TCN_SELCHANGE;
|
||||
SendMessageA(hwnd, WM_NOTIFY, IDC_CW_TABS, (LPARAM)&nm);
|
||||
}
|
||||
pump(data, 900);
|
||||
CHECK(companion_core_browse_count(peek->core) >= 5, "browse listing landed (%u)", (unsigned)companion_core_browse_count(peek->core));
|
||||
CHECK(SendMessageA(playlists, LVM_GETITEMCOUNT, 0, 0) == (LRESULT)companion_core_browse_dir_count(peek->core), "folder pane rows = dir count (%d)", (int)SendMessageA(playlists, LVM_GETITEMCOUNT, 0, 0));
|
||||
lv_text(playlists, 0, 0, buf, sizeof(buf));
|
||||
CHECK(string_is_equal(buf, ".."), "folder row 0 is '..' (got %s)", buf);
|
||||
CHECK(SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0) == (LRESULT)companion_core_browse_count(peek->core), "content rows = browse count (%d)", (int)SendMessageA(entries, LVM_GETITEMCOUNT, 0, 0));
|
||||
CHECK(Header_GetItemCount(ListView_GetHeader(entries)) == 4, "Name / Size / Type / Date columns (%d)", Header_GetItemCount(ListView_GetHeader(entries)));
|
||||
lv_text(entries, 2, 2, buf, sizeof(buf));
|
||||
CHECK(strstr(buf, "File") != NULL, "Type column text for a file (%s)", buf);
|
||||
{
|
||||
HBITMAP bm = (HBITMAP)SendMessageA(GetDlgItem(hwnd, IDC_CW_BOXART), STM_GETIMAGE, IMAGE_BITMAP, 0);
|
||||
CHECK(bm == NULL, "boxart pane empty in the browser");
|
||||
}
|
||||
/* header click sorts by Type */
|
||||
{
|
||||
NMLISTVIEW nl;
|
||||
memset(&nl, 0, sizeof(nl));
|
||||
nl.hdr.hwndFrom = entries; nl.hdr.idFrom = IDC_CW_ENTRIES; nl.hdr.code = LVN_COLUMNCLICK; nl.iSubItem = 2;
|
||||
SendMessageA(hwnd, WM_NOTIFY, IDC_CW_ENTRIES, (LPARAM)&nl);
|
||||
pump(data, 200);
|
||||
CHECK(companion_core_browse_sort_column(peek->core) == COMPANION_BROWSE_SORT_TYPE, "column click sorts by Type");
|
||||
SendMessageA(hwnd, WM_NOTIFY, IDC_CW_ENTRIES, (LPARAM)&nl);
|
||||
pump(data, 200);
|
||||
CHECK(!companion_core_browse_sort_ascending(peek->core), "same column again flips to descending");
|
||||
}
|
||||
/* Up / Start Directory */
|
||||
send_command(hwnd, IDC_CW_BR_UP);
|
||||
pump(data, 900);
|
||||
CHECK(!string_is_equal(companion_core_browse_dir(peek->core), ""), "Up navigated (dir=%s)", companion_core_browse_dir(peek->core));
|
||||
send_command(hwnd, IDC_CW_BR_START);
|
||||
pump(data, 900);
|
||||
CHECK(strstr(companion_core_browse_dir(peek->core), "content") != NULL, "Start Directory returns to content (%s)", companion_core_browse_dir(peek->core));
|
||||
|
||||
/* back to playlists */
|
||||
{
|
||||
NMHDR nm;
|
||||
TabCtrl_SetCurSel(tabs, 0);
|
||||
nm.hwndFrom = tabs; nm.idFrom = IDC_CW_TABS; nm.code = TCN_SELCHANGE;
|
||||
SendMessageA(hwnd, WM_NOTIFY, IDC_CW_TABS, (LPARAM)&nm);
|
||||
}
|
||||
pump(data, 400);
|
||||
CHECK(SendMessageA(playlists, LVM_GETITEMCOUNT, 0, 0) == 4, "playlist list back to 4 rows");
|
||||
|
||||
/* menus: Stop / Unload reach the core; the three windows open with rows */
|
||||
{
|
||||
int before = stub_calls_command;
|
||||
send_command(hwnd, IDC_CW_STOP_BTN);
|
||||
CHECK(stub_calls_command > before, "Stop reaches the core");
|
||||
}
|
||||
{
|
||||
static struct retro_core_option_v2_definition defs[3];
|
||||
struct retro_core_options_v2 v2;
|
||||
char cfg[600];
|
||||
HWND ow, ol;
|
||||
memset(defs, 0, sizeof(defs));
|
||||
defs[0].key = "test_speed"; defs[0].desc = "Speed";
|
||||
defs[0].values[0].value = "slow"; defs[0].values[0].label = "Slow";
|
||||
defs[0].values[1].value = "fast"; defs[0].values[1].label = "Fast";
|
||||
defs[0].default_value = "fast";
|
||||
defs[1].key = "test_color"; defs[1].desc = "Colour";
|
||||
defs[1].values[0].value = "rgb"; defs[1].values[1].value = "mono";
|
||||
defs[1].default_value = "rgb";
|
||||
v2.categories = NULL; v2.definitions = defs;
|
||||
snprintf(cfg, sizeof(cfg), "%s/core.opt", root);
|
||||
test_runloop.core_options = core_option_manager_new(cfg, NULL, &v2, false);
|
||||
send_command(hwnd, IDM_CW_CORE_OPTIONS);
|
||||
pump(data, 300);
|
||||
ow = find_class_window("RetroArchCompanionCoreOptions");
|
||||
CHECK(ow && IsWindowVisible(ow), "Core Options window shown");
|
||||
ol = ow ? GetDlgItem(ow, 50101 + 0) : NULL; /* not a dialog id: find the list by class */
|
||||
(void)ol;
|
||||
ol = ow ? FindWindowExA(ow, NULL, "SysListView32", NULL) : NULL;
|
||||
CHECK(ol && SendMessageA(ol, LVM_GETITEMCOUNT, 0, 0) == 2, "2 options listed");
|
||||
if (ol)
|
||||
{
|
||||
lv_text(ol, 0, 1, buf, sizeof(buf));
|
||||
CHECK(string_is_equal(buf, "Fast"), "value label (got %s)", buf);
|
||||
ListView_SetItemState(ol, 0, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
{
|
||||
NMHDR nm; nm.hwndFrom = ol; nm.idFrom = GetDlgCtrlID(ol); nm.code = NM_DBLCLK;
|
||||
SendMessageA(ow, WM_NOTIFY, nm.idFrom, (LPARAM)&nm);
|
||||
}
|
||||
lv_text(ol, 0, 1, buf, sizeof(buf));
|
||||
CHECK(string_is_equal(buf, "Slow"), "double-click cycles (got %s)", buf);
|
||||
}
|
||||
if (ow) ShowWindow(ow, SW_HIDE);
|
||||
send_command(hwnd, IDM_CW_SHADER_PARAMS);
|
||||
pump(data, 300);
|
||||
ow = find_class_window("RetroArchCompanionShaderParams");
|
||||
CHECK(ow && IsWindowVisible(ow), "Shader Parameters window shown");
|
||||
ol = ow ? FindWindowExA(ow, NULL, "SysListView32", NULL) : NULL;
|
||||
CHECK(ol && SendMessageA(ol, LVM_GETITEMCOUNT, 0, 0) == 2, "2 parameters listed");
|
||||
if (ol)
|
||||
{
|
||||
lv_text(ol, 0, 2, buf, sizeof(buf));
|
||||
CHECK(string_is_equal(buf, "0 .. 1 (step 0.05)"), "range column (got %s)", buf);
|
||||
}
|
||||
if (ow) ShowWindow(ow, SW_HIDE);
|
||||
send_command(hwnd, IDM_CW_OPTIONS);
|
||||
pump(data, 300);
|
||||
ow = find_class_window("RetroArchCompanionOptions");
|
||||
CHECK(ow && IsWindowVisible(ow), "Options window shown");
|
||||
ol = ow ? FindWindowExA(ow, NULL, "SysListView32", NULL) : NULL;
|
||||
CHECK(ol && SendMessageA(ol, LVM_GETITEMCOUNT, 0, 0) == 13, "13 settings listed");
|
||||
if (ow) ShowWindow(ow, SW_HIDE);
|
||||
core_option_manager_free(test_runloop.core_options);
|
||||
test_runloop.core_options = NULL;
|
||||
}
|
||||
|
||||
/* close: the window hides */
|
||||
SendMessageA(hwnd, WM_CLOSE, 0, 0);
|
||||
pump(data, 200);
|
||||
CHECK(!IsWindowVisible(hwnd), "companion window hidden after close");
|
||||
|
||||
ui_companion_wimp_win32.deinit(data);
|
||||
companion_test_teardown_fixtures(root);
|
||||
if (fails)
|
||||
{
|
||||
printf("companion_win32_test: %d failure(s)\n", fails);
|
||||
return 1;
|
||||
}
|
||||
printf("companion_win32_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -138,13 +138,22 @@ static CFRunLoopObserverRef iterate_observer;
|
||||
static void rarch_draw_observer(CFRunLoopObserverRef observer,
|
||||
CFRunLoopActivity activity, void *info)
|
||||
{
|
||||
int ret = runloop_iterate();
|
||||
int ret;
|
||||
|
||||
if (ret == -1)
|
||||
/* The desktop companion's per-frame hook: it lands the playlist
|
||||
* parse, the browser's listing, decoded thumbnails and animation
|
||||
* frames, and keeps its status current. Only the Qt-era rarch_main
|
||||
* loop used to call it; this observer is the whole main loop on a
|
||||
* non-Qt build, so without the call the Cocoa companion showed
|
||||
* "Loading playlist..." for ever and nothing else. */
|
||||
ui_companion_driver_wimp_iterate();
|
||||
|
||||
ret = runloop_iterate();
|
||||
|
||||
if (ret == -1 || ui_companion_driver_wimp_exiting())
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
application->quit();
|
||||
#endif
|
||||
ui_companion_driver_wimp_quit();
|
||||
ui_companion_driver_wimp_deinit();
|
||||
main_exit(NULL);
|
||||
exit(0);
|
||||
return;
|
||||
|
||||
+19
-4
@@ -649,6 +649,20 @@ static ui_application_t ui_application_cocoa = {
|
||||
- (void)keyDown:(NSEvent *)event { }
|
||||
- (void)keyUp:(NSEvent *)event { }
|
||||
|
||||
/* Another window of this app took the keyboard - the desktop companion,
|
||||
* a panel. Every key that is down right now will be released into that
|
||||
* window, so this window never sees the key-up: the key stays "held" in
|
||||
* apple_key_state, and a held key is exactly what the menu's flush-and-
|
||||
* wait-for-release (menu_st->input_driver_flushing_input) waits on -
|
||||
* for ever, since the release never arrives. That was the desktop
|
||||
* companion's "keyboard does not come back after closing it": the hotkey
|
||||
* that opened it was still down when the companion took the keyboard.
|
||||
* Same treatment as losing the application's active state. */
|
||||
- (void)windowDidResignKey:(NSNotification *)notification
|
||||
{
|
||||
apple_input_keyboard_reset();
|
||||
}
|
||||
|
||||
- (void)windowDidBecomeKey:(NSNotification *)notification
|
||||
{
|
||||
settings_t *settings = config_get_ptr();
|
||||
@@ -1127,6 +1141,7 @@ static ui_application_t ui_application_cocoa = {
|
||||
#endif
|
||||
if (application)
|
||||
application->process_events();
|
||||
ui_companion_driver_wimp_iterate();
|
||||
|
||||
ret = runloop_iterate();
|
||||
|
||||
@@ -1138,15 +1153,14 @@ static ui_application_t ui_application_cocoa = {
|
||||
|
||||
while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.002, FALSE)
|
||||
== kCFRunLoopRunHandledSource);
|
||||
if (ret == -1)
|
||||
if (ret == -1 || ui_companion_driver_wimp_exiting())
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
application->quit();
|
||||
#endif
|
||||
ui_companion_driver_wimp_quit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ui_companion_driver_wimp_deinit();
|
||||
main_exit(NULL);
|
||||
}
|
||||
#endif
|
||||
@@ -1775,6 +1789,7 @@ ui_companion_driver_t ui_companion_cocoa = {
|
||||
ui_companion_cocoa_init,
|
||||
ui_companion_cocoa_deinit,
|
||||
ui_companion_cocoa_toggle,
|
||||
NULL, /* iterate */
|
||||
ui_companion_cocoa_event_command,
|
||||
NULL, /* notify_refresh */
|
||||
NULL, /* msg_queue_push */
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1593,6 +1593,7 @@ ui_companion_driver_t ui_companion_cocoatouch = {
|
||||
NULL, /* init */
|
||||
NULL, /* deinit */
|
||||
NULL, /* toggle */
|
||||
NULL, /* iterate */
|
||||
ui_companion_cocoatouch_event_command,
|
||||
NULL, /* notify_refresh */
|
||||
NULL, /* msg_queue_push */
|
||||
|
||||
+668
-1090
File diff suppressed because it is too large.
Load diff
+117
-133
@@ -30,6 +30,11 @@
|
||||
#include <QPalette>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPixmap>
|
||||
#include <QTimer>
|
||||
#include <QHash>
|
||||
#include <QPersistentModelIndex>
|
||||
#include <QAbstractTableModel>
|
||||
#include <QIcon>
|
||||
#include <QImage>
|
||||
#include <QPointer>
|
||||
#include <QProgressBar>
|
||||
@@ -57,6 +62,11 @@ extern "C" {
|
||||
#include <queues/task_queue.h>
|
||||
|
||||
#include "../ui_companion_driver.h"
|
||||
#include "../companion/companion_core.h"
|
||||
|
||||
/* Shared companion core owned by the running Qt companion; NULL when
|
||||
* the Qt companion has not been initialised. */
|
||||
companion_core_t *ui_companion_qt_core(void);
|
||||
#include "../../retroarch.h"
|
||||
#include <formats/image.h>
|
||||
|
||||
@@ -66,10 +76,10 @@ extern "C" {
|
||||
|
||||
#define ALL_PLAYLISTS_TOKEN "|||ALL|||"
|
||||
#define ICON_PATH "/xmb/dot-art/png/"
|
||||
#define THUMBNAIL_BOXART "Named_Boxarts"
|
||||
#define THUMBNAIL_SCREENSHOT "Named_Snaps"
|
||||
#define THUMBNAIL_TITLE "Named_Titles"
|
||||
#define THUMBNAIL_LOGO "Named_Logos"
|
||||
#define THUMBNAIL_BOXART COMPANION_THUMB_BOXART
|
||||
#define THUMBNAIL_SCREENSHOT COMPANION_THUMB_SCREENSHOT
|
||||
#define THUMBNAIL_TITLE COMPANION_THUMB_TITLE
|
||||
#define THUMBNAIL_LOGO COMPANION_THUMB_LOGO
|
||||
|
||||
class QApplication;
|
||||
class QCloseEvent;
|
||||
@@ -86,7 +96,6 @@ class QToolButton;
|
||||
class QTabWidget;
|
||||
class QPixmap;
|
||||
class QPaintEvent;
|
||||
class QSettings;
|
||||
class QCheckBox;
|
||||
class QSpinBox;
|
||||
class QFormLayout;
|
||||
@@ -117,91 +126,9 @@ static inline double lerp(double x, double y, double a, double b, double d)
|
||||
}
|
||||
|
||||
|
||||
class ThumbnailLoader : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ThumbnailLoader(QObject *parent = 0) : QThread(parent), m_stop(false) {}
|
||||
~ThumbnailLoader() { stop(); wait(); }
|
||||
void stop() { m_mutex.lock(); m_stop = true; m_cond.wakeOne(); m_mutex.unlock(); }
|
||||
void request(const QModelIndex &index, const QString &path)
|
||||
{
|
||||
m_mutex.lock();
|
||||
/* Store a QPersistentModelIndex, not a QModelIndex: a plain
|
||||
* QModelIndex is only valid until the model changes, and a
|
||||
* decode queued here may not be delivered until after the
|
||||
* playlist has been reset (beginResetModel/endResetModel),
|
||||
* at which point the row it referred to is gone. A persistent
|
||||
* index is kept up to date by the model and goes invalid if
|
||||
* its row is removed, so the wrong row is never signalled. */
|
||||
m_queue.append(qMakePair(QPersistentModelIndex(index), path));
|
||||
m_cond.wakeOne();
|
||||
m_mutex.unlock();
|
||||
}
|
||||
static void cleanupTexturePixels(void *pixels)
|
||||
{
|
||||
free(pixels);
|
||||
}
|
||||
static QImage loadImageRA(const QString &path)
|
||||
{
|
||||
struct texture_image tex;
|
||||
QByteArray pathArray = path.toUtf8();
|
||||
|
||||
tex.width = 0;
|
||||
tex.height = 0;
|
||||
tex.pixels = NULL;
|
||||
tex.supports_rgba = false;
|
||||
|
||||
tex.compressed = NULL;
|
||||
|
||||
if (image_texture_load(&tex, pathArray.constData()))
|
||||
{
|
||||
if (tex.pixels)
|
||||
{
|
||||
/* Transfer pixel ownership to QImage - no copy needed.
|
||||
* QImage will call free() on the buffer when destroyed. */
|
||||
return QImage((unsigned char*)tex.pixels,
|
||||
tex.width, tex.height,
|
||||
tex.width * sizeof(uint32_t),
|
||||
QImage::Format_ARGB32,
|
||||
cleanupTexturePixels, tex.pixels);
|
||||
}
|
||||
|
||||
/* GPU-native (e.g. BCn) path: image_texture_load succeeded but
|
||||
* handed back compressed blocks rather than an RGBA buffer.
|
||||
* We have no way to display these here, so release them (this
|
||||
* would otherwise leak the compressed struct + mips + storage)
|
||||
* and fall back to Qt's own decoder. */
|
||||
image_texture_free(&tex);
|
||||
}
|
||||
|
||||
/* Fallback to Qt for unsupported formats */
|
||||
return QImage(path);
|
||||
}
|
||||
signals:
|
||||
void imageLoaded(const QImage image, const QPersistentModelIndex index, const QString path);
|
||||
protected:
|
||||
void run()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
m_mutex.lock();
|
||||
while (m_queue.isEmpty() && !m_stop)
|
||||
m_cond.wait(&m_mutex);
|
||||
if (m_stop) { m_mutex.unlock(); return; }
|
||||
QPair<QPersistentModelIndex, QString> item = m_queue.takeFirst();
|
||||
m_mutex.unlock();
|
||||
QImage image = loadImageRA(item.second);
|
||||
if (!image.isNull())
|
||||
emit imageLoaded(image, item.first, item.second);
|
||||
}
|
||||
}
|
||||
private:
|
||||
QMutex m_mutex;
|
||||
QWaitCondition m_cond;
|
||||
QList<QPair<QPersistentModelIndex, QString> > m_queue;
|
||||
bool m_stop;
|
||||
};
|
||||
extern "C" {
|
||||
#include "../companion/companion_thumbs.h"
|
||||
}
|
||||
|
||||
class PlaylistModel : public QAbstractListModel
|
||||
{
|
||||
@@ -215,6 +142,7 @@ public:
|
||||
};
|
||||
|
||||
PlaylistModel(QObject *parent = 0);
|
||||
~PlaylistModel();
|
||||
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
|
||||
@@ -223,6 +151,12 @@ public:
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
void addPlaylistItems(const QStringList &paths, bool add = false);
|
||||
/* True while addPlaylistItems() is still waiting on the companion
|
||||
* core's budgeted parse of one or more playlists. */
|
||||
bool isLoadingPlaylists() const;
|
||||
/* Companion core notification: the requested playlist finished
|
||||
* parsing. Appends its entries and starts the next pending one. */
|
||||
void onCorePlaylistChanged();
|
||||
void addDir(QString path, QFlags<QDir::Filter> showHidden);
|
||||
void setThumbnailType(const ThumbnailType type);
|
||||
void loadThumbnail(const QModelIndex &index);
|
||||
@@ -230,27 +164,67 @@ public:
|
||||
void reloadThumbnailPath(const QString path);
|
||||
void reloadSystemThumbnails(const QString system);
|
||||
void setThumbnailCacheLimit(int limit);
|
||||
/* Edge (px) the grid draws thumbnails at; requests are made at this
|
||||
* size so the engine hands back cells that need no further scaling. */
|
||||
void setThumbnailSize(int size);
|
||||
int thumbnailSize() const { return m_thumbSize; }
|
||||
/* Any image at any size through the same engine (the sidebar
|
||||
* thumbnails and the file-browser preview). imageAt() gives the
|
||||
* cached pixmap; requestImage() queues a decode, after which
|
||||
* thumbnailReady(path) is emitted. */
|
||||
bool imageAt(const QString &path, int w, int h, QPixmap *out) const;
|
||||
void requestImage(const QString &path, int w, int h);
|
||||
/* Play @path in the sidebar (APNG / animated WEBP / WEBM / MP4 - a
|
||||
* still plays nothing): frames come as frameReady(path, pixmap). */
|
||||
void animateImage(const QString &path, int w, int h);
|
||||
void stopAnimation();
|
||||
/* Drop queued decodes and abandon those in flight (the view moved
|
||||
* on); cached images stay. */
|
||||
void abandonPending();
|
||||
bool isSupportedImage(const QString path) const;
|
||||
QString getPlaylistThumbnailsDir(const QString playlistName) const;
|
||||
QString getSanitizedThumbnailName(QString dir, QString label) const;
|
||||
QString getPlaylistThumbnailsDir(const QString playlistName, const QString type) const;
|
||||
/* Repository thumbnail file for a label, ignoring whether the content
|
||||
* itself is an image (the save target / the sidebar images). */
|
||||
QString getRepositoryThumbnailPath(const QString playlistName, const QString labelNoExt, const QString type) const;
|
||||
|
||||
signals:
|
||||
void imageLoaded(const QImage image, const QModelIndex &index, const QString &path);
|
||||
/* Emitted once addPlaylistItems() has populated the model. */
|
||||
void playlistsLoaded();
|
||||
|
||||
private slots:
|
||||
void onImageLoaded(const QImage image, const QPersistentModelIndex &index, const QString &path);
|
||||
void pollThumbnails();
|
||||
signals:
|
||||
void thumbnailReady(const QString &path);
|
||||
void frameReady(const QString &path, const QPixmap &frame);
|
||||
|
||||
private:
|
||||
/* Thumbnails come from the shared companion engine (decode threads,
|
||||
* path+size keyed cache, visible-first queue) - the same one the
|
||||
* native Win32 and Cocoa companions draw through. m_cache only holds
|
||||
* QPixmap conversions of engine pixels, keyed path@size. */
|
||||
companion_thumbs_t *m_engine = NULL;
|
||||
int m_thumbSize = 256;
|
||||
QHash<QString, QPersistentModelIndex> m_pendingRows;
|
||||
QTimer m_pollTimer;
|
||||
static void onEngineDone(void *ud, const char *path, int w, int h,
|
||||
uintptr_t tag, const uint32_t *bits);
|
||||
void thumbnailArrived(const QString &path);
|
||||
QVector<PlaylistEntry> m_contents;
|
||||
QCache<QString, QPixmap> m_cache;
|
||||
QSet<QString> m_pendingImages;
|
||||
QRegularExpression m_fileSanitizerRegex;
|
||||
/* addPlaylistItems() state: playlists still to load, and the
|
||||
* entries collected so far (committed to m_contents in one reset). */
|
||||
QStringList m_pendingPaths;
|
||||
QVector<PlaylistEntry> m_pendingContents;
|
||||
bool m_loadingPlaylists = false;
|
||||
void appendEntriesFromCore();
|
||||
void startNextPendingPlaylist();
|
||||
mutable QCache<QString, QPixmap> m_cache; /* filled lazily from data() */
|
||||
/* stages of imageAt()/data(): one conversion per (path, w, h) */
|
||||
QPixmap *pixmapFor(const QString &path, int w, int h) const;
|
||||
ThumbnailType m_thumbnailType = THUMBNAIL_TYPE_BOXART;
|
||||
ThumbnailLoader *m_thumbnailLoader;
|
||||
QString getThumbnailPath(const QModelIndex &index, QString type) const;
|
||||
QString getThumbnailPath(const PlaylistEntry &entry, QString type) const;
|
||||
QString getCurrentTypeThumbnailPath(const QModelIndex &index) const;
|
||||
void getPlaylistItems(QString path);
|
||||
};
|
||||
|
||||
class ThumbnailWidget : public QStackedWidget
|
||||
@@ -384,21 +358,32 @@ public:
|
||||
void setThumbnailVerticalAlign(const QString valign);
|
||||
};
|
||||
|
||||
class FileSystemProxyModel : public QSortFilterProxyModel
|
||||
/* The file browser's table, backed by the companion core's browse
|
||||
* listing (enumerated, stat'ed and formatted off the UI thread, shared
|
||||
* by every companion): this class only paints it. Columns are Qt's
|
||||
* Name / Size / Type / Date Modified. A search filter keeps a row map. */
|
||||
class BrowseTableModel : public QAbstractTableModel
|
||||
{
|
||||
protected:
|
||||
virtual bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
|
||||
Q_OBJECT
|
||||
public:
|
||||
BrowseTableModel(QObject *parent = 0) : QAbstractTableModel(parent) {}
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const { (void)parent; return 4; }
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
|
||||
/* The core listing changed: rebuild the row map and reset. */
|
||||
void reload();
|
||||
void setFilter(const QRegularExpression &re);
|
||||
/* Header click: the core sorts, every companion the same way. */
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
|
||||
/* Browse index (into the core listing) behind a table row, or -1. */
|
||||
long browseIndex(const QModelIndex &index) const;
|
||||
QString pathAt(const QModelIndex &index) const;
|
||||
bool isDirAt(const QModelIndex &index) const;
|
||||
private:
|
||||
/* Cache for filterAcceptsRow's "is this row a child of the
|
||||
* source model's current root?" check. The QFileSystemModel
|
||||
* root path rarely changes, but filterAcceptsRow can fire
|
||||
* thousands of times per directory load / sort / filter, and
|
||||
* resolving rootPath() to a QModelIndex via sm->index(...)
|
||||
* is not free. Compare the cached path string against the
|
||||
* current one on each call; refresh both members on mismatch. */
|
||||
mutable QString m_cachedRootPath;
|
||||
mutable QModelIndex m_cachedRootIndex;
|
||||
QVector<long> m_rows; /* table row -> browse index */
|
||||
QRegularExpression m_filter;
|
||||
QIcon m_folderIcon, m_fileIcon, m_driveIcon;
|
||||
};
|
||||
|
||||
class LoadCoreTableWidget : public QTableWidget
|
||||
@@ -417,7 +402,9 @@ class LoadCoreWindow : public QMainWindow
|
||||
Q_OBJECT
|
||||
public:
|
||||
LoadCoreWindow(QWidget *parent = 0);
|
||||
void initCoreList(const QStringList &extensionFilters = QStringList());
|
||||
/* @contentPath: when non-empty, only cores that can run it (by
|
||||
* extension, archive members included) are shown. */
|
||||
void initCoreList(const QString &contentPath = QString());
|
||||
void setStatusLabel(QString label);
|
||||
signals:
|
||||
void coreLoaded();
|
||||
@@ -488,7 +475,6 @@ public:
|
||||
QTabWidget* browserAndPlaylistTabWidget();
|
||||
QString getPlaylistDefaultCore(QString plName);
|
||||
ViewOptionsDialog* viewOptionsDialog();
|
||||
QSettings* settings();
|
||||
QVector<QHash<QString, QString> > getCoreInfo();
|
||||
void setTheme(Theme theme = THEME_SYSTEM_DEFAULT);
|
||||
Theme theme();
|
||||
@@ -566,9 +552,12 @@ public slots:
|
||||
void onFileDropWidgetContextMenuRequested(const QPoint &pos);
|
||||
void showAbout();
|
||||
void showDocs();
|
||||
void onThumbnailPackExtractFinished(bool success);
|
||||
void onSingleThumbnailDownloadFinishedInternal(const char *system, const char *title, const char *final_path, bool success);
|
||||
void onPlaylistThumbnailDownloadFinishedInternal(const char *system, const char *title, const char *final_path, bool success);
|
||||
/* companion core download results (see ui_companion_qt_core_callbacks) */
|
||||
void onCoreThumbnailDownloaded(QString system, QString title, QString path, bool success);
|
||||
void onCoreThumbnailPackFinished(int result);
|
||||
void onBrowseChanged();
|
||||
void onSingleThumbnailDownloadFinishedInternal(QString system, QString title, QString path, bool success);
|
||||
void onPlaylistThumbnailDownloadFinishedInternal(QString path, bool success);
|
||||
void deferReloadShaderParams();
|
||||
void downloadThumbnail(QString system, QString title, QUrl url = QUrl());
|
||||
void downloadAllThumbnails(QString system, QUrl url = QUrl());
|
||||
@@ -578,17 +567,22 @@ public slots:
|
||||
void onThumbnailDropped(const QImage &image, ThumbnailType type);
|
||||
|
||||
private slots:
|
||||
void onLoadCoreClicked(const QStringList &extensionFilters = QStringList());
|
||||
void onLoadCoreClicked(const QString &contentPath = QString());
|
||||
void onUnloadCoreMenuAction();
|
||||
void onTimeout();
|
||||
void onCoreLoaded();
|
||||
void onCurrentTableItemDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles);
|
||||
void onCurrentListItemChanged(QListWidgetItem *current, QListWidgetItem *previous);
|
||||
void onPlaylistModelLoaded();
|
||||
void onScanDirectoryClicked();
|
||||
void onQuitRetroArchClicked();
|
||||
void onCurrentListItemDataChanged(QListWidgetItem *item);
|
||||
void onCurrentItemChanged(const QModelIndex &index);
|
||||
void onCurrentItemChanged(const PlaylistEntry &entry);
|
||||
void onCurrentFileChanged(const QModelIndex &index);
|
||||
void onPreviewImageLoaded(const QImage image, const QPersistentModelIndex &index, const QString &path);
|
||||
void onThumbnailReady(const QString &path);
|
||||
void onFrameReady(const QString &path, const QPixmap &frame);
|
||||
void showSidebarImage(int idx, const QString &path, bool acceptDrop);
|
||||
void onSearchEnterPressed();
|
||||
void onSearchLineEditEdited(const QString &text);
|
||||
void onContentItemDoubleClicked(const QModelIndex &index);
|
||||
@@ -609,10 +603,8 @@ private slots:
|
||||
void onContributorsClicked();
|
||||
void onItemChanged();
|
||||
void onFileSystemDirLoaded(const QString &path);
|
||||
void onFileBrowserTableDirLoaded(const QString &path);
|
||||
void onDownloadScroll(QString path);
|
||||
void onDownloadScrollAgain(QString path);
|
||||
int onExtractArchive(QString path, QString extractionDir, QString tempExtension, retro_task_callback_t cb);
|
||||
|
||||
void onThumbnailDownloadCanceled();
|
||||
void onDownloadThumbnail(QString system, QString title);
|
||||
@@ -647,7 +639,7 @@ private:
|
||||
|
||||
PlaylistModel *m_playlistModel;
|
||||
QSortFilterProxyModel *m_proxyModel;
|
||||
FileSystemProxyModel *m_proxyFileModel;
|
||||
BrowseTableModel *m_browseModel;
|
||||
LoadCoreWindow *m_loadCoreWindow;
|
||||
QTimer *m_timer;
|
||||
QString m_currentCore;
|
||||
@@ -655,7 +647,6 @@ private:
|
||||
QLabel *m_statusLabel;
|
||||
TreeView *m_dirTree;
|
||||
QFileSystemModel *m_dirModel;
|
||||
QFileSystemModel *m_fileModel;
|
||||
ListWidget *m_listWidget;
|
||||
QStackedWidget *m_centralWidget;
|
||||
TableView *m_tableView;
|
||||
@@ -672,16 +663,10 @@ private:
|
||||
QToolButton *m_stopPushButton;
|
||||
QTabWidget *m_browserAndPlaylistTabWidget;
|
||||
bool m_pendingRun;
|
||||
QPixmap *m_thumbnailPixmaps[4];
|
||||
/* Background loader for the file-browser preview pane. The
|
||||
* preview is decoded asynchronously so that selecting a large
|
||||
* image in the file table does not block the UI thread on a
|
||||
* full-resolution PNG decode. m_pendingPreviewPath is the path
|
||||
* we last requested; results for any other path are stale and
|
||||
* dropped on arrival. */
|
||||
ThumbnailLoader *m_previewLoader;
|
||||
QString m_pendingPreviewPath;
|
||||
QSettings *m_settings;
|
||||
/* Sidebar (four thumbnail widgets) and file-browser preview: the
|
||||
* path each widget is waiting on from the model's engine. */
|
||||
QString m_sidebarPending[4];
|
||||
bool m_sidebarAcceptDrop = false;
|
||||
ViewOptionsDialog *m_viewOptionsDialog;
|
||||
CoreInfoDialog *m_coreInfoDialog;
|
||||
QStyle *m_defaultStyle;
|
||||
@@ -712,9 +697,8 @@ private:
|
||||
QElapsedTimer m_statusMessageElapsedTimer;
|
||||
QPointer<ShaderParamsDialog> m_shaderParamsDialog;
|
||||
QPointer<CoreOptionsDialog> m_coreOptionsDialog;
|
||||
retro_task_t *m_currentHttpTask;
|
||||
bool m_downloadingPlaylistThumbnails;
|
||||
|
||||
QProgressDialog *m_updateProgressDialog;
|
||||
|
||||
QProgressDialog *m_thumbnailDownloadProgressDialog;
|
||||
QStringList m_pendingThumbnailDownloadTypes;
|
||||
|
||||
+484
-936
File diff suppressed because it is too large.
Load diff
@@ -47,7 +47,6 @@ class QLabel;
|
||||
class QLayout;
|
||||
class QPaintEvent;
|
||||
class QResizeEvent;
|
||||
class QSettings;
|
||||
class QVBoxLayout;
|
||||
/* Forward decls kept unconditional so that callers holding bare
|
||||
* pointers to these (e.g. ui_qt.h's QPointer<ShaderParamsDialog>
|
||||
@@ -690,6 +689,9 @@ private:
|
||||
void refresh();
|
||||
|
||||
int m_size = 255;
|
||||
public:
|
||||
int gridSize() const { return m_size; }
|
||||
private:
|
||||
int m_spacing = DEFAULT_GRID_SPACING;
|
||||
QVector<QModelIndex> m_visibleIndexes;
|
||||
ViewMode m_viewMode = Centered;
|
||||
@@ -835,9 +837,7 @@ private:
|
||||
void showOrHideHighlightColor();
|
||||
|
||||
MainWindow *m_mainwindow;
|
||||
QSettings *m_settings;
|
||||
QCheckBox *m_saveGeometryCheckBox;
|
||||
QCheckBox *m_saveDockPositionsCheckBox;
|
||||
QCheckBox *m_saveLastTabCheckBox;
|
||||
QCheckBox *m_showHiddenFilesCheckBox;
|
||||
QComboBox *m_themeComboBox;
|
||||
@@ -902,7 +902,6 @@ private:
|
||||
void loadPlaylistOptions();
|
||||
|
||||
MainWindow *m_mainwindow;
|
||||
QSettings *m_settings;
|
||||
QLineEdit *m_nameLineEdit;
|
||||
QLineEdit *m_pathLineEdit;
|
||||
QLineEdit *m_extensionsLineEdit;
|
||||
|
||||
@@ -1101,7 +1101,7 @@ static enum msg_hash_enums menu_id_to_label_enum(unsigned int menuId)
|
||||
return MENU_ENUM_LABEL_VALUE_INPUT_META_SCREENSHOT;
|
||||
case ID_M_MUTE_TOGGLE:
|
||||
return MENU_ENUM_LABEL_VALUE_INPUT_META_MUTE;
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
case ID_M_TOGGLE_DESKTOP:
|
||||
return MENU_ENUM_LABEL_VALUE_INPUT_META_UI_COMPANION_TOGGLE;
|
||||
#endif
|
||||
@@ -1444,7 +1444,8 @@ HMENU win32_resources_create_menu(void)
|
||||
win32_append_popup_utf8(window_menu, scale_menu,
|
||||
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_SCALE));
|
||||
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
/* Any desktop companion (Qt or the native one), not Qt alone. */
|
||||
AppendMenuA(window_menu, MF_STRING, ID_M_TOGGLE_DESKTOP,
|
||||
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_INPUT_META_UI_COMPANION_TOGGLE));
|
||||
#endif
|
||||
@@ -1599,6 +1600,7 @@ ui_companion_driver_t ui_companion_win32 = {
|
||||
ui_companion_win32_init,
|
||||
ui_companion_win32_deinit,
|
||||
ui_companion_win32_toggle,
|
||||
NULL, /* iterate */
|
||||
ui_companion_win32_event_command,
|
||||
NULL,
|
||||
NULL,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,103 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2026 - The RetroArch team
|
||||
*
|
||||
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
||||
* of the GNU General Public License as published by the Free Software Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
* PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* The Win32 companion's control and menu ids. Private to the driver,
|
||||
* shared with its regression harness (ui/companion/test/
|
||||
* companion_win32_test.c) so the harness sends exactly the commands the
|
||||
* widgets send. */
|
||||
|
||||
#ifndef __UI_WIN32_COMPANION_IDS_H
|
||||
#define __UI_WIN32_COMPANION_IDS_H
|
||||
|
||||
enum
|
||||
{
|
||||
IDC_CW_PLAYLISTS = 50001,
|
||||
IDC_CW_ENTRIES,
|
||||
IDC_CW_STATUS,
|
||||
IDC_CW_LOG,
|
||||
IDC_CW_INFO, /* core information pane: list view */
|
||||
IDC_CW_SEARCH, /* search box (left column, top) */
|
||||
IDC_CW_BOXART, /* boxart preview (right column, below info) */
|
||||
IDC_CW_SEARCH_LABEL,
|
||||
IDC_CW_CLEAR, /* "Clear" next to the search box */
|
||||
IDC_CW_BROWSER_LABEL,
|
||||
IDC_CW_TABS, /* Playlists / File Browser */
|
||||
IDC_CW_BR_UP, /* file browser: Up / Start Directory / Downloads */
|
||||
IDC_CW_BR_START,
|
||||
IDC_CW_BR_DOWNLOADS,
|
||||
IDC_CW_CORE_LABEL,
|
||||
IDC_CW_CORE_COMBO, /* launch-with core selection */
|
||||
IDC_CW_CORE_INFO_BTN,
|
||||
IDC_CW_RUN_BTN,
|
||||
IDC_CW_STOP_BTN,
|
||||
IDC_CW_ITEMS_LABEL,/* "N items" footer */
|
||||
IDC_CW_VIEW_LABEL,
|
||||
IDC_CW_VIEW_COMBO, /* List / Icons */
|
||||
IDC_CW_ZOOM_LABEL,
|
||||
IDC_CW_ZOOM, /* icon-view zoom trackbar */
|
||||
IDC_CW_THUMB_COMBO,/* boxart / screenshot / title / logo for the grid */
|
||||
IDC_CW_BOXART_TABS,/* the same four, for the boxart pane */
|
||||
IDC_CW_INFO_LABEL,
|
||||
IDC_CW_BOXART_LABEL,
|
||||
IDC_CW_CORES, /* Load Core window: list view */
|
||||
IDC_CW_CORES_OK,
|
||||
IDC_CW_CORES_CANCEL,
|
||||
IDC_CW_OPTS_LIST, /* Core Options window */
|
||||
IDC_CW_OPTS_RESET,
|
||||
IDC_CW_OPTS_RESET_ALL,
|
||||
IDC_CW_OPTS_CLOSE,
|
||||
IDC_CW_SHP_LIST, /* Shader Parameters window */
|
||||
IDC_CW_SHP_EDIT,
|
||||
IDC_CW_SHP_APPLY,
|
||||
IDC_CW_SHP_RESET,
|
||||
IDC_CW_SHP_CLOSE,
|
||||
IDC_CW_SET_LIST, /* Options window */
|
||||
IDC_CW_SET_EDIT,
|
||||
IDC_CW_SET_APPLY,
|
||||
IDC_CW_SET_CLOSE,
|
||||
IDM_CW_LOAD_CORE = 50101,
|
||||
IDM_CW_LOAD_CONTENT,
|
||||
IDM_CW_REFRESH,
|
||||
IDM_CW_RENAME_PLAYLIST,
|
||||
IDM_CW_ADD_FILES,
|
||||
IDM_CW_CLOSE,
|
||||
IDM_CW_QUIT,
|
||||
IDM_CW_START_CORE,
|
||||
IDM_CW_RUN,
|
||||
IDM_CW_DELETE_ENTRY,
|
||||
IDM_CW_ASSOC_DETECT,
|
||||
IDM_CW_SCAN_DIR,
|
||||
IDM_CW_TOGGLE_LOG,
|
||||
IDM_CW_TOGGLE_INFO,
|
||||
IDM_CW_TOGGLE_BOXART,
|
||||
IDM_CW_VIEW_LIST,
|
||||
IDM_CW_VIEW_ICONS,
|
||||
IDM_CW_BROWSE_FILES,
|
||||
IDM_CW_FIND,
|
||||
IDM_CW_HELP_DOCS,
|
||||
IDM_CW_HELP_ABOUT,
|
||||
IDM_CW_CORE_OPTIONS,
|
||||
IDM_CW_SHADER_PARAMS,
|
||||
IDM_CW_OPTIONS,
|
||||
IDM_CW_HELP_CONTRIBUTORS,
|
||||
IDM_CW_UNLOAD_CORE,
|
||||
IDM_CW_LOAD_CUSTOM_CORE,
|
||||
/* IDM_CW_ASSOC_BASE + i selects installed core i as the playlist's
|
||||
* default core; keep a wide gap after it. */
|
||||
IDM_CW_ASSOC_BASE = 51000,
|
||||
IDM_CW_ASSOC_MAX = 59999
|
||||
};
|
||||
|
||||
#endif
|
||||
+190
-38
@@ -20,6 +20,9 @@
|
||||
#include "../config.h"
|
||||
#endif
|
||||
|
||||
#include <string/stdstring.h>
|
||||
|
||||
#include "../configuration.h"
|
||||
#include "../list_special.h"
|
||||
#include "../verbosity.h"
|
||||
|
||||
@@ -32,6 +35,7 @@ static ui_companion_driver_t ui_companion_null = {
|
||||
NULL, /* init */
|
||||
NULL, /* deinit */
|
||||
NULL, /* toggle */
|
||||
NULL, /* iterate */
|
||||
NULL, /* event_command */
|
||||
NULL, /* notify_refresh */
|
||||
NULL, /* msg_queue_push */
|
||||
@@ -49,6 +53,7 @@ static ui_companion_driver_t ui_companion_null = {
|
||||
"null", /* ident */
|
||||
};
|
||||
|
||||
/* Platform drivers: OS glue. Exactly one is active, chosen by platform. */
|
||||
static const ui_companion_driver_t *ui_companion_drivers[] = {
|
||||
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
|
||||
&ui_companion_win32,
|
||||
@@ -63,6 +68,22 @@ static const ui_companion_driver_t *ui_companion_drivers[] = {
|
||||
NULL
|
||||
};
|
||||
|
||||
/* Desktop companion ("WIMP") drivers: the playlist / content browser
|
||||
* window. Selected by settings->arrays.ui_companion_driver. Order is
|
||||
* preference order for the default. */
|
||||
static const ui_companion_driver_t *ui_companion_wimp_drivers[] = {
|
||||
#ifdef HAVE_QT
|
||||
&ui_companion_qt,
|
||||
#endif
|
||||
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
|
||||
&ui_companion_wimp_win32,
|
||||
#endif
|
||||
#if defined(HAVE_COCOA) && TARGET_OS_OSX
|
||||
&ui_companion_wimp_cocoa,
|
||||
#endif
|
||||
&ui_companion_null,
|
||||
NULL
|
||||
};
|
||||
|
||||
static uico_driver_state_t uico_driver_st = {0}; /* double alignment */
|
||||
|
||||
@@ -79,15 +100,86 @@ uint8_t ui_companion_get_flags(void)
|
||||
return uico_st->flags;
|
||||
}
|
||||
|
||||
/* --- Desktop companion driver selection ------------------------------ */
|
||||
|
||||
const ui_companion_driver_t *ui_companion_wimp_find_driver(const char *ident)
|
||||
{
|
||||
unsigned i;
|
||||
for (i = 0; ui_companion_wimp_drivers[i]; i++)
|
||||
{
|
||||
if (string_is_equal(ui_companion_wimp_drivers[i]->ident, ident))
|
||||
return ui_companion_wimp_drivers[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *ui_companion_wimp_find_ident(int idx)
|
||||
{
|
||||
int i;
|
||||
if (idx < 0)
|
||||
return NULL;
|
||||
for (i = 0; ui_companion_wimp_drivers[i]; i++)
|
||||
{
|
||||
if (i == idx)
|
||||
return ui_companion_wimp_drivers[i]->ident;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const char *config_get_ui_companion_driver_options(void)
|
||||
{
|
||||
return char_list_new_special(STRING_LIST_UI_COMPANION_DRIVERS, NULL);
|
||||
}
|
||||
|
||||
const char *config_get_default_ui_companion(void)
|
||||
{
|
||||
return ui_companion_wimp_drivers[0]->ident;
|
||||
}
|
||||
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
/* Resolve the configured desktop companion driver. An unknown ident
|
||||
* falls back to the default (first in the table) so a config written
|
||||
* by a build with a different driver set still gets a companion. */
|
||||
static const ui_companion_driver_t *ui_companion_wimp_select(void)
|
||||
{
|
||||
settings_t *settings = config_get_ptr();
|
||||
const ui_companion_driver_t *drv = NULL;
|
||||
|
||||
if (settings && !string_is_empty(settings->arrays.ui_companion_driver))
|
||||
drv = ui_companion_wimp_find_driver(settings->arrays.ui_companion_driver);
|
||||
if (!drv)
|
||||
drv = ui_companion_wimp_drivers[0];
|
||||
return drv;
|
||||
}
|
||||
|
||||
static bool ui_companion_wimp_init(uico_driver_state_t *uico_st)
|
||||
{
|
||||
if (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
return true;
|
||||
if (!uico_st->wimp)
|
||||
uico_st->wimp = ui_companion_wimp_select();
|
||||
if (!uico_st->wimp || !uico_st->wimp->init)
|
||||
return false;
|
||||
uico_st->wimp_data = uico_st->wimp->init();
|
||||
if (!uico_st->wimp_data)
|
||||
return false;
|
||||
uico_st->flags |= UICO_ST_FLAG_WIMP_IS_INITED;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* --- Lifecycle -------------------------------------------------------- */
|
||||
|
||||
void ui_companion_event_command(enum event_command action)
|
||||
{
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
const ui_companion_driver_t *ui = uico_st->drv;
|
||||
if (ui && ui->event_command)
|
||||
ui->event_command(uico_st->data, action);
|
||||
#ifdef HAVE_QT
|
||||
if (ui_companion_qt.toggle && (uico_st->flags & UICO_ST_FLAG_QT_IS_INITED))
|
||||
ui_companion_qt.event_command(uico_st->qt_data, action);
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
if ( (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
&& uico_st->wimp && uico_st->wimp->event_command)
|
||||
uico_st->wimp->event_command(uico_st->wimp_data, action);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -101,14 +193,22 @@ void ui_companion_driver_deinit(void)
|
||||
if (ui->deinit)
|
||||
ui->deinit(uico_st->data);
|
||||
|
||||
#ifdef HAVE_QT
|
||||
if (uico_st->flags & UICO_ST_FLAG_QT_IS_INITED)
|
||||
ui_companion_driver_wimp_deinit();
|
||||
uico_st->data = NULL;
|
||||
}
|
||||
|
||||
void ui_companion_driver_wimp_deinit(void)
|
||||
{
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
if (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
{
|
||||
ui_companion_qt.deinit(uico_st->qt_data);
|
||||
uico_st->qt_data = NULL;
|
||||
if (uico_st->wimp && uico_st->wimp->deinit)
|
||||
uico_st->wimp->deinit(uico_st->wimp_data);
|
||||
uico_st->wimp_data = NULL;
|
||||
uico_st->flags &= ~UICO_ST_FLAG_WIMP_IS_INITED;
|
||||
}
|
||||
#endif
|
||||
uico_st->data = NULL;
|
||||
}
|
||||
|
||||
void ui_companion_driver_toggle(
|
||||
@@ -120,21 +220,57 @@ void ui_companion_driver_toggle(
|
||||
if (uico_st && uico_st->drv && uico_st->drv->toggle)
|
||||
uico_st->drv->toggle(uico_st->data, false);
|
||||
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
if (desktop_menu_enable)
|
||||
{
|
||||
if ((ui_companion_toggle || force) && (!(uico_st->flags & UICO_ST_FLAG_QT_IS_INITED)))
|
||||
{
|
||||
uico_st->qt_data = ui_companion_qt.init();
|
||||
uico_st->flags |= UICO_ST_FLAG_QT_IS_INITED;
|
||||
}
|
||||
if (ui_companion_toggle || force)
|
||||
ui_companion_wimp_init(uico_st);
|
||||
|
||||
if (ui_companion_qt.toggle && (uico_st->flags & UICO_ST_FLAG_QT_IS_INITED))
|
||||
ui_companion_qt.toggle(uico_st->qt_data, force);
|
||||
if ( (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
&& uico_st->wimp && uico_st->wimp->toggle)
|
||||
uico_st->wimp->toggle(uico_st->wimp_data, force);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ui_companion_driver_wimp_iterate(void)
|
||||
{
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
const ui_companion_driver_t *ui = uico_st->wimp;
|
||||
if (!ui || !(uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED))
|
||||
return;
|
||||
if (ui->iterate)
|
||||
ui->iterate(uico_st->wimp_data);
|
||||
/* A companion that owns its own toolkit event loop (Qt) pumps it
|
||||
* here; the native companions leave this NULL and ride the platform
|
||||
* driver's pump. */
|
||||
if (ui->application && ui->application->process_events)
|
||||
ui->application->process_events();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ui_companion_driver_wimp_exiting(void)
|
||||
{
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
const ui_companion_driver_t *ui = uico_st->wimp;
|
||||
if (ui && ui->application)
|
||||
return ui->application->exiting;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
void ui_companion_driver_wimp_quit(void)
|
||||
{
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
const ui_companion_driver_t *ui = uico_st->wimp;
|
||||
if (ui && ui->application && ui->application->quit)
|
||||
ui->application->quit();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ui_companion_driver_init_first(
|
||||
bool desktop_menu_enable,
|
||||
bool ui_companion_toggle,
|
||||
@@ -142,8 +278,9 @@ void ui_companion_driver_init_first(
|
||||
)
|
||||
{
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
#ifdef HAVE_QT
|
||||
/* Defer Qt companion (WIMP) initialization until the desktop menu
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
uico_st->wimp = ui_companion_wimp_select();
|
||||
/* Defer desktop companion initialization until the desktop menu
|
||||
* is actually toggled on. Constructing QApplication at startup
|
||||
* (which happens here only when desktop_menu_enable is set) aborts
|
||||
* the process on headless / KMS / Wayland-only systems with no
|
||||
@@ -151,11 +288,7 @@ void ui_companion_driver_init_first(
|
||||
* internally on platform-plugin init failure. This matches the
|
||||
* pre-1.21 behaviour for both Qt5 and Qt6. */
|
||||
if (desktop_menu_enable && ui_companion_toggle)
|
||||
{
|
||||
uico_st->qt_data = ui_companion_qt.init();
|
||||
if (uico_st->qt_data)
|
||||
uico_st->flags |= UICO_ST_FLAG_QT_IS_INITED;
|
||||
}
|
||||
ui_companion_wimp_init(uico_st);
|
||||
#endif
|
||||
uico_st->drv = (ui_companion_driver_t*)ui_companion_drivers[0];
|
||||
|
||||
@@ -179,10 +312,11 @@ void ui_companion_driver_notify_refresh(void)
|
||||
if (ui->notify_refresh)
|
||||
ui->notify_refresh(uico_st->data);
|
||||
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
if (config_get_ptr()->bools.desktop_menu_enable)
|
||||
if (ui_companion_qt.notify_refresh && (uico_st->flags & UICO_ST_FLAG_QT_IS_INITED))
|
||||
ui_companion_qt.notify_refresh(uico_st->qt_data);
|
||||
if ( (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
&& uico_st->wimp && uico_st->wimp->notify_refresh)
|
||||
uico_st->wimp->notify_refresh(uico_st->wimp_data);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -223,11 +357,12 @@ void ui_companion_driver_msg_queue_push(
|
||||
if (ui && ui->msg_queue_push)
|
||||
ui->msg_queue_push(uico_st->data, msg, priority, duration, flush);
|
||||
|
||||
#ifdef HAVE_QT
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
if (config_get_ptr()->bools.desktop_menu_enable)
|
||||
if (ui_companion_qt.msg_queue_push && (uico_st->flags & UICO_ST_FLAG_QT_IS_INITED))
|
||||
ui_companion_qt.msg_queue_push(
|
||||
uico_st->qt_data,
|
||||
if ( (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
&& uico_st->wimp && uico_st->wimp->msg_queue_push)
|
||||
uico_st->wimp->msg_queue_push(
|
||||
uico_st->wimp_data,
|
||||
msg, priority, duration, flush);
|
||||
#endif
|
||||
}
|
||||
@@ -250,14 +385,31 @@ const char *ui_companion_driver_get_ident(void)
|
||||
return "null";
|
||||
}
|
||||
|
||||
void ui_companion_driver_log_msg(const char *msg)
|
||||
bool ui_companion_driver_log_active(void)
|
||||
{
|
||||
#ifdef HAVE_QT
|
||||
uico_driver_state_t *uico_st= &uico_driver_st;
|
||||
bool window_is_active = uico_st->qt_data && (uico_st->flags & UICO_ST_FLAG_QT_IS_INITED)
|
||||
&& ui_companion_qt.is_active(uico_st->qt_data);
|
||||
if (config_get_ptr()->bools.desktop_menu_enable)
|
||||
if (window_is_active)
|
||||
ui_companion_qt.log_msg(uico_st->qt_data, msg);
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
const ui_companion_driver_t *ui = uico_st->wimp;
|
||||
settings_t *settings = config_get_ptr();
|
||||
if (!settings || !settings->bools.desktop_menu_enable)
|
||||
return false;
|
||||
return ui && ui->log_msg && uico_st->wimp_data
|
||||
&& (uico_st->flags & UICO_ST_FLAG_WIMP_IS_INITED)
|
||||
&& ui->is_active && ui->is_active(uico_st->wimp_data);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ui_companion_driver_log_msg(const char *msg)
|
||||
{
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
/* The caller has just asked ui_companion_driver_log_active(); do not
|
||||
* repeat the setting lookup and the is_active() round trip per line.
|
||||
* Only the pointers are re-checked, in case the companion was torn
|
||||
* down between the two calls. */
|
||||
uico_driver_state_t *uico_st = &uico_driver_st;
|
||||
if (uico_st->wimp && uico_st->wimp->log_msg && uico_st->wimp_data)
|
||||
uico_st->wimp->log_msg(uico_st->wimp_data, msg);
|
||||
#endif
|
||||
}
|
||||
@@ -30,6 +30,17 @@
|
||||
|
||||
#include "../command.h"
|
||||
|
||||
/* A desktop ("WIMP") companion backend is available in this build.
|
||||
* The Qt companion, the native Win32 companion (any desktop Windows
|
||||
* target) and the native Cocoa companion (macOS) all count. This
|
||||
* condition must be kept in sync with the guards in
|
||||
* settings/settings_def_desktop_menu.h, which cannot include this
|
||||
* header. */
|
||||
#if defined(HAVE_QT) || defined(HAVE_COCOA) || \
|
||||
(defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__))
|
||||
#define HAVE_COMPANION_WIMP 1
|
||||
#endif
|
||||
|
||||
RETRO_BEGIN_DECLS
|
||||
|
||||
enum ui_msg_window_buttons
|
||||
@@ -59,7 +70,7 @@ enum ui_msg_window_type
|
||||
|
||||
enum uico_driver_state_flags
|
||||
{
|
||||
UICO_ST_FLAG_QT_IS_INITED = (1 << 0),
|
||||
UICO_ST_FLAG_WIMP_IS_INITED = (1 << 0),
|
||||
UICO_ST_FLAG_IS_ON_FOREGROUND = (1 << 1)
|
||||
};
|
||||
|
||||
@@ -124,6 +135,11 @@ typedef struct ui_companion_driver
|
||||
void *(*init)(void);
|
||||
void (*deinit)(void *data);
|
||||
void (*toggle)(void *data, bool force);
|
||||
/* Per-frame hook for desktop companion drivers that do not own the
|
||||
* platform event pump (the native Win32 / Cocoa companions). Called
|
||||
* once per runloop iteration while the driver is initialised. Must
|
||||
* be bounded and never block. */
|
||||
void (*iterate)(void *data);
|
||||
void (*event_command)(void *data, enum event_command action);
|
||||
void (*notify_refresh)(void *data);
|
||||
void (*msg_queue_push)(void *data, const char *msg, unsigned priority, unsigned duration, bool flush);
|
||||
@@ -143,10 +159,18 @@ typedef struct ui_companion_driver
|
||||
|
||||
typedef struct
|
||||
{
|
||||
/* Platform driver: OS glue (message pump, message boxes, file
|
||||
* browser, window handling). Always present; selected by platform. */
|
||||
const ui_companion_driver_t *drv;
|
||||
void *data;
|
||||
#ifdef HAVE_QT
|
||||
void *qt_data;
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
/* Desktop companion ("WIMP") driver: the playlist / content browser
|
||||
* window. Selected by the ui_companion_driver setting, enabled by
|
||||
* desktop_menu_enable. Layered on top of the platform driver: with
|
||||
* the Qt companion on Windows / macOS the platform driver keeps
|
||||
* doing exactly what it does today. */
|
||||
const ui_companion_driver_t *wimp;
|
||||
void *wimp_data;
|
||||
#endif
|
||||
uint8_t flags;
|
||||
} uico_driver_state_t;
|
||||
@@ -181,6 +205,13 @@ void ui_companion_driver_msg_queue_push(
|
||||
|
||||
void ui_companion_driver_deinit(void);
|
||||
|
||||
/* Tear down just the desktop companion window (safe to call more than
|
||||
* once). Called at quit before the drivers go away, so the window is
|
||||
* gone while its own thread's message pump is still running - otherwise
|
||||
* a native companion window outlives the pump and the process hangs
|
||||
* with an undestroyed window. */
|
||||
void ui_companion_driver_wimp_deinit(void);
|
||||
|
||||
void ui_companion_driver_toggle(
|
||||
bool desktop_menu_enable,
|
||||
bool ui_companion_toggle,
|
||||
@@ -188,10 +219,41 @@ void ui_companion_driver_toggle(
|
||||
|
||||
uico_driver_state_t *uico_state_get_ptr(void);
|
||||
|
||||
/* True when a desktop companion with a log view is open. verbosity.c
|
||||
* asks this before formatting a copy of a log line and only then calls
|
||||
* ui_companion_driver_log_msg(), which delivers without re-checking. */
|
||||
bool ui_companion_driver_log_active(void);
|
||||
|
||||
/* Per-frame hook for the desktop companion driver; call once per
|
||||
* runloop iteration from the platform's main loop. */
|
||||
void ui_companion_driver_wimp_iterate(void);
|
||||
|
||||
/* True when the active desktop companion's toolkit application has
|
||||
* asked the process to exit (Qt sets this when its last window closes
|
||||
* with quit-on-close; the native companions never do - they only hide).
|
||||
* The main loops treat it like runloop_iterate() returning -1. */
|
||||
bool ui_companion_driver_wimp_exiting(void);
|
||||
|
||||
/* Tell the active desktop companion's toolkit application to quit
|
||||
* (Qt: QApplication::quit). No-op for companions without one. Called by
|
||||
* the main loops right before they break out on shutdown. */
|
||||
void ui_companion_driver_wimp_quit(void);
|
||||
|
||||
/* Desktop companion driver selection (Settings -> Drivers ->
|
||||
* Companion UI). */
|
||||
const ui_companion_driver_t *ui_companion_wimp_find_driver(const char *ident);
|
||||
const char *ui_companion_wimp_find_ident(int idx);
|
||||
/* Space-separated list of available desktop companion driver idents. */
|
||||
const char *config_get_ui_companion_driver_options(void);
|
||||
const char *config_get_default_ui_companion(void);
|
||||
|
||||
extern ui_companion_driver_t ui_companion_cocoa;
|
||||
extern ui_companion_driver_t ui_companion_cocoatouch;
|
||||
extern ui_companion_driver_t ui_companion_qt;
|
||||
extern ui_companion_driver_t ui_companion_win32;
|
||||
/* Native desktop companions (shared core + native controls). */
|
||||
extern ui_companion_driver_t ui_companion_wimp_win32;
|
||||
extern ui_companion_driver_t ui_companion_wimp_cocoa;
|
||||
|
||||
extern ui_msg_window_t ui_msg_window_win32;
|
||||
|
||||
|
||||
+43
-8
@@ -79,10 +79,23 @@
|
||||
#include "verbosity.h"
|
||||
#include "file_path_special.h"
|
||||
|
||||
#ifdef HAVE_QT
|
||||
#ifdef RARCH_INTERNAL
|
||||
/* Defines HAVE_COMPANION_WIMP when a desktop companion (Qt, native
|
||||
* Win32, native Cocoa) is in the build; its log view mirrors the log. */
|
||||
#include "ui/ui_companion_driver.h"
|
||||
#endif
|
||||
|
||||
/* The companion copy of a log line is formatted from a copy of the
|
||||
* argument list so the platform sink below still sees an untouched one.
|
||||
* C89 has no va_copy; gcc/clang spell the same thing __va_copy. */
|
||||
#ifndef va_copy
|
||||
# ifdef __va_copy
|
||||
# define va_copy(dst, src) __va_copy(dst, src)
|
||||
# else
|
||||
# define va_copy(dst, src) ((dst) = (src))
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef RARCH_INTERNAL
|
||||
#include "config.def.h"
|
||||
#else
|
||||
@@ -277,7 +290,35 @@ void RARCH_LOG_V(const char *tag, const char *fmt, va_list ap)
|
||||
FILE *fp = main_verbosity_st.fp;
|
||||
const char *tag_v = tag ? tag : FILE_PATH_LOG_INFO;
|
||||
|
||||
#if defined(HAVE_QT) || defined(__WINRT__)
|
||||
#ifdef HAVE_COMPANION_WIMP
|
||||
/* Mirror the line into the desktop companion's log view, when one
|
||||
* is open. Orthogonal to the platform sink: it formats from a copy
|
||||
* of the arguments and the normal output below still happens. */
|
||||
if (ui_companion_driver_log_active())
|
||||
{
|
||||
char buffer[1024];
|
||||
int r;
|
||||
va_list ap_cp;
|
||||
va_copy(ap_cp, ap);
|
||||
buffer[0] = '\0';
|
||||
r = vsnprintf(buffer, sizeof(buffer), fmt, ap_cp);
|
||||
va_end(ap_cp);
|
||||
if (r < 0)
|
||||
{
|
||||
buffer[sizeof(buffer) - 1] = '\0';
|
||||
if (buffer[0] != '\0')
|
||||
buffer[sizeof(buffer) - 2] = '\n';
|
||||
else
|
||||
{
|
||||
buffer[0] = '\n';
|
||||
buffer[1] = '\0';
|
||||
}
|
||||
}
|
||||
ui_companion_driver_log_msg(buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__WINRT__)
|
||||
{
|
||||
char buffer[1024];
|
||||
int r;
|
||||
@@ -299,13 +340,7 @@ void RARCH_LOG_V(const char *tag, const char *fmt, va_list ap)
|
||||
fprintf(fp, "%s %s", tag_v, buffer);
|
||||
fflush(fp);
|
||||
}
|
||||
|
||||
#if defined(HAVE_QT)
|
||||
ui_companion_driver_log_msg(buffer);
|
||||
#endif
|
||||
#if defined(__WINRT__)
|
||||
OutputDebugStringA(buffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
Reference in new issue
Block a user