Compare commits

..
Author SHA1 Message Date
LibretroAdminandf4mrfaux d530eb8f10 android: report a controller the OS has removed as disconnected
Android tells the activity when an input device goes away - a pad
switched off, out of range or unplugged - but nothing passed that on
to the input driver, and no input event marks a removal. A pad
therefore stayed connected for as long as the app ran:

- its port kept its device name, so an overlay hidden while a
  controller is connected never came back;
- whatever it was holding stayed held, since no release arrives from a
  device that is gone;
- no disconnect notification was shown, and pausing on disconnect
  could not happen.

RetroActivityCommon.onInputDeviceRemoved now hands the device id to
the driver. It is left in a small lock-free mailbox and taken out at
the next input poll, which clears the pad's key, axis and hat state
and reports the disconnect through input_autoconfigure_disconnect, as
the other joypad drivers do.

The pad's slot keeps its name and is marked removed instead of being
emptied, so the device gets its old port back when it returns, under
whatever id Android gives it; its return is reported as a connect. A
removed slot is matched before a live one of the same name, so of two
identical controllers the one that went away is not taken for the one
still connected.

A removal is read at the next poll, which can be long after it
happened if the app was in the background. If the id resolves again by
then, the device has come back under it and nothing is reported.

This is the device-removal part of PR #19077, reworked. That version
emptied the slot's name, which the driver uses to find a returning
device's port: every reconnect would have moved the pad to the next
free port.

Compiled with the NDK through the Android unity build. Not run on a
device.

Co-authored-by: f4mrfaux <[email protected]>
2026-10-02 06:05:48 +02:00
M00NR00STERandLibretroAdmin 4592b490bd Android: Samsung S-Pen stylus support (#19076)
* android: Add Samsung S-Pen stylus input support

Stylus input for the Android driver, squashed from PR #19076 and
rebased onto current master (three conflicts, all additive: the
hover-guard block in android_input.c, an include in runloop.c and the
stylus rows in tools/settings_table.reference).

- toolType-based stylus/finger classification with an input-source
  fallback
- hover, contact and click handled separately; hover can move the
  pointer without pressing
- phantom-touch suppression for Samsung pens (hover guard, proximity
  window)
- both barrel buttons read
- four Android-only settings: input_stylus_enable,
  input_stylus_require_contact_for_click,
  input_stylus_hover_moves_pointer, input_stylus_pressure_sensitivity
- android_mouse_calculate_deltas computed its fallback delta from the
  zeroed delta instead of the event position; fixed

Closes #15490
Closes #18194

* overlay: hide for a stylus without unloading the pack

The S-Pen support unloaded the on-screen overlay from the run loop
whenever a stylus became active and loaded it again two seconds after
the last pen event. A pen that leaves and returns does that over and
over, and each round is the pack's file and texture work, triggered by
input while a core runs.

It was a full unload because a soft-hidden overlay still took over
pointer input: with input_overlay_pointer_enable the overlay poll
tracks every touch as its own pointing device and sets
INP_FLAG_BLOCK_POINTER_INPUT, so input_state never reaches the driver
and the pen's native state (hover included) is lost.

Add INPUT_OVERLAY_STYLUS_HIDDEN and make it complete. While it is set
the overlay:
- is not drawn (alpha 0, as the gamepad soft-hide does);
- reads no touches, so the poll releases whatever a finger was holding
  the way lifting it would, and no hitbox or pointing device sees the
  pen;
- does not answer mouse, lightgun or pointer queries and does not
  block pointer input.

The run loop sets and clears the flag each frame on the current
overlay, the way the gamepad soft-hide is driven. Nothing is unloaded
or reloaded.

* android: report a pen as one pointer, its barrel button as the right mouse button

The S-Pen support gave pointer indices a meaning of its own: index 0
was the cursor, index 1 the tip pressing and index 2 the barrel
button, with the pointer count raised to 2 or 3 to match. Only a core
written for that contract can read it. Everywhere else an index is a
separate touch, so a pressing pen looked like two fingers and a press
with the barrel button like three - to cores that count pointers, to
the on-screen overlay, and to this driver's own lightgun mapping,
where two pointers are turbo and three are reload. With the barrel
button held but the tip not pressing, index 1 was stale and still
inside the count.

Map the pen onto the devices the driver already reports instead:

- position is pointer 0, and moves without a press while hovering
  (input_stylus_hover_moves_pointer);
- a press is pointer 0 pressed with a count of 1: the tip pressing
  past the pressure threshold, or, with
  input_stylus_require_contact_for_click off, the barrel button,
  touching or hovering;
- the barrel button is the right mouse button.

This also puts two settings to work. The contact-for-click option and
the pressure threshold only fed a latch nothing read; they now decide
the press, as their menu text says.

The pen no longer switches the driver into mouse mode
(mouse_activated). That flag is never cleared, and once set the
lightgun position is read from the mouse coordinates, which a pen does
not update, so lightgun aim went stale after the first pen contact.
The mouse_x_viewport/mouse_y_viewport values it wrote alongside are
not read anywhere.

A cancelled pen gesture now releases the press like a lift does;
before it fell through to the touchscreen path and left the pen's
contact latched.

Compiled with the NDK through the Android unity build. Not run on a
device: it needs a pass on S-Pen hardware before merging.

* android: let a mouse's hover events reach the mouse path again

The S-Pen support drops every hover event before the source is looked
at, so hover cannot be taken for a touch. A mouse moved with no button
held reports its motion as hover events too, and they were dropped
with the rest: the pointer only moved while a button was down.

Drop hover events after the mouse branch instead. Mouse motion is
handled as before the stylus work, and hover from any other source is
still kept out of the touchscreen path.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-10-02 05:48:02 +02:00
libretroadmin 11dc8ba30d apple: report the refresh rate of the screen the window is on
The Apple display server answers get_window_refresh_rate from the screen
the RetroArch window is on: on macOS the current mode of that display,
or, on a built-in panel whose mode carries no rate, the screen's own
maximumFramesPerSecond; on iOS and tvOS that screen's
maximumFramesPerSecond, which follows an iPad onto an external display
and a TV through a mode change. With no window yet the frontend falls
back to get_refresh_rate as before.

The reading is taken again whenever the window changes screen or a
screen changes mode: macOS posts NSWindowDidChangeScreenNotification
and NSApplicationDidChangeScreenParametersNotification, iOS and tvOS
the UIScreen mode, connect and disconnect notifications, and an
observer the display server installs at init passes each on as
video_driver_window_output_changed().
2026-10-02 05:42:40 +02:00
LibretroAdmin ae24f6b50d android: let the system keyboard's Backspace reach its text field
With "Use System Keyboard" on, the text being typed lives in a
Java-side EditText and the menu line is a copy of it: every change to
the field sends the whole text across. A soft keyboard commits most
text straight into the field, but sends some keys as key events
instead - Backspace when nothing is being composed (so always in
something like an IP address), digits on the AOSP keyboard.

RetroActivity is a NativeActivity, so those key events come out of the
native input queue first, and the views only see one that native code
finishes unhandled. android_input_poll_input_default finished every
key event handled, so a Backspace never reached the field. Its text
kept the characters, and the next character typed sent them back:
"192.168.17888", Backspace twice, "." gave "192.168.17888.".

While a system keyboard session is open, a key event the soft keyboard
sent - AKEY_EVENT_FLAG_SOFT_KEYBOARD or the virtual keyboard's device
id - is now finished unhandled and kept out of the native key state
and line editor, so the framework forwards it to the field. BACK stays
native: the field takes it ahead of the IME to cancel, and one left
unhandled would fall through to the activity's own back handling.
Hardware keyboards and gamepads are untouched.

The decision sits in input/drivers/android_kbd_route.h, free of NDK
types, so samples/input/android_kbd_keys can build it on a host. That
harness runs the reported sequence through the queue-then-views
pipeline and asserts the old behaviour is caught; it is wired into
the samples/input workflow.
2026-10-02 05:30:33 +02:00
libretroadmin 74935dc166 menu: Menu Frame Rate replaces Throttle Menu Framerate
Menu Frame Rate sets the rate the menu runs at while content is loaded,
in every sync mode: 'Display Rate', the default, runs it at the refresh
rate of the monitor the window is on, and 'Content Rate' at the
content's frame rate. At 'Display Rate' the silence the menu feeds is
written without blocking and measured out by the clock, so audio no
longer holds the menu to the content's rate; a core left running behind
the menu keeps its own clock, running when its next frame is due while
the menu re-presents its last frame in between; and behind the threaded
video wrapper without the display pacing hold, the timer holds the menu
to the display's period. A configuration with Sync to Exact Content
Framerate and Throttle Menu Framerate on keeps the content rate it had,
and the old key is dropped on save.

Display servers gain get_window_refresh_rate, the refresh rate of the
output the window is on, for Win32, X11 and SDL, and Wayland reports it
from surface enter and leave; the frontend keeps the reading until the
window moves, its output changes mode or the menu opens. The pace
decision, the menu pacing table, the threaded video harness and the X11
live sample cover both rates.
2026-10-02 05:29:34 +02:00
LibretroAdmin 6e4d60d553 settings: remove the menu_show_help setting
'Show Help' (menu_show_help) gated a main menu Help entry that no
longer exists; nothing read the setting, and the previous commits
took it out of the Views list and removed the Help list wiring.
Drop the def row, its default, the settings_t field and the label
string, and regenerate tools/settings_table.reference (the key is
the only change). An old retroarch.cfg line for it is ignored.

The generated intl tables still name the toggle's VALUE and SUBLABEL
ids, so those two stay in msg_hash.h as retired rows until the next
Crowdin fetch regenerates the tables without them; a follow-up
deletes the two rows then. help_list stays: its "Help" string is the
title of the per-setting help popup.
2026-10-02 05:19:13 +02:00
LibretroAdmin f705c273a1 menu: stop loading the unused Help icon textures
With the Help list wiring gone, the only remaining use of the Help
icon in XMB, Ozone and MaterialUI was loading it: every menu init
still read and uploaded menu_help.png / help.png for an entry that
is never drawn. Drop the texture slots and their file names.
2026-10-02 05:05:25 +02:00
LibretroAdmin 6cc0db0476 menu: remove the dead Help list wiring
The main menu stopped adding the Help entry long ago, and nothing
builds items for DISPLAYLIST_HELP_SCREEN_LIST, yet the plumbing for
it stayed: the display list type, its deferred push, the ok/title/
sublabel table rows, and the XMB/Ozone/MaterialUI icon cases. The
"Show Help" toggle under Menu > Views was also still listed even
though nothing reads menu_show_help, so it did nothing.

Remove the wiring and take the toggle out of the Views list. The
help_list and menu_show_help setting rows and their strings stay for
now: the generated intl tables reference their message ids, so
dropping them has to follow a Crowdin sync. DISPLAYLIST_HELP (the
per-setting help popup) is unrelated and untouched.
2026-10-02 05:04:05 +02:00
Vegard Bieker Matthey 89f63106e8 menu_displaylist: display releasemonth as a month name
In Information > Database Entry, Release Date Month showed the raw
number ("1"). Show the month name ("January") instead, through twelve
new translatable MENU_ENUM_LABEL_VALUE_RDB_ENTRY_RELEASE_MONTH_*
strings; languages without a translation yet fall back to English as
usual.

A month outside 1-12 in the database keeps the old behaviour and
shows the number, rather than an unresolved label.

Reworked from #19114: squashed, trailing-whitespace churn dropped so
it applies to current master, invalid-month fallback added, helper
returns enum msg_hash_enums from a static const table.
2026-10-02 04:38:34 +02:00
LibretroAdmin 925a185fe5 webOS: start Load Content on /media/internal
With no start directory set, Load Content on webOS opens on nothing
useful. Default the menu content directory to /media/internal, the
TV's shared storage, when it exists. Only a fresh config picks this
up; an existing retroarch.cfg keeps its saved rgui_browser_directory,
and nothing else about the config location changes.
2026-10-02 04:19:53 +02:00
LibretroAdmin 496c56dc92 menu: stop Parent Directory short of a root the drive list omits
The previous commit stopped offering "/" in the webOS drive list, but
Parent Directory could still walk up into it. Rather than special-case
webOS in the file browser, let the frontend say whether its drive list
offers the root: a new optional root_in_drive_list hook on
frontend_ctx_driver_t (NULL means it does), wired for the unix
frontend to frontend_unix_root_in_drive_list().

filebrowser_parse() then hides Parent Directory when going up would
land on the root and the root is not offered. Back returns to the
drive list as before. Rooted users can still open "/" deliberately
when the platform offers it; on webOS and Play Store Android, where
it is not offered, the browser no longer wanders into it.

Every other frontend leaves the hook NULL and is unchanged.
2026-10-02 04:19:50 +02:00
LibretroAdmin 0a531cdf60 webOS: do not offer the filesystem root in the drive list
Under jailer the root is not user storage, and opening it from Load
Content has been reported to freeze the file browser (PR #19203).
The webOS drive list already lists the real storage locations
(/media/internal, /tmp/usb and, in developer mode,
/media/developer/temp); stop appending "/" after them.

The root decision moves into frontend_unix_root_in_drive_list(),
which also carries the existing Play Store Android rule, so the next
commit can expose it to the file browser.
2026-10-02 04:19:47 +02:00
Acts1631 b396f73399 menu: bound playlist sublabel core name before appending
Playlist core names are copied from .lpl entries without a length
limit. strlcpy returns the source length on truncation, so using it
as the next offset can place the runtime text write beyond the
sublabel buffer. A crafted playlist with runtime data can corrupt
memory when its entry is displayed.

Stop appending when the prefix or core name exhausts the buffer.
2026-10-02 02:35:09 +02:00
libretroadmin 791657a40a CI: run the autoconfig suite under ASan + UBSan and TSan as well
The suite drives the whole frontend - runloop, task queue, autodetect - and fails on any sanitizer report, so it gains a sanitized lane for each, and a ThreadSanitizer warning now fails it as an error does.
2026-10-02 02:11:04 +02:00
libretroadmin 22571cbe42 task_queue: hand the fast-cores preference to the worker atomically
The preference is stored with release ordering on the main thread and loaded with acquire ordering by the worker as it starts, so a worker started while the setting is applied reads it without a data race.
2026-10-02 02:11:00 +02:00
libretroadmin 4bac9a1a01 runloop: count no core runtime without a frame rate
A core that reports no frame rate, such as the dummy core, adds nothing to the core runtime counter, and neither does one whose rate is so small that a frame would outlast a day; the frame time is no longer derived from a division by zero and a conversion of infinity. Every other rate is counted exactly as before.
2026-10-02 02:10:56 +02:00
LibretroAdmin 4c8630ba7f rmodtracker: slide IT panning the IT way
IT's Pxy shared the XM handler: right by x and left by y per tick, in
the engine's 0-255 units, and no fine slides. IT slides the other way
- Px0 left, P0y right - in its 0-64 units (four engine steps each),
and PxF / PFy are single fine steps on the row. A song panning with
Pxy had its channels drift the wrong way at a quarter of the speed:
its left/right balance was a median 2.8 dB off OpenMPT, now 0.1 dB.
2026-10-02 01:55:14 +02:00
LibretroAdmin 276918e82d rmodtracker: pan IT notes from their instrument, sample and envelope
Three ways IT notes landed in the wrong place in the stereo field,
compared with OpenMPT and Impulse Tracker:

- Pan envelope nodes are signed (-32 left .. +32 right, 0 centre), but
  the loader read them unbiased like volume nodes, so 0 meant hard
  left and negative nodes clipped to it. Songs with pan envelopes
  played lopsided and 2-2.7 dB loud.
- An instrument's default pan was ignored, and a sample's default pan
  overwrote the channel pan for good. IT gives each new note its own
  pan - instrument default if set, else the channel's, the sample's
  over either, then pitch-pan separation - and leaves the channel pan
  alone until a pan effect sets both.
- Pitch-pan separation was not applied.

On the 40-file corpus the worst stereo case, where instruments carry
default pans, goes from a median 4.5 dB left/right balance difference
to under 1 dB, and the three pan-envelope songs from +2.0/+1.6/+1.1 dB
to within 0.2 dB.
2026-10-02 01:55:10 +02:00
LibretroAdmin accf23a2d2 rmodtracker: play IT notes the way OpenMPT and Impulse Tracker do
Three note-level divergences from OpenMPT, found rendering 40 IT files
from modarchive against libopenmpt:

- A tone portamento on a channel with nothing sounding plays as an
  ordinary note in IT; rmodtracker kept it a slide with no source, so
  the note never sounded. An Impulse Tracker 2.15 song that opens every
  part this way was silent for its first 36 s.
- Exx, Fxx and Gxx share one memory in IT (Gxx its own with the
  compatible-Gxx header flag); rmodtracker kept three, XM-style. The
  volume column's Gx now uses IT's speed table and that memory, and its
  Ex/Fx pitch slides, which the loader dropped, are played.
- The instrument keyboard's note byte was ignored, so instruments that
  map keys to other notes (drum kits, transposed instruments) sounded
  at the key pressed - several semitones off in one song. Notes now
  also reach IT's C-0, eleven semitones below the engine's lowest key.

On the 40 files the share of 50 ms windows more than 6 dB off goes
from 67.6% to 2.5% for the portamento song (spectral match 0.66 ->
0.93), and the two keyboard-mapping songs' spectral match from 0.47
to 0.77 and 0.59 to 0.91.
2026-10-02 01:55:05 +02:00
LibretroAdmin 67356404af rmodtracker: match OpenMPT's IT mix levels and pattern length
Files ModPlug Tracker wrote (or OpenMPT in its ModPlug mode) are played
by OpenMPT at ModPlug's mix level, which pre-attenuates by a table of
the channel count; rmodtracker played them at Impulse Tracker's level,
3.4 dB (4-5 channels) to 9.5 dB (26 channels) louder, so they clipped.
Detect them with OpenMPT's header rules (cwt/cmwt pairs such as
0x217/0x200 and 0x888, or an OpenMPT version with "OMPT" in the
reserved field), read the mix-level mode ("PMM.") and sample pre-amp
("SPA.") from OpenMPT's song extensions when present, and scale the
gain as OpenMPT does: x64 / table[channels / 2] for ModPlug's levels,
x2 for 1.17RC3, the 1.17RC1/RC2 and FT2 variants likewise. The gain is
capped at 255 to keep the volume products within 32 bits.

Also accept patterns up to 1024 rows: Impulse Tracker stops at 200 but
OpenMPT writes more, and a longer pattern was replaced by 64 empty
rows (one OpenMPT song played 3 s of 98).

Across 40 IT files from modarchive rendered against libopenmpt 0.7.3,
the eight ModPlug/OpenMPT-levelled ones go from +3.4..+9.5 dB (one
OpenMPT 1.17RC3 file -6 dB) to within 1 dB.
2026-10-02 01:55:01 +02:00
libretroadmin 83895aef5b autodetect: order each port's connects and disconnects by sequence
Every connect and disconnect queued for a port takes the next number in
that port's sequence, and only the operation holding the newest number
writes the port's state when it completes, so a multi-pass connect scan
can no longer overwrite a disconnect or a different device queued after
it. A connect is skipped only when the same device's connect is already
the newest thing pending on the port, and a disconnect only when a
disconnect is; a device that is swapped or replugged while an earlier
scan is still running is configured rather than dropped.

The autoconf suite gains a phase that runs the connect/disconnect cycle
in several instances at once, so every step is queued before the first
scan as it is on a busy CI runner, and checks that the device connected
last is the one configured.
2026-10-02 01:36:23 +02:00
LibretroAdmin b990b4cfae win32: use retro_atomic for the deferred menu-command mailbox
The mailbox added for #19665 spelled its slots as volatile LONG with
InterlockedCompareExchange/InterlockedExchange. Use retro_atomic_int_t
with retro_atomic_cas_int/retro_atomic_exchange_int instead, like the
rest of the tree: same lock-free behaviour on Windows, and the slots
are real atomics to the compiler and to TSan rather than volatile.
2026-10-02 01:22:47 +02:00
LibretroAdmin 045c2d52b5 win32: run menu-bar commands on the main thread (#19665)
With threaded video the RetroArch window is created on the video
thread, so the menu bar's WM_COMMAND is handled there. Window Scale
calls CMD_EVENT_REINIT, whose driver teardown sends CMD_FREE to the
video thread and joins it - from the video thread itself. It waits
for itself forever, the window stops pumping, and the process has to
be killed from Task Manager.

win32_menu_loop() now parks the command when it is not on the main
thread, in a small lock-free mailbox, and the main thread's pump
(ui_application_win32_process_events, which the run loop calls every
iteration) runs it. That is the same thread the in-menu Windowed
Scale setting already runs on. The video thread also pumps through
that function, so draining is gated on task_is_on_main_thread().
Non-threaded video is unchanged: the window is on the main thread and
commands run immediately as before.

This covers every menu-bar entry, not just Window Scale: the others
(fullscreen toggle, save/load state, disc control, core/content load)
were also running run-loop work on the video thread.
2026-10-02 01:20:55 +02:00
LibretroAdmin 1b8917df23 rmodtracker: honour IT's linear slides flag
The IT loader never set linear_periods, so every IT file slid with
Amiga periods, where a slide's pitch change depends on the note. With
header flag 3 set (the default in Impulse Tracker, and set in all four
test files) Exx, Fxx and Gxx move xx * 4 sixty-fourths of a semitone a
tick on any note, which is exactly the engine's XM linear-period
arithmetic. Select it from the flag.

With the two pitch fixes before it, the pitch offset to it2play goes
from about 3 cents to 0, and over the three patches the share of 50 ms
windows more than 6 dB off it2play goes 0.3% -> 0.0% for Deus Ex - The
Illuminati, 1.0% -> 0.1% for Landing (mostly the pitch envelope fix)
and 0.8% -> 0.9% for through_the_infinite (OpenMPT: 0.5% -> 0.4%).
2026-10-02 00:52:16 +02:00
LibretroAdmin 2a3cb1edad rmodtracker: keep linear-period pitch on the reference rate
The linear-period frequency multiplied by c2_rate >> 4, dropping the
rate's low four bits (8363 Hz became 8352), and the IT loader
truncated each sample's C-5 speed to 1/128 of a semitone; together
every linear-slide note played about 3 cents flat of Impulse Tracker,
OpenMPT and FastTracker 2's 8363 * 2^((4608 - period) / 768).

Add the dropped low bits back as a second 32-bit product, and round
the IT tuning to the nearest 1/64 semitone, the step the period
arithmetic keeps. Across a spread of C-5 speeds and octaves the error
goes from -2.3..-4.0 cents to -1.3..+0.7. This also corrects XM, whose
linear periods use the same formula.
2026-10-02 00:52:13 +02:00
LibretroAdmin 709391c03b rmodtracker: apply IT pitch envelopes as an offset, not a slide
Impulse Tracker resets each voice's frequency every tick and applies
the pitch envelope on top: an envelope value v puts the note v/2
semitones away from its pitch (v * 32 / 768 octaves). rmodtracker
instead multiplied the period by it every tick, so any non-zero value
kept sliding the note further away for as long as it held - a flat
+4 envelope swept the note up by two semitones a tick rather than
holding it two semitones up.

Keep the offset per channel and apply it in the frequency calculation,
for linear and Amiga periods. Landing.it, whose instruments 6 and 17
carry pitch envelopes, goes from 1.0% to 0.1% of 50 ms windows more
than 6 dB off it2play.
2026-10-02 00:52:09 +02:00
LibretroAdmin c554c21e5f samples/menu/dirwalk: pair bench rounds with blocking walks
The bench gate timed the blocking reference once, before the task
queue existed, and the deferred walk much later; drift between those
two moments on a shared CI runner failed the gate on unchanged code
(24.0ms vs 17.1ms, ratio 1.40 against a 1.35 limit).

Time a blocking walk immediately before each frozen deferred round
and gate on the best per-pair ratio over five pairs: a real
regression is slow in every pair, noise only has to miss one.  Raise
the factor to 1.5, which still rejects what the gate exists to catch
(a second enumeration or a quadratic, 2x and up).  The paced rounds
stay at three on the real clock.
2026-10-02 00:51:44 +02:00
LibretroAdmin 70a4c0d142 rmodtracker: apply IT S70-S76 past-note actions and NNA overrides
S7x was ignored for IT. Apply the note-related half as Impulse Tracker
does:

- S70, S71 and S72 cut, release or fade the notes this channel has
  moved to the background (fade approximated as a release, as for the
  new-note action);
- S73-S76 override the playing note's new-note action (cut, continue,
  off, fade) until the next note, which a portamento does not count
  as.

S77-S7C (switching the envelopes per note) are still ignored. The
three test files without S7x render byte-identical.
2026-10-02 00:38:21 +02:00
LibretroAdmin f87d06d2f0 rmodtracker: give IT volume-column slides their shared memory
Impulse Tracker's four volume-column volume slides (fine up, fine
down, up, down) share one memory per channel, and a slide of 0 recalls
it. rmodtracker treated 0 literally, so a run of B0 after B1 - a fade
written as one fine step down and then repeats - did nothing.
through_the_infinite.it fades six channels that way from 147 s to 158 s
and played them up to 14 dB loud there.

Keep the memory for IT; XM's volume column has none and is unchanged.
Against it2play the share of 50 ms windows more than 6 dB off drops
from 2.4% to 0.8% (OpenMPT: 1.8% to 0.5%); the other three test files
render byte-identical.
2026-10-02 00:38:17 +02:00
LibretroAdmin c5600b329a rmodtracker: load Impulse Tracker 1.x instruments
Files whose compatible-with version is below 2.00 store instruments in
the IT 1.x layout, but the loader always read the 2.x one: NNA, DCT,
DCA and fadeout came from the wrong bytes, the global volume was read
from where 1.x keeps its fadeout, and the volume envelope from where
1.x keeps an unused 200-byte table, so the envelope was garbage or
silent. Such songs played up to 27 dB quiet and largely wrong.

Read the 1.x layout when the header's cmwt is below 0x200, as Impulse
Tracker and it2play do: NNA and DCT at 0x1A/0x1B, fadeout at 0x18 in
half the 2.x units, the volume envelope as up to 25 tick/value byte
pairs at 0x1F8, and no DCA, instrument global volume, filter, pan or
pitch envelope. 2.x files are unaffected.

Tested on landing.it and through_the_infinite.it rewritten into the
1.x instrument layout, against it2play rendering the same files: the
share of 50 ms windows more than 6 dB off drops from 41.8% to 0.1% and
from 28.5% to 2.6% (the latter's original renders at 2.4%).
2026-10-02 00:27:50 +02:00
LibretroAdmin 2a1b412a3f netplay: drop core packets from non-playing clients instead of NAKing
The server answered NETPLAY_CMD_NETPACKET from a client that is not
playing with a NAK, which closes the connection. A client reaches that
state without misbehaving: it calls the core's netpacket start() at the
end of the handshake while still a spectator and only then asks to
play, so an older client whose core sends from start() or in the frames
before its promotion lands, or a client set to start as a spectator,
was disconnected.

Read the packet off the stream as before, then drop it without passing
it to the core or relaying it, so spectators still cannot reach core
packet parsers but stay connected. Logged at debug level, since a
spectator's core may send every frame.
2026-10-01 23:58:56 +02:00
Acts1631 e8c6c72cdb netplay: restrict core packets to playing clients
The server accepts NETPLAY_CMD_NETPACKET from connected spectators,
including clients authenticated with a spectator-only password. It
forwards their payloads to the core or other players, exposing core
packet parsers to unauthorized input.

Reject packets from non-playing clients before dispatch and prevent
spectator clients from sending core packets. Leave client reception
unchanged because cores can send during player admission before the
mode change reaches the client.
2026-10-01 23:38:43 +02:00
LibretroAdmin a5e5555bba vfs_smb: build against both libsmb2 share-enumeration APIs
libsmb2 reworked libsmb2-share-enum.h in place upstream (48e2461,
2026-10-01): the srvsvc_* structures, SHARE_INFO_1 and
SRVSVC_SHARE_TYPE_* became a flat struct smb2_share_enum_reply,
SMB2_SHARE_INFO_1 and SMB2_SHARE_TYPE_*. The Linux libsmb2 CI lane
builds libsmb2 from upstream master, so smb_enum_shares stopped
compiling there.

Map the names once, keyed on SMB2_SHARE_TYPE_DISKTREE, which only the
new header defines; the async call and the level-1 netname/type
fields are the same in both. Builds against the headers before and
after that commit, and with HAVE_RETROSMB, which does not use this
path.
2026-10-01 23:28:01 +02:00
libretroadmin c734bc66a6 tasks: a refused blocking task is freed and reported, never left pending
The task queue refuses a blocking task while another runs, and four
pushers ignored the refusal. content_load_state() had already marked the
load pending, so a load pushed while a save state, a screenshot or a
Wi-Fi or Bluetooth task ran left the flag set for the rest of the
session: closing content waited on it forever, and a movie recording
never started. It now clears the flag, frees the task and returns false,
which every caller already handles. The undo save state, the Wi-Fi and
the Bluetooth pushers leaked the refused task and reported success; they
free it and return false.

samples/tasks/save_state checks that a load pushed while a save runs is
refused and not left pending - both fail without this change - with
LeakSan covering what the refusal frees.
2026-10-01 22:41:22 +02:00
LibretroAdmin 76b0a01740 rmodtracker: play IT at Impulse Tracker's level
The IT loader scaled the mix-volume byte by 4/3 to sit on libxmp's IT
levels, which run about 2.5 dB over Impulse Tracker itself. Against
it2play and OpenMPT every IT file came out 2.2-3.8 dB hot, and loud
songs clipped: Deus Ex - The Illuminati hard-clipped 7065 samples
(OpenMPT: 430), Jazz Jackrabbit 2's Streetwize 1941 (44).

Map the mix volume straight onto the gain, honouring its full 0-128
range (128 was masked to 0 and played at the default). Four test
files now sit within 0.7 dB of it2play and OpenMPT, and clip about as
often as OpenMPT does (398 and 37 samples).
2026-10-01 22:40:45 +02:00
LibretroAdmin 7e91634b64 rmodtracker: clip the IT filter where Impulse Tracker does
IT2 runs its resonant filter on the raw sample, scaled to 15 bits, and
clamps each output to 16 bits before the voice volume applies, so a
resonant peak clips at twice the sample's full scale. rmodtracker
filters after the volume, with headroom that never clips, so high-Q
sweeps through a loud sample came out several dB over IT2 and OpenMPT:
through_the_infinite.it's sweeps peaked about 4 dB hot.

Clip both filter paths at twice the side's gain, which is where a
full-scale sample lands in the buffer. Against an it2play render the
share of 50 ms windows more than 6 dB off drops from 3.7% to 2.5%
(OpenMPT: 4.8% to 1.9%); files without filters are unchanged.

The float path also kept its memory in the integer fields scaled by
32768, which overflows once a clipped value passes 65535 buffer
units; give it float fields of its own.
2026-10-01 22:40:41 +02:00
libretroadmin 07fd591e49 win32 companion: hand Windows its text as UTF-16
The Win32 companion went through the ANSI entry points with the core's
UTF-8 strings, so on NT every name outside ASCII was read in the local
code page: CJK, Korean or Thai file names in the browser and playlists,
and translated labels in menus and titles, came out as mojibake, and a
path the code page cannot hold could not be opened, picked or dropped.
Its windows, controls, menus, list rows and columns, tabs, status text,
file and folder pickers, dropped files, icons and message boxes now go
through the W entry points, converting at the edge - the virtual list
filling the control's own buffer row by row - and Windows does the font
fallback. A Win9x build (LEGACY_WIN32) keeps the ANSI entry points and
converts to and from the local code page instead of passing UTF-8
through.

The Win32 companion test reads rows as UTF-16 and browses a directory
holding a file named in CJK, Hangul and Thai, which now shows as itself;
the test script runs Wine in a UTF-8 locale so the file can be made.
2026-10-01 22:27:10 +02:00
libretroadmin 7a55ed4b27 font_driver: map font files; archives extract by rename
Where there is mmap(), a font file is mapped rather than read whole: a
face's pages are read in as its glyphs are drawn and stay the file's,
so the 4.4 MB CJK fallback face costs what is drawn from it - 2 MB with
900 distinct characters on screen, less in a menu that shows a few.
Win32, the consoles and builds without mmap() read the file as before.
font_driver.c includes config.h first, as vfs/vfs.h - which
streams/file_stream.h pulls in - decides from HAVE_MMAP whether the VFS
maps at all.

A mapping does not survive its file being truncated, so archive
extraction now writes each member beside its file and renames it into
place, falling back to writing in place where a rename cannot be done.
An Update Assets run while fonts are in use leaves them reading the
file they mapped, and an extraction stopped part way leaves the old
file whole rather than cut short.
2026-10-01 22:27:05 +02:00
LibretroAdmin 68d4e69d95 rmodtracker: avoid left-shifting negative filter values
The integer resonant filter took its error term as acc - ( y << 14 )
and stored its clamped output back as y << 2, both undefined for a
negative y (UBSan flags them on any filtered IT). Multiply instead;
the result is the same on every target.
2026-10-01 22:25:48 +02:00
LibretroAdmin a212646e9b rmodtracker: raise the background voice pool to 64
New-note actions move continuing notes to a shared pool of background
voices, stealing the quietest when it is full. Songs with continue or
fade actions on many channels exhaust 32 voices: through_the_infinite.it
keeps around 14 voices per channel ringing, and with 32 the stolen
voices left parts of each phrase several dB short of Impulse Tracker.
At 64 its output stops changing (128 and 256 give identical renders),
for 32 more struct channel copies per replay.
2026-10-01 22:25:44 +02:00
LibretroAdmin 7ed6ce397b rmodtracker: fade IT instruments at IT's rate
IT subtracts the instrument fadeout from 1024 each tick (it2play and
OpenMPT both load the IT 2.x value as is). The engine fades from
32768, so the step is fadeout * 32, not * 64; instruments faded out
twice as fast as they should.
2026-10-01 22:25:41 +02:00
LibretroAdmin 7deaa8ebf9 rmodtracker: use IT's 0-128 global volume for Vxx and Wxy
The IT loader halved the global volume into the S3M/XM 0-64 range and
shared their effect handlers, so Vxx clamped at 64 (V40, half volume
in IT, played at full) and Wxy slid by XM rules: WF1, a fine slide
down by 1, became a slide up of 14 per tick. through_the_infinite.it
ends with WF1 / W00 to fade out and instead stayed at full volume.

Keep IT's global volume in 0-128 and scale it accordingly, let Vxx
set up to 128, and slide as IT does: W0y and Wx0 on the non-row ticks,
WFy and WxF once on the row.
2026-10-01 22:25:35 +02:00
LibretroAdmin 4091d61f47 rmodtracker: no IT fadeout while a sustain loop holds the envelope
IT starts the fadeout when a volume envelope reaches its last node,
but a sustain loop keeps it from getting there while the key is held,
even when the sustain point is the last node. rmodtracker only
excluded normal loops, so notes on such instruments faded out while
still held; channels 9-11 of through_the_infinite.it were silent for
most of the song. Exclude sustained envelopes too.
2026-10-01 22:25:32 +02:00
LibretroAdmin de44ef0f20 rmodtracker: restart IT instruments on notes without an instrument
Impulse Tracker starts a new voice for every note, whether or not the
row has an instrument number: the envelopes restart, the key goes down
again, fadeout resets and the instrument's initial filter applies.
rmodtracker did this only when an instrument was given, as XM does,
so on a run of bare notes the volume envelope ran out once and the
channel faded to silence for good. Channel 2 of through_the_infinite.it
was silent from 16 s to 56 s this way.

Do the reset for IT on any non-porta note without an instrument; the
volume still carries over, as in IT.
2026-10-01 22:25:28 +02:00
LibretroAdmin 7d03e1d428 rmodtracker: apply IT Zxx and SFx filter macros
The IT loader ignored Zxx, so songs that sweep the resonant filter
with it played unfiltered: through_the_infinite.it uses Zxx 1861
times and came out 12-17 dB too bright between 3 and 8 kHz.

Reduce the MIDI macros to the filter controls the engine has. Without
an embedded configuration Impulse Tracker's defaults apply: SF0
makes Zxx 00-7F set the cutoff, and Z80-Z8F set the resonance in
steps of 8. An embedded configuration (special bit 3, after the
optional edit history) is parsed for the same forms: "F0F000" for
the cutoff and "F0F001" for the resonance, followed by z or a fixed
hex value; other macros are ignored. SFx picks the macro per channel.
2026-10-01 22:25:23 +02:00
LibretroAdmin f57fa00c69 CI: samples/input: install mingw-w64 and Wine in the bounded deps step
The winraw_abs_pos_test step installed gcc-mingw-w64-x86-64 and
wine64 with a bare apt-get install: no timeout, no download-first
retry, and -qq hiding all progress. A stalled mirror left the step
silent until the job's 20-minute timeout cancelled the run.

Add both packages to the "Install dependencies" list so they go
through the same bounded, retried download as the rest, and drop the
in-step install. Also kill wineserver on exit so a lingering server
cannot keep the step's output open.
2026-10-01 22:04:35 +02:00
libretroadmin 83e76b961d ctr, gx2, rsx: let the glyph atlas grow; the 3DS uploads new glyphs
The 3DS swizzled the atlas into its font texture once, at init, and
never again, so every glyph rasterized afterwards - all fallback-font
text, and whatever took a cell another glyph had held - drew from cells
the texture never received. Each message now swizzles the atlas's dirty
rectangle into the texture first. The 3DS, Wii U and PS3 drivers then
let the atlas grow - to 1024, 1024 and 2048 px - making texture memory
of its new size, and for the Wii U a texture-size block too, and keeping
what it replaces until the font is freed: a frame in flight may still
draw from it, and freeing it there means waiting for the GPU, which
none of them does for this. The atlas growing at most twice keeps that
to a third more memory. The 3DS's never-enabled VRAM variant of the
texture goes.
2026-10-01 21:42:56 +02:00
LibretroAdmin b63c4cb097 rmodtracker: limit XM sample sizes to the data left in the file
After the INT_MAX check, an XM sample header could still claim close
to 2 GB of data. A small file could make the loader allocate ~4 GB of
silence, and once that succeeded, offset + count in data_sam_s8 (and
offset + count * 2 in data_sam_s16le) overflowed, so their end-of-data
clamp failed and they read past the file buffer; the running offset
sum after each sample could overflow as well.

Clamp the sample size to the bytes remaining after the sample data
offset, counting the ADPCM table and two samples per byte for ModPlug
ADPCM. Samples that fit in the file are unchanged; a truncated file
now gets a sample ending at the end of the file rather than one padded
with silence.
2026-10-01 21:38:19 +02:00
LibretroAdmin e5bf4c893e rmodtracker: reject negative IT sample loop starts
The IT loader bounds the frame count, but reads the loop start into a
signed int and only checks that the loop end lies after it and within
the sample. A loop start of 0x80000000 or above becomes negative and
passes, leaving the loop sentinel copy and the mixer reading before
the sample buffer. Drop the loop when its start is negative.
2026-10-01 21:38:15 +02:00
LibretroAdmin 3650a9e270 rmodtracker: bound S3M sample length and loop fields
The S3M loader read the 32-bit sample length, loop start and loop end
into signed ints. Large values turned negative and passed the loop
checks: a length of 0xFFFFFFFF with a looped sample left loop_start at
-1, so after calloc(0) the loop sentinel was written before the sample
buffer, and a negative loop start with a small loop end left the loop
indexing before the buffer at load and during playback.

Keep the fields unsigned until they are bounded, limit the length to
0x1000000 frames as the IT loader does, clamp the loop end to the
length and drop the loop when it is empty. Well-formed files load the
same loop as before.
2026-10-01 21:38:12 +02:00
libretroadmin 060a5a297e gdi, switch, ps2, vita: let the glyph atlas grow
GDI already made its atlas DIB again when the atlas's size changed, and
the Switch blits glyphs from the atlas in memory; both offer it room to
grow and ask for it before drawing. The PS2 makes its EE copy of the
atlas again at the new size and has the GS sent it again, as it does
for the menu texture, with the atlas kept to 512 px for the GS's 4 MB.
The Vita makes a texture at the new size and keeps the one it replaced
until the font is freed - a frame in flight may still draw from it, and
freeing a texture there means waiting for the GPU - which the atlas
growing at most twice keeps to a third more texture memory.
2026-10-01 21:35:31 +02:00
Acts1631 7608a704c1 rmodtracker: bound XM sample loop indexes
XM loop fields are read into signed ints. An oversized loop length can
become negative and pass the end-only check, causing a heap write
before the sample buffer when the loop sentinel is copied.

Keep the fields unsigned until they are checked against the sample
length. Reject sample sizes that do not fit the decoder's signed
counts, and guard ping-pong loop doubling against integer overflow.
2026-10-01 21:38:18 +02:00
LibretroAdmin 1c08bce61f connmanctl: quote the tether AP name and password
connmanctl_tether_toggle pasted the AP name and password from the
user's tether config unquoted into a popen() command, so a space,
quote or ';' in either one split the arguments or ran as shell syntax.
Pass each as a single-quoted word, with embedded quotes escaped, and
refuse to run a command that would not fit rather than truncate it.

Turning tethering off no longer appends the two empty arguments the
callers pass; the command was already "connmanctl tether wifi off"
once the shell dropped them. Also stop on a failed popen().
2026-10-01 21:27:15 +02:00
LibretroAdmin a2b9374396 connmanctl: check and free the service name scratch buffer
connmanctl_get_connected_servicename used its malloc'd line buffer
without checking for NULL, and only freed it when an online service
was found; when the loop ran out of services the buffer leaked. Bail
out on a failed allocation and free the buffer on the not-found path.
2026-10-01 21:27:11 +02:00
LibretroAdmin 0fc546c074 connmanctl: share the final return in the service ID check
connmanctl_valid_wifi_service_id returned false from inside its loop
on a disallowed character and again after it when no terminator was
found. Break out of the loop instead so both cases use the single
return false below it. No change in behaviour.
2026-10-01 21:27:08 +02:00
libretroadmin db681b076d d3d12, d3d8, d3d9, metal, software drivers: let the glyph atlas grow
D3D12 makes its font texture again when the atlas grows and retires the
old one behind the queue fence - the value the frame being recorded
signals at its end - releasing it and its descriptor once the fence has
passed, so nothing waits on the GPU and no command list in flight loses
what it names. D3D8 and both D3D9 drivers already remade their texture
when the atlas's size changed; they offer the device's largest texture
and ask for the atlas before drawing. Metal makes its managed buffer and
the texture on it again, a command buffer in flight keeping the old pair
alive. The SDL1, Xv, Exynos, OMAP and OGA drivers blit glyphs straight
from the atlas in memory and only offer it room to grow.
2026-10-01 21:23:44 +02:00
libretroadmin 388637b6c4 gl1, sdl2, sdl3, d3d10, d3d11: let the glyph atlas grow
The GL1, SDL2, SDL3, D3D10 and D3D11 drivers offer their fonts' glyph
atlas room to grow, up to the largest texture each can make - from
GL_MAX_TEXTURE_SIZE, the renderer's reported limit, D3D10's, and
D3D11's for its feature level - and ask for the atlas before laying a
message out, remaking their texture when it has grown: GL1 respecifies
its texture, the SDL drivers already remade theirs at the atlas's size,
and D3D10 and D3D11 release and make it again, the device keeping the
old one alive for draws already issued. On GL1 and SDL2, thirty titles
of thirty CJK characters each in XMB's History now draw every glyph
where SDL2 drew the stand-in past the 256th.
2026-10-01 21:12:21 +02:00
Acts1631 444f987961 connmanctl: validate Wi-Fi service IDs before shell use
ConnMan prints discovered SSIDs directly in its service list. An SSID
with embedded newlines can forge a row whose final token is treated as
a service ID. Connect or disconnect then interpolates that token into
a popen() command, allowing a nearby AP to execute shell commands.

Accept only bounded Wi-Fi service IDs made of ASCII letters, digits,
and underscores. Check IDs again at command sites, including names
found in the settings directory.
2026-10-01 21:15:05 +02:00
Anthony SchemelandClaude Opus 5.5 9a64f3fa3c webdav: a fresh client nonce for each digest challenge
webdav_create_digest_auth set cnonce to the constant "1a2b3c4f" for
every login. The cnonce is the client's own input to the digest
response; with it fixed, a hostile server (or anyone in the middle of
plain HTTP) chooses every other input and can test password guesses
against precomputed tables.

webdav_create_cnonce fills a 33-byte buffer in the state with 16 bytes
from crypto_random_bytes as hex, or, without HAVE_CRYPTO or when no
entropy source answers, an MD5 of the microsecond clock and a counter.
The buffer replaces the const char pointer, so nothing is allocated.

Tested against a local server that checks the digest response: before,
every login sent cnonce=1a2b3c4f; after, each run sends a new 32-digit
cnonce, and the server accepts the response both times and the sync
completes. Builds clean with make; webdav.c compiles as gnu89 with and
without HAVE_CRYPTO.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 21:04:31 +02:00
LibretroAdmin b181eec6cc gl: drop benchmark figures from the gl_glsl_set_vbo comment
The comment added with the GL_STREAM_DRAW change quoted a single
radeonsi measurement. Those figures will go stale and are already
recorded in that commit's message; keep only the reason for the hint.
2026-10-01 20:57:59 +02:00
Anthony SchemelandClaude Opus 5.5 f79bf6ec22 gl: upload per-draw coords with GL_STREAM_DRAW
gl_glsl_set_vbo re-specifies the GLSL backend's vertex buffer with
glBufferData on every draw whose coords changed - each font flush and
menu quad, many times a frame - but declared it GL_STATIC_DRAW, the hint
for data written once and drawn many times. GL_STREAM_DRAW is the hint
for this pattern; the glcore driver's scratch VBOs already use it.

Measured: Ozone's idle menu on the gl driver spends 2.7% of its time in
this upload (perf, radeonsi). A windowless EGL bench on the same GPU
(RX 6600, radeonsi) of the font path's pattern, twelve ~6 KB uploads
and draws a frame: STATIC 278 us/frame wall, 99 us CPU; STREAM 228-236
us wall, 84-87 us CPU, in two runs. GL_DYNAMIC_DRAW measured like
STATIC. The hint changes no results, only placement.

Checked: the Ozone menu runs on the gl driver (Xvfb, llvmpipe) with no
GL errors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 21:00:17 +02:00
Anthony SchemelandClaude Opus 5.5 22c9d145f3 task_screenshot: initialise new_screenshot_dir before it is tested (#19667)
screenshot_dump only writes new_screenshot_dir when screenshot_dir is
set, then tests !*new_screenshot_dir to fall back to the content
directory. With the screenshot directory left at its default (empty),
that test reads uninitialised stack, so the screenshot can go to a
directory named from leftover bytes, which path_mkdir then creates.

Repro: null drivers, screenshot_directory = "", content loaded
(fceumm, an NES zip), --max-frames=30 --max-frames-ss. Under valgrind,
master reports 55 errors in 35 contexts, all in screenshot_dump
(conditional jumps, mkdir/openat on uninitialised bytes), and the PNG
was written to a new directory named with garbage bytes. With this
change: 0 errors, and the PNG lands beside the content.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-01 20:55:32 +02:00
libretroadmin 595257a21c font_driver, gl, glcore, vulkan: grow the glyph atlas a frame needs
A font's glyph atlas grows when a frame needs more glyphs than it has
cells for: at the next frame, before anything is drawn from it, its
columns and rows double - to 1024 cells, then 4096 - with every cell
staying where it was, so no glyph's offsets change. A video driver
offers it by setting the atlas's max_width and max_height to the largest
texture it can make and asking for the atlas before laying a message
out; it then remakes its texture at the new size. The GL, GL core and
Vulkan drivers do - GL respecifying its texture, GL core making a new
immutable one, Vulkan making a new staging and sampled pair and parking
the old one for the frames still drawing from it. Thirty titles of
thirty CJK characters each, in XMB's History, draw every glyph on all
three; a driver that has not opted in keeps the atlas at its first size
and draws the stand-in past it, as before.

The font_lifecycle frame test covers the growth: deferred to the next
frame, every earlier glyph in place, then the whole frame drawn, and a
second growth past 1024 cells.
2026-10-01 20:50:43 +02:00
libretroadmin f0abc20a4f samples/font_lifecycle: declare the read length at the top of its block
The stub's real-file branch declares its length before the read log
is taken under its lock, so the block opens with its declarations as
the coding guidelines ask.
2026-10-01 20:41:17 +02:00
libretroadmin c7221cae58 font_driver: keep a frame's glyph cells for that frame
A glyph cache hands out its least recently used cell for a new glyph,
and once a frame drew more distinct glyphs than the cache holds - 256 a
font - it handed out cells that glyphs earlier in the same frame were
already queued to draw from, so those drew as other characters, and
every glyph past the 256th was rasterized again and the atlas uploaded
again every frame. A cell looked up in the current frame is now left
alone until the next one begins: past what the cache holds, the rest of
a frame's glyphs go undrawn - their stand-in is drawn - and nothing is
rasterized again until the text changes. 400 distinct CJK glyphs a frame
cost 27 us instead of 1.4 ms, and nothing is drawn from a cell that has
changed under it.

font_driver_frame_begin() marks a frame: the video thread marks it
before the driver's frame when video is threaded, video_driver_frame()
otherwise. Until the first mark a cache is plain LRU, as it was.
samples/gfx/font_lifecycle gains a test of it.
2026-10-01 20:27:10 +02:00
libretroadmin e771f60819 samples/font_lifecycle: keep the read log under a lock
The fallback tests read fonts on threads of their own, and the stub's
record of which files were read was written by two threads at once in
the fallback test, which TSan reports. It is kept under a mutex, and the
chain test asks the stub whether a file was read instead of walking the
record itself.
2026-10-01 20:27:06 +02:00
libretroadmin 5829a0b7f3 font_driver: draw Ethiopic from its own fallback font
Ethiopic, its supplement and its extensions are drawn from
ethiopic-fallback-font.ttf in the assets' pkg/ when the font has no
glyph for them; the general fallback has none. The syllabary needs no
shaping, so the face renders it correctly a glyph at a time. Assets
without the file leave Ethiopic as undrawn as it was. The chain test
checks Ethiopic is looked for in its own face.
2026-10-01 19:40:45 +02:00
libretroadmin b29de508db font_driver: a second fallback face where the first lacks a range
A codepoint the font has no glyph for is drawn from the fallback face
its block picks, and where that face has none either, from a second one
known to carry the range with the fallback fonts the assets ship:
Hangul jamo, circled numbers and vertical and small form variants from
the CJK face, circled Hangul from the Korean one, Yijing hexagrams from
the general one. That is close to 400 more glyphs drawn - the circled
numbers in Japanese titles among them - and a codepoint no face is
known to carry still has no other file read for it.

samples/gfx/font_lifecycle gains a chain test, DejaVu Sans Mono standing
in for the general face and DejaVu Sans for the CJK one, run under ASan
and TSan in Linux-samples-gfx.yml.
2026-10-01 19:17:37 +02:00
libretroadmin c454fce0b0 gfx: resolve a font draw's parameters in one place
font_driver_resolve_params() fills in what a video driver's font draws
a message with: the caller's font_params, or for the on-screen message
the position and colour from the settings, left aligned, full screen,
opaque, under the usual 2 px shadow at 0.3 of the colour - with the
colour both as floats and as bytes, and the caller's float colour passed
through. Every video driver that drew from font_params resolves them
there instead of copying them field by field and reading the settings
itself; what is each driver's own stays with it - its packed colour
layout, the "alpha 0 means opaque" fix-up where it had one, and the 3DS
and PS2 drivers' own shadow for the message - and every driver ends up
with the same values as before. SDL2's and the Switch's on-screen
message take its position, and SDL2 its colour and shadow, from the
settings as the others do, where both drew it at the screen's corner.

The compile matrix's clang font layout lanes reject a declaration after
a statement, apart from the PS2 and Wii U drivers' C99 toolchains.
2026-10-01 19:07:20 +02:00
Anthony SchemelandClaude Opus 5.5 6ab85b6f5f core_option_manager: size the categories array by category count (#19668)
core_option_manager_new allocated opt->cats with calloc(_len, ...),
_len being the number of options, then filled it by category count.
A core declaring more categories than options writes past the end of
the heap block.

Repro: a minimal test core passing RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2
with four categories and one option, null drivers, under valgrind:
master reports 35 errors in 13 contexts, all invalid writes from
core_option_manager_new; with this change, 0. Every read of opt->cats
is bounded by cats_size. fceumm, which uses categories, loads with no
valgrind errors.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-01 18:41:43 +02:00
libretroadmin de9096b0cf vulkan: declare before the font layout's unused read
The line hook read its unused glyph count ahead of a declaration, which
the C89 lane rejects; the read follows the declarations.
2026-10-01 18:21:35 +02:00
libretroadmin 6bf58823c6 gfx: one text width measurement for every video driver
font_renderer_get_message_width() in font_driver.c measures text through
a font's glyph cache - the UTF-8 walk, the lookup with its '?' stand-in
and the summed advance - and every video driver's get_message_width
returns it, where each walked the text itself. Its loop is
gfx/font_measure.h, a statement block in the manner of
gfx/font_layout.h; Vulkan and Metal, which upload an atlas cell as each
glyph is looked up, include it with that upload as their hook, so no
loop pays a call per glyph. The widths and uploads are the same as
before; the 3DS driver no longer dereferences its font before checking
it.
2026-10-01 08:36:26 +02:00
libretroadmin 1923afac28 gdi: draw text through the shared font layout
The GDI driver's font lays text out through gfx/font_layout.h, which
leaves no video driver with a text layout of its own. The layout block
splits the message into lines and hands each one over, through
FONT_LAYOUT_SKIP, to be drawn as its shadow run and then its text run,
so the text still lands on the shadow; a run walks its glyphs through
the block too. Untinted text is AlphaBlended a glyph at a time as
before, and tinted text baked into the scratch DIB and blitted per line;
a tinted line that is also right or centred is measured once rather than
twice. The Windows 95 path, which hands the line to TextOut, is as it
was, and the calls into GDI and the scratch contents are the same as
before.

The layout block documents fl_m, the current line's first byte, as
readable from its hooks, which is how a driver drawing a line in passes
of its own gets the line. The compile matrix's clang font layout lanes
cover GDI.
2026-10-01 08:23:04 +02:00
libretroadmin b14d4982fb sdl, xvideo, exynos, omap, oga: decode on-screen messages as UTF-8
The software drivers that blit their own on-screen messages walked them
a byte at a time, so a multi-byte character came out as one Latin-1
glyph per byte - nothing a fallback font could help with - and a line
break was looked up as a glyph. They lay messages out through
gfx/font_layout.h instead: UTF-8, a codepoint the font cannot draw shown
as '?', and each line one line height below the last. Each blit is
unchanged; a glyph left out at the screen's edge now advances the pen,
where the next one used to be drawn in its place.

The OGA driver hung on a codepoint its font had no glyph for, never
stepping past it, and wrote past its message surface once the text
filled it. It wraps a glyph before it would cross the right edge, draws
nothing once a row would cross the bottom, and reports no more height
than the surface has.
2026-10-01 08:12:38 +02:00
libretroadmin f2e8be023f metal: draw text through the shared font layout, and compile it in CI
The Metal driver's font lays text out through gfx/font_layout.h, one
method where it had a per-line method, a wrapper splitting the lines and
a width method called per line for right and centred text. It keeps its
pixel-snapped line origin, its six-vertex quads in the vertex range and
its atlas-cell update after every glyph lookup, the measuring pass's and
the '?' stand-in's included. The message length is passed down rather
than rescanned, and the vertices and cell updates are the same as before.

tools/platform_stubs/metal stands in for the parts of the macOS SDK the
driver uses - Foundation, AppKit, Metal, MetalKit, QuartzCore, simd - in
the shapes the SDK gives them, and the compile matrix compiles
gfx/drivers/metal.m as Objective-C against them with clang. A selector
the stand-ins do not declare is an error there, so a new Metal call
needs its declaration added with it.
2026-10-01 08:01:00 +02:00
libretroadmin 569b0598c8 sdl3: draw text through the shared font layout
The SDL3 driver's font lays text out through gfx/font_layout.h, as SDL2's
does: one function where it had a per-line function and a wrapper
splitting the lines, keeping its top-left pixel origin, floating-point
pen, atlas upload at the start of each line and SDL_RenderGeometry
batches of up to 64 glyphs on the stack. A right or centred line is
measured in the layout's own pass rather than by its width function,
with the '?' stand-in known before it, and the vertices and draws are
the same as before.

The compile matrix's clang font layout lanes cover it where the SDL3
headers are installed.
2026-10-01 07:51:16 +02:00
libretroadmin 0b323245b8 d3d9: draw text through the shared font layout
Both D3D9 drivers' fonts lay text out through gfx/font_layout.h. Each
looks a glyph up once for a line's shadow run and its foreground run,
drawn in that order as before, where it used to measure and walk the
line again for each run, and the draw both runs repeated is a function
of its own. Empty lines are passed over before any lookup, as they were.
The vertices and draws are the same as before.

The compile matrix's clang font layout lanes cover both.
2026-10-01 07:44:39 +02:00
libretroadmin f884160138 gfx: look nothing up for a line a driver will not draw
D3D10, D3D11, D3D12, the Wii U and the Switch leave out a line too long
for their buffer, or past their limit, and before drawing through the
shared font layout they never looked its glyphs up. Through the layout
they did, measuring and walking a line they then skipped - rasterizing
glyphs nobody would see and evicting ones that were in use. Each now
names the line in FONT_LAYOUT_SKIP, so it is passed over before any
lookup, as it was.
2026-10-01 07:39:11 +02:00
libretroadmin 3f6df7b067 font_driver: the fallback file table and its references only with the loader
The fallback fonts load on a worker that needs threads and a lock-free
compare-and-swap. Where the atomics backend has none (the PS2 one), the
loader was compiled out but its file table and reference slots were not,
and -Werror stopped the atomic-backend lane on them. FONT_FALLBACK_LOADS
is now decided before both and guards them with the loader.
2026-10-01 07:38:42 +02:00
libretroadmin 23a9d859d6 CI: the Vulkan font layout lane on the tree's own Vulkan headers
The lane compiled gfx/drivers/vulkan.c against the system's
<vulkan/vulkan.h>, which the runner does not have, and failed there. It
now takes the Khronos headers the tree carries in gfx/include, as the
real builds do, and runs on any runner.
2026-10-01 07:27:28 +02:00
libretroadmin 224aa0d30f ctr, rsx, d3d8: draw text through the shared font layout
The 3DS, PS3 and D3D8 drivers' fonts lay text out through
gfx/font_layout.h. The 3DS keeps its vertex cache, its capacity check
ahead of each line and its per-line draw, now a function of its own; the
PS3 keeps its integer line origin, six-vertex quads and 64-glyph chunked
draws. D3D8 looks each glyph up once for a line's shadow run and its
foreground run, drawn in that order as before, where it used to measure
and walk the line again for each; its scissor cull now runs before the
line is measured, so a line out of view is not looked up at all. All
three produce the same vertices and draws as before.

The layout block takes an optional FONT_LAYOUT_SKIP, for a driver that
can tell a line will not be drawn before looking any of it up. The
compile matrix's clang font layout lanes cover D3D8.
2026-10-01 07:25:15 +02:00
libretroadmin fdbcf76eb6 sdl2: look the '?' stand-in up before laying a line out
A right or centred line is measured before its first glyph is drawn, so
the glyph that stands in for a codepoint the font cannot draw has to be
known by then. It was looked up as each line started, after the
measuring pass: the first such line was measured without it, and the
text drawn off by the stand-in's advance. It is looked up once, ahead of
the layout.
2026-10-01 07:25:11 +02:00
libretroadmin e8260a749f ps2, switch, gxm, gx2: draw text through the shared font layout
The PS2, Switch, Vita and Wii U drivers' fonts lay text out through
gfx/font_layout.h, one function where each had a per-line function and a
wrapper splitting the lines. Each keeps its own line origin and the way
it draws a glyph: PS2 its per-line alpha state and gsKit sprites, the
Vita its atlas copy and tinted draws, the Wii U its vertex cache, its
truncating pen and its per-line draw and capacity check, the Switch its
software blit and its line-length limit. The text length is passed down
rather than rescanned.

The Switch blit counted rows with the line's own y origin, so every glyph
after the first on a line was drawn at the height the previous one ended
on; it has counters of its own. The compile matrix's clang font layout
lanes cover the four, and the Vita driver no longer leaves its shader
patcher's result set but unread.
2026-10-01 07:16:15 +02:00
libretroadmin 4b32cdafc5 gl1, sdl2: draw text through the shared font layout
The GL1 and SDL2 drivers' fonts lay text out through gfx/font_layout.h,
one function where each had a per-line function and a wrapper splitting
the lines: GL1 keeps its integer line origin, six-vertex quads and
64-glyph chunked draws; SDL2 keeps its top-left pixel origin, its
floating-point pen, its atlas upload at the start of each line and its
SDL_RenderGeometry batches. The text length is passed down rather than
rescanned, SDL2 measures a right or centred line in the layout's own pass
instead of calling its width function per line, and both draw the same
pixels as before.

The compile matrix's clang font layout lanes cover both.
2026-10-01 07:08:36 +02:00
libretroadmin 5d5e3ebf0d d3d10, d3d12: draw text through the shared font layout
D3D10 measures a line with a shadow or an alignment first, then writes
each glyph once as its shadow and its foreground sprite, straight to
their slots - the line's shadows ahead of its glyphs, as before - where
it used to measure and walk the line again for each of the two. D3D12
keeps its single interleaved pass and its retroactive shift of right and
centred lines; the line splitting, glyph lookup and '?' stand-in are the
shared block's. Both keep their own truncating pens and their rule for
lines too long for the buffer, and write the same sprites as before.

The compile matrix's clang font layout lanes cover both.
2026-10-01 07:00:36 +02:00
libretroadmin 1f7be4e209 gfx: no set-but-unused warnings from the shared font layout
A driver that draws through gfx/font_layout.h but has no use for one of
the values it reports - a line's glyph count for Vulkan and the GL
drivers, the width and the shared pen for D3D11, which keeps its own -
reads it as void in its macro, so GCC 15 and clang no longer warn that
the layout's counter is set but never used.

The compile matrix builds each driver drawing through the layout with
clang and the warning made an error.
2026-10-01 06:55:20 +02:00
libretroadmin 4950ba810d d3d11: draw text through the shared font layout
The layout block also reports each line's glyph count and length. D3D11
measures a line first when it has a shadow, so every sprite is written
straight to its slot - the line's shadows ahead of its glyphs, as before
- instead of emitting the glyphs, cloning them and swapping the two
runs; right and centred lines are still shifted once their advance is
known, with their shadows taken from the shifted glyphs. The sprites are
the same as before, byte for byte, and lines too long for the buffer
are still left out.
2026-10-01 06:49:34 +02:00
libretroadmin adc900bdbf gl3: build without slang again
The GL core driver's scRGB/HDR10 state lives outside the slang guard,
and what needs the slang pipelines is inside one: the context's HDR
framebuffer is taken up only with slang, the HDR10-to-SDR downconvert
and the end-of-frame scRGB encode are compiled only with it, and the
ribbon VBO helper only where its caller is. A build without slang - the
C89 lane is one - compiles again, and draws SDR as it always has.

The compile matrix checks gl3.c both with and without slang.
2026-10-01 06:39:41 +02:00
libretroadmin 7101c67f85 gl3: draw text through the shared font layout
The GL core driver's font lays text out through gfx/font_layout.h, as the
Vulkan and GL drivers do: the line splitting, glyph lookup, alignment
width and pen advance are the shared block's, and the driver keeps its
own line origin, its six-vertex glyph quads and its 64-glyph chunked
draws. The text length is passed down rather than rescanned, and the
output is the same pixels as before.
2026-10-01 06:28:28 +02:00
libretroadmin ed4731f410 unix: reap the narrator with waitpid instead of ignoring SIGCHLD
After the first narration, SIGCHLD was set to SIG_IGN for the whole
process, so every later wait on a child - nmcli's connect and
disconnect among them - failed with ECHILD and read as failure. The
narrator stays a direct child and is reaped with waitpid(WNOHANG):
is_narrator_running_unix() reaps it once it has exited, so a finished
narrator never reads as running and its pid is not kept past its exit,
and one sent SIGTERM that has not exited yet is kept to reap on a later
call. Nothing waits, and SIGCHLD keeps its default action. As found in
#19647, which reparented the narrator instead; a finished narrator then
read as running until whatever adopted it reaped it.
2026-10-01 06:26:05 +02:00
libretroadmin c3c7efa427 gfx: one text layout for the video drivers' fonts, Vulkan and GL first
gfx/font_layout.h is the text layout every video driver's font repeats:
splitting lines, walking the UTF-8, looking each codepoint up with '?'
standing in for one the font cannot draw, measuring a line for right and
centred alignment, and advancing the pen. It is a statement block that a
driver includes in its own render function, with macros for what is the
driver's own - where a line starts, how a glyph's quad is written in its
vertex format, and when its atlas is uploaded - so the loop compiles to
what the driver wrote out by hand, with no call or buffer per glyph.

The Vulkan and GL drivers draw through it, and produce the same pixels as
before. GL measuring a line for alignment now counts '?' for a codepoint
the font cannot draw, as drawing it does.
2026-10-01 06:06:40 +02:00
libretroadmin be1e188d09 CI: the CoreText rasterizer in an Apple shape; fallback fonts under TSan
The compile matrix checks gfx/drivers_font_renderer/coretext.c and the
font driver's Apple branch, macOS and iOS, against stand-ins for the
CoreFoundation, CoreGraphics and CoreText declarations they use, so a
change to the rasterizer interface is caught before the Apple jobs.

samples/gfx/font_lifecycle gains a fallback test: two fonts with no
glyph for a set of codepoints, on two threads, ask for the same fallback
face at once, with real stb and real rthreads behind font_driver.c. The
file has to be read once and every glyph has to arrive. It runs in the
ASan/UBSan check and again under TSan in Linux-samples-gfx.yml.
2026-10-01 05:48:23 +02:00
libretroadmin f66810d639 font_driver: one glyph cache for every rasterizer, with fallback faces
The glyph cache - the 16 x 16 cell atlas, the codepoint hash, least
recently used eviction and the dirty rectangle - lives in font_driver.c,
and stb, freetype and CoreText are rasterizers behind it: they open a
face on bytes font_driver.c read for them, say whether it has a glyph
for a codepoint, and draw that glyph into the cell they are handed. The
video drivers keep calling get_glyph and get_atlas as before; a hit
costs the same hash walk it did in each renderer's own copy.

A codepoint the font has no glyph for is drawn from a fallback face
picked by its Unicode block - the CJK, Korean, Thai and general fallback
fonts in the assets' pkg/ directory - so a Chinese, Korean or Thai file
name shows in an English menu instead of boxes. Each fallback file is
read the first time a codepoint needs it, on a thread of its own, and
shared by every font afterwards; until it arrives that codepoint draws
blank and is not cached. A font that never meets such a codepoint reads
nothing and keeps nothing more.
2026-10-01 05:36:31 +02:00
Acts1631andlibretroadmin 8e0f4ae0ac nmcli: avoid shell execution from nearby SSIDs (#19646)
* wifi: pass discovered SSIDs to nmcli without a shell

The nmcli scan path interpolated each nearby SSID into a system()
command. A nearby access point could execute commands as the
RetroArch user when the Wi-Fi network list was opened.

Query saved profiles and connect or disconnect using argument-based
nmcli calls, so SSIDs and passphrases remain literal arguments.

* wifi/nmcli: fill the nmcli argument vectors by assignment

C89 takes only constant expressions in an aggregate initializer, and the
argument vectors held the SSID and passphrase: the C89 lane rejected all
five. The vectors are now declared and filled in by assignment.

---------

Co-authored-by: libretroadmin <[email protected]>
2026-10-01 05:32:26 +02:00
LibretroAdmin c4ff3d3369 samples/tasks/http: link compat_strl.c into http_dns_lifetime_test
9b5f6ed768 ("net/tls: certificates for IP addresses; no server name
for them") made net_http_connect() copy the TLS host name with
strlcpy(). compat/strl.h maps strlcpy to strlcpy_retro__, which lives
in compat_strl.c, but Makefile.dns_lifetime never linked that file.
The non-SSL build drops the call, so only the HAVE_SSL build failed
to link:

  undefined reference to `strlcpy_retro__'

Add compat_strl.c to the link line. The test passes again with
SSL=1, SSL=0 and SANITIZER=address,undefined.
2026-10-01 05:18:36 +02:00
Acts1631 f20adf39f8 bluetoothctl: validate addresses from device listings (#19651)
A nearby Bluetooth name can contain a newline that forges a line in
bluetoothctl devices output. The next status check or a connect action
used the forged line's second token in a popen() command, allowing
shell commands to run as the RetroArch user.

Accept only lines with a valid Bluetooth address, use that address
for status, connect, and remove commands, and cap the list to the
fixed status arrays.
2026-10-01 05:19:08 +02:00
libretroadmin 6d481880de menu, widgets: reload assets in place, releasing what was loaded
A menu driver's context_reset loads every texture into its slot without
looking at what the slot held, so calling it on a live menu leaks the
whole icon set already loaded. menu_driver_context_rebuild() runs
context_destroy first and then context_reset against the running video
driver - the pair the menu goes through when it survives a video driver
swap - and the in-place resets use it: the Dynamic Wallpaper, Show
Sublabels and XMB Entry Icons settings, Start on the wallpaper entry,
and Update Assets.

Update Assets also reloads the notification widgets with
gfx_widgets_reload_assets(): the message queue and volume icons are
reloaded under the widget state lock, asynchronously so nothing waits
on the video thread while the lock is held, and the next layout pass
rebuilds every widget font on the thread that drives the widgets,
retiring the old ones, so the fonts from the new assets replace the
built-in one the widgets fell back on.
2026-10-01 04:56:43 +02:00
LibretroAdmin e993371eb9 camera: the frame_work bit is the whole test; camera_allow drops it through a command
The camera poll in runloop_iterate() tested RUNLOOP_WORK_CAMERA and
then re-tested five more things every frame: settings->bools.
camera_allow, cb.caps, driver, driver->poll, data. The bit is meant to
mean "poll this every frame", so those conjuncts belong at the edge
that raises it, not on the frame path.

driver_camera_start() now raises the bit only when the start
succeeded, the driver can be polled, and the core registered caps
(data and driver are preconditions of reaching start at all). Stop
and teardown already drop it. The one condition that could change
underneath - the user flipping camera_allow off mid-session - gets
its own edge: the setting row's command trigger fires
CMD_EVENT_CAMERA_ALLOW_APPLY, which stops a running camera (and so
drops the bit). Allowing again does nothing by itself, as before: the
core has to call start() for the camera to come up.

A trigger rather than a change handler because the generated
settings glue (msg_hash, configuration.c, the string passes) only
defines the base S_BOOL variant for this row; other rows already pass
a CMD_EVENT_* through the same consumers, which discard it.

Two visible changes: a disallowed-while-running camera is now
actually stopped instead of merely no longer polled while still
capturing, and the frame path is one bit test. Frontend-overhead
harness passes.
2026-10-01 04:55:14 +02:00
libretroadmin 280094adb9 netplay: the password salt from the entropy source; hashes compared in constant time
The password challenge salt came from an LCG seeded with time(NULL):
anyone who saw one salt could predict every later one. It now comes from
crypto_random_bytes(), the platform entropy source on Windows, UWP,
POSIX and the 3DS, Vita and Switch; builds without the crypto library,
or a platform without a source, keep the LCG seeded from the
microsecond clock and the connection's address. The client's password
hash is compared with crypto_memeq_ct() rather than memcmp(), whose
early exit told how much of it matched. The salt travels in the same
field, so older clients are unaffected. As found in #19649.
2026-10-01 04:53:35 +02:00
Anthony Schemel 61b0a7eb28 config: re-apply tls_verify_mode on every settings load (#19648) 2026-10-01 04:43:43 +02:00
libretroadmin d66d06f85a crypto: NEON ChaCha20 on 32-bit ARM too; the crypto suite on 32-bit ARM in CI
The four-block NEON ChaCha20 ran on AArch64 only, though the Vita and
32-bit Android have NEON as well and are where ChaCha20-Poly1305 is now
negotiated first. It now runs wherever NEON is: ChaCha20-Poly1305 on
ARMv7 takes 14.7 instructions an octet, down from 32.

No job ran the crypto suite on 32-bit ARM, the consoles' architecture.
crypto-arm-check runs it under qemu-arm in their two shapes, ARMv7-A
with NEON as on the Vita and ARMv6K with VFP only as on the 3DS, where
every portable path is taken, and the libretro-common job runs it.
2026-10-01 04:20:48 +02:00
LibretroAdmin 9b5f6ed768 net/tls: certificates for IP addresses; no server name for them
A TLS connection to an IP address - https://[::1]:8443/ or
https://192.0.2.1/ - was treated like one to a host name, which is
wrong in three places:

- net_http handed the TLS layer the host as it appears in the URL, so
  an IPv6 literal kept its brackets ("[::1]"), which nothing can match.
  The brackets belong to the URL and the Host header only; the TLS
  layer now gets the bare address.
- The built-in client sent the address as server_name. RFC 6066
  allows host names only there; for an IP address the extension is now
  left out.
- x509_match_hostname() compared an address against dNSName entries
  and the commonName as text, so a name certificate whose CN or a DNS
  entry happened to read "10.0.0.1" vouched for that address, while a
  certificate carrying the address as a proper iPAddress entry never
  matched. An IP address now matches only iPAddress entries, byte for
  byte (RFC 9525 6.3); a host name matches as before.

x509_parse_ip() is the new, exact parser behind that: dotted IPv4
without leading zeros, IPv6 with "::" compression and a trailing dotted
IPv4, brackets and a zone id allowed.

On the way: the built-in client kept the caller's pointer for the host
name, and net_http passes one that dies with the transfer while the
connection lives on in the pool - where a TLS 1.3 session ticket
arriving later is filed under that name. It keeps its own copy now.

BearSSL and mbedTLS get the bare address too; their own IP handling is
theirs (BearSSL's minimal verifier has none, so such a connection fails
verification, as it did).

Tests: the crypto suite parses valid and malformed literals and matches
a SAN with DNS, IPv4, IPv6 and a DNS-spelled address, plus an IP-reading
commonName. The local openssl matrix connects to 127.0.0.1 against a
leaf with IP:127.0.0.1 (refused before this change) and one with
DNS:127.0.0.1 (now refused).
2026-10-01 04:18:09 +02:00
libretroadmin b5658e5a7f crypto, net/tls: ChaCha20 first without AES instructions; X25519 on 32-bit limbs
Where AES runs in software - the 3DS, the Vita, older phones - AES-GCM
costs about four times what ChaCha20-Poly1305 does (116 against 32
instructions an octet on 32-bit ARM), yet the client offered it first,
and servers that follow the client's order chose it. The client now
offers ChaCha20-Poly1305 first when crypto_aes_hw() reports no AES
instructions; the TLS matrix checks a build without them negotiates it
on 1.2 and 1.3, through ssl_socket_retro_suite().

X25519 on machines without a 128-bit integer ran on the generic
Montgomery arithmetic, 6.1 million instructions an operation on 32-bit
ARM. It now has arithmetic of its own mod 2^255 - 19 in ten 32-bit
limbs of 26 and 25 bits, every product one 32x32->64 multiply: 1.9
million on the Vita's ARMv7, 2.1 million on the 3DS's ARMv6.
2026-10-01 04:01:54 +02:00
LibretroAdmin cec500d0cb task_http: resume an interrupted download to a file
A download to a file - task_push_http_download_file(), which the core
updater and playlist thumbnails use - that lost its connection part way
through failed outright and deleted what it had. On a flaky link a
large core or asset bundle could fail at 90% every time.

It now picks up where it stopped, inside the same task:

- On the first response, a 200 that advertises "Accept-Ranges: bytes"
  and a strong ETag (or else Last-Modified) makes the download
  resumable. A weak ETag cannot validate a byte range.
- When the transfer then fails in transport mid-body - not an HTTP
  error, not a full disk, not a cancel - the task opens a new request
  for the same URL with "Range: bytes=N-" and that validator in
  If-Range, N being what is already on disk, and keeps writing to the
  same file.
- A 206 must continue exactly at N (Content-Range is checked); a 200
  means the server sent the whole resource again - it ignored the range,
  or If-Range found it changed - and the file starts over; anything
  else fails the download and removes the file, as before.
- At most three resumes, each only after progress.

A completed resumed download reports status 200 to the callback, since
the whole file was delivered (the thumbnail callback checks for exactly
200), and progress covers the whole file rather than the last slice.

net_http gains net_http_header(), a lookup of a response header while
the transfer is still running, which is what the sink needs to decide.

Two task_http fixes on the way: a failed download to a file set its
error string twice and leaked the first, and a task cancelled before
its connection became a transfer leaked the connection.

New samples/tasks/http/http_resume_test drives the real task_http.c,
net_http.c and task queue against a scripted server: resumed once,
resumed twice, resource changed (the new body alone, not spliced),
wrong Content-Range, no validator (no resume tried), and a connection
that drops every time (fails after three resumes). Six of its checks
fail on the previous code, where every one of these downloads failed.
2026-10-01 04:01:36 +02:00
LibretroAdmin beef1f13b9 net_http: share the header-text compactor; test the real one
The Emscripten backend gets response headers from the browser as one
text blob and turns it into net_http's header block itself. Its test
could not build task_http_emscripten.c (emcc only), so it carried a
copy of the parser - and the copy had already drifted: it still split
into a string_list, while the backend had moved to the block.

The conversion is now net_http_headers_compact() in net_http.c: header
text in, the block net_http_headers_take() returns out, in place, with
CR and trailing spaces trimmed, blank lines dropped and the final line
kept without its newline. The Emscripten backend calls it on the buffer
the browser filled; http_emscripten_headers_test links net_http.c and
calls it too, so the response side has no copy left to drift. The
request side stays a copy, being static to the emcc-only file, as the
test's header says.

http_emscripten_headers_test: the same response cases (splitting,
blank and final lines, lower-cased names, NULL and empty input) against
the real function, under ASan and UBSan.
2026-10-01 03:51:10 +02:00
LibretroAdmin 9aafb30bfb net_http: keep the body when more than Content-Length arrives
A response that ran past its Content-Length failed the transfer:
status -1, no body, although the whole body had arrived and was sitting
in the buffer. It can happen whenever the excess lands in the same read
as the body - with the header block, or with a sink, whose receive
window is not trimmed to what is left of the body.

The body is now complete at Content-Length. What follows it is garbage
or a response nobody asked for, so it is dropped together with the
connection - the same treatment as bytes after a response with no body.

net_http_semantics: "Content-Length: 2" followed by "okEXTRA-BYTES"
delivers "ok", with and without a sink, and the next request goes out
on a new connection. Both delivery checks fail on the previous code.
2026-10-01 03:51:06 +02:00
LibretroAdmin 65f42ddb41 net_http: drop a followed redirect's body as it arrives
A 3xx whose Location is going to be followed already kept its body
out of the caller's sink, but without a sink it was still read in
whole: the receive buffer grew to the advertised Content-Length - up to
the 256 MiB ceiling, on the server's say-so - only for net_http_redirect()
to throw it away.

Such a body is now handled the way a sink body is - drained after every
read, never sized to Content-Length - except that it goes nowhere. One
predicate, net_http_body_streams(), replaces the sink tests on the
body path. The redirect body is still read through, so the connection
stays usable for the redirected request.

net_http_semantics: a 302 with an 8 MiB body is followed with no
allocation reaching 1 MiB (it reached 8 MiB before), on the same
socket.
2026-10-01 03:51:03 +02:00
LibretroAdmin 347baf9fb9 net_http: send the request a slice per update instead of blocking
net_http_send_request() wrote the request with blocking sends - a
dozen for the head, then the whole body - before returning. net_http's
sockets are non-blocking, so once the body outgrew the socket buffer,
socket_send_all_blocking() spun on EAGAIN until the server had taken
all but the last buffer's worth. Measured against a server reading at
2 MB/s, a 16 MiB PUT spent 6057 ms in one net_http_update() call, at
100% CPU. In an unthreaded build that call is on the video thread; in a
threaded one it holds up every task queued behind it. Cloud sync
uploads of saves and states are exactly this request.

The request now goes out like the response comes in:

- The head is built once into one buffer, byte for byte what the old
  send sequence wrote: one send where there were a dozen.
- Each net_http_update() sends as much as the socket takes, at most
  NET_HTTP_SEND_BUDGET (256 KiB, the drain budget's twin) per call,
  then returns. Postdata goes straight from the caller's buffer; a
  streamed body is pulled a 64 KiB run at a time into one buffer kept
  for the transfer.
- A full socket marks the transfer blocked, so net_http_wait() waits
  for it to become writable - no spinning, no sleeping; stopping on the
  budget does not. Over TLS the last bytes are flushed out of the TLS
  layer before the request counts as sent.
- The stale-pool replay and a redirect start the send over, after the
  source rewind they already did.

Same 16 MiB upload: longest call 6057 ms -> under 1 ms, total time
unchanged (8.07 s), and CPU for the whole transfer 6.0 s -> 0.01 s with
a caller that waits between updates. Requests that fit in the socket
buffer go out in the first call as before.

cheevos_login_test: a 12 MiB PUT through net_http to a server that
stops reading for a second, plain and over TLS, must never spend
250 ms in one update; the longest is 4-8 ms. Plain and BearSSL spent
1020 and 1033 ms on the previous code. The request bytes the login
checks validate one for one are unchanged.
2026-10-01 03:31:27 +02:00
LibretroAdmin 1e44eb6774 net/ssl: make non-blocking send work on all three TLS backends
ssl_socket_send_all_nonblocking() had no callers, and none of the
three implementations was usable:

- mbedTLS took MBEDTLS_ERR_SSL_WANT_WRITE - an ordinary full socket -
  for an error, and on success reported the whole length as written
  whatever mbedtls_ssl_write() had actually taken.
- The built-in client sent the whole buffer blocking.
- BearSSL was close, but copied from a NULL buffer when the engine was
  full and could not say whether bytes it had taken were still waiting.

The contract is now written down in net_socket_ssl.h: take as much as
fits without blocking and return that (0 when the socket is full, -1
on error); after a 0, call again with the same buffer, at least as
long; once the last byte is taken, call the new
ssl_socket_flush_nonblocking() until it reports 1.

- mbedTLS remembers the length of a write that returned WANT_WRITE and
  repeats it, as mbedtls requires; a positive return already means the
  records are on the wire, so its flush has nothing to do.
- The built-in client seals one record from the bytes it takes and
  keeps whatever the socket did not accept in a per-connection buffer,
  sending it before sealing the next. A blocking send drains it first
  so records leave in sequence; close_notify is skipped behind a
  half-sent record rather than wait on a peer that stopped reading.
- BearSSL pushes its pending records before taking more, refuses a
  closed engine, and flushes in a loop while the socket keeps taking.

samples/network/cheevos_login gains a built-in TLS lane (SSL_BACKEND=
retro, trusting the test CA through ssl_socket_retro_set_trust_pem) and
a check that drives each backend's non-blocking send directly: a
12 MiB PUT to a mock /upload that stops reading for a second before
taking the body. Every call must return at once and the server must
receive every byte. mbedTLS fails it on the previous code.
2026-10-01 03:31:23 +02:00
libretroadmin 156de513c9 crypto: AArch64 AES-GCM eight blocks at a time; ChaCha20 on NEON
The AArch64 AES-GCM path still did as x86 did before it changed: CTR
four blocks at a time, round keys reloaded and byte-swapped every round,
each counter block rebuilt through a scalar swap, and GHASH reduced
after every block. CTR now keeps eight blocks in flight with the keys
loaded once, the counter stepped by one add in a lane-reversed vector,
and GHASH folds eight blocks per reduction with Karatsuba multiplies:
AES-128-GCM takes 40% fewer instructions, 3.4 per byte down from 5.7.

ChaCha20 ran scalar on AArch64, where it is what TLS chooses on cores
without the AES instructions. It now runs four blocks at once on NEON,
which every AArch64 core has: ChaCha20-Poly1305 takes 60% fewer
instructions, 9.6 per byte down from 24. Counts are executed AArch64
instructions under QEMU, start-up subtracted.
2026-10-01 03:30:32 +02:00
libretroadmin 118bb10b9a samples/tls_retro: link retro_eventcount into tls_http
net_http now waits on an eventcount, so tls_http, which links net_http
over the built-in TLS client, links rthreads/retro_eventcount.c as the
HTTP task samples do; without it the tool failed to link and the TLS
matrix stopped there.
2026-10-01 03:30:28 +02:00
LibretroAdmin 2302213409 net_http: parse bracketed IPv6 literals in URLs and redirects
An IPv6 address in a URL is bracketed because it contains colons
(RFC 3986 3.2.2): http://[::1]:8080/x. Both places that split host
from port took the first colon:

- net_http_connection_iterate()/_done() stopped the host at "[" and
  refused the rest as a bad port, so such a URL could not be requested
  at all;
- net_http_redirect() split the joined URL the same way, so a redirect
  to a literal failed (or, with digits in the right place, went to a
  junk host and port).

Both now treat a leading "[" as a literal running to its "]", with the
port colon only after it; an unterminated, empty or trailing-garbage
literal is refused. The domain keeps its brackets, which the Host
header needs ("Host: [::1]:8080"), and the resolver strips them before
getaddrinfo(), which wants the bare address. url_join needed nothing:
it never looks inside the authority.

Not covered: TLS to an IPv6 literal still passes the bracketed form
as the server name, as it passes any other host.

http_limits_test (net_http.c included whole, resolver stubbed): URL
split with and without a port and with a zone id, the three malformed
forms, the resolver's argument, and a redirect to http://[::1]:8443/v6.
All fail on the previous code.
2026-10-01 03:00:47 +02:00
LibretroAdmin 0324ba4909 net_http: a 1xx block's framing headers do not carry into the response
An interim response (100, 102, 103) is followed by the real status line
on the same connection, and its headers were cleared before reading it
- but not what they had set. bodytype, len and content_len survived:

- "Transfer-Encoding: chunked" on a 100 stayed T_CHUNK, and since
  Transfer-Encoding wins over Content-Length, the final response's
  "Content-Length: 2" was ignored and its body parsed as chunks.
- "Content-Length: 5" on a 103 stayed T_LEN 5, so a close-delimited
  final body was cut after five bytes and the connection pooled with
  the rest unread.

Interim blocks now reset the framing along with the headers. Costs
three stores per 1xx block; nothing on the body path.

Two new net_http_semantics checks, both failing before.
2026-10-01 03:00:43 +02:00
libretroadmin 7aa27bc5fe net/tls: records land in the caller's buffer; the end of the stream ends a download
A received record was decrypted in place, copied to a plaintext buffer,
and copied again into the caller's. The plaintext now stays where it was
decrypted, and when the caller's buffer holds the whole record - every
net_http read, which asks for 32 KiB or more - it is decrypted straight
into it: a download takes 31% fewer instructions. The AES-GCM key
schedule is built once per key rather than once per record.

The end of the stream came back as 0 bytes, which net_http takes for
'nothing yet': a body that runs until the server closes the connection
never finished, every byte received and the transfer polling forever.
It is now -1 with the error flag, as plain sockets and the library
backends report it. tls_http drives net_http over the built-in client in
the TLS matrix, four version and cipher pairs, checking the body by hash
and failing a transfer that never completes.
2026-10-01 02:57:20 +02:00
LibretroAdmin 3ef1a02a5d webdav: bound every scan in the Digest challenge parser
The WWW-Authenticate challenge is whatever the server sends with a
401, and the parser trusted its shape:

- realm, nonce and opaque took strchr(ptr, '"') + 1 - ptr as a length.
  With no closing quote strchr() returns NULL, the length is a huge
  value, and malloc() of it fails or the strlcpy() that follows writes
  through NULL.
- An unknown unquoted parameter was skipped with
  while (*ptr != ',' && *ptr != ','), which never checks for the end
  of the string, so one at the end of the line ("stale=FALSE") read on
  past it until it happened to meet a comma.
- qop and unknown quoted values had the same missing-quote hole.

Every value is now found with a bounded scan, and a challenge that
cannot be parsed is refused - which the caller already handles as
"cannot re-authenticate". Quoted values go through one helper that
copies them out and replaces any earlier copy, so a repeated parameter
no longer leaks the first; the HA1 hash is recomputed rather than
leaked when realm repeats. qop is read as a list whose members may be
spaced ("auth-int, auth" was refused), and a quoted algorithm is
accepted.

webdav_reauth_test parses well-formed and malformed challenges from
exactly-sized buffers under ASan. The previous parser aborts on the
first malformed one.
2026-10-01 02:56:44 +02:00
LibretroAdmin 1924d2a5dc webdav: take the Digest challenge as const
webdav_needs_reauth() now walks the response's header block with
net_http_header_next(), which hands out const char *, and passed each
line to webdav_create_digest_auth(char *) - a -Wdiscarded-qualifiers
warning. The parser only ever reads the challenge (every field it
keeps is copied out with strlcpy/strdup), so its parameter and cursors
are const now.
2026-10-01 02:48:25 +02:00
libretroadmin 963e80d925 menu: rebuild only the menu context after Update Assets
Update Assets rebuilds the menu's own context once the archive is
extracted - context_destroy then context_reset against the running
video driver, the same pair the menu goes through when it survives a
video driver swap - instead of CMD_EVENT_REINIT. The menu reads its
icons and fonts from the assets directory when that context is built,
so the new assets show at once without tearing down and recreating
the video driver, its context and its swapchain.

Texture and font releases go to the video thread under the threaded
wrapper, as on the driver-swap path. The notification widgets keep the
assets they loaded until the video driver is next initialised.
2026-10-01 02:46:56 +02:00
LibretroAdmin b071b96a07 net_http: fall back to the next address family when socket() refuses one
getaddrinfo() with AF_UNSPEC lists IPv6 addresses next to IPv4 ones,
often first. On a system without IPv6 - a kernel or console network
stack built without it - socket() refuses AF_INET6, and that ended the
attempt in two places:

- net_http_new_socket() created the socket for the first address only,
  so an IPv6 address listed first failed the transfer outright with
  "socket_create_failed", IPv4 addresses untried;
- the connect loop moved on with socket_next(), which returns -1 when
  socket() fails for the next address, and the loop read that as the
  end of the list, so an IPv6 address mid-list hid every address after
  it.

Both now go through net_http_socket_for(), which creates the socket for
the first address at or after the current one that the system accepts,
and the connection remembers that address's family so connect starts
there. An extra socket() call happens only on a refused family; the
common case is unchanged. Full Happy Eyeballs (racing connects) is not
attempted.

http_limits_test, with socket() stubbed to refuse AF_INET6: an IPv6
address first connects over the IPv4 one after it, and with the first
IPv4 address refusing the connection and an IPv6 one next, the IPv4
address after that is still tried. Both fail on the previous code.
2026-10-01 02:45:24 +02:00
LibretroAdmin 566f3ae9a0 net_http: cap idle pooled connections and close them after 30 s idle
Every keep-alive connection a finished transfer handed back stayed in
the pool until the server closed it and select() noticed. Nothing
bounded how many - each one a descriptor here and a slot on the
server - and nothing aged them. A connection whose NAT mapping or
server keep-alive timer expired silently is never reported readable,
so it was reused: the request went into a dead socket and the transfer
waited out its timeout.

A finished transfer now hands its connection back through
net_http_conn_pool_release(), which stamps it idle and, past 8 idle
connections, closes the one idle longest. The pool sweep that already
runs when a transfer looks for a connection closes any idle for more
than 30 s before it even asks select(). Reconnecting costs a handshake;
a dead socket cost the whole timeout.

Neither touches the receive path: the release runs once per finished
transfer and the reap once per pool lookup. With the pool owned by the
driving thread, neither takes a lock.

http_limits_test: 20 finished connections leave 8 idle (all 20 stayed
before), the longest-idle go first, and a connection is reused at 15 s
idle and closed at 30 s.
2026-10-01 02:45:20 +02:00
LibretroAdmin 02dd573a34 net_http: cap the DNS cache at 32 entries
Every distinct host:port added a cache entry, and a successful one had
its timestamp bumped on every hit, so it lived for as long as it kept
being used. A session that touches many hosts - thumbnail mirrors,
cloud sync, netplay lobbies, core and asset downloads - grew a list
that is walked on every connect, with no bound.

Inserting a new host:port now evicts the least recently used entry
once there are more than 32. A lookup still in flight is never the
victim: its resolver owns the entry until it publishes. The cap costs
one walk on insert only; a cache hit and the receive path are
untouched. The expiry sweep also reads the clock once per walk rather
than twice per entry.

New samples/tasks/http/http_limits_test (net_http.c included whole,
clock and resolver stubbed), threaded and unthreaded, under ASan+UBSan:
100 distinct hosts leave 101 entries before this change and 32 after,
the newest kept and their addresses freed as they go.
2026-10-01 02:45:17 +02:00
LibretroAdmin e2e07068eb net_http: drop the DNS cache and pool locks
net_http took a mutex around every DNS cache and connection pool
access and parked DNS waits on a condition variable, on the premise
that any thread might drive a transfer. None does: in RetroArch every
net_http_new/update/wait/delete comes from task_http on the task
queue's worker, or on the main thread with threaded tasks off, and
switching between the two stops the queue first, so the handover is
ordered by the join. The only real concurrency is the DNS resolver
threads, each of which fills in one cache entry.

So the cache and pool now belong to the driving thread, and nothing
takes a lock:

- A resolver reads its entry's domain and port in place (they are set
  before the thread starts and never change), writes addr, publishes
  valid with a release store and touches the entry no more. The owner
  reads valid with an acquire load, and reads addr, frees the entry or
  joins its thread only after that. The strdup the resolver took under
  the lock, a copy of something nobody could change, is gone.
- The pool loses its lock outright; only the driving thread uses it.
- connect no longer pins the cache entry with a user count: the pin
  covered another thread's expiry sweep running mid-connect, which
  cannot happen.
- net_http_wait() parks on a retro_eventcount the resolver notifies
  after publishing, re-checking the lookup after registering so a
  publish in between is never missed, and bounded by the caller's
  timeout as before. net_http_init() creates it; there are no locks
  left to create.
- net_http_deinit() can no longer deadlock: resolvers wait on nothing,
  so joining one only lasts as long as its getaddrinfo().

The sweep still never joins a resolver that has not published - that
used to deadlock on the lock and would now stall the driving thread for
the whole lookup - and find() no longer reads addr before valid is
published, which TSan would report as a race now that no lock orders
the two.

The rule is documented next to net_http_init() in net_http.h. The
stress test's concurrency case drove six transfers from six threads,
which RetroArch never does; it now interleaves them on one thread as
the task queue does, with the resolvers running alongside under TSan.
The DNS lifetime test loses its nested-connect case (the cross-thread
sweep it modelled) and the init-failure test its resolver strdup case
(the strdup is gone). The tests that build net_http with HAVE_THREADS
link retro_eventcount.c, as Makefile.common and griffin already do
under HAVE_THREADS. The samples network job now fails if net_http.c
calls an slock_ or scond_ function.

Stress, transport-failure, task-lifetime, DNS-evict and the semantics
harness pass under TSan and ASan+UBSan.
2026-10-01 02:45:14 +02:00
LibretroAdmin dba5458fb5 net_http: keep response headers in one block instead of a string_list
Every transfer allocated a string_list for its response headers: the
struct, a 32-slot array, and a strdup per header line, then freed all
of it when the task finished - on the task thread, or on the video
thread in an unthreaded build. A typical response has 10 to 20 lines;
a 64-line one cost 67 allocations before a byte of body arrived.

The headers now go into one block as they are parsed: each line
NUL-terminated, an empty line at the end, grown by doubling from 1 KiB
and bounded by the existing 64 KiB header cap. It is allocated with
the first header line (a failed connect allocates nothing) and handed
over whole. In the harness, 64 header lines cost one allocation more
than one line does. The Location offset replaces the pointer into the
list, since the block may move as it grows.

API:
  net_http_headers_take()  the block, ownership to the caller (free());
                           same NULL rules as net_http_headers_ex()
  net_http_header_next()   walk the lines
  net_http_header_value()  first field by name, case-insensitive
  net_http_body_is_framed() takes the block

net_http_headers() / net_http_headers_ex() remain for code outside
RetroArch, building a string_list from the block on demand; nothing
in-tree calls them.

http_transfer_data_t.headers is the block. task_http hands it over
and frees it; task_core_updater frees it; the emscripten backend
compacts the browser's header text into the same layout in the buffer
it already had, so it also drops to one allocation. WebDAV, S3 and
Google Drive walk it with net_http_header_next(), and S3's ETag lookup
uses net_http_header_value(). The WebDAV, S3, transport-failure and
cheevos harnesses build or take blocks; the S3 harness links net_http.c
for the helpers.
2026-10-01 02:28:04 +02:00
libretroadmin 2a0757e911 menu: measure navigation auto-repeat between input polls
The menu acts on input collected before the frame polls, so the state
menu_event() reads is one poll old. The runloop stamps each menu poll
with the frame's time and hands menu_event() the stamp of the poll whose
state it is reading (input_time_us); auto-repeat measures the hold on
that clock. Time spent after a poll - Ozone and XMB build a whole
playlist synchronously when the selection lands on it - is not counted
as time the button was held, so one press moves the selection once,
while a button held across the stall repeats as soon as a later poll
sees it down. Steady-state repeat timing is unchanged.

samples/menu/nav_repeat_stall drives the real runloop with a scripted
input driver whose poll stalls, and runs in the Linux menu harness
workflow.
2026-10-01 01:58:22 +02:00
LibretroAdmin 92da172f10 net_http: resolve redirects with net_http_url_join
net_http_redirect() had three ways to read a Location. An absolute URL
allocated a whole connection object just to parse it; an absolute path
replaced the path; anything else went through
fill_pathname_resolve_relative, a filesystem helper that knows nothing
of URLs. "../up" was not resolved, "?q=1" was treated as a file name
and replaced the last path segment, a "//host/path" reference stayed
on the old host, and a fragment went out in the request line.

It now rebuilds the current URL, joins the Location onto it and parses
the result: one path for every form, and the place to add IPv6
literals or userinfo if a caller ever needs them. The fragment is
dropped, the port is validated, and the Location is not decoded -
"%20" must stay encoded in the request line. One scratch allocation
replaces the connection object and its copies on absolute redirects;
file_path.h is no longer needed.

Runs on 3xx only. 5 new checks, 4 failing before (the %20 one guards
against decoding).
2026-10-01 01:56:59 +02:00
LibretroAdmin 51cf924533 net_http: add net_http_url_join and net_http_urldecode
Master could encode a URL but not decode one, and had no way to
resolve a reference against a base URL: redirects did it by hand in
three branches, one of them borrowing fill_pathname_resolve_relative,
a filesystem helper. The ideas come from the 2018 index_parse branch;
the code does not.

net_http_url_join() resolves per RFC 3986 section 5 into caller
storage with no allocation: empty reference, absolute http(s) URL,
"//" network path, "/" absolute path, "?" and "#" references, and
relative paths under the base directory, followed by "." / ".."
removal in place. index_parse strdup'd the base to punch NULs into
it; this walks pointers. A reference with any other scheme, or a
buffer too small, returns -1 with an empty result.

net_http_urldecode() and net_http_urldecode_inplace() decode %XX,
reading both digits before writing so the in-place form is safe
(the 2018 version rewrote the source nibbles). A bad escape is copied
as is, and '+' is not a space outside form bodies. Documented as not
for URLs about to be requested: decoding %20 there would put a space
in the request line.

Neither runs on the body path. The unit matrix is in the
net_http_semantics harness.
2026-10-01 01:56:56 +02:00
LibretroAdmin 7e80941861 net_http: create locks only in net_http_init, strncmp the scheme
net_http_new_socket() still created the DNS cache and pool locks on
first use, the unsynchronised initialisation net_http_init() exists to
replace: two threads arriving together could each create one and then
lock different objects. RetroArch calls net_http_init() before the
task queue starts, as do the threaded test harnesses, so the lazy path
only ever served programs that skipped it - and served them racily.
It is gone; such a program now runs unlocked (slock_lock(NULL) is a
no-op), which is documented in net_http.h and next to the sample,
which now calls net_http_init()/net_http_deinit(). No lock is added:
this removes three creations from the connect path.

net_http_connection_new() compared the scheme with memcmp, reading 7
or 8 bytes of a URL that may be shorter. strncmp stops at the NUL.

http_transfer_stress_test's silent-peer case called net_http_deinit()
to drop pooled connections and carried on relying on the lazy
creation; it now calls net_http_init() after it, as its re-init case
already did.

One new check (an exactly-sized "ht" URL), which ASan reports as a
heap-buffer-overflow on the previous code.
2026-10-01 01:56:52 +02:00
LibretroAdmin dbc82bd17f net_http: bound headers and close-delimited bodies, reject bad chunk sizes
Content-Length was already capped at 256 MiB, but nothing else was.
A header block with no end, a single header line with no newline, or
an endless run of 1xx blocks doubled the buffer for as long as the
peer kept sending; so did a close-delimited or chunked body. Headers
now fail past 64 KiB in total, interim blocks included, and the
receive buffer stops at the Content-Length ceiling plus one window
for every framing.

A chunk-size line with no hex digits parsed as 0, the last chunk: the
transfer "succeeded" with what had been decoded so far and the
connection went back to the pool with the rest unread. It is an error
now, and the connection is dropped.

The stale-pool replay only looked at the receive position, which is
back at 0 once a complete status line has been consumed. A pooled
connection that died right after it was replayed, appending a second
header set to the first - against the function's own promise that
nothing is replayed once response bytes have been seen.

A buffered Content-Length body no longer takes the drain loop's
window-floor growth. Its buffer is exactly the body, so near the end
the loop doubled it - 16 MiB to 32 MiB for the final 32 KiB - and
P_DONE shrank it back: two reallocs, and two whole-body copies on
allocators without mremap, per download. This removes work from the
drain loop; nothing is added to it.

6 new checks, all failing before; the buffer-size check reads the
largest realloc through --wrap=realloc.
2026-10-01 01:56:48 +02:00
LibretroAdmin 9826950e56 net_http: cap redirects, rewind streamed bodies, fix "GET //foo"
request.path is kept without its leading slash, since the request
line writes " /" before it, but an absolute-path redirect stored
"Location: /foo" whole and went out as "GET //foo". The slash is
dropped now.

Redirects were followed with no limit, so two URLs pointing at each
other kept a transfer going forever. After 8 the transfer fails
("redirect_limit").

A streamed request body was not rewound before the redirected send.
The source was already drained, so the resend on the pooled socket
went out as headers with a short body, and only the stale-pool replay
happened to rescue it on a second connection. It is now rewound first,
the same contract as that replay, and fails cleanly when the source
cannot rewind. The 3xx's own body is no longer handed to the caller's
sink - a download to a file used to start with the redirect page -
and the sink bookkeeping starts over for the new URL. A failed
net_http_connection_new() on an absolute Location is checked.

POST is still not rewritten to GET on 301/302/303: WebDAV PUT and
MKCOL would change meaning silently.

Costs one counter and a flag on 3xx only. 8 new checks, 5 of them
failing before (the others guard behaviour that already held).
2026-10-01 01:56:44 +02:00
LibretroAdmin 4eabee2419 net_http: read Connection and Transfer-Encoding as lists, drop P_DONE walks
Both headers are comma-separated lists, and both were matched as one
exact line. "Connection: Keep-Alive, Close" or "Connection:close"
did not close, so the socket went back to the pool; "Transfer-Encoding:
gzip, chunked" was not chunked, so the body was read to a close that a
keep-alive server never sends. Both are now token scans on those two
lines only.

HTTP/1.0 closes unless the server says keep-alive; the version is
taken from the status line, and such a connection no longer goes to
the pool to cost a failed send and a replay on the next request. A
close-delimited body ended the connection too, and it is no longer
pooled either. A Content-Length arriving after "chunked" reframed
the body; Transfer-Encoding now wins whichever comes first.

Location is captured during the same walk, as a pointer into the
string_list copy of the line (not the receive buffer, which is
memmove'd afterwards), and it now accepts "Location:" without the
space. P_DONE used to walk the whole header list twice, once for
Connection and once for Location; it reads flags now.
net_http_body_is_framed() takes the same list test.

GET 200 pays one extra prefix compare, on the Connection and
Transfer-Encoding lines only. 7 new checks, all failing before.
2026-10-01 01:56:40 +02:00
LibretroAdmin d9671eda2f net_http: finish HEAD, 204, 304 and 1xx responses at the headers
A response with no body was read as if it had one. A HEAD answered
with Content-Length, or a 304 carrying the cached copy's length,
waited for that many bytes; a 204 without a length was taken as
close-delimited and waited for the close. On a keep-alive server
none of that ever arrives, so every one of them sat until the caller
timed out - and had it arrived, it would have been the next
response on that socket.

Such a response now ends at the blank line: HEAD, 204 and 304 by
definition, 101 too, with the connection marked unusable. A 1xx
other than 101 is an interim block and the real status line follows
on the same connection; only 100 was handled that way, so 102 and
103 were reported as the final status with the rest of the stream as
their body. Interim headers are dropped rather than reported as the
final response's, and redirects reuse that clear instead of freeing
and reallocating the header list. Bytes left over after a bodyless
response keep the socket out of the pool, since the next request
would read them first.

GET 200 does not take this path: one status test at the blank line.

New samples/network/net_http_semantics harness: the real net_http.c
against a scripted in-process server that logs which connection each
request arrived on, run under ASan+UBSan and TSan in the samples
network job. 11 of its 18 checks fail on the previous code.
2026-10-01 01:56:36 +02:00
libretroadmin 85d24f5606 smb: SMB 3.1.1 signs with AES-GMAC where the server offers it
Signed SMB 3 traffic, every read reply included, carried an AES-CMAC,
which chains each block on the one before and so runs a block at a
time: a 256 MiB read from Samba with signing mandatory came at 380 MB/s.
NEGOTIATE on 3.1.1 now offers SIGNING_CAPABILITIES, GMAC before CMAC,
and where the server takes GMAC the signature is a GCM tag over the
message with the MessageId, sender and CANCEL bits as nonce: the same
read comes at 1.3 GB/s, and a server's own signing is as much cheaper.
Servers without GMAC keep CMAC.

The Samba matrix checks which algorithm was chosen, GMAC by Samba's
default and CMAC from a server that allows no other, through
rsmb_get_sign_alg().
2026-10-01 01:46:30 +02:00
libretroadmin fd54097b79 crypto: X25519 and P-256 ahead of BearSSL and mbedTLS
The handshake's public-key work trailed: X25519 took 0.18 ms where
BearSSL takes 0.13, P-256 key generation 0.43 where both take 0.31, and
an ECDSA check 0.90 where BearSSL takes 0.83. X25519 ran on the generic
Montgomery arithmetic, with an inversion that multiplied for every set
bit of p - 2; on 64-bit machines it now has arithmetic of its own mod
2^255 - 19 in five 51-bit limbs and the inversion by the usual addition
chain. The curve multiply went a bit at a time, an addition per bit; it
now goes four bits at a time from a table on the heap, chosen by
scanning every entry, a zero digit adding anyway and dropping the sum,
and a signature check does both of its products in one pass. X25519
0.045 ms (BearSSL 0.13, mbedTLS 0.67); P-256 key generation 0.26 (0.32,
0.33), ECDH 0.27 (0.53, 0.84), ECDSA verify 0.35 (0.94, 1.43). RSA-2048
verification ties mbedTLS with a fresh key, as a handshake has one.
2026-10-01 01:18:58 +02:00
libretroadmin c252393c13 crypto: AES-GCM and ChaCha20-Poly1305 ahead of BearSSL and mbedTLS
On x86 with AES-NI, AES-GCM ran at 1.3 GB/s against BearSSL's 3.1, and
ChaCha20-Poly1305 at 0.39 against 0.46. CTR now keeps eight blocks in
flight, loads its round keys once per call, and steps its counter with
one vector add on the byte-reversed block, where the scalar rebuild of
every counter had been the cost; GHASH folds eight blocks per reduction
with Karatsuba multiplies. ChaCha20 runs four blocks at once on SSSE3,
chosen at run time, and Poly1305 uses 44-bit limbs where the compiler
has a 128-bit integer. Per 16 KiB record: AES-128-GCM 4.3 GB/s (BearSSL
3.0, mbedTLS 0.23), AES-256-GCM 3.3 (2.8, 0.22), ChaCha20-Poly1305 0.68
(0.47, 0.29), SHA-256 1.2 (0.27, 0.28). Other machines keep their paths.

test_bulk_paths checks the bulk paths against the block functions over
lengths around each group size and counters across the 32-bit wrap.
2026-10-01 00:49:42 +02:00
LibretroAdmin 4b9dfb2114 build: the tree's own headers are found before the system's
The compile line searches -I., libretro-common/include, deps and the
other in-tree dirs first, then pkg-config's paths, then the absolute
dirs configure found on the system (/usr/local/include on the BSDs),
so a same-named header installed on the system cannot stand in for one
of ours. tools/include_order_check.sh plants colliding headers in both
kinds of system path and compiles retroarch.c against them.
2026-10-01 00:19:20 +02:00
LibretroAdmin 79019b2933 qb: the moc check writes to config.log
A moc that is found but does not work says why in config.log, like
every other configure check, and the qmake query stays off the
configure output.
2026-10-01 00:19:16 +02:00
libretroadmin aad8374288 msvc 2005-2017: TLS, keychain, SMB and NFS; the CA bundle in parts
The Visual Studio 2005 to 2017 projects build the network stack as the
2019 and 2022 ones do: the TLS client, the keychain, and the SMB and NFS
clients. What kept them out was the CA bundle, one 255 KB string, where
MSVC before 2019 stops at 65535 bytes (fatal error C1091). net/cacert.h
now holds it in five arrays, each under 60000 bytes and ending where a
certificate does, and x509_trust_load_pem_parts() loads them all into
one trust store with no copy. tools/cacert_split.py writes the header,
from a new cacert.pem or from itself, byte for byte the same bundle.

The rest already kept to what those compilers take: C89, the LL/ULL
suffixes code they build uses, and Windows entry points resolved at run
time. The compile matrix builds the stack for 32-bit Windows in C89 at
the 0x0410 and 0x0501 floors, and checks no part of the bundle reaches
the limit.
2026-09-30 23:42:30 +02:00
LibretroAdmin dbe9cdee76 wayland/vulkan: include the backport header by its name
The Vulkan context builds against the older libwayland fallbacks like
the GL one.
2026-09-30 23:19:09 +02:00
LibretroAdmin 31bbf0c34b wayland: the GL window follows configures after the splash, at any scale
Both context drivers share one resize step: the compositor's
configures count from the first resize whatever the scaling, and the
surface takes the buffer scale where it can. With fractional scaling
or an older compositor the GL window follows the compositor's size
once the splash is gone, and on whole scales it is redrawn at the
scale it is on.
2026-09-30 23:19:06 +02:00
libretroadmin 8412e7be79 android: save before onPause()/onStop() return, not after
A task swiped away from Recents is killed as soon as it drops to the
background, which is right after onPause() returns, and a stopped
process can be reclaimed once onStop() has returned; neither case
delivers onDestroy(). APP_CMD_PAUSE and APP_CMD_STOP are therefore
acknowledged only once the one-time background flush of SRAM, core
options and retroarch.cfg has run at the top of the next runloop
iteration, so the lifecycle callback waiting for the acknowledgement
cannot return with the settings unsaved. STOP performs the flush
itself when a timed-out PAUSE left it owed, a start or resume
supersedes a held acknowledgement, and the startup pump acknowledges
at once since nothing is loaded yet.

samples/input/android_lifecycle_flush reproduces the handshake with
a real UI thread and app thread, with sabotage lanes for
acknowledging before the save and for holding the acknowledgement in
the pump, and runs under ASan/UBSan and TSan in the samples/input CI
workflow.
2026-09-30 23:03:09 +02:00
LibretroAdmin a2fbc90900 Android: enable threaded video by default
DEFAULT_VIDEO_THREADED is now true for ANDROID as well as the Switch,
so a fresh Android install runs the core on one thread and presents on
another without the user having to turn it on.

Only the default changes. A config that already has video_threaded
saved keeps its value, and the option is still there under
Settings -> Video -> Output to turn it off.

tools/settings_table.reference regenerated; only the android profile's
video_threaded row changes.
2026-09-30 22:53:01 +02:00
LibretroAdmin ba0f7d1c04 Android: enable 'Use System Keyboard' by default
DEFAULT_INPUT_ANDROID_SYSTEM_KEYBOARD goes from false to true, so menu
text entry on Android opens the system (IME) keyboard out of the box
instead of the built-in on-screen keyboard. That gives clipboard paste
and password managers without the user having to find the option first.

Only the default changes. A config that already has
input_android_system_keyboard saved keeps its value, and devices whose
input method cannot be driven from a gamepad can still turn the option
off under Settings -> Input to get the navigable on-screen keyboard
back.

The key is registered on every platform but only read under ANDROID, so
the reference row changes in all three profiles. The SDL3 option of the
same name (input_sdl3_system_keyboard) is left as it was.

tools/settings_table.reference regenerated.
2026-09-30 22:52:58 +02:00
LibretroAdmin 8fbcec8fe4 settings_table.reference: add the smb_client_readahead row
The smb_client_readahead setting went in without the reference being
regenerated, so the "Settings table drift and griffin-order unity
compile" job fails on master:

  +uint	smb_client_readahead	1	0

in all three profiles. Regenerated with

  python3 tools/settings_check.py table --write tools/settings_table.reference

No code change.
2026-09-30 22:52:54 +02:00
libretroadmin 0ddc58c44d 3ds, vita, switch: the built-in SMB client
The 3DS, Vita and Switch build smb://: the SMB and Kerberos clients
reach the network only through libretro-common's socket helpers, and
the crypto they rest on draws on each console's hardware random number
generator. The Wii U, whose crypto has only a clock to stir, is left
out: SMB derives its signing and sealing keys from that randomness. The
3DS keeps its SMB transfers at 64 KiB, as it does NFS's.

The compile matrix builds the SMB client, the Kerberos client and the
SMB VFS backend for each of the three under their Makefiles' defines.
2026-09-30 22:44:16 +02:00
LibretroAdmin 3298947b8a video_thread: the paced frame goes out on the vblank nearest its due time
The hold picks the vblank nearest the frame's due time on a grid from
the display, and aims at the due time itself when the grid is only
the video thread's clock (vsync off, no present timestamps).  A 120 fps
core on a 120 Hz display holds 120.

The threaded-video harness gains a pacing-rate lane: every shipped
pairing of content and display rate, with and without timestamps,
with a core that takes 1.3 ms and a render that varies, must hold
within 2% of its rate.
2026-09-30 22:37:57 +02:00
libretroadmin 66939756ee net/nfs: NFSv4.2 reads with READ_PLUS
Over 4.2 the client reads with READ_PLUS, whose reply sends the holes
of a sparse file as their extent rather than as zeros: a 32 MiB image
holding 1 MiB of data costs 1.1 MB on the network instead of 33.6 MB.
The runs are written straight into the reader's buffer, data copied
and holes zeroed. A reply no READ could have given - no progress short
of the end, an end before the size the file was opened with, runs out
of order - is a server's broken READ_PLUS, not a failed read: the
connection goes back to READ and the range is asked again as that.
nfs-ganesha 4.3 answers every range with one empty data run.

local_read_plus_test.sh reads a sparse file from fake_nfs42.py, a
minimal NFSv4.2 server that reports the holes the filesystem really
holds, and has Wireshark's own dissector decode the replies to check
their data and holes add up to the file. The Ganesha matrix checks the
fall back to READ; the network workflow installs tshark and runs both.
2026-09-30 22:35:56 +02:00
libretroadmin 41490906e3 remove TODO turn into actual task - now obsolete 2026-09-30 22:34:12 +02:00
LibretroAdmin 8f711ae2dd media: redraw 16x16 icons as pixel art
The 16x16 PNG and ICO are hand-pixeled from the 96x96 logo at native
resolution, with 1px outlines and hard edges so the invader stays crisp
at icon size.
2026-09-30 22:29:10 +02:00
LibretroAdmin 4a0b96a4a3 media: regenerate 16x16 icons from the 96x96 logo
retroarch-16x16.png and retroarch-16x16.ico are now downscaled from
retroarch-96x96.png so the small icons carry the current logo.
2026-09-30 22:26:29 +02:00
LibretroAdmin 343064c4a8 wayland: the cursor theme follows XCURSOR_THEME, XCURSOR_SIZE and the output scale
With no cursor-shape-v1 the context draws the cursor from a theme of
its own. It now loads the theme and size the toolkits use, at the next
whole scale up from the surface's (fractional scales included), with
the cursor surface's buffer scale set to match, and loads it again
only when that scale changes.
2026-09-30 22:17:20 +02:00
libretroadmin 8549dce77a net/nfs: after a server restart, opens are claimed back in its grace period
A server that restarts grants a client it knew its opens back during
its grace period and refuses new ones until grace ends - 90 seconds on
Linux. A file open for writing went through an ordinary open by path
after the restart, which a slow machine could not wait out, so its
next write failed. The client keeps its files that hold open state,
and whenever it identifies itself again it claims each back with
CLAIM_PREVIOUS - for 4.1 before RECLAIM_COMPLETE - where a server that
did not restart refuses harmlessly and the state it kept goes on. A
WRITE built before the call that re-dialled carries the old state, so
one whose file's state changed during it is sent again with the new.

The Ganesha restart rounds bring the server back with a 60 second
grace period, longer than any retry waits, so they test the claim.
2026-09-30 22:02:48 +02:00
LibretroAdmin fcfb331b2f content: a core that fails after the old session closed lands on the dummy core
A library that opens but is not a core (no retro_init, say) fails the
core stage after the old session is gone. The stage now runs again for
the dummy core: the drivers come back, the menu runs, and the failure
is said. On webOS a surface kept across the reinit that no init claims
is released.

Supersedes #19503.
2026-09-30 22:02:27 +02:00
libretroadmin c9c104c2aa 3ds, vita, switch, wiiu: the built-in NFS client
The 3DS, Vita, Switch and Wii U build nfs:// - the NFS client needs none
of the crypto the RAM-starved consoles leave out, and it reaches the
network only through libretro-common's socket helpers, which these
consoles already build. Where a console's socket layer may not sleep
in a select() with no descriptors, the grace-period wait takes the
platform's own sleep (RARCH_CONSOLE); the 3DS keeps its transfers at
64 KiB, where four connections at the 1 MiB other platforms grow to
would hold 8 MiB of buffers for Wi-Fi that gains nothing from them.

The compile matrix builds the client, its VFS backend and the
prefetcher for each of the four under the defines their Makefiles
set: Wii U against its own wiiu/include, Vita against new stubs of
vitasdk's socket headers.
2026-09-30 21:37:19 +02:00
libretroadmin c8daa642db vfs/smb: open files survive a server restart
An smb:// file open while its server restarted - a NAS rebooting under
a paused game - stayed dead afterwards: only a new open, stat or
listing mended a connection, and a stream held on to the one it had.
Each stream now keeps how to open its file again (share, path, and
its flags less create and truncate), and a read, write or seek that
finds its connection or its handle gone mends the connection, opens
the file again and goes on from its own record of the position, once.

A dropped context frees its files with it, in the built-in client as
libsmb2 documents for its own: struct smb2fh is the client's file, with
no wrapper of its own, and rsmb_free() frees the files still open. For
libsmb2, whose context does not, the handle's close is queued before
the dead context is let go, which answers it and frees it.
local_samba_test.sh restarts smbd under smb_idle_test, the read and
then the write first, over the built-in client and libsmb2.
2026-09-30 21:09:18 +02:00
LibretroAdmin e347535945 ci: package downloads are bounded and fall back to the main archive
Each download attempt gets four minutes; a stalled or failed one moves
the sources off the Azure mirror to archive.ubuntu.com before the
next.  The install then runs once from the downloaded packages.
2026-09-30 20:56:26 +02:00
libretroadmin 31877b2aed net/nfs: files stay usable across a server restart, a dropped link or a long pause
A game paused, or between disc reads, while its NAS rebooted found its
nfs:// files dead afterwards: the client never dialled again, a 4.1
session lost while idle was not set up again on the read path, and a
4.0 client whose lease ran out could not open its files again. A call
that fails now drops the connection and the next one dials afresh -
same identity, so a server that kept this client's state gives it back
- and reads, lookups and writes, which change nothing when repeated at
their offset, go again on the new connection; other calls fail once
rather than risk running twice. A lost 4.1 session is set up again, an
expired 4.0 client identifies itself again, and a file open for
writing whose state the server lost is opened again by its path.
Arguments built in the shared buffers are set aside before a dial,
which would otherwise send what the dial left there.

nfs_idle_test holds a file open for reading and one for writing across
a pause; the Ganesha matrix runs it past a short lease and across a
server restart for 4.0 and 4.1, and the unfs3 matrix across a restart
with the read and then the write as the first call after it.
2026-09-30 20:44:04 +02:00
LibretroAdmin 18a8f558ea gfx_thumbnail_preview: the thread stubs take rthreads.h's types
The audio driver header now includes rthreads.h, so the stubs must
match its declarations.
2026-09-30 20:41:34 +02:00
libretroadmin c9852fd7e3 net/nfs: NFSv4.1 and 4.2 sessions; version 4 takes the newest the server offers
The v4 client spoke 4.0 alone, which servers that have dropped it -
Windows Server among them - refuse. With version 4 chosen it now asks
for 4.2, then 4.1, and settles on 4.0 only when the server answers
neither. From 4.1 the client identifies itself with EXCHANGE_ID, opens
a session with CREATE_SESSION and says RECLAIM_COMPLETE, and every
request after that leads with a SEQUENCE on a session slot, which also
keeps the lease alive. Pipelined READs each take a slot of their own,
as many as the server grants. A session lost while idle is set up
again once; the server lets a session lapse with its lease when the
connection closes.

The Ganesha matrix runs every case against servers offering 4.0, 4.1,
4.2 and all three, each round on a fresh export, and checks the
version the client settled on.
2026-09-30 20:03:18 +02:00
LibretroAdmin 0da22ec5f9 ci: the menu harness job checks the lock budget
tools/lock_acquire_budget.sh runs on the release objects the job
already builds, next to the hot-function budget.
2026-09-30 19:56:47 +02:00
LibretroAdmin 7bd428f268 task_queue: a main-thread handler's wait sees a worker's task retire
Covered by the main-thread sample: the nested gather retires the
task, its callback runs once, and the handler goes on.
2026-09-30 19:56:44 +02:00
libretroadmin b0728d0177 command: LIST_CORES, LIST_PLAYLISTS and GET_PLAYLIST
A client could tell what was running but not find anything to run:
LOAD_CONTENT takes a core path and a content path and nothing reported
either. LIST_CORES gives the installed cores and their paths,
LIST_PLAYLISTS the playlists, and GET_PLAYLIST a playlist's entries -
index, label, content path, core path - 200 at a time with MORE <next>
to page on. All three are read-only commands on every interface and
tools for MCP clients.

The playlist commands read files, so they answer from a task: the text
is made on the task thread and sent from its callback through the
asking interface's reply_to, which holds an MCP request open until
then, and is dropped if the interfaces were re-created meanwhile.
2026-09-30 19:47:54 +02:00
LibretroAdmin fc80bee135 ci: package installs retry and have time for a slow mirror
apt-get install is retried up to three times with a fresh index, and
the install steps get 15 minutes (20 for the network clients' larger
set) instead of 5 or 8, which a slow mirror ran past mid-download.
2026-09-30 19:41:01 +02:00
LibretroAdmin 7e4b79ea54 Document the waits the frame loop still allows
Each is commented where it is, and CODING-GUIDELINES lists them.
2026-09-30 19:21:07 +02:00
LibretroAdmin 2edc9afbc6 content_load: the screenshot lane holds the I/O window across its reset
The window resets by the clock; a reset between the lane's hog and the
screenshot handed the screenshot a fresh window.
2026-09-30 19:21:00 +02:00
LibretroAdmin 4e9bff8d64 task_queue: a main-thread handler that waits does not run them again
The threaded gather a wait runs from inside a main-thread handler
leaves the main-thread tasks to the pass further up the stack.  They
ran twice and were freed twice: a load that failed with Threaded Tasks
on crashed falling back to the dummy core.
2026-09-30 19:20:56 +02:00
libretroadmin 89a1a78582 audio: the driver header takes the thread types from rthreads.h always
Without threads audio_driver.h declared slock_t and scond_t itself,
which rthreads.h declares with or without threads, so a file including
both - task_save.c - repeated the typedefs and make C89_BUILD=1
--disable-threads stopped there. The header includes rthreads.h in
every build; it holds declarations only. The compile matrix builds the
tasks and the audio driver that way, C89 and without threads.
2026-09-30 19:20:33 +02:00
LibretroAdmin 1b1b53d305 threaded-video harness: the ring-full lane runs until the driver has drawn its ten
The lane hooks the driver's frame() with a 30 ms present and checks
that every frame the video thread draws behind it is newer than the
one before. It also asked for at least ten such draws out of the 60
frames the core pushes - a race between this thread and the slow
present on whatever GPU is there: 32 draws on an M1, 10 on the CI
runner's Apple Paravirtual device in one run and 8 in the next, which
failed the macOS metal-harness job's XRGB8888 pass. The count says
nothing about the wrapper; the staleness of the draws does.

The core now keeps pushing after the 60 until the driver has drawn
ten, up to 600 more frames; a driver that never gets there is still a
failure. The grant and zero-copy counts stay measured over the first
60. Linux on Vulkan: 29 drawn, all lanes pass.
2026-09-30 19:16:42 +02:00
libretroadmin fabebb1d08 vfs/smb, vfs/nfs: the log says how much of a read read-ahead had ready
Whether a read-ahead window helps could not be seen from outside. Each
SMB and NFS stream with a prefetcher counts the octets it served from
its windows and those it missed and read directly, and adds them to the
backend's totals when it closes; once the core has closed its files
the frontend logs them - [SMB] Read-ahead: 812 of 900 KiB read came
from its window (90%) - and takes them back to zero. Nothing is counted
or logged while read-ahead is off.

The SMB and NFS thread tests read the test file through and check the
counts add up to it with read-ahead on and stay empty with it off.
2026-09-30 19:07:08 +02:00
LibretroAdmin a2020dca46 threaded-video harness: do not let the core pause for the window not being active
On macOS every lane that counts frames the core ran failed - the
zero-copy lanes saw 0 of 60 asks granted, the lent-window lane found
no grant and skipped, and "Paused" sat in the corner of the window
throughout. The harness window is never the active one: it is run
from a terminal, and under Cocoa the application is never made key.
pause_nonactive defaults on, so the frontend paused the core while
the window was inactive, and the core ran nothing; the lanes that
count what the driver presents (dupes count there) passed and hid
it. The harness config now turns pause_nonactive off. The same on
Linux, where the Xvfb window happened to report focus.
2026-09-30 18:57:08 +02:00
libretroadmin 48629e9b8f net/nfs: an NFSv3 open in a known directory is one round trip
NFSv3 resolved a path with a LOOKUP per component and then a GETATTR,
so each open three directories down cost five round trips - paid on
every file by content that opens many, as ScummVM does. Each
connection keeps the handles of the directories it walked through
recently, and a walk starts from the deepest one on its path: an open
in a known directory is a single LOOKUP, whose own attributes stand in
for the GETATTR. A handle the server calls stale empties the cache and
the walk starts again from the root; a rename, an unlink or a new
connection empty it too. NFSv4 already walks in one COMPOUND.

The client test counts the round trips of a repeated open three
directories deep, and opens through a renamed directory's old and new
names.
2026-09-30 18:48:43 +02:00
LibretroAdmin cb8c7cf34a threaded-video harness: put default.metallib next to the harness binary
The Metal driver loads default.metallib from the executable's own
directory - a bare binary has no .app bundle for newDefaultLibrary to
look in - and the tree's make builds it next to retroarch. The
harness binary lives in samples/gfx/threaded_video, so the driver
looked there and found nothing:

  [Metal] Could not load ".../threaded_video/default.metallib": library not found.
  [Metal] Failed to load main pipeline shader functions.
  [Video] Cannot open video driver. Exiting...

On the Cocoa build path build.sh copies the tree's default.metallib
next to the harness binary, and says so if the tree has none. clean
removes the copy.
2026-09-30 18:46:40 +02:00
LibretroAdmin 4b3ffffe37 threaded-video harness: on macOS, forget the argument AppKit filed as content
The harness's one argument is its cycle count. AppKit hands every
command-line argument that is not a flag to the delegate as a file
to open (application:openFiles:) while the application finishes
launching, before rarch_main() is reached; with no core up yet the
delegate stores it as the content path, and retroarch_main_init()
then tried to load a file named "1":

  [Content] Loading content file: "1".
  [Content] Could not read content file: "1".
  FAIL: retroarch_main_init failed

The Cocoa rarch_main() clears the content path before the lanes run.
2026-09-30 18:43:33 +02:00
libretroadmin 38d05171fb net/nfs: an NFSv4 export is found by the path v3 knows it by
Servers may root the v4 namespace below their export paths - Linux's
fsid=0, a Ganesha Pseudo of / - so /media exported as the root is "/"
to v4 and /media/user is "/user"; the export field, given the
directory's path on the server as for v3, found nothing. When the
export is not there and its first component is not in the namespace
either, that component is taken to lie above the v4 root and dropped,
down to the root itself. A first component the namespace does have
means the path is wrong, and it is refused as before.

The Ganesha matrix mounts the export by its server path under v4 and
checks a wrong path under an existing export is still refused.
2026-09-30 18:38:25 +02:00
LibretroAdmin 82cd1f00c8 threaded-video harness: build.sh rewrites the link line with patterns BSD sed takes, and checks what it built
On macOS the harness build "succeeded" and left no threaded_video_test:
the link line's -o was rewritten with \?, a GNU sed extension that
BSD sed treats as a literal, so the pattern never matched, the link
produced retroarch in the tree root, and the script said "built".
check-driver then ran ./threaded_video_test and got "No such file or
directory".

The rewrite is two plain patterns now (-o retroarch and
-o retroarch_debug), and build.sh checks every object and binary it
was supposed to produce, so a substitution a sed does not take fails
the build where it happened instead of at the run. The script was
run under BusyBox's POSIX sed, grep, awk and sh as well as GNU's on
the Linux tree; both build.
2026-09-30 18:34:39 +02:00
libretroadmin 21377ad1b8 vfs/smb, vfs/nfs: read-ahead is off unless chosen, and off is off
SMB and NFS read one request per read again unless a read-ahead is
set: both default to 0, and 0 now means off. It had meant 1 MiB - the
backends substituted their window for it and still started a
prefetcher on every read-only open, each holding a second connection
and a thread, which content that opens many files at once paid for on
every open. With read-ahead off no prefetcher starts; with it on, both
behave as before. The ranges start at 0, NFS's in SMB's 64 KiB steps.

The SMB and NFS thread tests run with read-ahead off and on, checking
a read-only open starts a prefetcher only when it is on, and the menu
harness checks both settings can be set to 0 and default to it.
2026-09-30 18:33:26 +02:00
LibretroAdmin 92dc759b3b threaded-video harness: run on macOS, inside Cocoa's own main, and on the Metal driver in CI
The harness did not build on macOS. There the frontend's main() is
Cocoa's (HAVE_MAIN, ui_cocoa.m): it stands up NSApplication, the
delegate and the window, and calls rarch_main() from
applicationDidFinishLaunching:, and the Metal and Cocoa GL contexts
need all of it. build.sh replaced retroarch.c's main() with the
harness's own, which on macOS is a second main() against ui_cocoa.o
and an application that was never started.

On a tree whose compile line carries HAVE_MAIN the harness now keeps
Cocoa's main and takes rarch_main() over instead (harness_cocoa.m,
compiled with the tree's own Objective-C line; retroarch.c gets its
rarch_main renamed, the harness's main becomes harness_main): the
lanes run in rarch_main's place, on the main thread with the
application and window up. Each frame turns the main run loop once,
so the window's layer tree is committed and the drawables the driver
presents come back round. And the process leaves with the harness's
status: the Cocoa companion's deinit, reached from main_exit(),
terminates the application, which is exit(0), so an atexit handler
installed once the status is known ends the process with it. Nothing
changes off macOS - the hooks are under HAVE_COCOA and the other
build path is as it was. check-driver no longer wants $DISPLAY on
Darwin.

MacOS.yml gets a metal-harness job: a Makefile build without Qt on
the runner, then the harness on DRIVER=metal with the core pushing
RGB565 and again XRGB8888 - every lane the Vulkan and D3D legs run,
including the lent-window lane over the Metal lend of 771a19c8a1,
on the runner's GPU. It is the first end-to-end run of the Metal
driver anywhere in CI.

build.sh also says why it could not find the compile line when it
cannot: the usual reason is a config.mk that fell behind configure
after a pull ("config.mk is outdated or non-existing"), which make
reported and the script threw away - that was the failure reported
against it today.

The Linux path was rebuilt and rerun on Vulkan under ASan with this:
all lanes pass. The macOS path is compiled and run only by the new
CI job; no Apple toolchain here.
2026-09-30 18:30:49 +02:00
LibretroAdmin 27c7115d33 samples: nothing builds against the removed nbio
The PNG samples read their files back with filestream_read_file; the
nbio sample goes with the code it tested.
2026-09-30 18:21:23 +02:00
libretroadmin 1d32702659 menu: a value picked from a setting's list is the value listed
A setting's list starts at its minimum, but a pick was turned back
into a value from 0 (plus offset_by, which few settings set to their
minimum), so every range above 0 was a step short: NFS Version 4 set
1, an SMB timeout of 20 set 19, a read-ahead of 64 set 0, Minimum
Audio Latency and the rest likewise. The pick is taken back from the
minimum in whole steps, exactly as the list is built. The network
settings harness picks through the real dropdown to hold it there.

The SMB timeout goes to 60 seconds as NFS's does, and NFS Network
Settings gets the icon SMB Network Settings has in Material UI.
tools/console_objects_check.sh, run by the compile matrix, reads what
each console Makefile builds and fails on the MCP server anywhere or
the crypto library on GameCube, Wii and PS2.
2026-09-30 18:14:08 +02:00
LibretroAdmin dbc19b9b2f content_load: the close-waits lane is not bounded by the machine's speed
Under TSan the save takes some five hundred frames of the I/O window,
past the lane's 400; the bound now only stops a close that never ends.
2026-09-30 18:11:07 +02:00
LibretroAdmin 1986e2fe7f screenshot: encode a slice at a time from the frame loop
With Threaded Tasks off the whole deflate ran in one handler call on
the frame thread.  A cancelled screenshot leaves no partial file.
2026-09-30 18:11:03 +02:00
LibretroAdmin 4d04a1d3e6 rpng: encode in steps
rpng_encode_begin/step/free write the same file as the one-go encode,
a bounded number of rows at a time.
2026-09-30 18:11:01 +02:00
LibretroAdmin 1d17be24ab metal: lend the core a framebuffer, and read a frame pushed from it out of the loan
The Metal driver was the one desktop driver with no
get_current_software_framebuffer: a core's ask was declined, it
rendered into its own buffer, and every frame was copied into the
texture by the host (replaceRegion:). Vulkan and D3D12 lend; the
threaded wrapper lends; a core written for the loan (beetle-psx's
software renderer since 3f5be07) got nothing from it on macOS with
threaded video off.

FrameView lendFramebuffer: hands the core the contents of a
shared-storage MTLBuffer sized to its ask, in the frame's pixel
format - BGRA8 for XRGB8888, and R16Uint for RGB565, which is what
_src is, so the existing compute conversion applies unchanged - with
a linear texture over the buffer at the tight row pitch. On Apple
silicon that is the memory the GPU samples. updateFrame:pitch: then
tests the pushed frame against the loan the way vulkan_frame_window
and d3d12_sw_fb_window do (7510039d5d): the whole loan, or a window
into it at the loan's pitch and whole, is blitted out of the loan
texture with a source origin into the frame texture (or into _src
for RGB565), on the blit command buffer the context commits ahead of
the frame's own - one GPU copy, no host copy, and the cached frame's
pointer into the loan stays valid. The lend waits for the command
buffer of the last such blit before handing the buffer back to the
core, since the core is about to write over what the GPU read; a
resize, and the view going away, retire the cached frame first.

Declined, as the Vulkan and D3D12 lends decline, when the row pitch
the device needs for a linear texture (its minimum linear texture
alignment, at least 16) is wider than width * bpp - a core that
ignores fb.pitch would shear - and for a 10-bit source, which has no
such texture. At an alignment of 16 the widths cores use (320, 640,
beetle-psx's 700) all lend at the tight pitch in both formats.

samples/gfx/metal_texture_update gets a third lane, run on the macOS
CI leg's Metal device: for those widths in both formats, the shared
buffer and its linear texture at the tight pitch, a CPU-written
pattern, a window blitted out with an origin and read back at its
four corners, and a second write after the first blit's command
buffer completed that the second blit must show - the ordering the
lend relies on. The three counters of 7510039d5d cover this path too,
and the threaded-video harness's lent-window lane runs on it with
DRIVER=metal on a Mac.

Compiled and run only by CI here (no Apple toolchain on this box).
2026-09-30 18:01:26 +02:00
libretroadmin 9ffdab15a3 network: the MCP server is a desktop build option, HAVE_MCP
The MCP server was listed with the networking objects, so every
platform Makefile with networking compiled it, the consoles included,
and the 3DS - networking without the command interface it is built on
- failed to. It is HAVE_MCP now: a ./configure option, on by default,
dropped without networking, and built only with the command interface;
no console Makefile sets it. The server, its settings and its menu
entries all follow it.
2026-09-30 17:59:49 +02:00
LibretroAdmin bc3fde8e1d oga: a frame pushed as a window into the driver's own loan is blitted out of the loan
A core that renders into the buffer GET_CURRENT_SOFTWARE_FRAMEBUFFER
lends it may push back a pointer partway into that buffer, at the
buffer's pitch, with the size of the window it wants shown: that is
how an overscan crop is done by offset, and beetle-psx does exactly
that since 3f5be07. The threaded wrapper has accepted such a push as
a lend since 9ed0db58c3, the vulkan and d3d12 drivers since 7510039.
This driver lends frame_surface->map and knew a lent frame only by
that exact pointer, so a window into it took the copy path: memcpy
from the surface onto itself, one row pitch apart, shredding every
row the copy overtook, and then blitting from (0,0) so the crop was
ignored as well. Most visible on a paused frame or a screenshot,
where the frontend replays the cached window pointer.

Recognise the window the same way the other two drivers do: inside
the mapping, on the mapping's pitch, starting on a pixel. Then skip
the copy and blit from the window's origin, clamping the size to
what the surface holds from there. A pointer inside the mapping that
is not addressable as a window (another pitch, or mid-pixel) is not
copied either -- that would be the same self-overlapping memcpy --
the loan is shown from its origin instead.
2026-09-30 17:40:46 +02:00
libretroadmin de1fe72c69 network: an MCP server, the command interface for AI assistants
RetroArch serves the Model Context Protocol over Streamable HTTP as one
more command interface beside UDP and stdin, polled with them and
waiting on nothing. Its tools are the commands in command.h, listed
from the rows HELP prints with their read-only and destructive hints,
and a tool call runs the command's own handler; a command that answers
later holds its connection until it does. Both protocol eras are
answered: 2026-07-28 statelessly, with server/discover and the header
checks, and earlier revisions through initialize.

It is off by default and listens on 127.0.0.1. Every request carries a
bearer token, made on first start and kept in the keychain, and a
browser Origin other than this machine is refused. MCP Server, its port
and its token are under Settings > Network; docs/mcp-server.md covers
connecting a client.
2026-09-30 17:36:04 +02:00
libretroadmin 21890c064a Remove unused nbio code 2026-09-30 17:24:26 +02:00
libretroadmin 3321c7cb5a tasks: the keychain passphrase task is declared without networking too
task_push_keychain_passphrase() was declared inside the networking-only
part of tasks_internal.h, so a build with the keychain and without
networking called it from the menu undeclared. It is declared under
the keychain's own gate. The compile matrix builds the menu and the
task in that shape with implicit declarations as errors.
2026-09-30 17:15:30 +02:00
LibretroAdmin 877823df48 threaded_video: fast-display counts drains and cadence instead of timing
A stalled machine drains once per stall and moves a few frames; the
defects the lane is for drain every fifth frame or move every one.
2026-09-30 17:06:23 +02:00
LibretroAdmin 82a0f019dd video_thread: no catch-up burst after a stall
The hold measured from the vblank the video thread expected at its
last present.  After a stall that vblank had passed, and every frame
was released at once until the presenter caught up: the core ran at
the display's rate.  It is carried forward on the display's grid.
2026-09-30 17:06:19 +02:00
libretroadmin 2790aa0cce command: every command describes itself; HELP lists them
Both command tables carry a one-line description and flags saying
whether a command only reports or may lose data or load code, so the
tables in command.h are the one description of what RetroArch can be
told to do. HELP answers with every command - name, argument, what it
does, [read-only] or [destructive] - or with one when named; interfaces
that hand the commands to a client can be generated from the same rows.
tools/command_table_check.py, run by CI, fails on a command added
without a description.

A command sent without an argument no longer has its argument pointed
one byte past the end of the line, and SHOW_MSG is documented as
taking the message it shows.
2026-09-30 09:43:00 +02:00
libretroadmin 684be71938 menu: sublabels for the NFS settings, SMB read-ahead and Kerberos, TLS verify
Every NFS setting, SMB read-ahead, the SMB Kerberos realm and KDC, and
TLS certificate verification had help text but nothing that bound it
to the entry, so they showed without a sublabel. They are bound in the
sublabel map, each under the gate its setting is built under.
2026-09-30 09:24:07 +02:00
LibretroAdmin 2a17334ad2 vulkan: parking a texture allocates nothing and cannot fail
A texture released through vulkan_unload_texture is parked for the
frame thread to destroy once a swapchain's worth of frames has gone by,
because a frame already handed over, or menu code still looking at the
handle, may yet use it. Parking allocated a list node per texture, and
when that allocation failed the texture was destroyed on the spot,
after waiting for the frames already submitted - which covers neither
of those two.

The link and the countdown are in struct vk_texture now. Parking is the
compare-and-swap push it already was, on the texture itself: no
allocation, so no failure, so no path that destroys early or waits.
vulkan_texture_defer() loses the same fallback, and every unload loses
a malloc and a free.

What is left is vulkan_texture_defer_copy(), which parks a texture
embedded in another object (the font's two) by moving it to a heap
copy; if that copy cannot be allocated it still waits on the driver's
own fences and destroys. That runs on the frame thread.

Two comments that still described a vkQueueWaitIdle under queue_lock in
the unload path are brought up to date; there has been none for a
while.

There is no test that fails before this and passes after: the path it
removes needed an allocation to fail. The threaded-video harness lanes
that load and unload textures pass on lavapipe under the validation
layer (software core, hardware core, hardware core with its own
device), and a running RetroArch with the ozone menu toggled eight
times and quit, threaded video on and off, reports nothing - a texture
destroyed early or never would show there.
2026-09-30 09:09:08 +02:00
libretroadmin 2a4d05984e docs: the built-in network stack and the keychain
docs/network-stack.md describes what users meet of the built-in TLS,
SMB and NFS clients and the keychain: certificate verification, the
SMB and NFS settings and content addresses, read-ahead, where saved
passwords live, and how to move a keychain to another machine with a
passphrase; with the configure options that shape the stack. CHANGES
lists the stack under NETWORK.
2026-09-30 09:07:59 +02:00
libretroadmin f0ec62cd61 keychain: a passphrase lets the keychain move to another machine
The keychain's key comes from the key file and this machine's identity,
so a configuration carried to another machine lost every saved
password. Settings > User > Keychain Passphrase wraps that same key
under a passphrase in the key file, next to a wrap for this machine;
nothing is sealed again, and a key file without one is unchanged. On
another machine the keychain comes up locked, saves write nothing in
the clear, and entering the passphrase once unlocks it, wraps it for
the new machine and puts the credentials back into the running
settings. The key derivation runs as a task a slice at a time, so the
main thread never waits on it, with or without threads.

A sealed value that cannot be opened is now taken out of what the
loader sees even when there is no key at all, instead of becoming the
setting's value; and only the values that failed to open are carried
through a save, so clearing a password reaches the file and a normal
save no longer reports credentials from another machine.
2026-09-30 08:45:30 +02:00
LibretroAdmin 6d0a54a08c threaded_video: pacing-drain counts frames instead of timing them
Frames the wrapper replaced because the display ran late are no longer
a failure: every frame must be drawn or replaced, and nothing else.
2026-09-30 08:45:02 +02:00
LibretroAdmin b82a139538 explore: title the index build task
The slow-handler watchdog names a task by its title; this one was
reported as untitled.
2026-09-30 08:44:58 +02:00
libretroadmin f508e065c6 crypto: the AES tables are constants; nothing is set up on first use
The AES round tables, the inverse S-box and the InvMixColumns table
were built on first use behind a plain flag. Two threads setting up
keys at once both wrote them while each read the flag - a data race
and, on a weakly ordered CPU, a thread that sees the flag before the
tables and encrypts with half of them. They are now constants generated
from the S-box, so there is nothing to set up; the AES-NI check caches
its answer atomically.

crypto-tsan-check first uses every primitive with a table or a CPU
dispatch from eight threads released together, under TSan, and checks
they all compute the same bytes. It runs in the libretro-common tests
workflow and reports this race on any runner, where the SMB test saw
it only when two connections started together on two cores.
2026-09-30 08:23:37 +02:00
LibretroAdmin effa709984 vulkan: the emulated mailbox takes no lock
Emulated mailbox - FIFO kept while fast-forwarding with vsync on, with
a second thread doing the acquiring so the presenting thread never
blocks on the display - passed a request and an answer between its two
threads under a mutex, with a condition variable that carried the
request, the answer and the teardown all at once.

Each of the three is a word of its own now, written by one side and
taken by the other, and each direction has an eventcount:

  request   the presenting thread wants an image; the acquire thread
            takes it and acquires
  acquired  the acquire thread has an answer. result and index are
            written before it is raised, and not again until the
            presenting thread has lowered it and asked once more
  dead      teardown

The acquire thread sleeps on `work` and looks at request and dead again
between announcing the wait and committing to it; the blocking acquire
on the presenting side sleeps on `answered` the same way, against the
one deadline it already had. Whether a request is outstanding was only
ever the presenting thread's business and is a plain bool of its. The
finite waits that let the acquire thread notice the teardown are as
they were.

Nothing the driver does is different: same requests, same answers, same
bounds. This was the last lock in the Vulkan code that was not
queue_lock.

samples/gfx/vulkan_validation had no case for the mailbox at all. It
has one now: emulation switched on in place, forty frames drawn through
it under the validation layer, switched off with a request possibly in
flight - which must end with an image in hand and the acquire thread
joined - three times over, the last torn down with emulation on. It
passes on the mutex version and on this one, and on this one under
ThreadSanitizer.

samples/gfx/vulkan_mailbox_init_leak keeps verbatim copies of the
mailbox's init and deinit, with the primitives mocked, to hold the
partial-init leak fix in place; the copies and the mocks follow: the
failure stages are the first eventcount, the second, and the thread.

tools/lock_acquire_budget.sh measures the acquire thread (none), the
present (queue_lock, once) and the acquire (queue_lock, only in the
path that empties a full stale-acquire list). Against the code before
this change the first reads four and the last two.

With vsync on, fast-forward toggled six times in a running RetroArch
with Beetle PSX HW, threaded video on and off: emulation entered and
left three times each, no validation error.
2026-09-30 08:23:23 +02:00
LibretroAdmin b6f41430e0 command: answer PLAY_REPLAY_SLOT from the frame loop
The command returns at once; the reply follows once the movie task has
installed the replay, to the sender that asked.  command.c joins the
no-wait guard.
2026-09-30 07:55:25 +02:00
LibretroAdmin 4edb7a8ab9 natt: drop the blocking variants
No caller blocks: the NAT task steps each request itself.  network/
joins the no-wait guard.
2026-09-30 07:55:22 +02:00
LibretroAdmin b4098a2fb4 vulkan: the last drains outside the teardown go, and queue_lock guards only the queue
Three idle waits were still on the idle-wait allowlist, one list
borrowed queue_lock because it was the lock at hand, and the present
held it across its own failure handling. queue_lock is the
one lock this driver cannot lose - the libretro interface hands it to
hardware cores, and a VkQueue shared by two threads needs it - so what
it guards should be the queue call and nothing else.

  vulkan_submit_deferred_cmd_fenced
      With no node or fence to defer an upload with, it submitted and
      then called vkQueueWaitIdle, under queue_lock. That drains a
      hardware core's work too and keeps the core out of lock_queue for
      as long as the GPU takes. The upload is waited for on a fence now
      - the caller's, else one the driver keeps for the purpose - with
      the lock dropped after the submit.

  vulkan_acquire_next_image
      With the stale acquire list full it called vkDeviceWaitIdle and
      destroyed a semaphore. That is a use of every queue made without
      queue_lock on the presenting thread, while a core may be
      submitting; it waited on the core's work; and an acquire is not
      queue work, so it never waited for the signal it destroyed the
      semaphore under. One batch that waits on the whole list, with a
      fence, consumes the signals; the lock is held for the submit call.

  vulkan_present
      Kept queue_lock from the present call to the end of the function,
      across the failure handling. A failed present tears the swapchain
      down, the teardown waits for the present with an empty fenced
      submission behind it, and that submission takes queue_lock: where
      the mutex is not recursive - everywhere but Windows - the thread
      waited on itself, and an out-of-date present never returned. The
      lock is dropped when vkQueuePresentKHR returns.

  slang_chain_flush
      The vkDeviceWaitIdle fallback for a chain created without
      wait_submissions. Every chain in the tree is created with it. The
      callback is required now and the fallback, with the chain's copy
      of the lock callbacks that only it used, is gone.

  the parked-texture list
      vulkan_unload_texture parks textures for the frame thread to
      retire, and a reinit fallback can park one from the main thread,
      so the list was serialised - with queue_lock, which made parking
      a texture wait behind a present or a core's submit. Parking is a
      compare-and-swap push onto one atomic pointer now, and the list
      the frames count down is private to the thread that records
      them, which takes what was pushed with an exchange each tick.

tools/vulkan_idle_wait_allow.list is down to its one legitimate entry,
the device teardown; the existing idle-wait check fails on the three
removed sites if any comes back.

samples/gfx/vulkan_validation gains two cases. A present that fails:
the driver has to come back from it with the swapchain torn down. The
code before this change never returns from it on Linux (the test gives
it ten seconds). And the acquire case: frames presented
for more than two lists' worth without anything waiting on their
acquires. The driver has to get through it without one queue or device
drain, and without the validation layer objecting to the batch that
consumes the list or the semaphores destroyed behind it (the layer's
MissingAcquireWait on each such present is the scenario itself, and is
set aside by name). The code before this change drains 19 times there;
this one, none.

The threaded-video harness passes on lavapipe under the validation
layer with a software core, a hardware core, and a hardware core that
creates its own device.
2026-09-30 07:45:45 +02:00
LibretroAdmin fbff529434 vulkan: the filter chain no longer creates mirror-clamp samplers
The slang filter chain makes one sampler per filter, mip filter and
address mode. The address modes had a fifth entry, mirror-clamp-to-edge,
that no wrap mode maps to - a preset can name border, edge, repeat and
mirrored repeat, and rarch_wrap_to_address() returns one of those four -
so its four samplers were created on every chain and never bound.

Creating them is only valid with VK_KHR_sampler_mirror_clamp_to_edge
enabled. The frontend adds that, as an optional extension, to a device
it creates itself. A core that creates the device through the
negotiation interface's create_device is handed only the required list,
so on every such core (Beetle PSX HW, for one) each chain raised

  VUID-VkSamplerCreateInfo-addressModeU-01079
  VUID-VkSamplerCreateInfo-addressModeU/V/W-parameter

four times over. Drivers tolerate it; it is invalid all the same.

The entry and its case are gone. The optional extension stays in the
device list: nothing in the tree uses it now, but a core drawing on the
frontend's device may.

The threaded-video harness core can now be that kind of core:
HARNESS_CORE_HW_VULKAN=2 registers a version 1 negotiation interface
and creates the device with exactly what the frontend passes. The
Vulkan validation workflow runs the harness that way as well. Before
this change that run reports 48 validation errors and fails; with it,
none.
2026-09-30 06:49:47 +02:00
libretroadmin 4e6885d5a3 vfs/smb: libsmb2's list of live contexts is only touched under a lock
libsmb2 keeps every context on one process-wide list that
smb2_init_context() pushes to, smb2_destroy_context() unlinks from and
smb2_context_active() walks, with no lock of its own. Connections made
and dropped on several threads at once can lose an entry, after which a
live connection reads as dead and its seeks and reads are refused, or
break the list outright. Every one of those three calls in the backend
now goes through a wrapper holding a lock for the list operation alone;
none of them does network I/O. The cleanroom client has no such list,
and single-threaded builds none to guard: both call straight through.

The libsmb2 lane runs the SMB tests once more under TSan against a
TSan-built libsmb2, which reports this on any runner rather than only
when two cores happen to collide.
2026-09-30 06:48:39 +02:00
LibretroAdmin 3db41c4d27 task_queue: count due main-thread tasks in the progress walk
The gather already holds running_lock to publish progress; counting
the due main-thread tasks there saves the frame a second acquisition.
2026-09-30 06:16:46 +02:00
LibretroAdmin 1c6de25fd4 content: watchdog every frame-thread task, load stages excepted
Debug builds report slow handlers with Threaded Tasks on as well.  The
load stages that run the core's own load and the driver rebuild are
titled and exempt: they are one-off by design.  The content_load
harness gains the acceptance run - menus, playlists, overlays, the
Explore index and a load, with no handler over a frame.
2026-09-30 06:16:42 +02:00
LibretroAdmin dc321182c8 explore: name the release month category
It read "null (1 - 12)" on the top Explore screen.
2026-09-30 06:16:39 +02:00
LibretroAdmin d3149eb217 task_queue: time main-thread handlers on the threaded runner
The slow-handler watchdog now also measures RETRO_TASK_FLG_MAIN_THREAD
handlers when Threaded Tasks is on: they run on the frame thread there
too, so an overrun is the same visible stall.
2026-09-30 06:16:35 +02:00
libretroadmin 725016e00d vfs/smb: each directory listing returns entries of its own
retro_vfs_readdir_smb handed every caller the same function-static
entry, so listings on different threads overwrote one another's name
and type while the caller copied them out. The entry now lives in the
directory handle, for the cleanroom client and libsmb2 alike; the
libsmb2 threads test is clean under TSan with it.

vfs_threads_test names each failed operation with its thread, round
and what came back, so a failure on a runner says where it happened
rather than only how many.
2026-09-30 06:05:24 +02:00
LibretroAdmin b7b0920836 video: the hardware ring's fence is handed between threads lock-free; no lock in the Vulkan fence
7a8b2ab made the Vulkan ring fence re-signalable by giving each one a
mutex and an "armed" flag. That was a lock added to a path that is
meant to lose its locks. It is gone, along with everything else that
commit added to vulkan.c: the driver's ring fence is a bare VkFence
again.

What the lock was for is done in the wrapper instead, once, for every
driver, without one (gfx/video_thread_hw_fence.h). A slot has three
driver fences and each is only ever touched by the thread that owns it
at that moment; ownership moves by an atomic exchange:

 - the video thread, after a frame drawn from the slot, takes an idle
   fence, signals it and exchanges it into `armed`. If an earlier
   arming comes back out - a dupe: nobody waited it - the video thread
   owns that one, waits it out and returns it to idle;
 - the core's thread exchanges `armed` out, waits what it got, returns
   it to idle, and looks again.

At most one fence is armed and at most one is with the waiter, so one
of three is always idle. A driver's signal is only ever called on an
idle fence and its wait on a signalled one.

That also closes the same hole in the GL drivers, which needed no
change: gl2 and gl3 kept one sync object per slot behind a bare
pointer that the video thread deleted and replaced while the core's
thread could be waiting on it and then deleting whatever the pointer
held. And it retires the slot's in_flight flag, a plain bool written
by one thread and read by the other.

samples/gfx/hw_ring_fence runs the real header with two threads at
one slot and fake fences that count a signal on a fence not idle, a
wait on one never signalled, and two calls inside one fence at once;
under ASan+UBSan and under TSan. The single re-signalled fence it
replaces is built too and must fail. The Vulkan hardware-core lane of
the threaded-video harness (dupes in game and under the menu) passes
on lavapipe with the validation layer, as it did with the lock.
2026-09-30 05:48:15 +02:00
libretroadmin 6fe0b87b2e removed_member_check: leave the pruned trees out of the diff too
The check scans for readers with deps/ and the other vendored trees
pruned, but built its set of removed members from the whole diff, so
deleting a bundled library counted every member that library declared
- tv_sec, ss_family, session and the rest - as removed RetroArch API,
and the removal of deps/mbedtls, deps/bearssl-0.6 and deps/libsmb2
failed on 430 readers of the platform's own structs. The pruned trees
are excluded from the diff as they are from the scan: a vendored
library's members are its own API, read through its own headers. A
member removed anywhere else is still caught, readers and all.
2026-09-30 05:18:25 +02:00
LibretroAdmin 7a8b2ab26f vulkan: a dupe of a hardware frame may signal its ring fence again; hardware-core lane
The regression lane for 53ca1ac (a hardware core's image is let go of
before the core destroys it), and the second bug the lane ran into.

The bug. Under the threaded wrapper the video thread signals a ring
slot's fence after every frame it draws from the slot, and a dupe
draws from the slot last presented again: one frame in two for a
30 fps game, every frame while the menu is up over a hardware core.
The fence was a bare VkFence, signalled by an empty vkQueueSubmit, and
nothing reset it between a frame and its dupe, so every dupe submitted
a fence that was already signalled or still pending. That is invalid
usage (VUID-vkQueueSubmit-fence-00063/-00064); lavapipe aborts on it.

The ring's fence now records whether it is armed. A signal that finds
it armed waits the earlier arming out and resets it first - a wait on
work submitted a frame ago - and a lock orders that against the main
thread's wait-and-reset, so a reset cannot land on a fence the other
thread has just submitted. Direct3D 12's ring fence counts and never
had this.

The lane. The threaded-video harness core gains a Vulkan hardware
mode (HARNESS_CORE_HW_VULKAN=1): one image of its own, set_image and a
hardware frame per retro_run with every third a dupe, destroyed in
context_destroy without a word to the frontend, and an export that
withdraws it with set_image(NULL) and destroys it in mid-session. It
calls Vulkan through the frontend's interface and links nothing.

With that set the harness runs one lane, hw image lifetime:

 - unthreaded: frames in game, then a staged close from the menu;
 - the core again, under the wrapper: hardware frames reach the
   driver; the core withdraws and destroys its image, and no install
   names an image until it presents again; then the staged close, and
   no install names an image after the core's context_destroy.

It also requires that the close presented at least one frame between
the core's teardown and the new drivers, so it cannot pass by no
longer testing the window.

Run in the Vulkan validation job on lavapipe:

   with both fixes                 pass, no validation errors
   53ca1ac taken out               VUID-vkDestroyImageView-imageView-01026
                                   (view in use), then a segfault
   this fence change taken out     VUID-vkQueueSubmit-fence-00063,
                                   then lavapipe aborts

The software-core lanes are unchanged and pass on the same build.
2026-09-30 05:15:22 +02:00
libretroadmin f516093bc4 deps: drop the bundled mbedtls, BearSSL and libsmb2; library backends by option
Every build uses the built-in TLS, SMB and NFS clients, so the
bundled deps/mbedtls, deps/bearssl-0.6 and deps/libsmb2 are removed.
A system library can still stand in for a built-in client, off by
default: --enable-mbedtls and --enable-bearssl replace the TLS client
with a system mbedTLS or BearSSL, and --enable-libsmb the SMB client
with a system libsmb2; a library asked for and not found is a
configure error, and two TLS libraries at once are refused. The
older builtin/system switches go, as do the platform files' bundled
selections, and the libsmb2 backend follows the library's current
share-enumeration API.

Linux-library-net-backends.yml replaces the legacy lane with one lane
per library: each configures with its option, builds RetroArch to
the link, checks the library's backend is linked and the built-in
client is not, and operates it - the cheevos login transport over a
local HTTPS server for mbedTLS and BearSSL, the SMB VFS tests against
a local smbd for libsmb2 (built from upstream; Ubuntu has no package).
The cheevos_login sample links the system BearSSL, the smb_backup
sample carries stand-in libsmb2 headers for its fake share, and the
samples workflow installs libbearssl-dev.
2026-09-30 05:01:22 +02:00
LibretroAdmin 53ca1ace34 video: a hardware core's image is let go of before the core destroys it
Closing content with a Vulkan hardware core under threaded video could
freeze or crash the frontend inside the GPU driver.

A staged content close keeps the video driver up after the core is
gone and presents dupes of the last frame until the next session's
drivers are built. For a hardware core the last frame is the core's
own image, which the core destroys in context_destroy. Nothing told
the driver: video_driver_free_hw_context() called context_destroy and
left the driver holding the image, and every dupe after it sampled a
destroyed VkImageView. Seen on NVIDIA as a fault inside the driver,
called from the filter chain's final pass on the video thread, with
the main thread waiting on that frame in the ring drain.

Under the wrapper two more things made it certain rather than likely:

 - the wrapper had no invalidate_hw_render_cache, so the runloop's
   invalidate (core_reset) stopped at the wrapper and never reached
   the driver;
 - a core's set_image(NULL) - how a core withdraws its image before
   destroying it, and what LRPS2 does in context_destroy - only wrote
   the ring slot the core fills next. A dupe re-reads the slot last
   presented, so the withdrawal never took effect.

Now:

 - video_driver_free_hw_context() invalidates the driver's hardware
   render cache on both sides of context_destroy: before, so nothing
   is drawing from the image while the core tears it down, and after,
   because a core may present once more on its way out;
 - the wrapper implements invalidate_hw_render_cache: drain the frame
   ring, have the driver wait out its submissions and drop the image
   on the video thread, then clear the ring's slots and the slot a
   dupe would re-read;
 - set_image(NULL) under the ring does the same.

A dupe after that draws the driver's blank texture, as it already does
without the wrapper after a reset.
2026-09-30 04:43:21 +02:00
LibretroAdmin 1fda0518ef netplay: the deferred host setup frees its state
The task carrying the deferral's deadline had no cleanup, so its
state leaked once setup ran; LeakSanitizer on the content_load
harness's host-setup lane reports it.
2026-09-30 04:43:19 +02:00
LibretroAdmin 38c605ae44 netplay: host setup waits for the relay query from the frame loop
Hosting through a lobby relay needs the relay's address, and host
setup waited for the lobby's answer inside CMD_EVENT_NETPLAY_INIT - up
to ten seconds on a slow lobby.  When the query is still out, the
command now puts host setup off to a main-thread task that checks each
frame and reissues CMD_EVENT_NETPLAY_INIT once the answer is in, or
goes direct once the bound has passed; the frame loop keeps running.
netplay_mitm_query no longer waits at all.

The room reset at the start of host setup also cleared an answer
already in hand, so the prefetched query was asked - and waited for -
again every time; the answer is now carried across the reset.

content_load harness lane: with a relay configured and the query out,
the command returns in microseconds with host setup queued, frames
keep presenting, and setup runs once the query settles.
2026-09-30 04:16:57 +02:00
LibretroAdmin e5b3b3f7ea CI: the menu-path wait guard covers the content load; its harnesses run
The guard now greps tasks/task_content.c for task_queue_wait, forbids
the close's blocking helpers (content_wait_for_*_state_task, which
wait inside task_save.c where the first grep cannot see) anywhere in
menu/ and tasks/, and checks the load still closes through
retroarch_main_deinit_pending.  The menu-harness job builds and runs
samples/tasks/content_load and samples/menu/explore_build on its
non-Qt menu build.
2026-09-30 04:16:54 +02:00
LibretroAdmin 02c01f02e0 threaded-video harness: run the lent-window lane on the path each driver lends
The lent-window lane merged in 7510039d5d failed where a driver lends
nothing: d3d11 has no framebuffer to lend, and d3d12 lends only for a
row pitch on a 256-byte boundary, which the harness core's RGB565 at
320 wide (640 bytes) is not - so on both Windows legs the core was
granted nothing and the lane failed on the grant count. A driver that
lends nothing has no loan to push a window into: the lane now reports
a skip there instead.

That also left the D3D12 half of 7510039d5d - the window box copied
out of the loan's footprint - with nothing running it. The harness
core can push XRGB8888 (HARNESS_CORE_XRGB8888 set at start; the pixel
values are the same in the low bits either way, so every lane reads
as before): at 4 bytes a 320-pixel row is 1280 bytes, on the boundary,
and d3d12 lends. The d3d12 matrix entry runs with it. The Vulkan legs
run the harness under both formats, since the two take the driver's
two upload paths - RGB565 through the compute conversion of a staging
buffer, XRGB8888 through a linear image sampled directly and, for a
window, vkCmdCopyImage out of it; the second is the one beetle-psx
takes. Both pass on lavapipe under ASan with the validation layer
silent: 59 grants, 39 windows, 1 host copy (the core's own oversize
frame), 3 frame textures over 60 frames, and the paused redraw runs
through.
2026-09-30 04:16:41 +02:00
LibretroAdmin 7510039d5d vulkan, d3d12: a frame pushed as a window into the driver's own loan is read out of the loan
A core that renders into the buffer GET_CURRENT_SOFTWARE_FRAMEBUFFER
lends it may push back a pointer partway into that buffer, at the
buffer's pitch, with the size of the window it wants shown: that is
how an overscan crop is done by offset, and beetle-psx does exactly
that since 3f5be07 - the whole 700x480 scanout surface is rendered
into the loan and the visible part pushed. The threaded wrapper has
accepted such a push as a lend since 9ed0db58c3. The drivers' own
loans, the ones handed out with threaded video off, did not.

The Vulkan driver knew a lent frame only by the exact pointer of its
swapchain-index texture's mapping, so a window into it was a frame
of another size than the texture: the texture was recreated at the
window's size every frame (and again at the loan's size at the next
lend), the loan went to the deferred list, and the window was copied
out of the parked mapping by the host. That worked while the core
kept pushing. The cached frame kept naming the parked mapping,
though, and when the core stopped - the menu opened over a paused
core, or the window lost focus with pause-on-focus-loss on - every
iteration redrew the cached frame; after the deferral window the
mapping was unmapped and freed, and the next redraw read it. That is
libretro/RetroArch#19652 (Beetle PSX on an RTX 4060: memcpy fault
from vulkan_frame's row copy on entering core options or on losing
focus).

vulkan_frame() now tests a pushed frame against the texture's mapping
(vulkan_frame_window): inside it, on its pitch, whole, word-aligned.
Such a window leaves the texture alone - it keeps the loan's size for
the next lend and its mapping stays mapped, which is what the cached
frame needs - and is cut out on the GPU into texture_optimal, which
is what gets sampled: vkCmdCopyImage with a source offset for a
streamed linear image (the XRGB8888 case), vkCmdCopyBufferToImage
with a buffer offset and row length for a staging buffer, and a word
offset in the RGB565 compute upload's UBO (rgb565_to_rgba8888.comp,
.inc regenerated). No host copy, no texture per frame. texture_optimal
is kept at the frame's size whenever it exists, since it is sampled
whenever it exists; a window-sized one left over from a window is
recreated for a staging texture and retired for a streamed one. A
frame inside the mapping that is not a window (another pitch) is
still copied out, and the cached frame is then retired, so a paused
redraw can never follow a parked mapping again.

The D3D12 driver's loan is a persistent buffer of its own, so it was
memory-safe, but it decided by a flag set when the loan was asked
for, not by what was pushed: a window pushed into it was copied as
the whole footprint into a window-sized texture, and a core that
asked and then pushed its own buffer, or a dupe, was shown the loan.
It now makes the same pointer test (d3d12_sw_fb_window) and copies
the window's box out of the loan's footprint; the flag is gone.

Three GFX_INSTRUMENT counters cover the path: frame textures
created, frames copied by the host, frames read out of a loan.

The threaded-video harness gets a lane for it: threaded video off on
the real driver, the harness core's cropped-window lend for 60
frames, then the menu opened over the paused core for longer than
any deferral window. On the old driver on lavapipe under ASan the
lane counts 59 grants and 0 frames read out of the loan, then the
paused redraw aborts with the use-after-free; on the new one 39 of
40 pushes are read out of the loan (the 41st is the core's own
oversize frame, copied as it should be), no host copy, 3 frame
textures over 60 frames, and the paused redraw runs through with
the validation layer silent. It runs on the release, ASan and TSan
legs of Linux-vulkan-validation.yml as they are.
2026-09-30 03:47:56 +02:00
LibretroAdmin 8261ec0550 explore: build the index in steps under the shared I/O window
menu_explore_build_list was one handler call: every playlist parsed,
every RDB walked, every category sorted - on the frame thread when
Threaded Tasks is off.  The build is now begin/step/end/abort over
three phases (playlists through the resumable playlist parser, RDB
cursor items, one sort per step), consulting a budget between work
items; build_list is begin + one unbudgeted step + end, so the two
cannot drift.  The explore init task steps it under the shared
per-frame I/O window and aborts cleanly on cancel.

Harness samples/menu/explore_build writes a collection of playlists
and databases, checks the index built one item per step hashes the
same as the one built in one go, and that the real task returns
unfinished across checks and installs the same index.
2026-09-30 03:43:17 +02:00
LibretroAdmin f890800f72 content: the load is a chain of main-thread tasks
Each stage of the staged load is a task of the frontend's queue with
RETRO_TASK_FLG_MAIN_THREAD: its handler runs one stage per check and
returns unfinished while the stage waits, its callback pushes the next
stage, and the last one runs the after-work.  The hand-stepped
task_content_load_check and its hook in runloop_iterate go; the
prefetch continuation starts the load from its own callback.

The close, now a handler, asks whether a save state task is inside
the core from inside the gather, where the unthreaded runner has
lifted every task off the queue and a finder sees none: it judged
nothing pending and unloaded the core under a save still writing.
content_save_state_in_progress is now a flag that transitions on the
main thread - set at the push, cleared in the callback - as the
load-state one already is.  The harness lane that closes content
over a save in flight is what found it.
2026-09-30 03:07:28 +02:00
libretroadmin 00e29fe967 Makefile.common: TLS follows networking; every platform makefile checked
Makefile.retrofw asked for HAVE_RETROSSL with no HAVE_NETWORKING:
Makefile.common set HAVE_SSL and the define anyway, retroarch.c called
ssl_socket_set_verify_mode() under HAVE_SSL, and the object holding
it is only built with networking, so the RetroFW link failed. A
makefile without networking now gets no TLS backend at all, and the
call site is gated on networking as its definition is.

tools/platform_makefile_check.sh takes every platform makefile built
on Makefile.common - RetroFW, the OpenDingux family, webOS - without
its toolchain: the graph is generated with stand-ins, the DEFINES it
hands the compiler are read back and checked for consistency (TLS,
SMB and NFS need networking, the clients and the keychain need the
crypto, HAVE_SSL needs an SSL object in the graph), and the frontend
units are compiled by the host compiler under exactly those defines
with implicit declarations as errors. On the old Makefile.retrofw it
fails on the gate; on all eight it passes now. The compile matrix
runs it.
2026-09-30 03:07:16 +02:00
LibretroAdmin 8bd7c775d4 content: the close waits for a state task from the frame loop
A save or load state task still inside the core holds the close:
the task calls the core's serialize, and the close unloads the
library it lives in.  CMD_EVENT_CORE_DEINIT and RARCH_CTL_MAIN_DEINIT
split into a begin half (options, unpause, RAM state, the auto-save),
a pending predicate and a finish half; the commands still wait
between the halves, and the staged load returns to the frame loop
between them, asking each frame, so the frames keep presenting
"Saving state" and core_run's content_closing guard keeps retro_run
out.  The harness core carries a 64 MiB state so a save spans
several checks; the lane closes content over one and counts the
frames presented while the close waits.
2026-09-30 02:46:53 +02:00
libretroadmin f85123b0f5 consoles: the cleanroom crypto and TLS client on 3DS, Vita, Switch and OpenDingux
The last builds carrying another TLS library: the 3DS and Vita
makefiles compiled the bundled mbedtls, the Switch linked devkitPro's,
and the OpenDingux family had the bundled mbedtls (dingux) or a
HAVE_OPENSSL that linked a library no backend used (rg350, retrofw).
All six now select HAVE_CRYPTO, the keychain and HAVE_RETROSSL, as the
desktop, mobile and Windows builds do: the TLS 1.2/1.3 client with
certificate verification and the encrypted keychain, from sources the
tree already has, with nothing to link. The crypto's random source
already had a path for each of them (PS_GenerateRandomBytes,
sceKernelGetRandomNumber, randomGet). The 3DS makefile's note that TLS
could not go through griffin no longer holds, the unity lane proving
it, and the mbedtls include path goes with it.

The compile matrix compiles the crypto and keychain in the 3DS and
Vita shapes and the whole TLS client in the Switch shape, with the SDK
stand-ins the random sources need added to the stubs, and the whole
set with a MIPS cross compiler for the OpenDingux family; the matrix
job installs that compiler. The Vita, 3DS, Switch and RetroFW
workflows build the real things.
2026-09-30 02:38:53 +02:00
libretroadmin 8f602a2e7b crypto: AES-CCM length check as 64 bits; the crypto suite on MIPS
aes_ccm_core checked that the message length fits the L octets the
nonce leaves with len >> (8 * L), and for the 11-octet nonce SMB 3.0.2
uses L is 4: on a 32-bit size_t that is a shift by the word width,
undefined, and on MIPS it comes back unshifted, so every sealed
SMB 3.0.2 message was refused there. The same holds for 32-bit x86
and, by the letter of the language, every 32-bit target. The check is
done as 64 bits.

The crypto suite now also runs under qemu-mipsel: a 32-bit,
strict-alignment target, the one such a Linux host can run, standing
in for the 32-bit ARM and x86 builds. It fails on the old line and
passes on the new; the libretro-common tests job installs the MIPS
cross compiler for it.
2026-09-30 02:38:49 +02:00
LibretroAdmin af2997c9d2 task_queue: RETRO_TASK_FLG_MAIN_THREAD runs a handler on the checking thread
A task pushed with the flag has its handler run by task_queue_check()
- the main thread - under every runner: the threaded worker and the
GCD dispatch pass such tasks by, and the gather runs the due ones once
each within the handler budget, settling them as the worker settles
its own.  Work that must stay on the main thread (calls into the
core, the drivers, the menu) gets a task's lifetime: pushed, found,
cancelled, retired through callback and cleanup.

Sample task_queue_main_thread_test covers both runners: where the
handler runs, one call per check while unfinished, a future 'when',
task_queue_wait(), and plain tasks interleaved with main-thread ones.
tools/platform_stubs/gcd carries a dispatch.h stand-in so the HAVE_GCD
arm syntax-checks with clang -fblocks off Apple.
2026-09-30 02:23:13 +02:00
LibretroAdmin 1b5a5a84ff tools: ci_configure_link_check.sh builds every CI configure set
Reads each distinct ./configure flag set out of .github/workflows,
configures it in a worktree of HEAD and builds: --objects compiles the
frontend units and reports a symbol they call that the default build
defines but this set compiled out (a function inside #ifdef HAVE_MENU
called from runloop.c, the --disable-menu lanes' failure), the
default mode links the whole program.  Sets this host cannot
configure are skipped, cross-host sets listed.
2026-09-30 02:07:06 +02:00
LibretroAdmin cf4ae7e14b content: a load keeps the task queue and applies its settings
The queue was torn down and rebuilt at every load, which joined the
worker thread from a menu action.  retroarch_init_task_queue now
splits: startup builds the queue, and a load onto a running session
calls retroarch_task_queue_configure, which applies Threaded Tasks
(the queue swaps at the next check, between frames), the fast-cores
preference, the slow-handler probe and the budgets.  Every task in
flight, and the worker, survive the load; the load can become a task
of that queue.

Harness lane: under the threaded queue, a thread-local mark set by a
task before a reload is still on the worker after it, and a task in
flight across the reload retires afterwards.
2026-09-30 02:07:02 +02:00
libretroadmin 61e1817b3f lrc_hash: name the SHA-512 sigma macros after the algorithm
BS0 and BS1 are baud-rate constants in the termios headers, which the
Android unity build has in scope by the time lrc_hash.c is reached,
so clang reported the SHA-512 sigma macros as redefinitions. They are
SHA512_BSIG0/BSIG1/SSIG0/SSIG1 now, the names RFC 6234 gives them;
the hash and crypto suites pass unchanged.
2026-09-30 01:50:48 +02:00
LibretroAdmin 613da2ea55 content: task_content_load_check is built without the menu
It sat inside the menu-only block, so a menu-less build's frame loop
called a function that was compiled out.
2026-09-30 01:37:40 +02:00
libretroadmin e8127b7245 griffin: prefix the SMB client's octet helpers; a full-unity lane
The Android and Apple builds failed as soon as they carried the SMB
client: their griffin unit also holds rvorbis.c and iec61937.c, whose
static get32 and put16 collide with net_smb2.c's. The helpers are
now smb_get16/32/64 and smb_put16/32/64, crypto.c's aes_ccm_crypt
becomes aes_ccm_core so it cannot meet libsmb2's either, and the
NFS backend's window size is cast to the uint32_t the client takes,
which Xcode warned about.

The host griffin lanes never saw the clash because none of them
carried a codec beside the network stack. The compile matrix gains a
full-unity lane, griffin with every codec, image reader and subsystem
a host can hold beside the whole network stack, which is the shape
the Android, Apple, webOS and MSVC targets ship; on the old file it
fails on exactly these two names.
2026-09-30 01:32:39 +02:00
libretroadmin 3cbe3fe57f msvc: the cleanroom network stack in the 2019, 2022 and UWP projects
The MinGW builds have carried the stack since configure took it up,
but the MSVC 2019 and 2022 projects, which the x64 and ARM64 jobs
build, had networking with no TLS or SMB client at all, and the UWP
project had libsmb2 and no TLS. All three are griffin builds, so the
change is their preprocessor definitions: HAVE_CRYPTO, the keychain,
HAVE_RETROSSL, HAVE_RETROSMB and HAVE_RETRONFS on every configuration
line that has HAVE_NETWORKING, giving those builds the TLS 1.2/1.3
client with verification, the encrypted keychain, the SMB2/3 client
with signing, sealing, Kerberos and the read-ahead window, and
nfs://. UWP loses the libsmb2 configuration defines and include
directories with it and links bcrypt for the random source its
WinRT path draws on.

The stack is C89 and MSVC-clean by design; every translation unit
compiles at the _WIN32_WINNT=0x0400 floor with MinGW, and the Wine
lanes run the Windows binaries. The projects themselves are the
MSVC and Windows ARM64 workflows' to build.
2026-09-30 01:19:51 +02:00
libretroadmin 7265f686b4 apple, webos: the cleanroom network stack in place of mbedtls and libsmb2
The Apple targets still had griffin pull in the bundled mbedtls for
TLS, and the webOS build both mbedtls and libsmb2. BaseConfig.xcconfig
and Makefile.webos now select HAVE_CRYPTO, the keychain, HAVE_RETROSSL
and HAVE_RETROSMB as the desktop and Android builds do, and add the
NFSv3/v4 client, so macOS, iOS, tvOS and webOS get the same TLS
1.2/1.3 client with verification, the encrypted keychain, the SMB2/3
client with signing, sealing, Kerberos and the read-ahead window, and
nfs://; neither bundled library is compiled in any more. The Apple
MBEDTLS_BUILTIN_FLAG stays a variable a deps.xcconfig may still set:
with HAVE_RETROSSL defined griffin takes the built-in client either
way. The keychain's Darwin path resolves gethostuuid at run time
already, so the old SDK floors hold.

The nine objects of the stack compile through Makefile.webos with its
cortex-a9 softfp ARCHFLAGS on an ARM cross compiler with neither
warning nor error; the Xcode build is the Apple workflows' to run.
2026-09-30 01:12:22 +02:00
libretroadmin db9e44da97 android: the cleanroom network stack in place of mbedtls and libsmb2
The APK still had griffin pull in the bundled mbedtls for TLS and
libsmb2 for SMB. Android.mk now sets HAVE_CRYPTO, HAVE_KEYCHAIN,
HAVE_RETROSSL and HAVE_RETROSMB as the desktop builds do, so the APK
gets the same TLS 1.2/1.3 client with certificate verification, the
encrypted keychain, and the SMB2/3 client with signing, sealing,
Kerberos and the read-ahead window, and neither bundled library is
compiled in any more; the libsmb2 configuration defines and include
paths go with it. The mbedtls path stays selectable by turning
HAVE_RETROSSL off.

The compile matrix gains a lane per translation unit of the stack
compiled by clang for aarch64 with the cross headers standing in for
the NDK's: the same compiler family and target the APK is built
with, which is where the AES/PMULL path takes its +crypto target
attribute and which the gcc lanes never exercised. The matrix job
installs clang and the aarch64 cross toolchain for it.
2026-09-30 01:08:28 +02:00
LibretroAdmin 7e13753f73 content: carry the core's hardware-render request across the driver stage
A hardware core makes its SET_HW_RENDER request during the core
stage, while the previous session's drivers are still up.  Freeing
those drivers ran video_driver_free_hw_context on that request:
the new core's context_destroy before any context_reset, and the
request zeroed, so drivers_init built no context and the core ran
with none.  The driver stage now takes the request out before the
teardown and puts it back for drivers_init, the way a reinit
carries a running core's; the teardown moves into
retroarch_main_init_drivers with it.  Context flags set in that
window (glcore's core-context flag) defer to the next context
instead of landing on the one being freed.

The harness core makes such a request at load; a lane checks it
survives the rebuild with nothing destroyed, and that flags set
mid-load defer even on a context that takes them.
2026-09-30 01:01:00 +02:00
libretroadmin b5de27786d android: build the NFS client and the nfs:// backend
HAVE_RETRONFS in the Android makefile: the built-in NFSv3/v4 client
and its backend, which griffin already carries under HAVE_NFSCLIENT,
need nothing but sockets, so the APK gets nfs:// beside the smb:// it
had. The compile matrix compiles the client, the backend and the
prefetcher through the Android platform stubs.
2026-09-30 00:59:13 +02:00
libretroadmin 86bf1571f6 vfs: background prefetcher for the smb:// and nfs:// backends
The read-ahead window still stalls the reading thread once per
window, for a round trip and a window's worth of transfer, which on a
slow link is a visible pause every megabyte. vfs_prefetch.c is a
prefetcher a stream opened read-only reads through: a thread of its
own fetches aligned windows ahead of the reader on a connection of
its own (a second pool slot, or a private one), so a run of
sequential reads is served from memory and the reader never waits
for the network. The reader's side takes no lock: slots move
EMPTY -> FILLING -> READY under the thread and READY -> EMPTY under
the reader alone, so every write the thread makes to a slot is
ordered after the reader's CAS and every read the reader makes after
the thread's release; a seek out of the fetched range retargets the
thread by generation, a miss retargets it when it has fallen behind
rather than let it fetch what the reader pulls itself, and the thread
parks on a retro_eventcount when it has nothing to do. Windows are
the client's read-ahead size, two of them. On a miss the stream reads
the network as before, so without HAVE_THREADS, where the prefetcher
is stubs, nothing changes.

The nfs:// close now sends the NFSv4 CLOSE on a connection, which the
connectionless close skipped. The one object is built whenever a
network backend is, in griffin and the sample Makefiles too. A stress
test of random reads and seeks with every octet checked runs clean
under TSan and the address sanitizers, and the three server matrices
pass under TSan with the prefetcher active in the VFS lanes.
2026-09-30 00:59:09 +02:00
LibretroAdmin 587f86ea89 samples/tasks: content_load harness for the staged load
Links the shipping objects and drives the menu's load entry point on
a reload of the harness core, hooking the driver's frame() to count
what is presented.  Lanes: a frame drawn with the old core down and
another on the old drivers once the new core is up (a synchronous
load presents nothing between and fails), a second request refused
while one is in flight, a reinit deferred to the load's own driver
stage, the dummy-core fallback, and Close Content's reload landing
after the dummy session rather than in the middle of it.

Run by hand against a non-Qt build:
  ./configure --disable-qt && make
  make -C samples/tasks/content_load check
2026-09-30 00:42:52 +02:00
LibretroAdmin 2db2e86ba7 content: load content as a staged job the frame loop advances
The menu load ran the whole close-core/init-core/init-drivers cycle
inside one runloop_iterate, so the window went dark - or dropped to
the compositor on webOS - for the length of the read.  content_load
is now a job of three stages the frame loop steps through one per
frame: the old core closes with the drivers kept up, the new core
comes up behind them, then one driver reinit follows.  Between stages
the loop presents the cached frame under the menu, so the window
keeps drawing.

retroarch_main_init splits into a core phase and a driver phase for
this: the drivers are found in the driver phase, after the previous
session's are freed, so both sessions never hold a driver at once.
Core deinit keeps the drivers while switching, a reinit the core asks
for mid-load defers to the driver stage, and the pending menu actions
(config replace, close, core reload) wait for the frame after.
Startup and the CLI still load in one go, before any frame runs.
2026-09-30 00:42:48 +02:00
LibretroAdmin 4c61f9fede samples: frontend-overhead harness - heap on the frame path, and microseconds per frame
The threaded-video harness already counts heap calls over the frame
path, but its core pushes no audio, so the sample-batch callback -
where the fold and remap staging used to grow on the callback - was
never under that gate. Nothing in the tree measures what the frame
loop itself costs, either; the frame-path audit had to build that by
hand to learn the answer (about 5 us per iterate with an empty core,
on the null drivers).

samples/runloop/frontend_overhead links the shipping release objects
with main() replaced, the way the threaded-video harness does, boots
on the null drivers, and loads a core whose retro_run() does the
least a real one does: one 320x240 RGB565 frame, one 735-frame stereo
batch, one input poll and read. Two lanes:

  - heap: zero general-purpose heap calls over 300 frames of iterate
    + video frame + audio batch, counted by glibc interposition. A
    single malloc per frame in the core fails it with the count
    (checked: 300 over 300).

  - cost: microseconds per frame under fast-forward, engaged the way
    the hotkey does it so the pacer sees real facts. Printed so a
    trend is visible in the log; tripwire at 1000 us, loose on
    purpose, to catch a blocking call or a sleep landing on the frame
    path rather than to tune. 3-5 us here.

Linux-frontend-overhead.yml runs it on a default non-Qt release build
- no sanitizer, since one would replace malloc and be counted.
2026-09-29 23:49:34 +02:00
libretroadmin 841cbde3f4 local_ad_dc_test.sh: the skip and failure reports survive set -e
The script runs under set -e, and the oracle was called as a bare
statement: when Samba's own client failed on the runner, the script
ended on that line, exit 1 and not a word, instead of skipping with
the report. The oracle is now called in an if, and the report is
printed by a helper that turns errexit off, so a log that does not
exist yet cannot cut it short either. Both paths were forced locally
and print what they should.
2026-09-29 23:39:50 +02:00
LibretroAdmin d1a30ecab1 runloop: cheevos on a frame_work bit, presence on the 10 Hz cadence, setter contract
Three follow-ups on the product mask.

Cheevos was the product left on a settings bool: both rcheevos_test()
sites in the iterate read settings->bools.cheevos_enable and called
into cheevos.c every frame, whether or not anything was attached.
Put it on RUNLOOP_WORK_CHEEVOS with an honest lifetime: rcheevos_load()
raises it once it has passed the enable / core-support / account gates
and committed to loading content, rcheevos_unload() drops it. Between
those the tick runs exactly as before (the bg-thread mailbox, then
rc_client_do_frame or rc_client_idle). One visible change: flipping
cheevos_enable off mid-content no longer silently stops the tick,
which previously left a game attached but untested until the next
load; the attach/detach now follows content, as rcheevos already
assumes.

presence_update(PRESENCE_GAME) ran every frame while the bit was on.
"In a game" is a state, not a per-frame event, and its sinks are
Discord (whose pump is already 10 Hz) and Steam. Add
presence_poll(presence, now_us), a 100 ms throttle on the frame's
existing clock, and call that from the iterate. No edge is lost:
each sink compares against its own last status, so a change between
polls is written at the next one.

Document the threading contract on runloop_frame_work_set(): frame_work
is a plain word the iterate reads without an acquire, so every setter
must be on the main thread. All current callers are (init/deinit,
commands, setting handlers); a worker must post a command instead.
2026-09-29 23:34:55 +02:00
libretroadmin 265f7b1f1b smb, nfs: read-ahead window and pipelined reads
A game reading a disc image in small pieces paid one network round
trip per read, and over a real link that is the stutter reported
against the SMB backend. Both clients now keep a read-ahead window
per open file: a read smaller than the window is served from it, and
a miss refills the window from the current offset with pipelined
READs, up to eight in flight (SMB within the credits the server has
granted, NFS by RPC xid, matched as they come back in any order), so
a run of small sequential reads costs one round trip per window and
the fill runs near line rate. A short or empty reply ends the
contiguous run there whatever later chunks answered; reads at least
a window long go straight to the caller's buffer; a write, truncate
or seek outside the window invalidates it; the window is allocated
on the first small read and freed with the file. The named pipe the
share enumeration talks to keeps single-shot reads, since a pipe
holds extra reads pending. On the loopback, 2 KiB reads of a 256 MiB
file went from 37 to 347 MiB/s; with latency the gap is the ratio of
window to read size.

The window defaults to 1 MiB and is a setting per client -
smb_client_readahead and nfs_readahead, KiB, 64 to 16384, under the
SMB and NFS client settings - since it is that much memory per open
file. The nfs_test and smb_test samples read their file through a
4 KiB window in small pieces, across its edges, after seeks in and
out of it and at the end, so every server in the matrices exercises
it.
2026-09-29 23:26:49 +02:00
libretroadmin 042ddff59d fuzz: fuzz_smb links net_krb5.c; binaries depend on their sources
net_smb2.c calls into net_krb5.c since the Kerberos session setup,
and the SMB fuzzer's link line did not carry it. The four fuzz
binaries also depended only on net_fuzz.c, so a change in any client
they link left a stale binary in place and a local run passed on it;
they now depend on everything they link.
2026-09-29 23:00:30 +02:00
LibretroAdmin 55e82c3898 extra_capacity: the record lane carves its remap staging the way init does
The push no longer grows the region on the callback, so the lane
checks that it never allocates and that a state without staging drops
the audio while the int16 same-layout bypass still records.
2026-09-29 22:48:48 +02:00
LibretroAdmin f21180fe42 retro_eventcount: include the tree's own coreinit headers on the Wii U
Makefile.wiiu compiles against wiiu/include, not wut, so the event
and time headers live under wiiu/os and the tick conversion is spelled
OSMicroseconds there. tools/wiiu_syntax_check.sh compiles the rthreads
units with that build's include directories and defines.
2026-09-29 22:48:44 +02:00
libretroadmin ff175e0075 local_ad_dc_test.sh: Samba's own client as the oracle, logs on failure
The runner's domain controller provisioned but refused the ticket at
SESSION_SETUP, which passes here; the difference is in the runner's
environment, not visible from one status line. Before the built-in
client is tried, Samba's own smbclient now logs in to the DC with a
ticket from kinit: when it cannot, the DC is not usable there and
the lane skips, printing smbclient's and the DC's log tails; when it
can and the built-in client cannot, that is a real failure, and the
DC's logs are printed with it. The daemon runs with the krb5.conf
its provisioning wrote, and the workflow installs smbclient.
2026-09-29 22:25:55 +02:00
LibretroAdmin 1e77b228a6 retro_eventcount: park on the address arbiter on the 3DS
The 3DS arbiter is the futex shape with a less-than comparison: it
waits while the word is strictly below the value given.  The epoch only
climbs, so waiting while it is below key + 1 is waiting while it still
reads key, and a notify signals every waiter on the word.  INT_MAX has
no successor to compare against; a wait registered there is answered
as a spurious wake, once per wrap of the counter.  The unbounded wait
is a long bound re-armed while the epoch still reads key.

The test harness gains a CTR=1 build of the backend on the host, the
two arbiter wrappers stood in for over futex, and a lane at the epoch's
wrap: a wait registered at INT_MAX comes back, and one registered just
past the wrap is still woken.
2026-09-29 22:17:36 +02:00
LibretroAdmin bdc073cbb9 retro_eventcount: park on a pooled auto-reset event on the Wii U
The pooled backend with an OSEvent in auto-reset mode in place of the
semaphore: a signal that arrives before the wait sets the event and the
wait then returns at once, which is the counted property the protocol
needs, since it signals at most once per park.  The bound goes through
OSWaitEventWithTimeout in timer ticks.
2026-09-29 21:54:25 +02:00
libretroadmin b9ded403e3 CI: the AD DC lane installs samba-vfs-modules
The domain controller's provisioning failed on the runner with
vfs_init_custom failed for acl_xattr: the acl_xattr VFS module its
file server needs is in samba-vfs-modules, which samba only
recommends and --no-install-recommends leaves out. Reproduced by
removing the package. The login wait in local_ad_dc_test.sh also
retries a refused session setup for a while, since the KDC and file
server come up in their own time after the user exists.
2026-09-29 21:54:08 +02:00
LibretroAdmin e69947d832 menu: initialise walk_status in menu_displaylist_parse_playlists
Since the horizontal (driver-owned) list enumerates synchronously,
walk_status is only assigned on the interactive-playlists branch.
Initialise it to MENU_DIRWALK_FAILED so the variable has a defined
value on every path, matching the file browser site.
2026-09-29 21:47:11 +02:00
LibretroAdmin 5b5ee25450 retro_eventcount: park on a pooled lv2 semaphore on the PS3
The Vita's pooled semaphore backend, on the PS3's lv2 semaphore: both
SDKs' spellings, as rthreads.c has them for its mutex and condition
variable, and an lv2 wait's microseconds with zero standing for no
timeout.  Any failure of the wait is answered as a timeout, so the
block comes off the list before its frame goes.
2026-09-29 21:46:18 +02:00
libretroadmin 6bbde7b981 smb: Kerberos realm and KDC settings for the smb:// backend
smb_client_realm and smb_client_kdc join the SMB Client settings
(descriptor rows next to the authentication mode, configuration rows
kept literal as the cloud sync strings are). They reach the backend
through smb_settings and the connection key, and the compat shim's
Kerberos mode is now real: smb2_set_kerberos() hands the realm and
KDC to the client, and a KRB5 request needs a realm and succeeds
only when the session came from a ticket, so a pool trying Kerberos
first records the truth and its NTLMSSP attempt still runs, while an
NTLMSSP request never touches the KDC. Share enumeration takes the
same path. A libsmb2 build reads its realm from krb5.conf as before,
so the setter is HAVE_RETROSMB only.
2026-09-29 21:45:35 +02:00
keycross 4eb993442b Fix horizontal playlist enumeration at menu init 2026-09-29 21:47:47 +02:00
LibretroAdmin 864efd0d52 discrete_multichannel harness: carve the staging regions as init does
The fold staging and the recorder remap staging are carved out of the
arenas at init now, at fixed sizes, and the batch callback no longer
grows them.  The harness stands the state up by hand, as init does it,
and gave the state neither, so every fold answered as having no room
and the lane counted 165 failures.  Both stand-ups carve the two as
init does, and the two checks that watched them grow now watch them
stay.
2026-09-29 21:35:07 +02:00
libretroadmin 3b4a816719 menu: NFS Network Settings opens; SMB and NFS entries only under Network
The NFS Network Settings entry opened an empty "null" browser: none
of the NFS client labels had a row in msg_hash_lbl.h, so the label
its deferred list is pushed with resolved to "null" and the push
found no list. Add the rows and their identifier literals, as the
SMB ones have.

Both entries leave the Quick Menu: they are global settings that
already sit under Settings > Network, and a per-game menu is the
wrong place for a client configuration.

samples/menu/network_settings links the shipping objects with only
main() replaced, resolves the SMB and NFS labels, opens Settings >
Network and both screens through the real dispatch (this reproduced
the empty screen before the fix), and checks the Quick Menu no longer
carries the entries. The menu-harness workflow runs it and keeps
networking enabled in its configure so the screens are compiled in.
2026-09-29 21:20:51 +02:00
libretroadmin 88645f57a4 local_ad_dc_test.sh: provision on a filesystem that takes POSIX ACLs
Provisioning the domain controller sets POSIX ACLs on SYSVOL and
failed in setsysvolacl on the runner, where the temp directory does
not take them. The script now probes for ACL support with setfacl
and provisions in the first place that has it - the temp directory,
the repository's own filesystem, then /var/tmp - and skips with a
clear message when none does. A provisioning failure prints the
log's tail so the reason is visible. The workflow installs acl for
the probe.
2026-09-29 21:12:25 +02:00
LibretroAdmin 7c7ed62350 retro_eventcount: park on a pooled kernel semaphore on the Vita
The semaphore backend kept a semaphore per thread in a thread key,
which the console ports' threads do not have.  The Vita takes the same
backend with the semaphores drawn from a pool instead: a lock-free
stack of kernel semaphores, one taken for the length of a park and put
back on return, which the park protocol allows because a park never
returns with a signal still counted on it.  A pool that has run out
answers a park as a spurious wake after a yield.  The waiter list's
lock bit is now spun for a bounded count before yielding, so a holder
that is not running gets back on when the threads outnumber the cores.

The test harness gains a SEMPOOL=1 build of the pooled variant over
POSIX semaphores, and a crowd lane: more threads than the pool has
semaphores, all parking with a bound against a moving notifier, then a
concurrent probe on the semaphores handed back and forth, which must
expire clean on any semaphore backend.
2026-09-29 21:04:37 +02:00
libretroadmin acef386074 net_smb2: Kerberos authentication through SPNEGO
rsmb_set_kerberos(realm, kdc, port) makes the session setup obtain a
TGT and a cifs/<server> ticket through net_krb5 and send the RFC 4121
context token in a NegTokenInit with the Kerberos mechanism; the
server's NegTokenResp carries its AP-REP, which is verified before the
first sixteen octets of the context key become the SMB session key
(MS-SMB2 3.2.5.3.1) that the per-dialect signing and sealing keys are
derived from, now in one rsmb_derive_session_keys. An unreachable KDC
or a refused ticket falls back to NTLMSSP as before, and when that
fails too the error names both. The KDC defaults to the server, which
is right for a domain controller; service principals are named by
host, so a server given by address goes the NTLMSSP way.
rsmb_used_kerberos() reports which one the session came from.

smb_test takes the realm, KDC and port from SMB_KRB_REALM, SMB_KRB_KDC
and SMB_KRB_PORT, with SMB_KRB_REQUIRE failing a run that did not get
in by ticket. local_ad_dc_test.sh provisions a Samba Active Directory
domain controller on the loopback with NTLM disabled and mandatory
signing, creates a user, and runs the login, a wrong password, an
unknown user, and NTLMSSP alone (refused, so the login was the
ticket); the standalone matrix gains a lane where Kerberos is
configured but no KDC answers, so the fallback is exercised. The
network-clients workflow runs the DC matrix natively, under the
sanitizers and with the Windows tools under Wine, installing the AD DC
packages that samba only recommends.
2026-09-29 20:52:28 +02:00
LibretroAdmin d06baf07af savestate: time the automatic interval from the frame's clock, not time() per frame
With savestate_automatic_interval set, content_save_state_automatic()
called time(NULL) every frame to see whether the interval had passed.
The iterate already has the frame's cpu_features_get_time_usec()
reading in hand; pass that in and keep the last-save stamp in the
same units, so the check is a subtract and compare with no clock call
of its own.
2026-09-29 20:48:27 +02:00
LibretroAdmin 4239619d08 audio: carve the recorder remap staging out of the int16 arena at init
audio_driver_record_push() grew record_remap with realloc() on the
core's sample-batch callback the first time a recording needed a
layout or format remap - the same grow-on-the-callback the fold
buffer had before it was moved into the float arena. Recording is
exactly when a hitch on the audio path is visible in the capture.

Both halves are bounded: the slice is at most AUDIO_CHUNK_SIZE_
NONBLOCKING >> 1 frames, and neither a core's layout nor a recorder's
can declare more than AUDIO_RECORD_REMAP_CHANNELS channels. Carve two
halves of that size out of arena_int16 in audio_driver_init_internal()
next to the rewind region, drop the separate free, and make the size
test in the push a bound check that skips the batch rather than a
grow.
2026-09-29 20:48:23 +02:00
LibretroAdmin cfa7a7f6e0 wdmks: a ring of two fragments, and the geometry run as a regression lane
The refill moves the frontend ring into the loop once per notification,
and the ring was one fragment: supply exactly what a fragment plays, so
a pass that came late left a deficit nothing repaid and every fourth
pass was short for good.  The ring is two fragments now, so a pass can
move the fragment and the deficit.  The margin a resync leaves is
capped at half the loop rather than a quarter, which cut it under the
FIFO on a loop of two small fragments.  The loop's sizing is in one
function, wdmks_rt_geometry, so the harness runs the driver's numbers.

samples/audio/wdmks_loop gains the lane that runs them: for a latency
setting, the loop and ring the driver would make, the position moving
half a loop per notification, the refill once per notification, the
consumer feeding chunks of half the buffer it is told, one pass late by
a fragment and a half.  The loop must never run dry on time, cost at
most one short pull for the late pass, and heal.  It fails on each
geometry that shipped and crackled: a loop four times the setting, a
ring of one fragment, a margin under the FIFO.
2026-09-29 20:37:20 +02:00
LibretroAdmin c2d5b30078 video: vectorisable XRGB2101010 narrow, scratch sized at init instead of on the frame
video_driver_convert_xrgb2101010() is the fallback for a driver that
cannot present a 10-bit source surface, and it ran on the core thread
right before present with two costs it did not need. It grew its
scratch with realloc() the first time a frame arrived - a heap call on
the frame path - and its inner loop extracted each channel to its own
10-bit value before shifting it back into place, a form the compiler
did not vectorise.

Size the scratch in video_driver_init_internal() from the core's
declared max geometry, so the grow in the convert is only reached by
a core that delivers more than it declared. Rewrite the narrow as one
shift and one mask per channel directly into the output position;
output is bit-identical (checked against the old loop over a full
frame of random pixels) and at -O3 the compiler vectorises it. On a
2.1 GHz Xeon at 1920x1080: 736 us -> 631 us, the same as hand-written
SSE2 (604 us) - the loop is memory-bound past this point. For
comparison the 0RGB1555 scaler path, which the audit also flagged, is
7 us at 320x240 and 31 us at 640x480 on the same box, which is why it
is left alone.
2026-09-29 20:35:54 +02:00
LibretroAdmin b04b1a7afe video: format the FPS and memory status text on their update ticks, not every frame
With fps_show on, video_driver_frame() ran snprintf("%6.2f") on the
core thread every frame to print a value that only changes once per
fps_update_interval; with memory_show on it did the same for two
figures that only move once per memory_update_interval. That is
floating-point formatting on the frame path, right before present,
for a string that is identical to the previous frame's almost every
time.

Keep each piece as a static string: the FPS text is reformatted when
last_fps actually changes (a float compare per frame), the memory
text on the interval tick that already refreshes the figures. The
per-frame work is a strlcpy of a short cached string into
status_text. The frame counter is left as it was, since it changes
every frame when shown, and the time text was already cached.
2026-09-29 20:27:18 +02:00
LibretroAdmin 9700decb76 wdmks: two fragments of the held amount, refilled on the notification
The probe that slept 40 ms at init to measure how the position moves
is gone: a driver does not learn about a device by blocking on a
timer, and the refill did not need to pace on one either.  The loop is
two fragments, each the latency setting's worth: one is held ahead of
the hardware, the driver's two notifications a loop land once a
fragment as the hardware leaves it, and the refill wakes on that and
fills what it left.  The hardware's clock paces; the timer is a
watchdog.  The position can be stale by no more than a fragment by
construction, and a pass late by up to a fragment is still inside the
lap, which is what the loop the size of the setting could not give.
2026-09-29 20:12:46 +02:00
LibretroAdmin e39e641802 runloop: put cheats, presence, camera and BSV behind frame_work bits
The post-core_run product block called into four translation units
every frame to find out nothing needed doing: cheat_manager_apply_
retro_cheats() loads a cold global to see there is no cheat list,
presence_update() calls discord_update() and steam_update_presence()
which each compare a status word and return, the camera poll reads
settings_t for camera_allow before the caps check, and
bsv_movie_next_frame() pulled two settings through config_get_ptr()
before checking whether a movie handle exists at all.

Give each an up/down owner and a bit on runloop_state_t::frame_work,
raised and dropped by the product itself through
runloop_frame_work_set():

  RUNLOOP_WORK_CHEATS   - cheat_manager_new/realloc raise it when a
                          list is allocated, cheat_manager_free drops it
  RUNLOOP_WORK_PRESENCE - raised with Discord init and with Steam
                          (mist) init; dropped with whichever of the
                          two is the last compiled-in sink
  RUNLOOP_WORK_CAMERA   - driver_camera_start raises it when the core's
                          start succeeds, driver_camera_stop and the
                          driver teardown drop it. The remaining
                          camera_allow/caps/driver checks stay behind
                          the bit; a camera the core registered but
                          never started is no longer polled.

BSV needs no bit: the movie handles live on input_st, which the frame
already has in cache, so the iterate tests the pointers directly and
bsv_movie_next_frame() checks its handle before touching settings.
2026-09-29 20:11:58 +02:00
LibretroAdmin 34d87d5574 wdmks: hold at least two position steps ahead of a coarse position
A pin with no position register answers PlayOffset from its driver,
and a driver that reports from its fragment interrupts moves it a
fragment at a time - on a loop of two fragments, half the loop.  Audio
held ahead of a position stale by that much is audio the hardware may
already have passed: it plays the previous lap's bytes there, and the
count, which trusts the position, says nothing.  The step is measured
once the pin runs, 40 ms at 200 us, and what is held ahead is floored
at the margin and two steps before the frontend is told what that is.
Where the position moves finely nothing changes.
2026-09-29 20:09:29 +02:00
LibretroAdmin 3df357f12f runloop: gate the Discord pump on a frame-work bit, not a call into discord.c
Even throttled, the iterate reached the Discord pump by calling into
discord.c and loading a cold global to learn that Discord was not in
use. A product that is compiled in but idle should cost the frame one
bit test on a word the loop already has in cache, not a cross-TU call.

Add runloop_state_t::frame_work, an unsigned in the hot front of the
struct next to pace, with one bit per optional product that hooks the
iterate. Discord is the first: retroarch_main_init() raises
RUNLOOP_WORK_DISCORD when CMD_EVENT_DISCORD_INIT succeeds (which it
does not when discord_enable is off) and retroarch_main_quit() drops
it with inited. The iterate tests the bit and only then calls
discord_poll(). The other per-frame products (presence, cheats, BSV,
camera) can move behind the same word one at a time.
2026-09-29 20:07:36 +02:00
LibretroAdmin 4eff24c4e5 wdmks: pace the refill on the timer where the loop outsizes what is held
The notification event fires twice a loop, and the refill waits on it
where the pin gives one.  With the loop decoupled from the latency
setting - four times it - a notification comes every two settings'
worth of audio, and a refill that waits for it has run the held audio
out before it wakes: constant crackle on any pin with an event.  The
event is not taken when the loop is larger than what is held; the
timer path, whose cadence is sized from the held amount, paces instead.

samples/audio/wdmks_loop checks the choice.
2026-09-29 20:05:25 +02:00
LibretroAdmin 03cbe1796f discord: pump the RPC connection at 10 Hz from the runloop, not every frame
runloop_iterate() called Discord_RunCallbacks() and
Discord_UpdateConnection() at the top of every visible frame whenever
Discord was inited. UpdateConnection does a non-blocking read on the
RPC pipe (and a clock read plus reconnect backoff check while it is
not open) - a syscall on the emulation thread, every frame, for
callbacks that arrive a handful of times per session.

Add discord_poll(now_us), which runs both at most every 100 ms using
the frame's existing timestamp, and call that from the iterate
instead. Presence changes are queued by discord_update() and flushed
by the next pump, so nothing is lost; join/spectate callbacks arrive
within 100 ms, which is far inside what Discord itself tolerates. The
shutdown path in retroarch.c still calls Discord_UpdateConnection()
directly so the final ClearPresence goes out unthrottled.
2026-09-29 20:04:22 +02:00
LibretroAdmin a4372343b6 wdmks: hold the latency setting ahead of the hardware in a larger loop
The WaveRT loop was the size of the latency setting, so a 2 ms setting
made a 2 ms loop.  Two things went wrong with that.  A pass of the
refill thread 2.5 ms late was a lap the position could not be followed
across, so the whole loop was cleared and the cursor resynced, an
audible discontinuity for half a millisecond of missed audio.  And the
margin a resync leaves for the codec's FIFO was cut to a quarter of the
loop - 24 frames against a FIFO of 46 - so every resync, the start
included, wrote its first frames into bytes the hardware had already
fetched.

The loop is now four times the setting and 8 ms at the least, and the
setting is what is held ahead of the hardware beyond that margin: the
frontend's chunking, the ring it writes and the latency it is told are
all the setting's worth, a late pass has laps to spare, and the margin
is the FIFO's.

samples/audio/wdmks_loop gains the lane: a short setting on a large
loop fills the margin and the setting's worth and no more, and a pass
three settings late is an overrun on the held audio, not a lost lap.
2026-09-29 20:00:00 +02:00
LibretroAdmin 10c3cc5c58 runahead: clamp the frame count to what the measured step cost fits in the frame
Runahead is N + 1 core steps per visible frame plus a serialize and a
deserialize, and the serialize cost is the core's state size, not
something the frontend controls. For a 1-4 MB state that is nothing;
for a core whose state is tens of MB, or one that runs near the frame
budget on its own, N frames of runahead no longer fit and the result is
dropped frames and stretched audio - worse latency than not running
ahead at all. There was no gate: the requested count ran every frame
regardless of what it cost.

Measure the cost of one core step every frame (the whole of
runahead_run() divided by the steps it made, so the save and load are
folded in), IIR-average it (1/8), and clamp the frame count to what fits
in 70% of the core's frame period. The count climbs back only once the
request fits within 60%, so a core sitting on the boundary does not flip
between N and N - 1 every frame. Samples above 250 ms (a JIT warm-up, a
page-in) are dropped rather than dragging the average. Both instance
paths cost N + 1 steps, so one bound covers both.

A clamp is logged and, unless run_ahead_hide_warnings is set, shown once
on the OSD when it changes; recovery is logged only. The measurement and
the clamped count live in runloop_state_t next to the other runahead
state and reset with it in runahead_clear_variables().
2026-09-29 19:59:44 +02:00
LibretroAdmin bdf10bac35 audio: size the multichannel fold region at init instead of growing it on the batch callback
audio_driver_multi_fold_room() grew multi_fold with realloc() whenever a
batch slice was larger than any seen before. That is a heap operation
on the core's sample-batch callback - the one place in the audio path
that must not allocate.

Every multichannel entry (the discrete and folded paths of both the
int16 and float batch callbacks, and the rewind path) already slices its
batch to at most AUDIO_CHUNK_SIZE_NONBLOCKING >> 1 frames before
folding, so the buffer has a fixed upper bound of 1024 frames. Carve it
out of arena_float in audio_driver_init_internal() alongside the other
scratch regions, and turn the room check into a pure bound test. The
region is freed with the arena, so the separate free() goes too.
2026-09-29 19:59:40 +02:00
LibretroAdmin c45788af89 wdmks: run the refill thread in the Pro Audio class where asked
Time-critical is 15 in the normal class, and a display driver's worker
threads sit above it, which on a machine driving a 4K HDR swapchain is
where a 2.5 ms hole in a thread following a 2 ms loop comes from.  The
refill thread now takes the Pro Audio scheduling class under the same
setting the WASAPI pump takes it, falling back to time-critical where
the class is off, absent or refused; the teardown line names which.
2026-09-29 19:46:19 +02:00
LibretroAdmin 05be6626d1 audio: keep a frame of margin in the pipe where nothing regulates it
With Audio Sync and rate control off the pipe's fill is regulated by
nothing: it is whatever priming put there, and priming filled it to one
publish - which is a frame just after the core hands one over and
nothing just before the next, so a frame late by any amount reached the
device as silence.  The target on that path is two publishes now, a
frame's worth held at the worst phase, which is what a late frame costs.

Two things then kept the margin from lasting.  The trim after an
underrun cut the pipe back to the target without the device's share
priming had added on top, so the first pass after every prime threw
that share away as late audio and the fill settled a device buffer
short; the same figure now serves both.  And a stall that emptied the
pipe left it empty - a core paced to the display never runs ahead to
refill it - so silence found with the pipe short primes it again, the
wait the first pass made, while the device is silent anyway.

samples/audio/pipeline_clocked gains the lane: a 60 ms stall the core
does not catch up from, then three seconds of frames up to 12 ms late,
which must not reach the device.  It retries a run the host held the
process through, which its own wake lateness shows.
2026-09-29 19:46:15 +02:00
LibretroAdmin bb47929c1b audio: say at teardown whether the pipe ever ran short of the core
The driver's count of silence says the device went unfed; it does not
say by whom.  The threaded pipeline's consumer now keeps the other half
of the account - the passes after priming that found the pipe short and
had to wait for the core, the longest such wait, and the least the pipe
held at the start of a pass - and says so once beside the driver's line,
so a session with silence in it names the side that was late.
2026-09-29 19:23:24 +02:00
LibretroAdmin e9fd6fdb9e wdmks: say what the sampling floor means for a loop under 2 ms 2026-09-29 19:23:20 +02:00
LibretroAdmin b018d696a8 playlist: make the pushed entry's strings before moving the list
playlist_push_runtime() shifted every entry up and then filled the new
top one with strdup() results it never checked, so a copy that failed
partway left the list one entry longer with no path or no core on top,
and the next write put that on disk.  The strings are made first now,
and a push that cannot make them leaves the list as it was.

samples/playlist gains a lane that fails the copies at every point.
2026-09-29 19:23:17 +02:00
LibretroAdmin df1a0e3e9f wdmks: run the refill thread time-critical, sample at a quarter loop
The refill thread follows the hardware's position round the mapped
loop, and a pass that lands a loop late is a lap it cannot follow, which
the driver counts as a period of silence and resyncs across.  At a loop
of a few milliseconds that is one ordinary scheduling delay next to a
busy core, and the thread ran at normal priority.  It now raises itself
to time-critical where audio_thread_priority asks, as the WASAPI pump
does under the same setting, and a pin with no notification event is
sampled every quarter of the loop rather than every half, so a sample
late by up to three quarters is still inside the lap.

The thread keeps its own account of how it woke - the worst gap between
two passes and how many were a loop or more - and says so once at
teardown, beside which scheduling it ran under.

samples/audio/wdmks_loop gains a lane that runs the thread on the fake
register under both settings.
2026-09-29 19:01:21 +02:00
LibretroAdmin ec369ffc71 retro_eventcount test: take a last look at the count once the producer is done
The consumer could read the count, find it drained, then see the done
flag a producer set after adding one more item, and leave that item
behind: a fail one run in thousands, on any backend.
2026-09-29 19:01:17 +02:00
LibretroAdmin 7f783ebd0f retro_eventcount test: judge the bound by the held probes, not by burst expiry
Whether a bounded wait ever expires inside a burst depends on the
machine: a loaded runner under a sanitizer wakes every wait before its
bound, and the win32 event tier rounds the bound past the notifier's
period.  The held-notifier probes show the bound is honoured; the lane
now fails only when those all end early and no burst expired either.
2026-09-29 18:34:11 +02:00
LibretroAdmin f8346bed38 retro_eventcount: park on the Horizon address arbiter on Switch
svcWaitForAddress/svcSignalToAddress are the futex shape, so the Switch
build takes the lockless path on firmware 4.0.0 and newer and keeps the
condition variable only on older firmware, decided once per object at
init.

The test harness gains a SWITCH=1 build of the arbiter protocol on the
host, with the two system calls and the firmware query stood in over
futex; check runs it as current firmware and as pre-4.0.0.
2026-09-29 18:01:58 +02:00
LibretroAdmin d21573e7f9 retro_eventcount: keep the PS4 on the condition variable
The PS4 toolchain defines __FreeBSD__, but its libc carries neither the
POSIX clock the semaphore backend's bounded wait needs nor a semaphore
that backend can rely on, so it is kept off that backend the way
rthreads.c and retro_procbarrier.c keep it off the FreeBSD paths.
2026-09-29 17:36:57 +02:00
LibretroAdmin c6ed63f67c retro_eventcount: park on a per-thread semaphore on Darwin and the BSDs
The Windows backend's waiter list - stack blocks on the object, a spin
bit in the head pointer - gains a second sleep: a counting semaphore
of the waiting thread's own, made on its first park and kept in a
thread key for its life. A signal that lands before the wait is
counted rather than lost, the property the Windows ALERT tier has, so
no rendezvous is needed; a park is one wait and a wake one signal per
parked waiter, and nothing on either side takes a mutex. A bounded
wait that expires while a waker has taken its block consumes the
signal in flight, so the count never carries into the thread's next
park. The list is shared with Windows; only the sleep differs.

Darwin spells the semaphore in Mach - semaphore_create, semaphore_wait,
semaphore_timedwait, semaphore_signal - every call of which is in Mac
OS X 10.4's headers, so one binary runs from there up with no runtime
resolution; the BSDs spell it in POSIX. RETRO_EVENTCOUNT_FORCE_SEM
builds the POSIX spelling on any host, and the eventcount test's check
runs it beside the futex and scond backends. A new lane paces a
notifier to the consumer's bound so expiry and wake coincide, and
probes for a carried-over count after each burst with the notifier
held; on the semaphore backend the probe must expire.

The bench and test binaries that had been committed are untracked and
ignored.
2026-09-29 17:32:02 +02:00
LibretroAdmin 622c8649a6 audio_thread_wrapper: park on an eventcount, no lock
The wrapper's lock existed only to give its condition variable
something to sleep under. Both threads park on one retro_eventcount:
the audio thread in its stopped loop and its idle wait, the main thread
for the stop acknowledgement and for init() to return. Every change to
the words they test - alive, stopped, stopped_ack - is a release store
followed by a notify, and the re-check between prepare and commit is
what makes a request that lands in the window visible.

The stop acknowledgement answers the flag it read: block() clears the
ack before it raises the flag, so a thread that finds the ack clear
inside its wait window knows a start and a second stop landed since it
last answered, and answers again before parking. How init() ended, and
who owns the state when the frontend gives up waiting, is one word
moved by compare-and-swap from either side, so exactly one of them
frees it.

audio_thread_loop's ceiling in tools/lock_acquire_budget.ref is 0.
2026-09-29 16:32:28 +02:00
LibretroAdmin 990b294740 video_thread_wrapper: lock-free frame ring and pacing handoff
The frame ring's bookkeeping - the slot the video thread claims next,
how many filled slots wait, whether one is being rendered - is one
atomic word, frame.state, moved by compare-and-swap on both sides: the
push takes back the newest unclaimed frame with a CAS, the video thread
claims and completes with a CAS, so neither can win the same slot. The
ring's waiters park on a per-ring eventcount and the video thread on a
work eventcount, notified by whoever gives it something to do; the
menu-texture drain and idle wait slice their parks on the Apple main
thread so marshalled jobs still run. Slot contents stay ordered by the
publish and the claim.

The presenter's inputs to display pacing - period, next vblank, render
time - travel in the stats seqlock, so the hold reads them as one
snapshot; drain requests, repeat requests, the content period and the
main thread's core time and pacing flag are atomic words; the wrapped
driver's poke table is installed on the video thread through a
blocking command, the thread that reads it every pass. Released
textures ride an mpsc_stack to the next push. thr->lock covers the
command channel and the upload lists only, and video_thread_frame
takes no lock at all: its ceiling in tools/lock_acquire_budget.ref is
0, and video_thread_loop is measured there too.

The threaded_video harness reads the video thread's own fields and
swaps its drivers in through blocking commands on that thread, and
takes the repeat and swap counts in one read.
2026-09-29 09:10:17 +02:00
libretroadmin 98b2dcc478 net_krb5: prefix the DER helpers
The griffin unit includes net_krb5.c next to x509.c and net_smb2.c,
which have static der_peek and der_len of their own with other
signatures, so the build failed on the conflicting types. Every DER
helper, type and tag macro in net_krb5.c now carries a krb5_ / KRB5_
prefix; nothing else changes.
2026-09-29 09:09:23 +02:00
libretroadmin 62efd20b75 test_keychain: tamper with a character that always changes the octets
The tamper case flipped the low bit of the third-last base64
character, and one time in 64 that character was 'A', whose flip
'@' is outside the alphabet; unbase64 does not reject it and reads
it as 'A' again, so the sealed value decoded unchanged, opened, and
the assertion failed at random. Swap the character for a different
alphabet character instead, so the decoded octets always differ and
the AEAD tag always refuses them.
2026-09-29 09:02:45 +02:00
LibretroAdmin f2c0af37d6 libretro-common: add convert_float_to_s32, used by wdmks
Full-scale float to 32-bit integer with a valid-bits mask for
24-in-32, saturating with NaN to 0, in the shape of
convert_float_to_s16: an SSE2 arm, a NEON arm enabled from
cpu_features at init, and a scalar tail. The wdmks driver's write path
converts through it for the wide integer pins.

libretro-common/samples/audio/float_to_s32 checks every arm against a
scalar reference on the edges and at every tail length; it runs
natively for SSE2 and cross-built under qemu-aarch64 for NEON, and is
registered in the samples workflow.
2026-09-29 09:02:33 +02:00
LibretroAdmin 6e8ab65c17 wdmks: take 32- and 24-in-32-bit integer pins, propose float last
A pin whose ranges name only 32-bit PCM - a USB Audio Class 2 DAC as
usbaudio2.sys reports it - had nothing proposed to it and the driver
gave up with no format tried. Both wide integer shapes are proposed
now: the render side converts the frontend's float frames into them
on the write path, the capture side converts them back into float, and
either is the same size as float so nothing about a write or a read
changes. The order is float, 32, 24 in 32, 16. Float is also offered
once more as a last resort to a pin with a 32-bit-wide range and no
float subtype, since such pins have taken it when asked.

samples/audio/wdmks_loop gains a lane for the pin shape and both
conversions.
2026-09-29 08:47:06 +02:00
libretroadmin 91caaca299 net_krb5: the Kerberos client - AS, TGS and the GSS initial token
On the enctype layer, the protocol: a DER writer that closes
constructed values by moving their content up under the header (the
lengths stay minimal, as the KDCs require), a matching reader, and
the three exchanges. krb5_get_tgt sends an AS-REQ without
preauthentication and, when the KDC answers KDC_ERR_PREAUTH_REQUIRED
with ETYPE-INFO2, retries with PA-ENC-TIMESTAMP under the key from
the salt and enctype it named; the KDC's own time in the KRB-ERROR
corrects our clock for everything after. krb5_get_service_ticket
runs the TGS exchange with an AP-REQ of the TGT in PA-TGS-REQ (the
req-body checksum with usage 6, the authenticator with 7, the reply
under the TGT session key with 8). krb5_gss_init_token wraps an
AP-REQ for the service in the RFC 4121 initial context token with the
0x8003 checksum, mutual authentication and a fresh subkey, which is
the mechToken a SPNEGO NegTokenInit carries; krb5_gss_accept_token
takes the AP-REP and settles the context key. All over TCP with the
four-octet length, one connection per exchange, timeouts throughout;
no snprintf, frames under 1.4 KiB.

samples/network/krb5_retro/krb5_test drives the client against a KDC
and, given the service key, proves the ticket in the token is real by
decrypting it with that key (usage 2) and finding the client name
inside. local_kdc_test.sh stands up a MIT krb5kdc on a fixed port with
a realm, a user that requires preauthentication, one that does not,
and a cifs/ service whose key it reads back from its keytab, then
runs both paths, a wrong password against each, and an unknown
service. The network-clients workflow installs krb5-kdc and runs the
matrix natively, under ASan/UBSan/LSan and with the Windows build
under Wine. The crypto and keychain suites now link the socket layer
alongside net_krb5.c.
2026-09-29 08:44:32 +02:00
LibretroAdmin 0298cd093c wdmks: add the microphone driver
The capture side of kernel streaming, as microphone_wdmks: the same
filters enumerated under the capture alias, a pin audio flows out of
opened for reading, mono first and stereo folded down where the pin
captures nothing narrower. WaveCyclic pins are read with
IOCTL_KS_READ_STREAM packets; WaveRT pins through the mapped loop, read
from behind the hardware's position, with lost laps and overruns moving
the read cursor up to the hardware. Every wait is bounded, and the
packet reclaim on close is the render side's.

The WaveRT buffer, register and notification-event requests and the
device scan and pin open take the pin handle and direction, so both
sides share them. samples/audio/wdmks_loop gains lanes for the fold and
the capture loop model.
2026-09-29 08:36:53 +02:00
LibretroAdmin 73d582ca40 wdmks: match formats against each pin range, read the register at its width
A pin's data ranges are kept apart rather than folded into one box, so a
candidate format is proposed only when a single range holds its rate,
width, channel count and sample type together: a pin offering 48 kHz
stereo and 192 kHz 8-channel no longer has 192 kHz stereo asked of it.
Past twelve ranges the rest widen the last kept range of their type.
Pin records move to the heap with the device that owns them.

The WaveRT position register is read at the width the pin reports. A
64-bit register is a byte count, and its low word alone cannot place
the hardware once the count passes 2^32 on a loop that is not a power
of two; a 32-bit build reads the high word twice so it is never torn.

samples/audio/wdmks_loop gains lanes for both.
2026-09-29 08:24:25 +02:00
libretroadmin f61d348b25 net_krb5: swap the CTS blocks through temporaries
The whole-block ciphertext-stealing swap copied one block of the
output buffer over another. The two blocks are adjacent and do not
overlap, but the overlap-copy check keys on any copy within the same
buffer, so take both blocks into temporaries first. The RFC 3962 CTS
vectors pass as before.
2026-09-29 07:56:01 +02:00
LibretroAdmin 2279e1bbaa gfx_widgets: lock-free SPSC ring for pending messages
Every gfx_widgets_msg_queue_push() caller is main-thread code -
runloop_msg_queue_push() defers off-main callers before it gets there
and the task queue's progress push runs from its main-thread gather -
and the consumer is the thread that owns the widgets, so the pending
ring has one producer and one consumer. It is now the retro_spsc
shape: the producer owns msg_queue_tail and the consumer
msg_queue_head, free-running counts indexed by mask, each published
with a release store and read across with an acquire load.
msg_queue_lock is gone.

gfx_widgets_msg_queue_race_test builds the production push and pop
out of gfx_widgets.c and drives them from one producer thread and one
consumer thread, checking order, loss, full-ring refusal and the
consumer handover at deinit, under ThreadSanitizer in CI. The
snapshot-ownership test models a full ring through the cursors.
tools/libretro-common-link-check.sh also scans tests-other/.
2026-09-29 07:53:09 +02:00
libretroadmin 05e14927db crypto: AES inverse cipher, HMAC/PBKDF2-SHA1, Kerberos AES enctypes
The first layer of Kerberos for the built-in SMB client: the AES
enctypes of RFC 3962 (aes128/aes256-cts-hmac-sha1-96), which every
Active Directory domain and MIT/Heimdal realm offers. net_krb5.c
carries n-fold, DK, CBC with ciphertext stealing, string-to-key,
encryption with the confounder and truncated HMAC, checksums and the
PRF, on the cleanroom AES and SHA-1 alone; it is built with
HAVE_RETROSMB and the protocol (AS/TGS/AP exchanges, GSS tokens,
SPNEGO in SESSION_SETUP) goes on top of it next.

CTS needs the inverse cipher, which no mode so far did, so crypto.c
gains aes_decrypt_block on all three paths: the portable one runs the
standard inverse order on the encryption schedule with InvMixColumns
on the state, AES-NI applies aesimc to the round keys per block and
ARMv8 vaesimc likewise, so struct aes_ctx and every existing caller
stay as they are and no decryption schedule is cached. kdf.c gains
HMAC-SHA1 and PBKDF2-HMAC-SHA1 for the enctypes' checksum and
string-to-key, in the shape of the SHA-256 ones.

The crypto suite pins all of it: FIPS 197 C.1-C.3 for the inverse
cipher plus round trips through this build's own path, RFC 2202 and
RFC 6070 for the SHA-1 pair, all seven RFC 3961 A.1 n-fold vectors,
all six RFC 3962 B CTS vectors (encrypt, and decrypt in place), all
RFC 3962 B string-to-key vectors, and encrypt/decrypt round trips
with tampering, wrong usage and short input refused. The suite runs
natively, under qemu-aarch64 on both -march variants and as the
Windows binary under Wine.
2026-09-29 07:38:26 +02:00
LibretroAdmin 52634c32db Link retro_eventcount in every harness that builds rzip_stream.c
rzip_stream.c parks its chunk-writer pool on retro_eventcount, so the
content_prefetch sample and the companion test scripts (core, cocoa,
cocoa_macos, win32), which compile it with HAVE_THREADS, link
rthreads/retro_eventcount.c alongside rthreads.c.

tools/libretro-common-link-check.sh builds every sample Makefile in
the tree that names a given libretro-common unit and lists the
tools/*.sh harnesses that do, so a unit gaining a dependency on
another libretro-common TU is caught at link time across all of
them before delivery.
2026-09-29 07:38:09 +02:00
LibretroAdmin 0ca0d328ad rzip_stream: lockless handshake for the parallel chunk writer
Each pool slot is owned by one worker and touched by exactly two
threads, so the slot status is an acquire/release atomic word that
orders the slot's inputs and outputs, and parking goes through
retro_eventcount: one per slot for its worker, notified only by the
writer, and one for the writer, notified by whichever worker finishes.
A dispatch or a completion wakes exactly the thread waiting for it.
Output stays byte-identical to the serial writer.

rzip_parallel_test builds rzip_stream.c with HAVE_THREADS, drives the
pool at every width (the serial fallback, two, eight, and a count past
RZIP_MAX_THREADS) with the core count wrapped at link time, and
compares each result to the serial file byte for byte, both codecs,
including a stream written in pieces across pool wake-ups and two
hundred short pool lifetimes. It runs in the samples workflow under
ASan/UBSan and under ThreadSanitizer. The other sample Makefiles that
build rzip_stream.c with HAVE_THREADS link retro_eventcount.c.
2026-09-29 07:07:59 +02:00
LibretroAdmin fe9feb9ee8 rzstd: make the predefined-table pointer array a constant
rzstd_encode()'s predefined-table pointer array is a table of address
constants, so it is initialised once as const and concurrent encoders
only ever read it. Clears the ThreadSanitizer report on parallel
Zstandard writes.
2026-09-29 07:07:59 +02:00
libretroadmin 90092e4425 nfs_retro: local_ganesha_test.sh runs from its own directory
The other local_*_test.sh scripts cd to their own directory before
building; this one did not, so invoked from the repository root (as
the workflow does) its bare make hit RetroArch's top-level Makefile
and stopped on the missing config.mk.
2026-09-29 06:27:37 +02:00
libretroadmin 2b9fb492e7 menu: NFS Version entry in the NFS Client settings
nfs_version had a config key but no menu entry, so choosing NFSv4
meant editing retroarch.cfg. Add it to the NFS Client group next to
the ports as a 3..4 range with a sublabel explaining what each
version needs from the server.
2026-09-29 06:09:17 +02:00
LibretroAdmin c28019cc73 task_content: build the .part staging name with strlcpy/strlcat
Avoids the -Wformat-truncation warning from formatting a PATH_MAX_LENGTH
path plus a suffix into a buffer of the same size.
2026-09-29 05:55:01 +02:00
libretroadmin 859132bd9d net_nfs3: NFSv4.0 client behind the same rnfs API, nfs:// version setting
Add NFSv4.0 (RFC 7530) to the built-in NFS client, selected per
connection with rnfs_set_version(c, 4): one COMPOUND per operation
over a single TCP connection to port 2049, no portmapper or MOUNT
service, the export given as the server's pseudo-filesystem path.
SETCLIENTID/CONFIRM establishes the client, OPEN/OPEN_CONFIRM with a
per-connection open-owner and advancing seqid creates files, READ and
WRITE use the anonymous stateid, SETATTR truncates, CREATE/REMOVE/
RENAME (SAVEFH: source dir saved, target dir current) and READDIR
with fattr4 cover the rest. NFS4ERR_GRACE and DELAY are retried on a
fixed bound so a restarting server never blocks a caller forever;
the wait is a bare select so no timer header is pulled in. Large
COMPOUND buffers are heap-allocated so every frame stays under 2 KiB
including where the helpers inline into rnfs_connect. The client
verifier and open-owner are derived without shared state, and the
VFS backend's last-error string now goes through a ring of slots
claimed by an atomic counter, so the pooled threads test is clean
under TSan for both versions. Nothing here depends on the crypto
code, so the file still links without HAVE_CRYPTO.

The nfs:// backend gets nfs_settings.version and RetroArch a
nfs_version config key (default 3, unchanged behaviour). The NFS
samples take the version as a fifth argument, local_ganesha_test.sh
runs the same four lanes as the unfs3 matrix against nfs-ganesha,
and the network-clients workflow runs it natively, under ASan/UBSan/
LSan and as the Windows tools under Wine.
2026-09-29 05:42:13 +02:00
libretroadmin f632672461 x509: decode certificate validity without 32-bit long overflow
der_read_time multiplied the day count by 86400 in long, which is 32
bits on Windows on both architectures, so any notBefore/notAfter past
2038-01-19 wrapped negative and the certificate read as expired. The
leaf-policy test vectors run to 2046 and tripped this in the Wine lane
while the Linux run, with a 64-bit long, passed. Do the arithmetic in
int64_t, and where time_t itself is 32 bits saturate rather than wrap
so a far-future notAfter stays in the future.
2026-09-29 05:42:10 +02:00
libretroadmin 1b32c75460 CI: the network client job installs the wine launcher
The job installed wine64 alone, which on the runner brings the 64-bit
loader but not the wine command the Windows lanes run the tools
through; the unit-test job had always installed wine as well. With
the launcher missing the Wine step's own check skipped cleanly, but
the matrices then ran their MinGW builds through nothing and failed
with command not found. The job installs wine with wine64 and
asserts the launcher is present before the lanes run.
2026-09-29 04:28:45 +02:00
libretroadmin b9e70fca38 crypto suite: leaf policy vectors
The certificate policy the verifier gained from the audit - unknown
critical extensions refused, an extendedKeyUsage without serverAuth
refused, a keyUsage that cannot sign refused - was covered by the
server matrix only, which needs openssl at hand. The crypto suite now
carries an independent P-256 CA and six leaves under it, valid for
twenty years: serverAuth with digitalSignature and an unknown
non-critical extension pass; an unknown critical extension, a
clientAuth-only EKU, a codeSigning-only EKU and a keyUsage without
digitalSignature are refused. Runs natively, on both limb widths,
under qemu-aarch64 and as the Windows build under Wine.
2026-09-29 04:26:48 +02:00
libretroadmin ae200e0ed1 smb:// and nfs:// backends: connection pools without locks
Both backends had one lock around every operation, held across the
network I/O, so a read on one connection blocked every other thread's
access to that backend and a stalled server held up all of them. The
pools are now lock-free. Each slot is a connection and a busy flag; a
caller takes a slot by swapping the flag from 0 to 1, uses the
connection alone, and swaps it back - no waiting anywhere. When every
slot is busy the caller connects privately for its own use and drops
that connection afterwards: slower, but never blocked behind another
thread. The pool itself is published with a CAS by whichever caller
builds it first, and a pool whose slots are still held is left alone
by a settings change (callers connect privately until it is idle).

The two protocols place their handles differently: an SMB file or
directory handle belongs to its session, so an SMB stream holds its
slot from open to close and a listing from opendir to closedir, while
stat and share enumeration borrow one per call; an NFS file handle is
valid on any connection to the export, so an NFS stream holds nothing
and every read borrows a slot. Slots connect on first use, and the
SMB auth-mode probe settles per pool from the first connection.

Both backends are clean under ThreadSanitizer with four threads on a
two-slot pool, which exercises the private-connection path; the SMB
matrix gains that test beside the NFS one. Both pass native, under
ASan and as the Windows builds; without threads the atomics compile
to plain operations and the objects carry no thread references.
2026-09-29 04:18:21 +02:00
libretroadmin d8f72f74d2 CI: the network client lanes get a workflow of their own
The libretro-common test job has a fifteen-minute budget, and the
network lanes that accumulated in it - three server matrices, the
Windows builds under Wine, the sanitizer rerun, the fuzz targets and
building unfs3 from source - now take it past that, so the runner
cancels whichever step is running when the clock runs out; the last
run died in the Cocoa companion harness, which has nothing wrong with
it. The six network steps move to Linux-network-clients.yml with a
budget of their own and only the packages they need; the unit-test
job is back to what it was before the network stack arrived.
2026-09-29 04:18:17 +02:00
libretroadmin 4d4d85b4f2 Network stack: the audit's three P1 findings and three P2s
Trust store and curves: the X.509 trust store is process-wide and
the first handshakes on the updater, achievements and cloud-sync
threads raced to build it - free, reallocate and publish it
field by field. One lock now covers building, freeing and every
chain walk over it, and a load of the source already loaded is the
no-op every later handshake takes. The same test found a second
first-use race in pk.c: the curve constants were computed on first
use under a "every thread writes the same values" argument, which
lets a thread publish the ready flag while another is still writing
a constant a third one reads. The computation runs under a lock,
with an acquire load of the flag on the fast path. Six threads making
their first connections at once are clean under ThreadSanitizer
(seven reports before), and the case is in the matrix.

Certificate policy: an extension marked critical that the verifier
does not understand now makes the certificate unusable, as RFC 5280
requires; extendedKeyUsage is parsed and a leaf whose EKU lacks
serverAuth (client-auth or code-signing only) is refused, as is a
leaf whose keyUsage cannot sign the handshake or that is itself a CA
in a chain. Six matrix cases cover the refusals and the two shapes
that must still pass. The 155-certificate bundle still loads.

Staging: a core without VFS support had its remote content copied to
<cache>/<basename>, which overwrote any file of that name already in
the cache and deleted it on unload. The copy now goes into a private
directory made for the connection, under a part name until complete,
then renamed; file and directory are registered for removal only
once they exist, the directory after its file. Verified with the
real binary: an unrelated <cache>/vfs.bin survives the run.

Also from the audit: the smb:// / nfs:// scheme check read six
octets of any path (string_starts_with now); the built-in TLS
backend's verification mode is an atomic that each connection copies
at creation, like the mbedtls backend already did; and net_http
refuses a source callback claiming more bytes than the buffer it was
handed.
2026-09-29 03:53:25 +02:00
libretroadmin 609f485534 libretro-common: fuzz the network parsers
libFuzzer harnesses under test/fuzz for the four parsers that read
bytes off the network: the X.509 parser (the input as a DER
certificate and as a PEM bundle), and the TLS, SMB and NFS clients'
receive paths - a listener in the harness accepts every connection
the client makes and answers with the fuzzer's input, so the real
socket and parsing code runs against arbitrary bytes. Seed corpora
were recorded off openssl s_server (1.2 and 1.3, RSA and ECDSA), Samba
and unfs3 through a small relay, which is what gets the fuzzer past
the first framing checks. Two minutes per target under ASan and UBSan
found nothing; the libretro-common CI job runs thirty seconds per
target and fails on any artifact. The SMB client gains rsmb_set_port()
for the harness, which is also what a server on a non-standard port
needs.
2026-09-29 03:25:30 +02:00
libretroadmin a8211af1be TLS client: the AES-256-GCM-SHA384 suite for 1.3
The third TLS 1.3 suite runs its whole key schedule over SHA-384:
48-octet secrets, HKDF and HMAC over that hash, the SHA-384 transcript
the client already keeps for 1.2, a 32-octet AES key. The 1.3 code
now takes the hash length from the suite everywhere it had assumed
SHA-256 - the secrets, Derive-Secret, the Finished MACs, the
CertificateVerify content, the ticket PSK, the binder (over the hash
of the suite the ticket was issued under) and KeyUpdate - and kdf.c
gains HKDF over SHA-384, checked against an independent computation
in the crypto suite. Servers restricted to this suite, which
compliance profiles such as CNSA call for, now connect and resume.
Two matrix cases: the suite on an RSA certificate, and on ECDSA with
three rounds resuming.
2026-09-29 03:01:39 +02:00
libretroadmin 76dd734ccb tls_retro matrix: KeyUpdate in both directions
The 1.3 KeyUpdate handling had no test: nothing in the matrix ever
rotated keys mid-connection. tls_keyupdate drives openssl s_server's
console - lines to the client, 'K' for a KeyUpdate with update
requested, 'k' for one without - and exchanges a line with the server
across each: the client must keep reading under the server's new keys
and, on the requested one, rotate its own so the server can read the
reply, which the server's output shows. Both directions pass, native,
under the sanitizers and as the Windows build under Wine.

The bench and this tool no longer connect the socket before handing it
to the client, which connects it itself: a second connect passes on
Linux but Winsock refuses it, which the Wine run caught.
2026-09-29 02:52:42 +02:00
libretroadmin bd9e70eceb TLS client: X25519 key exchange
X25519 is the group most servers prefer and some allow alone; the
client offered P-256 and P-384 only, so a 1.3 server wanting X25519
had to retry, which the client refuses, and a 1.2 server restricted to
it could not connect at all. pk.c gains RFC 7748 X25519 - the
Montgomery ladder over 2^255 - 19 on the field arithmetic the curves
already use, scalar clamping, the all-zero result refused - checked
against the RFC's vectors in the crypto suite on both limb widths.
The ClientHello offers x25519 ahead of the NIST groups and carries an
X25519 share beside the P-256 one, so no 1.3 server needs a retry;
the 1.2 ServerKeyExchange accepts the group too. Four matrix cases
pin each group alone on each version.
2026-09-29 02:41:44 +02:00
libretroadmin a0a3840dc0 Cleanup 2026-09-29 02:33:09 +02:00
libretroadmin 50922acca4 TLS client: 1.3 PSK resumption, and no Nagle on TLS sockets
With 1.3 negotiated everywhere, every connection had gone back to a
full handshake. The client now keeps the resumption secret from each
1.3 handshake, turns the server's NewSessionTicket into a PSK
(Expand-Label over the ticket nonce, the ticket as identity, its
age obfuscated as the server asked) and offers it in the next
ClientHello with psk_dhe_ke and a binder computed over the truncated
hello. When the server selects it the early secret comes from the
PSK and the server's flight is EncryptedExtensions and Finished
alone; when it does not, the handshake runs in full as before. The
cache entry for a host is either a 1.2 session or a 1.3 ticket. Real
hosts resume on the second connection; the matrix gains three cases:
three rounds on AES-GCM, two on ChaCha20, and a server issuing no
tickets, where no round resumes.

Measuring the resumed handshakes with a request and reply attached
showed the 1.2 abbreviated case at 44 ms: the first request went out
while the client's Finished was still unacknowledged and Nagle held
it for the server's delayed ACK. TLS sockets now set TCP_NODELAY
where the platform has it - everything RetroArch does over TLS is
request-then-reply, so there is nothing for Nagle to coalesce -
which puts a resumed 1.2 connection with its first exchange at
0.7 ms and a resumed 1.3 one at 1.6 ms on loopback.
2026-09-29 02:29:33 +02:00
github-actions b429d60a24 Fetch translations from Crowdin 2026-09-29 00:31:04 +00:00
libretroadmin 2361aa9796 CI: the sudo Wine run keeps its prefix where root may create one
Wine refuses to create a prefix inside a directory it does not own,
and $RUNNER_TEMP belongs to the runner user, so the SMB matrix under
sudo could not start its Windows tools. The root run now uses a
prefix under /tmp with root's own HOME, initialised with wineboot
before the first tool runs.
2026-09-29 02:14:28 +02:00
libretroadmin fe4c9f894b TLS client: TLS 1.3
The ClientHello now offers TLS 1.3 ahead of 1.2 - supported_versions,
a P-256 key_share, the two SHA-256 suites (AES-128-GCM and
ChaCha20-Poly1305) and the rsa_pss_rsae signature schemes 1.3
requires - and the ServerHello decides which handshake runs. For 1.3
that is the RFC 8446 1-RTT flow: the HKDF key schedule from the
shared secret and the running transcript, handshake traffic keys for
EncryptedExtensions, the 1.3-shape Certificate (verified through the
same chain code), CertificateVerify over the transcript hash (ECDSA
P-256 / P-384, or RSA-PSS with MGF1 and a hash-length salt, which
pk.c gains and the crypto suite checks against OpenSSL signatures in
both hash sizes plus four refusals), the Finished MACs both ways, then
the application keys. Records take the 1.3 shape: outer type
application_data, the real type inside, iv XOR sequence as the
nonce, the header as AAD; the compatibility ChangeCipherSpec is sent
and tolerated. After the handshake, tickets on the application stream
are dropped (no PSK resumption yet) and a KeyUpdate rotates the
server keys and is answered when asked. A HelloRetryRequest is
refused since every 1.3 server takes P-256; a server offering only a
suite we do not carry gets no downgrade, just no connection.

Offering the PSS schemes lets a 1.2 server sign its
ServerKeyExchange with them too, so the 1.2 verifier takes them as
well. The 1.2 matrix is unchanged and green; nine 1.3 cases join it
(RSA and ECDSA certificates, both suites, a P-384 signature, an
intermediate chain, untrusted CA and wrong host refused, the
unsupported-suite case), all passing native, under the sanitizers and
as the Windows build. Real hosts negotiate 1.3 with it; tls_fetch
now prints the version.
2026-09-29 02:10:48 +02:00
libretroadmin 3b64466dcb smb_retro matrix: take port 445 from a distribution smbd first
The runner's samba package comes up as a service on 445, so the
matrix's own smbd could not bind and every case reached the
distribution server instead: it maps an unknown user to guest (the
unsigned-session failure seen earlier) and has no [share], hence
tree connect refused with STATUS_BAD_NETWORK_NAME. The script now
checks the port before the first case, stops the service and any
other smbd holding it, and fails plainly if it cannot be freed.
Reproduced with a second smbd on 445 and confirmed the guard takes
it over.
2026-09-29 01:55:04 +02:00
libretroadmin 250bedcb87 tls_bench: bulk receive throughput mode
tls_bench host port ca get path fetches a file from an openssl
s_server -WWW and reports the receive rate through the client's
record path. Against a local server: AES-128-GCM 560 MB/s and
AES-256-GCM 552 MB/s, the same server limit openssl s_client reaches
(565 MB/s); ChaCha20-Poly1305 238 MB/s, the software cipher, still
well above any home link. No client change came out of the
measurement.
2026-09-29 01:43:32 +02:00
libretroadmin 601c9de9aa SMB and NFS clients: transfers up to 1 MiB per request
Both clients moved data 64 KiB per request, so a large read was a
round trip per 64 KiB: fine on loopback, the bottleneck on a real
network. The SMB client now does multi-credit I/O on 2.1 and later -
CreditCharge of one credit per 64 KiB, the message id advanced by the
charge, a running credit balance from each reply's grant, requests
clipped to what the server has granted - up to 1 MiB, the negotiated
MaxRead / MaxWrite, or 64 KiB on 2.0.2, which has no multi-credit;
the connection's buffers grow to the negotiated size once the
NEGOTIATE reply has been read. The NFS client asks FSINFO for the
server's rtmax / wtmax after the mount and sizes its transfers and
buffers to that, up to 1 MiB.

On loopback, where round trips are nearly free, SMB reads go from 256
to 343 MB/s and writes from 248 to 320 MB/s against Samba; NFS
against unfs3 from 1.5 to 2.1 GB/s reads and 181 to 414 MB/s writes.
Against a server across a LAN the gain is the sixteen-fold drop in
round trips per megabyte. Both matrices pass native, under ASan
(which caught the NEGOTIATE parser reading its reply after the
buffer had been grown, now done afterwards) and as Windows builds.
2026-09-29 01:28:58 +02:00
libretroadmin b3519a31ce SMB client: do not verify signing on a guest or anonymous session
A guest or anonymous/null session is returned unsigned even when the
server requires signing, so a server that maps failed or empty
credentials to guest answers the final SESSION_SETUP with SUCCESS and
no signature. The client read that as a bad signature and refused the
connection - intermittent in the field, since whether a login lands on
a real or a guest session depends on the server's mapping. The session
setup now reads SessionFlags before checking the response: on
SMB2_SESSION_FLAG_IS_GUEST or IS_NULL it turns signing off for the
session and skips the check, as MS-SMB2 3.2.5.3.1 requires. The samba
matrix gains a guest-mapping server (map to guest, mandatory signing)
that reproduced the failure exactly before the fix.
2026-09-29 01:19:59 +02:00
libretroadmin e28f750752 CI: a Wine prefix of its own for the SMB run under sudo
The SMB matrix runs under sudo for port 445, and Wine refuses to use a
prefix owned by another user, so the Windows build of the SMB tools
failed before connecting. That run now gets a root-owned prefix, and
its clean runs under sudo too so the root-built tools can be removed.
The shim's fixture registration is a macro, which stops the unused
function warning in suites that register none.
2026-09-29 01:06:13 +02:00
libretroadmin 96cffab5ed SMB client: send the NetBIOS prefix and the frame in one write
Every request cost 44 ms on loopback. The four-octet NetBIOS length
prefix went out as its own write and the frame as a second one, and
the frame sat in Nagle's algorithm behind the unacknowledged prefix
until the server's delayed ACK - the same stall the TLS handshake
had, here on every SMB request: each directory listing, stat and read
the menu browser makes. The transmit buffer now keeps four spare
octets ahead of the frame and prefix plus frame leave together;
0.02 ms per request on the same loopback, and the samba matrix that
used to run for minutes takes fifteen seconds.

The SMB and NFS test tools print their per-request latency so a stall
of this shape shows in the CI log. NFS was already a single write.
2026-09-29 00:51:37 +02:00
libretroadmin e28044d649 TLS client: send each handshake flight in one write
A full handshake took 47 ms against a local server and a resumed one
0.1 ms, which no amount of public-key work explains. The client wrote
ClientKeyExchange, ChangeCipherSpec and Finished as three small
records, and the second of them sat in Nagle's algorithm waiting for
the server's delayed ACK of the first: a 40 ms stall on every full
handshake, on every platform, to every server. The flight is now
corked into one buffer and written once, and the resumed flight's
CCS and Finished the same way; the socket keeps its normal options.
Full handshakes drop to 2.9 ms on the same loopback, RSA and ECDSA
suites alike.

samples/network/tls_retro/tls_bench measures full and resumed
handshake time against a server; it is what found this.
2026-09-29 00:40:58 +02:00
libretroadmin fa518115af Run the Windows builds of the network stack under Wine
The MinGW builds of the crypto and keychain suites and of the three
network test tools were only ever linked; now they run. Makefile.test
gains win32-wine-check, which builds the crypto and keychain suites
with the libcheck shim as Windows binaries and runs them under Wine -
RtlGenRandom, the MachineGuid machine identity, the Win32 cpu-feature
dispatch onto AES-NI - and the three server matrices take RUNNER=wine
EXE=.exe so the MinGW tools drive the same local openssl, Samba and
unfs3 over Winsock, the four-thread NFS test on the Win32 thread
backend included. All of it passes; the libretro-common CI job runs
it with wine64 installed.

Two things the run needed: the shim now runs checked fixtures around
each test, as libcheck does, since the keychain suite sets its key
file up in one (nothing ran it, so the suite saw an empty path); and
that suite takes its temp dir from TMP / TEMP before falling back to
/tmp, which a Windows build cannot write.
2026-09-29 00:34:12 +02:00
libretroadmin 6095cd97d9 Network backends: serialise the shared pools and the TLS session cache
The VFS is entered from several threads at once - content loading,
cloud sync, thumbnail and playlist scans - and the smb:// and nfs://
backends hand those threads contexts from a shared pool whose
connection is not safe to use from two of them, with the pool's own
bookkeeping racing on top. ThreadSanitizer reports 72 data races from
four threads on a two-slot pool, and the reads come back corrupted.
Each backend now takes one lock around every entry point (HAVE_THREADS
only; the lock is published once with a CAS so two first callers
cannot each install their own): calls to one server serialise, which
is no loss on a network-bound path. The TLS client's session cache,
the one thing its connections share, is guarded the same way, and the
ClientHello copies the cached id and ticket out under the lock rather
than reading the slot while it is written.

The unfs3 matrix gains a four-thread test through the nfs:// VFS
backend, clean under ThreadSanitizer with the locks and failing without
them. Without threads every lock compiles to nothing; the three units
build with the tree's --disable-threads defines and carry no rthreads
symbol.
2026-09-29 00:34:09 +02:00
libretroadmin 15ebb6c825 tls_retro matrix: certificate chains with an intermediate CA
Every case so far had the leaf signed by the trust root directly; real
servers send leaf plus one or two intermediates and the client trusts
only the root. Four cases cover that: an RSA intermediate, a P-384
intermediate under the RSA root, the intermediate itself as the trust
anchor, and a server that omits the intermediate, which must be
refused. The intermediate is handed to s_server with -cert_chain, as
its -cert takes only the first certificate of a file.
2026-09-29 00:16:53 +02:00
libretroadmin 8e781c0806 lrc_hash: hex output without snprintf in sha256_hash()
The hex loop used snprintf, which the strict C89 lane rejects since
-ansi does not declare it; a nibble table writes the same 64 lowercase
digits and the terminator. Verified against the FIPS 180-4 "abc"
vector; the one caller (cloud sync's S3 signing) already sizes for it.
2026-09-29 00:12:36 +02:00
libretroadmin 0ff5ce4383 Fix three CI failures from the NFS menu, sanitizer and legacy lanes
The NFS menu declarations - the MENU_LABEL rows, the deferred-list
enum, the display-list and action enums, the settings-list enum and
the deferred-push / ok-list cases - had been added inside the
neighbouring HAVE_SMBCLIENT blocks, and the netplay action def file
that carries the NFS settings entry was only consumed under
HAVE_SMBCLIENT. Every shape with NFS and without SMB failed to compile,
which is what the compile matrix's NFS-with-menu gate and the griffin
HAVE_RETRONFS shape build. Each declaration now sits in its own
HAVE_NFSCLIENT block and the def-file regions take either client.

The sanitizer lane builds unfs3 from source with the sanitizer CFLAGS
still exported, and a server built that way does not come up, so the
NFS run could not mount. The script now builds it with the flags
unset, and the test tool frees its context on a failed check so a
real failure is not buried under a leak report.

The legacy-backend job rejected x509_verify_chain in the binary; it is
part of HAVE_CRYPTO, which the keychain needs whichever TLS backend is
selected. The check now asserts on client symbols only.
2026-09-28 23:50:54 +02:00
libretroadmin e76051ba58 Network clients: sanitizer sweep, SHA-2 stream empty updates
The TLS, SMB and NFS server matrices were run with the clients built
under ASan, UBSan and LeakSanitizer. UBSan caught one thing: the
SHA-384 PRF path hands the HMAC an empty second seed as a NULL pointer
with length zero, and the SHA-256 and SHA-512 stream updates passed
that straight to memcpy, which is undefined even for zero bytes. Both
updates now return at once on an empty length. Everything else -
14 TLS cases, 8 SMB dialect and sealing cases with the VFS on top,
the NFS run with the VFS on top - came through clean, no leaks.

The libretro-common CI job now runs the three matrices a second time
under the sanitizers, so any memory error a real server can drive is
caught there rather than on a user's machine.
2026-09-28 23:18:29 +02:00
libretroadmin ef4c9714eb CI: build with the legacy TLS and SMB backends selected
The built-in clients are now the defaults, so nothing in CI builds the
mbedtls backend or the bundled libsmb2 any more - the configuration a
packager pinning a system library, or a platform that keeps one of
them, actually uses. This job configures with every built-in client
off (system mbedtls, bundled libsmb2, no NFS), builds to the link, and
checks the binary carries the legacy backends and none of the built-in
client symbols. Locally, full builds with --disable-networking and
with --disable-retrossl both link, and the flag-sensitive units
(configuration, retroarch, task_content, the VFS, the menu units,
cloud sync) compile under --disable-crypto, --disable-networking and
the clients disabled one by one.
2026-09-28 22:39:16 +02:00
libretroadmin 407a9d6ac1 TLS client: session resumption by id and by ticket
Every reconnect to the buildbot or the achievements server was a full
handshake: certificate chain, two signature verifications and an ECDHE
exchange. The client now keeps a small per-process cache (eight
hosts, six hours, never on disk) of the master secret, suite, server
session id and RFC 5077 ticket from each completed handshake. A
ClientHello offers the cached id and the ticket in the session_ticket
extension (empty when there is none, so the server knows tickets are
taken); when the ServerHello echoes the id, the abbreviated flow runs
- key expansion from the cached secret, the server's ticket, CCS and
Finished, then ours - and skips everything the cache already vouched
for. A NewSessionTicket after a full handshake is stored, so servers
that only do tickets resume as well. ssl_socket_retro_was_resumed()
reports which happened, for tests.

The fetch tool takes a round count and requires every round after the
first to resume; the server matrix runs three rounds with tickets,
three with -no_ticket (session ids), and two on the ECDSA + ChaCha20
suite. Against a real host three rounds went full, resumed, resumed.
2026-09-28 22:01:18 +02:00
libretroadmin 7d579dfa3d Stage smb:// and nfs:// content for cores that read paths themselves
A core without VFS support hands the content path to its own fopen(),
where an smb:// or nfs:// URL means nothing. task_content.c now copies
such content into the cache directory first and passes the copy on,
registering it as a temporary so it goes when the content is unloaded
- the same shape the UWP fallback already takes, and the staging rule
the plan carries over from PR #19269. A core that reads through the
VFS still streams the original, which is what keeps large disc images
usable; compressed content was extracted through the VFS earlier and
is local by then. Without a cache directory the load fails with a
message saying to set one. Verified end to end: a need_fullpath core
without VFS support loads nfs:// content through the real binary and
opens the staged copy.

Two things that run found. Both network clients took their settings
in rarch_main() before the configuration file was read: the string
fields point at the live buffers so those followed, but the numeric
ones (timeouts, pool sizes, the NFS ports) were copied as zero.
retroarch_smb_init() and retroarch_nfs_init() run again right after
config_load(). And with no export configured, the nfs:// backend took
only the first path component as the export; it now mounts prefixes
shortest first until the server accepts one and remembers the split
per connection, so nfs://server/srv/nfs/roms/file works for an export
of /srv/nfs/roms.
2026-09-28 21:57:55 +02:00
libretroadmin 85f7a64bed Menu: NFS Network Settings and Browse NFS Export
Settings > Network gains an NFS Network Settings group (server,
export, sub directory, timeout, maximum connections, NFS and MOUNT
ports) and Load Content gains Browse NFS Export, which opens the
configured server as a content list the way Browse SMB Share does.
The group entry is an S_ACTION row in the netplay action definitions
so its label string is generated; the settings rows themselves are
descriptor holdouts in menu_setting.c (free-input strings, plain uint
ranges), and the list, ok, deferred-push, sublabel and title handlers
mirror the SMB ones under HAVE_NFSCLIENT. Every deliverable of the
network stack plan now has its surface in the tree. The compile
matrix gains the NFS + menu shape over the touched units.
2026-09-28 21:49:21 +02:00
libretroadmin 75afa24129 Configuration keys for the nfs:// client
nfs_server, nfs_export and nfs_subdir, plus nfs_timeout,
nfs_num_contexts and the nfs_port / nfs_mount_port pair for servers
without a portmapper, under HAVE_NFSCLIENT in the settings struct,
config.def.h and configuration.c, handed to the VFS backend from
retroarch.c beside the SMB settings at startup and released at exit.
An nfs://server/path URL then resolves against the configured export;
nfs://server/export/path keeps working with nothing configured. The
keys round-trip through retroarch.cfg. Menu rows for them follow; the
compile matrix gains configuration.c with NFS and no menu.
2026-09-28 21:36:48 +02:00
libretroadmin e4fb323d86 Add nfs:// to the VFS over the built-in NFSv3 client
vfs/vfs_implementation_nfs.c implements the nfs:// backend contract
carried over from PR #19269 (nfs_init_cfg, retro_vfs_*_nfs, the
nfs_dir_handle and nfs_dirent shapes, probe and last-error) on
net_nfs3.c, with a small pool of connections keyed by export so a
stream keeps the one it opened on. A URL carries everything a mount
needs - nfs://server/export/path - so it works with no settings at
all; the settings, when present, supply a default export and subdir
(nfs://server/path is then relative to them), the timeout, the pool
size and fixed NFS / MOUNT ports for servers without a portmapper.
vfs_implementation.c dispatches the scheme everywhere it dispatches
smb://: open / read / write / seek / tell / flush / close, stat,
opendir / readdir / dirent name, type and stat, closedir, and the
copy path's native-or-not decision.

HAVE_RETRONFS turns it on (configure: yes whenever networking is on;
the console Makefiles do not set it) and defines HAVE_NFSCLIENT for
the surface, replacing the vendored-libnfs approach of #19269. The
unfs3 matrix now also drives the real VFS - filestream and
retro_dirent over nfs:// URLs - and the compile matrix gains the
griffin HAVE_RETRONFS shape. A full RetroArch build links with it on.
Menu and configuration rows for the settings come next.
2026-09-28 21:27:47 +02:00
libretroadmin ed1043377f libretro-common: add the built-in NFSv3 client, net_nfs3.c
An NFSv3 client (RFC 1813) over ONC RPC on TCP with AUTH_UNIX, one
translation unit with a small synchronous API (rnfs_*): the portmapper
for the MOUNT and NFS ports unless given, MOUNT v3 for the export's
root handle, then paths resolved component by component with LOOKUP
and GETATTR, READ and WRITE (FILE_SYNC) chunked to 64 KiB, CREATE,
SETATTR for truncation, REMOVE, MKDIR, RENAME, and directory listing
with READDIRPLUS, falling back to READDIR plus a LOOKUP per entry on a
server that answers NOTSUPP (unfs3, some embedded ones). The XDR is
built and unpacked by hand. The socket is non-blocking and every send
and receive is bounded by the context timeout; reply records may
arrive fragmented. This is the transport for the vfs_implementation_nfs
contract carried over from PR #19269; hooking it in comes next.

samples/network/nfs_retro holds a test tool and local_unfs3_test.sh,
which builds unfs3 from its repository when no unfsd is installed and
runs the tool against it on fixed ports: mount, a 200 KB write and
read-back, seeks, stat, mkdir, rename, truncate, listing, unlink, and
a refused export. The libretro-common CI job runs it.
2026-09-28 20:46:44 +02:00
libretroadmin f3b5d1039d SMB client: share enumeration over srvsvc, bounded I/O
rsmb_enum_shares() runs NetrShareEnum level 1 through the srvsvc named
pipe on an IPC$ connection: a DCERPC bind (srvsvc 3.0 over NDR 2.0)
and one request, both built and unpacked by hand since this is the one
RPC the frontend makes. The VFS backend's share listing for smb://server
is back on the built-in client with the same filtering as before
(disk shares only, hidden, temporary and $-suffixed ones left out), so
the share browser works again.

The connection socket is now non-blocking and every send and receive
goes through the timeout variants bounded by the context timeout: a
request to a server that stops answering fails after that long instead
of holding the caller forever, which the frame loop cannot afford.

The samba matrix enumerates shares at the API level (the share in use
listed as a disk share, IPC$ typed as IPC) and through the VFS with a
hidden share in the config, and its cleanup now also stops the
samba-dcerpcd that smbd spawns per config - a stale one answers the
srvsvc pipe for nobody. The compile matrix gains configuration.c with
SMB and no menu, and with neither.
2026-09-28 20:39:14 +02:00
libretroadmin 53de1ffe59 configuration: SMB show-settings row only with a menu
settings_def_settings_show_smb.h reads DEFAULT_SETTINGS_SHOW_SMB_CLIENT,
which config.def.h defines inside HAVE_MENU, but configuration.c was
including the row under HAVE_SMBCLIENT alone. That was latent while
SMB support needed a libsmb2 the menu-less jobs did not have; with the
built-in client on by default it broke every --disable-menu build.
The five includes are now under HAVE_MENU as well, like the other
settings-visibility rows.
2026-09-28 20:33:13 +02:00
LibretroAdmin b6d67bb049 video: a frame due every other vblank is not a queued one
The threaded video's display pacing judges a frame queued behind
another - and drains a period for it - when its push-to-present
latency reaches a period and a half of the display. On a display faster
than the content that period is the wrong one: a 60 fps frame on a
120 Hz panel is due every other vblank, and a render of a few
milliseconds plus the wait to its vblank reached 12.5 ms on its own, so
every longer frame drained a queue that was never there and the core
lost a content period each time. The judgement is now against the
content's period, which the hold already computes, or the display's
where that is longer. On a display at the content's rate nothing
changes.

samples/gfx/threaded_video gains the fast-display lane: a presenter on
a 120 Hz grid with the core at 60 fps and the render alternating 2 and
7 ms must show no drains and keep the core at 60, where the display's
period drained 29 of 120 frames and ran it at 48.
2026-09-28 20:23:11 +02:00
libretroadmin 7e4ca8f2c6 Use the built-in SMB2/3 client for smb:// and SMB cloud sync
HAVE_RETROSMB puts net_smb2.c behind the VFS SMB backend and the SMB
cloud-sync driver in place of libsmb2. configure turns it on whenever
HAVE_CRYPTO is on and no libsmb2 was asked for, so main builds stop
carrying the bundled library; --enable-smbclient / --enable-libsmb
still select a libsmb2. Both consumers keep their libsmb2-shaped source
through net/net_smb2_compat.h, which presents the calls they make -
context and credential setup, connect, open / read / write / lseek /
ftruncate / close, stat / unlink / mkdir / rename, opendir / readdir /
closedir, echo, the I/O size limits, error text - over the built-in
client with the same return conventions (lseek's -EINVAL, libsmb2's
type and stat structs). net_smb2.c gains unlink, mkdir, rename (SET_INFO
FileRenameInformation) and ftruncate (FileEndOfFileInformation) for
them. A Kerberos request fails at connect so the backend's NTLMSSP
fallback runs; share enumeration (SRVSVC over IPC$) is not carried yet,
so browsing starts at a named share.

The samba matrix now also drives the real vfs_implementation_smb.c
(stat, open, seek, read, tell, opendir / readdir) over each dialect,
and the compile matrix gains the griffin HAVE_RETROSMB shape. A full
RetroArch build links with the client selected and no libsmb2 symbol.
2026-09-28 20:17:59 +02:00
libretroadmin 780a209406 tls_retro / smb_retro matrices: do not fail on the EXIT trap
Both scripts run under set -e and end with a trap that kills the
server they started; when it has already gone, kill (or pkill)
returns non-zero, the trap aborts on it and the script exits 1 after
every case has passed - which is how the libretro-common CI job failed
on a green TLS matrix. The kill in each trap is now allowed to fail.
2026-09-28 20:03:35 +02:00
libretroadmin f87f1a9eae libretro-common: add the built-in SMB2/3 client, net_smb2.c
An SMB2/3 client on the cleanroom crypto, one translation unit with a
small synchronous API (rsmb_*): connect to \\server\share over TCP
445, open / read / write / seek / close, stat, opendir / readdir /
closedir, echo. NEGOTIATE offers 2.0.2 through 3.1.1 with the 3.1.1
preauth-integrity (SHA-512 chain) and encryption-capability contexts,
GCM preferred over CCM; SESSION_SETUP carries NTLMv2 inside SPNEGO
(NEGOTIATE, CHALLENGE, AUTHENTICATE with the target info echoed and a
MIC over all three; no NTLMSSP integrity flags, so SPNEGO needs no
mechListMIC leg and no RC4); keys come from the session key through
the SP 800-108 KDF on 3.x with the preauth hash as context on 3.1.1.
Every request after setup is signed - HMAC-SHA256 on 2.x, AES-CMAC on
3.x - and replies are verified; when the server or the share requires
it every message is sealed in a TRANSFORM_HEADER with AES-CCM (3.0,
3.0.2) or AES-GCM (3.1.1). Reads and writes are chunked to the
negotiated limits (64 KiB cap); STATUS_PENDING interim replies are
skipped. Two 70 KiB buffers per context on the heap; the largest
frame is under a kilobyte.

samples/network/smb_retro holds a test tool and local_samba_test.sh,
which runs smbd on the loopback through a matrix: every dialect with
signing mandatory, CCM and GCM sealing required, and a refused logon,
each writing, reading back, seeking, stat-ing and listing a 200 KB
file. The libretro-common CI job installs samba and runs it. Hooking
the client up behind vfs_implementation_smb.h in place of libsmb2 is
the next step.
2026-09-28 19:58:58 +02:00
libretroadmin 4519d80d39 libretro-common: SMB3 and NTLMv2 crypto for the SMB client
The pieces the cleanroom SMB2/3 client needs that the stack did not
have: AES-CCM (RFC 3610, any nonce 7-13 and tag 4-16, the 11/16 shape
SMB 3.0 and 3.0.2 seal with) in crypto.c beside GCM and CMAC; and in
kdf.c the SP 800-108 counter-mode KDF over HMAC-SHA256 that SMB 3.x
derives its signing, sealing and application keys with, MD4 and
HMAC-MD5 for NTLMv2, and ntlm_ntowf_v2() computing the NTLMv2 key from
password, user and domain with the UTF-16LE and upper-casing rules of
MS-NLMP. MD4 and HMAC-MD5 exist for that one protocol and say so.

Every one is checked against published vectors in the suite: RFC 3610
packet vector 1 plus the SMB3 CCM shape, RFC 1320 A.5, RFC 2202, a
KBKDF vector cross-checked against a reference implementation, and the
NTOWFv2 vector from MS-NLMP 4.2.4 including the case rules. kdf.c now
uses utils/md5.c, so the test, bench and sample link lists carry it.
2026-09-28 19:22:01 +02:00
libretroadmin 064aa5fc08 tls_retro sample: link Winsock and fopen_utf8 on Windows
The fetch tool's Makefile now adds -lws2_32 -liphlpapi when built with
MinGW (MSYS2 or a cross gcc) and compiles compat/fopen_utf8.c, which
vfs_implementation.c calls on Windows; without them the link failed on
every net_socket.c and net_compat.c symbol. The libretro-common CI job
cross-links the tool with x86_64-w64-mingw32-gcc after running the
local server matrix, so the Windows link line stays checked on Linux.
2026-09-28 19:15:09 +02:00
libretroadmin 208e997863 TLS client: AES-256-GCM-SHA384 suites, CI lanes
The built-in client now also offers ECDHE-ECDSA and ECDHE-RSA with
AES-256-GCM, whose PRF and transcript run on SHA-384: kdf.c gains
HMAC-SHA384 (RFC 4231 vector in the suite), tls_prf() and the Finished
computation take the hash from the negotiated suite, and both digests
run over the transcript until the ServerHello picks one. That is the
full set of AEAD suites a TLS 1.2 server can require, so a server
configured for AES-256 only no longer turns us away.

The libretro-common CI job runs samples/network/tls_retro/
local_server_test.sh - the openssl s_server matrix over every suite,
both curves, RSA / P-256 / P-384 certificates and the verify-mode
refusals - and the compile matrix gains the griffin HAVE_RETROSSL
shape. A full RetroArch build links with the client selected and no
mbedtls or BearSSL symbol in the binary; the matrix also passes with
the fetch tool built under ASan and UBSan.
2026-09-28 19:11:35 +02:00
libretroadmin 73a676b4eb libretro-common: add the built-in TLS 1.2 client, net_socket_ssl_retro.c
A TLS 1.2 client behind the existing ssl_socket_* API, on the
cleanroom crypto: ECDHE over P-256 or P-384, RSA or ECDSA server
authentication through crypto/x509 against net/cacert.h, AES-128-GCM
or ChaCha20-Poly1305 records, SHA-256 transcript and PRF, SNI,
signature_algorithms and renegotiation_info sent; no resumption,
renegotiation or client certificates. The handshake runs inside
ssl_socket_connect() as the other backends' do; application reads
hand back one decrypted record at a time, and the verify-mode and
logging hooks behave as with mbedtls (required fails closed, optional
logs and continues, disabled logs once). Handshake messages accumulate
in one 64 KiB buffer that is not compacted until the handshake ends,
since the parsed Certificate points into it. x509.c exposes its ECDSA
signature decoder for the ServerKeyExchange.

HAVE_RETROSSL selects it: configure prefers it whenever HAVE_CRYPTO is
on, so main builds stop needing mbedtls or BearSSL, both of which stay
selectable under their own flags. samples/network/tls_retro carries a
fetch tool and local_server_test.sh, which runs an openssl s_server
matrix: every suite, both curves, RSA / P-256 / P-384 certificates
under a CA made on the spot, and the refusals that must happen (wrong
hostname, untrusted CA) with verify required, optional and disabled.
The tool also completes real HTTPS round trips against public hosts.
2026-09-28 18:50:45 +02:00
libretroadmin 26c4b52af8 libretro-common: add P-384 to the cleanroom public-key code
pk.c's elliptic-curve code now runs over a curve descriptor - prime,
order, b, generator, word count, and the Montgomery constants derived
from them on first use - with P-256 and P-384 as the two instances;
keygen, ECDH and ECDSA verification are exposed for both. P-256 stays
on the TLS fast path, so its word count is spelled as a constant on
that branch and the compiler keeps its unrolled Montgomery clones:
the bench numbers do not move. x509.c accepts P-384 subject keys and
ECDSA signatures over them, so every one of the 155 certificates in
net/cacert.h now loads as a trust anchor; the 15 P-384 roots (DigiCert
G3 and G5, GlobalSign E46, Amazon Root CA 3 and 4, Entrust EC1 and the
others) were being skipped before. Verified against reference P-384
ECDH and ECDSA vectors on both limb widths, the qemu-aarch64 lane, and
the shipped bundle.
2026-09-28 18:41:21 +02:00
libretroadmin 38cbed4d6c libretro-common: add X.509 certificate verification
crypto/x509.c gives the cleanroom stack the certificate layer its TLS
client needs: a DER reader (definite lengths only, non-minimal
encodings refused), x509_parse() for the fields a client acts on
(issuer, subject and its commonName, validity, SubjectPublicKeyInfo
for RSA and P-256, basicConstraints, keyUsage, subjectAltName,
signature algorithm), x509_verify_signature() over RSA PKCS#1 v1.5 and
ECDSA with SHA-256/384/512, hostname matching on dNSName entries with
a single leftmost-label wildcard (commonName only when there is no
subjectAltName), a trust store loaded from the PEM bundle in
net/cacert.h, and x509_verify_chain(): from the leaf, each step is a
certificate from the server's chain or a trust anchor whose subject
names the current issuer and whose key verifies the signature, with
validity, cA, keyCertSign and pathLenConstraint checked on the way, in
whatever order the server sent the chain. Names compare as DER octet
strings. Everything points into the caller's DER; the parsed chain is
heap-allocated so a frame never carries it.

140 of the 155 bundle certificates load as anchors; the rest carry
P-384 keys, which pk.c does not have yet, and are skipped rather than
refused so the store still loads. test_crypto.c gains a suite built
from a generated three-level chain (RSA root, P-256 intermediate with
pathLen 0, RSA leaf with wildcard SAN): signatures under each
algorithm, hostname rules, expiry, a bad signature, a rogue CA under
the pathLen-0 intermediate, an out-of-order chain with the root
included, and loading the shipped bundle. Built under HAVE_CRYPTO
beside the other crypto units.
2026-09-28 18:30:17 +02:00
libretroadmin 784067e2ff libretro-common: ARMv8 AES + PMULL path for the cleanroom crypto
crypto.c gains the ARM counterpart of its x86 AES-NI + PCLMULQDQ path:
the block cipher, CTR with four blocks in flight and GHASH on the
ARMv8 crypto extension, gated the way lrc_hash.c gates its SHA path -
used unconditionally when the baseline carries the extension, else
compiled in under target("+crypto") on toolchains whose arm_neon.h
declares the intrinsics regardless of the baseline (Clang 16+, GCC 9+)
and chosen at run time on RETRO_SIMD_AES. The GHASH multiply is the
byte-reversed-operand algorithm the x86 path uses, with the lane
shifts spelled as vext. Apple Silicon, Android, Switch and Linux ARM
builds get hardware AES-GCM; everything else keeps the round-table
cipher and 4-bit GHASH tables.

Nothing on an x86 runner executes this, so Makefile.test gains
crypto-aarch64-check: with an aarch64 cross gcc and qemu-user on PATH
the crypto suite runs under emulation in both the dispatched and the
baseline-crypto shape, built against test/check_shim.h since libcheck
is not installed for the target; without the tools it says so and
passes. The libretro-common CI job installs both and runs it.
2026-09-28 18:17:55 +02:00
libretroadmin 0de2afffd1 keychain: resolve gethostuuid at run time on macOS
The PowerPC job builds against an SDK whose unistd.h does not declare
gethostuuid(), so the Apple machine-identity branch failed to compile
under -Werror=implicit-function-declaration. The function is looked up
with dlsym() instead, with the prototype declared locally: no header
has to know it and a host without the symbol (a 10.4 libSystem) simply
takes the key-file-only key, which is what every non-Apple platform
without a machine identity does already.
2026-09-28 18:09:26 +02:00
libretroadmin 614d2fe2cd Encrypt credentials at rest in retroarch-keychain.cfg
Every sensitive value written to retroarch-keychain.cfg is now sealed:
"$kc1$" followed by base64(nonce || ciphertext || tag), ChaCha20-Poly1305
under a master key with the setting's own name as associated data, so a
blob cannot be moved from one key to another. The master key is
HKDF-SHA256(salt = retroarch-keychain.key, a 32-octet random file
created owner-only beside retroarch.cfg, ikm = the machine identity:
MachineGuid on Windows, /etc/machine-id on Linux, gethostuuid() on
macOS 10.5+, nothing elsewhere, where the key file alone is the key).
A keychain file or a copied config directory on its own no longer
carries usable secrets. Plaintext values from before this change load
as they always did and are sealed on the next save; a keychain file
whose key file is missing or belongs to another install keeps its
blobs untouched through load and save, with a warning, rather than
having them overwritten by empty values. Values are sealed and opened
through heap buffers so nothing path-sized lands on the stack. The
layer is HAVE_KEYCHAIN (default on) and needs HAVE_CRYPTO: a build
without the crypto library, such as the small consoles, keeps the file
in the clear as before. No user-facing setting, there is no trade-off
to expose.

libretro-common/file/keychain.c holds the format and key derivation
and is exercised by test/file/test_keychain.c (round trip, per-setting
binding, tamper, plaintext pass-through, key file persistence, loss and
corruption) from Makefile.test; configuration.c opens the entries after
merging the keychain file and seals them in config_save_credentials.
2026-09-28 17:50:04 +02:00
libretroadmin 4490a7e1ee libretro-common: add cleanroom crypto foundation
Adds libretro-common/crypto in three translation units, built under
HAVE_CRYPTO (qb default on; the GameCube, Wii, PS2 and other console
Makefiles do not set it, so nothing lands in their RAM). crypto.c:
AES-128/192/256 (forward cipher on round tables, CTR, GCM with 4-bit
GHASH tables, CMAC), ChaCha20, Poly1305, an AEAD front (ChaCha20-
Poly1305, AES-256-GCM), constant-time compare and clear. kdf.c:
HMAC-SHA256 over the streaming SHA-256 in lrc_hash, HKDF, PBKDF2, a
ChaCha20 fast-key-erasure DRBG and crypto_random_bytes() over the
platform entropy source (RtlGenRandom / CryptGenRandom resolved at run
time on Win32 with no header or link dependency, BCrypt on WinRT,
getrandom then /dev/urandom on POSIX, the kernel RNG on 3DS, Vita and
Switch, a clock-mixed fallback elsewhere). pk.c: fixed-width Montgomery
bignum with 64-bit limbs where the compiler has 128-bit products and
32-bit limbs otherwise, RSA PKCS#1 v1.5 verification, and P-256 ECDH,
key generation and ECDSA verification. SHA-384/512 join SHA-256 in
lrc_hash. Pure C89 with no third-party code; on x86 with GCC or Clang an
AES-NI + PCLMULQDQ path is compiled in under the target attribute and
chosen at run time, the way lrc_hash already does for SHA-NI, so HMAC,
HKDF, PBKDF2 and the DRBG inherit that SHA-NI path as well.

This is the base the keychain, TLS, and SMB signing and encryption
build on. test/crypto/test_crypto.c checks each primitive against
published vectors (FIPS 197, RFC 4493, the GCM spec cases including 8-
and 60-octet IVs, RFC 8439, RFC 4231, RFC 5869, PBKDF2, FIPS 180-4) and
the public-key side against reference-generated ones, on both the
vector and scalar AES paths and both limb widths, from Makefile.test in
the libretro-common CI job. samples/crypto/bench compares throughput
with the mbedtls the Linux build links: on an AES-NI host AES-256-GCM
runs 5.8x, ChaCha20-Poly1305 1.2x, HMAC 2.4x, P-256 ECDH 2.5x, ECDSA
verify 1.8x, and RSA verify 1.0-1.1x per fresh key. Every frame stays
under the 2 KiB libretro-common budget; RSA keeps its operands on the
heap. The compile matrix gains two griffin lanes, crypto on and off,
since griffin is where a file-local name can collide with another
unit's and only the MSVC job builds it.
2026-09-28 17:50:01 +02:00
Codex 41caa78885 Handle HTTP cache allocation and resolver startup failures
Reject cache entries whose hostname allocation fails, check DNS entry and resolver-thread creation, and roll back failed startup without leaving a pending cache entry. Complete resolver hostname-copy failures as failed lookups. Close newly created sockets when connection-pool allocation fails.

Add deterministic allocation and thread-start failure tests, cleanup checks and recovery coverage for threaded and non-threaded builds, and register both variants in CI.
2026-09-28 05:55:26 +02:00
Codex 9ada776276 Retain cached DNS addresses during HTTP connection setup
Connection setup uses addrinfo after releasing the DNS cache lock. An expiration sweep can reclaim it while a slow connection attempt still uses it. Count active connect users under the cache lock, skip their entries during eviction, and release the reference on every exit path without holding the lock across socket or TLS operations.

Add deterministic eviction tests for plain and TLS success/failure, early exits and overlapping users, and register both build variants in task CI.
2026-09-28 05:51:45 +02:00
Codex 9ce46a1c61 Rebind download widgets at extraction handoff
Copying frontend_userdata leaves the widget keyed to the finished download until extraction publishes progress. A delayed start can let the widget expire without unlinking the extraction task.

Transfer the link under the widget state lock after extraction creation succeeds, reusing the existing owner-change logic to clear terminal state and cancel expiration. Cover pending and displayed widgets, delayed progress, and teardown before the new task runs.
2026-09-28 05:47:10 +02:00
libretroadmin f8d59795c1 Remove unused functions 2026-09-28 05:44:24 +02:00
Codex 108642a968 Publish progress from the regular task runner
The regular runner executes on the pumping thread but skipped running-task progress pushes as though a separate threaded gather would deliver them. Publish updates for unfinished tasks after their handler, while leaving final delivery to retirement. This restores notifications and progress callbacks when threaded tasks are disabled.

Test intermediate updates and exactly one final update; clarify prefetch's existing callback deduplication.
2026-09-28 05:41:53 +02:00
Codex aa76e13d37 Cancel superseded widget title transitions
Rapid title updates queued multiple completion callbacks. After the first transferred msg_new into msg, a later callback could free the displayed text with no replacement. Give title transitions their own animation tag, cancel before replacing them, and cancel that tag at widget teardown.

Exercise queued callbacks and unrelated movement animations in the ownership test, and run that test in graphics CI.
2026-09-28 05:16:09 +02:00
Codex 140a30ea9f Publish suppressed task completion to attached widgets
A muted task or failed final title allocation can suppress its last progress push, leaving a widget linked to a freed task. Send an empty completion message when frontend data is attached. The runloop updates widget lifetime state without forwarding empty text to notifications or speech.

Cover muted and missing-title completions, callbacks outside the property lock, and real task retirement with an attached widget.
2026-09-28 05:12:20 +02:00
Codex 32b32a0ec2 Wire task snapshots into sample CI and widget harnesses
Classify task_queue_snapshot_test as runnable so the sample workflow executes it instead of rejecting the new target. Link the production task queue in both widget harnesses to resolve task_get_progress_snapshot, and initialize its lifecycle around the tests.
2026-09-28 04:16:15 +02:00
Codex a5fdae785b Avoid restarting unchanged widget title transitions
Compare snapshots with the displayed title when no transition is pending. Transferring the pending string on animation completion clears msg_new, so comparing only that pointer restarts identical titles on later progress pushes. Cover repeated snapshots after completion in both widget styles.
2026-09-28 04:12:22 +02:00
Codex 2828675c32 Preserve finished task state while drawing widgets
A cancelled or failed task may finish with progress between 1 and 99. Clearing its finished flag while drawing makes widget cleanup treat its retained task pointer as live after the task has retired. Keep the completion state authoritative regardless of the displayed progress.

Add a production-renderer regression covering 38 completion, cancellation, error and live-task cases across both widget styles. Include the compatibility string header required by the shared test stubs on Windows.
2026-09-28 04:04:45 +02:00
Danny KrauseandClaude Opus 4.8 71445e8e50 metal: load default.metallib by path when newDefaultLibrary returns nil
On macOS, -[MTLDevice newDefaultLibrary] only resolves default.metallib
from inside a .app bundle's resources. For a bare executable (CLI builds,
non-.app installs, a dev build run in place) it returns nil, leaving the
Metal driver with no shader library -- every -newFunctionWithName: then
returns nil.

That nil function is passed straight to
-newComputePipelineStateWithFunction:error: when building the pixel-format
conversion filters, which asserts ("computeFunction must not be nil") and
aborts the process. Depending on timing it can instead surface as a
half-initialized driver or a plain "Cannot open video driver".

Two fixes:
 - When newDefaultLibrary returns nil, fall back to loading
   default.metallib explicitly via -newLibraryWithURL:error: from the
   executable's own directory (where `make install` places it). The normal
   .app bundle path is unaffected.
 - Guard -newFunctionWithName: in +[Filter newFilterWithFunctionName:...]:
   a nil function now returns gracefully with an error log instead of
   tripping the NSAssert inside newComputePipelineStateWithFunction.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-09-28 04:07:32 +02:00
Codex f9490313cd Transfer owned task snapshot strings into widgets
Reuse snapshot strings for widget creation and title updates, and move the pending string when its animation completes. Check the one remaining creation duplicate before publishing the task association. This removes redundant allocations and avoids NULL dereferences on allocation failure.

Exercise the production message path with allocation-failure injection, ownership accounting, immediate and animated updates, terminal flags, error text and full-queue rollback.
2026-09-28 03:59:37 +02:00
Codex 9d8228ebb1 Snapshot task display properties before updating widgets
Copy title, error, flags and progress together under the task property lock. Widgets consume the owned copy after that lock is released, preserving the existing task association and widget-state locking. Keep terminal flags available on allocation failure so retiring tasks do not leave live widget references.

Add regression coverage for concurrent string replacement, retained snapshots, allocation failures and an unthreaded runner.
2026-09-28 03:35:32 +02:00
Codex 0b3c7c495e Capture autosave SRAM on the main thread
Replace concurrent live-SRAM reads and the uninitialized frame sequence with worker-requested snapshots. Preserve nested netplay scopes, service paused iterations, and keep disk I/O outside the publication lock. Retain failed writes for retry instead of advancing the saved-image baseline.

Add regression coverage for snapshot ownership during slow I/O, nested scopes, paused checks, unchanged data, write failures, compressed routing and shutdown.
2026-09-28 03:35:32 +02:00
github-actions dd7f3fa64a Fetch translations from Crowdin 2026-09-28 00:28:45 +00:00
libretroadmin ce8f93cc1c gx: bridge internal libogc's __syscalls table to modern devkitPPC
The default GameCube/Wii build links the prebuilt in-tree libogc, whose
SYS_Init installs its heap, lock, exit and time handlers into newlib's
__syscalls dispatch table -- a table devkitPPC r38 and later no longer
provide, so the EXTERNAL_LIBOGC=0 path has not linked on any current
toolchain (CI never sees it because it builds EXTERNAL_LIBOGC=1).
Defining bare storage for the symbol would link but misbehave:
libsysbase's default sbrk would place the heap by linker symbols
instead of libogc's MEM1 arena, colliding with SYS_GetArena1Lo
allocations, and malloc would run unlocked under lwp threads.

wii/libogc_syscalls_compat.c therefore defines the table with the slot
layout the prebuilt SYS_Init actually stores to (offsets 0..32,
verified against the archive's relocations) and provides strong
definitions for the per-symbol weak __syscall_* hooks that modern
libsysbase dispatches through -- sbrk, the four lock operations, the
malloc locks, exit and gettod all route to the same libogc handlers
the old toolchain reached, with the old pre-SYS_Init NULL-slot
behaviour preserved.  The file is compiled only under
EXTERNAL_LIBOGC=0, external-libogc builds are untouched, and the
default make -f Makefile.ngc links again on the CI toolchain
(devkitPPC r38, GCC 10.2.0).
2026-09-28 02:11:25 +02:00
libretroadmin e43ea7cf34 ngc: compile out features the GameCube cannot use
24 MB of MEM1 makes resident code weight count, and the dol carries all
of it, so features that cannot do their job on this hardware are
compiled out of the GameCube build: libretrodb (defaulted on by
Makefile.common; database scanning is impractical here and the explore
menu goes with it), the webp/dds thumbnail decoders, the mod/s3m/xm
tracker, 7z (LZMA dictionaries alone outsize the console's free RAM, so
7z archives could never extract anyway) and runahead (the second
savestate never fits beside a running core).

Measured with the CI toolchain (devkitPPC r38, GCC 10.2.0) in the CI
configuration, the frontend's static image shrinks from 4.43 MB to
3.95 MB -- about 490 KB of MEM1 returned to content on every core --
and the link map confirms no libretrodb, explore, webp, dds, mod
tracker, 7z or runahead code remains in the image.
2026-09-28 02:11:23 +02:00
Acts1631 9a19e7a94d netplay: validate assigned client number before use
A host can send an out-of-range client number in SYNC and later grant
play mode, causing the client to write past fixed per-client arrays.

Reject invalid assigned numbers during SYNC and require MODE/YOU
updates to name the assigned client.
2026-09-28 01:38:29 +02:00
LibretroAdmin 4ac19205a1 xmb: recover cancelled icon thumbnails during held scrolling
Fixes #19075.

Every selection-pointer change in a playlist bumps the thumbnail
generation and cancels all in-flight icon loads.  An entry whose load
had already been dispatched on the previous step is left in the
PENDING state with no task behind it: gfx_thumbnail_handle_upload
refuses to touch a thumbnail through a stale tag, so nothing ever
clears it.

The icon dispatcher in xmb_render then skips that entry on every
subsequent pass because its status is not UNKNOWN, and the recovery
in gfx_thumbnail_request_stream (gfx_thumbnail_reset_if_orphaned) is
never reached.  Holding Down for a few seconds therefore leaves a
random subset of the visible entries permanently blank until the
list is repopulated.

Call gfx_thumbnail_reset_if_orphaned before the status early-out so
orphaned entries drop back to UNKNOWN and are re-requested.
2026-09-27 22:33:02 +02:00
Rob Loach 520097d511 tools: cover sdl3_gfx overlay setters 2026-09-27 22:13:54 +02:00
Rob Loach 2d3982c990 sdl3: Mark borrowed overlay textures 2026-09-27 22:13:54 +02:00
Rob Loach c3e3a101fa sdl3: Update comments 2026-09-27 22:13:54 +02:00
Rob Loach 12ac6b8690 sdl3: Overlay load_textures and cleanups 2026-09-27 22:13:54 +02:00
Rob Loach 73159cf698 sdl3: Adapt overlay to upstream API 2026-09-27 22:13:54 +02:00
Rob Loach 93d40d3b7c sdl3: Overlay comment and check cleanup 2026-09-27 22:13:54 +02:00
Rob Loach 6806d1aa56 sdl3: Simplify overlay rendering 2026-09-27 22:13:54 +02:00
Rob Loach b185f5e73b sdl3: Trim overlay comments 2026-09-27 22:13:54 +02:00
Rob Loach d80790dd20 sdl3: Use SDL_FRect for overlay coords 2026-09-27 22:13:54 +02:00
Rob Loach 0b1609a159 sdl3: Implement overlay interface 2026-09-27 22:13:54 +02:00
keycrossandkeycross 30a56fc750 Fix resumable core CRC during restore (#19639)
Co-authored-by: keycross <[email protected]>
2026-09-27 21:56:47 +02:00
LibretroAdmin 8e4598b6e6 tasks/task_http: remove task_push_webdav_put
Uploads go through task_push_webdav_put_stream, and nothing calls the
buffered PUT any more, on either the native or the emscripten backend.
2026-09-27 21:10:48 +02:00
LibretroAdmin 6941f76379 samples/network/webdav: stub task_push_webdav_put_stream
webdav.c uploads through task_push_webdav_put_stream, so the three
harnesses that link it need that stub in place of the plain PUT one.
The re-authentication stub pulls the body from the source in send-buffer
sized pieces, so its retry check still sees the bytes that would go on
the wire.
2026-09-27 21:10:48 +02:00
LibretroAdmin c66005dbff video_thread_hw: fence a duped frame's read of the HW ring slot
A dupe (video_refresh(NULL)) from a hardware core reached the worker
with no HW slot, so the driver drew whatever it had retained from the
last real frame - the core's texture or ring copy - without the slot
being installed or its fence re-signalled afterwards. The core's wait
on that slot at its next publish covered only the first draw, not the
dupe's repeat of it.

The ring now remembers the slot the worker last claimed a real frame
from. A dupe installs and fences that slot around the driver's NULL
frame exactly as the first draw did, so the slot's fence covers every
read of it. The push side waits, bounded, before handing the core a
slot that a still-queued dupe is about to re-read, since that dupe has
not re-signalled the fence yet.

Frontend-internal; the core-facing interfaces and their versions are
unchanged.
2026-09-27 19:36:35 +02:00
LibretroAdmin 401cb3e839 video_thread_wrapper: a dupe must not replace a queued real frame
When both ring slots are claimed, the incoming push replaces the newest
frame the worker has not taken yet. A dupe (video_refresh(NULL))
carries no image, so letting it replace a real frame threw away the
only new image the core produced in that period - a 30 fps game sends
real, dupe, real, dupe - and the driver showed the frame before it for
two periods. Without the wrapper a NULL push can never lose a frame.

When the incoming push is a dupe and the slot it would take holds a
real frame, drop the dupe instead: count the miss, queue nothing, and
leave the real frame pending.
2026-09-27 19:36:32 +02:00
LibretroAdmin 89b2562e31 d3d12: fence a duped frame's read of the core's texture (hw interface v2)
A core that has nothing new sends video_refresh(NULL). The driver then
draws the core's texture from the previous frame again through
hw_direct.current, but frame_took_texture is only set inside the
handoff block that a NULL frame skips, so no hw_v2.done signal was
queued behind the dupe's command list.

wait_sync_index promises the core that the frontend is finished with
every use of the texture. With one sync index and one texture in the
non-threaded path, the core's next wait saw the previous value already
complete and started writing the texture while the dupe's draw could
still be reading it on the GPU.

Set frame_took_texture whenever the frame draws the core's texture
directly, so the done fence covers repeats as well. No interface
change: the header already promises this.
2026-09-27 19:36:28 +02:00
LibretroAdmin 290acd340b unix: default to packaged libretro data dirs under share/libretro
Distribution packages install the libretro data sets under
<prefix>/share/libretro/<name>: FreeBSD's games/retroarch-assets and
games/libretro-core-info put assets and info in
/usr/local/share/libretro/, and Debian uses /usr/share/libretro/ for the
same.  The FreeBSD port has to sed those paths into the installed
retroarch.cfg because a stock RetroArch never looks there; anyone
building from source on such a box starts with an empty ~/.config
tree and a menu without assets or core info even though both are
installed.

The Unix defaults already probe /usr/{local/,}share/{games/,}retroarch
for assets, so extend that to the share/libretro layout: when no
LIBRETRO_* environment override is set and <prefix>/share/libretro/
{assets,info,shaders,autoconfig} exists, it becomes the default for
assets_directory, libretro_info_path, video_shader_dir and
joypad_autoconfig_dir.  Only read-only data sets are affected; cores,
saves and everything RetroArch writes keep the per-user default, so
the online updater still works.  Existing configs are untouched (the
defaults only apply to keys the config does not carry).

samples/frontend/packaged_dirs/run.sh runs the built retroarch
headless with a throwaway HOME and config_save_on_exit and checks the
written config: per-user paths when /usr/local/share/libretro is
absent, the packaged paths when it is present (that lane needs root to
create the directory and skips otherwise).  It fails on the previous
platform_unix.c and passes with this one.  The FreeBSD CI job runs it
after the build.
2026-09-27 18:53:21 +02:00
LibretroAdmin 4e1eb70caa udev: finish the kqueue code path so the BSDs no longer need libepoll-shim
udev_input.c had a half-written kqueue alternative to epoll behind a
"#if 1 / TODO" that hard-selected epoll everywhere, which is why the
FreeBSD games/retroarch port has to link the udev input driver against
libepoll-shim (UDEV_LIB_DEPENDS/UDEV_CFLAGS/UDEV_LDFLAGS in the port).

Complete it: on FreeBSD, OpenBSD, NetBSD, DragonFly and macOS the evdev
fds are registered with EV_SET(EVFILT_READ, EV_ADD, udata = device)
(the placeholder referenced an undefined LISTENSOCKET), the poll uses a
zero timespec like the epoll_wait(..., 0) it mirrors (the placeholder
referenced an undefined `timeout`), and the event loop tests
filter == EVFILT_READ && !(flags & EV_ERROR) instead of the epoll-only
`events & EPOLLIN`.  kqueue is level-triggered by default so the
drain-until-EAGAIN read loop is unchanged, and closing a device fd
removes it from the kqueue just as it does from the epoll set.
NetBSD's kevent.udata is intptr_t rather than void *, hence the
UDEV_KQ_UDATA cast.

Linux still uses epoll; that object is unchanged.  The kqueue variant
was compiled with libkqueue's <sys/event.h> to catch type errors.

CI: the FreeBSD job now fails if the linked retroarch binary still
imports any epoll_* symbol.
2026-09-27 18:34:07 +02:00
LibretroAdmin 422ed61c9d freebsd: build udev, Wayland and V4L2 without the Linux compat headers
A stock FreeBSD install could compile RetroArch only after
`pkg install evdev-proto v4l_compat`, the Linux header shims:

  input/common/wayland_common.h:23: fatal error: 'linux/input.h' file not found
  camera/drivers/video4linux2.c:35: fatal error: 'linux/videodev2.h' file not found
  cores/libretro-video-processor/video_processor_v4l2.c:49: fatal error: 'linux/videodev2.h' file not found

Neither package should be needed:

- FreeBSD ships the evdev button/key codes in base as <dev/evdev/input.h>,
  and udev_input.c already includes that header on __FreeBSD__.  Do the
  same in input/common/wayland_common.h and input/drivers_joypad/udev_joypad.c,
  the two remaining unconditional <linux/input.h> users on this platform.
  Every BTN_/KEY_/EV_/ABS_/FF_/EVIOC symbol they use exists in FreeBSD
  14.3's dev/evdev/ headers.

- configure turned V4L2 on because libv4l2.pc was installed (libv4l is a
  common FFmpeg dependency), but the sources include the kernel header,
  which on FreeBSD only v4l_compat provides.  After the libv4l2 check,
  verify <linux/videodev2.h> actually compiles and disable V4L2 when it
  does not, like the EGL check already does.

CI: the FreeBSD job no longer installs evdev-proto or v4l_compat, enables
Wayland (previously disabled for exactly this reason), and asserts that
configure auto-disables V4L2 with libv4l but no headers.  v4l_compat is
then installed and only the two V4L2 objects are recompiled so that
code path stays covered without a second full build.

Reported-by: gouchi
2026-09-27 18:31:18 +02:00
LibretroAdmin b4b7710c3e metal: take the next drawable late under threaded display pacing
With the threaded presenter holding each push to the display's vblank,
the frame no longer acquires the next drawable right after presenting;
the next frame acquires its own when it first draws. Held from the
present, it sat idle for a period with one drawable on screen and one
queued, so the video thread blocked until the next vblank on every
frame - read by the pacer as a frame queued behind another, draining
every few presents and stalling the core. Without the pacer the
acquire-after-present is unchanged.

video_frame_info_t::threaded_display_pacing is set only while the
wrapper runs, so a driver can rely on it meaning the push is being
timed; its one existing reader is the wrapper itself. threaded_video
gains a lane checking the flag against the setting and the wrapper.
2026-09-27 18:28:55 +02:00
LibretroAdmin f2ee3fd988 metal: report when a drawable reached the display
get_last_present_time now answers from each drawable's presented
handler (macOS 10.15.4, iOS/tvOS 10.3, probed at runtime; the selectors
are declared locally so any SDK builds it). presentedTime is on the
host clock and is bridged to cpu_features_get_time_usec() with one
paired read in the handler, since the frontend's CLOCK_MONOTONIC counts
sleep and the host clock does not.

The threaded presenter then lays its vblank grid on the display's own
timestamps, so display pacing holds the push against real vblanks with
vsync on instead of an estimate that drifts off phase.
2026-09-27 18:09:57 +02:00
LibretroAdmin 109b98467f video_thread: lend a spare buffer when the ring is full
A core asking for GET_CURRENT_SOFTWARE_FRAMEBUFFER while one frame is
queued and the other is being rendered - the steady state under a
driver whose present blocks until vblank, as Metal's drawable acquire
and a FIFO MoltenVK swapchain do - is now lent a third buffer kept
outside the ring. The push that returns it swaps it by pointer into
whichever slot it picks, and the buffer it displaces becomes the next
spare, so the frame is still published without a copy. The spare is
allocated on the first ask that needs it; ring-slot loans are
unchanged.

threaded_video gains a lane with a driver whose present blocks past a
display period: nearly every ask must be granted, published zero-copy,
and each drawn frame must be newer than the one before.
2026-09-27 17:58:38 +02:00
LibretroAdmin 75599abff9 runloop: back off and eventually give up when the GPU device keeps being lost
A driver that lost its GPU device set VIDEO_FLAG_GPU_DEVICE_LOST and
the runloop rebuilt the video driver at once, every time. That is the
right answer to a device lost once - a TDR, a driver update - which
comes back on the first rebuild. A device that is lost again straight
after being rebuilt is a failing driver or GPU, and rebuilding on it
every frame was a busy loop of full driver reinitialisations, each of
which failed the same way.

The first loss is still rebuilt immediately. A loss following it is
retried with a doubling delay, from one second up to sixteen, and
after six in a row the rebuild is abandoned with an on-screen message
until a minute has passed. A loss a minute or more clear of the
previous one is a fresh incident and starts the count again.

Falling back to a different video driver is left as a policy choice:
this only bounds the retry loop.
2026-09-27 17:37:49 +02:00
LibretroAdmin 7ec3c021a5 net_http/webdav: stream the upload body from the file instead of reading it whole
A WebDAV upload read the complete file into a buffer, and
net_http_connection_set_content() then copied that buffer again, so
two copies of the file sat in memory for the whole transfer. Saves are
small, but a save state is not, and several uploads run at once.

net_http gains a request-body source: net_http_connection_set_content_source()
takes a Content-Length, a read callback and an optional rewind
callback. net_http_send_request() pulls the body from the source in
64 KiB runs and sends each as it arrives, so one run is all that is
ever held; a source that delivers fewer bytes than the advertised
length fails the request as a transport error would, since the header
has already promised them. When a request has to be replayed on a
fresh connection the source is rewound first; a source with no rewind,
or one whose rewind fails, is not replayed.

task_push_webdav_put_stream() wraps that for the task queue, and the
cloud-sync WebDAV driver uploads straight from the open RFILE through
filestream_read, rewinding with filestream_seek. The existing seek to
the start before the upload is kept, as the Digest-challenge retry
still reaches this path with the file at its end.

Checked with a 3 MB body PUT to a local server: the received body is
byte-identical (SHA-256) and the request completes with 201.
2026-09-27 17:31:15 +02:00
libretroadmin 3cd67707ce compat/ifaddrs.h: C linkage for C++ includers
getifaddrs and freeifaddrs are declared with C linkage under C++, so a
C++ caller resolves them against compat_ifaddrs.c rather than looking
for mangled names that nothing defines.
2026-09-27 17:29:11 +02:00
LibretroAdmin 5ec4f2abb0 memmap: memzero_pages reports a Windows recommit failure instead of faulting
On Windows, memzero_pages() decommits the range and commits it again to
get fresh zero pages. If the decommit succeeded and the recommit then
failed, control fell through to the generic memset over a range that
was no longer backed, which faults.

memzero_pages() now returns bool. On Windows, a failed recommit after
a successful decommit returns false and leaves the range alone: it is
decommitted, and the caller must treat the allocation as lost. Every
other path succeeds or falls back to memset and returns true.

There are no callers of this API yet; this settles its contract before
one arrives.
2026-09-27 17:20:34 +02:00
LibretroAdmin 1a13723fb8 net_socket_ssl_mbed: make the verification mode an atomic rather than a volatile
ssl_authmode is written from the settings thread and read on whichever
thread opens a TLS connection. It was a `volatile unsigned`, which
promises nothing about visibility between threads and is a data race
under the C memory model, whatever the hardware happens to do with an
aligned word.

It is now a retro_atomic_int_t, stored with release and loaded with
acquire. retro_atomic.h supplies a C89 fallback on the console
toolchains without <stdatomic.h>, which is why volatile was chosen
originally, so nothing is lost there. Compiles under -std=c89
-pedantic.
2026-09-27 17:20:30 +02:00
LibretroAdmin b42d47575d 2xbr: pixel-double a frame whose key buffer cannot be grown
The per-frame YUV key buffer is grown with realloc when a frame is
larger than the buffer was sized for. If that realloc fails, the key
pass was skipped but the workers still ran, indexing the keys buffer by
the new frame's width and height - a read past the end of a buffer
sized for the smaller frame.

When the buffer cannot be grown, the workers are now given a plain 2x
pixel-doubling callback for that frame instead of the edge detector,
so the output is filled sensibly and nothing is read out of bounds.
The next frame tries to grow the buffer again.

Checked with the realloc wrapped to fail: every output pixel is the
doubled source under ASan, and the ordinary path runs once the growth
succeeds.
2026-09-27 17:20:26 +02:00
LibretroAdmin 1342ec290e video_thread_wrapper: compare a pushed frame against the lent slot as integers
A core that took a framebuffer loan may push a pointer anywhere inside
the lent slot (cropping overscan by offset) or somewhere else entirely
(it rendered into its own buffer and the loan lapses). The check was
`f >= b && f < b + buffer_size` on the raw pointers, and ordering two
pointers into different objects with the relational operators is
undefined in C. It happens to work on every compiler in use, but the
ownership test should not rest on that.

Both addresses are converted to uintptr_t and the frame is accepted
when it is at or past the slot start and its offset is inside the
slot. The row clamp for the zero-copy case is reworked to compute the
rows that fit as (buffer_size - offset) / stride rather than forming
height * stride, which can wrap.
2026-09-27 17:17:01 +02:00
LibretroAdmin ce04b1f8fe d3d12: retire recording readback buffers behind their fence instead of releasing them at once
The streamed recording ring records a copy of each frame's back buffer
into one of three readback buffers, inside that frame's command list,
and signals the queue fence behind the list. d3d12_record_free() then
released all three buffers immediately - when recording stops, when the
swapchain changes size or format, and at teardown. D3D12 requires a
resource to stay alive until every command list naming it has
executed, so the ordinary stop and resize paths could free a buffer a
still-queued copy was about to write into.

d3d12_record_free() now moves the ring onto a small retired table,
tagged with the highest fence a capture into it was signalled with,
and d3d12_record_reclaim(), run once per frame, releases a generation
once GetCompletedValue() has passed its tag. A ring nothing was ever
captured into is released outright. Should the table fill under a
storm of resizes, the oldest generation is waited for with the same
bounded wait the queue drain uses. Final teardown, which already
drains the queue, releases the ring and every retired generation
through d3d12_record_teardown().

Nothing waits on the GPU in the stop or resize paths, so the stall
the ring was introduced to remove does not come back.
2026-09-27 17:16:57 +02:00
libretroadmin 651679fc20 retro_procbarrier: claim a slot before writing into it
Two walks can read the same slot as free at the same time; if both then
wrote their fields and one lost the compare-and-swap, the winner's slot
would carry the loser's count and target, its acknowledgement would go
to the wrong walk, and the winner would wait on it for good. The
compare-and-swap now comes first and the fields are written only into a
slot the walk owns. Nothing reads them before the signal that is sent
after them, and a signal from an earlier holder cannot match the new
generation.
2026-09-27 16:23:14 +02:00
libretroadmin 53383bcfbb memmap: memjit_alloc returns NULL where there is no mmap
DJGPP ships a sys/mman.h with the protection bits and mprotect and no
mmap, so HAVE_MMAN is defined on DOS while MAP_ANONYMOUS is not.
memjit_alloc and memjit_free now take the same gate as every other mmap
user in this file -- HAVE_MMAN, not Emscripten, and MAP_ANONYMOUS
defined -- and on DOS, the consoles and Emscripten memjit_alloc is a
plain NULL, as its documentation says.
2026-09-27 16:23:10 +02:00
libretroadmin e84ba6b2f9 retro_procbarrier: stop waiting on a thread that blocks the signal after being picked
A thread can block the signal between the walk's look at its mask and
the signal's delivery, and then hold it queued for as long as it likes;
the walk now closes such a slot at its recheck instead of waiting. For
that to be safe the slot must outlive the signal, which may arrive
whenever the thread unblocks: slots move from the walker's stack to a
static pool of 128, each claimed under a generation, and the queued
signal carries index and generation rather than an address. One
compare-and-swap in the handler both checks that the slot is still the
signal's and closes it, so a delivery that comes after the slot was
closed, released or claimed again is ignored, and the pointer to the
walker's count is only ever followed while the walker still waits on it.

retro_procbarrier_test gains a check with a busy thread flipping its
mask between all-blocked and open every few microseconds while barriers
run; thousands of its signals are delivered after their slot was closed,
and none may be waited on or land anywhere.
2026-09-27 06:35:50 +02:00
libretroadmin c421134213 retro_procbarrier: identify a signalled thread by tid and start time
A tid is reused as soon as its thread is gone, so a walker rechecking a
target it is still owed cannot take the tid's existence as the thread
still being there: with threads starting and exiting around it, a later
thread under the same number would keep the walk waiting on a signal
the kernel discarded. The walk now records each target's start time
from the stat line it already reads to test for runnable, and at the
recheck closes a slot whose thread is gone or whose start time no longer
matches.

The recheck sleep starts at 250 us and doubles to 10 ms, so a walk that
caught a thread in its last microseconds is over in a fraction of a
millisecond, while one owed by a thread that is merely slow to be
scheduled costs a syscall every 10 ms.
2026-09-27 06:17:36 +02:00
libretroadmin a07fb4756f memmap: add memjit_alloc/memjit_free and memzero_pages
memjit_alloc gives a mapping the caller both writes code into and
executes: PAGE_EXECUTE_READWRITE on Windows, an executable anonymous
mapping elsewhere, and on macOS on Apple Silicon the MAP_JIT kind that
memjit_write_begin/end already serve, under the same TARGET_OS_OSX
test so iOS and tvOS keep the plain mapping. It returns NULL where no
such mapping exists, and memjit_free releases it.

memzero_pages zeroes a page-aligned range as cheaply as the system
guarantees: MADV_DONTNEED where that zero-fills (Linux, Android),
decommit and recommit on Windows, fresh pages mapped over the range
where the advice would only be advice (Darwin, the BSDs), and memset
where nothing else exists. memdecommit's documentation now says the
same about what a non-strict range reads back as.
2026-09-27 06:08:18 +02:00
libretroadmin 15aebc2a3b retro_procbarrier: don't probe membarrier on Android below API 29
Android's seccomp filter answers a syscall outside its allowlist with
SIGSYS, which ends the process, and membarrier is on that allowlist
from Android 10 (API 29) on: on Android 8.0 through 9 the probe's query
alone would kill the process at its first asymmetric eventcount. The
API level is read from ro.build.version.sdk and, below 29 or unreadable,
the probe is not made, so those systems resolve to the signal tier,
whose rt_tgsigqueueinfo, futex and tgkill are allowed there.
2026-09-27 05:39:38 +02:00
libretroadmin 3c57861087 retro_procbarrier: skip threads that block the signal on Linux
A thread with the barrier's signal blocked would never acknowledge it,
and a GL or Vulkan driver blocks every signal in each worker it creates,
so a walk that picked one waited on it for good. The walk now reads
SigBlk from /proc/self/task/<tid>/status, in small pieces so the frame
stays within budget, and passes such a thread by. That is sound under
the contract the header now states: a thread that notifies an
asymmetric eventcount must not block the signal, and every thread
started through rthreads inherits an unblocked mask.

retro_procbarrier_test gains a check that runs barriers past a busy
thread with every signal blocked. Under ThreadSanitizer walks run one
at a time, spinning for the turn rather than blocking: the tool keeps
one deferred signal per number per thread and drops a second, and runs
a deferred handler only once the thread is back in instrumented code.
With that, the signal tier completes under the tool.
2026-09-27 05:39:34 +02:00
libretroadmin a8ffa9929b retro_procbarrier: don't wait on a thread that exits before answering
A thread the Linux signal tier picks can exit before its handler runs,
and a signal queued to an exiting thread is discarded, so nothing would
ever answer for it. Each target now gets a slot of its own on the
walker's stack, carried by the queued signal: the handler closes it, or
the walker does when tgkill(pid, tid, 0) reports ESRCH, and a CAS on the
slot makes sure only one of them counts it. The walker still sleeps on
the futex and is woken by every acknowledgement; the wait times out
after 10 ms only so that a target that died is noticed. Targets are
walked in batches of 32 slots to keep the frame bounded.

retro_procbarrier_test gains a check that runs barriers while two
threads keep starting and joining short-lived threads, which stalled
the signal tier every time; the concurrency check only hit it by chance
on SMP runners. Under ThreadSanitizer the slot handoff is annotated
with __tsan_release/__tsan_acquire, since the syscall that orders it is
invisible to the tool.
2026-09-27 05:16:54 +02:00
LibretroAdmin 5ba409d23a audio: oss and audioio open the path of a list entry; audioio lists its nodes
oss lists its nodes with the card's name after the path - "/dev/dsp0
(HDA Intel)" - so the user can tell them apart. The menu stores the
entry as shown, and it comes back to init as the device string; init
opened that string as a path and failed. A device picked from the
list could not be used on OSS4 and the BSDs, where SNDCTL_AUDIOINFO
is there to name the card.

audio_device_label_path() takes the path off the front of an entry -
everything before the first " (" - and leaves a bare path alone; oss
and audioio open what it gives.

audioio listed nothing, the one driver left whose kernel offers a
choice of nodes without a list to choose from. It now walks
/dev/audio and /dev/audio0..15 as oss walks /dev/dsp*, with the name
AUDIO_GETDEV gives after the path. That needs no driver instance,
which is what the menu has when init failed and the user needs to
pick another device.

samples/audio/device_label_path compiles both drivers whole with
open() redirected to a recorder and checks each reaches it with the
path of an entry, a bare path as it is, and the default with none;
and pins the helper on its own. It fails on the previous oss and
audioio at "init opens the path of a list entry" and passes now.
2026-09-27 05:08:48 +02:00
libretroadmin 12ab7d8e89 retro_procbarrier: make every tier safe for concurrent callers
Any number of threads can be inside retro_procbarrier() at once, and no
caller waits for another. On Darwin the walk takes each running thread's
state with thread_get_state, which holds the thread, waits in thread_stop
until it is off its CPU and releases it before the call returns, so two
callers stopping each other can never be left parked on each other's
hold; KERN_ABORTED is retried. The Linux signal tier queues a real-time
signal per thread with rt_tgsigqueueinfo carrying the walk's own
acknowledgement counter, which the walker sleeps on with a futex, and
init probes that path on the calling thread and refuses non-real-time
signal numbers. The page flip claims one of four locked pages with a CAS
and flips a page of its own when all are busy.

retro_procbarrier_test gains a concurrency check: four threads issuing
barriers against each other under a watchdog, run for every tier by the
existing CI lanes. The header documents the reentrancy guarantee and
the Linux tier's real-time signal requirement. Pre-10.11 Darwin SDKs
spell MAP_ANONYMOUS as MAP_ANON, which the page flip now accepts.
2026-09-27 03:51:39 +02:00
LibretroAdmin 745a261d75 (GX) Keep the display un-blanked across video driver teardown so libogc's exception screen is visible
Every GameCube crash during content load is reported as "black screen,
then the console reboots" (RetroArch #19637 and friends) and nobody
can supply the register dump / stack trace we ask for. Reason: the
video driver is freed and re-initialised around each content load,
gx_free() called VIDEO_SetBlack(true), and libogc's default exception
handler only swaps VI to its own console framebuffer; it never clears
the black flag. So a fault in retro_load_game or driver re-init prints
its registers and stack trace to a display nobody can see, then the
__exception_setreload(8) countdown resets the console.

gx_free() now clears XFB 0 to black, points VI at it and leaves the
display enabled instead of blanking it. Looks identical to the user;
lets the exception screen show through.

For that to be safe VI must keep a valid framebuffer after the driver
instance is gone, so the two XFBs move from per-instance allocations
to process-lifetime statics (g_framebuf), allocated once in
setup_video_mode and never freed. They were already fixed at the
largest mode's size, so nothing changes per mode; it also removes a
1.4 MB memalign/free pair from every content load on a 24 MB box.

If gx_free() runs before any XFB exists (early init failure) it
blanks VI as before.
2026-09-27 03:12:19 +02:00
LibretroAdmin a47d76335f menu: only show Send Frame Luminance on Wayland builds
video_hdr_send_luminance is read solely by the Wayland context driver,
yet the HDR settings list showed it on every platform once HDR was on.
Gate the entry on HAVE_WAYLAND so Windows/macOS/X11-only builds don't
expose a toggle that does nothing there.
2026-09-27 02:45:07 +02:00
github-actions 8e35f7df88 Fetch translations from Crowdin 2026-09-27 00:28:38 +00:00
LibretroAdmin 55830573e9 rthreads: don't include pthread_np.h on PS4/Orbis
The PS4 toolchain defines __FreeBSD__ but Sony's libc has no
pthread_np.h, so the include added for FreeBSD's pthread_set_name_np()
broke the Orbis build.  Exclude __ORBIS__/ORBIS, as retro_procbarrier.c
already does for the same reason.
2026-09-27 01:50:54 +02:00
LibretroAdmin ed889cbee5 rthreads: fix FreeBSD build of retro_procbarrier and rthreads
retro_procbarrier.c gated <unistd.h>/<sys/mman.h> on PB_FREEBSD_MEMBARRIER,
but that macro is only defined further down the file (after the
FreeBSD version check), and the x86 page-flip tier uses mmap()/sysconf()
on plain PB_FREEBSD regardless.  So on FreeBSD the headers were never
included and the file failed with undeclared PROT_*/MAP_*/_SC_* symbols.
Gate the POSIX headers on PB_FREEBSD like the code that needs them.

rthreads.c calls pthread_set_name_np() on FreeBSD/OpenBSD/DragonFly,
which those systems declare in <pthread_np.h>, not <pthread.h>; include
it so the call is no longer an implicit declaration.

Found by the CI FreeBSD job.
2026-09-27 01:20:17 +02:00
libretroadmin 1de81209fb audio: name the wrapped driver in the start/stop log lines
audio_driver_start() and audio_driver_stop() log the driver through
audio_driver_get_ident(), which resolves through the threaded
pipeline's wrapper, so the lines name the device driver in use
("alsa", "wasapi", ...) rather than "audio-thread". The buffer
report in audio_driver_init_internal() uses the same accessor in
place of its own copy of that lookup.
2026-09-27 01:11:27 +02:00
LibretroAdmin d52fecafc8 menu: show sublabels for Send Frame Luminance, Peak Brightness, Use Display Peak
The three HDR settings added to settings_def_video_hdr.h ship a
sublabel string, but menu_cbs_sublabel.c never mapped their labels to
it, so the menu showed nothing under them. Add the missing rows to the
sublabel table next to the other HDR entries.
2026-09-27 01:07:39 +02:00
LibretroAdmin 961da8b1a6 (GX/GameCube) Fix content load exiting to a reboot after switching core DOLs (#19637)
Reporter (SD2SP2 + Swiss): "lighter" cores like Nestopia or SNES9x crash
on the first content load but work after the console restarts; PrBoom
and ECWolf go to a black screen and the GameCube reboots.

The attached log is not a crash. It is a clean retroarch_main_quit():
two "Saving salamander config" lines, "Saved config", "Unloading core",
performance counters. RetroArch chose to exit.

Root cause, in three parts:

1) retroarch_main_init() reads retroarch-salamander.cfg and sets
   RARCH_PATH_CORE from its libretro_path *before*
   frontend_gx_process_args() runs, and process_args only fills in
   argv[0] when RARCH_PATH_CORE is still empty. The salamander config
   names whichever core DOL ran last, so launching a different core
   DOL from Swiss leaves RARCH_PATH_CORE pointing at the wrong core.

2) On static builds every content load asks RARCH_CTL_IS_CORE_LOADED
   whether the chosen core matches the loaded one. With the stale
   path it does not, so the load goes down the "fork a new instance"
   path (command_event_cmd_exec -> frontend_driver_set_fork).

3) GameCube has no fork/exec support (set_fork is NULL, exec is
   HW_RVL-only). command_event_cmd_exec ignored the false return and
   still reported success, so the caller set RARCH_CTL_SET_SHUTDOWN
   and the frontend exited; with nothing to return to, libogc resets
   the console. That quit rewrote the salamander config with the
   correct core, which is why the *second* boot of the same DOL
   loads content in-process and works.

Fixes:

* platform_gx.c: on HW_DOL always take the core path from argv[0].
  The core linked into the DOL the loader started is the only core
  this process can run, so argv[0] is authoritative over the
  salamander config. Wii keeps the fill-if-empty behaviour.

* task_content.c: command_event_cmd_exec() now returns false when
  the frontend cannot fork, logging which core was requested and
  pushing MSG_FAILED_TO_LOAD_CONTENT; the two menu callers that
  unconditionally issued CMD_EVENT_QUIT afterwards return to the
  menu instead of shutting down. A core mismatch on a no-fork
  platform is now an on-screen error, not a reboot.

* task_content.c: "Always reload core on run content" needs fork
  support; on frontends without it the running core is all there
  is, so load in-process when it matches instead of exiting.

Host syntax-checked task_content.c with HAVE_DYNAMIC/HAVE_DYLIB
undefined and platform_gx.c with -DHW_DOL against the vendored
libogc headers. No devkitPPC on this box.
2026-09-27 01:07:34 +02:00
LibretroAdmin 52db9d0a2d camera/pipewire: rename static pipewire_* functions out of libpipewire's namespace
FreeBSD's PipeWire 1.6.9 headers declare pipewire_init(int *, char ***),
so the camera driver's static pipewire_init() fails to compile there:
"static declaration of 'pipewire_init' follows non-static declaration".
This is what the games/retroarch port's patch-camera_drivers_pipewire.c
worked around.

Rename the driver's five static entry points to pwire_camera_* so they
cannot collide with anything libpipewire exports, matching the pwire_
prefix the audio driver already uses.  No behaviour change.
2026-09-27 00:49:15 +02:00
LibretroAdmin 4cab0b8781 ci: add a FreeBSD build job
GitHub has no FreeBSD runners, so run the build inside a FreeBSD 14.3 VM
via vmactions/freebsd-vm with the games/retroarch port's default feature
set minus Qt and Wayland.  The job first checks that --disable-pipewire
leaves HAVE_PIPEWIRE_STABLE off with libpipewire installed, then builds
with udev (libudev-devd), X11, SDL2, FFmpeg, PipeWire, ALSA, PulseAudio
and OSS forced on so a missing dependency fails loudly.
2026-09-27 00:33:11 +02:00
LibretroAdmin 16e21db836 qb: do not probe PIPEWIRE_STABLE when PipeWire is disabled
HAVE_PIPEWIRE_STABLE only qualifies HAVE_PIPEWIRE (it gates the camera
driver), but it was probed independently, so --disable-pipewire on a box
with libpipewire installed still ended up with HAVE_PIPEWIRE_STABLE=1 in
config.mk and config.h.  Tie it to HAVE_PIPEWIRE: when PipeWire is off,
record PIPEWIRE_STABLE as off without probing.

While here, make griffin's include of the camera driver require
HAVE_PIPEWIRE_STABLE like Makefile.common and camera_driver.c already do,
so a griffin build against pre-1.0 PipeWire does not try to compile it.

Reported via the FreeBSD ports patch-qb_config.libs.sh ("Fix build if
multimedia/pipewire installed").
2026-09-27 00:33:07 +02:00
LibretroAdmin 023f1e8b05 udev: build the udev input driver on FreeBSD without linux_common
linux_common.o is only compiled on Linux, but udev_input.c called its
illuminance-sensor helpers unconditionally, so --enable-udev on FreeBSD
(libudev-devd) failed to link.  The sensor reads sysfs IIO nodes, which
only exist on Linux, so keep it Linux-only: the udev driver on FreeBSD
now reports the illuminance sensor as unavailable and everything else
works as before.

This is the last of the source patches the FreeBSD games/retroarch port
still had to carry; the ALSA errno.h and FFmpeg 7.1+ ones were already
fixed in master, and the camera pipewire_init clash went away when the
audio driver was renamed pwire_init.
2026-09-27 00:33:03 +02:00
LibretroAdmin 6b6b8eb597 image_blit_bands: pool the groups, retire queued runners in the caller
image_blit_bands() ran a decoder's frame conversion as row bands on a
thread pool and malloc'd a group - the bands and their counters - for
every blit, freed by whichever holder let go last. That is one heap
round trip per decoded video frame, on the frame's path (the audit's
"multi-band image conversion allocates a coordination group per
blit").

Groups now come from a static pool of eight, taken by CAS and given
back when the last holder lets go, so a runner that starts late is as
safe as it was with the heap; the heap is the fallback when every
slot is taken. And the caller, once its bands are done, runs the
runners it posted that are still sitting in the pool's queue
(tpool_help) rather than leaving them: each finds no band left and
lets go. Before, a caller faster than the pool left thousands of no-op
runners queued behind it, each holding a group - which is also why the
pool alone was not enough.

image_blit_bands_test (new, wired into Linux-libretro-common-samples)
wraps malloc through the linker and checks: every row covered once for
1080 and 719 rows at align 2, 500 blits with zero mallocs, four
callers blitting at once, and the no-pool path. The old code mallocs
once per blit and fails the count lane.
2026-09-27 00:27:01 +02:00
LibretroAdmin a2b99b09b5 build: link video_mode_select on every Linux build, not just Wayland
06aed75 made mutter_displayconfig.c call video_mode_list_finish but
only added gfx/common/video_mode_select.o under HAVE_WAYLAND, so every
Linux build without Wayland (the plain Linux job and linux-c89) failed
at link time with an undefined reference from
mutter_displayconfig_get_resolution_list.

video_mode_select.c is plain C with no platform dependency, so it goes
in the base object list next to video_driver.o, and the same include
moves next to video_driver.c in griffin.
2026-09-27 00:03:16 +02:00
LibretroAdmin 7c8e7b4230 d3d11/d3d12: stream the recording readback through a ring
The D3D recorders read the frame back synchronously, every frame:
re-render it (video_driver_cached_frame), create a fresh staging
texture (D3D11) or readback buffer (D3D12), copy the back buffer into
it, then Map with a blocking read - D3D12 drained the whole queue
first. The frame loop stalled for the GPU on each recorded frame,
which is the audit's "generic recording fallback can synchronously
serialize GPU readback". GL and Vulkan already stream theirs.

Both drivers now do what gl2's PBO ring does:

- While recording, each frame's render queues a copy of the back
  buffer into the next slot of a 3-deep ring, right after the render
  and before Present. D3D11 uses staging textures and CopyResource;
  D3D12 records the copy into the frame's own command list and
  signals a fence value behind it.
- read_viewport() takes the slot copied three frames ago: D3D11 maps
  with D3D11_MAP_FLAG_DO_NOT_WAIT and declines the frame on
  DXGI_ERROR_WAS_STILL_DRAWING; D3D12 checks the slot's fence against
  the queue and declines until it has passed. The recorder already
  treats a declined frame as "try again next frame". No re-render, no
  drain, no per-frame allocation, no wait.
- The ring is (re)created when the swapchain changes size or format,
  and freed when recording stops or the driver goes down.

HDR swapchains are decoded on the CPU from the ring copy, as the GPU
tonemap pass renders from the live back buffer and waits on it;
screenshots keep the GPU path.

The recorded frame is three frames behind, as with gl2's ring.
Compile-checked under MinGW; needs a real D3D11/D3D12 recording run.
2026-09-26 23:57:46 +02:00
libretroadmin 06aed75951 video: one place decides which mode a size and rate name
Mutter, KWin and wlroots each carried their own copy of the same two
decisions - the listed mode a requested size and rate mean, and the
order a resolution list is shown in. gfx/common/video_mode_select.c
holds them once and all three call it, reading their own mode
structures through a callback rather than copying them.

The rules are the ones Mutter already applied and the two Wayland
backends lacked: a rate further off than half a hertz is not the mode
the caller asked for unless it carries that whole-hertz label, a
progressive mode comes before an interlaced one, and the mode in use
wins a tie. samples/gfx/video_mode_select checks each of them.
2026-09-26 23:37:01 +02:00
LibretroAdmin f5503fb604 get_list_refresh_flags_test: park the worker so the HTTP task is freed ahead of the updater
The two unthreaded parity lanes relied on the unthreaded gather
reversing the running list every pass, which e8c5c81 removed: the
list keeps its order now, the HTTP task always ticks ahead of the
updater and is still allocated when the updater sees it finished.
Built against the old handler under ASan those lanes pass 9/9, so
they guard nothing.

The window that remains is the threaded one: the queue frees the
HTTP task on the main thread right after its callback, and with a
single worker the free can only land between the HTTP task's last
tick and the updater's next one. The test hits it deterministically
instead of by timing. task_set_flags() is wrapped at link time; when
the HTTP task marks itself finished, on the worker, the wrapper
pushes the updater's 'when' two seconds out. The worker only looks
at the front of the running list, so it sleeps; the main thread
checks until nothing but the updater is findable, i.e. the queue has
freed the HTTP task; then the updater ticks holding a freed pointer.
'when' is written and read on the worker thread, so there is no
race, and TSan agrees.

Against the old handler ASan reports heap-use-after-free in
task_get_flags from task_core_updater_get_list_handler on every run;
against the fixed handler the lane passes plain, ASan+UBSan and
TSan. One plain unthreaded lane stays so the direct-read path runs
under the sanitizers too.
2026-09-26 23:24:01 +02:00
LibretroAdmin b1365e86cd task_core_updater: read sub-tasks directly on the unthreaded queue
core_updater_sub_task_running() reads the sub-task through
task_queue_find() so the threaded queue cannot free it underneath
the worker. On the unthreaded queue that read is a no-op: the
gather detaches the running list before it ticks anything, and
retro_task_regular_find() only walks tasks_running, so from inside
a handler no sub-task is ever findable and the download and
decompress progress never reached the updater's progress bar.

The unthreaded queue has not needed find() since e8c5c81: it runs
handlers in list order (a sub-task pushed from the updater's tick
always sits ahead of it) and only retires finished tasks after the
pass. So when the updater reads a sub-task it has already ticked,
alive, at worst finished but not yet freed; by the next pass its
callback has set the complete flag and the updater stops looking.
Read flags and progress directly there.
2026-09-26 23:24:01 +02:00
Artem Lytkin 74f6867163 task_core_updater: don't read HTTP/decompress tasks after the queue has freed them
The list and download handlers polled their sub-tasks with
task_get_flags()/task_get_progress() and only then checked the
*_complete flag the sub-task's callback sets. The queue frees a task
right after that callback, so a tick that runs after the free still
dereferences the stale pointer. This happens in CORE_UPDATER_LIST_WAIT,
CORE_UPDATER_DOWNLOAD_WAIT_TRANSFER and, on decompress_task,
CORE_UPDATER_DOWNLOAD_WAIT_DECOMPRESS. In the unthreaded queue each
gather reverses the running list, so the updater runs ahead of its
sub-task every other pass. In the threaded queue the window is
narrower but still open.

Moving the complete check first is not enough on its own: the worker
can read 0, the gather can then free the task, and the read that
follows still hits freed memory. The flags and progress are now read
through task_queue_find(), which scans under the queue locks, and a
task only leaves the retiring list under those locks before it is
freed. The complete flag is still checked first, so a reused address
costs one wrong progress value at most. The *_task_finished bools
had no other use and are gone.

get_list_refresh_flags_test gains two unthreaded lanes. They hold the
loopback response until a gather pass of each parity, so one of them
always frees the HTTP task ahead of an updater tick. The ASan build
reports that read as heap-use-after-free on the old handler.

Fixes #19633, reported by milnet01.
2026-09-26 23:24:01 +02:00
libretroadmin f70cb54dfb video: offer only the GPUs there are, and say when one cannot present
The GPU Index entry lists the devices the running driver found rather
than a fixed sixteen: an index past them names no device, and choosing
one leaves the frontend rendering on a GPU it cannot display from, with
nothing on screen to say why.

Where that has already happened - a GPU that answers that it can
present, on a compositor that then cannot import its buffers - Vulkan
makes swapchain after swapchain and throws each away. It now says so
once, naming the GPU the index picked and where to set it back, instead
of looping in silence.
2026-09-26 23:06:23 +02:00
LibretroAdmin 9ed0db58c3 video_thread: accept a lent frame at any offset and pitch in the slot
The zero-copy lend only counted when the core pushed exactly the
pointer it was given. A core that renders its whole surface into the
loan and then crops by pointer offset - beetle-psx trims overscan that
way, with its own stride - pushed a pointer past the slot's start,
the loan lapsed, and every frame was copied as if it had never asked.

A pushed frame that lies anywhere inside the lent slot is now
published from there, with the pitch the core pushed; the slot
records the offset and the worker presents from it. Rows past the
slot are clamped as before, now counting from the offset. A push
outside the slot still lapses the loan.

threaded_video_test: the harness core gains a mode that renders the
loan and pushes a cropped window of it (8 pixels in, 4 rows down, the
loan's pitch); the zero-copy lane runs in both modes. The cropped
lane publishes 0 of 59 grants zero-copy on the old wrapper and 39,
the same as the plain lane, on this one.
2026-09-26 23:03:35 +02:00
libretroadmin 0e7a56f171 video: follow the GPU an index was chosen as, not the position
A GPU index is a position in the list its driver enumerates, which a
driver update, a BIOS change or another card reorders; the saved index
then names a different GPU, and the frontend renders on a device that
may not be able to reach the display at all. Every index - Vulkan,
OpenGL, Direct3D 10, 11 and 12, and Metal - now remembers the device it
was chosen as: where that device has moved in the list its new position
is used, where it is gone the driver's first device is used with a
warning, and index 0 means the driver's own choice as it always did.
The name is kept up to date with what was resolved, and an index set by
hand takes the device at that position and remembers it.
2026-09-26 22:44:35 +02:00
LibretroAdmin 82c06f7d17 task_queue: init(false) clears the threaded flag
task_queue_init() set task_threaded_enable only when asked for a
threaded queue and never cleared it, so init(false) after a threaded
session left the flag on and the next task_queue_check(), seeing the
flag and an unthreaded queue disagree, re-initialised the threaded
one behind the caller's back. RetroArch never hit it because its
settings path clears the flag through task_queue_unset_threaded()
first; any other user of the queue did.

The flag now follows the argument both ways. task_queue_budget_test
drops its workaround and checks that init(false) after a threaded
session stays unthreaded across a check; that lane fails on the old
queue, and the two unthreaded lanes after it fail with it.
2026-09-26 22:35:25 +02:00
LibretroAdmin e8c5c813ef task_queue: budget retirement and unthreaded handlers per check
Two audit P1s, both about task_queue_check() taking as much of the
frame as it likes on the thread that drives the frame loop:

- Retirement had no bound. Finished tasks retire (progress push,
  callback, cleanup, free) on the checking thread, and a burst of
  completions - a thumbnail scan, a bulk download - retired in one
  check however long the callbacks took.
- On the unthreaded queue every running task's handler ran once per
  check, so the frame cost grew with the running list.

task_queue_set_budget(retire_usec, retire_max, handler_usec) bounds
one check. Retirement stops once retire_max tasks or retire_usec have
gone by, after at least one; what is left stays on the retiring list,
findable, for the next check (the threaded early-out is told there is
work pending). Handlers stop once handler_usec has gone by, after at
least one due handler; the tasks not run go back to the front of the
running list, the ones that ran behind them, so the list rotates
instead of starving its tail. The unthreaded gather now keeps FIFO
order rather than reversing the list each pass, which the rotation
needs. Zero means unbounded and is the default, so nothing changes
for a caller that does not opt in.

RetroArch sets 2 ms or 32 callbacks for retirement and 4 ms of
handler time per check, leaving most of a 16.7 ms frame to the core.
task_queue_wait() loops on the check, so a blocking wait is slowed,
never changed.

task_queue_budget_test covers the count and time retire budgets and
the unbounded case on the threaded queue, and the handler budget,
rotation fairness and the unbounded case on the unthreaded one. Four
of its seven lanes fail against the old queue. Wired into
Linux-libretro-common-samples.
2026-09-26 22:33:49 +02:00
LibretroAdmin 4d7d2b0652 cpu_class: keep cpu_class_by_value under the 2 KiB thread-stack budget
The clock/capacity table was unsigned long[256] (2 KiB on 64-bit)
beside a 512-byte path, a 2704-byte frame that the stack-budget
check refuses for the 8 KiB thread stacks on PSP and GX.

32-bit values are enough (kHz clocks stay below 10^7, capacities at
1024) and the path buffer is 256 bytes; the frame is now about 1.3
KiB. No behaviour change; fast_cores_test still passes.
2026-09-26 22:32:25 +02:00
LibretroAdmin 2a4bd2162f features_cpu: rank the bigger last-level cache first within a class
The two CCDs of an X3D part are one performance class and within a
few percent of one clock, so cpu_features_get_processor_order() put
the frequency CCD first and a core pinning its emulation thread to
"the strongest processor" landed on the die without the V-cache.

Within a class the ranking now prefers the larger last-level cache,
then the higher clock. On Linux that is cache/index3/size per
processor (96M behind the V-cache CCD, 32M behind the other); on
Windows the level-3 RelationCache record whose mask covers the
processor, from the same GetLogicalProcessorInformation call the
core list already comes from. Parts with one L3 size are unaffected.

fast_cores_test: the X3D fixture with and without the cache figures,
and confined to the frequency CCD.
2026-09-26 22:08:13 +02:00
LibretroAdmin f7d228d77c features_cpu: rank processors by class, within the affinity mask
cpu_features_get_processor_order() is what a core uses to pin its
threads to the strongest processors (lrps2 pins EE, VU and GS to the
first three). It ranked by raw cpuinfo_max_freq on Linux and not at
all on Windows, and it ignored the process affinity mask. rthreads'
fast-core classification had just been fixed to use the kernel's own
P/E topology; this brings the ranking onto the same source.

- The per-processor class read moves into cpu_class.h, a private
  header included by both features_cpu.c and rthreads.c (neither may
  link the other: rthreads.c is built alone by the sample harnesses,
  features_cpu.c by the cores). Linux: cpu_core/cpu_atom, then
  cpu_capacity within 70%, then max clock within 85%. Windows 10
  1607+: CPU Sets EfficiencyClass per processor.
- The ranking sorts by class first, then clock, then SMT sibling,
  then id. An E-core can no longer outrank a P-core on clock, and on
  Windows a hybrid part is now ranked by class instead of by
  enumeration order.
- Processors outside the calling thread's affinity mask are left out
  of the order: a pin from a parent process, a container or the user
  is a boundary, not a target.
- The Linux gather ranks every processor the kernel publishes rather
  than the first N by id, where N was the thread count.

fast_cores_test now also drives the ranking through the fixture tree:
Intel hybrid (P, P siblings, then E; confined to E), ARM three-tier,
x86 favoured cores (lead the order, no class of their own; confined
to four), and an X3D part (clock decides within one class).
2026-09-26 21:51:01 +02:00
LibretroAdmin df96c5f5d6 video_filter: size the Automatic worker count from spare cores
RARCH_SOFTFILTER_THREADS_AUTO gave a filter one worker per logical
CPU. That is wrong twice over: the pool competes with the emulation,
video, audio and task threads for the cores those need, and the light
filters get slower as workers are added (EPX at 256x224 measured ~26%
slower on eight workers than on one; LQ2x and 2xSaI lose their
four-worker gain at eight). A filter sits on the frame's critical
path - every worker must finish before the frame presents - so a few
unhindered workers beat many contended ones.

Automatic now resolves as:
- physical cores the process may use (sthread_get_core_topology, or
  cpu_features_get_core_amount_physical where there is no topology),
- minus the reserved frame-critical threads: the emulation thread,
  plus the video, audio and task threads where each runs on its own
  thread (video_driver_init_filter tells the filter the count),
- minus one of headroom for the GPU driver and the OS once above two,
- capped at RARCH_SOFTFILTER_AUTO_MAX (8),
- then capped by the plugin's workload: light filters (epx, lq2x,
  crop_borders, darken) run alone, medium ones (2xsai, super2xsai,
  supereagle, phosphor2x) stop at four, heavy ones (ntsc, ntsc_crt,
  2xbr, blargg_ntsc_snes, and any plugin not in the table) take the
  budget.

On a 4-core with everything threaded that is one worker; on a
16-core X3D with everything threaded, 8 for the heavy filters, 4 for
the medium, 1 for the light. An explicit Video Filter Threads value
is passed through untouched, as before.

The arithmetic is rarch_softfilter_auto_budget(); filter_lifecycle
checks it across eleven machine/reserve/plugin combinations and that
the live answer stays within bounds.
2026-09-26 21:34:09 +02:00
LibretroAdmin 050d0a92c2 Place the video thread and task worker on the fast cores too
"Prefer Performance Cores" (thread_prefer_fast_cores) pinned only the
main and audio threads. The two other frame-critical threads never
asked: the threaded video worker (video_thread_wrapper.c) and the task
queue worker (task_queue.c), which runs the completion handlers the
frame loop retires.

Both now take the same sthread_prefer_fast_cores() placement when the
setting is on:

- video_thread_set_prefer_fast_cores() is set from video_driver.c
  before video_init_thread() and applied at the top of
  video_thread_loop().
- task_queue_set_prefer_fast_cores() is set in retroarch.c before
  task_queue_init() and applied at the top of threaded_worker(). It
  is a setter rather than a task_queue_init() argument so the samples
  and stubs that call task_queue_init() are untouched. No effect on
  GCD, which has no worker thread.

The setting's sublabel now names all four threads.
2026-09-26 21:20:09 +02:00
LibretroAdmin fae92c6116 rthreads: add sthread_get_core_topology()
Counts the physical cores the calling thread may run on, split into
fast and slow by the same classification sthread_prefer_fast_cores()
pins by. SMT siblings count once. On a homogeneous part every core is
fast and slow is 0.

This is what a worker pool needs to size itself: how many cores are
left once the frame-critical threads have theirs, and on a mixed part
whether growing further means spreading onto the slow cluster.
cpu_features_get_core_amount() gives the thread count and knows
nothing about classes.

Sources:
- Linux/Android: the process affinity mask, thread_siblings_list for
  the core grouping, and the class from the topology read (cpu_core/
  cpu_atom, cpu_capacity, or clocks within the band).
- Windows: CPU Sets, one core per (Group, CoreIndex), fast when its
  EfficiencyClass is the highest present. Before 1607,
  GetLogicalProcessorInformation core count, all fast.
- Apple: hw.perflevel0/1.physicalcpu on Apple silicon; hw.physicalcpu
  otherwise, all fast.
- Elsewhere: returns false and the caller falls back to the thread
  count.

The Linux class read is split out of the classifier so both share it.
fast_cores_test gains topology cases: Intel hybrid with SMT, the same
confined to either cluster, ARM three-tier, an X3D 16c/32t part and
an affinity of six threads on it, and an empty sysfs.
2026-09-26 21:15:45 +02:00
LibretroAdmin 2ffc7931d9 rthreads: classify fast cores from the kernel's topology, not exact clocks
sthread_prefer_fast_cores() on Linux took as "fast" exactly the CPUs
whose cpuinfo_max_freq equalled the highest in the system. That is
only right when the fast and slow clusters differ by a lot, as on
classic ARM big.LITTLE. On x86 it goes wrong:

- Favoured cores (Intel Turbo Boost Max 3.0, AMD CPPC preferred core)
  report a few percent more than their siblings, so the fast set
  shrank to those two cores and the main and audio threads were both
  pinned onto them.
- On an X3D part the V-cache CCD boosts lower than the other, so the
  emulation thread was pinned to the frequency CCD - the opposite of
  what a game wants.

The classifier now takes its answer from, in order:
1. the kernel's own hybrid classification (/sys/devices/cpu_core and
   cpu_atom on Intel hybrid parts);
2. cpu_capacity on ARM, keeping every core within 70% of the best so
   a three-tier part (prime + big + little) keeps its big cores;
3. cpuinfo_max_freq, within 85% of the highest, so favoured cores and
   X3D CCDs are one class while a real slow cluster (20-40% lower)
   still splits off.

The classification is split from the affinity read so it can be
driven from a fixture sysfs tree. fast_cores_test covers the three
sources, the affinity-already-inside cases and the two x86 regressions;
against the old logic it fails the favoured-core, X3D and three-tier
ARM cases. Wired into Linux-libretro-common-samples.
2026-09-26 21:10:52 +02:00
Rob LoachandLibretroAdmin c8a2d80e5f SDL3: Add Sensor Support (#19455)
* Add SDL3 device sensor support

* Gate sensors to port 0, zero on failure

* sdl3: Sensor comments

* sdl3: simplify sensor input

* sdl3: Fix comments

* sdl3: Fix comment

* SDL3: Log sensors on joypad connect

* SDL3: Restore sensor state on reconnect

* SDL3: Track sensor state in pad struct

* sdl3: Update comment

* input: route sensor disable requests to both the joypad and input drivers

input_driver_set_sensor stops at the first driver that reports the
action as handled. That is right for an enable, where the sensor ends
up held by exactly one driver, but wrong for a disable: joypad drivers
are documented to report the disable of a sensor they never had as a
success, so when the host sensor is actually held open by the input
driver (e.g. the SDL3 input driver's accelerometer/gyro fallback for a
pad with no sensors), the disable never reaches it. The sensor stays
open, keeps being polled, and keeps feeding the core after it asked
for it to stop.

Send disable actions to both drivers. Disabling a sensor a driver does
not hold is already defined as a no-op success, so this is safe for
every existing driver pair.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-09-26 21:12:29 +02:00
LibretroAdmin 02ec762c0c menu: show physical CPU cores in System Information
The "CPU Cores" row printed cpu_features_get_core_amount(), which is
the logical processor count, so a 16-core/32-thread 9950X3D read as
"CPU Cores: 32".

It now prints the physical count from
cpu_features_get_core_amount_physical(), and adds the thread count
when SMT makes it larger: "CPU Cores: 16 (32 threads)". Parts without
SMT keep the plain "CPU Cores: 4" form.
2026-09-26 20:58:12 +02:00
Rob Loach f5919416a9 SDL3: Add Pen Support (#19488)
* Add pen support to SDL3 input driver

* sdl3: Report pen as pointer input only

* Skip pen density lookup out of range

* Cache SDL_Window in sdl3_input_t

* sdl3: Update pen comments

* sdl3: infer pen proximity from events

* sdl3: Unneeded comment

* sdl3: map pen barrel buttons to mouse
2026-09-26 20:45:48 +02:00
LibretroAdmin 93c5294ceb tools: an extern "C" definition in a C++ file is checked against its prototype
uwp_main.cpp defines several win32_common.h functions under extern
"C" and does not include the header, so when a prototype changes no
compiler compares the two: the C callers are updated and pass, the
C++ body keeps its old parameters and links, and the mismatch is only
found at run time. That is how 0b2613d crashed Xbox on boot, and how
win32_set_video_mode was left taking four parameters against a
three-parameter prototype.

tools/extern_c_prototype_check.py reads the tree as text: every
function defined inside an extern "C" region of a .cpp or .mm file
is looked up in the .h files, and its parameter list is compared with
each prototype's, type by type with the names stripped. A definition
with no prototype anywhere is not judged. --selftest runs it over
three fixtures: a match, the 0b2613d parameter-count drift, and a
parameter-type drift. The new workflow runs both the self-test and the
tree check on every push and pull request.
2026-09-26 19:24:35 +02:00
LibretroAdmin 276ea271cc uwp: win32_set_video_mode takes its size as one word
Same drift as win32_check_window: the header and every caller
(uwp_egl_ctx, d3d11, d3d12) moved to (data, dims, fullscreen) when
the size became one word, and the UWP definition in uwp_main.cpp,
which never sees the header, kept (data, width, height, fullscreen).
Under the C calling convention it took the packed word as width, the
fullscreen flag as height, and read fullscreen from a register no
caller set - so the launch view size and windowing mode UWP asks for
were nonsense. Found by tools/extern_c_prototype_check.py.

The body unpacks the word with VIDEO_SCALE_W/H; nothing else changes.
2026-09-26 19:24:32 +02:00
LibretroAdmin de109de02a uwp: win32_check_window hands its size back as one word
dc44cc7b0a changed win32_check_window to take one unsigned* for the
size, and every caller passes one. uwp_main.cpp carries the UWP
definition of that function, and it does not include win32_common.h,
so the compiler never saw the prototype change: it kept taking width
and height as two pointers. The callers pass four arguments, the
body reads a fifth that was never passed and writes through it, and
on Xbox it writes both axes on every call - RetroArch crashes at the
first alive() check on boot. Bisected by Nejsplitter to 0b2613d, the
first build after that change to get through the MSVC lane.

Both paths now pack width and height with VIDEO_SCALE_PACK and hand
the word back, matching the prototype and win32_common.c.
2026-09-26 19:24:28 +02:00
LibretroAdmin 15eddd52b9 Video: add Video Filter Threads setting
CPU video filters always ran on one worker per detected core
(RARCH_SOFTFILTER_THREADS_AUTO). That suits the heavy filters (NTSC,
NTSC CRT, 2xBR) but over-parallelises the light ones: at 256x224 EPX
is ~26% slower on eight workers than on one, and LQ2x/2xSaI lose
their four-worker gain at eight.

video_filter_threads (Settings > Video > Output, under Video Filter;
advanced) picks the count: 0 is Automatic, the previous behaviour and
the default; 1-16 forces that many workers. Changing it while a filter
is running rebuilds the filter on the new count at once; with no filter
loaded the value applies at the next load.

video_driver_init_filter() now frees a live filter before building the
new one. Re-init while content was running (the new setting, and also
a video filter path change) previously overwrote state_filter and
state_buffer, leaking the old filter, its worker pool and its output
buffer.

tools/settings_table.reference is regenerated; it had drifted on
master (HDR, custom viewport and window size rows) independently of
this change.
2026-09-26 18:52:29 +02:00
LibretroAdmin 3c1b3a5f5e net_http: move the body-framing check out of cloud_sync_driver
cloud_sync_http_body_is_framed lived in network/cloud_sync_driver.c,
which the webdav_options, webdav_reauth and gdrive_backup samples do
not link, so all three failed with an undefined reference once
webdav.c and google_drive.c started calling it (#19631).

The check restates the two tests net_http itself uses to pick the body
framing, so it belongs beside them: it is now net_http_body_is_framed
in libretro-common/net/net_http.c, which every one of those samples
already links. cloud_sync_driver.c/h lose the copy and its includes.

webdav_reauth's stale-nonce test answered the retried GET with a 200
that carried no headers, which the read callback now correctly refuses
as unframed; the test's response gets a Content-Length like a real one.
All three samples build and pass under ASan/UBSan again.
2026-09-26 18:50:15 +02:00
LibretroAdmin b9119c03ff net_socket_ssl.h: include net_compat.h for ssize_t
The header declares ssize_t-returning functions but only pulled in
stdlib.h, boolean.h and retro_common_api.h, so it compiled only when
the includer had already brought net_compat.h in. network/tls_log.c
includes it first, and the 3DS and Vita builds fail there with
"unknown type name ssize_t". Include net_compat.h, as net_socket.h
does for the same declarations.
2026-09-26 18:50:11 +02:00
LibretroAdmin 8251314c43 tls: point at the system clock when a certificate fails on its dates
Under the default Required policy a device with an unset or badly
wrong clock rejects every server's certificate as expired or not yet
valid. The mbedTLS verify text alone reads like a server-side problem,
so log an explicit hint that the device's date and time are the likely
cause, and name the setting that restores connectivity meanwhile.
2026-09-26 18:33:36 +02:00
Anthony SchemelandClaude Opus 5.5 ba4c8b0798 net/tls: verify server certificates by default (#19626)
The mbedTLS backend set MBEDTLS_SSL_VERIFY_OPTIONAL on every connection
and ignored the verification result. A handshake therefore
succeeded against any certificate, including self-signed and expired
ones, so anyone able to intercept traffic could impersonate the
servers used by the online updater, RetroAchievements and cloud sync.

Add a tls_verify_mode setting (Required, Optional, Disabled) with
Required as the default:

- Required: the handshake fails on a bad certificate, the connection is
  refused and the reason is logged.
- Optional: the previous behaviour, now with a logged warning.
- Disabled: no verification, with a warning on every connection.

The mode is applied at startup and whenever the setting changes, and
takes effect on the next connection. Logging goes through two hooks
that the backend defines as weak no-ops, so libretro-common still
builds on its own; network/tls_log.c routes them into the RetroArch
log. Griffin builds skip the weak definitions, since tls_log.c lands in
the same translation unit. BearSSL already verifies and fails closed,
so it only gains a no-op setter.

Tested on Linux with the bundled mbedTLS: full build, and the touched
files also compile with C89_BUILD=1. A small program linked against
the built objects connected successfully to buildbot.libretro.com,
thumbnails.libretro.com, retroachievements.org and the Google APIs
hosts in Required mode, and refused self-signed.badssl.com and
expired.badssl.com with the reason logged. Optional and Disabled
connected to the self-signed host and logged their warnings.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 18:36:20 +02:00
Acts1631 7a39d86aa4 netplay: bound remote savestate size (#19614)
LOAD_SAVESTATE carries a peer-controlled output size. The client used it
to grow and zero every rollback buffer before checking decompression,
letting a remote host request excessive memory with a small packet.

Bound output size to the fixed per-session zbuffer limit before any
rollback buffer is resized.
2026-09-26 18:02:23 +02:00
LibretroAdmin 6de51c466c Revert "config: create retroarch.cfg owner-readable only"
This reverts commit e4ed924601.

The rationale was stale: credentials are no longer written to
retroarch.cfg. config_save_file strips every sensitive key into
retroarch-keychain.cfg, and config_save_credentials already calls
path_set_private() on that file after writing it. The chmod on the
main config protected nothing.
2026-09-26 17:56:52 +02:00
LibretroAdmin e4ed924601 config: create retroarch.cfg owner-readable only
The main config stores plaintext credentials (WebDAV, Google Drive and
streaming tokens, netplay passwords). With the usual umask a freshly
written file is world-readable. When config_save_file creates the file
it now chmods it to 0600 on POSIX platforms.

A config that already exists is left with the mode it has: users share
configs across accounts and read them from scripts, so changing the
mode of an existing file on every save would break those setups.
2026-09-26 17:54:56 +02:00
LibretroAdmin 788be8dbae cheevos: use memmove when dropping a URL parameter
rcheevos_filter_url_param removes a parameter by copying the tail of
the URL over it with strcpy. Source and destination are overlapping
ranges of the same buffer, which strcpy does not define; memmove does.
2026-09-26 17:54:56 +02:00
LibretroAdmin fa49a43dc2 command: bound READ_CORE_RAM and WRITE_CORE_RAM to the memory region
rcheevos_patch_address returns a pointer with no length, so
READ_CORE_RAM served whatever byte count the request asked for and
WRITE_CORE_RAM wrote every byte in the payload, both running past the
end of the core's memory region. READ_CORE_RAM also computed its reply
buffer as `40 + nbytes * 3` in unsigned int: a request for
~1431655766 bytes wrapped that to a few bytes and the reply loop then
wrote 4 GiB through it.

Add rcheevos_patch_address_avail, a thin wrapper over
rc_libretro_memory_find_avail that also reports how many bytes remain
in the region. READ_CORE_RAM clamps nbytes to that, WRITE_CORE_RAM
stops there and logs that the rest of the payload was dropped. Both
READ_CORE_RAM and READ_CORE_MEMORY additionally refuse requests above
16 KiB: the reply is 3 characters per byte in one UDP datagram, so
larger requests could never be delivered anyway, and the cap keeps the
size arithmetic well clear of the wrap. READ_CORE_MEMORY already
clamped to the descriptor length.

Also size the " -1" reply against the allocation instead of
sizeof(reply), which was the size of the pointer.
2026-09-26 17:54:56 +02:00
LibretroAdmin b5172edaa1 command: add network_cmd_bind_address
The network command interface has always bound to every interface, so
with network_cmd_enable on, any host that can reach the port can send
LOAD_CORE, WRITE_CORE_RAM or WRITE_CORE_MEMORY. Many setups rely on
exactly that (phone remotes, cabinet front-ends, home automation), so
the default cannot change without breaking them.

Add network_cmd_bind_address. Empty, the default, keeps the historical
all-interfaces bind; "127.0.0.1" restricts the interface to the local
machine. command_network_new takes the address and passes it to
socket_init, which only sets AI_PASSIVE for a NULL server, so an
explicit address binds to that interface alone. The startup log now
names what the socket is bound to.
2026-09-26 17:54:56 +02:00
Anthony SchemelandClaude Opus 5.5 4ee62a575f security: validate replay, UPnP and cloud sync input (#19628)
Three places trust data that comes from outside RetroArch:

- replay_set_serialized_data reads the embedded replay length from the
  save state. A negative value, or one shorter than the replay header,
  was passed on to the seek and write that restore the replay. Refuse
  such states.
- natt_parse_desc_node walks the IGD description XML a router serves.
  On a node without children it recursed with a NULL child and then
  read child->next, so a leaf node or an empty root crashed it. It
  also recursed only into childless nodes, so a <service> nested in
  <root>/<device>/<serviceList>, the normal UPnP shape, was never
  found. It now always recurses into children and checks the
  serviceType and controlURL text before using it.
- task_cloud_sync_fetch_server_file took the local path from a
  server-supplied manifest key as strchr(key, '/') + 1. A key without
  '/' read from address 1, and a key containing ".." or an absolute
  path was joined onto the sync directory, so a hostile server could
  write outside it. Such keys are now refused and counted as a
  failure.

The description walk moves to network/natt_desc.c and the key check
to tasks/task_cloudsync_path.c, so the new sample tests
(samples/tasks/natt_desc, and cloudsync_path_safety_test beside the
existing cloudsync test) compile the shipped code directly. Both are
added to Linux-samples-tasks.yml.

Tested on Linux: full build; touched files also compile with
C89_BUILD=1 without warnings. The workflow's commands for both new
steps pass, and the existing cloudsync test still passes under
SANITIZER=address. With the old parser or the old key handling
substituted, each test segfaults.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 17:22:37 +02:00
Anthony SchemelandClaude Opus 5.5 891d09385c cloud_sync: refuse unframed download bodies, handle allocation failure (#19631)
net_http fails a transfer that ends before its Content-Length or its
final chunk. A response with neither is delimited by the connection
closing, and net_http documents that it cannot tell a complete body
from one cut off by a dropped connection. The WebDAV and Google Drive
read callbacks accepted such a body as a successful download and wrote
it over the local save. A connection that drops mid-download could
therefore replace a save with a truncated copy, which the next sync
then uploads.

The read callbacks now treat a 2xx response without Content-Length or
"Transfer-Encoding: chunked" as a failed download and leave the local
file alone. The check is shared as cloud_sync_http_body_is_framed and
matches the headers net_http itself uses to pick the body framing.

Also NULL-check allocations that were used unchecked: the WebDAV
digest-auth chain (HA1, HA2, response, header), webdav_delete's
callback state, and the Google Drive context in
gdrive_begin_with_token. Each now fails the operation instead of
writing through NULL.

Tested on Linux: full build; touched files also compile with
C89_BUILD=1 without warnings. A small program linked against the
built cloud_sync_driver.o returned true for Content-Length and for
chunked (any case), false for no framing, "gzip, chunked" and a NULL
list. Not tested against a live WebDAV or Google Drive server.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 17:18:17 +02:00
LibretroAdmin ff1d2de796 wayland: NULL-check the drag offer before writing to it
wl_data_device_handle_drop set offer_data->dropped before checking
whether offer_data was NULL, so the check never protected anything.
Move the assignment after the check.
2026-09-26 16:28:34 +02:00
LibretroAdmin fee695d45c statestream: insert the real index on hash collision
uint32s_index_insert_exact declared a shadowing 'uint32_t idx = 0' in
the RHMAP_HAS branch, so every object whose hash collided with an
existing bucket was recorded in that bucket as index 0 instead of its
own index. Lookups through the bucket then compared against the
all-zeros pattern rather than the new object, and pop/remove could
never find the entry to take it back out. Use the caller's idx.
2026-09-26 16:28:34 +02:00
Anthony SchemelandClaude Opus 5.5 67c857e163 misc: fix crashes and leaks in wayland, drm, replay index and drivers (#19630)
Small, independent fixes for crashes and leaks on reachable paths:

- wayland: gfx_ctx_wl_get_video_size_common dereferenced a NULL output
  when no window output was set and all_outputs was empty. The DnD
  drop handler leaked the received buffer when fmemopen failed, and
  always leaked the line getline allocated.
- drm: drm_plane_setup logged a NULL drmModeGetPlaneResources result
  and then dereferenced it. init_drm read `connector` uninitialised
  when the card reported no connectors.
- uint32s_index (replay statestream): uint32s_bucket_free left the
  freed pointer and length in the bucket. RHMAP_CLEAR keeps value
  slots, so a later RHMAP_PTR on the same slot could see them and free
  the vector again. uint32s_index_pop now also refuses an empty index
  and checks RHMAP_HAS before RHMAP_PTR, which would otherwise create
  a bucket for a missing hash.
- ozone: ozone_context_reset_horizontal_list allocated a node for an
  entry without one but never stored it in the list, so it leaked.
- discord: discord_json_next_strdup leaked the previous string when a
  key repeated.
- core_backup: core_backup_add_entry leaked an empty filename.
- audioworklet, caca, sixel, vga: the init functions leaked their
  struct when the second allocation or the font renderer failed.

Tested on Linux: full build. The touched Linux files show no new
warnings; the C89 build of the Wayland and DRM files fails on this
host in system headers and existing lines, with no diagnostic on a
changed line. drm_gfx.c was compiled although HAVE_PLAIN_DRM is off.
samples/tasks/core_backup passes. The audioworklet, caca, sixel and
vga drivers are not buildable here and were not compiled.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-09-26 16:18:37 +02:00
LibretroAdmin ffa4ec336a ps3: add a PS3 display server, switch resolutions through it
Screen resolution on PS3 lived in the ps3 context driver: a list of
resolution ids it probed for at init (leaked on every reinit - the
malloc'd list was never freed), stepped through with left/right on
the menu entry and applied with OK by reinitialising the driver. On
PSL1GHT that did nothing at all: the RSX driver configures the video
output itself, from the mode the system menu has it in, and never
looked at the chosen id.

dispserv_ps3 is now the display server for __PS3__. It lists the
modes the connected display takes (480, 576, 960x1080, 720, the
1080-line scalers, 1080), marks the one in use, and switches between
them. The menu entry now opens the same resolution dropdown the
desktop platforms use, and Start resets to the system menu's mode as
it does elsewhere. A choice is stored in current_resolution_id - the
video output resolution id, as before, so existing configs keep
their modes - and the video driver is reinitialised to configure the
output in it; picking the mode already in use reinitialises nothing.
A refresh-rate-only set_resolution (the autoswitch) is refused.

ps3_display_server_resolution() is what configures the output: the
chosen mode when the display takes it, otherwise the system menu's.
The RSX driver's rsx_init_screen now uses it in place of the system
mode, which makes the setting work on PSL1GHT; the PSGL context uses
it for its device size in place of the unchecked stored id, so a
choice the current display cannot show (a config moved to another
TV) falls back instead of failing.

The mode table and those rules are in dispserv_ps3_modes.c, which
takes the display's answers through a callback and so has no SDK
dependency. The context driver loses its list and its output
size/prev/next callbacks, and global_t loses the list, count, check,
idx and initial fields nothing reads any more.

Test: samples/gfx/dispserv_ps3 links the real server and mode rules
against stubbed PSL1GHT video output queries (declared in the tree's
PS3 stub <sysutil/video.h>, now carrying PSL1GHT's layouts) for
displays taking different sets of modes with the system menu in each:
the list is exactly what the display takes with the mode in use
marked; each entry, through the label the menu prints and the parse
the menu does, selects its mode, stores its id, reinitialises once
(never for the mode in use) and reports its rate and size; the mode
the driver configures follows the choice and falls back to the system
mode for one the display lacks or an unknown id; refresh-only and
unavailable modes are refused without touching anything. Wired into
Linux-samples-gfx (ASan + UBSan); compile-matrix gains a PSL1GHT
syntax lane for the server.
2026-09-26 04:45:01 +02:00
LibretroAdmin 8e386c7642 gx: add a GameCube/Wii display server, switch resolutions through it
Screen resolution on GameCube/Wii was a private mechanism of the gx
video driver: a fixed table in gx_gfx.c, stepped through with
left/right on the menu entry and applied with OK, with no idea of
what each mode would actually run at. Issue #19380 is a user on a
240p CRT working out modes and aspect by trial and error.

dispserv_gx is now the display server for GEKKO. It lists the modes
the console's TV standard can show, each with the refresh rate and
scan type it really runs at (640x240 at 59.826 Hz, 640x480 at
59.94 Hz interlaced, 50.0801/50 Hz on PAL, progressive on a
component cable with 480p on), and switches between them. PAL's own
preferred mode (640x574) is in no fixed entry and is listed as the
default. The menu entry now opens the same resolution dropdown the
desktop platforms use. A choice is stored in current_resolution_id
- the table keeps its order, so existing configs keep their modes -
and handed to the driver, which still programs the VI since it owns
the framebuffers and the menu surface a switch has to redo. A
refresh-rate-only set_resolution (the autoswitch) is refused: the VI
cannot change rate without changing mode.

The mode table and the rules for what a mode becomes (double strike
at or below half the standard's lines, interlaced or progressive
above, clamping, field rate) move to dispserv_gx_modes.c, which has
no libogc dependency. gx_set_video_mode and the server's list both
use it, and the TV standard detection is shared, so the list cannot
promise a mode the driver programs differently.

Fixes a bug in the resolution dropdown on every platform along the
way: action_cb_push_dropdown_item_resolution parsed the label with
strtoul() inside VIDEO_SCALE_PUT_W/H, whose VIDEO_SCALE_PACK
evaluates its arguments twice. The height's second strtoul() started
where the first had left 'end', returned 0, and every mode picked
from the list went out as Wx0. Each axis is now parsed into a local
first.

Test: samples/gfx/dispserv_gx links the real server and mode rules
against stubbed libogc queries and runs NTSC, NTSC 480p, PAL 50,
EuRGB60 and MPAL: every listed mode fits the standard, exactly one
is current, and each entry - through the label the menu prints and
the parse the menu does - selects that mode, stores its id and
reports its listed rate; refresh-only and unknown modes are refused
without touching anything; config ids keep their meaning. It failed
on every selection against the old dropdown parse. Wired into
Linux-samples-gfx (ASan + UBSan) with a grep guard against the
double-evaluation shape; compile-matrix gains Wii and GameCube
syntax lanes for the server.
2026-09-26 04:21:55 +02:00
LibretroAdmin 8dc24ef3e5 menu: return to Load Core when backing out of Download a Core
The core updater list's cancel handler flushed the menu stack back to
'Online Updater'. When the list was opened from 'Load Core' there is no
'Online Updater' entry on the stack, so the flush popped all the way to
the Main Menu.

The list only ever occupies one stack level, so use the default pop
instead - it already handles removing core updater search terms and
returns to whichever menu the list was opened from.

Fixes #19625
2026-09-26 03:34:21 +02:00
LibretroAdmin 4be3eba587 midi: keep the driver usable after a failed lazy open
midi_driver_open() called midi_driver_free() when the device failed to
open, which also dropped the device lists and cleared
rarch_midi_drv_inited. After that the MIDI menu lists came back empty
and the next GET_MIDI_INTERFACE could not retry until a driver reinit.

Split the device teardown out into midi_driver_close() and use that on
the failure path: the driver stays selected, the enumerated lists stay
valid for the menu, and the next request from the core retries the
open. midi_driver_free() still does the full teardown for driver_uninit.

Also make midi_driver_set_input()/set_output() a quiet no-op while no
device is open: with lazy open the menu can change the device before
any core has asked for MIDI, and the new value is picked up when the
device is opened, so this is not the error the DEBUG log claimed.
2026-09-26 03:22:09 +02:00
Artem Lytkin fa876218d1 midi: open MIDI devices only when a core requests the interface
drivers_init enumerated and opened the MIDI devices on every start,
whether or not a core would ever use them. With the default WinMM
driver and "Microsoft GS Wavetable Synth" output that means a MIDI
stream for every Windows user, and on Windows 11 the first WinMM MIDI
call after boot starts the Windows MIDI Service and blocks until it is
up. That is the slow first launch in #19247.

Keep driver selection in drivers_init, and enumerate and open the
devices when a core calls GET_MIDI_INTERFACE. The request survives
driver reinits (SET_SYSTEM_AV_INFO, audio latency changes) and is
dropped when the core is unloaded. The menu device lists are filled
on demand without opening a device.

Frame Delay no longer relies on the MIDI stream holding the timer
period down since 5c30a403be, and the WASAPI workaround for it was
removed in 8d4bccac55.

Fixes #19247
2026-09-26 03:22:09 +02:00
Artem Lytkin cb8a7083fb companion: smooth thumbnails when upscaling (#19621) 2026-09-26 03:02:35 +02:00
github-actions 4afe682ee1 Fetch translations from Crowdin 2026-09-26 00:26:05 +00:00
LibretroAdmin 3df09555d4 (PSP/Vita/PS4) A window handed to the device is its until the next call returns
Vita nightlies since the audio rework crackle in PCSX, UAE4ARM, blueMSX,
VICE, Fuse and more (#19624). The worker released a window to the writer
as soon as the output call that handed it over returned, on the reading
that the call returns once the device has taken the window. It does not.
sceAudioOutOutput / sceAudioSRCOutputBlocking wait for the *previous*
window to finish, queue this one and return with it still in flight: the
SDKs document passing NULL as the way to wait for that last window, and
the PSP driver, traced on hardware for PPSSPP (docs/sceAudio.md), arms a
descriptor pointing at the caller's memory, waits for one descriptor to
retire and returns - nothing is copied, the DMA reads the window out of
the ring over the next period. A write landing in it in that period is
heard.

The old driver had the same early release, one call earlier still, but
an 8192-frame ring that rate control kept about 85 ms from the edge, so
the writer never reached the window in flight. The reworked ring is the
latency setting - five periods at 64 ms - and the threaded pipeline
keeps it full, so the writer sits on the freshly released period every
period and lands in audio being played.

The worker now keeps its own cursor for the next window to hand over, and
publishes read_pos one window behind it: after each call returns, the
window that call handed over is the device's and everything before it is
the writer's. The window in flight is therefore held inside the ring, so
the ring is sized to the setting directly with no device period beside
it, buffer_size() is the ring, and the floor goes to six periods - five
of room, which still holds a 30 Hz video frame of audio beside the
window in flight. Same fix on all three drivers; they share the code and
the semantics.

samples/audio/psp_ring's mock device now reads a window across the period
after the call that queued it, the way the hardware does, rather than
inside the call. On the previous drivers every lane fails with a break in
the middle of a window - frame 384 of 512, the writer's overwrite - and
the old unified psp_audio.c fails the same way under a flat-out writer.
The harness also gains the check target the unclaimed-samples job looks
for, since without one it was never being run.
2026-09-26 02:10:24 +02:00
LibretroAdmin c33b44231e replay: play back a zero-frame recording, halt a pending one, add init/shutdown regression test (#19622)
Follow-up to 368b842. Two more ways the replay state machine could
get stuck, both found by the test added here, plus the test.

1. A recording halted before its first frame produced a file with a
   header and a checkpoint and nothing after it. The player rejected
   it: bsv_movie_reset_playback() reads the first frame's events at
   init, hit EOF, raised BSV_FLAG_MOVIE_END and failed - the same path
   the truncated-file deadlock came from, now landing as "failed to
   load movie file" for a file the recorder itself wrote. Treat it as
   what it is, a replay of zero frames: leave the checkpoint pending
   at init and let the first frame restore it and hit EOF, which ends
   playback through the normal end-of-replay path.

2. movie_stop_record() only looked at the promoted handle. The record
   task installs the handle as next_handle and the run loop promotes
   it on the next frame, so halting from the menu before a frame ran
   (core paused) found no handle, returned false and left the
   RECORDING flag set and the pending handle allocated: the session
   was stuck "recording" and every later Record/Play Replay refused
   until restart. Finish a pending recording like any other; it is a
   recording of zero frames.

samples/tasks/bsv_replay_init: links the shipping tasks/task_movie.c
and input/bsv/bsvmovie.c against a stub core through the real task
queue, intfstream and VFS, in the iOS configuration (HAVE_RZSTD on,
HAVE_STATESTREAM off) the issue was filed against, and mirrors the
run loop's two-line replay protocol. Lanes: record/play round trip
(raw and zstd, core restored to the checkpoint), truncated first
checkpoint (start fails, user told, no MOVIE_END left, nothing pauses,
Close Content clears), zero-frame replay (plays and ends on frame one),
stray MOVIE_END (movie_stop clears it). Against b40db22 the truncated
lane reproduces the report exactly - paused after every frame, carried
across Close Content - and the empty lane fails on both bugs above.
Clean under ASan+UBSan with leak detection. Wired into
Linux-samples-tasks.yml next to bsv_replay_bounds_test.

Not changed: retro_vfs_file_truncate_impl calls ftruncate() with no
fflush() first. The recorder truncates every frame, so this was a
suspect for the reporter's short file, but the preceding
fseek(0, SEEK_CUR) flushes on glibc and BSD libc alike and the round
trip lane cannot make it lose a byte. The origin of the short file on
iOS is still unconfirmed; the new OSD message and the [Replay] log
lines will name the read that came up short.

Signed-off-by: LibretroAdmin <[email protected]>
2026-09-26 01:54:55 +02:00
LibretroAdmin 368b842086 replay: don't leave MOVIE_END set when playback init fails (#19622)
bsv_movie_init_playback() reads the first checkpoint and the first
frame's events before the handle is installed. Every short-read branch
in bsv_movie_load_checkpoint() and bsv_movie_read_next_events() (the
latter called with end_movie=true from bsv_movie_reset_playback()) sets
BSV_FLAG_MOVIE_END on the global input state. When init then fails, the
handle is freed and never enqueued, so BSV_FLAG_MOVIE_PLAYBACK is never
set alongside MOVIE_END.

The run loop checks MOVIE_END after every frame and issues
movie_stop() + CMD_EVENT_PAUSE. movie_stop() only clears MOVIE_END via
movie_stop_playback()/movie_stop_record(), which it never reaches
without the PLAYBACK/RECORDING flag, so the flag stays set forever:
every pause toggle runs exactly one frame and re-pauses, input looks
dead, and since Close Content goes through the same movie_stop() the
flag carries into the next core and content. Only a process restart
clears it. A truncated replay is one trigger; a replay halted before
any frame was recorded (EOF at the first frame) is another.

Fix:
- bsv_movie_init_internal(): clear MOVIE_END on the error path. A
  handle that failed to initialise leaves no flag behind.
- movie_stop(): also clear MOVIE_END when no movie is active, so a
  stray flag never survives Close Content.
- bsv_movie_start_playback(): push the "failed to load movie file"
  OSD message on failure. It runs from the task callback after
  CMD_EVENT_PLAY_REPLAY already returned success, so the command's own
  failure message never fired for an unreadable file.

Signed-off-by: LibretroAdmin <[email protected]>
2026-09-26 01:39:33 +02:00
libretroadmin b40db2251a build: ask for a reconfigure when the wayland protocol list changes
configure generates the Wayland protocol sources, so a tree configured
before a protocol was added has no way to make the new object and stops
on a missing file. config.mk depends on the generator script too, so
make asks for the reconfigure that produces it, as it already does for
configure and qb.
2026-09-25 19:53:45 +02:00
libretroadmin 0c0cd89068 wayland: tell the compositor the brightness the GL frame carries
Send Frame Luminance, off by default, describes the GL drivers' HDR
frame to a Wayland compositor with its own luminances instead of as
Windows-scRGB: the same extended-linear sRGB frame at BT.709
primaries, with the reference white at the Peak Brightness setting's
paper white and the peak at what the display or that setting gives, so
the compositor maps it from what the frame holds rather than from an
assumption about scRGB content. The parametric description is used
only where the compositor advertises the parametric and set_luminances
features with the transfer function and primaries it needs; anywhere
else, and with the setting off, the surface carries the scRGB
description exactly as before.

samples/gfx/wayland_color_management gains the parametric path against
its compositor, which rejects a creator made without the feature, a
property set twice and a description created with one unset, and the
fallback to scRGB where the features are missing.
2026-09-25 19:48:44 +02:00
libretroadmin 0e3b24938c metal: tell the display what luminances the HDR frame carries
The Metal driver sets its layer's EDR metadata, as the D3D and Vulkan
drivers set theirs: the peak is the display's where Use Display Peak
supplies one, else the user's Peak Brightness, with the optical output
scale the layer's own colour space calls for - 10,000 nits for PQ, 80
for extended-linear scRGB. A display that reads it maps the picture to
what the frame actually contains rather than to its own guess; one that
ignores it behaves as before, as does an OS below CAEDRMetadata's, and
the metadata is dropped again when HDR goes off.
2026-09-25 19:33:59 +02:00
libretroadmin ad6af1eb8d netplay: carry the session's bools in one flag word
struct netplay keeps its fourteen boolean states - server or client,
CRC checking, pausing, replay and catch-up, the forced rewind, reset
and savestate send, and the host settings - as bits of one uint16_t,
enum netplay_flags, rather than as separate bools. Conditions test the
bit; the one value sent to peers, whether pausing is allowed, is
converted to 1 or 0 before it goes on the wire.
2026-09-25 19:00:57 +02:00
libretroadmin b8d61c6834 samples: check the modes KMS lists to the modeline engine
display_servers_kms expects the connector's modes from
kms_display_server_modeline_enum(), which lists them so the engine can
score one when generation is off (#19619), rather than the empty list
of before: the mode's whole DRM timing, tagged DRM/KMS, and marked the
desktop mode where it is the live one, with an empty list and no
failure while there is no connector. Each failure exit frees the
display server it took, so a failing check reports itself rather than
the leak behind it.
2026-09-25 18:51:28 +02:00
libretroadmin 57da4697cc wayland: generate the wlroots output protocol with any wayland-scanner
The bundled wlr-output-management protocol leaves out the enum
attributes that name another interface's enumeration
(wl_output.transform, zwlr_output_head_v1.adaptive_sync_state).
wayland-scanner resolves such a reference only against the protocol it
is given, so scanners before 1.15 stop with "could not find
enumeration", which failed every build generating the bundled
protocols. The attributes annotate what an argument means and change no
generated C; the arguments keep the types the protocol gives them.
2026-09-25 18:47:14 +02:00
libretroadmin 9f3c519888 settings: name the GPU index descriptor files for the driver they hold
Four GPU index descriptor files carry the row of another driver than
the one their name gives: settings_def_gpu_index_d3d11.h holds
Direct3D 10's, _d3d12.h Direct3D 11's, _gl.h Direct3D 12's and
_vulkan.h Metal's. They are renamed for what they hold, and the four
sources that include them follow.
2026-09-25 17:35:35 +02:00
libretroadmin f6f9fb2642 video: carry the menu frame settings' bools in one flag word
video_frame_menu_settings_t keeps its 29 boolean settings as bits of
one uint32_t flags word, enum video_frame_menu_flags, rather than as
separate bools. video_driver_build_info sets the word from the same
settings as before, and the menu drivers read the bits through
'? true : false', so a flag stays true when bool is a narrow type.
2026-09-25 17:31:35 +02:00
libretroadmin bf21ab2a08 samples: time every video filter at one worker and at N
filter_plugins_test --bench, or make -C samples/gfx/filter_plugins
bench, times each video filter over 256x224 frames at one worker and
at as many workers as there are CPUs online (--threads N or THREADS=N
to choose), and prints the median per-frame time of each and the
speedup. The workers are kept alive across frames and woken once a
frame, as RetroArch's dispatcher does, so the times include what the
wake and join cost. It is not part of make check.
2026-09-25 17:31:31 +02:00
libretroadmin 2141cd1676 wayland: do not wait on the compositor after a Mutter switch
A mode switch through Mutter returns once it is asked of Mutter's
worker, which applies it later, so the Wayland display server no
longer follows it with a roundtrip on its connection. The roundtrip
waited on the compositor for the new mode's wl_output events, which
cannot come before the worker has applied the switch; they arrive
through the display server's pump when they come, as they already did.
2026-09-25 17:10:37 +02:00
libretroadmin 931fe84df6 wayland: list and switch display modes on wlroots compositors
The Wayland display server lists and switches modes on sway, Hyprland,
river and the other compositors that offer wlroots' output management,
as it does on GNOME and KDE. gfx/common/wayland_wlr_output.c speaks
zwlr_output_manager_v1 on the display server's connection: the heads
and their modes as the compositor describes them, and a switch as a
configuration carrying the last done serial and every head exactly
once, as the protocol requires - the chosen head with its new mode,
the other enabled heads as they are, the disabled ones disabled. The
compositor's succeeded, failed or cancelled answer is logged when it
comes, so nothing waits.

Mutter and KWin keep their place, and compositors with none of the
three still have no resolution list. The protocol is bundled from
wlr-protocols in deps/wayland-protocols/wlr. samples/gfx/wayland_wlr_output
checks the backend against an in-process compositor that rejects a
configuration leaving a head out or naming one twice, at manager
versions 4 and 2; the samples linking dispserv_wl.c link it too.
2026-09-25 17:10:34 +02:00
libretroadmin 886caa0007 vga: convert 32-bit menu frames too
vga_set_texture_frame downscales ARGB8888 menu frames into the BGR332
menu buffer the way it does RGBA4444 ones, taking each channel's top
bits as the core frame path does, rather than leaving the buffer
holding whatever it held before.
2026-09-25 16:49:35 +02:00
libretroadmin 8e764ec181 wayland: list and switch display modes on KWin
The Wayland display server lists and switches modes on KWin as it does
on GNOME, so Screen Resolution and the refresh rate autoswitch work
under KDE Plasma. gfx/common/wayland_kwin_output.c speaks KWin's own
output protocols on the display server's connection: the outputs and
their modes from kde_output_device_v2, published through its registry
from version 21 and as globals before it, and a switch through a
kde_output_management_v2 configuration, whose applied or failed answer
is logged when KWin gives it, so nothing waits. The modes are those of
the output the window is on, matched by connector name.

Mutter keeps its place on GNOME, and every other compositor has no
resolution list, as before. The two protocols are bundled from
plasma-wayland-protocols (MIT-CMU) in deps/wayland-protocols/kde, so
nothing of KDE's is needed to build. samples/gfx/wayland_kwin_output
checks the backend against an in-process compositor speaking both
forms; the samples linking dispserv_wl.c link it too.
2026-09-25 16:44:44 +02:00
libretroadmin 55b5c0f81d ntsc_crt: decode bands of scan lines on the workers RetroArch asks for
Demodulation runs in two passes. crt_demodulate_begin adds the noise,
finds vertical sync and, in line order, the horizontal sync and colour
carrier that carry from one line to the next, recording each active
line's decoding parameters. crt_demodulate_lines then equalises and
resamples a range of lines from those parameters alone, with its own
copies of the equalisers and its own line scratch in place of the
function-static one.

The filter's packets pass converts the input, modulates it and runs
the first pass once a frame; each worker decodes a band of lines and
copies the band of output rows those lines own. The output is the
single-worker output frame for frame, and about three quarters of a
frame's work is spread across the workers.
2026-09-25 16:43:24 +02:00
libretroadmin 96bc5a3f66 blargg_ntsc_snes: start each band at its own burst phase; run on the workers RetroArch asks for
The colour burst phase advances one step a row, so each worker's band
of rows starts at the frame's phase plus its first row, where a single
pass would be on that row. The per-frame toggle of the phase, used when
fields are not merged, happens once a frame before the workers run
instead of once per worker, which also leaves the workers nothing
shared to write. The filter runs on as many workers as RetroArch
creates it with, giving the single-worker output frame for frame.
2026-09-25 16:28:16 +02:00
libretroadmin bb7602686d phosphor2x: write the last pixel of each line; run on the workers RetroArch asks for
The horizontal stretch writes the pixel past the last source pixel as
black instead of blending it from whatever the buffer held, so the
partial clear of the output that it relied on - its length taken in
pixels as bytes, covering only part of each band - is gone. The blue
phosphor pass stops at the end of the line rather than writing one
pixel past it. The image is unchanged.

The filter runs on as many workers as RetroArch creates it with,
giving the single-worker output.
2026-09-25 16:23:21 +02:00
libretroadmin 758781e170 lq2x: take the rows above and below from the frame; average XRGB8888 per channel
Each pixel's upper and lower neighbours are the rows above and below it
in the frame, repeating the edge row only at the frame's own top and
bottom. The last worker's band had used its own row as the one below
for every row, which with one worker is the whole frame, so the filter
compared neighbours only across and above. It now runs on as many
workers as RetroArch creates it with, giving the single-worker output.

The XRGB8888 averages clear the lowest bit of every byte before
halving, so an odd sum in one channel no longer shifts into the top of
the channel below it: averaging green 1 with green 0 had given blue 128.
2026-09-25 16:23:17 +02:00
LibretroAdmin a1fc1c20cd KMS: list the connector's modes to the modeline engine (#19619)
kms_display_server_modeline_enum() returned 0, so the engine started
on KMS with no modes at all. With modeline_generation off there is
no generated candidate either, so mode selection had nothing to score
and failed with "Could not find a video mode that meets your specs".

Fill the list from g_drm_connector->modes, each with its whole DRM
timing, tagged MODELINE_TIMING_DRMKMS, with the entry equal to
g_drm_mode marked as the desktop mode - the same shape the XRandR
server already provides. Setting one goes through the existing path:
modeline_set() copies the timing into crt_switch_st and drm_ctx
rebuilds the drmModeModeInfo from it, so an existing mode round-trips
exactly.

g_drm_connector is NULL between a context teardown and the reinit;
the list is empty then rather than an error, so a generated mode
still goes through.

Not a regression from the in-tree engine: the previous KMS path
used switchres's "dummy" display, which enumerated nothing either.
With generation on the existing modes now compete in scoring the
way they already do under X11.
2026-09-25 16:14:31 +02:00
libretroadmin 313ddb3197 2xbr, 2xsai, super2xsai, supereagle, epx: run on the workers RetroArch asks for
These filters split the frame into row bands across as many workers as
RetroArch creates them with, one per core, rather than one; each band
reads its neighbouring rows from the frame, so the output is the same
as with a single worker. A single-core machine still gets one worker.

The filter plugin harness runs every video filter that takes more than
one worker with 2, 3 and 4 workers at once at an odd frame size and
checks the output byte for byte against a single worker's, and the
Linux-filter-plugins workflow runs it under ThreadSanitizer as well.
2026-09-25 16:14:02 +02:00
LibretroAdmin 176e996302 CRT: .switchres.ini geometry overrides survive the slider write (#19618)
The core/directory/game .switchres.ini files are loaded through
crt_adjust_ini(), then switch_res_crt() writes the H-Size, H-Shift
and V-Shift sliders onto the generator right before modeline_get(),
so an h_size/h_shift/v_shift the files set never reached the mode:
with the sliders at their defaults the override became 1.0/0/0.

The sliders are now the base the files refine. crt_adjust_ini()
seeds the generator from the sliders before loading the files,
records which of the three the files changed and their values, and
switch_res_crt() writes the recorded value for those until that
slider is moved, after which the slider wins for that one value.
The write still happens on every switch because the generator
clamps the geometry in place per mode.

Two things this uncovered:

- current_content_name was never filled, so the content-change
  check fired on every mode change and re-parsed switchres.ini and
  all the override files each time. It is now filled from the
  content basename the way crt_adjust_ini() records it.

- On the first switch the slider values carried in from the config
  counted as a move; crt_adjust_ini() now takes the slider state as
  seen, so a config value does not clobber a file's override on the
  first mode.

samples/gfx/crt_switch_modeline gains a case with a game override
file: its h_shift/v_shift reach the generator with the sliders at
default, survive the next mode change, are parsed once per content
rather than per switch, a moved slider takes only its own value
back, and other content without a file returns to the sliders.
2026-09-25 16:09:52 +02:00
libretroadmin 9390e5d274 2xbr: compare precomputed YUV keys in the edge decisions
Once a frame, before the workers run, the filter fills a buffer with
one YUV key per source pixel, RGBtoYUV of its RGB565 value. The edge
decisions load the keys of the 5x5 neighbours with the same clamped
indices as the pixels and compare them by neighbour name, so each
comparison is a subtraction instead of two conversions and table
lookups, and the outer ring of the neighbourhood, used only in
comparisons, is no longer loaded as pixels. The output is unchanged;
at 256x224 the XRGB8888 path takes about 60% of its previous time.
2026-09-25 16:08:43 +02:00
libretroadmin b0f0d9ba26 2xbr: make the XRGB8888 edge decisions through the YUV table; build filters optimised
The XRGB8888 path makes its edge decisions at RGB565 precision through
the same YUV table as the RGB565 path, two lookups per comparison in
place of a double-precision YUV transform, and blends at full 8-bit
precision as before. At 256x224 it runs in about the RGB565 path's
time, around a fifth of what the floating-point comparisons took.
df8 and eq8 are macros local to the file rather than exported symbols.

The video filter Makefile defaults to a release build, as the DSP
filter Makefile does, so the plugins are built with -O2 unless
build=debug is given.
2026-09-25 16:05:28 +02:00
libretroadmin fa9eb93e04 filters: link the plugins against libm; load and run them in CI
The video filter and audio DSP filter Makefiles name libm after the
objects, so --as-needed keeps it and the plugins that use math
functions record it; they loaded only into a host that had libm
already.

samples/gfx/filter_plugins loads every plugin with RTLD_NOW into a
host that does not link libm and runs a frame or a run of audio blocks
through it at its default settings. The Linux-filter-plugins workflow
runs it as built and again under ASan, UBSan and LeakSanitizer.
2026-09-25 15:54:02 +02:00
libretroadmin 246d4372c9 2xbr: filter over the whole 5x5 neighbourhood
Each pixel reads the two rows above and below it in the frame, and at
the frame's edges the rows and columns repeat the edge rather than
reading outside it. With the filter on its single worker the row step
had been zero for every row, so the neighbours above and below were
the pixel's own row, none of 2xBR's edge rules could fire and the
output was plain pixel doubling, identical to normal2x. It is now
2xBR's.
2026-09-25 15:53:59 +02:00
libretroadmin 0565336164 ntsc_crt: clear the CRT output buffer, keep the modulator's arithmetic defined
The CRT output buffer is cleared when it is allocated and whenever it
is resized. crt_demodulate does not write every pixel of it, and what
it leaves is copied to the screen, so without the clear those pixels
showed whatever the heap held and changed from run to run.

The noise generator wraps in unsigned arithmetic, and the negative
filter coefficients and luma samples are scaled by multiplication
rather than left-shifted; the output is unchanged.
2026-09-25 15:53:55 +02:00
libretroadmin c98957fdcb ntsc, ntsc_crt: widen RGB565 input with its top bits copied down
The NTSC filters widen RGB565 channels to 8 bits by replicating each
channel's top bits into the new low bits, as the rest of RetroArch
does, so white enters the filter at full scale and greys stay
neutral, rather than as 248/252/248 with green a step ahead of red
and blue.
2026-09-25 14:46:58 +02:00
libretroadmin dba7c1e66a fpga, vga: step source rows by the frame's pitch
The scale loops find each source row pitch bytes into the frame
rather than width pixels, so a core frame whose rows are padded is
read at its own stride instead of coming out skewed. Tightly packed
frames, and the menu frames these drivers pack themselves, read
exactly as before.
2026-09-25 14:21:18 +02:00
libretroadmin cc0021051f ci: compile the DOS VGA driver in the compile matrix
vga_gfx.c is built against stub dpmi.h and pc.h carrying the DPMI
register block, the real-mode interrupt call, dosmemput and the port
I/O it uses, in the shapes DJGPP gives them, with declarations after
statements rejected. Outside the DJGPP job nothing compiled it.
2026-09-25 14:00:29 +02:00
libretroadmin 581c2796ac pixconv: SSE2 for the last scalar converters, NEON for ARGB8888 to 4444
conv_argb8888_rgba4444, conv_argb8888_0rgb1555 and conv_rgba4444_rgb565
have SSE2 paths, and conv_argb8888_rgba4444 a NEON one. The 32-bit
sources narrow with _mm_packs_epi32, the 4444 results sign-extended
first so values past 0x7fff are not saturated. conv_bgr24_argb8888
reads four pixels as three 32-bit words on little-endian hosts that
have no NEON path. conv_bgr24_rgb565 keeps its byte loop, which the
word loads did not beat.
2026-09-25 13:50:01 +02:00
libretroadmin 7ebb02cdcc gl1: declare the Vita depth-test flag at the top of its block
The compile matrix builds gl1.c's Vita branches against a vitaGL stub
over the host GL headers, rejecting declarations after statements the
way the PSP lane does; nothing else compiles them outside the Vita
toolchain.
2026-09-25 13:41:27 +02:00
libretroadmin 4e12008793 screenshot: free the task state after a synchronous dump
screenshot_dump() without a thread writes the file and then releases
its state and, once written, the caller's buffer - the same ownership
the task path has, where a failed write leaves the buffer the caller's
to free. It also releases the state when the screenshot directory
cannot be created. --max-frames-ss and save-state thumbnails both take
the synchronous path, and each call left the state and a full captured
frame behind.

The ASan/UBSan workflow takes a screenshot on exit under
LeakSanitizer, with only the system libraries suppressed, and fails on
any leak left over.
2026-09-25 13:41:23 +02:00
libretroadmin d06b53d690 gl1: upload core and menu frames without a staging copy
gl1_draw_tex updates its texture in place with glTexSubImage2D, reading
the frame at its own pitch through GL_UNPACK_ROW_LENGTH, and respecifies
the storage, cleared, only when the power-of-two size or internal
format changes. XRGB8888 frames go up straight from the core's buffer,
RGB565 frames as GL_UNSIGNED_SHORT_5_6_5 on GL 1.2 and newer, and
RGUI's RGBA4444 framebuffer as it is; the no-GL_EXT_bgra swizzle writes
just the frame, tightly packed. GL 1.1 without packed pixels, the HDR10
tonemap and the Vita build, which has no GL_UNPACK_ROW_LENGTH, keep
their staging buffers.

samples/gfx/gl1_upload runs the driver under Xvfb with a test core and
checks the screen pixel for pixel down every upload path, threaded
video and frame size changes included, in the Linux-gl1-upload
workflow.
2026-09-25 04:33:38 +02:00
libretroadmin 9a9dc0ffde pixconv: NEON for the remaining converters; fix SSE2 565 to 1555
conv_rgb565_0rgb1555, conv_0rgb1555_rgb565, conv_rgba4444_rgb565,
conv_bgr24_argb8888, conv_bgr24_rgb565, conv_argb8888_0rgb1555,
conv_argb8888_bgr24, conv_abgr8888_bgr24 and conv_yuyv_argb8888 have
NEON paths, so every converter the scaler dispatches to is vectorised
on ARM. YUYV keeps the scalar path's 16-bit fixed point, with
vqmovun_s16 doing the clamp. The SSE2 path of conv_rgb565_0rgb1555
shifts red and green down one bit, matching its MMX and scalar paths.

The pixconv test covers every converter, including the 24-bit and
YUYV sources, so each SIMD path is checked against a reference on
x86 and, in the cross-arch lane, on aarch64, armv7 and PowerPC.
2026-09-25 04:13:43 +02:00
libretroadmin 1375d13f74 samples: stub the HDR metadata peak for vulkan_validation
vulkan_common.c asks the video driver for the peak its HDR metadata
carries; the sample's stub answers with the driver's own value, as the
frontend does with no display peak in use.
2026-09-25 04:01:17 +02:00
libretroadmin 6d536cff0f video: Use Display Peak, and the display's peak on Windows and Android
Use Display Peak, off by default, makes the peak luminance the display
reports stand in for Peak Brightness wherever a peak is used: what
cores are told through GET_HDR_MAX_NITS, and the HDR metadata the
drivers send - DXGI's for D3D11 and D3D12, Vulkan's vkSetHdrMetadataEXT
(which reaches the compositor under Wayland), and the KMS blob. Off, or
with no reported peak, every value stays as it was: the setting for
cores, each driver's fixed 1000 nits for its metadata.

Windows reports the panel's peak through DXGI_OUTPUT_DESC1, read where
the HDR check already reads it and passed on only when it changes, and
Android through HdrCapabilities (API 24), queried once by its display
server; Wayland and KMS already did. Apple's EDR APIs give a headroom
over the current SDR white, never an absolute peak, so macOS and iOS
report none.
2026-09-25 03:21:58 +02:00
libretroadmin 9612092490 video: show the display's own peak luminance beside the HDR setting
The Wayland and KMS contexts learn the display's peak luminance where
it is on offer - the compositor's description of the output
(wp_color_management_output_v1, its target luminance) and the sink's
EDID - log it, and hand it to the video driver, whose Peak Brightness
entry shows it beside the setting, as "1000 (display: 650)". The
setting's own value is unchanged and nothing applies the display's:
peak luminance is something users calibrate around.

The compositor's description is read without waiting, from events the
context already dispatches: its target luminance, not the transfer
function's range - 10,000 nits for PQ - which says nothing about the
panel. samples/gfx/wayland_color_management covers the query against
its compositor, with and without a target luminance.
2026-09-25 02:54:09 +02:00
libretroadmin 393c9b8b24 pixconv: NEON paths for the 0RGB1555, RGBA4444 and BGR24 converters
conv_0rgb1555_argb8888, conv_rgba4444_argb8888, conv_0rgb1555_bgr24
and conv_rgb565_bgr24 convert eight pixels per step on ARM NEON. Each
channel is shifted to the top of its lane with its own top bits
inserted beneath, so narrowing gives the bit-replicated 8-bit value;
the BGR24 variants store through vst3_u8. Output matches the scalar
path bit for bit.

The cross-arch samples lane runs the pixconv test under qemu on
aarch64, armv7 with and without NEON, and big-endian PowerPC, and the
scalar test build leaves out NEON as well as SSE2 and MMX.
2026-09-25 02:52:59 +02:00
github-actions 55286e6c00 Fetch translations from Crowdin 2026-09-25 00:32:03 +00:00
libretroadmin 82c33d2c3e kms: HDR10 output for the GL drivers
The KMS context drives an HDR10 sink when HDR is on: where the sink's
EDID declares SMPTE ST.2084 and the connector has HDR_OUTPUT_METADATA
and a Colorspace with BT2020_RGB, it sets static metadata built from the
user's peak (capped at the sink's), Rec.2020 and 10 bits, then scans out
XRGB2101010 from a 10-bit EGL config. The properties are set before the
surface exists; if the connector refuses them, or there is no 10-bit
surface or config, what changed is put back and the output stays SDR,
so a PQ picture never reaches a sink not told to expect it. The
connector is restored on the way out. GFX_CTX_FLAGS_HDR10_FRAMEBUFFER
tells the video driver its framebuffer is Rec.2020 PQ.

gl and glcore render it through their scRGB offscreen and encode pass,
finished in PQ: the encode shaders gain an HDR10 output switch - a
uniform in gl2's, a UBO field in glcore's, off everywhere else - that
rotates the composited scRGB to Rec.2020 and PQ-encodes it at absolute
luminance, a PQ source passing through as it came. The native HDR
screenshot read-back, which reads an FP16 backbuffer, stands aside.
gfx/common/drm_hdr.c holds the EDID parse and metadata build as pure
functions, which samples/gfx/drm_hdr checks, including the blob layout
against the kernel's own struct.
2026-09-25 02:17:16 +02:00
libretroadmin a1ae21a756 video: the GL GPU index on X11 EGL and KMS, and only where it can choose
The X11 EGL context chooses its GPU as the Wayland one does: it lists
EGL's devices, makes its display on the chosen one through
EGL_EXT_explicit_device, falls back to the default display where the
device cannot drive it, and publishes the list to the menu. The KMS
context's display follows the card it opens, through GBM, so there the
chosen device's DRM card (EGL_EXT_device_drm, now kept per device by
egl_common) is tried before the others, which follow in their usual
order when it has no usable output.

The GL drivers' GPU Index entry appears only while the context has
published such a list; GLX, and any context without device enumeration,
has nothing to choose from and shows none.
2026-09-25 01:51:15 +02:00
libretroadmin 0f1b7c8ca9 wayland: present the FP16 scRGB surface opaque
The GL drivers' scRGB frame carries alpha - their frame target clears to
transparent black and the encode pass keeps its alpha - and on Wayland
an RGBA16F surface's alpha is blended by the compositor, the opaque
region being only a hint, so the letterbox around a core's frame showed
whatever lay behind the window. The context now creates that surface
with EGL_EXT_present_opaque, which egl_common applies to the next window
surface when the context asks, and scRGB is offered only where the
display has it.
2026-09-25 01:42:53 +02:00
libretroadmin e795894c9b ci: build the configuration with each GPU index owner on its own
compile-matrix.sh builds configuration.c with EGL on and Vulkan off, and
with Vulkan on and EGL off. Each video API owns a GPU index setting and
its default under its own gate, and a build has any mix of APIs; a
default defined under another API's gate builds only where both are on,
as the GL GPU index default did before it moved under HAVE_EGL, which
broke every EGL build without Vulkan, the PS Vita's among them.
2026-09-25 01:19:10 +02:00
libretroadmin 6dc05aa557 Remove unused variable 2026-09-25 01:19:03 +02:00
libretroadmin 9af8aea1a5 config: define the GL GPU index default in every EGL build
DEFAULT_GL_GPU_INDEX is defined wherever the gl_gpu_index setting is,
under HAVE_EGL, rather than inside the Vulkan block, so EGL builds
without Vulkan build again.
2026-09-25 01:11:12 +02:00
sonninnos f494e600a6 Use common value label for video_shader_watch_files since it is a normal bool 2026-09-25 02:13:19 +03:00
libretroadmin 9b2a7f8bd1 video: keep the GL drivers' GPU list for the menu
The GPU list the Wayland context publishes for the GL drivers has a slot
of its own, for OpenGL and OpenGL ES alike, so the GPU Index entry names
each device and steps through them as it does for Vulkan and Direct3D.
A device's label leaves out the vendor where its renderer string
already begins with it, as NVIDIA's does.
2026-09-25 01:09:34 +02:00
libretroadmin a618658334 pixconv: packed 16-bit pixel expanders; fix rgba4444 and B5G5R5A1 alpha
pixconv.h provides single-pixel expanders from RGB565, 0RGB1555,
RGBA4444 and ARGB4444 to 32-bit that move every channel into its byte
lane and widen them together with one shift and mask (a multiply by
0x11 for 4-bit channels). The pixconv scalar paths, dxgi_copy, the PNG
and BMP encoders and the HUB75 driver use them. conv_argb8888_rgba4444
takes the high nibble of each channel, dxgi_copy reads and writes
B5G5R5A1 alpha at bit 15 and widens it to full scale, and the MMX
paths load and store through memcpy since rows need not be 8-byte
aligned.

libretro-common/samples/gfx/pixconv runs every 16-bit input through
each converter with SSE2, with MMX only and with no SIMD, and is part
of the libretro-common samples workflow.
2026-09-25 01:00:50 +02:00
libretroadmin 06781a4041 menu: offer the GPU Index entry for the GL drivers
The GPU Index entry is a row per video driver, bound to that driver's
own index and added only while it runs; gl, glcore and gl1 get theirs,
bound to gl_gpu_index, so the GL GPU list the Wayland context publishes
reaches the menu. The entry's label reads the bound index again, as it
does for every other driver.
2026-09-25 01:00:46 +02:00
libretroadmin fa408a551f wayland: choose the GPU the GL drivers render on
The GL drivers on Wayland take a GPU index as Vulkan does. The context
lists EGL's hardware devices through EGL_EXT_device_enumeration - under
glvnd every vendor's, so an NVIDIA GPU beside a Mesa one - labels them
by vendor, renderer and DRM node, and makes its display on the chosen
one through EGL_EXT_explicit_device. Index 0 leaves the choice to the
EGL implementation, as before; a device that cannot drive the display
gives way to the default with a warning. The list reaches the GPU Index
entry like Vulkan's, whose label now shows the running API's own index.

egl_common gains the device list and a device hint for the next display
it makes, usable by any EGL context.
2026-09-25 00:38:18 +02:00
libretroadmin b24cb4ee5c wayland: HDR for the GL drivers through the compositor's colour management
The GL drivers get HDR on Wayland the way they have it on Windows: an
FP16 framebuffer composited as scRGB, which gl, glcore and gl1 already
render to when the context reports GFX_CTX_FLAGS_SCRGB_FRAMEBUFFER. No
EGL driver tags a Wayland surface's colour space for GL, so the context
does it through wp_color_manager_v1: where the compositor offers the
windows_scrgb feature with a perceptual intent, EGL has an RGBA16F
config (EGL_EXT_pixel_format_float) and HDR is on, the context takes
that config, marks the surface opaque and tags it Windows-scRGB. The
tag is set from the image description's ready event, as the protocol
requires, during the context's own dispatch, so nothing waits for it.
The HDR settings are offered where the compositor and EGL could do it;
GL HDR is scRGB only, so HDR10 maps to it. The Vulkan context binds the
manager but never tags its surface, which the Vulkan driver's WSI owns.

gfx/common/wayland_color.{c,h} holds the protocol client, built on the
colour-management protocol now bundled in deps/wayland-protocols.
samples/gfx/wayland_color_management runs it against an in-process
compositor that records every fatal rule a client could break -
Windows-scRGB without the feature, a description set before it is
ready, an intent not offered, a second colour-management object on a
surface - and CI runs it under ASan and UBSan.
2026-09-25 00:05:05 +02:00
libretroadmin d048deca5c wayland: report the DRM lease once, where the log is on to show it
The Wayland display server's DRM lease report is only a log line, so
it is started once per instance through wl_display_server_report_lease()
and only where the log is on: video_display_server_init() starts it when
it makes or keeps dispserv_wl with verbosity enabled. The early display
server init runs before the command line turns the log on, and its
report used to race that, appearing on some starts and not others;
now the report comes with the video driver's context, after
"Found display server", on every start, and without the log no report
connection is made at all.
2026-09-24 22:41:17 +02:00
libretroadmin f24b1a7217 video: keep the early Wayland display server for the driver's session
The display server set up early from WAYLAND_DISPLAY, before the video
driver has a window, is kept when the driver's context declares Wayland
too, instead of being destroyed and made again. dispserv_wl keeps a
connection of its own whatever window the driver makes, so the early
one serves the session as it is, and startup makes one connection and
reports the DRM lease once.
2026-09-24 22:25:48 +02:00
libretroadmin 914e2e7cea frontend: give the audio thread a short EEVDF slice, without privilege
Since Linux 6.12 a time-shared thread may ask for its own EEVDF slice
through sched_setattr(), with no capability or rlimit. A new backend,
frontend/thread_elevation/eevdf.c, asks for the kernel's shortest, 0.1
ms, so the thread gets an earlier deadline and the CPU sooner when it
wakes, with its share of the CPU unchanged. It leaves a real-time,
batch or idle thread alone, and counts only when the slice reads back as
asked, which kernels before 6.12 do not do. Under two busy threads on
one CPU, an audio-shaped thread's p99 wake-up lateness drops from about
3 ms to under 0.1 ms, and a burst longer than the slice completes no
later than before.

The chain gains additive backends: self-acting, tried first, and never
ending the chain, since a scheduler hint complements a raised priority
rather than standing in for it; a continuation never reaches one.
thread_elevation_raise_current() reports one that applied, and the
audio thread logs it. samples/frontend/thread_elevation covers additive
ordering and continuation, and runs the real backend on the kernel it
finds: granted with the slice read back where it is supported, and a
real-time thread left as it is.
2026-09-24 22:08:41 +02:00
libretroadmin 3b3453e38b wayland: native contexts use the Wayland display server, which never waits
The Wayland contexts declare RARCH_DISPLAY_WAYLAND once their display
is up, as the X11 ones do, so a native Wayland session gets dispserv_wl
- the output's refresh rate, size, metrics and EDID, and on GNOME
Mutter's resolution list and refresh-rate switching - rather than the
null display server. dispserv_wl keeps a connection of its own, used
only on the thread that calls it, and makes no roundtrip: init asks for
the registry and returns, and every call first takes whatever the
compositor has sent without waiting for more, so the output is bound
and described over the first few calls. The DRM lease report, which
needs answers, runs on a detached thread with a connection of its own.

compile-matrix.sh builds the D-Bus, Mutter and RealtimeKit units and
the elevation chain as a target with no POSIX headers, against
tools/platform_stubs/no_posix, where any of them reaching <unistd.h>,
<poll.h>, <fcntl.h>, <dlfcn.h> or <sys/*.h> outside its platform gate
fails. samples/gfx/wayland_drm_lease builds with threads, as RetroArch
does, and waits for the report; samples/gfx/mutter_displayconfig checks
that the output is described without a roundtrip.
2026-09-24 21:32:01 +02:00
libretroadmin 1d52796dbb Silence qualifier and unused-variable warnings on MSVC, glibc and X11
Where retro_atomic_int_t is volatile (the MSVC backend), the address
of a waiter's flags or of the audio reinit request travels through
volatile-qualified pointers: the runtime-resolved wait-on-address
entry points and the MONITORX/UMONITOR helpers take a volatile
address, and wasapi hands the reinit request to the watcher thread
through an explicit void pointer, which mmdevice_thread casts back.

The autodetect index parser keeps the strchr() result in a const
pointer, so the qualifier-preserving strchr of C23 glibc has nothing
to discard, and the X11 modeline path drops a crtc counter it never
reads.
2026-09-24 21:30:22 +02:00
libretroadmin 1865a93c24 griffin: build the D-Bus/Mutter/RealtimeKit units only on Linux and BSD
Griffin mirrors Makefile.common, which adds dbus_runtime, dbus_common,
mutter_displayconfig and rtkit only for Linux and BSD targets, so
griffin.c includes them under the same platform set instead of under
HAVE_THREADS alone. Windows, consoles and other griffin targets no
longer see these units at all.

mutter_displayconfig.c keeps <unistd.h> inside RARCH_HAVE_MUTTER_DC,
next to the pipe/poll code that needs it, so the file stays an empty
translation unit on any platform where the Mutter backend is off.
2026-09-24 21:16:11 +02:00
libretroadmin b8f36709aa mutter: follow GNOME's display modes on a worker, over libdbus loaded at run time
Mutter's org.gnome.Mutter.DisplayConfig is used by every threaded Linux
and BSD build, through libdbus loaded at run time, and nothing that
calls into it waits on the bus. One worker for the process connects,
subscribes to MonitorsChanged and publishes Mutter's state, reading it
again after every change; it waits in poll() on the bus socket and a
wake pipe. The resolution list and availability are answered from the
published state under a lock the worker takes only to swap it, so the
menu, the refresh-rate check and the runloop's autoswitch no longer
make a D-Bus round trip. A switch is checked against that state at once
- a size or rate the head does not list is refused without asking
Mutter - and applied by the worker against the state Mutter reports
then, with the serial, layout and carried-over settings as before.
Until the worker has answered, or where Mutter is not there, callers
keep their own behaviour.

dispserv_wl and dispserv_x11 ask for Mutter at each call rather than
once at init. The runtime libdbus table gains the container, append,
match and dispatch entry points this needs, checked against libdbus's
headers by samples/frontend/dbus_runtime. samples/gfx/mutter_displayconfig
builds on the loader and waits for the worker where it used to get a
synchronous answer, and its mock announces MonitorsChanged as Mutter
does.
2026-09-24 21:12:46 +02:00
libretroadmin e1a497fbdd dbus: suspend the screensaver on a worker, through libdbus loaded at run time
Screensaver inhibit over org.freedesktop.ScreenSaver works in every
threaded Linux and BSD build, on libdbus loaded at run time. One worker
for the process loads the library, opens and closes the session-bus
connection and makes the Inhibit and UnInhibit calls; the main thread
only publishes the state it wants under a lock the worker never holds
across a call, and never waits for an answer. A generation that moves
on every open keeps a slow answer from being reported for a newer
request, and losing the session bus ends the connection rather than
RetroArch.

On X11, XScreenSaver is asked alongside D-Bus on the main thread, where
its display lives. xdg-screensaver, whose suspend lasts as long as the
window, starts only once D-Bus has failed: at once when that is already
known, otherwise from x11_check_window() when the worker's answer
arrives. Wayland compositors without idle-inhibit use the same worker.
2026-09-24 20:35:21 +02:00
libretroadmin 340e129377 dbus: load libdbus at run time for RealtimeKit on every Linux build
RealtimeKit is a backend of every threaded Linux build: libdbus is
loaded when first needed through gfx/common/dbus_runtime, which mirrors
libdbus's public, ABI-stable structs and constants under names of its
own, so no D-Bus headers or link dependency are needed and a system
without the library runs as before. The rtkit helper loads it on its
own thread, so neither the audio thread nor the main thread waits on
the dynamic loader or the bus, and whichever thread loads first
publishes the table without anyone waiting. HAVE_DBUS no longer gates
the backend.

The helper now tells apart what can happen: libdbus not available, no
system bus, rtkit not running, rtkit refusing with its D-Bus error
name, and each grant. samples/frontend/dbus_runtime checks the mirror's
sizes, alignments, field offsets and constants against libdbus's own
headers, and the no-pkg-config CI job runs it.
2026-09-24 20:05:17 +02:00
libretroadmin 7193a19c47 qb: find D-Bus, Wayland, libdecor, OpenSSL and Qt without pkg-config
Without pkg-config, configure finds the packages it used to need
pkg-config for through check_nopkg(): the include directories are
looked for under the include and library directories, where D-Bus keeps
its arch header, and a probe program must build and link before the
package counts, with the CFLAGS and LIBS pkg-config would have given.
Qt is described by its own qmake -query, headers, libraries and mkspec
alike, and wayland-scanner answers for its version, with the protocol
generator using deps/wayland-protocols. With pkg-config present none of
this runs and configure is unchanged.

A package asked for by name stops configure when nothing can find it:
check_pkgconf() dies on a forced package without pkg-config unless a
library check or a check_nopkg() probe follows, and a forced Wayland
stops configure when its libraries or scanner are missing rather than
being disabled with a notice. The no-pkg-config CI job installs these
packages, configures with --enable-dbus and checks each is found and
built, then checks that a forced rsound fails and a forced pulse, which
has a library behind it, still configures.
2026-09-24 19:08:09 +02:00
libretroadmin ea89f665f2 settings: route every packed-row read and write through the half accessors
The custom viewport origin and size, the window size pairs and the
desktop menu window geometry each keep both axes in one settings_t
word, and each menu row owns one half of it. The generic uint/int
display strings, the custom viewport width/height display strings,
the dropdown builders and commits, and the Qt spin box, combo box and
radio button widgets all dereferenced value.target directly, so these
rows showed and edited the whole word: a 960x720 viewport displayed
its width as 62915280, stepped in units of 65536, clamped to 9999 in
Qt, and a Qt edit or dropdown choice overwrote the partner axis.

setting_uint_get/set and setting_int_get/set now live in
setting_list.h so the menu and the desktop UIs share them, and all of
those paths go through them. samples/settings/packed_rows drives the
rows through their display strings, a right press and the dropdown,
and runs in the menu harness workflow.
2026-09-24 19:05:35 +02:00
libretroadmin 115b1eb010 frontend: raise threads through an ordered list of elevation backends
thread_elevation_raise_current() tries each backend in
frontend/thread_elevation.c's list until one grants, and returns at
once. A self-acting backend changes the calling thread and answers
there; a brokered one acts on a thread id from any thread and may
answer PENDING, finishing on a thread of its own and carrying on with
thread_elevation_continue() if it is refused. Every self-acting backend
precedes every brokered one, so a continuation only ever meets brokered
backends. The list is rthreads' kernel request, then RealtimeKit on
D-Bus builds; the audio thread asks once and names none of them.

RealtimeKit moves out of gfx/common/dbus_common.c into
frontend/thread_elevation/rtkit.c as a brokered backend, unchanged in
what it asks for and logs. samples/frontend/thread_elevation drives the
chain runner with a scripted list: order, the thread id and resume
point a brokered backend is given, and a pending request refused on its
own thread continuing past a self-acting backend placed after it.
2026-09-24 17:01:11 +02:00
sonninnos 98a05c5099 Scanline Sync sublabel requirement addition 2026-09-24 17:35:35 +03:00
sonninnos ba2e008501 Reorder notification visibility list + add refresh to stats bool 2026-09-24 17:22:17 +03:00
sonninnos fb514f5cf7 Remove newline from runloop pace string due to Diplay Information page 2026-09-24 16:56:53 +03:00
sonninnos 28bdb34b0c Video stats alignments 2026-09-24 16:44:41 +03:00
libretroadmin ee372be96e alsa: start the device clock fits again after a pause or an xrun
Both device clock fits, from the audio timestamp and from the position,
start again from the writer's next sample whenever the device stops and
restarts: alsa_start(), alsa_stop() and every playback xrun recovery
raise an atomic flag that alsa_clock_sample() consumes. Through a pause
or an underrun the position stands still while time runs on, so a fit
spanning one reads the device as slow; the last published estimate
stands until the new window publishes its own.

alsa_clock_test covers a 2 s pause and a 300 ms underrun mid-window.
2026-09-24 15:07:16 +02:00
libretroadmin 067c652ff7 dbus: ask RealtimeKit for the audio thread when the kernel refuses
When sthread_raise_current_priority() is refused on a Linux build with
D-Bus, the audio thread hands its tid to dbus_rtkit_raise_current_thread(),
which runs the whole RealtimeKit exchange on a detached thread of its own
and returns at once; nothing waits on the bus and the outcome is only
logged. The helper asks for real-time round-robin at rtkit's
MaxRealtimePriority, capped at the middle of the band, after bringing
RLIMIT_RTTIME within rtkit's RTTimeUSecMax as rtkit requires, and falls
back to a nice value no lower than -11 or rtkit's MinNiceLevel. One
request is in flight at a time.

audio_thread_wrapper.c includes config.h so that HAVE_DBUS reaches it.
2026-09-24 15:07:12 +02:00
libretroadmin 01cca3a938 x11: disable xdg-screensaver where its D-Bus helper cannot run
On KDE 4 and later and on GNOME 3, xdg-screensaver suspend hands the
inhibit to a detached Perl helper that needs Net::DBus and
X11::Protocol. The probe reads the desktop from the environment the way
xdg-screensaver does and, on those desktops, checks that both modules
load; without them screensaver suspension is reported as disabled
instead of launching a helper that fails on stderr while the command
itself exits 0.
2026-09-24 14:19:57 +02:00
libretroadmin 1b8c56a0e7 rthreads: raise the audio thread within the process's rlimits on Linux
sthread_raise_current_priority() asks for SCHED_RR at the middle of the
band first. When that is refused on Linux it retries at RLIMIT_RTPRIO
if the limit grants a lower real-time priority, and failing that lowers
the calling thread's nice value to -11, or as far as RLIMIT_NICE lets
it. A process that holds CAP_SYS_NICE, or whose limits allow the full
request, gets exactly what it got before.

audio_thread_loop() checks its argument before touching it.
2026-09-24 14:19:54 +02:00
sonninnos cc5fecc8e7 Remote RetroPad updates (#19617)
- Change screen switch combo to LShift+Tab
- Default pointer mode to pointer
- Restart UDP socket on IP option change
- Call GET_SENSOR_INTERFACE only on page switch and not on every frame in sensor page
- Hide pointer and sensor debug under define instead of comment
- Cleanups and nits
2026-09-24 10:24:10 +02:00
github-actions 38eaee3943 Fetch translations from Crowdin 2026-09-24 00:24:27 +00:00
LibretroAdmin 8105343981 record: the output and framebuffer sizes are words
struct record_params carries out_dims and fb_dims as VIDEO_SCALE_PACK
words; ffmpeg scales and evens the output size in locals and stores
the result as a word, and sizes its buffers with VIDEO_SCALE_AREA.
2026-09-23 22:46:04 +02:00
LibretroAdmin 824a200baf video: the custom viewport is read as the settings' words
The viewport parameter snapshot keeps the custom viewport's origin and
size as the VIDEO_POS_PACK and VIDEO_SCALE_PACK words the settings
store, and vulkan's font pass sets its viewport from the video size
word it already holds.
2026-09-23 22:46:00 +02:00
LibretroAdmin bb026dd95f d3d10/11/12: a texture's mip count halves one word
The OR of the two axes is above 1 exactly when either axis is, and
halves the same, so the mip levels are counted on that one word.
2026-09-23 22:45:57 +02:00
LibretroAdmin ee03686655 gl: the scRGB offscreen, streamed and filter-chain texture sizes are words
The scRGB offscreen in gl1, gl and glcore, glcore's streamed textures
and gl3_filter_chain_texture keep their sizes as VIDEO_SCALE_PACK
words, so frame, history, feedback and framebuffer sizes pass through
the filter chain as the words they are stored in. The offscreen target
helpers, gl3_update_cpu_texture(), gl3_framebuffer_set_size(),
gl3_pass_get_output_size() and the size semantic builders take words.
2026-09-23 22:45:53 +02:00
LibretroAdmin 4a99e49828 wdmks: the loop counts its underruns for the statistics
Every driver with a pump or a callback counts the periods it filled
with silence for want of audio and hands the number to the statistics
overlay; wdmks handed nothing, so it was the one driver where the
screen could not say whether it was keeping up.

The loop knows exactly when it runs out: the hardware running past the
write cursor, or laps going by unobserved, which is where the write
cursor is resynced. That is counted - once per gap in the audio. A
start is not a gap, and neither is every further sample of a pause
finding the loop empty again: the count moves only when audio had been
placed since the last resync, so a pause of any length is one
underrun, the one where the audio stopped.

The packet path still reports nothing: a packet the device found
nothing behind is the port driver's to fill, and it does not say so.

The note on the position register that ended with "a thread of this
driver's own - which is not here yet" is brought up to date: the
refill thread has been there since it landed.

samples/audio/wdmks_loop asserts the count through the contract: nil
after a start, one when the audio runs out, still one as the pause
goes on, two after laps are lost with audio queued, and unchanged by
a restart.
2026-09-23 22:19:16 +02:00
LibretroAdmin fbf3be4e7d metal: the sprite vertex colour is four 16-bit channels
SpriteVertex carries its colour as UShort4Normalized, packed by
rgba16_pack(): a vertex is 24 bytes against 32, and the menu bake,
the font quads and the overlay store one word per vertex. The vertex
functions still read a float4, so the shaders and the prebuilt
metallib are unchanged.
2026-09-23 22:16:23 +02:00
LibretroAdmin d38c398ffe glcore: the overlay colour is four 16-bit channels
The overlay's colour stream is UNORM16, bound as normalised
GL_UNSIGNED_SHORT: eight bytes a vertex against sixteen for four
floats, and set_alpha writes the one alpha channel it changes.
2026-09-23 22:16:23 +02:00
LibretroAdmin 6f62ca8b27 vulkan: the vertex colour is four 16-bit channels
struct vk_vertex carries its colour as R16G16B16A16_UNORM, packed by
rgba16_pack() in gfx/common/rgba16_pack.h, which replaces rgba8_pack.h.
Sixteen bits keep the high-precision menu text colour and every float
fade finer than any swapchain the menus draw to, so a fade on a 10-bit
or scRGB target is not held to 256 steps; a vertex is 24 bytes against
32 for four floats.

samples/gfx/rgba16_pack holds the SSE2, NEON and scalar paths to the
same reference, including a lane that reads every 10-bit level back at
10 bits, which an 8-bit vertex colour fails.
2026-09-23 22:16:23 +02:00
sonninnos 9783568f8e Smart Integer Scale adjustments for PAL content 2026-09-23 22:57:40 +03:00
LibretroAdmin dcba79ee2f wdmks: the resync margin is the FIFO the pin reports
After the hardware runs past the write cursor - and at every start -
the write cursor is put a margin ahead of the register, so the bytes
are in place before the DMA fetches them. That margin was a fixed two
milliseconds, a guess, and on the loop it is also the floor the
reported latency can reach: a queue cannot be driven below it because
every resync puts it back.

The pin says how far past the register the DMA has already fetched:
the FIFO in its hardware latency report, which the driver was already
reading for the statistics. The margin is now that FIFO plus half a
millisecond for the write itself, and two milliseconds only where the
pin reports no FIFO. On a codec reporting a 1 ms FIFO that takes the
floor from 2.7 ms to about 1.5 ms; on a pin whose FIFO is longer than
two milliseconds the old margin was a write into bytes already gone,
a click on every resume, and the margin is now sized to it.

The latency report is taken before the pin is started, because the
first sample after a start resyncs by the margin.

samples/audio/wdmks_loop runs the contract with no FIFO report, a
1 ms one and a 10 ms one, and pins the margin's arithmetic for each.
2026-09-23 21:49:27 +02:00
LibretroAdmin 01f0c3edc9 video: a driver's frame() takes the frame size as one word
video_driver_frame packs the core's width and height once and hands
that word to the driver, the threaded wrapper passes its slot's word
through, and the drivers that compared or stored the size packed
compare and store it directly. sdl2's texture record and gl2's pass
sizing take the word too.
2026-09-23 21:11:20 +02:00
LibretroAdmin b177f08b4d wdmks: the WaveRT loop plays silence when the audio runs out
Pausing a core on the wdmks driver, or anything else that stops the
frontend writing - the menu with the core silent behind it, a stall -
played the last few milliseconds of audio round and round at full
level for as long as it lasted.

A WaveRT pin plays a mapped loop; unlike a packet pin it has no notion
of running out. The frontend writes a fade to silence on a pause and
then nothing, on the understanding that a device with nothing to play
plays nothing. The loop kept what it held and the hardware kept
reading it, so the tail of the fade and whatever came before it in
the loop repeated at the loop's own rate until writes resumed.

Now every byte the cursor passes is cleared as soon as it has been
played, so from then until it is written again it holds silence, and
whatever the hardware reads past the end of the audio is silence.

Two more things fell out of the same bookkeeping:

The two positions alone could not say whether a loop with the cursors
together was full or empty. The model took it for full, so at start
the first audio was only ever placed behind the hardware as it moved
off, one loop late; and after the hardware overran the write cursor
the next write went in behind it too, to be played a loop late for
good. Room is now the loop less a count of what has been placed and
not played, and when that count shows the hardware past the write
cursor - or the cursor could not be watched round - the write cursor
is moved a small margin ahead of it.

start() takes the pin through ACQUIRE, which puts the hardware back at
the top of the loop; the model now starts over from the first sample
after it rather than believing a write cursor left for the old one.

samples/audio/wdmks_loop compiles the driver whole and drives the loop
by hand on both the caller-driven path and the refill thread's pump,
under wine; it fails on the previous driver at "the audio ran out: the
whole loop is silence, not its tail again", among others, and passes
now.
2026-09-23 21:06:10 +02:00
libretroadmin 535b272e73 overlay: ledN_map hides the image at its slot, whatever the desc does
ledN_map reaches the image at the slot it names whatever the desc
there does when pressed, as it did before the _led naming: an LED
overlay may put its lights on keys or buttons, and a config
calibrated on such a pack keeps working. A pack that names its LED
images with _led is still not mapped, and a hidden image still stays
hidden while its desc is pressed.

overlay_visibility_bounds_test pins every mapped slot, control or
"nul", as hidden while its LED is off; overlay_led_hidden_test now
expects the mapped "up" square hidden, pressed or not.
2026-09-23 20:40:22 +02:00
LibretroAdmin d8ca8d978a gfx_thumbnail: draw dimensions work in locals
gfx_thumbnail_get_draw_dimensions() unpacks the thumbnail's size once,
folds the core-aspect correction into one ratio that is 1 when it does
not apply, and writes the two results once at the end rather than
reading them back through the pointers between steps. Bit-identical
results. vulkan_init_readback() reads the viewport size once.
2026-09-23 20:20:30 +02:00
LibretroAdmin aed07d40dc video: Y+X integer scaling multiplies the frame width, not the aspect width
With Integer Scale Axis set to Y + X (or any of the modes that touch X),
the horizontal multiplier was applied to the aspect-corrected width
rather than the frame's own width. A 256x224 Snes9x frame with its core
aspect of 64:49 is 293 wide after correction, so at 1080p the scaler
picked 293 x 4 = 1172 wide over 224 x 4 = 896 high: a 4.58x horizontal
scale, pixel columns alternating 4 and 5 wide, and the fraction the
statistics overlay shows as Scale X/Y. It now picks 1280x896, 5x/4x.

The "reset width to exact width" step used to read the frame cache
fields directly. When those became a local snapshot (69efa8a894) the
reset started reading content_width/content_height, which by that point
hold the aspect-corrected width and the rotated height. The frame's
size is now kept in its own pair and the reset reads that; rotated
content takes the frame height as its width, as it did before.

Driver-agnostic: the scaler is in video_driver.c, so d3d11, Vulkan and
the rest all showed it.

Regression test: samples/gfx/integer_scale_axis runs the scaler out of
video_driver.c (extracted by awk, as asio_clock does) over a sweep of
viewports, frame sizes, axis and scaling modes, rotated and hi-res
content, and checks each drawn size is a whole multiple of the frame
size; the first check is the reported 1080p 256x224 case. On the old
code it fails 386 of 778 checks, reporting 1172x896 for the report
case; on the new code all 783 pass. Wired into CI Linux samples/gfx
under ASan + UBSan.

Reported-by: sonninnos
2026-09-23 20:11:44 +02:00
LibretroAdmin 7ab06c244e gfx, menu: a size parameter is unpacked once
Functions that unpacked the same dims parameter many times read its
halves into two locals at the top instead. Only where the object code
came out the same size or smaller.
2026-09-23 20:06:38 +02:00
LibretroAdmin 4d8511960b gfx: a size word repacked from its own halves is the word
VIDEO_SCALE_PACK(VIDEO_SCALE_W(x), VIDEO_SCALE_H(x)) is x; ten
places that built it that way take x.
2026-09-23 19:57:10 +02:00
LibretroAdmin b93becffd5 vulkan: render targets and the overlay take the size word
vulkan_init_render_target() and vulkan_render_overlay() take the
size as the VIDEO_SCALE_PACK word every caller already holds.
2026-09-23 19:57:06 +02:00
LibretroAdmin e0c314236e vulkan: the display mode search unpacks each size once
The wanted size is unpacked once, and a mode is taken on one path
after the two tests that can turn it down.
2026-09-23 19:45:55 +02:00
LibretroAdmin 41a9193557 vulkan: the swapchain and surface are asked for by size word
vulkan_create_swapchain() and vulkan_surface_create() take the wanted
size as one VIDEO_SCALE_PACK word, and so does KHR_display's mode
search, from vulkan_display_surface_info through the chosen mode. The
context drivers pass the words they hold rather than their halves.
2026-09-23 19:40:56 +02:00
libretroadmin d2a55553f4 CI gl-harness: drop the no-display step
The step started the build with no display and required exit status 1.
On the runners it fails at once with nothing in the run log, on every
driver and sanitizer, and the same run passes on a runner-shaped build
elsewhere, so it tests the runner rather than the code. It goes; the
glcore teardown guard it was written for stays in gl3.c.
2026-09-23 19:22:59 +02:00
LibretroAdmin ce44f46d77 gfx: set_resize takes the size word
A context driver's set_resize takes the output size as one
VIDEO_SCALE_PACK word, which every caller already holds; the D3D8 and
D3D9 drivers' own set_resize helpers take it the same way. The X11
contexts log the size as unsigned.
2026-09-23 19:17:38 +02:00
libretroadmin a58b0edc26 x11: the pointer poll reads the pointer from events
With XInput 2, the driver's own connection takes the master pointers'
motion, button and crossing events on the window, and the poll reads
each pointer's position, buttons and whether it is inside from them,
so it sends no request. A pointer outside the window reads nothing
held, a drag out of it included. That connection holds the window's
button events, so it latches the wheel and buttons 8 and 9. A grabbed
pointer is warped back on the same connection, and a position older
than the warp is ignored. Each pointer is queried once at init; a
server that refuses the selection, or no XInput 2, leaves the query.

samples/input/x11_key_poll becomes x11_input_poll and drives the
pointer too: buttons, the wheel, a drag, a new driver over a held
button, and grabbed motion across a warp held behind a server grab.
2026-09-23 19:16:37 +02:00
libretroadmin 233b871543 x11: the keyboard poll reads keys from events
x_input_poll() reads the keyboard from key events on a connection of
the driver's own, drained at the poll, so it sends no request and
never waits behind what the video thread presents through the shared
connection; keys still read as of the poll. KeymapNotify after each
FocusIn replaces the key vector and a focus change clears it, except
under another client's grab, which keeps the window's focus. Detectable
auto-repeat keeps a held key from ever arriving as a release. Without
that connection the poll queries the server as before.

samples/input/x11_key_poll drives the driver through XTEST on Xvfb:
no requests over 200 polls, a key held before init, auto-repeat, focus
changes, a keyboard grab and the fallback.
2026-09-23 18:55:27 +02:00
LibretroAdmin 539a87bdb8 vulkan: the filter chain's textures carry one size word
vulkan_filter_chain_texture keeps its size as one VIDEO_SCALE_PACK
word, so the driver's texture, framebuffer and history sizes pass
through as words. slang_pass_get_output_size(),
slang_framebuffer_set_size() and the texture-size semantic builders
take the words they were unpacked from.
2026-09-23 18:44:47 +02:00
LibretroAdmin a2fad5f361 X11: detect XWayland by its extension, not its output names
The Mutter path in dispserv_x11 (17c7c6a) recognised XWayland only by
RandR output names starting with XWAYLAND. That holds for older
XWayland and for compositors that do not name their outputs, but under
Mutter XWayland takes each connector's name - DP-1, HDMI-1, Meta-0 -
like any X server. On a GNOME desktop the check never matched, the
XRandR path ran, and the Screen Resolution list stayed XWayland's:
every mode at the desktop's current rate, no 60 Hz on a 240 Hz panel.

XWayland 23.1 and later advertise the XWAYLAND extension, which is
what xrandr tests for its "running against an Xwayland server"
warning. That is now checked first, the name prefix kept as the
fallback for older XWayland. A real X server has neither, so it still
never talks to D-Bus.

Regression test: a new mutter_displayconfig case runs dispserv_x11 on
the XWayland of a real headless Mutter (one 2560x1440@60 virtual
monitor, output named Meta-0) and requires Mutter's list - one mode at
exactly 60.000 Hz - rather than XWayland's CVT list. Against the
previous dispserv_x11 it fails with XWayland's 33 entries; mutter joins
the step's packages.
2026-09-23 18:41:55 +02:00
LibretroAdmin 318e92d61b vulkan: a texture's size is one word
struct vk_texture keeps its size as one VIDEO_SCALE_PACK word. The
readback staging check compares it with the viewport's word directly;
elsewhere the halves are read where Vulkan wants them apart.
2026-09-23 18:34:56 +02:00
libretroadmin ad5a999ee8 CI samples: winraw_abs_pos runs in the input job
samples/input/winraw_abs_pos is cross-built with mingw-w64 and run under
Wine, and no workflow named it, so the job for unclaimed samples picked
it up with only build-essential installed and failed on a missing
compiler. The input job now runs it, installing mingw-w64 and Wine when
the runner lacks them, which also takes it off the unclaimed list.

The unclaimed job installs the same two the first time a leftover
sample's Makefile uses them, so the next Windows sample without a home
runs there instead of failing for want of a compiler.
2026-09-23 18:26:39 +02:00
libretroadmin 71c885ef27 CI gl-harness: the no-display step pins the null GL context
Left to probe with no display, the context the GL drivers find depends
on the runner rather than on the code: a build with SDL2 brings up its
offscreen EGL backend on llvmpipe, and a VM with a DRM device can bring
up KMS. The step then runs a live driver instead of the refusal it is
there to test, and its exit status follows the image.

The step sets video_context_driver to null, so both drivers are handed
the null context on every host, and requires the log to name it before
checking the exit status. With the teardown guard in gl3.c reverted the
glcore run still faults with 139.
2026-09-23 18:26:36 +02:00
LibretroAdmin 289dcd9124 switch: the original size is one word; unread sizes go
switch_video_t keeps the size o_size shows the frame at as one
VIDEO_SCALE_PACK word. Its width/height, and the context's
width/height, native_window and resize, were written and never read;
they go, and the context crops from the size word it already has.
check_window's locals are declared C89-first.
2026-09-23 18:21:25 +02:00
LibretroAdmin 3c0df2d8da vulkan: the swapchain's size is one word
The Vulkan context keeps its swapchain size as one VIDEO_SCALE_PACK
word, and khr_display its own size likewise; the retained backbuffer
and the window size compare and copy the word rather than repacking
its halves.
2026-09-23 18:11:43 +02:00
LibretroAdmin 17c7c6a393 Screen Resolution under GNOME through Mutter's DisplayConfig
On GNOME's Wayland session RetroArch's X11 driver runs on XWayland.
XWayland lists every mode at the desktop's current rate - a 240 Hz
panel shows no 60 Hz mode - and a "switch" only scales the fullscreen
window; the real modes belong to the compositor. The Wayland driver
had no resolution list at all. Under Mutter no X11 or Wayland protocol
reaches the real modes; its org.gnome.Mutter.DisplayConfig D-Bus
interface (what GNOME Settings uses) does.

gfx/common/mutter_displayconfig.c (HAVE_DBUS only) reads
GetCurrentState and applies ApplyMonitorsConfig:

- The list is the target head's real modes, one entry per size and
  rate (a fixed and a variable rate twin are merged), sorted, the
  scanned-out mode current.
- A switch picks the listed mode of that size nearest the rate, the
  same rule as the XRandR path; a zero size keeps the current one (the
  refresh rate autoswitch). It is applied as a temporary configuration:
  not saved to monitors.xml and no "keep these settings?" prompt.
  Every other monitor keeps its mode, position, scale and transform;
  every monitor keeps its colour mode (HDR), RGB range and
  underscanning; a mirror switches all its monitors or none; heads to
  the right of or below the switched one move with its new size; the
  layout mode goes along only where Mutter allows choosing it.
- The target head is the connector (wl_output v4 name), else the head
  under the RetroArch window (XWayland root coordinates are Mutter's
  logical layout), else the monitor index, else the primary.
- Each call opens and closes its own private session bus connection,
  bounded by a 2 s timeout. With no bus address and no
  $XDG_RUNTIME_DIR/bus it does nothing, so no bus is ever autolaunched.

Backwards compatibility - every existing path behaves as before:

- Builds without HAVE_DBUS compile exactly the previous code.
- dispserv_x11 only asks Mutter when the output is XWayland's
  (XWAYLAND<n>); a real X server never talks to D-Bus. On XWayland
  without Mutter (KDE, sway, ...) XWayland's own list and switch are
  used as before. Only when Mutter answers is its answer final.
- dispserv_wl gets set_resolution, get_resolution_list and get_flags
  in HAVE_DBUS builds. A new display server flag,
  DISPSERV_CTX_NO_RESOLUTION_LIST, lets it report "nothing to list
  right now": set when Mutter was not on the bus at init, and
  video_display_server_has_resolution_list() then answers false, so
  the Screen Resolution entry and the refresh rate autoswitch stay off
  on non-GNOME Wayland exactly as before. No existing server sets the
  flag, and its bit is not DISPSERV_CTX_MODELINE's.

Checked against a real headless Mutter 46: GetCurrentState parses and
ApplyMonitorsConfig is accepted.

Regression test samples/gfx/mutter_displayconfig (ASan + UBSan, new
Linux-samples-gfx step): mock_mutter.py carries Mutter's signatures and
its ApplyMonitorsConfig checks (serial, connectors, modes, scales,
every lit monitor configured) on two monitors - a 240/120/60 Hz HDR
panel and an underscanned one beside it. Cases: no session bus and a
bus without Mutter (unavailable, nothing autolaunched); the list, a
60 Hz switch with every other setting carried over, rate only, whole
hertz, a smaller mode with the neighbour moved, another head by
connector, unlisted size and rate refused without an apply; dispserv_x11
on a real Xorg dummy server with Mutter on the bus (XRandR list, Mutter
never asked); dispserv_x11 on Xwayland under a headless Weston with
Mutter (Mutter's list and switch) and without (XWayland's list as
before); dispserv_wl with Mutter (list, switch, not flagged) and
without (flagged, no list, as before).
2026-09-23 17:59:15 +02:00
LibretroAdmin 8cee4866b1 vulkan: the filter chain is handed the texture size word
vulkan_filter_chain_create_info carries the largest input as the
driver's own VIDEO_SCALE_PACK word rather than two axes the chain
packs again.
2026-09-23 17:40:17 +02:00
LibretroAdmin 987b8c1d12 gfx_thumbnail: an animation job's size is one word
The decode job keeps its surface's size as the VIDEO_SCALE_PACK word
the surface was made with.
2026-09-23 17:40:13 +02:00
LibretroAdmin 9477335429 gl1, gl2: the font atlas texture's size is one word
The raster font keeps its power-of-two texture size as one
VIDEO_SCALE_PACK word; the atlas upload unpacks it once.
2026-09-23 17:22:29 +02:00
LibretroAdmin 19427174a8 X11: implement the Screen Resolution display server callbacks
dispserv_x11 left set_resolution and get_resolution_list NULL, so
video_display_server_has_resolution_list() was false on X11: the
Settings > Video > Output > Screen Resolution entry was hidden, and
the automatic refresh rate switch and its PAL threshold never ran.

Both are implemented on XRandR 1.2, gated on HAVE_XRANDR like the
rest of the RandR code:

- get_resolution_list lists the modes of one head - the one under
  the RetroArch window, or under the Xinerama screen
  video_monitor_index names, else the primary output, else the first
  lit one (the rule modeline_open already uses). The field rate is
  computed from the timing with interlace and doublescan accounted
  for, the same size and rate listed under two names is merged into
  one entry, exactly the scanned-out mode is marked current, and the
  list is sorted by size then rate.

- set_resolution switches that head to the listed mode of the asked
  size nearest the asked rate (within half a hertz, or matching the
  whole-hertz menu label), progressive over interlaced. A zero size
  keeps the current one, which is how the refresh rate autoswitch
  asks. The framebuffer grows before the crtc takes a larger mode
  and shrinks to the lit area after, so each step is a configuration
  the server accepts; other heads are left where they are, the DPI is
  kept, and the root window geometry is read for the current size
  rather than Xlib's cached DisplayWidth(). X errors are trapped, a
  failed switch puts the framebuffer size back, and the kept refresh
  rate is invalidated. An unlisted size is refused and changes
  nothing. No libm: the driver still links without it.

Regression tests:

- samples/gfx/display_servers_x11_live (Xorg dummy server, ASan +
  UBSan, already run by Linux-samples-gfx): the list must describe
  the lit head with one current entry, in order, without
  duplicates; switches to a smaller mode, a larger mode, a rate only
  (size 0), a whole-hertz rate, and back must land on the wire with
  the framebuffer following; an unlisted size must fail and change
  nothing. Fails on the old tree (callbacks NULL).

- samples/gfx/display_servers (stubbed RandR): every failing query
  and a disconnected head give an empty list and a refused switch,
  with no crtc configured, no leak and no bad free.
2026-09-23 17:18:40 +02:00
LibretroAdmin 69d740361c CI PS4: build in the orbis image with docker run
The orbis image is Alpine 3.15, whose musl cannot load the runner's
Node 24, so checkout and upload-artifact run on the host and the
dependency install, build and toolchain version steps run inside the
image through docker run.
2026-09-23 17:03:37 +02:00
LibretroAdmin 59e6f166cd gfx: six more context drivers keep one size word
opendingux, mali and videocore keep their size as one VIDEO_SCALE_PACK
word and hand it back as it is. vivante, vita and orbis stored a size
nothing read; those fields go, with vivante's unused default size and
EGL format and mali's unused set_video_mode locals.
2026-09-23 16:57:44 +02:00
libretroadmin d69c11b20e qb: find PipeWire and SPA headers when pkg-config is absent
Without pkg-config, check_val falls back to a plain link test and takes
the include flags from the directories it is given. check_lib now accepts
a list of include subdirectories, each of which has to exist for the
library to be enabled, and PipeWire passes both pipewire-0.3 and spa-0.2.
A system that has the library but not the headers ends up with PipeWire
disabled rather than enabled with no include path, and PIPEWIRE_STABLE
takes its 1.0.0 floor from PW_CHECK_VERSION in the headers.

A CI lane configures with PKG_CONF_PATH=none and builds to completion,
checking that PipeWire is enabled with both include directories.
2026-09-23 16:46:30 +02:00
LibretroAdmin 58dc63615a audio: the resume mark has one writer
The main thread publishes the resume mark as one word, a sequence and
an armed bit, and is its only writer; the consumer records the
sequence it acted on instead of clearing the flag, and takes the
position between two reads of the mark that agree. A pause and resume
landing while the consumer reads can no longer have their mark
cleared unseen, so the resumed audio keeps its ramp.

samples/audio/pipeline_fade_mark runs the shipping publish, pending
and clear functions with a pause and resume delivered in every gap of
the consumer's read.
2026-09-23 16:39:59 +02:00
LibretroAdmin 5108ed0a2c d3d11, font renderers: the retained buffer and the glyph cell are words
d3d11 records the back buffer size its copy was made at in one word,
as the d3d12 driver does. Both font renderers hold the cell every
glyph is rendered into the same way, so the atlas layout, the copy
clamp and the blank-glyph clear all read their axis from one place.
2026-09-23 16:39:41 +02:00
LibretroAdmin 0fbc22f324 android: the drive list's volume loop is C89
Its counter is declared with the function's other locals.
2026-09-23 16:16:05 +02:00
LibretroAdmin 7ff2befb44 android: the content rectangle's size is one word
onContentRectChanged() publishes the size as one VIDEO_SCALE_PACK
word, so the Vulkan context never pairs one change's width with
another's height, and check_window() takes the changed flag with an
exchange, so a change raised while it reads is not cleared unseen.
The context keeps its own size as one word too.

samples/gfx/android_content_rect runs both shipping functions on the
host, taken out of their files at build time, with the UI thread's
change delivered in every gap between the reader's operations.
2026-09-23 16:16:01 +02:00
LibretroAdmin d1fb122b6e winraw: an absolute report's position is one word
The report handler publishes an absolute report's x and y as one
VIDEO_POS_PACK word, so winraw_poll() on the main thread reads a
position a report gave rather than one report's x with the next
report's y.

samples/input/winraw_abs_pos runs the real driver under Wine and
delivers a second report between poll's loads.
2026-09-23 16:08:50 +02:00
libretroadmin b07619da1e glcore: a context that never came up tears down without GL calls
With no display only the null GL context is found, and the glcore
driver refuses it. Its teardown now touches the vertex array object
only when one was made, so the refusal reaches the frontend as a
failed video init and an exit with an error, as the gl driver's does,
instead of a call through a GL entry point the null context never
resolved.

The llvmpipe harness job starts the build it tests with no display,
directly and threaded, on both GL drivers, and requires exit status 1.
2026-09-23 16:07:02 +02:00
Jesse TalaveraandClaude Opus 5 25116c7774 libretro-db: write the tags field in c_converter (#19613)
Explore already reads "tags" from database entries, but c_converter had
no mapping for it, so a tags field in a DAT never reached the .rdb.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
2026-09-23 16:06:24 +02:00
LibretroAdmin e31b6de5a6 vulkan, sdl2: the readback record and the font atlas are sized by word
Each readback slot records the extent its copy was taken at in one
word, and the SDL2 font holds its atlas texture's size the same way,
so the glyph scale and the upload stride read their axis from one
place.

The two padded structs in vulkan's uniform block stay as they are:
the shaders read that layout.
2026-09-23 15:58:17 +02:00
LibretroAdmin f19921bf81 coreaudio: the worst underrun is one word
The deepest shortfall and the ring's fill in that same callback share
one atomic word, in frames, 16 bits each: one store records both and
the teardown log reads them together.

coreaudio_write runs underruns of different depths and checks the
word keeps the deepest with its own fill.
2026-09-23 15:58:04 +02:00
libretroadmin bd710babc3 x11: the grabbed mouse asks the server only for the pointer
With the mouse grabbed, the X input driver takes the window's centre
from the size the event pump records and sends the warp back to it
without waiting: the next poll's pointer query follows the warp on
the connection, so it reads the warped position. A poll costs the
keymap query, the pointer query and the warp. The recorded size is
one word, written by the pump and read by the poll on the runloop
thread; the server is asked for the size only for a window the pump
has not seen.

The threaded video harness moves the pointer from a connection of its
own between polls, directly and under the wrapper, and requires the
exact motion from each poll and no more than those three requests.
2026-09-23 15:49:52 +02:00
LibretroAdmin 29b85f70ab video_thread: the window's answers are one word
alive, focus, presentable and has_windowed are bits of one atomic
word the video thread publishes with a single store after each frame.

The window answers lane drives each answer on its own through a
swapped driver and reads it back on the main thread, so an answer
landing in another's bit, or one that does not move, fails.
2026-09-23 15:49:37 +02:00
LibretroAdmin 1b9b4a1d90 switch: the video driver's sizes are words
switch_video_t holds the frame size the scaler was last built for,
the window size the hardware scaler asks the compositor for, and the
menu texture's arriving and on-screen sizes one word each, so the
rebuild test is a single comparison and the two nwindowSetDimensions
calls read their pair from one place.

Five declarations in this file move to the top of their block, two of
them out of a for statement, so it builds as C89.
2026-09-23 15:49:23 +02:00
LibretroAdmin c94e465bbe sunxi: the emulator framebuffer's size is one word
The driver keeps the size the core renders at in src_dims, and
sunxi_setup_scale takes it as that word. Four menu fields go with the
change: menu_rotation, menu_width, menu_height and menu_pitch were
declared and never touched, and menu_active is the only one the
driver reads.

The two __disp_ rect types above stay as they are: they are the
kernel's disp ioctl layout, not ours.
2026-09-23 15:49:20 +02:00
LibretroAdmin 926070761d drm: the plain DRM driver's sizes are words
A dumb buffer's pixel size sits beside the stride the scanout walks,
and the driver holds the surface's internal size, the mode the
display is running and the size the core last handed over the same
way. drm_surface_setup takes the size as the word its menu caller
already had.

struct drm_rect went with them: nothing has ever used it.
2026-09-23 15:25:44 +02:00
LibretroAdmin e94271d4e4 d3d12: the retained back buffer and the software framebuffer are sized by word
The copy of the last presented back buffer records the size it was
made at, and the software framebuffer the size it was laid out for,
one word each, so each recreate test is a single comparison against
the size in hand.

The OutputSize pair in the uniform block stays two floats: the shaders
read it as a float2, so its layout is not ours to change.
2026-09-23 15:25:41 +02:00
LibretroAdmin 8520f2471a matrix: lanes for the shared Wayland unit
gfx/common/wayland_common.c is what both Wayland contexts call into
and was the one Wayland unit no lane compiled. It gets three, for the
plain build, the libdecor build and the backport build, each carrying
the declaration-after-statement gate the PSP lane already uses.

The libdecor lane defines HAVE_DYLIB alongside HAVE_LIBDECOR_H: the
function pointers that half calls through are declared under both, so
a lane naming only the first compiles a file with no libdecor calls
left in it.
2026-09-23 15:25:37 +02:00
LibretroAdmin 39fbd1ca2c vulkan: the vertex colour is four bytes
struct vk_vertex carries its colour as R8G8B8A8_UNORM, packed by
rgba8_pack() in gfx/common/rgba8_pack.h: a vertex is 20 bytes instead
of 32, and the menu's per-vertex bake packs with one SSE2 or NEON
sequence rather than four float stores. The font and quad paths pack
their colour once per string or quad.

samples/gfx/rgba8_pack holds the vector path and the scalar one to
the same reference: every 8-bit level, both sides of every rounding
boundary, saturation past either end, infinities and NaN.
2026-09-23 15:22:43 +02:00
LibretroAdmin 8394074dd3 CI: overlay_led_hidden_test runs in the overlay on-screen workflow
The workflow runs it after the menu-mouse test, installs libxtst6 for
its XTEST key presses, and also triggers on led/drivers/led_overlay.c.
The overlay_visibility_bounds_test step in the tasks samples workflow
describes what the test now covers: input_overlay_alpha.c's LED hiding
and its per-poll alpha pass.
2026-09-23 15:19:34 +02:00
libretroadmin 04cc6c998d x11/vulkan: the swapchain presents on an X connection of its own
The X Vulkan context opens a second connection to the server for the
swapchain and hands it to the XCB surface; the window stays the
frontend's. A software WSI writes each frame into its connection as
image data, and on the frontend's connection every request the
frontend makes - the input driver's keymap and pointer queries, the
event pump - waited behind that frame on whichever thread presented.
The connection closes after the swapchain and surface; if it cannot
be opened the frontend's is used. vulkan_surface_create() takes the
XCB connection itself.

The threaded video harness counts the requests on the frontend's
connection over 60 presented frames with the input poll left out and
requires none. The compile matrix builds the context with XCB, as
the real build does.
2026-09-23 15:19:07 +02:00
libretroadmin 51e356bc3c samples/x11_refresh_rate: count requests once the reader has settled
The reader thread asks for the rate on g_x11_dpy, the connection the
requests are counted on. After the switch its ask can still be reading
the server when the count starts, or can withdraw a rate it kept across
the pump's invalidations, and either lands requests inside the count.
The count now starts only once an ask that began after the pump has
finished, so every earlier ask is done and the kept rate is final.
2026-09-23 15:07:33 +02:00
libretroadmin 8eb6034dd0 x11: keep the display's refresh rate until the display changes
The X display server reads the refresh rate from the server once per
display change and answers from memory in between, so a core polling
the throttle state every frame, or the menu showing the rate, sends
nothing to the server. On g_x11_dpy it selects RandR screen, crtc and
output changes on the root window; the event pump drops the kept rate
on each of them, and the display server drops it after its own mode
sets. The rate is kept only once the pump drains that connection, and
a reader that raced a change withdraws what it kept. A NULL output
from XRRGetOutputInfo is skipped, and opening the display no longer
writes the display server's flags, which two threads ask at once.

samples/gfx/x11_refresh_rate runs the real pump and display server on
an Xorg dummy server against mode switches from another client, with
a second thread asking throughout, under ASan+UBSan and TSan.
2026-09-23 14:49:47 +02:00
LibretroAdmin 168dadd291 audio: one word for the device-clock estimate
A driver's device-clock estimate and whether there is one share a
single int, AUDIO_CLOCK_PPM_NONE until the first estimate: one store
publishes it, one load reads it, and a reset landing inside a read can
no longer pair an old flag with the reset's value (ASIO, JACK, Pulse,
CoreAudio, WDM-KS, 3DS DSP).

asio_clock runs the driver's own publish, reset and reader, taken from
asio.c at build time, with a reset between the reader's loads;
live_drivers checks Pulse and ALSA report no clock before any audio.
2026-09-23 14:30:25 +02:00
LibretroAdmin 13cd2a02e2 video_thread: suppress_screensaver reaches the driver
Send the request to the video thread, which owns the window, and
return the driver's answer rather than a constant.
2026-09-23 14:05:58 +02:00
LibretroAdmin e308425da8 wayland: the surface, its buffer and the floating size are words
gfx_ctx_wayland_data_t holds the surface's size, the buffer behind it
and the size to restore when the window floats again one word each,
and the latched xdg_toplevel configuration holds the size the
compositor asked for the same way. Both configure paths scale one
axis into each half of the buffer word in a single pack.

Four declarations in this file move to the top of their block so it
builds as C89 under the libdecor path as well as without it.
2026-09-23 12:47:51 +02:00
LibretroAdmin 5b90ce9a16 wayland: an output's mode is one word
output_info_t carries the mode the output is running in dims, which
is the word both readers of it were building anyway. The physical
size the geometry handler also stored goes with them: nothing read
it, and the display server keeps its own copy for the DPI it reports.
2026-09-23 12:47:48 +02:00
LibretroAdmin 84e54a6f4c emscripten: the canvas size is one word, read and written whole
The browser's canvas size is stored as one packed word, so a read on
the main thread can no longer pair a width from one resize with a
height from another; platform_emscripten_get_canvas_dims returns it
and platform_emscripten_set_canvas_size takes it. The EGL and WebGL
contexts keep their framebuffer size the same way. The JavaScript
side keeps its two numbers.
2026-09-23 12:47:34 +02:00
LibretroAdmin 2d075314fb Sync to Exact Content Framerate: keep vsync within the skew tolerance
A content rate slightly past what the display presents at the chosen
interval - a 60.0988 Hz core at swap interval 2 on a 120 Hz panel, or
60 Hz content on a 59.94 Hz VRR ceiling - is paced by vsync with audio
skewed as it is without VRR, rather than forcing the loop nonblocking.
Only a content rate beyond audio_max_timing_skew keeps its own rate
with vsync off.

The decision is runloop_sync_plan_for() in runloop.h, pinned by a
table and an invariant sweep in samples/runloop/pacing. Fixes #19600.
2026-09-23 12:38:19 +02:00
LibretroAdmin 18b806e732 metal: no emulated swap interval while display sync is off
With display sync off a repeated present is not held for another
display frame, only presented again unpaced. set_nonblock_state now
takes the interval to 1 in nonblocking mode, as Vulkan's does.
2026-09-23 12:38:14 +02:00
LibretroAdmin b5096ce522 x11: answer the per-frame window poll from events
x11_check_window() and x11_has_focus() run on the frame path and must
not wait on the server. Keyboard focus is tracked from FocusIn/FocusOut
on the window (grabs ignored), so the GLX, EGL and Vulkan X contexts
select FocusChangeMask. The window size is recorded when the window
maps, since without a window manager no ConfigureNotify follows; it is
cleared with the window, so a new window never reports the old size.

The main thread no longer shares a round trip with a software Vulkan
WSI presenting a whole frame through the same connection.
samples/gfx/x11_frame_poll runs the poll on Xvfb and requires it to
send no requests, with size and focus still following the window.
2026-09-23 12:36:11 +02:00
LibretroAdmin a4d56f5a34 gxm: the Vita driver's sizes are words
The display mode's pixel size sits beside the stride it scans out
with, and the driver holds the frame the core hands over, the size it
presents at, the menu frame and an overlay image's size the same way.
An overlay's tex_w and tex_h stay floats: they are a fraction of the
image, not the image.
2026-09-23 12:35:57 +02:00
LibretroAdmin 9a0613340d tools: drop a checked-in bytecode cache
tools/__pycache__ is written by the Python checkers whenever they run,
so a copy tracked in the tree shows up as a modified file after any
run of them, and the one that was there was built by a different
interpreter version than the tree's lanes use. .gitignore keeps it
out.
2026-09-23 12:35:52 +02:00
LibretroAdmin 861f6c9c53 tools: a lane for the readers a removed struct member leaves behind
The members a change removes, minus the ones something still declares,
against every reader in every language the tree is written in. A
reader the Linux compilers never see - an Objective-C driver, anything
behind a platform #ifdef - is named by file and line here rather than
by whichever platform job happens to build it.

The checker carries fixtures of its own: a member removed with its
readers, one left behind, one that merely moved, one whose name
another struct still declares, a rename, a name appearing only in a
comment or a log string, a declarator list, and a name too common to
reason about.
2026-09-23 12:21:09 +02:00
LibretroAdmin b8c64ac8f3 cocoa: the window geometry handlers read the packed origin
ui_cocoa reads and writes settings->uints.window_position_pos, so a
window placed on a display left of or above the main one keeps the
negative origin AppKit gives it.
2026-09-23 11:52:41 +02:00
libretroadmin c04bc789d6 Clear warnings in thread-less, microphone-less and GL-less builds
runloop_msg_queue_off_main() is built only with HAVE_THREADS, like its
callers. WASAPI's error-string helper is built only with
HAVE_MICROPHONE, the one path that calls it, and the driver casts
GetProcAddress straight to its function-pointer types, so it builds in
the Windows C89_BUILD lane. The ffmpeg core's slot helpers for the GL
path are built only with OpenGL, and its channel-layout declaration
sits at the top of its block. The IIR filter's RIAA CD case marks its
fall into the high shelf, and the ffmpeg_live sample's GL frame
callback has the libretro signature, which GCC 14 and later require.

The compile matrix installs mingw-w64, which turns its Direct3D lanes
on, and compiles WASAPI with the C89 flags both with and without the
microphone, treating unused functions as errors. That lane compiles
for real rather than with -fsyntax-only, which never reports unused
static functions.
2026-09-23 06:36:16 +02:00
LibretroAdmin 6a9e8a99a6 menu: the draw helpers take the screen size once, as the word it is
ozone_draw_icon and xmb_draw_icon already take video_dims and read the
screen height from it; the width and height they were also handed
are gone. xmb_draw_text, xmb_draw_dark_layer and the MaterialUI
scrollbar helpers take the packed size their callers hold instead of
its two halves, and xmb_frame reads video_info->dims where it rebuilt
it from its own halves. The locals that only fed those arguments go
with them.
2026-09-23 06:36:00 +02:00
LibretroAdmin 12438582fd rgui: the thumbnail box downscaler takes and returns its size as one word
rgui_downscale_box takes the source size packed and hands the result
back the same way; a source too large to pack goes straight to the
resampler.
2026-09-23 06:35:57 +02:00
LibretroAdmin 03fa7238b6 win32: the window's own position and frame size are words
The module's window state keeps its origin and its frame size one
word each, and a pos_set flag says whether an origin has arrived yet,
so the case where Windows chooses the position is a flag rather than
a sentinel coordinate sitting in the same field as a real one.
Carrying the origin between the setting and the window is now a
single assignment either way.
2026-09-23 06:35:37 +02:00
LibretroAdmin ce864e8977 settings: the window's origin is one signed word
window_position_pos holds where the window sits in VIDEO_POS_PACK's
layout, and its two config keys are signed rows over the halves of
it, so a display left of or above the primary one saves the negative
coordinate it actually has and reads it back the same way. The keys
keep their names and a non-negative position carries over from an
existing config unchanged.
2026-09-23 06:35:33 +02:00
LibretroAdmin dc6f2b8542 d3d9: a shader pass's last frame size is one word
Each pass holds the frame size it last built vertices for in
last_dims, and the ring of previous-frame sizes is one array of those
words rather than a pair of parallel arrays, so carrying a size
between the ring and pass zero is a single assignment.
2026-09-23 06:02:43 +02:00
LibretroAdmin 2377645933 d3d: a shader pass's render target size is one word
LinkInfo carries the size of the texture a pass renders into as
tex_dims, and set_pass_size in both d3d9 chains takes that word in
place of two axes, so the test for whether a pass has to be
respecified is a single comparison against the size the caller
computed.
2026-09-23 06:02:39 +02:00
LibretroAdmin b93d6e03a2 cocoa: cocoa_common.h keeps its CRLF line endings
A rewrite of the output-size query saved the header with LF endings;
it goes back to the CRLF it had.
2026-09-23 06:02:27 +02:00
LibretroAdmin d12e50b716 camera: a driver is opened at one packed size
camera_driver_t init takes the requested frame size as one word, which
the frontend packs from the setting or the core's request. The FFmpeg
driver keeps its requested and target sizes that way, V4L2 the size
its device negotiated, and the PipeWire format builder takes the
size packed. The FFmpeg live test checks that frames arrive at the
size asked for.
2026-09-23 06:02:24 +02:00
LibretroAdmin df41a367a6 companion: rectangles and dock rows are an origin and a size word
companion_rect_t is an origin packed with VIDEO_POS_PACK and a size
packed with VIDEO_SCALE_PACK, and companion_dock_state_t holds a dock
row's position and size the same way; the rows read and write as
before. companion_place_window takes the needed and minimum sizes
packed. The dock layout works in signed locals and packs each
rectangle once, clamping the content width and a pane's height to
zero before they are stored. The Win32, Cocoa and Qt companions fill
and read the packed rectangles.
2026-09-23 05:51:05 +02:00
LibretroAdmin 53b1b1ec7f d3d8: the driver's own size and origin pairs are words
The soft scissor keeps its rect as an origin word and a size word, so
the rect the menu hands scissor_begin is stored as the word it
arrives in. The render chain holds the frame size it last built
vertices for and the size of the texture behind them the same way,
and the font's atlas mirror does too, which makes the respecify test
a single comparison against the size the atlas reports.
2026-09-23 05:50:50 +02:00
LibretroAdmin a2f9b67aa0 d3d: an overlay's texture size is one word
overlay_t carries the size of the texture behind it in tex_dims, so
the menu overlay's size test in every d3d driver is a comparison
between the word the frame arrives as and the word the texture was
made from, rather than a pair of axis comparisons either side of it.
2026-09-23 05:50:47 +02:00
libretroadmin 34f87ddc18 d3d9: give the font shadow colour a defined start value
The d3d9 HLSL and Cg font renderers start color_dark at zero, as d3d8
does. It is only read by the drop-shadow pass, under the same has_drop
test that sets it, but once the quad emitter is inlined GCC no longer
ties the read to that test and reports -Wmaybe-uninitialized at -Os.
2026-09-23 05:43:34 +02:00
LibretroAdmin 35351ca9ca ozone: name the message box's slice size for what it is
dialog-slice.png is square, so one edge is the source size on both
axes.
2026-09-23 05:31:35 +02:00
LibretroAdmin fbedb1848c companion, modeline: clear the unused-symbol warnings
ct_anim_first is built only where the decode threads that ask it
are. The modeline goldens and the EDID parse test drop the engine's
log lines through a do-nothing function rather than a macro that
swallowed its arguments, so the buffers the engine formats them into
are used, and the golden tool needs no variadic macros.
2026-09-23 05:31:32 +02:00
LibretroAdmin 9f0351f9fa ctr, gx, gx2, gxm, rsx: the overlay setters refuse an image the page does not hold
set_alpha, tex_geom and vertex_geom compare the index with the page's
count and return with no page loaded; the "&overlay[image] is NULL"
tests they had can never be true.

tools/overlay_setter_bounds.py now reads every driver with an overlay
interface, lets a stub setter with an empty body pass, and fails a
driver in gfx/drivers that defines the setters without being in its
table.
2026-09-23 05:31:16 +02:00
LibretroAdmin 993cc446ea d3d8, d3d9 (HLSL): an overlay image's vertex buffer is locked only when its quad changes
overlay_t keeps the quad last written to the image's vertex buffer
(vert_sent). The draw builds the quad as before and locks and fills
the buffer only when it differs, or when the buffer is new; a failed
lock leaves it to be written next frame, and a NULL lock is no longer
written through. D3D9 Cg draws from user memory and is unchanged.

tools/overlay_setter_bounds.py fails a D3D8 or D3D9 HLSL overlay draw
that locks its buffer before comparing the quad with vert_sent.
2026-09-23 05:24:45 +02:00
LibretroAdmin 6e88895542 modeline: a mode's size label and the requests are one word
video_modeline_t lists its mode under one packed size beside the
refresh label, the EDID standard timings hold theirs the same way,
and modeline_get and modeline_set_user_mode take the requested size
packed. The timing fields stay per axis. The SDL display servers pass
the packed size through to their apply, and the Windows, X11, KMS and
VideoCore servers fill and read it where they list and set modes.
The goldens are unchanged.
2026-09-23 05:21:22 +02:00
LibretroAdmin d695a9031b vulkan: an overlay page is drawn from one uniform and one vertex range
vulkan_render_overlay() takes one uniform range for the MVP and one
vertex range holding the page's whole vertex array, binds the vertex
buffer once, and draws each image's quad at its offset (firstVertex
4 * i). Each image keeps its own descriptor set for its texture, still
written in batches of 16.

GFX_INSTRUMENT counts the pages drawn and the buffer ranges they take
(overlay_draw, overlay_draw_alloc); the threaded_video overlay page
lane holds that to two ranges per drawn page on drivers that report
it, which the Vulkan validation leg runs.
2026-09-23 05:20:39 +02:00
libretroadmin 3c3561f8c4 glslang: clear two -Warray-bounds false positives
parseSwizzleSelector() bounds its checking loop by the decoded length
as well as by the selector's size. The selector never holds more than
that many entries, and the decoded length is visibly at most
MaxSwizzleSelectors, so the compiler can see every index stays inside
the selector and field-set arrays.

TokenizableIncludeFile's notifyActivated() and notifyDeleted() are
noinline under GCC and Clang. They are the only overrides of those
tInput hooks, so GCC speculatively inlines them into every pushInput()
and popInput() call and then bounds-checks the guarded body against
the smaller input objects those calls push. They run once per include.
2026-09-23 05:13:23 +02:00
LibretroAdmin d000d17abc crt_switch: requested and published sizes are words
The CRT consumer keeps the requested and last-applied core sizes as
one word each and passes sizes packed from crt_switch_res_core down
to the aspect switch. Two framebuffer fields nothing read are gone,
as are the aspect switch's two unused mode-size parameters. The
harness checks the size published for each switch.
2026-09-23 05:12:49 +02:00
LibretroAdmin af3a356f3e video: recording_dump_frame takes the frame size as one word
The software-filter path hands it the filter's packed output size as
it is.
2026-09-23 05:12:45 +02:00
LibretroAdmin 01459fddc0 task_image: the box downscaler takes and returns its size as one word
downscale_box takes the source size packed and hands the result back
the same way; a source too large to pack goes straight to the sinc
stage. task_image_process no longer returns the decoded size, which
it already stores in the image and no caller read.
2026-09-23 05:01:44 +02:00
LibretroAdmin 21cdb789e3 overlay: the output and image sizes are passed as one word
input_overlay_set_scale_factor and input_overlay_auto_rotate_ take the
packed output size their callers already hold, and
task_overlay_load_desc takes the base image's size packed once by the
loader.
2026-09-23 04:59:09 +02:00
LibretroAdmin 7ba4e7207f gdi: texture, atlas and scratch sizes are single words
The GDI driver's remaining size pairs join the ones already packed on
gdi_t: a loaded texture carries its pixel size in gdi_texture_t::dims,
and the font renderer's atlas mirror and tint scratch DIB each keep
their capacity in one word. Both DIBs are created from the same word
their extents are read back out of, so the surface and the size
recorded for it cannot drift apart, and the pixel-count multiplies go
through VIDEO_SCALE_AREA so a buffer size widens to size_t before the
bytes-per-pixel factor.

Either helper refuses a size past what a word holds rather than
recording a capacity smaller than the surface it describes.
2026-09-23 04:58:51 +02:00
libretroadmin e77492d102 glslang: read a double's bits through memcpy in OutputDouble
The binary dump of a double copies its bits into an unsigned 64-bit
word with memcpy rather than reading the double through a long long
pointer, which broke strict aliasing and tripped -Wstrict-aliasing at
-Os. The word is unsigned so shifting its top bit out is defined, and
the loop only shifts it, so each printed digit is the matching bit of
the value.
2026-09-23 04:56:28 +02:00
LibretroAdmin 6a6ef15435 companion: the thumbnail engine's sizes are words
companion_thumbs keys its cache, requests, animations, deliveries and
scaler on one VIDEO_SCALE_PACK size, and refuses a size with an empty
axis. The Win32, Cocoa and Qt companions carry it the same way through
their own pane and pixmap helpers. A decoded image that does not pack is
not scaled rather than scaled from a clamped size.
2026-09-23 04:52:02 +02:00
LibretroAdmin 85a8c089a5 d3d10/11/12: the overlay setters write a copy the draw uploads once
A page's sprites live in a CPU copy (overlays.shadow): set_alpha,
vertex_geom and tex_geom write their sprite there and mark the page
dirty, and the overlay draw uploads the whole page in one map when it
is - a WRITE_DISCARD on D3D10/11, so the frame before keeps the buffer
it drew from. A page load resets the copy and leaves the upload to the
first draw. The setters still refuse an index off the page.

tools/overlay_setter_bounds.py fails a D3D10/11/12 setter, or the
helper it goes through, that maps the sprite buffer.
2026-09-23 04:41:10 +02:00
LibretroAdmin 9b731bfda0 video_filter: sizes cross the softfilter wrapper as words
rarch_softfilter_new, rarch_softfilter_get_output_size and
rarch_softfilter_process take the size packed, the wrapper keeps its
maximum as one word, and video_thread_filter hands the size back the
same way. The plugin interface in softfilter.h keeps its two axes.
2026-09-23 04:40:17 +02:00
libretroadmin 94e754ff94 ozone, companion: clear -Wmaybe-uninitialized and -Wformat-truncation
ozone_alloc_node() zeroes the node's packed attr word in one store
instead of going through the height, sublabel and wrap setters, each of
which reads the word first. Bits outside those three fields are now
defined too, so the bitwise node copy in ozone_snapshot_visible() never
carries indeterminate data.

companion_core_browse_name() bounds the volume label with a precision
in the drive-name format, so the letter suffix always fits in the
64-byte buffer. Real volume labels are far shorter than that bound.
2026-09-23 04:34:54 +02:00
LibretroAdmin 65175892f8 video: the scaled-viewport helpers take the viewport size as one word
video_viewport_get_scaled_aspect2 and video_viewport_get_scaled_integer
take the packed size; the frontend hands them vp->full_dims as it is,
and gx passes its own full_dims.
2026-09-23 04:34:27 +02:00
LibretroAdmin 3733267c5f settings: the last two pairs in settings_t are words
The desktop companion's saved window geometry and the camera's
requested capture size were the two width/height pairs left in
settings_t. Six unsigned fields, three words.

The companion's geometry is a rectangle four UI drivers agree on --
Qt, win32, Cocoa and the two companion test harnesses -- written as
four axes at a time and read back as four. It is an origin and a size
now. Its origin packs as a signed word even though nothing negative
reaches it: both writers clamp at zero and every row is bound to
32767, so VIDEO_POS_PACK's range covers it with the sign intact
rather than laundered.

The four config keys are unchanged, which matters more here than
usual: tools/companion_qt_persist_test.sh reads
desktop_menu_window_width and _height straight out of the retroarch.cfg
RetroArch writes on quit, and companion_win32_test / companion_cocoa_test
assert on the saved values. The four rows move from S_UINT_EX, which
binds by field name, to S_UINT_AT_EX with a half tag, so they are
literal in configuration.c now; S_UINT_AT_EX is added to the one label
pass in msg_hash_us.h that did not define it.

The camera pair needed none of that. No config key and no menu row
binds either axis -- it is read once, in retroarch.c, to ask the camera
driver for a size.
2026-09-23 04:34:14 +02:00
LibretroAdmin 647e0a32fb Threaded video: an overlay alpha apply sets only the images that changed
The video thread keeps the float bits it last handed the driver for
each overlay image (alpha_applied, its own) and an apply passes on
only those that differ, so one press on a page is one driver set. A
page load marks them all unknown and the next apply sets every image;
without the array every image is set at every apply.

The overlay page lane checks one change is one set, a repeated value
is none, and a new page gets all of its images.
2026-09-23 04:33:54 +02:00
LibretroAdmin 26d185d03c overlay: an image's alpha is set only when it changes
input_overlay_alpha_pass() works out every image's alpha for the poll
- opacity, a pressed desc's highlight, 0 for an LED image that is off -
into scratch and hands the driver only the images whose alpha differs
from the last one it was given. A page load, through load() or
load_textures(), forgets what was sent, so the next pass sets them
all. The per-image sent and scratch arrays are one block sized for the
pack's largest page; without it every image is set every pass.

The LED hiding moves with it into input/input_overlay_alpha.c, and the
immediate hide of an LED going out goes through the same record, so a
light that goes out and back on between two polls is shown again.
overlay_visibility_bounds_test pins no-change, press/release, forget,
opacity change, that LED case and the no-block fallback.
2026-09-23 04:16:59 +02:00
LibretroAdmin 7c7fbeb37a threaded_video test: the aspect index is read from the viewport flag word
The viewport publish carries the aspect index in six bits of slot 0,
so the lane decodes it from there.
2026-09-23 04:16:20 +02:00
LibretroAdmin 5fc62fef3f video: the statistics text has room for every line and stays in bounds
The statistics overlay's text and the threaded wrapper's copy of it are
both VIDEO_STAT_TEXT_SIZE (1664), room for every section at once. Each
line goes through video_driver_stat_appendf(), which keeps the text
terminated within the buffer and takes nothing once it is full.

The threaded video harness's stat text bounds lane appends past the
buffer and checks the length, the terminator and the byte after it.
2026-09-23 04:16:16 +02:00
LibretroAdmin 2e4fc9fcaa settings: the windowed-mode geometry is two words
The window's own size and the ceiling its auto-resize honours were
four unsigned fields in settings_t. Both pairs are one word now, using
the half-aware binding the custom viewport got: the config keeps
video_windowed_position_width/_height and video_window_auto_width_max/
_height_max as four keys and the menu keeps four rows, each carrying
which half it is.

The four menu rows move from S_UINT, which takes a field name, to
S_UINT_AT_EX, which takes an offset -- that is where the half tag
rides. Two of the blocks that include this header define only the
macros their rows used, so S_UINT_AT_EX is added to the enum pass in
msg_hash.h and the label pass in msg_hash_us.h. The auto-resize pair
was config-bound through the S_UINT pass, which binds by field name;
with the field gone those two rows are literal in configuration.c like
the size pair beside them.

win32_common is the reason this pair was worth packing. It copied all
four axes out of the window, then subtracted the border from the width
and the border, title bar and menu bar from the height -- four writes
to a stored pair, one axis at a time, with the struct holding a
half-adjusted size in between. The chrome comes off two locals now and
the pair is stored once.

Not touched: window_position_x/y. They launder a negative coordinate
through an unsigned field -- SDL and win32 both hand over a signed
position and cast it in, and sdl3_common casts it back out -- so
packing them is a question about what the config file should hold for
a window on a monitor left of the primary, not a mechanical change.
2026-09-23 04:15:32 +02:00
libretroadmin cb83fb2d37 Qt: radio-button choices are enumerated in unsigned integers
UIntRadioButtons walks a setting's range with unsigned counters
instead of a float, which stops advancing past 2^24. The bounds go
through qt_setting_bound(), negative bounds clamp to zero, a step
below one is one, and the walk stops before it would pass the maximum
or wrap. At most QT_RADIO_BUTTONS_MAX buttons are made, so a row with
no enforced maximum lays out a bounded list instead of hanging.
2026-09-23 04:11:31 +02:00
LibretroAdmin 2c8d6b4555 settings: the custom viewport is two words in settings_t
video_viewport_settings_t held x, y, width and height as four members
while video_viewport_t next to it held the same rectangle as two. It
was the one pair in the tree the campaign had been leaving alone, with
a comment on the struct saying why: the menu and the config bind a
setting by address, and a packed half has no address to bind.

So the binding learns about halves. A config row carries CFG_HALF_HI
or CFG_HALF_LO and its ptr addresses the whole word; every read and
write of a uint or int row goes through cfg_uint_get/set and the
signed pair rather than dereferencing ptr. A menu row carries the same
in SD_FREE_FLAG_PACKED_HI / _LO, seeded from two spare bits in the
descriptor's value_offset -- settings_t is nowhere near 2^30 bytes --
and setting_uint_get/set and the signed pair stand in front of
value.target for the generic paths: the typed-value setter, the
default left and right handlers, and setting_reset_setting. Rows
without a tag read and write exactly as before.

The config file is unchanged: custom_viewport_x, _y, _width and
_height are still four keys, so a config written before this still
loads.

x and y went through the uint rows before, with a cast, because a
whole-word store round-trips a negative fine. A half store does not --
the unsigned accessor clamps, so -100 would have come back 0 -- so
they move to the int rows, where the half accessors sign-extend
through VIDEO_POS_X/Y.

Most of the consumers get shorter: video_driver.c and rgui.c were
packing these four into a video_viewport_t's two words and unpacking
them back, and the publish of the seqlock'd parameters was doing the
same. Those are word copies now.

Checked by exhausting both accessor pairs over the ranges the setting
rows allow -- 50015 size cases and 53865 origin cases, including
negative origins -- confirming each axis round-trips and that writing
one never disturbs its partner.
2026-09-23 04:06:37 +02:00
libretroadmin 7d6c71b491 Qt: spin box ranges are clamped in double, and a resync is shown once
A setting's range is float, and a spin box with no enforced maximum
falls back to INT_MAX; that ternary is a float, and 2147483648.0f
converted back to int is undefined - at run time on x86 it is
INT_MIN, which collapses the box to a single value of -2147483648.
The resync in paintEvent then never matches the stored value and
re-sets it on every paint, repainting without end (Auto-Shader Delay,
SaveRAM Autosave Interval).

UIntSpinBox, IntSpinBox and SizeSpinBox now take their bounds through
qt_setting_bound(), which clamps in double and gives an unenforced or
NaN bound the widget's default. Their paint-time resync compares
against the stored value clamped to the box's range, so a value the
box cannot hold is shown at the nearest bound once.
2026-09-23 03:26:02 +02:00
LibretroAdmin c2aa9282fe video: the portrait bias is published only where it is read
Off mobile the two portrait-bias slots were a copy of the two
landscape ones: published every time the parameters changed, loaded by
every read, and never looked at. Both sites that reach for the portrait
pair pick it on the window being taller than it is wide, and both sit
inside #if defined(RARCH_MOBILE).

They are not published off mobile now, and the pair sits last so every
other slot keeps its index on both platforms. Eight slots on desktop,
ten on mobile.

The two fields of the snapshot struct are guarded the same way. Off
mobile nothing assigns them, so a reader added outside the #if has to
fail the build rather than pick up whatever the stack held -- which is
what it would have done had the fields stayed.

On packing the bias pairs into one slot each, which is the other way to
spend these four slots: measured rather than assumed, and it does not
work. The consumers are (int)(padding * bias) and a roundf of the same
shape, so the only question is whether a sixteen-bit bias ever moves
the pixel they land on. Over the two-decimal values the menu and its
typed entry can produce, against paddings from 1 to 8192, it moves
15382 of 827392 -- including at bias 0.53 -- and 1.6% of six-decimal
values, the precision a config file keeps. A pixel of viewport offset
is not worth two slots.
2026-09-23 03:16:46 +02:00
LibretroAdmin e12ccc1df2 hub75: the driver's five stored sizes are five words
hub75_t held the panel chain's own size, the core frame's, the menu
frame's, and where the last frame landed on the panel as an origin and
a size; hub75_overlay_t held an overlay image's size. All five pairs
are one word each now.

The panel width is the row stride, so it is read three times per pixel
in hub75_refresh_once and once per pixel in the overlay blend. Both
take it into a local once rather than through the macro each time.

Left as they are: the overlay's tex_w/tex_h and vert_w/vert_h, which
are normalised 0..1 coordinates rather than sizes, and three blocks of
function locals that never leave the frame they are computed in.
2026-09-23 03:16:43 +02:00
LibretroAdmin cbcc8696ec overlay: LED images are the pack's to name; ledN_map reaches nul buttons only
A pack names the image that shows an LED with overlayN_descM_led = K
(1-based, as ledN_map counts): under led_driver = "overlay" that image
shows while the core has LED K lit, and a pack that names any LED image
is not mapped at all. For a pack that names none, ledN_map still picks
slots of the page, but only the image of a "nul" button answers to it:
a desc that does something when pressed is never an LED, so a config
made for an LED overlay leaves a gamepad pack's controls where they are.

The LED driver reports the core's LEDs (input_overlay_set_led) and
input/input_overlay_leds.c decides what they hide. The per-slot
visibility array, its enum and the parameter threaded through the
alpha and poll paths are gone.

overlay_visibility_bounds_test now builds input_overlay_leds.c with
the LED driver and pins the map/named/off cases and the page bound;
overlay_led_hidden_test runs a mapped pack and a named pack under Xvfb
with Up held.
2026-09-23 03:16:29 +02:00
LibretroAdmin 808f35f950 d3d8: the overlay setters refuse an image the page does not hold
set_alpha, tex_geom and vertex_geom compare the index with the page's
count and return with no page loaded. load() refuses a failed calloc
and frees a page whose texture could not be made, so no size is left
without an array behind it; enable() sets its flag once rather than
inside a loop that never runs for an empty page, and full_screen()
takes a NULL page. tools/overlay_setter_bounds.py now reads d3d8.c.
2026-09-23 03:16:25 +02:00
LibretroAdmin d35d6c2feb runloop: read output dims once for overlay and FULL aspect checks
runloop_check_state() read video_driver_get_output_dims() separately
in the overlay block and the ASPECT_RATIO_FULL block. Read it once
into the existing output_dims snapshot (now declared unconditionally)
and use it in both.

Resolve the TODO/FIXME above the FULL aspect block by documenting why
the rest of it stays as is: each block keeps its own last_dims since
they are gated independently, and the check is polled from the run
loop because video_driver_set_output_dims() runs on the video thread
under threaded video, where the blocking driver pokes done by
video_driver_set_aspect_ratio() cannot be issued.

No functional change.
2026-09-23 03:08:14 +02:00
LibretroAdmin c1da28604d video: the viewport-parameter publish is ten slots, not fifteen
The seqlock'd viewport parameters were fifteen atomic ints, walked in
full by the publish (a compare against each, then a store to each) and
by every read. The video thread reads them once per frame through
video_driver_update_viewport, and every shader bind reads them again
through video_driver_get_rotation_snapshot.

Ten now. The custom viewport was four slots holding one rectangle:
its own setting rows bound the origin to -9999..9999 per axis and the
size to 1..9999, so VIDEO_POS_PACK and VIDEO_SCALE_PACK cannot reach a
clamp, and it is two. Four bounded selectors -- scale_integer,
aspect_ratio_idx, video_scale_integer_scaling and
video_scale_integer_axis -- shared thirteen bits of one slot, with a
negative-array-size check that fails the build if any of those enums
grows past the field it goes into.

rotation and core_req_rotation stay a slot each and are deliberately
not narrowed. Both trace back to a core's RETRO_ENVIRONMENT_SET_ROTATION,
which a core may set to any unsigned, and shader_glsl and shader_gl_cg
pass the value to a shader uniform without reducing it -- so a field
too small, or a modulo four on the way in, would change what a shader
sees for a core that sets something unexpected.

The four bias floats keep a slot each. Two of them are a mirror of the
other two off mobile, which is real waste, but folding a pair into one
slot means sixteen bits an axis, and that is a quantisation rather than
a repacking; left alone here.

Checked by exhausting the flag word over all 1456 legal combinations of
the four selectors and the custom viewport over its range, both
round-tripping exactly, and by confirming the sizing check fails the
build when a field is made too narrow.
2026-09-23 03:07:00 +02:00
LibretroAdmin f6cf4e07a6 materialui: the on-screen keyboard's text box is two words
osk_textbox_x, _y, _w and _h are one rectangle held as four ints. The
whole of it is written in one place, when the text box is drawn, and
read in one place, by the pointer hit test that decides whether a tap
landed inside it. Two fields now: an origin in VIDEO_POS_PACK's layout,
because the box's x goes negative when a wide message pushes it off the
left edge, and a size in VIDEO_SCALE_PACK's.
2026-09-23 02:59:30 +02:00
LibretroAdmin 25d078ca08 overlay: a hidden image stays hidden when pressed; alpha bytes saturate
The pressed-desc highlight in input_overlay_post_poll() asks the page's
visibility first: an image the LED driver has hidden (led_driver =
"overlay", ledN_map) stays hidden while its button is held, instead of
showing for exactly as long as it is pressed.

An overlay alpha is opacity times a desc's alpha_mod, which packs set
above 1 for pressed buttons. Drivers that pack it into a byte (d3d8,
d3d9 cg/hlsl, d3d10/11/12, gdi, gx, gx2, gxm, sdl2) now convert through
VIDEO_ALPHA_BYTE(), which saturates, so a pressed button no longer
wraps to a dimmer alpha than a released one. tools/overlay_setter_bounds.py
fails any driver that packs one without it; overlay_led_hidden_test holds
Up through XTEST under Xvfb and checks the hidden image stays off screen.
2026-09-23 02:52:31 +02:00
LibretroAdmin 24a012f7a8 video: the statistics overlay's text is built out of the frame body
The statistics text is built in video_driver_frame_statistics(), a
VIDEO_NOINLINE helper called only while the overlay is shown, so
video_driver_frame carries none of its formatting.
2026-09-23 02:50:06 +02:00
libretroadmin e4ed43c4d2 Reapply "video: the overlay viewport's seqlock gets its release fence"
This reverts commit 02d267c75c.
2026-09-23 02:24:51 +02:00
github-actions af46d2beed Fetch translations from Crowdin 2026-09-23 00:24:43 +00:00
libretroadmin 02d267c75c Revert "video: the overlay viewport's seqlock gets its release fence"
This reverts commit 025f456b2a.
2026-09-23 02:09:38 +02:00
sonninnos 434faf2aad Move Display Information to Information root + add menu icon 2026-09-23 03:01:20 +03:00
libretroadmin 119e832217 scanline sync: read the output lines outside the frame path
The Scanline Sync helpers are built on every platform. When the sync is on, video_driver_init_internal() and a CRT mode switch read the output lines directly, so the first frame after startup or a mode change no longer spends time on the lookup. Only switching the sync on at runtime leaves the read to its first frame, and nothing is read while it is off.
2026-09-23 01:51:39 +02:00
libretroadmin ac52e5206d scanline sync: read the output lines only when the sync runs
video_driver_scanline_before_frame() reads the display's active and total lines itself, on its first frame and again after a video driver reinit or a CRT mode switch clears them, so nothing is fetched while Scanline Sync is off and each change is read once. An output size that cannot be read is retried about once a second.

The lookup is file-local to video_driver.c and built only with HAVE_D3DKMT, the one configuration with a beam position, and the display-change handler and d3dkmt_init() no longer touch it.
2026-09-23 01:35:06 +02:00
sonninnos 14e4374ecc Add more missing sublabels 2026-09-23 02:16:31 +03:00
LibretroAdmin 0778788524 menu: an overlay with nothing to press leaves the mouse to the menu
The menu turned its mouse off whenever any overlay was alive, so that
a press on the overlay is not also a click in the menu. An LED overlay
(led_driver = "overlay") is a page of "nul" buttons whose images are
lit and unlit - nothing on it responds to a press - yet it took the
menu's cursor away all the same: fullscreen with such an overlay shown
in the menu, no cursor was drawn and the mouse did nothing.

The desc loader now marks a page OVERLAY_TAKES_INPUT when any of its
descs acts on a press: every type but a button, and a button that
binds something other than "nul". The menu gives its pointer up only
to an alive overlay whose active page carries that flag, or whose own
pointer mode (input_overlay_pointer_enable) is on. struct overlay's
flags widen to 16 bits for the ninth flag.

overlay_menu_mouse_test runs the real binary under Xvfb in the overlay
on-screen workflow: with a "nul" pack the RGUI cursor must follow the
X pointer, with the same pack bound to "a" it must not, each with
threaded video off and on. It fails on the old code (no cursor with
the "nul" pack) and passes on the new.
2026-09-23 01:12:38 +02:00
sonninnos 8d4bccac55 Remove outdated wasapi+sleep+midi-hack 2026-09-23 01:51:25 +03:00
libretroadmin d9b13c0640 config.def.h: make the menu enums visible without HAVE_MENU
configuration.c expands the single-source setting rows from
settings/settings_def_menu_filebrowser.h in every build, so the
default of menu_file_browser_extension_display names
MENU_FILE_BROWSER_EXTENSION_DISPLAY_ALWAYS even when the menu is not
compiled in. Include menu/menu_defines.h ahead of the HAVE_MENU gate;
it only carries enums and retroarch_types.h already pulls it into
menu-less builds, so builds with the menu see no change.
2026-09-23 00:39:20 +02:00
sonninnos 9e3ee45f9e Scanline Sync refactor (#19610) 2026-09-23 00:37:45 +02:00
RyunamandLibretroAdmin 1243747c4b Config: Convert Ozone 'Color Theme' setting to array (#19451)
* Config: Convert Ozone 'Color Theme' setting to array

* Ozone: keep the color theme list in one place

The array conversion left the list of themes spelled out three times
- the identifier-to-theme table in ozone_set_color_theme(), the
identifier-to-label table in menu_setting.c, and the hand-written
'|'-separated values string in settings_build_menu() - all of which
had to be kept in step by hand for every theme added.

Move the list into menu/drivers/ozone_color_themes.h as an X-macro
and expand it in both places; the values string is now built from
the same list.  The frozen legacy numeric table in configuration.c
stays separate on purpose.

While here:

- Left/right on a value that is not in the list (an out-of-range
  number from an old config, a hand edit) did nothing, leaving only
  the dropdown to get out.  Step from the default theme instead,
  which is what such a value renders as; the label shown for it is
  now derived from DEFAULT_OZONE_COLOR_THEME rather than hard-coded.

- Find the color theme row to turn into ST_STRING_OPTIONS by its
  enum rather than by assuming it is the first row of
  settings_def_ozone_sidebar.h.

* Ozone: drop stray struct member, guard a NULL color theme

- Remove the unused 'unsigned draw_entry_delay' that the array
  conversion's conflict resolution left in struct ozone_handle.

- ozone_set_color_theme() now strlcpy()s the identifier into
  last_color_theme, so a NULL would crash.  The frame path only runs
  while the menu is alive, when video_info carries the pointer, but
  fall back to the default identifier rather than depend on that.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-09-23 00:12:51 +02:00
LibretroAdmin f26a0f09e8 menu: limit file extension display to content browsing
filebrowser_parse() backs every file picker, not just Load Content,
so Duplicates Only / Never also stripped extensions in the shader,
overlay, config, font and playlist pickers.  There the extension is
often the only thing telling files apart (crt.slangp vs crt.glslp),
so apply the setting only to content lists (DISPLAYLIST_DEFAULT and
DISPLAYLIST_CORES_DETECTED, minus the overlay, image, video font and
playlist-collection pickers that also come through there).  Say so
in the sublabel.

Also move the Always / Duplicates Only / Never strings in
msg_hash_us.h from after the multimedia group to just ahead of it,
beside the file browser group, and regenerate
tools/settings_table.reference for the new setting.
2026-09-22 23:53:33 +02:00
LibretroAdmin 03478265b7 menu: tidy up file extension display modes
Follow-ups to the File Extension Display setting (#19611):

- Move the helpers out of menu_cbs_get_value.c, where they were not
  value callbacks, and make them static next to their only callers:
  the prepare step in menu_displaylist.c and the label formatter in
  menu_driver.c.  Only menu_file_browser_stem_length() stays shared,
  declared in menu_entries.h; menu_cbs.h goes back to how it was.
- Replace the bare 0/1/2 entry states with
  enum menu_file_browser_extension_state.
- Duplicate detection worked out every name's length-without-
  extension again inside the qsort comparator, each time going
  through path_basename() and its archive-delimiter scan.  Work it
  out once per entry and sort an array of {cbs, path, length}
  instead, which matters in large ROM folders on slow hardware.
- Compare names with TOLOWER instead of strncasecmp, which nothing
  else in the frontend relies on.
- Build the " (ext)" hint with offset-tracked strlcpy calls instead
  of chained strlcat, which rescanned the buffer on every append.
- Put the rich-label condition in menu_entry_get() back to plain
  cbs->action_label; every entry gets a label callback, so the extra
  file_extension_state test never changed anything.

No change in what gets drawn.
2026-09-22 23:53:29 +02:00
Chris Bradel 8b6b60d37a menu: add file extension display modes (#19611)
Add a File Browser setting with Always, Duplicates Only, and Never modes. Duplicate detection uses the filtered view and preserves list order. Format only the drawn label so stored paths and menu entry paths remain unchanged.
2026-09-22 23:45:29 +02:00
LibretroAdmin af6aa7a1bc gdi: the driver's nine stored sizes are nine words
gdi_t carried eight width/height pairs and struct gdi_overlay a ninth:
the core frame's size, the screen's, the window surface's, the DDB's,
the menu frame's, the menu compositing surface's, the capacities of the
two cached scratch DIBs, and an overlay's texture size.

Two of them are there specifically to be compared against a size and
acted on. bmp_dims is the clearest, and its own comment says why it
exists: it tracks the DDB's real size because comparing against the
core's frame size instead triggers a destructive DeleteObject +
CreateCompatibleBitmap every frame, racing WM_PAINT and producing
visible flicker. That comparison is now one word.

The scratch DIB slots said they kept width and height apart rather
than a pixel count "because BITMAPINFOHEADER cares about both". It
still does; both axes are still there, in one field.

Two "use this size, or fall back to that one" reads in the paint path
picked each axis independently. Both fields in each pair are only ever
written as a whole, so the fallback is one ternary over two words.
The overlay's zero-size guard is left per-axis: it exists to reject a
malformed overlay, and a word test would accept one whose width alone
came back zero.

Also corrects three comments still naming vp.full_width/full_height
and bmp_width/bmp_height, which went to single words earlier.
2026-09-22 23:06:19 +02:00
LibretroAdmin 22bb3ddedc gl2: the driver's four stored sizes are four words
gl2_t held the output size, the streaming texture's size, the viewport
the last frame went out at, and the size of the retained backbuffer
copy as eight fields. All four are one word each now, which finishes
the GL family -- gl1, gl3 and the FBO chain have already been through.

tex_w and tex_h were the pair with reach: thirty-five sites, most of
them sizing a buffer from the product of the two. Those go through
VIDEO_SCALE_AREA, so the byte counts widen before the bytes-per-pixel
multiply rather than after.

The retained backbuffer's pair was compared a half at a time to decide
whether the texture could be reused, then stored in two statements;
it is one word either way. Five sites clamped the readback rectangle
to the output size with (a > b) ? b : a written out per axis; they use
MIN.
2026-09-22 23:01:04 +02:00
LibretroAdmin d04d2a6adc video_defines: a packed size's area, widened to size_t
VIDEO_SCALE_W(d) * VIDEO_SCALE_H(d) had grown to forty sites across
twenty-three files, nearly all of them sizing an allocation. Both axes
clamp at 65535, so the product alone still fits in a 32-bit unsigned --
0xfffe0001 -- but the thing being computed is almost never the pixel
count on its own. It is the pixel count times the bytes per pixel, and
that multiply wraps. A wrapped allocation size does not fail; it
succeeds at the wrong size and the write past it comes later.

VIDEO_SCALE_AREA(d) widens to size_t first, so the bytes-per-pixel
multiply that follows is done at pointer width on any 64-bit target.
Three call sites had already been written with a (size_t) cast in front
of the first half for exactly this reason; the rest had not.
2026-09-22 23:01:01 +02:00
LibretroAdmin 067df73763 gl3: the driver's four stored sizes are four words
gl3_t kept the output size, the viewport the last frame went out at,
the hardware render target's maximum size and the menu texture's size
as eight fields. All four are now one word each.

The menu texture's pair was the one that could drift: its guard
compared each half against the incoming size to decide whether to
reallocate, then stored the two halves separately. It is one word
either way now.

Four sites clamped the viewport to the output size with
(a > b) ? b : a written out per axis, twice over; they use MIN.
2026-09-22 22:55:40 +02:00
LibretroAdmin f89bae0c26 video: frame conversion and the recording readback take size words
video_driver_convert_xrgb2101010, the threaded wrapper's frame
conversion and video_thread_record_take take the frame size as one
packed word. The recording readback keeps its encoder size and its
last source size as words, so a viewport change is a word compare and
the recorder's gpu_dims reaches it as it is stored.
2026-09-22 22:55:37 +02:00
LibretroAdmin 1c6a1c1ea5 gl3: the non-Slang render chain drops four fields nothing reads
last_width[GL_CORE_NUM_TEXTURES], last_height[GL_CORE_NUM_TEXTURES],
hw_render_last_width and hw_render_last_height are declared in the
chain struct and never written or read, here or anywhere else in the
tree.
2026-09-22 22:55:31 +02:00
LibretroAdmin 5514fd3eef gfx_surface: surfaces are made from a size word
gfx_surface_new and gfx_surface_new_static take the surface's size as
one packed word, stored as given, and the thumbnail's surface helper
passes it through. VIDEO_SCALE_FITS says whether a size packs
unclamped; the thumbnail and overlay loaders check it before sizing a
surface for a decoded image, since a clamped axis would allocate less
than the decoder writes.
2026-09-22 22:48:43 +02:00
LibretroAdmin 84fbb08706 gl1: the driver's four stored sizes are four words
gl1_t kept the last frame's size, the screen's, the menu frame's and
the viewport the last frame went out at as eight fields. All four pairs
are now one word each.

Three of the four existed to be compared against an incoming size and
then written back, a half at a time, to decide whether to reallocate:
the frame path in gl1_gfx_frame, the one in the render-target setup,
and the menu path guarding MENU_SIZE_CHANGED. Each is now one word
compared and one word stored.

The font viewport's fallback reads better for it as well. It wanted the
screen size, or the frame size when the context driver has not reported
one yet, which took a VIDEO_SCALE_PACK over two ternaries picking each
axis on its own; it is one ternary over two words.

Left alone: inv_win_width/inv_win_height, which are reciprocals for the
vertex math rather than a size.
2026-09-22 22:43:53 +02:00
LibretroAdmin 6d16b0e0f6 vulkan: the driver's six stored sizes are six words
vk_t held six width/height pairs: the output size, the streaming
texture's size, the viewport the last frame went out at, the size of
the retained backbuffer copy, the extent the last readback actually
copied, and the size the hardware-render interface last saw.

Three of them were being compared a half at a time to decide whether to
rebuild something. The retained backbuffer is the clearest: two
separate comparisons against the swapchain's width and height decided
whether the copy could be reused, and the pair was written back in two
statements after the reallocation. That is now one word compared and
one word stored, so a reallocation cannot leave the struct claiming a
size the image does not have.

Two viewport clamps read (a > b) ? b : a spelled out per axis over
three lines each; they use MIN, which is what they were.
2026-09-22 22:43:49 +02:00
LibretroAdmin 0298cc3173 video: drop read_frame_raw, which no driver provides
No video driver fills read_frame_raw - GL2's implementation only built
under a test define that nothing sets - so the slot, its frontend
wrapper, GL2's implementation and the screenshot path that consumed it
are gone. The screenshot task always takes the raw frame from the
frame cache, and prefers a viewport read on any hardware-context core.
2026-09-22 22:42:43 +02:00
LibretroAdmin 7c6966487b translation: the frame size travels as one word
The translation driver's translate hook and its HTTP and Apple
implementations take the frame size as one packed word, which
run_translation_service and the response handler keep as they get it
from the frame cache. The software-convert context's size fields,
which the callback no longer reads, are gone.
2026-09-22 22:38:33 +02:00
LibretroAdmin fddea56df2 shader_gl3: the slang filter chain's stored sizes are words
The GL3 half of the same change: gl3_framebuffer's size and the size a
gl3_pass last rendered at are one word each, and gl3_pass_get_output_size
returns the size rather than writing it through two out-pointers.

Its float-to-pixel conversion goes through VIDEO_PX for the same reason
as the Vulkan one -- a plain cast of roundf() is undefined outside int
range.

Two blocks here declared a pair of locals only to copy the input
texture's size into them and hand them straight to
gl3_framebuffer_set_size, with the size comparison above spelled out a
half at a time. Both are now one packed comparison and a direct call.
Six declarations across the file were indented a level too deep, which
is how those blocks looked from a distance; they are gone with the
locals and fields they belonged to.
2026-09-22 22:38:17 +02:00
LibretroAdmin 1f9f2275bd shader_vulkan: the slang filter chain's four stored sizes are words
slang_framebuffer, slang_pass and vulkan_filter_chain between them held
four width/height pairs as eight fields -- a target's size, the size a
pass last rendered at, the chain's maximum input size, and the source
size accumulated across a per-frame build. All four are now single
words.

Two of them were being passed through out-pointers, which is what made
the packing worth doing rather than merely tidy. slang_pass_set_pass_info
wrote its result into &source_w and &source_h for the caller to feed
into the next pass, and slang_pass_get_output_size did the same one
level down; at the deferred call site those out-pointers were aimed
straight at the chain's own deferred_source fields. Both now return the
size as a word, so the chain walk carries one value per pass instead of
a pair the callee reaches back through two pointers to set.

slang_pass_get_output_size also converted its two floats with a plain
cast of roundf(), which is undefined for anything outside int range.
It goes through VIDEO_PX, which rounds and bounds.

Four declarations in those structs were indented one level too deep;
they are gone with the fields they belonged to.
2026-09-22 22:38:14 +02:00
LibretroAdmin c8f10ce378 screenshot: the task carries its frame and output sizes as words
The screenshot task keeps the frame's size and the size it writes out
as two packed words; screenshot_dump, the rotate step and the PNG/BMP
wrappers take them as words and unpack only where libretro-common's
encoders and scaler want width and height. The output buffer is
allocated straight into the task state, and the scaler pointer sits in
the PNG branch that uses it.
2026-09-22 22:35:52 +02:00
LibretroAdmin c6212745a6 video: the cached frame's API speaks its size word
video_driver_cached_frame_info, the cached_frame_read callback and
video_driver_cached_frame_publish pass the frame's size as the packed
word the cache already stores. The reinit snapshot keeps it as one
word from read to republish, and the screenshot copy carries it the
same way.
2026-09-22 22:32:17 +02:00
LibretroAdmin fb5dc31ecb gl2: the Mali scissor workaround packs its pair, and compiles as C89
gfx_display_gl2_discard_draw_rectangle took width and height apart and
kept the last pair it saw in two statics, comparing them a half at a
time to decide whether the scissor rectangle needed rebuilding. It now
takes the size word its only caller already holds and keeps one static,
so the test is a word comparison. That also retires the two locals
gfx_display_gl2_draw was unpacking solely to pass them back in.

The same block had a declaration after a statement, which C89 does not
allow. It has been there a long time: the whole workaround sits behind
#ifdef MALI_BUG, which is defined only on __arm__ and __aarch64__, so
no lane in the compile matrix -- all of which run on x86-64 -- has ever
preprocessed it. Compiling gl2.c with __arm__ forced is what found it.
2026-09-22 22:30:16 +02:00
LibretroAdmin d5621fd9e7 gl2/gl3: the FBO chain's three sizes are three words
struct video_fbo_rect held six unsigned fields -- img_width/height,
max_img_width/height and width/height -- three pairs describing one
pass of the shader chain: what it renders this frame, the largest it
renders at any input size, and the texture that holds it. They are now
img_dims, max_img_dims and dims.

The struct goes from 24 bytes to 12, and each driver keeps
GFX_MAX_SHADERS of them, so gl2 and gl3 each drop 768 bytes of state.

gl2_renderchain_recompute_pass_sizes and its gl3 twin are where this
matters. A shader pass scales its two axes independently -- absolute on
one, relative to the input on the other -- so the old code wrote each
axis of each pair in a switch of its own, clamped the four halves to
GL_MAX_TEXTURE_SIZE one at a time, then carried four loop variables to
the next pass. Six of those eight writes could land without their
partner. The axes are still found separately, because they genuinely
are separate, but into plain locals; the pair is joined only where it
is stored, and the loop now carries two words instead of four.

Verified over the 156 compile-matrix lanes, which include six for these
two drivers (desktop, GLES2, GLES3 and the C89 and no-threads gates).
2026-09-22 22:30:13 +02:00
LibretroAdmin 4275acf3fc video: an output's size is one word
video_output_info_t carries a connected output's size as one packed
word, which the display servers fill in one store and the menu and CRT
switch lists read their labels from.
2026-09-22 22:25:26 +02:00
libretroadmin 7c4d623e50 samples/dirwalk: fast-path and sort-only lanes hold the window open
The fast-path and sort-only deferral lanes pin the mechanism - a walk
that fits the window completes inline with no task and no refresh, and
a completed walk too large to sort inline hands only the sort to the
task. They now run with the instrument frozen, so the outcome no longer
depends on whether the small tree walks inside 4ms of wall-clock, which
a TSan build on a CI runner does not. The pacing lane keeps the real
clock and owns the wall-clock claim.
2026-09-22 22:25:00 +02:00
LibretroAdmin 2c1329fd33 video: a display mode's size is one word
video_display_config_t carries a mode's size as one packed word. The
display servers fill and match their resolution lists on it and keep
the current mode's size as a word; the SDL servers sort the list on the
word, which orders by width and then height as before.
2026-09-22 22:23:04 +02:00
LibretroAdmin a19855295a video: the display server's set_resolution takes the size word
set_resolution on the display server, its frontend wrapper and every
display server's implementation take the requested size as one packed
word; a zero axis still means the current one, filled in on the word.
The win32 server keeps the mode it restores as one word, and the menu's
resolution picker parses into one before writing the fullscreen
settings.
2026-09-22 22:18:22 +02:00
LibretroAdmin f65f4cfe5d menu: materialui's savestate list drops its unreachable ticker branch
materialui_render_menu_entry_savestate_list is a copy of
materialui_render_menu_entry_default with the smooth ticker switched
off -- if (0 && mui->flags & MUI_FLAG_USE_SMOOTH_TICKER) -- so only
the plain ticker has ever run there. The dead branch was also the only
writer of value_x_offset, which was initialised to zero and added to
the value's x position on every frame.

The default renderer is untouched: its ticker branch is live and stays
exactly as it is.
2026-09-22 22:15:48 +02:00
LibretroAdmin 6d9d982be3 menu: xmb's thumbnail drop shadow was already switched off
The shadow config sat behind if (0 && shadows_enable) with an else that
set GFX_THUMBNAIL_SHADOW_NONE, so the branch has only ever taken the
else. The comment above it said why -- the thumbnails carry their own
background border -- and that reason is now the whole of it.
2026-09-22 22:15:45 +02:00
LibretroAdmin e661d8daee menu: rgui's mini thumbnail loses two draw paths that never ran
rgui_render_mini_thumbnail carried a drop shadow behind if (0 &&
shadow_enable) and, in the savestate placeholder branch, an "N/A"
label behind an unconditional return. Neither has drawn a pixel.

The label's TODO said what it was waiting on -- a caption belongs on
a missing state file, not on a missing image file -- so that survives
as a comment on the background it would have sat on. With the label
gone the text position, the message lookup and the function's fb_width
unpack have no remaining reader.
2026-09-22 22:15:41 +02:00
LibretroAdmin 7ce68d42f3 gfx_widgets: context_reset and init take the size word
gfx_widgets_init, the widgets' context_reset and the volume and
load-content implementations of it take the output size as one packed
word, which becomes last_video_dims as given. The screenshot widget
keeps its box and thumbnail sizes as words, and the thumbnail scale
factor is computed from the destination and image words.
2026-09-22 22:13:30 +02:00
LibretroAdmin 37f5f0b19d menu: rgui's thumbnails carry their two sizes as words
thumbnail_t held four unsigned fields: max_width, max_height, width and
height. Both are pairs that have to agree -- rgui_process_thumbnail
sets width and height together from the decoded image and the copy loop
below walks exactly that rectangle, so a field updated without its
partner reads off the end of the buffer. They are now one word each,
max_dims and dims, which makes that impossible to write.

One of the two was already packed at its source: mini_thumbnail_max_dims
is a packed word that was being taken apart into max_width and
max_height for two thumbnails and read back as halves. Those four
assignments are now two word copies.

rgui_downscale_thumbnail's max_width/max_height parameter follows, since
its only caller now holds the word.

The halves are still what most of the arithmetic wants, so the two
functions that read them repeatedly unpack once into locals rather than
reaching through the macro at each use.

This costs about 270 bytes of text on x86-64 -O3, from the clamp
VIDEO_SCALE_PACK applies at the two sites that build a word from
unrelated values. That clamp is the point: it is what stops one axis
writing over the other.
2026-09-22 22:10:57 +02:00
LibretroAdmin 5a98e84e4e menu: rgui's render tree carries one framebuffer size word
Eleven functions in rgui.c took the framebuffer size as a separate
fb_width and fb_height, from the two primitives that write pixels
(rgui_fill_rect, rgui_color_rect) up through the render phases that
call them. Every one of those calls started from p_disp->framebuf_dims,
which is already a packed word, so the pair was unpacked at the top of
rgui_render and carried down seventy call sites only to be compared
and multiplied as two halves again.

The parameter is now a single fb_dims. Functions that need the halves
unpack them locally, which costs a shift and a mask once per call
rather than a second argument at every level, and the call sites pass
p_disp->framebuf_dims straight through.

Two consequences worth naming. rgui_render no longer unpacks fb_height
at all, and the framebuffer-resize test it feeds -- last_dims against
a freshly packed pair -- becomes a comparison of the two words it was
always really comparing.
2026-09-22 22:10:53 +02:00
LibretroAdmin 189a7d2669 gfx_display: texture loaders report one size word, the cursor reads it
gfx_display_reset_textures_list and its buffer variant report the
loaded image's size as one packed word, which the AI service overlay
and the screenshot widget keep as they get it. The icon loader's size
outputs, which no caller reads, are gone, and draw_cursor places the
cursor from the size word it is already given.
2026-09-22 22:08:56 +02:00
LibretroAdmin 144c75cfdf video: set_texture_frame takes the size word
The poke interface's set_texture_frame and every driver's
implementation of it take the menu texture's size as one packed word;
drivers that clamp it to their texture do so on the word's axes, and
Metal's menu view takes it as updateDims:. The threaded wrapper stores
the word it is handed, and rgui passes its framebuffer's word.
2026-09-22 22:04:08 +02:00
LibretroAdmin 053bdea488 menu: materialui's draw path carries one size word
The twenty-two draw helpers in materialui.c each took the output size
as a separate width and height, and each of them handed that pair
straight back to gfx_display, which packs it again. Both halves of the
round trip are gone: the helpers take a single video_dims word and pass
it through untouched.

Two of the call sites are function-pointer variables rather than
definitions -- materialui_render_menu_entry and
materialui_render_selected_entry_aux carry the callback shape -- so
their declared types move with the functions they point at.

The frame-level locals that fed these calls were only ever unpacked
copies of video_info->dims, so they go too, and the seventeen sites
that used them read the packed field directly.
2026-09-22 21:59:32 +02:00
LibretroAdmin 64282e4869 compile-matrix: wayland lanes probe for the generated protocol headers
The Wayland protocol headers are generated into gfx/common/wayland by
configure, so the Wayland lanes probe for them as well as for the
system's wayland-client.h, and the Vulkan one for vulkan.h;
platform_video takes a list of probes and skips on the first missing.
2026-09-22 21:54:15 +02:00
LibretroAdmin 5ed3a1fc8a gl2/gl3: the iOS/tvOS per-frame viewport reset takes the size word
b1e88d6f62 changed the set_viewport slot and every driver's own setter
to take the output size as one packed word, but the two TARGET_OS_IPHONE
calls in gl2_frame and gl3_frame still passed width and height as two
arguments. Those lines only build on iOS and tvOS, so no Linux, Windows
or macOS lane saw them, and the tvOS build stopped compiling.

Pack the two axes with VIDEO_SCALE_PACK, as the other call sites do.
2026-09-22 21:53:07 +02:00
LibretroAdmin dc4a4f132f gfx: the font renderer's flush takes the size word
font_renderer_t's flush and the GL, Vulkan, D3D11/12 and RSX
implementations of it take the output size as one packed word, as do
the GL and RSX font viewport setters behind them. render_msg builds
that word from the driver's own size once, and font_flush, the widgets,
xmb and the screensaver pass the size word they already hold.
2026-09-22 21:49:36 +02:00
libretroadmin d0c0b52693 cloud sync: progress reaches 100% when a sync toggle is off
Server and local manifest entries under a directory whose sync toggle
is off are carried forward rather than synced, and the diff steps over
them in one go. Progress now leaves them out of both the done and the
total counts, and is taken after each diff step rather than before it,
so the step that ends the diff also lands the bar on 100%. Previously
a disabled directory sorting last (thumbnails after saves and states)
was skipped in the final step and the bar stayed at the in-scope share,
e.g. 5% (#19606).

The directory-scope test moves into task_cloud_sync_key_in_scope(),
shared by the skip and by the count taken when the diff starts, so the
two cannot disagree.
2026-09-22 21:42:03 +02:00
LibretroAdmin 025f456b2a video: the overlay viewport's seqlock gets its release fence
The writer marks the stamp odd with a store-release and then writes the
four fields relaxed. A store-release orders the accesses before it, not
the ones after, so nothing stops those field stores from being hoisted
above the odd stamp. On a machine that reorders store with store - ARM,
PPC, so Android, iOS, the Apple Silicon builds, Wii, Wii U, PS3 - a
reader can then load an even stamp either side of a tuple that is half
written, which is the one thing the stamp exists to prevent.

The other two seqlocks in this file already carry the fence, and the
frame cache's says why in a comment. This one was the odd one out.

The harness reproduced the same shape, so it could not have caught
this; it carries the fence now. x86 does not reorder store with store,
so no lane there can make the hole visible - which is the point worth
recording next to it.
2026-09-22 21:41:03 +02:00
LibretroAdmin b1e88d6f62 video: set_viewport takes the size word
The video_driver_t set_viewport slot and each driver's own viewport
setter take the size as one packed word; the GL, Vulkan, GDI, RSX, GXM
and Metal setters store it as the viewport's full size directly, and
d3d8's rect helper returns the fitted viewport's word. Metal's
setViewportDims: replaces the width/height selector.

The menu drivers, widgets and screensaver hand set_viewport the frame
info's size word as they receive it.
2026-09-22 21:40:14 +02:00
LibretroAdmin 7d5b196aa3 ozone: the draw path takes the output size as one word
Sixteen of the driver's draw functions took video_width and
video_height as a pair. They take the word.

A hundred and fifty-eight repacks went with them - each one a
VIDEO_SCALE_PACK of two locals that came from the same word a few
lines above, which is the residue the layers below this one left as
they were packed. Ten locals had no reader afterwards and went too.
259 lines out, 205 in.

Matrix clean, the file native, under mingw and under C89, all at zero
warnings.
2026-09-22 21:31:45 +02:00
LibretroAdmin 39f38d8914 menu: the driver's render and OSK hooks take the size word
render, osk_ptr_at_pos and osk_pointer_over_textbox receive the output
size as one packed word, and the menu drivers pass it on whole: to the
screensaver, to materialui's entries box and to xmb's layout choice.
The menu drivers' init reads the output word without unpacking it, and
the pointer DPI cache keys on the same word.

The ozone_color_theme_defer sample hands the frame info's size word to
render.
2026-09-22 21:31:34 +02:00
LibretroAdmin b32af95295 dispserv_win32: cast QueryDisplayConfig to the local prototype
Builds without HAVE_DYLIB bind pQueryDisplayConfig to the SDK function
directly; it takes the file's own DISPLAYCONFIG structs, so it is cast
the way the GetProcAddress path already casts it.
2026-09-22 21:31:30 +02:00
LibretroAdmin 53c5d0ca36 xmb: the draw path takes the output size as one word
Six of the driver's draw functions took video_width and video_height as
a pair, and the draw context it fills for each item carried the same
pair again. Both take the word.

Thirty-six repacks went with them: each was VIDEO_SCALE_PACK of two
locals unpacked from the same word a few lines above, which is what
every call site turned into as the layers below this one were packed.
Three locals had no reader left afterwards.

xmb_draw_item reads its width and height off the context rather than
its own parameters, so it keeps a pair of locals - they are the halves
of the context's word now rather than two fields.

Matrix clean, the file native, under mingw and under C89, all at zero
warnings.
2026-09-22 21:25:50 +02:00
LibretroAdmin f88f2e31ea video: the display's output size comes back as one word
get_video_output_size - on the display server, the poke interface and
the context driver alike - fills one packed size word, as do the
frontend wrappers over it and the Cocoa and PS3 helpers behind their
implementations. Callers that set the video mode from it pass the word
straight through; the auto window cap keeps its limit packed.

The thumbnail sample stubs include video_defines.h for the size macros
they use.
2026-09-22 21:25:35 +02:00
LibretroAdmin d785da23da gfx: the display and widget helpers stop unpacking what they repack
Four helpers in gfx_display.c and gfx_widgets.c still took the output
size as a pair: the quad batcher, the indicator draw and the two
message draws. The batcher took a second pair for the quad's own size,
and its one caller was handing it VIDEO_SCALE_W(draw.dims) and
VIDEO_SCALE_H(draw.dims) - the halves of a word it already held.

With those four taking words, sixty-two repacks in the two files had
nothing left to do. Each was VIDEO_SCALE_PACK(video_width,
video_height) where the two locals came from VIDEO_SCALE_W and
VIDEO_SCALE_H of the same word a few lines above, so the word goes
through instead. Ten of those locals had no other reader and went with
them.

Matrix clean, both files under mingw, C89 pass, call_arity_check at its
baseline.
2026-09-22 21:20:53 +02:00
LibretroAdmin 4d896053af compile-matrix: wayland and webOS context lanes
The Wayland GL and Vulkan contexts, the GL context as webOS builds it,
and the webOS shim each get a lane. The shim's protocol headers are
wayland-scanner output of LG's webos-wayland-extensions (Apache-2.0),
kept under stubs/webos since the SDK only generates them at build time.

Every lane rejects incompatible pointer types and int-to-pointer
conversions, as gcc 14 does, so a callback whose signature no longer
matches its slot fails here and not only on newer toolchains.
2026-09-22 21:10:47 +02:00
LibretroAdmin cada67a3ef gfx: fpga init and fbdev EGL windows match what they are passed to
fpga_init takes the input driver pointer the video_driver_t slot hands
it, and the opendingux and vivante fbdev contexts cast their native
window to the pointer egl_create_surface takes, as vita and wayland do:
Khronos' eglplatform.h makes EGLNativeWindowType an integer on unix
builds without X11, which gcc 14 refuses to pass as a pointer.
2026-09-22 21:10:43 +02:00
LibretroAdmin 599cf50381 gfx: the font flush and the widget icon take their sizes as words
font_flush took the output size as a pair at all twenty-three of its
call sites, and gfx_widgets_draw_icon took two pairs - the output size
and the icon's own - at eighteen.

The icon draw gains the most: it was unpacking the icon pair only to
pack it straight back into the descriptor's dims, and packing the
output pair again for the draw call under it. Both go through whole.

The packs that appear at call sites here are the menu drivers' own
video_width/video_height parameters, which are the next pair along and
are left for their own change.

Matrix clean, C89 pass over every file touched, call_arity_check at its
baseline.
2026-09-22 21:10:27 +02:00
LibretroAdmin f8cd750b75 wayland/webos: size callback fills one packed word
The webOS get_video_size hands back a single packed size word like the
common Wayland one, and wayland_ctx.c's webOS prototypes for it and for
check_window match the context driver's get_video_size slot.
2026-09-22 21:03:10 +02:00
LibretroAdmin 41bacf060c gfx: three more context drivers read their size word
cocoa_vk's live-size query passes the word through, the WinRT window
size no longer shadows its out parameter, and the emscripten EGL init
takes its axes from the word it fills.
2026-09-22 20:58:55 +02:00
LibretroAdmin a59561a718 gfx: repair three context drivers' size-word reads
sdl3_vk and switch fill a local size word and take their axes from it;
the UWP context's local no longer shadows its out parameter, and the
Cocoa GL live-size query passes the word through.
2026-09-22 20:48:23 +02:00
LibretroAdmin 98fe9f6aa9 samples/gamemode: a clean target, and a skip without a built frontend
The unclaimed-samples job runs make clean before make check in every
sample directory and never builds the frontend. The gamemode sample
now has a clean target (run.sh keeps its stub in a temporary
directory it removes itself), and run.sh skips when there is no built
./retroarch to run.
2026-09-22 20:47:58 +02:00
LibretroAdmin 7673d8a171 gfx: the quad, the cursor and the keyboard take their sizes as words
gfx_display_draw_quad carried three width/height pairs in one
signature and two of them were output sizes with nothing in the names
to tell them apart: video_width/video_height went to the driver, and
width/height was the space the quad's coordinates are measured
against, which the body divides x and w by and flips y against. They
are video_dims and ref_dims now, and the quad's own w/h is dims.

At 133 of the 141 call sites the two are the same value passed twice.
The eight where they are not are the ones the names were hiding: the
menu screensaver measures against its own last size, and four draws in
xmb measure against the view rather than the window.

draw_cursor and draw_keyboard take the output size as a word with it,
which is the shape draw_texture_slice and the scissor calls already
had.

The line count is the argument lists: a pair that took two lines takes
one, so eighteen files lose 837 lines and gain 558.

Full matrix clean, the menu drivers and widgets under mingw, a C89 pass
over every file touched, and call_arity_check at its baseline.
2026-09-22 20:42:22 +02:00
LibretroAdmin e3151742e1 gfx: the thumbnail draw takes its sizes as words
gfx_thumbnail_draw takes the output size and the rectangle the
thumbnail is fitted into each in one word, and
gfx_thumbnail_get_draw_dimensions takes its rectangle the same way.
2026-09-22 20:42:12 +02:00
LibretroAdmin 2540f41fae gfx: the display driver's draw takes the size word
draw and draw_pipeline were the two members of gfx_display_ctx_driver_t
still taking the output size as a pair, beside the scissor calls that
already take it as a word. Both take it as one now, and so does
gfx_display_draw, which is the shim every caller reaches them through
and which passes the word straight down rather than unpacking it on the
way.

Twenty-two backends implement draw and six implement draw_pipeline.
Most read both halves and get them back from the word at the top of the
body; the few that read neither take the word and ignore it, as they
ignored the pair.

The call sites that still hold a pair of their own pack it at the call.
Those pairs are the next layer down - the gfx_display_ * helpers and
the menu drivers' own parameters - and are left for their own change
rather than dragged into this one.
2026-09-22 20:29:21 +02:00
LibretroAdmin 48a2bea0d5 gfx: the nine-slice draw takes its four sizes as four words
gfx_display_draw_texture_slice takes the output size, the texture's
size, the size to draw it at and the area it is placed in each in one
word; the body reads the axes it needs once at the top.
2026-09-22 20:23:46 +02:00
LibretroAdmin bd39245dd2 metal: the driver init reads the window size as one word
video_info_t's width and height became dims; metal.m's init was still
packing the two fields that had gone away. iOS and tvOS both stopped
there, six errors from the one line. applyVideoMode already takes the
word, and the other two callers already pass it.
2026-09-22 20:21:49 +02:00
LibretroAdmin 31a9a89382 x11: the pointer-entered flag and the mouse latch are atomic words
Under the threaded wrapper x11_alive() pumps X events on the video
thread while the input driver polls on the runloop thread, and both
touch g_x11_entered and the wheel/extra-button latch. Both are now
retro_atomic words; a wheel bit is read and cleared in one fetch_and,
so a notch latched between the two is kept.

The threaded-video harness gains an X11 event-pump lane: it sends
crossing and button events from its own connection while a frame is
on the worker and polls against it, so TSan sees the unordered
accesses on every run rather than when the server's pointer happens
to sit over the window.
2026-09-22 20:20:47 +02:00
LibretroAdmin a36955e3ac compile-matrix: the two SDL video drivers get lanes
Nothing here compiled sdl2_gfx.c or sdl3_gfx.c. That is how both went
on reading video_info_t's width and height after the pair had been
packed into dims: every job was green and the build broke.

Each lane skips where its headers are absent, as the dingux and exynos
lanes do, so the SDL2 one runs wherever libsdl2-dev is installed and
the SDL3 one wherever libsdl3-dev is. Putting the old field back fails
the SDL2 lane with the error the build gave.

sdl3_poke_set_video_mode gets the leftover from the interface change
with it: it took the size word apart into two locals and packed them
back together twice, once for the call it forwards to and once for the
field it stores, and nothing else read them. The word goes through.
2026-09-22 20:15:01 +02:00
LibretroAdmin bbdf94b1fc gfx: the scissor rectangle travels as two words
gfx_display_scissor_begin, the display context's scissor_begin and
scissor_end and all seventeen driver implementations take the output
size and the rect's size each in one word; a driver that needs the axes
takes them once at the top.
2026-09-22 20:14:29 +02:00
LibretroAdmin 0e9a40d2d9 qb: add --disable-gamemode
Configure builds can now leave out GameMode support with
--disable-gamemode; it stays enabled by default. Builds that do not
use config.h (the handheld Makefiles, griffin) keep GameMode as
before.
2026-09-22 20:10:14 +02:00
LibretroAdmin dc26c27bf7 GameMode: only leave GameMode if this process entered it
frontend_unix_deinit() leaves GameMode unconditionally, so shutdown
loads libgamemode.so.0 and queries gamemoded even with gamemode_enable
off. Track whether this process entered GameMode and make the leave
request a no-op when it did not.

samples/frontend/gamemode runs the built binary against a stub
libgamemode.so.0 and checks that nothing calls it with the setting
off, and that GameMode is entered and left with it on.
2026-09-22 20:10:10 +02:00
LibretroAdmin d4a816549d sdl: the stored window size is read and written as one word
sdl2 and sdl3 keep a video_info_t of their own; its size reaches the
raster font path and the resize through VIDEO_SCALE_W/H and
VIDEO_SCALE_PACK.
2026-09-22 20:09:07 +02:00
LibretroAdmin 4917564ea0 video: the context size comes back as one word
get_video_size was the last of the size interfaces still split: every
context driver wrote two unsigneds through two pointers, and every
caller supplied a pair of locals to catch them. Thirty-one
implementations, forty call sites, and the same shape at each one.

It writes the word now. The window checks are where that reads best.
Each of them used to hold a pair, fill it, pack it to compare against
the word the caller passed, and pack it again to store:

   unsigned new_width, new_height;
   get_video_size(x, &new_width, &new_height);
   if (VIDEO_SCALE_PACK(new_width, new_height) != *dims)
   {
      *dims   = VIDEO_SCALE_PACK(new_width, new_height);
      *resize = true;
   }

which is now one local, one comparison and one assignment. Eleven
drivers carried that.

Two of them were taking apart something already packed. The Cocoa
contexts publish their backing size to a thread-safe word as
(width << 16) | height, which is VIDEO_SCALE_PACK's layout exactly, and
their _ts readers existed to split it into the pair this interface
wanted; they hand the word over whole now. The Android Vulkan context
built its pair from two atomic loads and compared the halves
separately.

The driver locals that fed it go the same way: mode_width/mode_height
and temp_width/temp_height in gl1, gl2, gl3, vulkan, vg and gdi, which
is where the line count comes from. One test kept its two halves on
purpose - the fallback that asks video_driver for the output size when
the context reports nothing runs when either axis is zero, and the
packed word being non-zero does not mean both are, so it reads
VIDEO_SCALE_W() != 0 && VIDEO_SCALE_H() != 0 rather than dims != 0.

Full matrix clean, the Win32 drivers under mingw, the X11, EGL,
Wayland, DRM and OSMesa translation units the matrix does not reach
compiled here directly, and a C89 pass over every plain-C file the
change touches. The Cocoa contexts have no lane; their hunks were
reviewed by hand.
2026-09-22 20:08:12 +02:00
LibretroAdmin 4e31cd1cc7 menu: resetting the resolution keeps the window state
Start on the Resolution entry sets the video mode full screen only when
the frontend already is - from the setting or the forced-fullscreen
flag - so a windowed desktop session stays windowed; GameCube/Wii and
PS2 stay full screen as before.

samples/menu/resolution_reset links the shipping objects, swaps the
null driver's poke table for one that records the mode, and presses
Start in a windowed, a fullscreen-setting and a forced-fullscreen
session; it runs in the menu harness workflow.
2026-09-22 20:05:58 +02:00
LibretroAdmin c4be8acc48 d3d: drop the dead locals the overlay load and the queue drains leave
Assigned and never read: d3d8's overlay list pointer and the fence the
d3d12 queue drains take a copy of.
2026-09-22 20:03:29 +02:00
LibretroAdmin ff12e7fb6b video: the window size the drivers are initialised with is one word
video_info_t::dims carries both axes, so the size the frontend resolves
travels to every driver's init as one word and the drivers read the axes
they need with VIDEO_SCALE_W and VIDEO_SCALE_H. The Direct3D drivers'
stored copy compares and updates as one word.
2026-09-22 20:03:26 +02:00
LibretroAdmin 8f4180862f metal: implement set_video_mode
The poke sizes the window or takes it full screen through the same
-applyVideoMode: that driver init uses, on the main thread via
cocoa_main_thread_sync so threaded video can reach it, and shows the
cursor only when windowed - matching the Cocoa GL and Vulkan contexts.
A zero axis keeps the view's current size.
2026-09-22 20:00:19 +02:00
LibretroAdmin 12e54560f0 video: a context mode carries its size as one word
gfx_ctx_mode_t holds VIDEO_SCALE_PACK(width, height), and the Cocoa
GL/Vulkan contexts hand it through their main-thread argument blocks
the same way. The size copies those contexts kept but never read are
gone, as are the mode locals in gl1, vulkan and sixel that only
repeated the frame size or were never used.
2026-09-22 20:00:15 +02:00
LibretroAdmin 3245a3abc6 gfx: the display text draw takes the size word
gfx_display_draw_text, its high-precision sibling and the internal they
share take the area the text is placed in as one word; the menu drivers
and the widget layer hand over the word they already hold.
2026-09-22 19:49:56 +02:00
LibretroAdmin 0023598b0f video: a driver's window size is one local, not two
Six video drivers open with the same twenty lines: take the requested
size out of the frame descriptor, fall back to the detected screen size
when both axes are zero, fall back again to the configured fullscreen
size when fullscreen had to be forced, hand the result to the context
driver. With set_video_mode taking the size as one word, the two locals
that carried it between those steps have nothing to be two of.

The zero test is the part worth reading twice:

   if (video->fullscreen && (win_width == 0) && (win_height == 0))

becomes a test of the word, because neither axis set is the whole word
clear. The OpenVG driver gains the most - its fallback already asked
the video driver for a packed output size and took it apart to fill the
pair, and now assigns it.

gl1 and gdi keep their mode_width/mode_height, which are read
separately further down; they take the halves back out of the word.
sdl3_vk_ctx keeps its pair: it passes both to
sdl3_window_get_video_size by address, and a packed half has no
address, which is the same reason the settings viewport is not packed.

Full matrix, a native build and a C89 build from a clean tree, all at
zero warnings, plus the GDI driver under mingw.
2026-09-22 19:46:06 +02:00
LibretroAdmin 98028de03c video: setting the video mode takes the size word
Three interfaces in one call chain each carried the same loose pair.
video_driver_set_video_mode takes it from the frontend, hands it to
video_poke_interface's set_video_mode, and the video driver hands it on
to gfx_ctx_driver_t's - 59 declarations and 35 call sites, all of them
(unsigned width, unsigned height, bool fullscreen). The thread wrapper
already stored the pair packed in its mode packet and unpacked it for
no reason other than to make that call, which is what the chain looked
like from the inside.

All three take the size as one word now, and so do the two helpers
under them, win32_set_video_mode and sdl3_window_set_video_mode.

Most of the payoff is at the call sites. The thread wrapper passes
pkt.data.new_mode.dims straight through. d3d10, d3d11 and d3d12 were
unpacking their own viewport to pass the halves and now pass
->vp.full_dims. Twelve bodies that did nothing but forward the pair
pass the word instead of taking it apart and putting it back together.

sdl3_window_set_video_mode is why the rewriting worked from the
declarations rather than the calls: it takes five arguments and its
pair is not the one before the last, so anything that guessed the
position from the shape of the call would have packed its height
together with its fullscreen flag.

Full matrix, a native build and a C89 build from a clean tree, all at
zero warnings, plus the Win32 drivers under mingw. The Cocoa contexts
and the Metal driver have no lane here; their hunks are the parameter
rewrite and were reviewed by hand.
2026-09-22 19:46:01 +02:00
LibretroAdmin b8093d23d1 gfx: the widget frame path takes the size word
gfx_widgets_iterate, iterate_layout, the per-widget iterate callback,
gfx_widgets_draw_text and gfx_widgets_flush_text all carry the output
size as one word. The two scale caches compare one word, and the layout
check compares against last_video_dims directly.
2026-09-22 19:22:43 +02:00
LibretroAdmin 02102f12d3 menu: a metric rounds to the nearest pixel rather than the one below
Every menu metric is a constant times a DPI scale that is almost never
whole, and the plain conversion to the int it is stored in truncates.
That loses up to a pixel off each one, always downward, and it stacks:
at the 1.3333 scale Ozone's 50px row came out 66 rather than 67, which
is thirteen pixels over a twenty-row list. Ozone's three spacers
already rounded by hand - someone knew - while the other twenty-two
metrics beside them did not.

All of them go through VIDEO_PX now, in Ozone, MaterialUI and the two
integer metrics XMB keeps, so there is one way to turn a scaled length
into a pixel and it is the same one the draw origin uses. The 1px
spacer keeps its floor of one: a hairline that rounds to nothing is not
a hairline.

MaterialUI's node->entry_height is left alone deliberately. It is a
float that accumulates several terms before anything reads it, so
rounding each term into it would be rounding the same value twice.

Pixels move, which is the point: a metric that was a pixel short is a
pixel taller. ENTRY_HEIGHT gains one at the 1.3333 and 1.7778 scales,
ENTRY_ICON_SIZE at 1.7778 and 2.6667, HEADER_HEIGHT at 1.7778. At the
whole scales - 1.0, 1.5, 2.0 - nothing moves at all.

Full matrix, native build and C89 build from a clean tree, all three at
zero warnings.
2026-09-22 19:18:16 +02:00
LibretroAdmin 6a62c64930 gfx: a draw descriptor's origin travels as one word, in whole pixels
The descriptor carried its size as a packed integer pair and its origin
as two floats - the same quad's geometry in two number systems - and
every driver had to bridge that itself. None of them rounded. gl1, gl2
and gl3 hand the float to glViewport, which takes a GLint; gdi and d3d8
cast with (int). So a position that came off a scale factor and landed
at 99.99998 drew at 99, while its width was exact. gx2, d3d10, d3d11,
d3d12 and the two SDL drivers did not truncate at all - they divide by
the viewport to get normalised coordinates - so the same descriptor
drew at a different sub-pixel position depending on which driver was
up.

The origin is one signed pair now, in VIDEO_POS_PACK's layout, like a
viewport's. Whole pixels, decided once where the layout is known
instead of differently by each driver on its way out.

VIDEO_PX is what decides. It rounds rather than truncates, which is
worth a pixel on every value that came off a DPI scale, and it bounds
the conversion, which the plain cast does not: converting a float past
INT_MAX is undefined. That was not theoretical. gfx_widgets_draw_icon's
coordinate math underflows in the first frames after an icon loads, and
the float the descriptor carried took that value as far as
SDL_RenderGeometry, where it became NaN vertex positions and an SDL
error that poisoned the renderer state for every draw after it - the
widget simply vanished. sdl2_gfx and sdl3_gfx each carried a
hand-written range test against it. Both are gone: a signed 16-bit
origin and an unsigned 16-bit size cannot hold a value they would
reject, and a NaN reaching VIDEO_PX lands at the far edge, off-screen
and over the following frame.

The twelve positions that genuinely animate keep their float - the
tween is a pure function of the clock and wants sub-pixel motion - and
round on the way into the descriptor, which is where a sub-pixel scroll
position has to become a pixel regardless.

Checked with the full matrix, a native build and a C89 build from a
clean tree, both at zero warnings, plus the Win32 drivers under mingw
and sdl2_gfx against the host SDL2. metal.m and the 3DS and RSX
drivers have no lane here; their hunks are the mechanical read rewrite
and were reviewed by hand.
2026-09-22 19:18:12 +02:00
LibretroAdmin 1c26684ad1 threaded_video: the 4K surface lane checks the mean submit time
Thread CPU time on Windows advances in whole 15.625 ms scheduler
ticks, so one submit that straddles a tick reads as a full tick and
fails a per-sample percentile. The lane now budgets the mean over all
submits, which tracks the time actually spent on any clock and still
fails when every submit carries a 4K copy. The p50/p95/p99 figures
stay in the baseline line.
2026-09-22 19:04:08 +02:00
LibretroAdmin fe9a3325a7 rgui: the confirm dialog's icons are placed in whole pixels
RGUI blits into an integer framebuffer, and these were the only two
floats in the file. They existed to halve a glyph: icon_padding is
font_width_stride / 2, and every offset built from it was truncated
back to int one to five lines later.

Most of those halves cancelled - the padding appears in pairs that add
back up to a whole glyph - so the icon origins were always integers
taking a detour through float. The two that did not cancel are the hit
box's top edge and its height, and there the truncation threw the half
away. The integer forms keep exactly what the truncation produced:
(size + 1) / 2 for the edge, (size * 5) / 2 for the height, checked
against the float arithmetic across every font stride from 1 to 39 and
every framebuffer geometry RGUI runs at.

No pixel moves. The point is that a software blitter has no half
pixels to place anything on, so nothing here should be asking for one.
2026-09-22 18:39:19 +02:00
LibretroAdmin 3017b2015a gfx: Ozone's scale cap divides by three rather than by a literal
The cap is a third of the screen width, written as a multiply by
0.3333333f. That literal is one ulp below the nearest float to 1/3, so
the cap lands just under the third it is meant to be - 639.99994 rather
than 640 at 1920 wide - and the scale that comes out of the division is
off by the same amount. Dividing says what it means and is exact to the
float.

The name went with it. new_width is not a width of anything; it is the
largest the sidebar is allowed to be, which is what the code below it
compares against.
2026-09-22 18:35:10 +02:00
LibretroAdmin b4d16d67bc video: the frame info's output size travels as one word
video_frame_info_t::dims carries both axes beside the scale_dims and
custom_vp_dims words already there, and the two writers hand over the
packed word they already held. The widget tick takes it straight
through; the netplay widgets read their axes once into locals.
2026-09-22 18:30:43 +02:00
LibretroAdmin bd6bcc21d3 gfx: the animation tick and the DPI scale take the size word
gfx_animation_update, gfx_animation_update_widgets, the update_time_cb
the menu drivers set, and gfx_display_get_dpi_scale all take both axes
in one word. The scale caches compare one word where they compared two,
and the widget path hands its packed size straight through.
2026-09-22 18:30:40 +02:00
LibretroAdmin b3b05b5ef1 compile-matrix: the PS2 video driver gets a lane
ps2_gfx.c is the other console video driver nothing here compiles, for
the same reason as the Vita one: it reaches a compiler only inside the
PS2 toolchain. The viewport work touched it in two places, and neither
has been through a compiler.

This adds the gsKit and PS2SDK stubs it needs - gsKit.h with dmaKit.h
under it, kernel.h for the semaphores its vsync handler takes, and
tamtypes.h for the scalar names the rest lean on - and a lane that
compiles it against them. gsToolkit.h and gsInline.h are the two other
headers the driver includes; gsKit ships both behind its master header,
so here they are one line each.

Same rule as the Vita stubs: the declarations the driver names and only
those, each in the shape the real header gives it, so the lane rejects
what the SDK would reject. Dropping an argument from
gsKit_prim_sprite_texture fails it.

The lane takes the window offset, which Makefile.ps2 turns on and the
driver reads; without it the settings struct has no such member and the
lane would fail on the tree rather than on a change to it.

It is also the only lane that compiles retro_atomic.h's EE
interrupt-mask backend. No other platform selects it, so DIntr/EIntr
and every operation built on them were unreachable from any job here.

ExitHandler() is the one declaration whose body cannot run on the host:
it is the R5900 sequence the SDK writes, and the lane gets away with it
because it parses rather than assembles. A lane that compiled to an
object would need a stand-in; the note in kernel.h says so.
2026-09-22 18:29:27 +02:00
LibretroAdmin f12c36929a compile-matrix: the Vita video driver gets a lane
gxm_gfx.c reaches a compiler only inside the Vita toolchain. Nothing
here builds it, so a change to a struct or a signature it touches is
green in every job until that console job runs - which is how four
lanes broke in a row on the viewport work, one round trip at a time.
The driver is one of the files that work rewrote most: its viewport
origin, its two size pairs and the output size all travel as packed
words now, and none of that has been through a compiler.

This adds the psp2 stubs the driver needs - gxm.h, display.h,
kernel/sysmem.h, common_dialog.h, and the scalar types the rest lean
on - and a lane that compiles it against them.

The stubs carry the declarations gxm_gfx.c names and only those, each
in the shape the real header gives it, so the lane rejects an argument
list the SDK would reject rather than only a malformed one. Dropping
an argument from sceGxmSetRegionClip fails the lane; so does dropping
one from the driver's own gxm_set_viewport. The enums keep the SDK's
values and lose the members nothing here names, which is why a format
constant may be missing: take it from the real header rather than
inventing a value.

Same narrowing suppression as the psp1 lane above it - the driver
folds pool addresses into pointers that a 64-bit host widens, and the
arithmetic is 32-bit on the target.
2026-09-22 18:29:22 +02:00
LibretroAdmin 22a0a7a43d compile-matrix: the WiiU and HUB75 video drivers get lanes
Both compile here already - gx2_gfx.c against the wiiu headers the tree
vendors, hub75_gfx.c against plain POSIX - so the only reason neither
was checked before delivery is that neither had a lane. They are the
drivers this campaign kept breaking unseen.

HOSTOFF moves up to the flags every lane shares. It was defined two
hundred lines below the first platform_video lanes, so a console lane
written above it expanded to nothing and compiled with the host
identity left on, taking the Linux branch of every #if ladder and
proving nothing.
2026-09-22 17:44:08 +02:00
LibretroAdmin 9803e3eadc vg: the OpenVG driver's init reads the output size again
vg_init's fullscreen branch takes the window size from the output word
and writes it straight to win_width/win_height; its two declarations of
out_dims become one. The driver has had neither since the output size
started travelling as one word: nothing here compiled it, so the
statements left in the middle of an if condition went unseen.

tools/platform_stubs/openvg carries the OpenVG surface the driver names
- generated from what it uses, not from the specification - and the
compile matrix gains a lane for it, so the next change to this driver
is compiled rather than read.
2026-09-22 17:31:57 +02:00
LibretroAdmin 481b5050d1 video: the output size's threading note names the atomics
The pair is published with a release store and read with an acquire
load, which is what makes both calls safe from any thread.
2026-09-22 17:26:03 +02:00
LibretroAdmin 3ec31e2ddd tools: a call's argument count is checked in every platform branch
A signature change is caught by the compiler wherever it compiles. The
call sites it does not compile are found by whichever CI lane builds
that platform, one round trip each: changing check_window's parameters
broke the PS3 driver, two Win32 contexts and the webOS window check
that way, one after another. tools/call_arity_check.py reads the tree
as text instead, so a branch behind GEKKO, WEBOS, VITA or _XBOX is
examined like any other, and reports a call whose argument count does
not match the declaration.

It declines what text cannot settle: varargs, calls through a pointer,
function-like macros, an argument list carrying a preprocessor
conditional, and any name a harness stub redeclares with a shape of its
own.

Its first finding goes with it. gx_efb_screenshot() called
intfstream_open() with intfstream_open_file()'s three arguments, so the
GameCube and Wii builds would not have compiled it - nothing did: it
sat behind a TAKE_EFB_SCREENSHOT_ON_EXIT that an "#if 0" defines, and
nothing called it. It goes, with the stream include it was the only
user of.
2026-09-22 17:23:14 +02:00
LibretroAdmin b6222d4505 wayland: the webOS window check takes the size word
gfx_ctx_wl_check_window_webos is the webOS build's stand-in for the
shared handler, so it takes the same one word and compares it whole.
Its declaration in wayland_ctx.c follows.
2026-09-22 17:11:46 +02:00
LibretroAdmin 40e2fa3c88 (Vulkan) Negotiation interface v3 is withdrawn; the frontend presents on the graphics queue
The frontend stops presenting on a queue of its own, and the negotiation
interface goes back to version 2. This removes 5d950a7288 (the second
queue), dadbd009d9 (v3 in libretro_vulkan.h), c265d78ee9 (the Android
carve-out) and 9f686efdfa (the comment on when it is used), and adapts
4823918abe, which stays.

What it bought was narrow: on one vendor, in exclusive fullscreen,
vkQueuePresentKHR blocks inside the call waiting on the display, and
with one queue under queue_lock a core's submissions sat behind that.
What it cost is on record. Its first act was to break every frontend
older than four days against cores that took the queue (libretro/ps2
#167), it needed a per-platform exception within five days because a
window system could not present on the second queue, and it is the only
change in the path where the same issue now reports random aborts at
start and heap corruption at exit - on the v3 path every time. None of
that is proof, and none of it is a measurement in its favour either.
Three places had to agree about it, and the feature was on for some
systems and silently off for others. That is not worth the stall it
removed.

4823918abe - a swapchain rebuild waits out the presents it retires - is
kept, because the bug it fixes is not the second queue's. A present's
semaphore wait is a queue operation behind the last frame's fence, not
under it, on any queue; Mali's Android WSI (#19601) fails every later
present when the semaphore goes out from under one. The empty
submission that fences the presents now goes to the graphics queue,
under queue_lock like any use of it.

A core that asks for version 3 gets 2 and the graphics queue as its
presentation queue, which is what every build before the 17th did.
The vulkan_validation sample builds and passes, the teardown-behind-a-
present case included, on llvmpipe here.
2026-09-22 17:00:47 +02:00
LibretroAdmin 0b2613d40a gfx: the Win32 get_video_size calls take the size word
wgl_ctx and uwp_egl_ctx ask win32_check_window for the size and hand
their own two out-parameters back from its halves. Both also write
through those parameters rather than over them, so the Xbox display
resolution reaches the caller.

tools/mingw_syntax_check.sh reports these files at all now. A header
this box lacks made the compile stop at the include, and the
missing-header filter then reported the file clean: HAVE_D3DKMT pulls
in d3dkmthk.h, so every consumer of win32_common.h went unchecked that
way. The define is dropped when its header is absent, and a parse that
stops early is named and counted rather than folded into the ok line.
2026-09-22 16:57:39 +02:00
LibretroAdmin e57668d229 Derive swap intervals above four, bounded by what the driver presents
'Auto' derives the swap interval as the whole multiple the display rate
is of the content rate, up to MAXIMUM_SWAP_INTERVAL, which the manual
setting's range reads too so the two cannot drift apart. A 480 Hz panel
running 60 fps content holds each frame for eight display frames;
driver_adjust_system_rates() then divides the target by that interval
and the audio rate follows the display.

The multiple has to be one the driver can present: the D3D APIs carry
the interval as a present parameter that stops at four, so those drivers
report that bound through the new get_swap_interval_cap poke and the
ceiling comes down to it. A multiple past the cap falls back to 1 rather
than clamping short, since a short multiple paces the loop fast.

The decision itself moves to runloop_video_swap_interval_for() in
runloop.h, next to the other pure pacing decisions, and
samples/runloop/pacing covers the multiples, the driver cap, the ratios
that are not whole multiples, and the degenerate rates.
2026-09-22 16:47:21 +02:00
LibretroAdmin 676d426e45 gfx: the window check's size reaches every caller
Six drivers read the output size in the else branch of their init:
that branch takes the three statements it needs as a block. The Win32
Vulkan context, the SDL3 Vulkan context and the SDL3 shared handler
pass their own size word on rather than the two names it replaced.
2026-09-22 16:41:03 +02:00
LibretroAdmin 944fb12100 wayland: the toplevel icon and its buffer are released once the icon is set
xdg-toplevel-icon-v1 lets the client destroy the icon object right
after set_icon without the toplevel losing its icon, and the buffer
only has to outlive the icon object; wl_buffer.release is never sent
for it. So wl_create_toplevel_icon() now destroys both straight after
set_icon, and nothing about the icon has to live with the window.

That drops the libdecor_icon, xdg_toplevel_icon and icon_buffer fields,
the shm_buffer back-pointer and the release handler special case. The
failure path also destroys the icon object instead of leaking it.
2026-09-22 16:38:48 +02:00
Davey Hughes ff0000f895 wayland: the libdecor objects and the icon buffer are freed with the window (#19609)
gfx_ctx_wl_destroy_resources_common() released every other Wayland
object but left the libdecor frame, context, toplevel icon and dlopen
handle behind, so every video driver reinit dropped a set and built
new ones. Release them before the surface is destroyed and the display
is disconnected: the frame owns xdg objects built on that surface, and
the context still talks to the display.

The icon's shm buffer leaked the same way. It is freed on
wl_buffer.release, which a compositor that holds the icon need never
send, so the window owns it now; an early release clears the
reference.
2026-09-22 16:37:19 +02:00
LibretroAdmin dc44cc7b0a video: the output size travels as one word, in and out
video_driver_get_output_dims() hands back the word the state already
holds and video_driver_set_output_dims() takes one, so a reader gets a
coherent pair from a single atomic load rather than two halves split
out of it. VIDEO_DRIVER_OUTPUT_WIDTH/HEIGHT were aliases for
VIDEO_SCALE_W/H and go with them.

The window check hands its size back the same way: check_window on the
context-driver vtable, win32_check_window and x11_check_window each
take one unsigned* where they took two, and a driver passes the field
itself - d3d10, d3d11 and d3d12 give &d3dN->vp.full_dims straight to
win32_check_window. The drivers' own size locals travel packed
alongside, and the runloop's two aspect checks compare one word
against the size they last laid out for.
2026-09-22 16:32:54 +02:00
libretroadmin 9383fe9628 gfx_thumbnail_preview: the audio chain against ffmpeg
The preview's sound had been held to itself - decoded, fed, not
stalled - and never to a reference, while its first seconds are
reported as distorted in the field. preview_audio_oracle_test decodes
a track through the chain a preview uses, audio_mixer to
audio_transfer to the demuxer to the AAC decoder, from the first
sample against ffmpeg's decode of the same track: at the track's rate
the two agree to the rounding of a float decoder, and through the
mixer's resampler, the filter's delay aligned for, to a third of a
percent RMS in every half second from the first. So the chain from
the file to the mixer's output is not where the first seconds go
wrong; what remains is downstream of the mixer, or the feed. In the
check target and CI.
2026-09-22 16:05:11 +02:00
Antonio Orefice fc5bf86d63 Change logging verbosity for "optional include not found" shader compilation message from info to debug. (#19608)
"Optional includes are not widely used across shaders and require user tinkering, so I think it is better to cut the noise"
2026-09-22 15:41:38 +02:00
LibretroAdmin 289b74d8b9 gl3: the shader chain's viewport travels as two words
gl3_viewport is this backend's own, so its origin and its drawn area
pack like the frontend's and glViewport, glScissor and the pass sizing
take the halves back out. gl3_set_viewport hands the frontend viewport
over as two word copies.
2026-09-22 15:37:54 +02:00
LibretroAdmin 7d1cf55a0a save: drop the unreferenced autosave dirty marker
Nothing in the tree calls it and no header declares it; the dirty bit
is set where the autosave buffers are compared.
2026-09-22 15:37:40 +02:00
LibretroAdmin d460d0ec9f rsx: the RSX viewport travels as two words
rsx_viewport_t is this driver's own, so its origin and its drawn area
pack the same way the frontend's viewport does and rsxSetViewport takes
the halves back out. A viewport handed straight over is a word copy -
rsx_set_viewport and rsx_frame take vp.dims from rsx->vp.dims as it
stands. The depth range, scale and offset stay floats: they are not
pixel counts.

tools/viewport_shadow_check.py reports the remaining names that mean a
video_viewport_t in one place and another type elsewhere, skipping the
ones already packed, so what is left is the types whose layout is not
ours to change - VkViewport, MTLViewport, wp_viewport and the GL
handles.
2026-09-22 15:06:09 +02:00
LibretroAdmin 138dcfefc6 rsx: the RSX viewport keeps its own origin
rsx_viewport_t is the RSX's own float x/y/w/h that rsxSetViewport and
rsxSetScissor take; only rsx->vp and gcm->vp are the frontend's packed
viewport, so the local takes its two axes out of those and passes them
on as floats.

tools/viewport_shadow_check.py reports the names this confusion needs:
one that means a video_viewport_t somewhere in the tree and some other
type in the file at hand. It names rsx_viewport_t, VkViewport,
MTLViewport, gl3_viewport, wp_viewport and the GL handles, by
declaration rather than by compiling, so the drivers no lane here
builds are covered too.
2026-09-22 14:58:13 +02:00
LibretroAdmin 05292a712d companion: poll() reads the published session without the lock
An atomic beside anim_sess says whether the animation thread has one
published, so a poll with nothing playing takes no lock at all. The
audio window is still started and fed under the lock, where the
session stays valid.
2026-09-22 14:54:33 +02:00
LibretroAdmin 2967bab24f tpool: the queue depth and the outstanding count answer without the mutex
Two atomic counters track the items in the queue and the items queued
or in progress, written under work_mutex beside the queue itself and
read without it. tpool_help returns on an empty queue and tpool_wait
returns on an idle pool without touching the lock, so a caller looping
in either while it waits on work in other hands leaves the queue's
lock to the threads that need it.
2026-09-22 14:54:28 +02:00
LibretroAdmin a9dfba8ac9 menu: the laid-out sizes the drivers cache travel as one word
Every menu driver keeps the video size it last laid out for and
compares the incoming one against it; that pair is one word in
VIDEO_SCALE_PACK's layout, so the check is a single compare against
last_dims rather than one per axis. RGUI's glyph cell, its window size
and its mini-thumbnail cap, and MaterialUI's nav-bar layout go the same
way.

The pairs whose axes are derived apart from each other keep two
members: the widgets' bg_width/bg_height come from text metrics and
line height separately, and MaterialUI's thumbnail caps are clamped one
axis at a time, so packing either would buy one store and cost eleven
read-modify-writes.
2026-09-22 14:52:58 +02:00
LibretroAdmin 5ea7612b07 video: the viewport's origin travels as one word
video_viewport_t holds x and y in VIDEO_POS_PACK's layout, x in the high
half and y in the low, each a signed 16-bit offset that sign-extends
back out through VIDEO_POS_X/Y - an origin goes negative wherever
integer scaling overscans the window. VIDEO_POS_PUT_X/Y set one axis
where a caller has only that one, and an offset past a half saturates,
so neither axis can put the image on the far side of the display.

The whole viewport is now three words. The threaded wrapper's seqlock
publishes the origin as one slot, so VIDEO_THREAD_VP_SLOTS drops to
three and a reader takes three relaxed loads between the sequence
checks. VkViewport keeps its own float x and y, as does the overlay's.

samples/gfx/viewport_pack gains an origin lane: the round trip over
negative offsets, a sign bit that stays in its own half, one axis put
without moving the other, and the saturation. Dropping the sign
extension from either half fails it six times.
2026-09-22 14:34:22 +02:00
LibretroAdmin 72d39a2c00 gfx: the Win32 viewport reads reach the packed word
d3d9_make_d3dpp takes the back buffer's size out of d3d->vp.full_dims,
and the GDI window's paint handler takes its viewport rect out of
gdi->vp.dims. Both files reach a viewport through a member their own
header declares, so neither names video_viewport_t.
2026-09-22 14:09:28 +02:00
LibretroAdmin 89fe288c71 video: the viewport's two size pairs travel as one word each
video_viewport_t carries the drawn area and the window that holds it in
VIDEO_SCALE_PACK's layout, one word each, so a viewport reaches a driver
in two stores and every producer holding both axes writes them together.
VIDEO_SCALE_PUT_W/H set a single axis where a caller has only that one,
and a viewport passed straight through is a word move per pair - the GL
backends hand gl->vp.dims to their shader parameters as it stands.

The custom viewport the settings own keeps its axes as two members,
under video_viewport_settings_t: configuration.c's SETTING_UINT rows and
the menu's offsetof rows bind an unsigned by address, and half a word
has no address to give them. video_driver_init_internal() seeds it
through a viewport of the packed type and takes the two axes back out.
VkViewport and MTLViewport keep their own width and height.

samples/gfx/viewport_pack holds the layout: the round trip on both
words, one axis put without disturbing the other, and the clamp that
keeps an oversized axis out of its neighbour's half. Transposing the two
halves fails it 29 times.
2026-09-22 14:00:00 +02:00
LibretroAdmin 25c0612d23 (h265) Publish rows through an eventcount
Row publication notifies a retro_eventcount: a release store for the
row count and a notify that touches no lock unless a thread is parked.
The reference-row wait, the DPB output wait and the context join
register, re-check their own predicate and only then sleep. The
lossless sample links retro_eventcount.c for its threaded build and
keeps it out of the no-threads one.
2026-09-22 13:59:54 +02:00
LibretroAdmin 733d78bf27 (h264) Publish rows through an eventcount
Row publication notifies a retro_eventcount: a release store for the
row count and a notify that touches no lock unless a thread is parked.
The three waits on it - a reference picture's rows, the output pop and
the context join - register, re-check their own count and only then
sleep. The lossless sample links retro_eventcount.c for its threaded
build and keeps it out of the no-threads one.
2026-09-22 13:59:51 +02:00
LibretroAdmin 9f686efdfa libretro_vulkan.h: a v3 presentation queue is accepted, not necessarily used
The frontend may present on queue even when a core hands back another
queue of the graphics family: where the window system is known not to
tolerate a present on another queue, it does, still serialised with the
core's submissions through lock_queue/unlock_queue. Say so, so a core
does not depend on the separate queue being used. Comment only.
2026-09-22 13:19:08 +02:00
LibretroAdmin c265d78ee9 (Vulkan) Android presents on the graphics queue
Since 5d950a7288 the frontend presents on the graphics family's second
queue wherever the family has one. Behind Android's WSI the loader
turns a present's wait semaphore into a sync fd through the driver's
QueueSignalReleaseImageANDROID, and Mali (G615 MC6, r44p1, which
reports two queues in family 0) fails that call when the semaphore was
signalled on another queue: every present fails, the failed present
destroys the swapchain, the next acquire rebuilds it, and the menu
freezes in that loop (#19601). A CI build from before 3df4bd91b7 - the
swapchain-teardown hazard fixed in 4823918abe - already froze, so that
fix alone does not cover it.

The choice is made in one place now, vulkan_select_present_queue_index():
queue 1 where the family has a second queue, queue 0 behind Android's
WSI. Android's WSI is VULKAN_WSI_ANDROID, and any context driver in an
ANDROID build, the SDL3 one included. A core that provides its own
presentation queue in the graphics family gets the graphics queue
there as well; one in another family is still refused as before. The
desktop drivers keep the second queue and its lock-free presents.

The vulkan_validation sample checks the policy ahead of its display
check, so it runs on every lane: second queue elsewhere, the graphics
queue behind Android's WSI at one, two and four queues. The old
selection behind the same helper fails it on the Android cases.
2026-09-22 12:20:19 +02:00
LibretroAdmin 4823918abe (Vulkan) A swapchain rebuild waits out the presents it retires
A present is a queue operation that vkQueuePresentKHR returns ahead
of: the presentation engine still has to wait on the frame's swapchain
semaphore, and no frame fence covers that wait - the fences are
signalled by the render submission, which the present comes after.
Since 5d950a7288 the present goes to a queue of its own where the
family has one, with nothing on it that a frame fence orders, and
since 3df4bd91b7 a swapchain rebuild or teardown waits on the frame
fences instead of draining the device. Together, a resize or an
out-of-date acquire destroyed the swapchain semaphore, and the
swapchain, under a present still waiting on them.

Mali's Android WSI (r44p1, MT6897) does not survive that: every
present after it fails inside QueueSignalReleaseImageANDROID, the
failed present destroys the swapchain, the next acquire rebuilds it,
and the menu freezes in that loop (#19601: the 09-16 nightly clean,
09-18 failing every frame).

vulkan_context_wait_frames() now also waits out the presents when one
has been queued since it last ran: an empty submission on the present
queue, behind them, signals a fence the rebuild waits on with the
frame fences. One submission per rebuild, nothing per frame, and only
the present queue - a hardware core's work is still not drained. The
shared-queue case takes queue_lock for the submission as any use of
that queue must.

The vulkan_validation sample gets the case: acquire, a fenced
submission signalling the swapchain semaphore, the present, and a
teardown behind it. The validation layer retires a present's waits
when vkQueuePresentKHR returns, so it passed the old code; the sample
now tracks presents on the symbol wrapper's function pointers and
fails when a semaphore or swapchain a present is queued against is
destroyed before a fence behind that present has been waited on. Fails
on the old code (both objects), passes on this.
2026-09-22 12:03:06 +02:00
LibretroAdmin b0a070c746 compile-matrix: the Vulkan and X11 back-end lanes skip without headers
They go through platform_video with the header each one needs as its
probe, so a runner without the Vulkan or X11 development headers says
so and the matrix passes, the way every other lane whose headers may be
absent already does.
2026-09-22 03:27:43 +02:00
LibretroAdmin 6eadbc07a4 compile-matrix: lanes for the back ends a desktop job does not select
A Linux desktop configuration builds no Vulkan, no Cg and none of the
framebuffer or X11 back ends, so a change to the frame descriptor or the
shader parameter block those read is green here until a job that selects
them runs. Five lanes compile them, and the UI companion's own tests
join its core.
2026-09-22 03:08:48 +02:00
libretroadmin 7420784664 settings: the animated-thumbnail threads help says what the threads do now
The setting's help described the threads as converting frames to
pixels, which was all they did when it was written; they carry the
decode itself now, and the preview keeps to one thread while a core
is running. The help says so, in terms of what turning it up buys
and what it costs, and names no format.
2026-09-22 03:05:38 +02:00
libretroadmin 18a18ae91e rh264: catching up drops pictures with the pool busy too
With pictures decoding concurrently the decoder declined catch-up
drops, on the argument that a non-reference picture runs beside the
chain of references and dropping it buys no time. That holds when the
threads outnumber the pictures in flight and nowhere else: on a
machine whose pool is saturated by a 4K stream, the non-reference
pictures are half the work, and a preview that fell behind stayed
behind. The drop now happens after the slice header is parsed and
before any of the picture's state is taken on - the first slice of a
nal_ref_idc 0 picture and every slice after it - which leaves the
decoder exactly as it was, one picture at a time or four in flight,
as the HEVC decoder already did.

The catch-up oracle gains a build with four pictures in flight on a
pool, and CI runs both - it had built the oracle and never run it.
Every picture shown while catching up is the full decode's, at the
same count threaded as not, under ASan and TSan.
2026-09-22 03:02:18 +02:00
LibretroAdmin 584bc57007 gfx, companion: the Cg backend and the thumbs test read the packed sizes 2026-09-22 02:59:25 +02:00
LibretroAdmin 56b34f5a52 video: the frame descriptor's scale and custom viewport are one word each
video_frame_info_t carries the content scale and the custom viewport as
packed words. video_thread_get_scale() hands back the word the video
thread published rather than splitting it for a caller that stores it
whole, so the threaded path carries the scale across without touching
either half.

The custom viewport's full size went unread by anything, and goes.
2026-09-22 02:54:04 +02:00
LibretroAdmin 83f434af39 gfx: the shader parameter block's four size pairs travel as one word
video_shader_ctx_params_t carries the viewport, input, texture and
output sizes as four packed words, so the GL drivers fill four members
per pass and the GLSL backend unpacks them once into the locals its
uniforms are built from.
2026-09-22 02:39:18 +02:00
libretroadmin 128443f9fb rvp9: the tile jobs are waited on by their count, not by the pool falling idle
The last of the three fan-outs that waited for the whole pool to go
idle after handing it a frame's work: the blit's bands and the HEVC
wavefront went before, and the VP9 tile columns go now. The frame's
jobs share a count; the last to finish wakes the caller, who takes
queued work from the pool meanwhile rather than sit idle, and waits
for the frame's own jobs and nothing else. Harmless while VP9 was
alone on the pool, a stall per frame the moment anything shares it.
The tiled fixture hashes the same at every thread count, under ASan
and TSan.

Two built harness binaries had been committed - the ignore pattern
covered <dir>_test and not the _test_threaded and _test_norwebm
names beside it - so every make clean there showed as a deletion;
they are untracked, and the patterns cover names built on _test_ too,
sources excepted.
2026-09-22 02:30:09 +02:00
LibretroAdmin 765eea22af compile-matrix: the UI companion's shared core gets a lane
companion_core, companion_thumbs and companion_dock reach a compiler
only in a Qt or Cocoa build, so a desktop job configured --disable-qt
compiles none of them and a change to a struct they read stays green
until one of those runs. Two lanes compile all three, with and without
threads.
2026-09-22 02:22:33 +02:00
libretroadmin 348341a4e9 rh264_lossless_test, rh265_lossless_test: a build without threads runs the oracle too
The last patch found the HEVC decoder applying SAO to nothing in a
build without threads, the flag that asks for it having been set
inside a HAVE_THREADS block; the oracle could not have caught it,
being built with threads only. Each oracle's Makefile now builds the
same test a second time without HAVE_THREADS - no pool, no rthreads,
the concurrent legs skipped, every one-thread and rotated leg held
to ffmpeg as before - and the CI sample runner runs both. A decoder
path right only with threads, or wrong only without them, shows in
this lane rather than on a DOS or console build.
2026-09-22 02:18:40 +02:00
libretroadmin 24cff0686d rh265: the wavefront fan-out goes; pictures on the pool are the threading
Since pictures decode concurrently, a pool given to the decoder is
the pictures', and the wavefront fan-out - a picture's CTB rows dealt
to the pool, with shadow decode states, row progress counters and
the group on an eventcount - could no longer be reached: the two
threadings shared the pool and the one for pictures won, being ×N on
any content rather than on wavefront-coded content alone. What could
not be reached goes, 350 lines and the eventcount dependency with
it; wavefront-coded pictures decode their substreams in order as
they always did on one thread, and the fixture's hash is the same.

With it a bug that came in with the row hook: the flag that says a
picture asked for SAO was set inside the fan-out's HAVE_THREADS block,
so a build without threads applied SAO to nothing. It is set where
the slice data starts, in every build. The header describes the
decoder as it is - Main and Main 10, P and B, WPP, bypass, constrained
intra, pictures concurrent - and what it still refuses.
2026-09-22 02:14:30 +02:00
LibretroAdmin eaf3d788bf companion_thumbs: the preview's size comes from the packed word 2026-09-22 02:13:34 +02:00
LibretroAdmin c741a3331e gfx: the preview and line-ticker size pairs travel as one word
gfx_anim_preview_t carries the animation's size as one word, packed
from the two the stream fills, and gfx_animation_ctx_line_ticker_smooth_t
its field size. gfx_anim_preview.h takes video_defines.h along, so a
consumer of the header has the accessors.

The plain smooth ticker keeps its field_width: it has no height beside
it, so there is no pair there to fold.
2026-09-22 02:10:36 +02:00
libretroadmin d17b578687 rh264: interlaced coding in the oracle and the preview fixtures
The oracle had no interlaced case: pair scanning, field motion
compensation, the per-pair deblocking and, with pictures in flight,
the rows of a MBAFF picture published only at its completion, were
held to nothing. Three x264 MBAFF cases join it - CAVLC, CABAC with a
B-pyramid, weighted prediction - decoded in all four modes against
ffmpeg, and an MBAFF fixture joins the preview oracles, which run it
plain and concurrently under both sanitizers. All byte-exact as the
decoder stands; a comment claiming B field pictures refused, stale
since the field machinery landed, goes.
2026-09-22 02:08:18 +02:00
libretroadmin 854f7de0be rh265: transquant bypass and constrained intra prediction
Two Main-profile features the decoder refused, so that a stream using
either got no picture at all. Transquant bypass: the CU's flag is
parsed, its residual is its coded levels with no scaling and no
transform, its transform_skip_flag is not read and its signs are not
hidden, and the loop filters leave its samples as they are - the
deblocking filter runs the edge and puts the bypass side back, SAO
passes the samples over. Constrained intra prediction: a reference
sample from a CU that is not intra-coded is unavailable to intra
prediction, the motion field's pred saying which. Byte-exact against
ffmpeg in the four decode modes: lossless 8-bit, 10-bit, with the
wavefront and with B-frames, bypass CUs mixed with coded ones under
deblocking and SAO, and constrained intra in P and B pictures; the
streams that never used either decode as before, to the hash.
2026-09-22 01:49:23 +02:00
libretroadmin d2cbd6114c rh265: an oracle against ffmpeg, and the row hook filters a row behind
HEVC decoding is specified to the sample, so for any conforming
stream the decoder's output is ffmpeg's to the byte, lossless or not;
the decoder had only ever been held to its own output across modes.
rh265_lossless_test builds its cases with x265 - deblocking and SAO,
B-frames and a pyramid, the wavefront and none, 10-bit, a tall
picture with a partial last CTB row, 16-sample CTBs - and decodes
each four ways, one thread, four contexts in rotation, four pictures
concurrently, and concurrently with every row's publication held
back, every way against ffmpeg's frames. Transquant bypass the
decoder refuses, out of its profile; the oracle holds it to
refusing whole rather than decoding wrong.

It found the row hook filtering a CTB row the moment it was
reconstructed, when the next row's intra prediction reads this row's
last line as reconstructed and unfiltered. The hook filters the row
above now, once nothing will predict from it, and SAO runs two rows
behind; a row is final below what SAO has been over. Both preview
fixtures were wavefront-coded, where the hook never runs, which is
how the hashes stayed equal: a non-wavefront fixture joins them, and
the preview oracles run it plain and with rows held back. The CI
sample runner takes the new oracle.
2026-09-22 01:41:19 +02:00
LibretroAdmin 5cc7019d05 video: the threaded wrapper's size pairs travel as one word
The viewport and video-mode commands, the menu texture and the frame
slot each carry their size as one packed word, so a frame's size
reaches the video thread in one store and is read back in one load.

The packet union's unused 'output' member goes with them.
2026-09-22 01:34:55 +02:00
libretroadmin 494039408e rh264_lossless_test: the reference is the stream's pictures, no more
The field file that found the multi-slice fault kept a residue of
1,166,172 differing samples at one thread, all in the last picture.
It was the reference: ffmpeg duplicates pictures to hold a file's
declared rate against its timestamps, so a file of 127 packets gave
129 frames, and the decoder's last picture - correct to the byte -
was compared against a copy of the one before it. Every reference
the oracle makes now takes -fps_mode passthrough, and the file mode
makes its own reference that way from the file alone, RH264_REF
naming one made already; its exit status is the comparison. The
field file decodes byte-exact at one thread, four contexts and four
threads.
2026-09-22 01:26:37 +02:00
libretroadmin 4b5a76b875 rh265: pictures decode concurrently on the pool
The fourth step, and the arc's point: with a pool and more than one
thread, HEVC pictures decode concurrently. A picture's slices are
queued on its context as they arrive - each job takes the unescape
buffer it was parsed from, the decoder draws another from a pool, and
the slice header and lists go with it - and go to the pool when the
next picture opens, at which moment the picture's place in the
sequence is settled, out of the decoding slot and into the output
order, with its rows still counting up. A reference read sleeps for
the rows it reaches and no more; a picture holds the slots it reads
so the DPB hands none of them to a new picture; the output holds a
picture until it is complete and waits on its last row only when the
queue is full; the drain and the close land everything first. The
wavefront stays off while pictures are on the pool - the pool is the
pictures'. The delay knob holds every row's publication back at
random, so that the readers wait rather than nearly always find their
rows, and the output must not change under it.

Three things the sanitizers found on the way, all outside the
scheduler: the pool entry and the close changed state under pictures
in flight, and join first now; the catch-up flag crossed from the
caller's thread to the decode's as a plain int in both decoders, and
is atomic in both; and the preview's resident-bytes diagnostic read
the window's bookkeeping while the feeder moved it, and reads a value
the feeder publishes instead.

Byte-exact against one thread on both HEVC fixtures at two and four
threads, plain and with rows held back, under ASan and TSan; the
threaded preview harness at four threads, both codecs, under both.
The check target and CI run the concurrent and the held-back lanes.
2026-09-22 01:18:59 +02:00
LibretroAdmin dd877f674e gfx: the display, widget and thumbnail size pairs travel as one word
struct gfx_display carries its framebuffer size, the gathered batch's
first rectangle and the batch's video size as three packed words;
dispgfx_widget_t its last video size and the AI service overlay's; and
gfx_thumbnail_t its own. The batch compare that decides whether a quad
can join asks one word per pair, and a thumbnail takes its size from
the surface as a single copy.

The AI overlay's loader keeps two locals for the decoder's out-params
and packs the answer, since the decoder fills them separately.
2026-09-22 01:18:45 +02:00
libretroadmin 123026d622 rh265: pictures take decode contexts in rotation, and the WPP oracle decodes at four
The decode state - the parameter sets and slice header the picture
decodes with, its reference lists, its maps of intra modes, coded
depths, edge strengths, QPs and SAO parameters, its scratch, its
SAO bands - was one struct in the decoder; it is one per context now,
eight at most, and a new picture takes the next context round, which
readies its own maps for the decoder's geometry while the DPB is
remade only when the geometry changes. With one context that is the
decoder as it was. The pictures still decode one after the other, so
rotating changes which memory a picture uses and nothing else: the
WPP fixture and the B-frame fixture hash the same at four contexts as
at one, and the WPP oracle now decodes at four - a difference would
be a piece of picture state still in the decoder rather than the
context, shared between pictures the moment they decode concurrently.
This is the third of H.264's four steps; the scheduler is what
remains.
2026-09-22 00:56:25 +02:00
LibretroAdmin 8bd1e965a9 video: the size pack takes each axis as unsigned
VIDEO_SCALE_PACK converts an axis before it compares it against the
limit, so a caller holding its sizes in int or float packs without a
sign-compare warning and without a cast of its own. The clamp is
unchanged: at the limit, past it, negative and fractional inputs all
land where they did.
2026-09-22 00:45:56 +02:00
libretroadmin 5966b671d9 rh265: a picture counts the CTB rows that are final, and every reference read asks
Each picture in the DPB counts the CTB rows of it that are final -
reconstructed, deblocked, SAO'd, written by nothing again - counted
up by the row hook as the filters pass and set to the whole picture
at the finish. What is final is what SAO has been over, since the
row above's SAO reads this row's first line and the edge at the top
of the row below still touches this row's last; without SAO, what is
deblocked below the row still being filtered against. Every read of
a reference asks for the row it reaches: the motion compensation for
the block's last line displaced by its vector and the eight-tap
filter's four lines past it, temporal prediction for the co-located
row. On one thread a reference is complete before it is read and the
ask is answered at once; a picture on another thread will wait
there, and nowhere else. The WPP oracle asserts that no read on one
thread or on the wavefront finds its rows short, the bench reports
the count beside the hash, and both fixtures hash as before.
2026-09-22 00:45:36 +02:00
libretroadmin f2beaaca54 rh265: the loop filters run a CTB row behind the decode
The deblocking filter ran over the whole picture at the finish, and
SAO after it. Both go a CTB row at a time now: when a row is
reconstructed its edges are filtered - vertical, then horizontal from
its top edge down, which reads only lines the band above has already
filtered vertically and reaches no line the band below has yet to -
and SAO runs over the row above, whose last line reads this row's
first as deblocked. Done band by band from the top that is the
picture's filter to the sample; the WPP fixture and the B-frame
fixture hash the same as before at every thread count. The finish
does what the hook left: the last row's SAO, or the whole picture
when the hook could not run - the wavefront, or more than one slice.
This is the first of the four steps H.264 took towards pictures
decoding concurrently: rows that become final one at a time, for a
later picture to wait on.
2026-09-22 00:36:29 +02:00
LibretroAdmin 89b917eb82 osmesa_ctx: the driver declares the types it uses
The headless OpenGL context includes the headers that declare
gfx_ctx_driver_t, gfx_ctx_proc_t and input_driver_t, so a build that
configure enables from its pkg-config probe compiles. bind() takes its
address as a struct sockaddr, and the unused second sockaddr goes.

compile-matrix gains eleven context-driver lanes, osmesa among them:
these answer the frontend's window and size questions and no other job
here compiles them, so each lane names the headers it needs and says so
where the runner has none. vivante_fbdev stays out, since only that
vendor's EGL headers declare the functions it calls.
2026-09-22 00:29:53 +02:00
libretroadmin 6b83f9407a rh264: a sample's slices are all fed when pictures are in flight
A file whose every picture is coded as eight slices rendered at one
thread and not at two or more. rh264_video_decode fed a sample's NALs
until a picture was shown and stopped - one coded picture per call,
which sequentially is exact: the picture shown is this sample's,
complete at its last slice. With pictures in flight the picture shown
at a slice's open is an earlier one, and stopping there left the
sample's remaining slices unfed: rows of the first slice decoded and
the rest never, the picture never complete, every picture predicting
from it waiting for rows that could not come. The call now ends with
the sample. Two more that the same file found: a continuation slice
was refused because its start was checked against a picture end the
job had not yet reached - the slices must arrive in raster order at
submit, and the job checks each against the last one's end when it
runs them; and a picture that refused published no rows on the
inline path, which was the hang. Every picture is published whole
once its run is over, however it ended.

The oracle gains an eight-slice case with B-frames, decoded in all
four modes, and a mode that decodes one file against its ffmpeg
reference at one thread and at N - the file from the field, and the
next one. The preview sample's threaded target was listed in all:
before its variable was set, so make read an empty prerequisite and
CI found no binary; the definitions come first now.
2026-09-22 00:29:32 +02:00
LibretroAdmin 58a1a327c8 samples/audio: sweep every DSP filter with adversarial presets
The lane for the two preceding fixes. It asks each filter which
parameters it reads, then re-inits it once per parameter per poison
value - zero, negative, huge, infinity, NaN, and the int edges - across
five sample rates including zero, and runs audio through both the float
and the int16 path. Discovery is by callback, so a parameter a filter
gains later is covered without touching the harness.

Each filter is swept in its own process, so one that dies still leaves
the other fourteen reported, and an alarm turns a loop the preset sizes
into a named failure rather than a hang. Built with
-fno-sanitize-recover, a sanitizer report is a trap, which the parent
reports the same way.

Run it as: make SANITIZER=address,undefined,float-cast-overflow check

Not wired into .github/workflows/Linux-samples-audio.yml, which names
its directories one at a time.
2026-09-22 00:28:16 +02:00
LibretroAdmin b3d5060d40 dsp_filters: bound the rest of the preset parameters
The same class as the vibrato and tremolo fix, across the remaining
filters: a preset value reaches a Q16/Q24/Q30 conversion, a divisor, an
allocation size or a loop bound without one.

Clamp each parameter at init to the range its buffers and fixed-point
format hold, with the NaN-safe ordering. The bounds that are not simply
the control's own range: chorus takes the modulated delay against the
ring, since the int16 LUT carries it as Q16 samples; chorus lfo_freq
sets the LUT length and the loop that fills it; eq block_size_log2 is a
shift count, an FFT order, an arena size and a block divisor at once;
iir quality divides the biquad alpha, as wahwah resonance does.

Refuse a zero input rate, which is what a filter is handed when the
audio device never opened, and floor the reverb delay lines at one
frame - at a low enough rate every comb length rounds to zero and
comb_process reads past a one-byte arena.

Saturate the Q16 state in echo and reverb. Both let a preset set a loop
gain of exactly 1, and echo's amp sits inside the feedback path, so the
state grows until the int64 product overflows. The float paths go loud
instead; the ceiling is 2^20 times full scale, so a setting that decays
never reaches it and its output is unchanged.
2026-09-22 00:28:13 +02:00
LibretroAdmin 4d7d2d3141 dsp_filters: bound the preset parameters that kill the filter
A .dsp preset is a text file the user picks, and rstrtof parses "inf"
and "nan", so every parameter reaching these filters can be zero,
negative, enormous or non-finite.

Vibrato divides by an LFO period it re-derives per sample from freq
without the clamp vibratocore_carve applies, so a freq above the sample
rate truncates it to zero and the modulo raises SIGFPE; take the clamped
core->maxphase instead, which also drops a division from the per-sample
path. Tremolo takes its wavetable index modulo a length derived the same
way, with the same result on both the float and the int16 path.

Overdrive, early reflection reverb and WSOLA clamp their parameters, but
the clamp returns NaN unchanged because NaN compares false against both
bounds. In overdrive that reaches the Q20 bias, and the tanh LUT is then
indexed out of its 4097 entries - a segfault. Order the comparison so a
NaN lands on the low bound.

Clamp vibrato and tremolo's freq and depth to the ranges their buffers
are sized for, floor the vibrato delay line at the four samples the
Hermite tap reads, and refuse a zero input rate, which is what the
filter is handed when the audio device never opened.
2026-09-22 00:28:09 +02:00
LibretroAdmin fed510bc42 xmb: a shrunken icon centres on its own height
The VITA, Wii U and PS3 centring path offsets y by the icon's height,
so a non-square icon drawn with a scale factor below 1 sits centred on
both axes.
2026-09-22 00:16:39 +02:00
LibretroAdmin 69d23129c2 xmb: the console scale goes into the packed size
The VITA, Wii U and PS3 path takes its scale in the icon size, so the
size is packed already scaled rather than multiplied afterwards.

compile-matrix gains five lanes that compile the shared menu and video
files under the console platform macros, against the stubs already in
the tree. The blocks those macros select are not compiled by any
desktop job, so a change to a struct they touch stays green until a
console job runs; these fail by name instead.
2026-09-22 00:08:01 +02:00
LibretroAdmin 0124aab168 audio_mixer: bound the streaming voice's resampled buffer
A streaming voice sized its resampler output buffer as
AUDIO_MIXER_TEMP_BUFFER * ratio plus a fixed 16 samples, then stored
whatever process() reported into it. The pad does not scale, but the
overshoot does: a call ends on a phase it did not start on, so roughly
one input frame's worth of output lands on top of the estimate, and
that is ratio samples, not 16.

Measured with the mixer's own constants and the sinc driver: at ratio 6
(8 kHz source into a 48 kHz mixer) the buffer holds 49168 samples and
process() reports 49166 - two to spare. At ratio 12 it holds 98320 and
process() reports 98330, and at ratio 24 it is short by 34. Both are
ordinary sources; the buffer is on the heap, so the excess lands in
whatever follows it.

buf_samples was written at both allocation sites and never read - the
bound was recorded and not enforced.

Scale the pad with the ratio, as one_shot_resample() already does for
the non-streaming path, and clamp the reported count to buf_samples at
both fill sites, so a backend that reports more than the estimate
truncates rather than overruns.

The audio_mixer_rate_guard harness gains a streaming lane driving
64 mix calls per voice at 44.1k, 8k, 4k, 3k and 2k sources into 48 kHz
and 96 kHz mixers, both sample formats. Reverting this change makes it
report a heap-buffer-overflow in resampler_sinc_process_sse_kaiser
under ASan.
2026-09-21 23:56:26 +02:00
LibretroAdmin 1291fee739 exynos: the driver's size pairs travel as one word
exynos_data carries the framebuffer size as dims, exynos_video the
core's current mode, and the image defaults table its own sizes, all in
VIDEO_SCALE_PACK's layout. The frame path compares one word to decide
whether the core changed resolution, and the scaling maths unpacks once
into locals.

compile-matrix gains an exynos lane against libdrm's exynos module and
its G2D headers, which nothing else in the tree compiles; it skips
where those headers are absent. platform_video's failure output now
goes through show_out, so a lane that fails leads with its error rather
than whatever warning came first.
2026-09-21 23:36:09 +02:00
libretroadmin b31b6c180b rh265: SAO reads from a band of one CTB row, not from a copy of the picture
The sample adaptive offset reads the deblocked samples around each one
and writes in place, and it did that from a copy of the whole picture
- three planes malloc'd, twelve megabytes at 4K, copied and freed for
every picture with SAO on. A band of one CTB row and a line above and
below, kept with the decoder, serves the same reads: the line above
comes from the previous band, where it is still as deblocked, the rest
from the picture ahead of the pass. The output is the same to the
hash on the WPP fixture, at every thread count. The preview bench
takes BENCH_HASH, a hash over every frame's pixels, which is how two
builds of a decoder are held to the same output on a file.
2026-09-21 23:27:47 +02:00
libretroadmin 9f3a581199 rh265: the wavefront's rows wait on their own count, not on the pool falling idle
The WPP rows went to the pool and the caller waited for the pool to
fall idle - a wait for everything on it, which is fine while a pool
carries one picture's rows and nothing else, and a stall on every
picture once pictures decode concurrently on the same pool, as the
H.264 decoder's do and this one's are to. The rows belong to a group
on the heap now: the runners claim them, the caller too, and the
caller waits on a process-wide eventcount for the rows in other hands
and then for the runners to let go - a runner still queued behind
other work finds its rows gone and leaves at once, but until it has,
the shadows and counters it can reach are not the next picture's to
reset; that was the race the first shape of this had. Byte-exact on
the WPP oracle across the thread counts, under ASan and TSan.
2026-09-21 23:21:47 +02:00
LibretroAdmin 744d9daad4 samples/gfx: the async lane asks the worker, not the clock
The lane's non-blocking check timed six posts against a fixed 5 ms
budget, which on a two-core WARP runner under gdb measures how often
the poster is descheduled rather than whether the post waits on the
video thread: it reports ~19 us here and failed CI at 5649 us.

The first load now parks the worker inside the upload, where it runs
with no lock held and with nothing on the main thread waiting on it.
The remaining five are posted into that, and the lane asserts they
came back while the worker was still in there - the worker's own
state answers, so the check holds however loaded the machine is. A
post made to wait on the video thread fails it with the hold's own
bound.
2026-09-21 23:21:21 +02:00
LibretroAdmin 0c7e6bbf9d gfx: a draw descriptor carries its size as one word
gfx_display_ctx_draw holds dims in VIDEO_SCALE_PACK's layout, so the
size reaches every display backend in one member. The producers pack
where they already had both axes in hand, and each backend unpacks
where it builds its viewport or its quad.

samples/gfx/gfx_widgets_sanitize gains an icon-draw lane: it draws at
64x16 and checks both halves of the word the widget layer hands the
driver, so a transposition on the way in fails there. The Direct3D
lanes in the compile matrix cover the six backends no other lane
compiles.
2026-09-21 23:12:12 +02:00
libretroadmin 29e24ae6a2 gfx_thumbnail: the preview's threads defer to a core that is running
The plan for threaded previews had the worker count defer to a
running core, and the setting was taken as it stood: a 4K preview at
four threads under a menu open over running content took the cores
the game was on. The count is the setting unless content is loaded
and not paused, in which case one - the preview decodes as it did
before the setting, and the pool is remade as the count changes,
which it already was.
2026-09-21 23:06:24 +02:00
LibretroAdmin 98d5a9ef57 (Resampler) sinc and nearest hold their ratio contract per call, not just at init
Both drivers validate the nominal ratio when the instance is made and
then take a different number on every call: audio_driver_effective_ratio()
multiplies by slow motion and fast-forward, and audio_driver_bound_ratio()
returns the ratio untouched wherever the caller reports no capacity of
its own. Nothing checked that one.

sinc derives its phase step as phases / data->ratio and nearest as
1.0 / data->ratio, so a ratio of zero, a negative one, a NaN or one
larger than a pair of sample rates can name leaves a step that never
advances the loop, and the inner loop emits past data_out until it
leaves the buffer. The integer drivers reach an int64 cast on the same
value, where the conversion is undefined.

Each dispatcher now holds the range its own arithmetic needs before it
reaches any of that. sinc uses sinc_resampler_ratio_valid(), the
predicate its init already refuses on - it classifies by bit pattern,
so it survives the float driver's fast-math mode - and nearest bounds
the step the same way the CC driver does.

resampler_reset drives all three backends through the ratios a clamp
would have caught, instead of CC alone with a note that the other two
were theirs to answer for. Reverting either fix turns that lane into an
ASan heap-buffer-overflow, in resampler_sinc_process_c_kaiser and in
resampler_nearest_process.
2026-09-21 22:57:57 +02:00
libretroadmin 350f7f7142 rh264: the slice jobs are kept between slices
Each slice's job was made with calloc and freed after - two kilobytes,
a B context inside, zeroed and returned per slice on the submitting
thread. A finished job goes to a pool on the decoder beside the
unescape buffers, and the next slice takes it from there, cleared;
its buffer is settled - pooled or freed - before the job is offered,
since a pooled job is another thread's to take. With this the per-
picture path on the submitting thread makes no allocation in steady
state: blocks, buffers and jobs all cycle.
2026-09-21 22:55:26 +02:00
libretroadmin e51169e0fd gfx_thumbnail_preview: the preview oracle runs threaded, on the app's own path
The preview harness built the thumbnail without HAVE_THREADS: the
decode ran inline on the poll, no worker, no pool, and the paced
stall check could say nothing about the app, where the picture
decodes on the worker and, above one thread, on the pool. The same
test now builds a second way, gfx_thumbnail_preview_test_threaded,
with rthreads, the pool and the eventcount real and the stubs' thread
shims compiled out; THREADS sets the count as the menu setting would.
The check target and the CI job run it at four threads, paced, with
a bound on the longest stall, under ASan and under TSan - the first
time the worker, the pool and the poll have been under a sanitizer
together. Clean.
2026-09-21 22:51:00 +02:00
libretroadmin 560dab6972 Animated previews: the feed's sums saturate rather than wrap
Three sums of file-derived sizes were unchecked: twice what the decoder
took since the last feed, the decoder's position plus the lookahead,
and the same for the audio window. On a normal file none can wrap; on
one that can, the first would budget a decoder that took the most next
to nothing and the others would clamp a wrapped bound against the
length and repair nothing. Each saturates.
2026-09-21 22:40:44 +02:00
LibretroAdmin 6bdbe17439 compile-matrix: the Direct3D drivers get a lane
Seven lanes compile d3d8, both d3d9 backends, d3d10, d3d11, d3d12 and
gdi against the DirectX headers the tree vendors in gfx/include/dxsdk,
through a mingw-w64 cross compiler. These drivers read the same frame
and draw descriptors every other video driver does, and a descriptor
field they are not carried along with fails all seven by name.

The cross compiler is the only thing needed beyond the tree; where
there is none the lanes say so and the matrix passes as before. A
runner installing gcc-mingw-w64-x86-64 turns them on.
2026-09-21 22:28:43 +02:00
libretroadmin fd368664ae rh264: a slice is unescaped once, into a buffer that is neither made nor copied per slice
The queued job copied the unescaped slice out of the context's scratch
into a buffer made for it - a megabyte moved and an allocation freed,
per slice, on the submitting thread, for bytes that already sat in
memory nobody else needed. The job takes the buffer the slice was
unescaped into - whichever context's was current then, the one before
the rotation when the slice opened a picture - and that context draws
its next from a pool of buffers on the decoder; a finished job returns
its buffer there. Workers return and the submitter draws, under the
blocks lock when there is a pool. Byte-exact in every mode, clean
under the sanitizers.
2026-09-21 22:27:30 +02:00
LibretroAdmin d1d064e24c gfx: a surface carries its size as one word
gfx_surface_t holds dims in VIDEO_SCALE_PACK's layout. The thumbnail
animation path compares one word to decide whether a surface still fits
the frames it is being handed, and the overlay and swizzle paths unpack
where they compute a frame length.
2026-09-21 22:18:59 +02:00
LibretroAdmin 1d19ba92c5 video: the softfilter output size is one out-param
rarch_softfilter_get_output_size() and get_max_output_size() hand the
size back as one word in VIDEO_SCALE_PACK's layout. The plugin ABI is
untouched: a filter still answers through two pointers, seeded from the
word so one with no size query of its own leaves it as the caller had
it.

video_driver_init_filter() carries the packed size from the geometry
through to the buffer allocation, and the frame path, the threaded
wrapper and the recording setup each hold one local for the pair.
samples/gfx/filter_lifecycle checks the output dimensions it gets back,
so a halved unpack fails there.
2026-09-21 22:18:55 +02:00
libretroadmin 9705e03a59 rh264: output slots hold no block of their own, and let a shown block go at once
Every output slot was allocated a sample block of its own - twelve
megabytes at 4K, zeroed - to have a frame picture's block shared over
it on the spot, and once a picture had been shown the slot kept the
block until it came round again, which with eighteen slots is a dozen
pictures later. So the free list stayed empty and every picture paid
a fresh block: a mapping from the kernel and its zeroing on the
submitting thread, per picture, and the memory of a dozen pictures
pinned in slots nobody read. A slot for a frame picture is now made
as a shell, bookkeeping only, and a slot that has been shown and is
not on show lets its block go the moment the next picture is pushed.
Field pairs, which copy, keep a block. The free list holds sixteen.
2026-09-21 22:13:01 +02:00
LibretroAdmin 0d7e6acb7f (CC resampler) Full precision builds, and builds alone
CC_RESAMPLER_PRECISION above 4 selects a trig kernel only the reference
implements. Two things followed from that and neither was intended.

It did not compile: cc_int() uses M_PI and sinf() at that setting while
the file included neither <math.h> nor retro_math.h, so the mode the
header calls full precision has been unbuildable under -std=c89. Both
includes are there now, and 0, 1 and 5 compile clean on x86 and armhf.

And since the arms became a runtime choice, building it would have left
one binary resampling differently depending on which arm the mask
picked - the reference on its trig kernel, the vector arms on the
polynomial they are the only implementation of. Above 4 the vector arms
are no longer built, so the setting means the same thing whatever the
mask says.

resampler_simd reads the same condition, so at that setting it reports
that there is no vector arm rather than comparing the reference against
itself.

What the setting buys, measured at 32 to 48 kHz: about 21 dB of THD+N
at 100 Hz, 8 dB at 500 Hz, 3 dB at 1 kHz, and nothing at all above
2 kHz, where the width of the kernel is the limit rather than the way
it is evaluated.
2026-09-21 22:12:40 +02:00
libretroadmin d4a7c4ba5a Animated previews: the feed's budget follows the decoder's appetite
The feed runs once per frame, on the decoding thread, and read half a
megabyte a tick. A decoder with pictures in flight takes its samples
in bursts of three and four a frame, more than that refills, so the
lookahead ran down each few seconds - and then the feed's answer was
to extend unpaced: two seconds of 4K read from disk in one go, on the
thread the frame was waiting on. A pause every few seconds, in a
preview that decodes faster than it plays.

The budget is now at least twice what the decoder took since the
last feed, so a decoder that takes four samples a frame is kept ahead
of without the lookahead ever running down; and a lookahead that has
run down is made up a quarter of the shortfall a tick rather than at
once. Only a decoder already past the frontier gets the unpaced
extend, since its next read would fault.
2026-09-21 21:58:16 +02:00
LibretroAdmin ffb120c83b samples/audio/extra_capacity: stub the config_userdata callbacks
4d5f87f added audio_resampler.c to this sample's link, and that unit's
resampler_config table points at config_userdata_get_float() and the
rest of the config_userdata family. Without a sanitizer --gc-sections
drops the table and nothing notices; under
SANITIZER=address,undefined it survives and the link fails with
undefined references, which is how the Linux audio samples job broke.

Stub them the way resampler_options_test does, aborting since nothing
here passes backend config userdata, and mark them externally_visible
so -fwhole-program on extra_capacity_test.o does not internalise
them.
2026-09-21 21:57:53 +02:00
LibretroAdmin ae58b972cb (Audio) The flush's zero-copy path does not ask for an aligned buffer
audio_driver_flush() points the resampler at the core's own buffer when
nothing needs to be produced into input_data, and fell back to the copy
unless that pointer was 16-byte aligned. The requirement was the CC
resampler's ARM assembly, which loaded its input with an explicit
alignment hint; that file is gone and nothing has taken its place.

Every consumer of src_data.data_in reads it without one: sinc pushes it
into its own ring a sample at a time, nearest copies frame structs, CC
takes it with _mm_loadl_pi and vld1_f32, and the bypass clamp uses
_mm_loadu_ps and vld1q_f32. So the check only decided whether a batch
was copied, and a core-owned buffer under an allocator that aligns to 8
paid for it every flush.

Worth about 1% of the resample where it fires - 512 stereo float frames
at 48/32, best of 9, 0.0862 s against 0.0871 s through CC and 0.1266 s
against 0.1283 s through sinc. Small here, and this is the host with the
fastest memcpy and the batch already in L1.
2026-09-21 21:57:53 +02:00
LibretroAdmin 15ac073eef extra_capacity: supply the int16 factory rather than linking the layer
The harness builds its test object with -fwhole-program and overrides
retro_resampler_realloc_hq(), so it has always supplied the resampler
layer rather than linking it. When retro_resampler_int16_new() arrived
the layer was added to its sources instead, which links
audio_resampler.c's driver table and with it the config_userdata_*
callbacks the harness does not define. The plain build survived because
-fwhole-program lets the linker drop the table; the sanitizer build did
not, so the suite could not be sanitized at all.

It supplies the factory too now, against the sinc int16 driver its lanes
already drive, and links neither resampler-layer unit. Both build modes
pass, ASan and UBSan among them.
2026-09-21 21:57:53 +02:00
LibretroAdmin 5149b27677 (CC resampler) A nominal ratio it cannot serve is refused at init
sinc_resampler_int16.h promises NULL when the nominal ratio cannot
safely advance the phase clock, and both sinc drivers enforce it before
they configure anything. CC took any ratio and only refused per call,
through the output bound, so a core reporting no rate gave a resampler
that created cleanly and then produced nothing, where sinc gives the
frontend a failed init and a log line.

Both CC drivers now hold the same range at init that process() holds:
positive, and no larger than a pair of sample rates can name. The
predicate is the one the output bound already used, named once.

resampler_options pins it for the float driver and stretch for the
integer one; without the check each reports three failures.
2026-09-21 21:57:53 +02:00
Gorilli09 bffc23a5aa Add explore-by-release-month option (#19602)
Introduce a new "release month" explore category. Adds EXPLORE_BY_RELEASEMONTH and its explore_by_info entry in menu/menu_explore.c, and the corresponding MENU_ENUM_LABEL_VALUE_EXPLORE_BY_RELEASE_MONTH in msg_hash.h to enable browsing by release month.
2026-09-21 21:49:37 +02:00
LibretroAdmin 5bb94c70a5 omap: both of the driver's size pairs travel as one word
omap_video_t carries the current mode as dims and omapfb_data_t the
native screen size as nat_dims, in VIDEO_SCALE_PACK's layout. The
frame path compares one word to decide whether the core changed
resolution, and the font, viewport and menu-texture paths unpack once
into locals.
2026-09-21 21:44:47 +02:00
LibretroAdmin 9c504d6852 video: the statistics overlay reads the cached size from one variable
The overlay's six references to the cached frame's dimensions unpack
from the word frame_cache_peek() hands back, so the block carries one
local for the pair.
2026-09-21 21:44:43 +02:00
libretroadmin cc1df72c75 rh264: the submitter helps the pool, eight contexts, and catching up leaves the pool its pictures
Two threads gave a picture in flight and nothing else: the pool had
one worker and the submitter sat idle at the join. The pool gains
tpool_help(), which runs the job at the head of the queue on the
calling thread, and the join takes queued pictures that way before it
sleeps - the one it waits for, or an earlier one the workers have not
reached; the head is taken as a worker takes it, so a picture never
waits on one still queued. The contexts number the threads plus one,
up to eight, so the pictures in flight are the threads that can
decode them and one being built.

Catching up dropped the pictures nothing references. Decoding
concurrently those cost nothing - they run beside the chain of
references, the critical path either way - so dropping them bought no
time and emptied the pool: a preview that fell behind stayed behind,
on the pictures it had left, in fits. Threaded, the drops are
declined; on one thread they are what they were - except that a
dropped picture now says so, which lets the container pass its slot
on the timeline: the flag was never set here, so every H.264 preview
that caught up read its frame durations one slot behind.
2026-09-21 21:40:27 +02:00
libretroadmin f15022a18f audio_transfer: the AAC loop withdraws the window bound on the rewind it actually takes
The mixer's repeat rewinds an AAC stream through audio_transfer_seek's
own frame-zero path, not through the seek-to routine, and only the
latter withdrew the window bound on a rewind. So a looping preview
still read the head at once - the faulting instruction is the first
byte of the head packet, inside its declared size, on a page the
window gave up a lap ago. Both rewinds withdraw the bound now, through
one helper, and the reads stand at the wall until the feeder brings
the head back and publishes a bound for the new lap.
2026-09-21 21:25:26 +02:00
libretroadmin e16a4855d5 Animated previews: every windowed audio stream decodes under a bound
The bound the feeder publishes is what lets the decoder stand at the
wall on a loop instead of reading the head out of pages the window
gave up - and it was published at open only for the island layout.
For every other file the decoder trusted the demuxer's offsets
outright the whole lap, and at the lap's end looped and read the
head before the feeder's next tick brought it back: the access
violation in the AAC decoder, in the faulting instruction's own
terms, a byte inside the packet's declared size on a page that is
not there. The bound is published for every windowed stream from
its first read.
2026-09-21 21:19:54 +02:00
LibretroAdmin aab19d1a06 video: the cached frame's size pair stays packed end to end
frame_cache_snapshot(), peek() and store() hand the pair across as the
one word it is stored as, so the seqlock body is a straight assignment
and the four consumers that need separate axes unpack once, at the
point of use. The two that only forward the pair and the one that reads
only the pointer keep it packed.

video_driver_cached_frame_publish() compares one word where it compared
two, and the lock-based branch holds one field where it held two.
samples/gfx/threaded_video's size-pair round-trip lane covers this
path: a halved unpack anywhere along it fails the lane by name.
2026-09-21 21:19:01 +02:00
libretroadmin 860de3d0b3 Animated previews: the looping audio stands at the wall until the head is back
The preview's audio decodes on the audio thread out of a sliding
window the menu thread feeds, and the feeder lets the head go as the
lap goes on. When the audio reaches the end it loops, on its own
thread, and reads the head at once - pages the feeder gave back long
ago, and can only bring back on its next tick. That read is the
access violation in the AAC decoder when a preview reaches its end.

On a loop the decoder now stands at the wall: the bound the feeder
published for the old lap is withdrawn, so every read waits, and the
feeder, seeing the position fall back, forgets the bound it published
too, brings the head back and publishes one for the new lap. The
decoder resumes on it. A whole-file buffer has no window and no bound,
and loops as it did.
2026-09-21 21:08:33 +02:00
LibretroAdmin dd3f51b56b CI: companion_thumbs_test links retro_eventcount.c
dc98c1a moved image_blit_bands onto retro_eventcount, but
tools/companion_thumbs_test.sh links image_blit_bands.c without
retro_eventcount.c, so the link fails with undefined references to
retro_eventcount_init / notify / prepare_wait / commit_wait /
cancel_wait. Add it to the script's source list.
2026-09-21 21:05:43 +02:00
LibretroAdmin 617a1c13df (CC resampler) The kernels are chosen by the mask, and NEON is one of them
CC picked its kernel with a preprocessor chain: SSE under __SSE__, the
external NEON assembly under HAVE_ARM_NEON_ASM_OPTIMIZATIONS, the C
reference otherwise. That macro is defined nowhere, so every ARM build
took the scalar path while sinc - which keeps a NEON intrinsics kernel
beside its assembly one and dispatches on resampler_simd_mask_t - was
vectorised on the same hardware.

Every arm is now built and init() picks the pair the mask names, as
sinc does. The kernels are NEON intrinsics rather than assembly:
vzipq_f32 gives the lane spreads _mm_shuffle_ps built and vcombine the
half splices, so the two arms are the same arithmetic in the same
order. Multiply and add are kept separate rather than vmlaq_f32, which
would round once where the other arms round twice.

cc_resampler_neon.S goes with it. It addressed struct offsets that no
longer held, read its ratio with vldr s4, [sp] under a hard-float
platform, and had no output bound; it is in the history if the
alignment hints are ever wanted back.

samples/audio/resampler_simd drives every arm a build has over the same
stream - upsampling, downsampling, unity, rate control's excursions -
against the scalar reference, and skips an arm this build lacks rather
than comparing the reference against itself. The arms agree to one
ulp, not to the bit: cc_int()'s polynomial has double literals, so the
reference rounds through double where the vector arms stay in float.
Swapping the two NEON lane spreads moves the worst difference from
1.19e-07 to 1.05.

Built armhf with -mfpu=neon and run under qemu-arm: same frame counts
as the reference, same one-ulp agreement. Measured on x86, SSE is 4.84x
the scalar arm upsampling and 2.98x downsampling; the qemu figures say
nothing about speed and are not offered as such.

No AVX arm. One was written and measured: correct to the same ulp, and
6% slower than SSE at 48/32. The kernel is four taps wide, so a 256-bit
multiply saves one instruction and costs three - an insert to spread the
weights, an extract and an add to fold the halves. sinc gains from AVX
because it streams 128 taps and more.
2026-09-21 21:05:13 +02:00
LibretroAdmin 69dcae7499 video, record: one packing for every size pair
The width/height pairs that travel as a single word all use
VIDEO_SCALE_PACK and its accessors now: the threaded wrapper's
viewport slots, the cached frame's dimensions, the output size, and
record_video_data, which carries both axes as one member. The macros
move to video_defines.h so the record drivers reach them. Clamping
applies everywhere the macro does, so an axis past 65535 saturates
rather than wrapping into a small number.

samples/gfx/threaded_video gains a size-pair round-trip lane: it
publishes at dimensions no square frame can hide, reads both axes back
through the public accessors, and checks the clamp at the top of the
range. A store and a load that disagree on which half holds which axis
fails it by name.
2026-09-21 20:46:04 +02:00
LibretroAdmin 63cc48514a CI: run tpool_parallel_test in the libretro-common samples job
5f19526 added samples/rthreads/tpool_parallel_test, but the samples
workflow fails any built target that is in neither RUN_TARGETS nor
NORUN_TARGETS, so the job has failed since. The sample is a
self-contained pass/fail test that runs in about 1.2 s and passes on a
one-core machine, so it goes in RUN_TARGETS beside tpool_wait_test.
2026-09-21 20:44:03 +02:00
libretroadmin dc98c1a6ac image_blit_bands: the bands are claimed, the caller helps, and nobody waits on the pool
The row bands of a blit went to the pool and the caller waited for
the pool to fall idle - which, once the pool also carries pictures
decoding for the frames to come, is a wait for every one of those on
every frame. A sixteen-core run of the concurrent H.264 decoder had
three pictures in flight and one core's worth of decoding, and this
was why.

No band belongs to a runner now. Whoever runs - the caller included -
claims the next band from an atomic index and does it until none are
left; a runner the pool starts late, behind a picture, finds them all
claimed and leaves. The caller never sleeps while there is a band it
could do itself, and waits only for the last few in other hands, on a
process-wide eventcount: no lock, no allocation but the group's, and
a syscall only when there is nothing left to do. The group is on the
heap and the last holder frees it, so a late runner has something to
find. Byte-exact on the blit-bands oracle under ASan and TSan.
2026-09-21 20:39:56 +02:00
LibretroAdmin 4d5f87fdbe (Resampler) One place builds an int16 backend, and the mixer uses it
The integer counterpart of a float backend was selected by an
if-chain on short_ident, written out three times in audio_driver.c -
the front pair, the extra-channel pairs and the microphone - and a
fourth site did not select at all: audio_mixer.c called
sinc_resampler_int16_init() outright, for both the one-shot WAV
conversion and the streaming s16 voice. So with CC or nearest chosen,
the game stream went through the selected backend while menu sounds
and BGM went through sinc, and those voices honoured Resampler Quality
while the stream did not.

retro_resampler_int16_new() is that selection, once, in
libretro-common. It resolves the name through the same lookup
retro_resampler_realloc() uses, so NULL or an unknown name lands on the
fallback, and it hands back the instance with its process, reset and
free - zeroed when the backend has no integer implementation, which is
the caller's signal to keep the float path. The quality mapping onto
sinc's own enum, which audio_driver.c and audio_mixer.c each carried a
copy of, lives there too.

It goes in its own unit rather than audio_resampler.c: a target that
never asks for an integer backend links none of the three int16
drivers, which putting it beside the driver table would have forced on
every consumer of audio_resampler.o.

audio_mixer_play_s16() takes the resampler_ident its float twin has
always taken, and the s16 voice carries the whole handle rather than a
bare pointer with sinc's functions assumed around it.

The HQ policy now reads RESAMPLER_CAP_HQ_OVERSAMPLE rather than
comparing the backend against &sinc_resampler, so extra_capacity's
stand-in for "another backend" clears that bit instead of relying on
its address.
2026-09-21 20:37:08 +02:00
LibretroAdmin 4820970267 (Resampler) A backend says what it reads, and the menu offers only that
Resampler Quality and HQ Oversampling were shown whenever the resampler
was not "null" (menu_displaylist.c:9149), while only sinc reads either.
With CC or nearest selected both rows were live controls that changed
nothing, and Quality cost a CMD_EVENT_AUDIO_REINIT to do it. On PSP,
where CC is the default driver, config.def.h even defaults Quality to
RESAMPLER_QUALITY_LOWEST.

retro_resampler_t gains caps, a RESAMPLER_CAP_* mask, last so a backend
that leaves it zero offers no control. sinc sets both, nearest and CC
zero. audio_resampler_driver_caps() answers for a name through the same
lookup realloc uses, so an unknown name reports the fallback's. The two
audio-page rows and the microphone page's quality row are shown against
it, and the HQ policy in audio_driver_resampler_realloc() asks the mask
rather than comparing the backend against &sinc_resampler.

resampler_options pins the mask per backend and through the name lookup,
including the fallback.
2026-09-21 20:37:04 +02:00
LibretroAdmin d6cd0395c8 (CC resampler) resampler_options drives CC, and three dead build bits go
The ident-lookup suite built only sinc and nearest, so nothing checked
that "cc" and "CC" reach CC_resampler through find_resampler_driver(),
or that the HQ request and the quality tier - neither of which CC reads
- still give one stream. It links CC now and covers both.

The suite freed its handle at the end of the nearest block and left the
pointer, which the next realloc would have freed again; the new block
clears it first.

Dead with it: CC_RESAMPLER_IDENT, defined three times and referenced
nowhere; HAVE_CC_RESAMPLER in Makefile.griffin, a make variable the
file never reads, since each platform passes -DHAVE_CC_RESAMPLER in
PLATCFLAGS; and cc_resampler_neon.o on Vita, which is built and linked
there although Vita defines neither HAVE_CC_RESAMPLER nor takes CC in
griffin.c, under PLATCFLAGS carrying -mfloat-abi=hard - the ABI those
kernels' vldr s4, [sp] is wrong for.
2026-09-21 20:37:00 +02:00
libretroadmin 1ab50b9d16 rthreads: the pthreads-on-Win32 timed wait reads the clock into the right struct
_ftime64_s fills a __timeb64 - seconds and milliseconds - and the
timed wait handed it the timespec it was about to give
pthread_cond_timedwait, which did not compile under a recent MinGW
and would have read garbage under an older one. The clock goes into
its own struct and is converted.
2026-09-21 20:14:23 +02:00
libretroadmin ad974bc082 samples/rthreads: the pool samples link without librt on Windows
MinGW has no librt, so -lrt stops both pool samples at the link;
glibc is the one that ever needed it, and only its older releases.
2026-09-21 20:07:56 +02:00
libretroadmin 5f19526311 rh264: the bench says how many pictures the pool decoded at once, and a pool oracle
A sixteen-core run had the pool's CPU time equal to the wall time:
however the pictures were posted, they decoded one at a time. The
decoder counts the most pictures its pool had decoding at one moment,
and the bench prints it beside the row waits. And rthreads gets a
sample that settles the pool by itself: four jobs each burning a spell
of CPU, posted to a pool of four, must run together and finish in
about one spell; jobs that never overlap fail it, and a machine that
gives the process one core is told apart from a pool that does not
wake its workers.
2026-09-21 19:57:30 +02:00
LibretroAdmin 44fb0e27f2 video, record: the remaining size pairs travel as one word
Four variables become two, and each site that used both now reads one.

The content scale the viewport maths produces was two fields that the
threaded wrapper then packed by hand into scale_packed; it is one field
in that same layout, so the wrapper publishes it with a copy and only
one place knows the packing.

The recording state's two pairs go the same way. A resize check that
compared width and compared height is one comparison against a freshly
packed viewport; the GPU size drops four size_t-to-unsigned casts with
the field; and --size's override is one test for "was it given" rather
than two. That override is now clamped at 65535 an axis by the pack,
which is past any size a recorder here opens.

video_frame_info_t keeps its own scale pair: it feeds the float
arithmetic the shader chain wants, where a pack would only be unpacked
again.

Verified: make check on plain, address+undefined and thread builds;
compile-matrix 112/0; C89_BUILD from a clean tree; hot-function-budget;
the tree builds release and C89 clean.
2026-09-21 19:39:25 +02:00
LibretroAdmin 9cb39b0af5 Threaded video: the published viewport travels as two words
Width and height go in one slot and the full size in another, width in
the high half, the layout scale_packed in the same struct already uses.
Four slots instead of six: two stores a publish, two loads a read, and
each pair is whole before the sequence is even checked.

Verified: make check on plain, address+undefined and thread builds; the
forced-atomic-backend loop over C11/GCC_NEW/SYNC/PS2/VOLATILE;
compile-matrix 112/0; C89_BUILD from a clean tree; hot-function-budget.
2026-09-21 19:39:21 +02:00
LibretroAdmin 978e8e0589 compile-matrix: two more files no job compiles, and errors first
The S3 cloud-sync backend only griffin includes, and nothing defines
HAVE_S3 for it at all; the Lakka wifi driver needs HAVE_LAKKA. Neither
is in the C89 build's object list, so neither was checked, and both had
a mixed declaration - s3_parse_url declaring a size_t after a log call,
connmanctl_tether_start_stop an int after a popen.

A failing lane also used to print the first twelve lines of the
compiler's output, which for a file with a page of warnings ahead of
its one error showed nothing to act on. The reporting moves into one
helper the four call sites share, and it prints the errors when there
are any.

Verified: 112 lanes, 0 failures, from a clean tree; C89_BUILD clean;
putting either declaration back fails its own lane.
2026-09-21 19:39:18 +02:00
libretroadmin b8860185ca rh264: the bench says where a picture in flight spends its time
The pipeline counters showed a sixteen-core run with one picture in
flight and nothing blocking the submitter, which leaves the pictures
serialising on each other. Two more numbers say whether that is so:
the reference reads that had to wait for rows, and how many rows short
they stood when they did, process-wide and reset on reading. And the
bench splits the CPU time of a run between the calling thread and the
pool (CPU_SPLIT=1), so a submitter busy the whole time is told apart
from one that only waits.
2026-09-21 19:38:48 +02:00
libretroadmin e11fadc1cb gfx_thumbnail_preview: clean removes the bench and the catch-up oracle too
The two newest binaries were left behind by clean, the .exe forms
along with them, so a rebuild after a pull could report the bench up
to date against sources it had never seen.
2026-09-21 19:22:03 +02:00
libretroadmin dfa4e68ced rh264: the pipeline says what it did, and the bench prints it
A four-core run of the concurrent decoder showed no gain, and nothing
in the tree could say why. The decoder counts what its pipeline does:
pictures posted to the pool, the pictures in flight at each post,
posts that found the most possible in flight, joins that had to wait
for a context, output pops that held a due picture for being
incomplete, and pops that waited for one with the queue full.
rh264_video_stats() reads them, the container and the image layer hand
the decoder out for a bench to ask, and the preview bench prints the
line under each thread count. Those six numbers tell a submitter
blocking on a context from pictures serialising on their reference
waits, which is the question a flat bench leaves open.
2026-09-21 19:15:14 +02:00
LibretroAdmin e410bb1cba (CC resampler) The NEON kernels guard on the macro that selects them
cc_resampler_neon.S compiled on __ARM_NEON__ alone, while the C file
selects the kernels on __ARM_NEON__ && HAVE_ARM_NEON_ASM_OPTIMIZATIONS
and every sibling .S - sinc_resampler_neon.S, float_to_s16_neon.S -
guards on both. Nothing in the tree defines that macro, so every ARM
build took the C reference and carried two exported functions nobody
called. It also left the reverse open: the macro defined, the file
assembled without the flags the kernels need.

Checked with clang -target armv7-linux-gnueabi -mfpu=neon
-mfloat-abi=softfp: no symbols without the macro, the two kernels with
it, and the file assembles cleanly either way.

Reviving the path needs more than this. The kernels take their ratio
with vldr s4, [sp], which is the soft-float convention and wrong under
-mfloat-abi=hard, and the upsampler has no output bound, so they should
not be switched on without a device to test them on.
2026-09-21 19:10:10 +02:00
LibretroAdmin a31f03b33a (CC resampler) The stream's state leads the struct
resampler_CC_init() allocates with memalign_alloc(32, ...), but the
function pointer sat first, so buffer landed at offset 8 on a 64-bit
host and 4 on a 32-bit one - never 16-byte aligned, and the SSE arms
had to take it with _mm_loadu_ps/_mm_storeu_ps for a state the
allocation had gone out of its way to align.

It is also the layout the NEON kernels address: cc_resampler_neon.S
documents "[r2+0] --> buffer, [r2+32] --> distance" and acts on it at
:84, :103, :187 and :188. With the pointer leading, the 32-byte store
at :187 writes buffer[0].l over re->process and distance is read and
written at buffer[3].r. Nothing compiles that path today, so this has
been latent rather than broken, but the offsets are now the ones it
was written against: buffer at 0, distance at 32, on both widths.

The SSE accesses become _mm_load_ps/_mm_store_ps. init() is the only
place the struct is allocated, so the alignment holds. No measured
speed change on x86 - best-of-7 over 20000 calls sits in the same
0.174-0.177 s band either way, which is what movups on aligned data
costs on anything recent; the older in-order targets this driver
exists for are where it is not free.
2026-09-21 19:10:07 +02:00
LibretroAdmin cd848c0539 (CC resampler) The int16 arm takes the float arm's two guards
cc_resampler_int16.c mirrors the float driver, including both of the
things just fixed in it.

The upsample loop had no output bound, and its phase step is quantized:
ratio = (1/data->ratio) * 2^24 is zero once data->ratio passes about
3.4e7, so re->distance never advances and the loop writes past data_out
forever. A negative ratio walks the other way, and a ratio that is not a
number reaches the int64 casts, where the conversion is undefined. Both
arms now refuse a ratio outside (0, 65536] before those casts, and the
upsampler stops at what the ratio asks for.

The direction is also latched at init from the nominal ratio while the
runtime one is multiplied by slow motion, and cc_i16_downsample() emits
at most one frame per input frame. Same move as the float driver: above
a ratio of one the stream goes to the upsampler and stays.

samples/audio/stretch gains both lanes for CC, float and int16 - eight
calls at 2.0 on an instance initialised at 0.5, and the five ratios no
pair of sample rates can name, into an exact heap buffer. Without this
commit the int16 half segfaults. sinc and nearest do run past data_out
on those ratios; the suite says so where it drives only CC.
2026-09-21 19:10:04 +02:00
LibretroAdmin 63a70f15c6 (CC resampler) A ratio above one moves the stream to the arm that serves it
resampler_CC_init() picks the direction once, from the nominal ratio
the frontend passes as bandwidth_mod. The ratio the backend is then
handed is a different number: audio_driver_effective_ratio() multiplies
it by the slowmotion setting, 3.0 by default.

resampler_CC_downsample() emits at most one frame per input frame - the
store sits inside the once-per-input body - so above a ratio of one it
cannot produce what it is asked for. Measured over 2,048,000 input
frames, an instance initialised at 0.5 and run at 2.0 returned 2,047,999
frames where 4,096,000 were due: half the audio, gone, for any core and
device whose nominal ratio is under 0.75 the moment slow motion starts.

So the stream moves to the upsampler, which serves either direction, and
stays there. The two carry distance differently, so the move restarts
the stream; it happens once. Measured at 48000 to 32000, the two arms
agree to two decimals on gain and THD+N at every frequency from 100 Hz
to 14 kHz, so nothing is given up by staying.

resampler_reset gains the lane: eight calls at 2.0 on an instance
initialised at 0.5. Without this commit it reports 7999 frames of 16000.
2026-09-21 19:10:00 +02:00
LibretroAdmin b5ee334623 fill_pathname_test: cover fill_pathname_join truncation
The function reports the length it wanted, so >= len is how a caller
detects a join that did not fit; callers sizing a path buffer from a
bound held elsewhere depend on that and on nothing outside the buffer
being written. Neither was covered.

Checked against an injected defect that clamps the return to len - 1:
the case fails there and passes once reverted. ASan clean.
2026-09-21 18:56:09 +02:00
LibretroAdmin 62c402acc6 vfs_implementation: use retro_posix_source.h
The inline feature block predates the shared header by one commit and
does the same job, leaving two idioms in the tree for it. The object is
byte-identical under the default profile.
2026-09-21 18:56:05 +02:00
LibretroAdmin 40334507dc psp_joypad: include the SDK header it takes its controller API from
The PSP path uses SceCtrlData, the PSP_CTRL_* bits and three sceCtrl
calls and includes none of them. It compiles because griffin includes
psp_input.c, which does include <pspctrl.h>, ahead of it - the VITA
path in this same file includes <psp2/ctrl.h> for itself, so the
omission is one-sided.

A compile-only pspctrl.h joins the seven psp stubs already there, and
two lanes follow: psp_input and psp_joypad, neither compiled anywhere
else. SceCtrlData carries what pspsdk's does in the same order and no
more - the PSP has one analog stick, and a stub offering Rx/Ry would
let a lane pass what the real SDK rejects. Taking the include back out
fails the new lane.

The 3ds audio drivers stay out: they want fifteen more symbols whose
libctru signatures cannot be checked from here, including a Result
returning entry point, and a guessed one is a lane that passes code
the SDK refuses.

Verified: 110 lanes, 0 failures, from a clean tree; the tree builds;
the stub header compiles standalone under -std=c89 -pedantic.
2026-09-21 18:55:23 +02:00
LibretroAdmin c5aa2ae368 compile-matrix: cover the platform drivers no other job compiles
griffin includes each of these behind its platform's #ifdef and the
desktop makefiles never build them, so a C89 slip or a missing
declaration in one waits for the console job. The stubs to reach them
are already in the tree; nothing joined the two.

Ten lanes, on the files the existing stubs reach today, with the same
warnings the driver lanes above them use - C89 declarations included,
because that is the rule these files are furthest from anyone
checking. GEKKO takes the vendored libogc headers ahead of its stub,
which carries only what libogc does not. dispserv_android already had a
lane and gains the declaration check rather than a second one;
psp1_gfx is covered by platform_video.

gx_joypad.c needed a fix to pass: WPAD_StickX declared its double
mid-function, where WPAD_StickY immediately below it declares the same
variable at the top. Reintroducing it fails the new lane, as do
injected implicit declarations in ctr_input.c and ps4_audio.c.

Files the stubs cannot reach are left out rather than papered over -
gx_audio, gx_gfx, ctr_gfx, wiiu_audio, gx2_gfx and the rest still want
headers no stub here supplies, and two of them collide with the host's
own nanosleep.

Verified: 108 lanes, 0 failures, from a clean tree; the tree builds;
hot-function-budget unchanged.
2026-09-21 18:47:35 +02:00
LibretroAdmin 2f07610657 retro_common_api: take ssize_t from <sys/types.h>
<stdio.h> carries ssize_t only for a profile asking for the BSD or
POSIX.1-2008 set, so a consumer building under a stricter one gets
"unknown type name 'ssize_t'" out of any public header here that uses
it, chd_stream.h among them. Include the header POSIX puts it in.

Scoped to __unix__ and Haiku alongside the existing Apple arm; console
toolchains define none of those and keep the path they have.
2026-09-21 18:45:59 +02:00
LibretroAdmin baa8536a17 libretro-common: ask for the POSIX profile the sources need
strdup(), strcasecmp(), fstatat(), O_CLOEXEC, M_PI, sigaltstack() and
syscall() all sit behind __USE_XOPEN2K8 or __USE_MISC, so a consumer
compiling the library under a stricter feature profile than the default
sees none of them. The Debian/Launchpad core packaging passes
-D_XOPEN_SOURCE=600, where ten of these translation units reach an
implicitly declared function; at -D_POSIX_C_SOURCE=200112L strdup joins
them, and an implicit strdup returns int, so the pointer is truncated on
LP64 rather than merely warned about.

Add retro_posix_source.h, which asks for that profile, and include it
first in the files that need it. The guards only raise the profile and
ask for what a build passing no feature macros already gets, so no path
the library selects changes: every touched object is byte-identical
under the default profile.

nbio_linux.c defined _GNU_SOURCE after its first include, where it had
no effect; retro_posix_source.h replaces it.

Verified: all 221 standalone-compilable libretro-common units clean
under no macros, _XOPEN_SOURCE 600 and 700, _POSIX_C_SOURCE 200112L and
200809L, and _GNU_SOURCE, with implicit declarations and int-conversion
as errors; the fifteen samples suites that link these units build and
pass; compile-matrix.sh gives results identical to before the change.
Building with -DRETRO_POSIX_SOURCE_CHECK_ORDER turns a late include into
an error, and was itself checked both ways.
2026-09-21 18:45:55 +02:00
libretroadmin bcd929ffe5 rh264: pictures decode concurrently, each waiting on the rows it reads
Given a thread pool, the decoder keeps up to four pictures in flight.
A picture's slices are queued as they arrive, on its own context, and
go to the pool when the next picture opens; the sequence's side - an
IDR emptying the DPB, reference marking, the DPB and the output queue
- is settled then, on the submitting thread, with the picture's rows
still counting up. Every place a picture reads a reference - the
inter prediction, the B predictions, and temporal direct's co-located
cells - works out the row the read reaches and waits on the block's
counter for it, and nowhere else. A picture due for output leaves
only once complete; the reorder depth is the pool's head start. Field
pictures and intra pictures stay on the submitting thread: a pair
fills one block a field at a time, and an intra picture's grid is
marked at completion. The waits sleep on a condition; nothing spins.
Without a pool, or with one thread, this is the decoder as it was.

The lossless oracle decodes every case four ways and holds each
byte-exact against ffmpeg: on one thread, with four contexts rotating,
with four pictures decoding concurrently on a pool, and concurrently
with every row's publication held back by a random spell so that the
readers wait for their rows rather than nearly always finding them.
That last run, and the sanitizers, found what they were for: a hook
on the submitter's picture rather than its own, a completion asking
the sequence's flag, reference counts that were not atomic, a field
view reaching half the rows it read, and the B predictions reading
their references past the wait altogether.

The MP4 container hands its pool to the H.264 decoder as it does to
the HEVC and VP9 ones, so the thumbnail's thread setting reaches all
three; a bench on one core is what it was, and the number that matters
comes from a machine with cores to give.
2026-09-21 18:34:38 +02:00
LibretroAdmin 7c9fa046fd (CC resampler) The Allegrex stream travels in the handle
The VFPU register file is per-thread context on PSP, and the two ends
of this backend are on different threads: audio_driver_init_internal()
creates the resampler on the main thread, and the threaded audio
pipeline runs process() on the audio worker. The constants pi and 1/pi
that init() issued, and the accumulator and position in c720/c730,
never reach the thread that resamples - it starts on whatever its own
context holds. One register file also cannot hold several instances,
so the frontend's resampler, the extra-channel ones and every mixer
voice were the same stream.

So the handle carries them: init() allocates it, process() issues the
constants per call and loads c720/c730 from it and stores them back
around the loop, and reset() and free() become the plain C the other
arms already have. Two vcst.s and a pair of quad accesses per call,
against a loop that runs per frame.

This is the crash reported on PSP: a stale position kept the loop
emitting after the input was spent, and the scalar float stores walked
out of the output buffer and off the end of user RAM. The bound in the
previous commit stops the walk; this stops the stale position.

The asm is compiled here only through gcc -fsyntax-only against the
PSP stub headers, which does not assemble the VFPU mnemonics. It wants
a devkitPSP build before it goes near master.
2026-09-21 18:29:04 +02:00
LibretroAdmin 5dbe67ced6 (CC resampler) Stop at the output the ratio asks for
struct resampler_data hands a backend an output pointer and no
capacity, so the frontend's own clamp is all that keeps a ratio inside
data_out - and audio_driver_bound_ratio() returns the ratio untouched
wherever the caller reports no capacity. The arms that can emit more
than one frame per input frame now stop at what the ratio asks for
plus the frame a carried phase adds, and a ratio no pair of sample
rates can name resamples nothing rather than guessing at one.

This is what keeps a bad phase inside the buffer instead of walking
memory: the upsample loops exit only on their own accumulator, so a
ratio or a position that never brings it back under one emits forever.

resampler_reset gains the contract: every backend's reported frames
against what the ratio asks for, across several calls so a carried
phase is in play, and CC against the ratios a clamp would have caught,
into an exact heap buffer so a sanitizer build sees a write past it.
Without this commit that lane is an ASan SEGV in the SSE upsample arm.
sinc and nearest run past data_out on the same ratios and are left for
their own change; the suite says so where it skips them.

The bound is free: best-of-7 over 20000 calls at 48/32 on the SSE arm
lands in the same 0.174-0.177 s band with and without it.
2026-09-21 18:28:59 +02:00
LibretroAdmin ab583a16b6 rwebaudio: declare memalign, and stop on a refused allocation
Three things in one function. memalign is called with no declaration -
<stdlib.h> does not carry it - and this driver is its own translation
unit in the emscripten build, not part of griffin's, so nothing hides
it: emscripten's clang makes an implicit declaration an error by
default. audioworklet.c, the sibling driver in the same directory,
already includes <malloc.h> for the same call.

The two mixing buffers were used unchecked. rwebaudio_write()
interleaves into them on every call with no test of its own, so a
refused allocation handed back a context that faults on first use;
init now fails and tears down instead.

And the driver context leaked when RWebAudioInit() failed, between the
calloc and the error return.

The allocator itself stays memalign: the buffers cross into the JS
side, and libretro-common's memalign_alloc keeps a header word below
the pointer it returns, which is not a swap to make on a platform this
cannot build.

Verified: the file compiles standalone under gnu99 and c89 with
-Werror=implicit-function-declaration, where it failed before, and the
tree builds. No samples/audio suite references it, so there is nothing
to run.
2026-09-21 18:26:30 +02:00
LibretroAdmin a1de2ae3bc librsound: declare perror and snprintf
librsound.c calls both and includes no stdio.h. It reaches a compiler
only through griffin's unity build, where stdio arrives 900 lines
earlier from compat_snprintf.c, so the two implicit declarations have
never fired - they wait for the include order to change or for the file
to be built on its own. Compiled standalone it is two
-Werror=implicit-function-declaration errors, which is a hard error on
devkitPPC rather than a warning.

The rsound driver's own comment also still pointed at librsound's
callback lock as the thing its ring avoids touching. That lock is gone;
the ring is what the handoff runs on.

Verified: librsound.c standalone clean under gnu99 and c89 with
-Werror=implicit-function-declaration, where it failed before; the tree
builds; and the new include is provably a no-op in the unity build,
which already has stdio.h by then.
2026-09-21 18:19:25 +02:00
LibretroAdmin 14e495b4bf libretro-common: ask for the POSIX profile vfs_implementation needs
O_CLOEXEC, fstatat() and dirfd() are POSIX.1-2008 and syscall() is a
glibc/musl extension, so a consumer compiling this file under a
stricter feature profile than the default sees none of them; the
Debian/Launchpad core packaging passes -D_XOPEN_SOURCE=600 and stops
on undeclared O_CLOEXEC. Request the profile the file needs before its
first system header instead, raising it only when what the consumer
asked for is lower.

What is requested matches a build passing no feature macros at all, so
every path this file selects stays as it is: the object code is
byte-identical under the default profile, and a -D_XOPEN_SOURCE=600
build now produces that same object.
2026-09-21 18:19:03 +02:00
LibretroAdmin 2d777c75fb video_driver: publish the viewport parameters only when they change
The snapshot is published once per frame as the catch-all for settings
toggles, and the video thread reads it every frame. What it carries
changes on a settings toggle, a rotation or an aspect change - never
during play - so the unconditional store spent a cache line transfer a
frame to say nothing, and took the line away from the reader to do it.
Compare first: fifteen relaxed loads, which leave the line where it is,
and the sequence bump and the stores only on a difference.

The slot count stops being a literal in two places, with headroom left
in the array so a parameter can be added without moving anything after
it.

Nothing notices if the compare goes - the values are still right and
every lane passes - so the harness watches the sequence counter, which
is the one thing that stands still exactly when the publish did
nothing: ten quiet frames must not move it, a changed aspect index
must, and the new value must reach the slot. Publishing
unconditionally fails the first; a slot that stops following its
setting fails the others.

Verified: compile-matrix 98/98, C89_BUILD from a clean tree, the
RARCH_MOBILE branch of the publisher, hot-function-budget,
stack-frame-sweep, lock_acquire_budget, thread_read_audit,
video_thread_state_check, and make check on plain, address+undefined
and thread builds plus 15 cycles.
2026-09-21 18:13:13 +02:00
LibretroAdmin ee3bb8ed40 retro_eventcount: say why the two cursors share a line
Padding epoch and waiters apart is the obvious-looking fix and it is
the wrong one, so record the measurement rather than leave the next
reader to rediscover it. Both sides read both cursors on every
operation, so separated, each pays two lines where one served.

Measured on 32 cores, shared against padded, medians over nine rounds:
where the operations punctuate real work, which is every caller in the
tree, no difference at all - 1.00 with the bracket inside a percent,
with one waiter or four. In a tight loop the direction is not even
consistent: one notifier and one waiter lose a third of the notify rate
separated, four waiters gain a seventh. The video ring is the former
shape, so the padding would cost it something for nothing.

retro_spsc pads its cursors because each of its sides reads only its
own; the note points at that difference.
2026-09-21 17:47:13 +02:00
LibretroAdmin 6fae03444c ctr: build the DSP audio driver against libctru 1.x
SYSCLOCK_ARM11 falls back to the SoC clock's multiple where libctru does
not define it, and the frame wait polls LightEvent_TryWait in short
slices where LightEvent_WaitTimeout is absent; the ctr-legacy image has
neither.
2026-09-21 17:37:45 +02:00
LibretroAdmin bc01dfd1be Threaded video: keep the two threads off each other's cache lines
Three places in thread_video_t had fields written by different threads
on one 64-byte line, so every write to either pulled the line across:
the snapshots the video thread publishes every frame sat with the head
of cmd_data, which a sender rewrites for every synchronous command; the
per-frame published flags sat with nonblock and the deferred ring's
head; and the ring's producer and consumer indices were four bytes
apart. A line of padding each way, so the separation holds wherever the
fields land rather than depending on an offset, at 192 bytes on a 22 KiB
struct.

Measured on a 6-core Windows workstation with a bench that models these
exact offsets, so its "shared" variant is the layout as it shipped.
Publishing during a burst of synchronous commands goes 2.13x
[1.93 .. 2.19 over 9 rounds] with no cost to the sender, 0.99
[0.96 .. 1.00]; the ring's throughput goes 1.20x [1.10 .. 1.33] on both
ends. A quiet frame - one command between two publishes - came out as
noise on both machines, so that part is not claimed.

The padding is the whole of the mechanism and nothing else notices when
a field added above one of the pads moves the field below it back onto
the shared line, so the harness asserts each separation by offset
against the struct the build actually has. Removing pad_published,
pad_flags or pad_ring each fails the lane by name.

Verified: compile-matrix 98/98, C89_BUILD from a clean tree, the
forced-atomic-backend loop over C11/GCC_NEW/SYNC/PS2/VOLATILE,
hot-function-budget, stack-frame-sweep, lock_acquire_budget,
thread_read_audit, video_thread_state_check, and make check on plain,
address+undefined and thread builds plus 15 cycles.
2026-09-21 17:30:09 +02:00
libretroadmin ab7eca949f rh264: pictures take contexts in rotation, and the oracle decodes every case at four
The finish comes apart into the picture's own completion - the slice
map for the filter, the rows the row hook did not filter, the intra
marks in the grid, the block's row count set to the whole picture,
the references let go - and the sequence's side: an IDR emptying the
DPB, reference marking, the picture entering the DPB and the output
queue. The first takes only the context and is what a pool thread will
run; the second runs in decode order on the thread that submits.
Allocation comes apart the same way: the DPB and the output queue are
remade only when the geometry changes, and a context is readied for
that geometry when its picture opens, not before its header is read.

With that a picture can take any context: the decoder holds four, a
new picture takes the next one round, and a field pair stays on the
context its first field took. rh264_video_set_contexts() sets how many
are in rotation; one is the decoder as it was. The pictures still
decode one after the other, so rotating changes which memory a picture
uses and nothing else - and that is the proof this sequence of
changes was for. The lossless oracle decodes every case at one context
and at four and holds both byte-exact against ffmpeg: a difference
would be a piece of picture state still in the decoder rather than the
context, shared between pictures the moment they decode concurrently.
There is none, and the check stays.
2026-09-21 17:09:21 +02:00
LibretroAdmin 2131c183fe overlay_on_screen_test: turn widgets off so a notification can't cover a square
A CI run failed with threaded video off: rows 377-418 of the bottom
right square were not magenta and its centre read (22, 22, 22). That
is 0x161616, BG_COLOR_DEFAULT, the background of a widget notification
box, which at 640x480 lands over that square. The runner reinitialised
a driver during startup, and the notification it raised was still up
when the screen settled and was grabbed.

The test only looks at the overlay, so set menu_enable_widgets to
false in its config.
2026-09-21 16:51:33 +02:00
LibretroAdmin 9c61c12805 tools: hold the lock-free paths to their acquisition count
The statistics the overlay reads, the viewport an input driver asks for
on every poll, the refresh rate the runloop asks for every iteration
and the audio thread's two loop conditions all carry their values in
atomics so the read costs no acquisition. Nothing stops the next change
from putting an slock_lock back, and a reinstated one breaks no test:
the suite passes and it shows up as a slower frame on someone's
machine.

So count them. The count is read off the relocations inside each
function's own block in the release objects, which catches a lock taken
through a helper the compiler inlined and misses one taken through a
call it kept - a floor, like tools/video_thread_state_check.py, and
worth having because every acquisition it reports is a real one. Three
of the counts are deliberately not zero: viewport_info writes read_vp
when the viewport has changed, the audio loop takes the lock on the
pass that parks or leaves, and the gather takes it once it has
something to retire.

Run by hand after a plain make. It reports which architecture it can
count on rather than passing silently where it cannot.

Negative-tested: an acquisition put back into presenter_stats, into
viewport_info's poll, into the audio loop's every pass, and into
video_driver_frame each fails its own line.
2026-09-21 16:50:29 +02:00
libretroadmin e45b786cbe rh264: a picture's reference lists are its own snapshots
A slice decoder read its reference lists through pointers into the
DPB slots. On one thread that is sound; with pictures decoding
concurrently a slot can be taken by a later picture while an earlier
one still decodes from it, and the reference would be pulled out from
under the read. Each picture's context now snapshots the frames its
lists name - a copy of the frame holding its own reference on the
sample and motion blocks - and the lists point at the snapshots.
Entries naming the same picture share one, and the finish releases
them. The counted blocks are what make that a pointer swap and a
count, not a copy.

With this a picture's decode depends on nothing in the decoder a
later picture can change: the slice decoders take their frame, their
parameter sets and their lists as arguments, the lists are owned, and
the row counter is on the block. What remains for pictures to decode
concurrently is the sequencing. Byte-exact on every case of the
lossless oracle, with no reference read short of its rows.
2026-09-21 16:41:55 +02:00
LibretroAdmin e69fd2efaf audio: take the lock off the audio thread's loop
The loop took thr->lock every pass to read two booleans that are false
while audio is playing. They become atomics: the loop tests them
without the lock and takes it only on the pass that has something to
coordinate - leaving, or parking on a stop. A writer still holds the
lock while it changes one and signals, which is what keeps the change
and the wakeup atomic against a waiter, and the stop path re-tests
under it so a start that lands in between is not stopped and started
again for nothing. alive() and apply_control() read them as atomics
too, where they read a plain bool before.

That plain read was a race, and samples/audio/thread_handshake now
reaches it: the scripted device refuses a write, which is the one
place the audio thread marks the device gone, and the frontend then
asks whether it is alive. The lane also feeds the callback through the
wrapper's own write() rather than the inner driver's, which is the
path the frontend uses. On the old code TSan reports the race between
audio_thread_alive() and audio_thread_write(); with the atomics it is
clean, and the lane holds the device-gone contract - reported, stop
returns without waiting for an acknowledgement, free() joins.

Verified: thread_handshake and pipeline_wakeups, the two suites that
compile the wrapper, on plain, address+undefined+leak and thread
builds; compile-matrix 98/98; C89_BUILD from a clean tree; the
forced-atomic-backend loop over C11/GCC_NEW/SYNC/PS2/VOLATILE;
hot-function-budget; stack-frame-sweep; thread_read_audit; and
samples/gfx/threaded_video make check.
2026-09-21 16:40:08 +02:00
LibretroAdmin 87ba61a2b6 CI: run every retro_procbarrier tier on Linux, Windows and macOS
The Linux samples job runs retro_procbarrier_test once, so it only
ever exercises membarrier. The page-flip and signal fallbacks, the
Windows FlushProcessWriteBuffers path and the Darwin paths never ran
in CI at all; the Darwin code had never been compiled.

Add procbarrier-tiers.yml, which builds retro_procbarrier_test and
retro_asym_eventcount_test on Linux x86_64/arm64, Windows x64/arm64
and macOS arm64/x86_64, and runs both once per tier the platform has.

run_tiers.sh checks the "tier:" line of each run, not just the exit
code: a forced tier the platform lacks falls back to the natural one
without an error, which would otherwise pass unnoticed. With
REQUIRE_SMP=1 it also fails on a one-CPU runner, where the fence
checks cannot catch a missing barrier.
2026-09-21 16:36:39 +02:00
libretroadmin bed37a08f4 rh264: the picture's state is its own, and its block counts the rows that are final
Everything that belongs to the picture being decoded - the working
frame, its motion grids, the slice bookkeeping, the slice scratch, the
NAL unescape buffer - moves into rh264_pic_ctx, and the decoder holds
one of them for now. What stays in rh264_video is the sequence:
parameter sets, the DPB, picture order, reference marking, the output
queue. The slice decoders already took their frame, parameter sets and
reference lists as arguments and touched the decoder only for the
sequence, so one of these per picture in flight is what lets pictures
decode on different threads, and a scheduler is all that is missing.

The block behind a picture's planes counts the macroblock rows that
are final. The row hook counts them up as each row's filtering
completes - after row r is filtered everything above it is final,
while r's own last lines are still touched through the edge it shares
with r+1 - and the finish counts the whole picture. The one place a
reference is read works out which row the read reaches, the block's
last line displaced by its vector and the six-tap filter's three past
it, and asks the block for that row. On one thread a reference is
complete before it is read and the ask is answered at once; a picture
decoding on another thread will wait there, and nowhere else. The
lossless oracle asserts that on one thread no read ever finds its
rows short, across every case: a short read would have a threaded
decoder waiting on a row it had already been handed.
2026-09-21 16:12:50 +02:00
LibretroAdmin b74767fc76 Threaded video: publish the viewport and the refresh rate
Both come from the wrapped driver on the video thread, which is their
only writer, and both are read from elsewhere every frame - an input
driver asks for the viewport on each poll, the runloop asks for the
rate on each iteration. Publish them instead of locking for them: the
viewport as a seqlock over six slots, the rate as one word of float
bits. read_vp still has to follow what was reported, because
CMD_READ_VIEWPORT compares the driver's own viewport against it, so
viewport_info writes it when it changes - a resize - and the steady
state takes no lock at all. Every reporter writes the same published
value, so a compare that races another's write converges on it.

thr->vp goes with them; the snapshot is what callers read now.

The harness lane asserts the reported values arrive, that read_vp
follows a resize, and that the read touches no lock: a helper thread
holds thr->lock for 200 ms and the viewport, the rate and the four
statistics readers are timed inside that window, so a reader that
still takes the lock is delayed rather than deadlocking. Putting
either read back under the lock fails it at 199 ms; swapping the
width and height slots, dropping the per-frame publish, never
publishing the rate, and stopping read_vp from following each fail
their own check.

Verified: compile-matrix 98/98, C89_BUILD from a clean tree, the
forced-atomic-backend loop over C11/GCC_NEW/SYNC/PS2/VOLATILE,
hot-function-budget, stack-frame-sweep, thread_read_audit,
video_thread_state_check, and make check on plain, address+undefined
and thread builds, plus 15 cycles with no lane skipped.
2026-09-21 16:11:49 +02:00
LibretroAdmin 9b398489e1 net: declare struct pollfd only where the SDK does not
winsock2.h declares struct pollfd when _WIN32_WINNT >= 0x0600. Gate
the libsmb2 compat.h declaration on that, so Vista+ targets take the
SDK's and the Xbox XTL, legacy winsock.h and pre-Vista SDKs take the
compat one. Fixes C2011 on the UWP build.

net_compat.h supplies the same declaration for pre-Vista Win32
targets, where socket_poll() needs it and no builtin SMB client is
compiled: msvc2003 builds at 0x0400, msvc2005 at 0x0410.

tools/win32-pollfd-check.sh cross-compiles both headers in both
include orders from 0x0400 to 0x0A00, plus net_socket.c on its own.
Run it after touching either header.
2026-09-21 16:11:15 +02:00
libretroadmin 229c1cecbe rh264: a reference's own motion grids are made when a field needs them
Every DPB slot carved two full motion-vector grids into its meta block
when the decoder was set up - thirty-two megabytes a slot at 4K - and
since a frame picture shares the working grid into its slot, nothing
read them: they were allocated and zeroed for the field pairs alone,
which copy into grids of their own. The slot makes its two grids the
first time a field is copied into it and keeps them with the frame;
a stream of frame pictures never allocates them.

The preview bench takes BENCH_MAX, to profile a stream's start on its
own: what it showed on a 4K recording was that a second picture taking
most of a second was a 1.2 MB P-frame's entropy decode, not the
decoder's setting up.
2026-09-21 15:46:33 +02:00
LibretroAdmin 1371dc922d Threaded video: publish the overlay's statistics as a seqlock
The statistics overlay reads four groups of wrapper numbers once a
frame, and each read took thr->lock - a write-share on the busiest
lock in the wrapper, from the runloop thread, for data it only reads.
Publish them instead: the video thread stamps the snapshot inside the
region it already holds that lock for, so publishers stay serialised
by it, and a reader only loads. 64-bit counters travel as two
int-wide halves, low first. core_time and display_pacing come from
the main thread, so they ride a frame behind; everything else is this
frame's.

samples/gfx/threaded_video gets a lane that brackets each published
field against the field it came from, and asserts the swap count
across a carry out of the low half. The flags word is only meaningful
where a bit is set, so the repeat, display-phase and display-pacing
lanes call the same check with their own bools true. Negative-tested
per bit and per slot: each dropped half, swapped slot pair and
dropped flag bit fails the lane.

Verified: compile-matrix 98/98, C89_BUILD from a clean tree, the
forced-atomic-backend loop over C11/GCC_NEW/SYNC/PS2/VOLATILE,
hot-function-budget, stack-frame-sweep, thread_read_audit,
video_thread_state_check, and make check on plain, address+undefined
and thread builds.
2026-09-21 15:33:30 +02:00
libretroadmin 827ed072db Animated previews: the feed keeps ahead of a decoder that is catching up
The windowed feed reads half a megabyte a tick, sized for a decoder
that takes one sample a tick. A decoder catching up takes three or
four - it passes over the pictures nothing references - and a fast
one takes them at whatever rate it decodes, so either eats through the
two seconds of lookahead faster than the feed refills it and then
stands at the wall for the forty ticks a refill takes: on a 4K
recording, a stall near a second at every group of pictures. The
pacing holds only while the feed is comfortably ahead now; once the
resident lookahead is below half its target the tick extends unpaced,
the burst a lap or an open already pays, and the decoder never sees
the wall. On the recording that showed it, the stalls at each group
went from eight hundred milliseconds to two hundred, which is one
picture's decode.

The preview sample's paced check bounds the stall now, on the
B-frame fixtures, and its audio checks apply only when audio was asked
for. The bench names any frame that took over a third of a second.
2026-09-21 15:32:28 +02:00
grokiepoki f670c27aa3 net: implement Win32/Xbox socket_poll() with select()
WSAPoll is Vista+ only. 9x, XP, and Xbox XTL do not export it.

Keep socket_poll() on every platform. On Win32 and Xbox implement it
with select() and Microsoft POLL* bits so socket_wait() and SMB keep
working. POSIX/Vita/Gekko/PSL1GHT backends are unchanged.

libsmb2: stop mapping poll to WSAPoll. Use the existing select()-based
poll() in compat.c on all Windows targets, including desktop Winsock2.
2026-09-21 15:31:34 +02:00
LibretroAdmin 6702926dd9 icloud: report failures as failures, and keep copies under non-destructive sync
The CloudKit driver:

- reported success for a read whose local file could not be written
  (with no file, which cloud sync takes as "not on the server" and
  drops from the server manifest) and for a record with no readable
  data (with an empty file, which cloud sync takes as the save);
- saved a new record when the lookup before an upload failed, leaving
  a duplicate of a record that may well exist;
- reported a delete as done before the deletes were sent, and as a
  success whatever became of them;
- ignored non-destructive sync: deletes removed records for good and
  uploads replaced their data.

Reads now fail unless the data was loaded and written in full. Uploads
fail on a failed lookup. Deletes report once every record has been
dealt with, with the result. With destructive sync off, a delete
renames the record's path to <path>-<yymmdd-hhmmss> and an upload that
replaces a record first saves one under that name holding the old
data, not replacing anything if that fails, as the other drivers keep
their copies. The server manifest is not kept. A duplicate-record log
line also passed an NSString to %s.

samples/network/icloud_cloudkit includes icloud.m and replaces
CloudKit with an in-memory database by overriding +[CKContainer
defaultContainer] in a category, using real CKRecord, CKAsset and
CKQuery objects. It checks each case above and that every request
reports once. Against the old driver 8 checks fail. Runs in CI macOS
under ASan+UBSan.
2026-09-21 15:22:18 +02:00
LibretroAdmin dadbd009d9 (Vulkan) Negotiation interface v3: a presentation queue in the graphics family
A presentation_queue that is another queue of queue_family_index is only
valid for a frontend that takes one, so it gets a version of its own.
GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT reports 3, and a core
may hand back that queue only when it sees 3 or higher. Frontends below
that fail context creation on any presentation_queue that is not the
graphics queue.

Acceptance stays version-agnostic: a core that provides the queue without
asking is still honoured when the family matches.
2026-09-21 15:17:40 +02:00
LibretroAdmin 5bb3e201a5 google_drive: keep the replaced copy when an upload overwrites a file
With destructive sync off, a Google Drive delete keeps the server's
copy, renamed in place to <name>-<yymmdd-hhmmss>, but an upload
replaced the file's content outright, so the setting protected against
deletes and not against overwrites.

An upload to an existing file now asks Drive to copy it (files.copy,
server-side) to <name>-<yymmdd-hhmmss> in the same folder, the name a
delete uses, before the PATCH. A failed copy stops the upload rather
than overwrite without a backup. A file that does not exist yet has
nothing to keep, and the server manifest, rewritten every sync, is not
backed up.

A 401 on the PATCH now retries only the PATCH. It used to go back
through the whole update, search included, which with the backup would
take it a second time; the file ID is known by then.

samples/network/gdrive_backup includes google_drive.c and plays the
Drive API request by request: copy before PATCH, from the file being
replaced, named and placed as a delete names it; a failed copy stopping
the upload with one failure report; a 401 on the copy and on the PATCH
each refreshing the token once; destructive sync, a new file and the
manifest not copied. Against the old driver 5 checks fail. Runs in CI
Linux samples/network under ASan+UBSan and TSan, as does the SMB
harness.
2026-09-21 15:11:28 +02:00
LibretroAdmin ff1d6571b3 smb: upload through a temporary file and keep the replaced copy
smb_update() opened the server's file, truncated it and wrote. A write
that failed part way - a dropped connection, a full share - left the
server holding a truncated save, in either sync mode. And with
destructive sync off, a delete kept the old file as
<path>-<yymmdd-hhmmss> while an upload overwrote it in place, so the
setting protected against deletes and not against overwrites.

Uploads now write <path>.rauploading and rename it into place once it
is complete. Before that rename, the file being replaced is renamed to
the backup name a delete uses (non-destructive) or removed
(destructive); if the final rename fails, it is put back. A failed
write, a backup that cannot be made or a failed final rename all leave
the old file exactly as it was and no temporary file behind, reported
as one failure. The server manifest is never backed up.

Also: the local file is read from its start rather than wherever the
stream was left, a short read fails the upload, and a zero-byte write
from the server no longer loops forever.

samples/network/smb_backup runs smb.c against an in-memory libsmb2
share and checks what the share holds after each case: new content in
place and the old kept beside it; no backup for destructive sync, a new
file or the manifest; the old file intact after a failed write, backup
or final rename; an empty upload; the configured subdirectory. Against
the old driver 13 checks fail.
2026-09-21 15:11:23 +02:00
LibretroAdmin 05f19857db s3: honour non-destructive sync by backing up before delete or overwrite
The S3 driver never read cloud_sync_destructive: a delete removed the
object for good and an upload overwrote it, so turning destructive
sync off did nothing for S3 users. WebDAV, Google Drive and SMB keep
the server's copy in deleted/ on delete, and WebDAV now on overwrite.

With the setting off, a delete or upload first copies the object to
deleted/<path>-<yymmdd-hhmmss>, the name WebDAV uses, with CopyObject
(a PUT on the new key naming the source in x-amz-copy-source), which
is server-side. The source is /<bucket>/<key> for both addressing
styles: a path-style endpoint's canonical URI already is that, a
virtual-hosted one gets the bucket in front. A 404 means there is
nothing to keep; any other failure, including a 200 carrying an
<Error> body, which CopyObject can answer, stops the delete or upload
rather than lose the copy. The server manifest is not backed up.

S3 requires every x-amz-* header to be signed, so the SigV4 code takes
an optional copy source and signs it in its sorted place. The
signature for a CopyObject request and for a plain GET was checked
against botocore's SigV4 for the same inputs and matches.

samples/network/s3_backup includes s3.c and plays the server request
by request: copy first, into deleted/, with the right source for
virtual-hosted and path-style endpoints and x-amz-copy-source signed;
404 not an error; a failed or <Error> copy stopping the operation with
one failure report; destructive sync and the manifest not copied.
Against the old driver 6 checks fail. Runs in CI Linux samples/network
under ASan+UBSan and TSan.
2026-09-21 14:52:37 +02:00
libretroadmin e138e37f55 Previews: the decoders recycle their blocks, a dropped picture's slot passes, and a paced check
Three things the 4K preview was doing wrong on Windows, none of them
visible to a decode oracle.

The shared plane and motion blocks are recycled rather than freed.
The working frame takes a fresh block for nearly every picture - the
one it holds is a reference's or a queued picture's by then - and a
4K block is twelve megabytes: on the C runtime MinGW builds against
that is a page-zeroed mapping from the kernel and back, twice per
picture, where the swap it replaced allocated nothing. A released
block goes on a short free list by length and the next of that length
comes from there, so the steady state allocates nothing again.

A picture passed over to catch up left the presentation index where
it was: the decoder returns 0 for it as it does for a picture held
back for reordering, and the container took it for one. From then on
every shown frame read its duration from the slot before its own. The
decoders say when a call passed a picture over and the container lets
its slot go by, as it does for a refused one.

The preview sample's oracle animated flat out, which decodes six
hundred 4K pictures twice and shows nothing about playback. PACE_HZ
polls it at a display's cadence, which is how the thumbnail is driven,
and reports the picture changes and the longest run of polls in which
the picture did not change - the stall a viewer sees.
2026-09-21 14:45:06 +02:00
LibretroAdmin 67e3aef27d webdav: back up the server's copy before an upload replaces it
With destructive sync off, a WebDAV delete moves the server's file
into deleted/, but an upload overwrote it in place: the setting
protected against the server's copy being removed and not against it
being replaced, which is the more likely way to lose a save (an older
or broken save from another device overwriting a good one). The code
carried a TODO/FIXME for exactly this. The local side already keeps
cloud_backups/ before a fetch replaces a local file.

An upload now copies the server's file to deleted/<path>-<yymmdd-
hhmmss>, the name a delete uses, before the PUT. COPY rather than
MOVE, so the server keeps its file until the PUT replaces it, and an
upload that fails after the backup leaves the server as it was. A 404
means there is nothing to back up yet; any other failure stops the
upload rather than overwrite without a backup. The COPY and the PUT
each get their own Digest retry. The server manifest is rewritten
every sync and is not backed up; its name moves to
cloud_sync_driver.h as CLOUD_SYNC_SERVER_MANIFEST so the driver can
recognise it.

Adds task_push_webdav_copy() beside task_push_webdav_move() in both
HTTP backends, sharing the Destination-header code.

samples/network/webdav_backup includes webdav.c and plays the server
request by request: backup before PUT, into deleted/ named as a delete
names it, 404 not an error, a failed backup stopping the upload with
one failure report, destructive sync and the manifest not backed up,
and a Digest retry each for COPY and PUT. Against the old driver 5
checks fail. Runs in CI Linux samples/network under ASan+UBSan and
TSan; the other WebDAV harnesses gain the new stub.
2026-09-21 14:40:01 +02:00
LibretroAdmin a513f2b3e3 samples/gfx: put the wrapper's overlay path under test
Both overlay passes skipped before they reached a single assertion:
the null driver has no overlay_interface, so the lane got its
textures and then stopped at "no load_textures". Everything the lane
describes - the page of borrowed textures, the switch that uploads
nothing, the disable-before-unload order - was never run in CI, and
neither was any of the wrapper code behind it.

So give the driver an overlay back end. It goes on video_null itself
and BEFORE the mode switch, because the wrapper decides at init
whether it has an overlay to offer from the driver it is about to
wrap: with one there, the threaded pass runs the real path, main
thread to command ring to the video thread's captured table, and
what the driver is told can be compared with what the frontend
asked for.

That comparison is what the pass now makes. Two of the checks are
against wrapper code nothing else in the tree touches, and both were
negative-tested:

  - an alpha set cannot be lost. The set is fire-and-forget: the
    value goes into an atomic and raises a flag the video thread
    clears with an acquire exchange BEFORE reading the values, so a
    set that lands mid-apply is applied whole next pass. The lane
    fires one from inside the apply - the driver's set_alpha runs on
    the video thread, after the clear and after image 0's value was
    read - and asserts it still arrives. Clearing the flag after the
    read instead loses it: "an alpha set from inside the apply was
    lost: the driver last saw 0.500, not 0.375".

  - the pack's textures are retired, not released. Unloading under
    the wrapper must NOT hand the driver a release, because the
    in-flight frame may still name the texture; it goes on the retire
    list and the video thread frees it once it has drawn the frame
    that carries it. Releasing inline instead fires "3 of the pack's
    textures were released on the main thread instead of being
    retired", and the named handles are then each asserted released
    once the frames have run.

The third check covers the page switch itself: the handles that
arrive are the frontend's own, in order, and the uploading load()
never runs. Forcing the fallback fires six lines, from "load_textures
refused page one" through "page two arrived as (0, 0), not (5002,
5003)".

With GFX_INSTRUMENT=1 the overlay baseline reports real numbers for
the first time - 6 loads, 6 unloads for 4 pages over 2 passes - where
it used to print zeroes, which also means the pre-existing bound on
uploads per page switch is finally being exercised.

Verified: make check clean on plain, address+undefined, thread and
GFX_INSTRUMENT=1 builds, and over 25 cycles; the harness TU clean
under -Wall -Wextra -Werror on the tree's own compile line; all three
negative tests fail as quoted.

The harness now has no skipped lane left on the null driver.
2026-09-21 14:39:40 +02:00
LibretroAdmin 6aa1b1b779 icloud_drive: report every request exactly once
Cloud sync counts its outstanding requests and waits for each one to
report, so a driver must call the completion handler exactly once per
request. The iCloud Drive driver broke that both ways:

- a read whose local destination could not be created never called
  it, so the sync waited forever;
- a read whose cloud file existed but could not be loaded, and an
  upload whose local file could not be read, reported failure and then
  carried on and reported again, decrementing the outstanding count a
  second time.

It also treated an unavailable iCloud container as "file not
present": a read reported success with no file, which cloud sync now
takes as the server not having the file and drops from the server
manifest, and a delete reported success. Nothing was checked, so all
three now report failure. A short write of the downloaded data is a
failure too.

samples/network/icloud_drive includes icloud_drive.m and redirects
the container to a temporary directory by overriding
URLForUbiquityContainerIdentifier: in a category, so the driver runs
unchanged; a nil answer stands for iCloud being unavailable. It checks
that every request reports once with the right result, for the paths
above and the ordinary read, missing file, upload and delete. Against
the old driver 8 checks fail. Runs in CI macOS under ASan+UBSan.
2026-09-21 14:18:12 +02:00
LibretroAdmin 9867add40b webdav: retry a Digest challenge once, and rewind a retried upload
Every WebDAV request callback retried when the server answered 401
with a Digest challenge, and nothing limited how often. That is right
for a stale nonce, but with a wrong password the server answers every
retry with a new challenge, so the first request of a sync - the
OPTIONS probe - looped forever and the sync never reported anything.
Allow one retry per request (webdav_retry_auth()); a second challenge
now fails the request with an error naming the credentials. Each
MKCOL in a directory walk counts as its own request.

A retried PUT also sent garbage. webdav_do_update() reads the upload
into memory, leaving the file at its end, and the retry read it again
from there: it got nothing and sent a buffer of the right length full
of uninitialised memory, replacing the save on the server whenever a
nonce expired mid-upload. Rewind before reading, and fail the upload
on a short read.

samples/network/webdav_reauth includes webdav.c and plays the server
for OPTIONS, GET, PUT, DELETE, MOVE and MKCOL: a server that keeps
rejecting the credentials must get exactly one retry and one reported
failure, a single stale-nonce challenge must still be retried and
succeed, and every PUT must carry the file's bytes. Against the old
code every request type loops and every retried PUT sends the wrong
bytes. Runs in CI Linux samples/network under ASan+UBSan and TSan.
2026-09-21 14:18:10 +02:00
libretroadmin fa823c3052 rh264: a picture has one home - reference, output and working frame share it
A finished frame picture was copied plane by plane into the DPB when
it was a reference and swapped into an output slot when it was to be
shown, and its motion-vector grid was copied into the slot twice, once
per parity. At 4K that is twelve megabytes of samples and two grids
per reference picture, and five per cent of the decode was memcpy.

The plane block and the working grid now carry a reference count in a
header ahead of the data. A frame picture's block is shared into the
DPB slot and the output slot rather than copied, its grid is shared
into the slot for both parities, and the working frame takes fresh
blocks on its way into the next picture only while the ones it holds
are still someone else's. Field pairs, which fill a block a field at
a time, keep the copy, into grids and samples the frame owns - a
field can never land in a block a queued picture still reads. The
lossless oracle holds every case byte-exact against ffmpeg, plain and
under the sanitizers, with every block released by its last holder.

Beyond the memcpy this is the shape frame threading needs: a picture
that lives in one place for its whole life is one a row counter can
sit on, and one a picture decoding on another thread can wait for a
row at a time. Under the copy model a reference did not exist until
the copy at the finish, and there was nothing to wait on.
2026-09-21 14:18:06 +02:00
LibretroAdmin 9e083a1d9f samples/gfx: give the harness driver a texture back end
The surface lane stopped at "driver made no texture" on the null
driver, which has no load_texture at all, and returned before the
three things it exists to check: that a streaming surface loads ONE
texture and keeps it across every frame, that each queued submit is
released exactly once, and that freeing a surface with a frame still
in flight releases its texture rather than leaking it. Everything
after the early return - the 30-submit loop, the in-place handle
check, the whole direct half, both instrument baselines - was
unreachable in CI.

So lend the driver a back end. Handles carry a tag and are tracked
one by one, so the checks name the surface's own texture and are not
disturbed by what else the frontend uploads through the same poke
(the menu's framebuffer comes through it). It goes on per video mode,
because each switch reinits the driver and rebuilds the poke from the
real one: threaded it sits on the wrapped driver, so the worker's
uploads run through it; direct, on the driver's own poke.

Two checks the back end makes possible are new, and are the ones that
have teeth:

  - the surface's texture is named and asserted released after a free
    with a frame in flight. ASan cannot see this leak: a texture
    handle is a driver-side resource, not an allocation.
  - the load count is bounded at one for the whole run, in both
    halves.

Both were negative-tested. Dropping the unload from the dying branch
of gfx_surface_done() fires the first ("1 textures still loaded after
the threaded half (1 loads, 0 unloads)"); clearing can_update on
every completion fires the second ("31 texture loads for one
streaming surface") along with the pre-existing in-place check, which
had never once run.

With GFX_INSTRUMENT=1 the lane now reports a real budget for this
driver instead of nothing:

  surface, threaded: 1 loads, 31 updates, 32 posts (0 allocated), 0 copies
  surface, direct:   1 loads, 10 updates, 1 unloads, 0 posts, 0 copies

Verified: make check clean on plain, address+undefined, thread and
GFX_INSTRUMENT=1 builds; the harness TU clean under -Wall -Wextra
-Werror on the tree's own compile line; both negative tests fail as
quoted.

The two overlay lanes still skip: they need an overlay interface with
load_textures, which the null driver also lacks. That is a separate
piece.
2026-09-21 14:07:29 +02:00
LibretroAdmin 2bb0215ddd webdav: check callback state before using it
webdav_read_cb(), webdav_update_cb(), webdav_delete_cb() and
webdav_backup_cb() dereferenced webdav_cb_st->path to log a failure,
and read and delete again on the reauth path, before the NULL check
that came further down. Check once at the top and return, as
webdav_stat_cb() already does, and drop the checks that can no longer
fail.
2026-09-21 13:39:05 +02:00
LibretroAdmin d997fe013b cloudsync: stop refetching files the server does not have
A file listed in the server manifest but missing on the server (such
as one uploaded under a name cut short at '#' before the WebDAV path
encoding fix) came back from the driver as success with no file.
task_cloud_sync_fetch_cb() logged that as "Failed to write file from
server", kept the entry in the server manifest and failed the sync,
so the same fetch failed on every sync after.

That result was ambiguous in the drivers too: WebDAV and Google Drive
also returned success with no file when the download could not be
written locally, and when the file on the server was empty. Both now
hand back an open RFILE for any found file, empty included, and
report a local open or short write as a failure. Success with no file
now means only "not on the server", which is what SMB, S3 and
iCloud Drive already meant by it.

task_cloud_sync_fetch_cb() then drops such an entry from the server
manifest and marks it for upload, leaving the local file and local
manifest alone: if this device has the file, the next sync sees it
as new and uploads it, which repairs the server.

Since an empty server manifest now arrives as an empty RFILE rather
than none, treat an empty one as no manifest, as an empty body was
before; rjson rejects empty input.
2026-09-21 13:39:01 +02:00
LibretroAdmin 182c6cb6c6 cloudsync: write manifest.local through a temporary file
The local manifest was rewritten in place, so a crash, a full disk or
a power cut part way through left it empty or truncated. The last fix
makes the next sync discard such a file, but that costs the sync its
record of the previous state, and every differing file then goes
through conflict resolution.

Write manifest.local.tmp and rename it over manifest.local only once
it is complete. filestream_rename() replaces an existing destination
on every platform, including Windows. If the write or the rename
fails, the temporary file is removed and the old manifest is left in
place, which is the safe outcome task_cloud_sync_commit_local_manifest()
already describes: the next sync re-examines the same files.

libretro VFS has no fsync, so on filesystems that do not order a
rename after the data it replaces, a power cut can still lose the
new manifest; the old one, or the discard path, covers that.
2026-09-21 13:38:53 +02:00
LibretroAdmin 7d158390ae s3: log the transport failure reason
task_http passes the failure reason (stage plus code, such as
"ssl_connect_failed (-0x7780)") to every transfer callback as err,
but every S3 callback discarded it with (void)err, so a transport
failure was logged only as "HTTP -1". Pass err through to
s3_log_http_failure() and s3_multipart_fail() and print it when there
is no status, the way webdav_log_http_failure() already does; the
"Did not get HTTP data" warnings include it too.
2026-09-21 13:30:59 +02:00
LibretroAdmin 32d5dbe1d7 net_http: report the OS socket error for socket stages
net_http_failure() gave a code only for the TLS stages, so a plain
connection refused, timed-out connect or failed send reached the log
as "socket_connect_failed" with nothing to say why.

Record errno (WSAGetLastError on Windows) for socket_create_failed,
socket_connect_failed and socket_send_failed, right after the failing
call and only for the first failing stage, as the TLS code already is.
task_http prints positive codes as "(111)", so the log line becomes
"Download failed: socket_connect_failed (111)."

socket_connect_with_timeout() now leaves the reason in errno too: a
non-blocking connect reports a refused or unreachable peer through
SO_ERROR, and a timeout through nothing at all, so both are copied
over (SO_ERROR value, or ETIMEDOUT/WSAETIMEDOUT). This is limited to
platforms that keep socket errors in errno or WSAGetLastError; PS3,
Vita, Wii U, GameCube/Wii and 3DS keep theirs elsewhere and still
report 0 (no code).

Receive failures are left without a code: a peer closing the
connection sets no errno, so any value there would be stale.
2026-09-21 13:30:55 +02:00
libretroadmin 58ddb6118c rh264: a row is deblocked once the row below has read it
The decoder reconstructed a picture whole and then filtered it whole,
one sweep over every macroblock at the end. Each slice decoder now
calls a hook as it completes a macroblock row, and the row above it is
filtered then: intra prediction reads unfiltered neighbours, and by
the time row r is complete it has read row r-1's last line, so the
filter can have it. Macroblock order is what it was, so the picture
is the whole-picture pass's, macroblock for macroblock - the lossless
oracle holds every case byte-exact against ffmpeg - and the row is
filtered while it is still in cache. Pair-scanned and field pictures
keep the whole pass, as does whatever a second slice leaves unfiltered.

What the boundary strengths read matters here: pic_mvg is the
picture's motion-vector grid, folded from the working grid once each
slice is done, and mid-slice it still holds the last picture's. The
hook reads the working grid; the finish reads pic_mvg, which by then
is the same.

A row that is filtered is final, and rows become final one at a time
from the top: that is what a picture decoding on another thread will
wait on, and the reason for this step.
2026-09-21 13:29:59 +02:00
LibretroAdmin 60766518d7 Threaded video: pin the drain that keeps frame.lock uncontended
samples/gfx/threaded_video gains a menu-texture lane. The harness driver
had no set_texture_frame at all, so nothing in it exercised the handoff
or the three texture lanes that skip; this one supplies the receiving
end and a slow frame() to make the timing visible, and asserts what
actually holds: with the menu texture enabled, video_driver_frame()
returns with the ring drained, and the driver is handed one push's
pixels rather than a mixture. Removing that drain fails the lane on 12
of 12 menu frames.

That drain is also the correction to what the previous commit claimed.
It said a main-thread set_texture_frame() had been parked on a frame and
its swap on every RGUI frame. It had not: the drain leaves the worker
idle before the next iteration's push, so the lock was never contended
on that path. Narrowing it still stands - set_texture_enable() and
apply_state_changes() arrive on a user action with no push behind them
and were waiting out a render - but the comments now say what is true
instead, and name the drain the lane depends on.

The lane checks the tear case too, cheaply, though that half has no
demonstrated teeth: with the drain removed and the handoff unlocked, the
window between the push's memcpy and the driver's read stayed too narrow
to reproduce here.
2026-09-21 13:28:29 +02:00
Chris BradelandLibretroAdmin 4ae32cee45 Fix WebDAV cloud sync crashes and encoded save paths (#19598)
* cloudsync: reject invalid manifests

Check the JSON parser result before sorting a manifest. Discard partial manifests and stop the sync with an error instead of dereferencing an incomplete entry.

* webdav: handle missing files and encode sync paths

Treat HTTP 404 as a missing file without writing the server's error page to disk. Encode sync paths separately from the WebDAV base URL so filenames containing '#' are sent as filenames, not fragments.

* webdav: keep '/' as separator in deleted/ backup path

fill_pathname_join_special(dest, "deleted", path) appends
PATH_DEFAULT_SLASH when the directory has no slash of its own. On
Windows that is a backslash, which net_http_urlencode then sends as
%5C, so every backup MOVE targeted "deleted%5C<path>" instead of the
deleted/ collection. Pass "deleted/" so the existing '/' is kept.

* cloudsync: recover from an invalid local manifest

manifest.local is rewritten in place, so a crash, full disk or power
loss during the write can leave it empty or truncated. rjson rejects
empty input, so with the new manifest validation every later sync
stopped with "Invalid manifest" and the only way out was switching
sync drivers, which is what deletes the file.

Log a warning, delete the bad local manifest and continue as a first
sync. With no local manifest, files that differ between local and
server go through conflict resolution instead of being overwritten.
An invalid server manifest is still fatal.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-09-21 13:22:18 +02:00
LibretroAdmin cc9f8777dc Threaded video: hold frame.lock for the handoff only
The video thread takes frame.lock for the thread_update_driver_state()
that applies the menu texture and apply_state_changes, and releases it
before the render. A driver takes the texture's pixels inside its own
set_texture_frame(), uploading or copying them there rather than keeping
the pointer, so the staging buffer is free as soon as the update returns
and the render needs nothing the lock guards.

That is what a main-thread set_texture_frame() now waits for: the
handoff, not a frame and its swap behind it. RGUI pushes a texture every
frame the menu is up, so with the software menu open the main thread was
parked on a full render and vsync each frame - hardest on the handhelds
where RGUI is the default. The repeat path drops the lock too: it
presents a retained frame and touches no menu texture.

samples/gfx/threaded_video passes under ASan+UBSan and under TSan, but
it does not cover this handoff: its null driver has no set_texture_frame,
so the video thread clears the update flag without ever reading the
buffer, and removing the lock outright still passes. Giving the harness
driver texture entry points would cover it, and would also un-skip the
three texture lanes that skip today.
2026-09-21 12:52:59 +02:00
libretroadmin 5929cdd3a6 gfx_thumbnail_preview: builds on MSYS2/MinGW, so the bench runs where the question is
The sample stopped at its first file on Windows: setenv and unsetenv
are POSIX, and the C runtime MinGW builds against has _putenv, where
"NAME=" removes the variable. One helper takes both. Every binary in
the sample also took -DHAVE_MMAP, which to the VFS and memmap means
POSIX mmap and sys/mman.h; without it they take the Win32 mapping
calls, the way the tree's own Windows build does, so the define is
set off Windows only. The windowed-open oracle reads /proc and asks
madvise for huge pages, and is built and run on Linux alone.

With that the decoders, the bench, and the catch-up and blit-band
oracles cross-build as PE, which is where the thread-scaling numbers
have to come from: the hosts that produced the first table had one
core.
2026-09-21 12:40:57 +02:00
libretroadmin a9f8b1cc86 gfx_thumbnail_preview: the preview decoders' throughput, in numbers
Whether a 4K preview is fast enough has been answered by watching it.
preview_decode_bench decodes each file given through the stream the
thumbnail uses, at one, two and four threads, and prints frames per
second, the megapixels per second that implies, and the speed-up over
one thread; with --catchup it runs with the stream told it is behind,
so the gain from dropping droppable pictures sits beside the gain from
threads. `make bench` runs it over the fixtures. It is not a test - it
exits 0 unless a decode fails - and it is never built by check, since
numbers from a sanitized build mean nothing.

On one core, single-threaded: VP9 at 300 Mpix/s, H.264 at 190, HEVC
at 85, against the 250 a 4K stream at 30 fps needs. The thread columns
say nothing on a one-core host and everything on a real one: HEVC's
wavefront rows and VP9's tile columns should scale, and H.264 has no
intra-frame parallelism to scale with.
2026-09-21 12:27:58 +02:00
libretroadmin 3db44ec087 Animated previews: behind the clock, the stream drops what nothing references
A preview whose decode is slower than its frame rate showed every
frame late - a 4K H.264 stream played in slow motion - because the
scheduler noticed it had fallen behind and did nothing about it. It
now tells the stream to catch up, and clears that the moment a frame
lands on time. Catching up means passing over the pictures nothing
predicts from: H.264's non-reference pictures (nal_ref_idc 0), and
HEVC's sub-layer non-reference pictures in the highest sub-layer,
which needs sps_max_sub_layers_minus1 kept from the SPS the parser
read and discarded. Each is consumed without being decoded and the
decode call returns 0, the contract the container already keeps for
a picture held back for reordering, so its presentation slot passes
and the clock catches up. What is still shown decodes exactly as it
would have: a B-frame-heavy 4K stream does about a third of the work
while behind, and no setting, since this only ever engages once late.
WebM, APNG and WebP reference every frame and take no action.

samples/gfx/gfx_thumbnail_preview gains two fixtures with three
non-reference B-frames between references and no pyramid, and an
oracle that decodes each twice: every frame the catch-up run shows
must be a frame of the full run, byte for byte and in order, or a
dropped picture was referenced after all, and it must show fewer.
H.264 shows 18 of 60 and HEVC 17 of 60 - the I- and P-frames exactly.

The oracle that already lived there animated a fixed twelve frames
and asserted a texture had arrived; with two more fixtures ahead of
hevc_wpp.mp4 in the glob, their jobs were still draining when it
opened and under a sanitizer its first picture missed the window, on
this tree and on master alike. The twelve frames stay as the
measurement; the arrival is now waited for.
2026-09-21 12:13:52 +02:00
LibretroAdmin 3d0e133db1 task_queue: push a task's message with property_lock released
The message is built from the task's title, progress and flags under the
lock that guards them, and the push that carries it to the frontend runs
with that lock released. msg_push() inserts into the widget queue and, on
a build with accessibility enabled, reaches a speak backend that forks;
a worker's task_set_progress() has no business waiting behind that.

The push therefore carries the formatted string and nothing the lock
guards, which makes the title and mute tests in
runloop_task_msg_queue_push() the caller's to make: re-reading the title
there would be reading a pointer a worker is free to free, so they go.
The widgets' one remaining read, ALTERNATIVE_LOOK, is set while the task
is built and never changed after, and says so where it is read.
progress_cb() stays under the lock - it reads the task's progress and
finished flag, and pokes the display server rather than going through the
message path.

samples/tasks/progress_push holds the contract. msg_push() there calls
task_get_progress(), which takes that same non-recursive lock, so a push
made under it deadlocks and a watchdog names the phase; the worker churns
the title free-then-set throughout, so a push reading task->title is a
race TSan reports against task_free_title(). Both halves were checked by
injecting each defect. Clean under ASan, ASan+UBSan and TSan; the thread
read audit stays at 0 findings.
2026-09-21 12:11:30 +02:00
LibretroAdmin 262472efa3 samples/gfx: a lane that asserts the filter barrier's contract
filter_lifecycle checks a processed frame's pixels, which reads like
coverage for the fan-out/join and is not: deleting the join outright
leaves it passing, because its slices finish while the verification loop
is still reading them. It also asks for four worker threads and gets
one - Darken and ntsc are the only builtin filters whose
query_num_threads returns the count it was given, every other one pins
filt->threads to 1 - so nothing in samples/gfx has been running the
pool at all.

This lane includes video_filter.c the way samples/audio/mixer_lock
includes audio_driver.c, creates a real pool over Darken, and swaps
filt->impl for slices that sleep on a skewed schedule. On return from
rarch_softfilter_process() it asserts the contract itself: every slice
ran in this generation, outstanding is zero, and no worker still holds a
packet. A watchdog turns a stranded join into a named failure naming the
phase rather than a job that hangs to its limit.

It names four injected defects: the join deleted, the count armed after
the fan-out, a worker that decrements without notifying, and a worker
that tests its flag outside the wait window - that last one a race,
caught on 5 of 5 runs by the no-sleep loop. Clean under ASan, ASan with
UBSan, and TSan. Run by hand; no workflow lists it.
2026-09-21 11:42:16 +02:00
LibretroAdmin faaf0fdec5 video_filter: fan out and join on eventcounts
Each worker parks on an eventcount and takes its packet from a release
store, so the fan-out is a store and a notify per worker with no lock
either side. The join waits on one shared eventcount against an atomic
count armed before any worker is woken, and only the worker that takes
that count to zero notifies, so a frame costs one wakeup rather than one
per thread. Workers test their go flag inside the wait window, which is
what lets a fan-out publishing concurrently release them.

Counted with strace over 1000 frames of the same shape: futex calls drop
from 8919-10574 to 5113-5507 at two threads, 11928-12503 to 7596-7863 at
three, and 12569-14759 to 9460-9856 at four. Wall time is not reported:
on a two-core box the run-to-run spread is wider than the effect.

samples/gfx/filter_lifecycle links retro_eventcount.c, which
video_filter.c now needs.
2026-09-21 11:17:53 +02:00
LibretroAdmin 7269984855 Pace the framebuffer drivers on an eventcount
dispmanx, sunxi and the videocore context carry their pageflip state as
an atomic and wait on retro_eventcount. Each waiter opens its wait
window before the work that a vsync callback answers - the swap, or the
flip test - so a callback landing from there on either shows up in the
re-test or makes the commit return at once, and the frame is never held
for a period it already had. The callbacks themselves now take nothing:
dispmanx's runs inside a VideoCore display callback and sunxi's inside a
thread that should be back in its ioctl.

sunxi's nextPage handoff rides the release store that clears
pageflip_pending, so the page update_main() blits into is published
rather than ordered by coincidence. The videocore context also clears
vsync_callback_set when it stops the callbacks, so raising the swapchain
depth and lowering it again re-arms them instead of leaving a waiter
with nothing to wake it.
2026-09-21 11:01:44 +02:00
LibretroAdmin 1342fdff19 audio_mixer: carry a voice's type as an atomic
A voice publishes its type with a release store when it is claimed and
clears it the same way once released, so the mix loops pass over an idle
voice without taking its lock and a fully idle mixer takes none at all.
Both loops read type again under the lock, where an idle voice lands on
its NONE case, and audio_mixer_play()'s slot search keeps its locked
re-check.

audio_mixer_has_float_voices() and audio_mixer_has_s16_voices() read the
same field, which makes their lockless scans well defined; play_s16()
sets is_s16 ahead of the store that publishes the voice, so those two
see the format flag of any voice they see claimed.
2026-09-21 10:28:17 +02:00
LibretroAdmin d0cb8317dd Take four locks off fast paths
android_display_has_focus() returns one acquire load of an atomic the
focus commands publish with a release store, which is all the ordering
there is to carry; the input poll reaches it every frame. A dispmanx
page carries its used flag as an atomic, claimed by the frame thread and
released in the vsync callback - the form the free-page scan already
reads it in - so a page needs no mutex of its own.

task_queue keeps one lock over the running queue: running_lock guards
every walk and every splice of it, task_queue_remove() included, and
task_queue_push()'s blocking-task scan takes it like the push_running()
that follows. librsound calls the client's audio callback directly;
rsd_callback_lock() and rsd_callback_unlock() go with the lock they
were the only users of.
2026-09-21 10:28:13 +02:00
LibretroAdmin 6f30de6a6f CI: samples/gfx/wayland_drm_lease gets a lane that installs wayland
The sample was running in the leftover job, which installs only
build-essential, so wayland-scanner and the client/server libraries
were missing and the run failed at protocol generation. It belongs in
samples/gfx alongside the other display-server lanes; naming it there
also takes it out of the leftover job, which skips whatever a workflow
claims.
2026-09-21 10:28:09 +02:00
github-actions a8e400bf35 Fetch translations from Crowdin 2026-09-21 00:27:29 +00:00
libretroadmin 0558afa6f3 CI: the Vulkan harness leg holds the surface budgets
No job built the tree with GFX_INSTRUMENT, so the harness lanes that
print what a run cost and hold the budgets - one texture per streaming
surface, no allocation per post, no copy, a 4K submit that stays a
descriptor hand-off - compiled those checks out on every push. The
numbers existed only on whoever ran them by hand. The plain Vulkan
validation leg builds instrumented now and passes the flag to the
harness; it is the one leg for it, since the timings mean nothing
under a sanitizer and the counters change no behaviour.
2026-09-21 00:16:44 +02:00
libretroadmin e3d800c625 Surfaces: the 4K number the plan asked for, and the counters that were missing it
The surface plan holds that nothing lands on architectural
cleanliness alone, and no gate in it had ever been run. The harness
gains a lane that streams a 3840x2160 surface through the wrapper for
sixty frames and takes the numbers: the time a submit costs the main
thread and the frames it takes for the slot to come back, as
p50/p95/p99, with the budgets asserted - one texture, no allocation
per post, no copy, and a submit that stays a descriptor hand-off. It
is thread CPU time, not wall clock: on a one-core host the video
thread's 33 MB upload preempts the main thread inside the submit, and
the clock on the wall charged that to the hand-off.

The counters were missing half the work. A load or update posted
through the asynchronous list never passed through the synchronous
entry point where the counter sat, so every upload the surfaces make
- the point of them - went uncounted, and the baselines printed so
far undercounted by that much. They are counted on the thread that
runs them now. With the true figures the surface lane's own
assertion, two loads for two surfaces, turned out to have passed only
on the undercount: the mode switch between its halves reinits video
and loads the menu's textures through the same counters. Each half is
measured on its own now, with the reinit outside both.
2026-09-21 00:10:37 +02:00
LibretroAdmin 6ef5a6e8f8 CRT: pair the super resolution dropdown's label with its string
Without the row in intl/msg_hash_lbl.h the label resolves to "null",
so the deferred push matches nothing and stays on its default, which
is a file browser: OK on CRT Super Resolution opened Select File
instead of the list.
2026-09-21 00:03:05 +02:00
LibretroAdmin 59ae39cb0f CRT: the refresh-only mode is not about LCDs
What it does is hold the display's own line count and move only the
rate, which fits any fixed-pixel display - an OLED as much as an LCD.
The label named a panel technology instead, so an OLED owner would
read past the one mode that serves them. It now says what it does,
and matches the wording already in the sublabel.

The ini keeps monitor lcd: that is switchres's name for the preset
and it is in people's configuration files.
2026-09-20 23:41:16 +02:00
LibretroAdmin 5a41661b0f Wayland: name the route when a connector is offered
The log said the display server cannot apply a modeline, and
separately which connectors are up for lease, and left the reader to
join them. It now names the setting that takes the offer, and only
where there is one to take.
2026-09-20 23:36:35 +02:00
LibretroAdmin 8582eacdbc Wayland: one place speaks the lease protocol
The display server reported leasable connectors with its own copy of
the listeners, and the lease module then grew the same ones to take a
lease. Reporting moves to the module, on the connection the display
server already has, so the protocol is spoken once - a sequencing
mistake has one place to be made rather than two.

samples/gfx/wayland_drm_lease covers both sides already and is
unchanged, which is what says the reporting still behaves.
2026-09-20 23:34:37 +02:00
LibretroAdmin d8221280b4 Wayland: say when the compositor takes the lease back
A revoked lease reaches the DRM context as a page flip that will not
queue, and looked like any other flip failure. The question is asked
there, where it already went wrong, rather than on every frame: the
lease reads its connection non-blocking - dispatch what is queued,
then read the socket only when it has something - and the context
says so once.

samples/gfx/wayland_drm_lease revokes a live lease from the
compositor's own event loop, since its resources belong to that
thread, and waits on the client side with the same non-blocking call
a caller would use.
2026-09-20 23:32:07 +02:00
LibretroAdmin 373d021c16 thread_read_audit: the Windows entries are reachable, and clean
A mingw cross build puts them in a binary the audit can walk - WASAPI,
WDM-KS, DirectSound, XAudio, ASIO, raw input, the DirectInput
enumeration job, the modeline resync and the D3D11 and D3D12 frame
contexts, none of which a Linux build contains. It audits 62 entries
with no findings, and the note now carries the two commands.

Between that and a wide Linux build's 76, the entries neither can reach
come down from 52 to 27: Apple, Android, emscripten, the consoles, Steam
and a few drivers whose library is not packaged here. Those still want
their own toolchain.
2026-09-20 23:29:02 +02:00
LibretroAdmin 9d3537725c Wayland: scan out on a leased DRM connector
A Wayland client cannot put a modeline on the wire, and a card opened
inside a session cannot modeset because the compositor holds master.
A DRM lease settles both: the compositor gives up a connector, its
CRTC and a plane, and hands back a descriptor that is master for
exactly those, on which an atomic modeset carries a full
drmModeModeInfo.

With that descriptor the existing path serves it unchanged. The DRM
context scans out on it - resources on a lease are only the leased
objects, so the head is already chosen - and the display server is
the KMS one, which applies the engine's modelines to that connector
the way it does on a bare KMS session. A leased connector is one the
compositor has given up, so the desktop keeps the heads it did not
lease: this is a second output, not a takeover.

Reached by setting the video context driver to the DRM one inside a
Wayland session; without a lease that context cannot come up there at
all, so nothing changes for anyone else. The monitor index counts
through the offered connectors as it does elsewhere.

samples/gfx/wayland_drm_lease grants leases now as well as
advertising them, so the request, the descriptor, the index and the
refusal are all exercised against libwayland rather than reasoned
about.

Revocation is not handled: a compositor may take the lease back, and
noticing its finished event needs the connection's descriptor in the
poll the DRM context runs for flip events. Until then a revoked lease
shows up as DRM calls failing.
2026-09-20 23:26:50 +02:00
LibretroAdmin 2db857035c CRT: a dropdown for CRT Super Resolution
Left and right were the only way through NATIVE, DYNAMIC, 1920, 2560
and 3840, so the setting takes OK to a list like the rest of them.

The values are widths, not an index - 0 and 1 mean native and best
fit - which the generic uint dropdown cannot express, since it walks
min to max by step and writes the index back. So the list is its own
DISPLAYLIST_DROPDOWN_LIST_CRT_SUPER_RESOLUTION beside the resolution
one, each row carrying its value, with the entry index only ordering
them. The stored value is unchanged, and left and right still work.

No ST_UI_TYPE: the Qt companion's uint combobox populates from min to
max, which are zero on this row, so the hint would give the desktop
menu an empty box.
2026-09-20 23:14:52 +02:00
LibretroAdmin 25aea68dc4 CRT: the sublabel describes what the setting does
"For CRT displays only" is not true of the engine: the EDID and LCD
modes take their limits from whatever the display reports, and a
panel gets the content's refresh rate the same way a tube does. The
sublabel now says what the setting generates and which modes are for
which display.

English source only; the locales follow through Crowdin.
2026-09-20 23:06:35 +02:00
LibretroAdmin 65f82f7e12 samples/audio: a suite for the PS3 driver's wait and teardown
Two properties with a history, one harness, both against the shipping
translation unit rather than a model of it.

ps3_audio_wait_block() has to hold cond_lock across sysLwCondWait():
that call wants the lwcond's own mutex and fails with EPERM without it,
which is what turned the write's bounded waits into microseconds of
spinning that returned zero and dropped the audio on a full ring. The
device mock refuses on the same precondition and counts it, so the check
is a run of blocking writes longer than the ring: they must all land,
with no refusal counted. Removing the lock again fails at round 16.

quit_thread is written by the thread tearing the driver down and read by
the output thread's loop every pass, with no lock either side. Against
the volatile bool it used to be, ThreadSanitizer reports the read at
ps3_audio.c:89 against the write at :302; it is clean as an atomic.

tools/audio-suite-check.sh finds this for ps3_audio.c on its own, so the
driver's pre-commit check covers it without a workflow change.
2026-09-20 23:04:56 +02:00
LibretroAdmin 64115bd26e CRT: name the display-matching modes, and add refresh-only
"INI" and "EDID" said nothing about what they do, and the one thing
the engine can do for a panel - hold its resolution and move only the
rate - had no entry at all. The dropdown now reads Custom
(switchres.ini), Match Display (EDID) and Match Refresh Only (LCD);
the strings are literals, so no locale is disturbed.

The new mode selects the lcd preset, whose refresh band is seeded
from the display's EDID once the server is bound - its own default is
the desktop rate either side by one, which switches nothing - and the
preset is filled again from that band, since setting the monitor
filled it from the old one. A display that states no band keeps the
desktop rate and says so.

The value is appended, so a saved crt_switch_resolution keeps its
meaning.

samples/gfx/crt_switch_modeline covers it: 70.086 Hz content on a
panel reporting 50-75 Hz lands at [email protected], the panel's own
line count at the content's rate.
2026-09-20 23:04:39 +02:00
LibretroAdmin 4a5863ebb7 samples/audio: a suite for the windowed feeder's dead flag
The flag became an atomic with nothing to reproduce the race, and no
existing suite could: mixer_lock stubs the mixer, and the two mixer
suites under libretro-common are the wrong layer for a tasks file.

audio_mixer destroys a voice from the mixer thread as well as the
caller's, so the borrow's release callback sets dead on one thread while
the feeder task's handler reads it on another, with no lock either side.
This drives that pair directly - including the task's translation unit to
reach them, as samples/audio/psp_ring does for the console drivers, since
both are static. The handler and the release are the shipping ones.

The stubs alongside keep a pass cheap: with no transfer window the
handler bails on its first tick, and the classic loader it falls back to
gets no task, so a round costs the read of dead and little else. The
codec defines matter - without one of them the whole windowed path is
compiled out, and an earlier measurement of this seam was taken against
a build that did not contain it.

Against the volatile bool the field used to be, ThreadSanitizer reports
the read at task_audio_mixer.c:1265 against the write at 1225. Clean as
an atomic, and clean under ASan with leak detection and UBSan.
2026-09-20 22:54:17 +02:00
LibretroAdmin 3958a263ab samples/gfx: pin the EDID preset against the content's refresh
The engine is display-agnostic and the EDID preset takes its limits
from whatever the display reports, so a panel that says it syncs to
75 Hz is handed the content's rate rather than the desktop's. The
case builds a 50-75 Hz block with the tree's own generator, so the
block the parser reads is the one the writer emits, and asks for
70.086 Hz content.

Nothing in the menu says this works on a display that is not a CRT,
which is exactly why it wants a lane: with the display reporting no
EDID the preset falls back to generic_15 and the applied mode is not
at the content's rate.
2026-09-20 22:52:48 +02:00
LibretroAdmin 8bce16cb3c samples/gfx: pin what a rebound display server is handed
Content loading tears the display server down and builds it again
with the engine alive across it. The rebind does not re-enumerate and
an already flushed mode is not flushed again, so the new instance is
asked to switch to a mode it was never handed.

Every backend is fine with that for its own reason - X11 finds the
RRMode still in the X server, KMS reads the timing out of the struct,
the Win32 backends registered it with the driver - and all of them
rely on the mode carrying its whole timing at set(). That is what the
new case pins, together with the switch going through, so a backend
that grew a dependency on its own add() shows up here rather than as
a black screen after a core loads.
2026-09-20 22:46:41 +02:00
LibretroAdmin 90d830f7b1 samples/audio: a lane for the consumer leaving under a parked producer
pipe_consumer_gone became an atomic without anything to reproduce the
race it was: the suite's ThreadSanitizer lanes passed the same either
way, because none of them has the consumer leave its loop while a
producer is still waiting on it.

CONSUMER_EXIT drives exactly that. The producer fills until it has to
wait, the device then refuses every write, and audio_thread_write()
clears alive under thr->lock and the wrapper leaves its loop - reading
alive under the same lock, but publishing pipe_consumer_gone under none,
to a producer reading it under none either. Nothing joins between the
two, because the producer is parked rather than tearing the driver down.

Against the volatile bool the field used to be, ThreadSanitizer reports
the pair: the read in the wait at audio_driver.c:5596 on the submitting
thread against the write in audio_driver_pipeline_consumer_exit() on the
wrapper thread. It is clean as an atomic.

The lane also asserts the producer comes back at all, since a consumer
that has gone is one it must stop waiting for.
2026-09-20 22:44:48 +02:00
LibretroAdmin ede06252c0 Wayland: report the connectors offered for DRM lease
A Wayland client cannot put a modeline on the wire: every modeline op
in dispserv_wl.c is NULL, and the protocol that changes a mode carries
width, height and a refresh rate, nothing that can express a porch.
A DRM lease hands a connector over whole, which is the one route that
can, so the display server now says at init whether the compositor
offers one and which.

Discovery only. The lease is not taken and no scanout changes; what
the log answers is whether a given compositor could ever serve a CRT,
which today nothing tells a user.

The non-master fd the compositor sends for device picking is closed
as it arrives, and the device is released once the connectors are
read.

drm-lease-v1.xml is bundled, and generate_wayland_protos.sh falls
back to the bundled copy per protocol, so a wayland-protocols older
than the one a protocol first shipped in no longer fails the build.

samples/gfx/wayland_drm_lease runs the real display server against a
compositor built on libwayland-server: the mistakes this code can
make are protocol errors, which kill the connection at startup and
are invisible to a compile. Run by hand:

   make -C samples/gfx/wayland_drm_lease check
2026-09-20 22:42:41 +02:00
LibretroAdmin 7fcfa83218 metal: rotate the HDR composite for the slang path too
Under HDR the last slang pass is force-allocated as the composite's
input, so it renders with the unrotated mvp_last_pass like any other
pass that owns an RT. Pass the content rotation to the composite for
that source as well, so TATE content is rotated by the composite's UV
transform whether or not a shader chain is active.
2026-09-20 22:42:25 +02:00
LibretroAdmin 3944cf5b31 CRT: say the context lines once, not on every geometry change
crt_engine_init() runs on every geometry change, so the lines that
describe the context rather than the switch repeat for as long as a
core keeps changing mode. The context identity and the engine-alive
line now belong to the engine's start.

Vulkan direct-to-display says what it cannot do at bind, beside the
no-modeline-path line, which leaves the per-switch branch as the
apply alone.

samples/gfx/crt_switch_modeline covers both contexts over three
geometry changes.
2026-09-20 22:30:52 +02:00
LibretroAdmin 5a4fd86f73 CRT: say when the display server has no modeline path
A server whose modeline ops are all NULL - Wayland - now says so
once, when the engine binds it, naming itself, and the per-switch
apply is skipped rather than reported as a failure. The mode list is
still generated, so the aspect and the timing the consumer publishes
are unchanged.

A server that does have ops and whose set() fails still reports it.

samples/gfx/crt_switch_modeline links the consumer and the engine
and stubs the display server, which is the only way to stand up a
server with no modeline ops on demand. It also pins the connector
the EDID install hint names. Run by hand:

   make -C samples/gfx/crt_switch_modeline check
2026-09-20 22:26:47 +02:00
LibretroAdmin 714d69bf13 CRT: name the selected head in the EDID install hint
The firmware-EDID hint names the connector the monitor index lands
on, resolved the way the Monitor Index setting itself resolves a
head; "auto" is named only where there is a single output, and the
list is read up to eight heads deep.

A second line gives the debugfs edid_override route, which puts the
block in place without a reboot. Not every driver exposes
trigger_hotplug, so the line says so.
2026-09-20 22:15:06 +02:00
LibretroAdmin 50d7e40212 libretro-common: PSP sleeps through its own delay, not newlib's
retro_timers.h includes <pspthreadman.h> for PSP, whose only use in this
header is sceKernelDelayThread, and then named PSP in neither sleep
ladder - so both calls fell through to the generic nanosleep body and the
include was dead. Frame Delay and the runloop's pace to content frame
time are both sub-frame sleeps that go through retro_sleep_us, and on PSP
they were getting libc's resolution rather than the microsecond delay the
kernel offers.

PSP joins VITA, which takes microseconds through the same call with the
same signature. Preprocessing -DPSP now yields sceKernelDelayThread where
it yielded the nanosleep body; GEKKO, WIIU, _3DS, VITA and the generic
branch are unchanged.

The macro also drops a libc symbol where there was one, which the
statically linked platforms care about more than most: nothing in the PSP
salamander object set includes this header, and the one salamander TU
that does is the Wii's, whose branch does not move.
2026-09-20 22:10:13 +02:00
LibretroAdmin da232eb6c0 samples/audio: link audio_upmix into the opensl suite
opensl.c asks audio_layout_channels() which layout the frontend wants,
and the suite builds the driver itself, so its link needs the unit that
answers - the lane fails at link without it.
2026-09-20 21:47:05 +02:00
LibretroAdmin 13239b6f64 microphone: the capture worker reads what wait_readable reports
wait_readable() returns a count, not a yes or no: it is what the device
has now, and that is regularly less than the slice asked for. Every
driver returns short when its bounded wait expires with something in
hand, and alsa returns a period even where the slice spans several -
a small device cannot hold a whole slice at all. read() blocks until it
has everything it was asked for, so the worker must ask only for what
wait_readable() just reported. Otherwise it sits in a read that only
more samples can end, and teardown - which joins this thread
unconditionally - hangs with it, taking mic close, driver switch and
shutdown down too.

The lane goes in samples/audio/mic_handshake, which already drives the
shipping worker against a clocked device. Its read returns what it has,
so it could never show this; the case adds one that blocks as the
header specifies, lets the device go quiet under it, and tears down
with a watchdog so a stuck join fails in five seconds instead of
hanging. The measurement runs keep the non-blocking read and their
numbers.
2026-09-20 21:47:01 +02:00
LibretroAdmin fb3512d39f microphone: drop the read paths nothing can reach
The nonblocking halves of alsa, sdl2, sdl3 and coreaudio's mic reads
went with the hook that selected them, so every read takes the blocking
path the header describes.

That empties the alsa and sdl2 driver contexts: what init() and free()
manage is ALSA's global state and SDL's audio subsystem, and the
frontend only needs a handle to give back, so both hand back the
driver's own vtable rather than an allocation of nothing. CoreAudio's
keeps the microphone it tracks.
2026-09-20 21:36:56 +02:00
LibretroAdmin 662511b338 audio: rate control also requires write_avail
The init gate's comment says rate control needs write_avail and
buffer_size, but it only tested buffer_size. A driver with a size and
no fill took AUDIO_FLAG_CONTROL, and that flag is the only guard on
audio_driver_compute_rate_adjust()'s write_avail() call, so the first
batch past the DRC threshold called through a null pointer. audioio
has exactly that shape on the platforms whose audio_prinfo carries no
seek.

The regression lane goes in samples/audio/pipeline_format, which
already drives audio_driver_init_internal() against a patched
audio_null: rate control must stay off without write_avail and come on
with it. Without the fix it segfaults. That case also needs the
wide-then-float one to release the driver it leaves initialised, which
it now does.
2026-09-20 21:34:06 +02:00
LibretroAdmin b2cc971351 microphone: drop the driver set_nonblock_state hook
Nothing in the frontend has ever called it: audio_driver_set_nonblock_state()
reaches the playback vtable only, and there is no setting, flag or menu
entry behind a non-blocking microphone. The reads the header describes
as the default - blocking until the frontend has what it asked for - are
the only ones a driver is asked for, and the threaded capture worker
wants exactly those, with wait_readable() bounding the wait instead.

Six drivers and the null driver lose the slot, and the three harnesses
that drove it by hand lose the call. The nonblock each driver latches
stays where another path still writes it, so wasapi keeps the state it
takes from audio_sync at init and pipewire keeps what the playback
setter leaves on the shared core.
2026-09-20 21:16:42 +02:00
LibretroAdmin b9da4befff audio: report the room, not a constant, in write_avail
roar and xenon360 both answered write_avail with a literal 0. A driver
that cannot measure its fill should leave the slot NULL rather than
report a constant: NULL disables rate control, while a constant feeds
it a fill that never changes and it corrects against that forever.

switch audout returned the whole size of the buffer it was filling
instead of the room left in it, so a nearly full buffer read as an
empty device. wait_writable in the same file already had the right
expression; write_avail now uses it, and reports nothing when no
buffer is in hand, since acquiring one may block.
2026-09-20 21:05:45 +02:00
LibretroAdmin 513d1b5ed4 audioio: report the room, not the buffer
write_avail() returned buffer_size() - the whole buffer, every call,
whatever was queued in it. Rate control reads that as a device that
never drains and answers with its full upward correction for as long
as it is on, which is the half percent of pitch the PS2 driver carries
a note about having had.

audio(4)'s play.seek is the count of bytes pending, so the room is the
buffer less that. It comes from AUDIO_GETBUFINFO, which exists to be
the cheap read of exactly these fields.

Both are NetBSD's: the Solaris audio_prinfo has buffer_size and
samples and no seek, so there the hook is not offered at all. The
interface asks for nothing rather than a constant, and the frontend
turns rate control off and says so, which is the truth.

buffer_size() returned false from a size_t on a failed ioctl.
2026-09-20 20:54:23 +02:00
LibretroAdmin 4042760eea platform_stubs: an audio(4) header for the BSD audio driver
audio/drivers/audioio.c compiles on the systems whose toolchain ships
<sys/audioio.h> - NetBSD and Solaris - so nothing here has held it to
a syntax gate. A compile-only stand-in, shaped after NetBSD's:

  gcc -fsyntax-only -std=c89 -pedantic -Wall audio/drivers/audioio.c \
      -isystem tools/platform_stubs/bsd -Ilibretro-common/include -I. \
      -DRARCH_INTERNAL

AUDIO_GETBUFINFO is NetBSD's alone, which is what the driver keys its
play.seek branch off; BSDSTUB_NO_GETBUFINFO compiles the other side of
that gate on a host.
2026-09-20 20:54:19 +02:00
LibretroAdmin b71096dc1d samples/audio: a suite for the SDL3 underrun count
sdl3_counters drives the two callbacks directly and then runs the
driver against SDL's dummy device, asserting what the frontend reads:
a covered callback is not a shortfall, microphone puts never reach the
count, feeding leaves it alone, starving moves it, and a reopen keeps
what it has. The shape sdl1_counters uses for the same contract.

Hand-run like the other two sdl3 suites - tools/audio-suite-check.sh
audio/drivers/sdl3_audio.c builds and runs all three, plain and under
ASan+UBSan and TSan.
2026-09-20 20:46:44 +02:00
Rob Loach 232d2604ba SDL3: count underruns on the playback callback only
The output stream gets its own get callback for the count, and the
shared wake goes back to waking. A put callback carries the audio the
microphone just delivered in additional_amount, so counting in the
function both streams call has a working microphone reporting an
underrun per device iteration on its own context.

The count also starts at init rather than at the stream, which is how
audio_driver.h specifies it and how the frontend reads it: the pipeline
compares underruns() against what it last saw and treats any change as
fresh silence, so a count reset by a device reopen costs it a pass and
the audio it was holding.
2026-09-20 20:46:40 +02:00
LibretroAdmin 246f41cef6 audioworklet: count the silent quanta instead of printing one
The process callback fills a quantum with silence in two places: when
the ring comes up short, which it already computes zero_frames for,
and when it cannot take the lock at all - and that one printed a
warning from the render callback and returned. Both are one atomic add
now, and underruns() hands the count to the frontend, which shows it
and says it once at teardown.

The short-ring case counts only while the driver is running: one that
is not running is not being starved.

memalign() reached this file through <memory.h>, which emscripten's
own webaudio.h pulls in; <malloc.h> is included here now, so the
driver stands on its own.
2026-09-20 20:42:07 +02:00
LibretroAdmin 3130d7d3d6 platform_stubs: emscripten headers for the audio worklet driver
audio/drivers/audioworklet.c compiles only under emscripten, whose
headers refuse to be included from its source tree - they want a
generated sysroot - so nothing here has held that driver to a syntax
gate. Stand-ins for <emscripten/webaudio.h>, <emscripten/wasm_worker.h>
and <emscripten/atomic.h>, and the EM_ASM_* macros beside the
emscripten.h stub that was already here:

  gcc -fsyntax-only -std=gnu99 -Wall audio/drivers/audioworklet.c \
      -isystem tools/platform_stubs/emscripten \
      -Ilibretro-common/include -I. -D__EMSCRIPTEN__ -DRARCH_INTERNAL

The webaudio stub includes emscripten.h as emscripten's own does, so
the driver reaches the EM_ASM_* macros the way it does in a real
build. Struct shapes follow emscripten's, because the driver
initialises the options positionally and indexes the sample frames.
2026-09-20 20:42:04 +02:00
LibretroAdmin 4b22f89e7a alsa_qsa: count the underrun instead of logging it
check_pcm_status() runs from the write path and said SND_PCM_STATUS_
UNDERRUN to the log every time the device ran dry - a line during
play, on the path whose margin it is reporting on. It is one atomic
add now, and underruns() hands the count to the frontend, which shows
it on the statistics overlay and says it once at teardown.

The file also reached memset, calloc and the errno values through
QNX's <sys/asoundlib.h>, which pulls them in; they are included here
now, so the driver stands on its own includes.
2026-09-20 20:35:18 +02:00
LibretroAdmin 87d3596639 platform_stubs: a QSA header for the QNX audio driver
audio/drivers/alsa_qsa.c compiles nowhere but QNX, whose headers ship
with its toolchain, so nothing in this tree has ever held it to a
syntax gate. A compile-only stand-in for <sys/asoundlib.h> is enough
to, as the android and ps3 stubs do for theirs:

  gcc -fsyntax-only -std=c89 -pedantic -Wall audio/drivers/alsa_qsa.c \
      -isystem tools/platform_stubs/qnx -Ilibretro-common/include -I. \
      -D__QNX__ -DRARCH_INTERNAL

Shapes and spellings follow QSA rather than ALSA; the values are
arbitrary where the driver only assigns or compares them.
2026-09-20 20:35:15 +02:00
LibretroAdmin 1462d76b7a samples/audio: a suite for the SDL3 capture wait
sdl3_mic_wait drives the put callback by hand against a real unbound SDL
stream and asserts what the capture worker depends on: a silent device
answers within one device period, a period satisfies a slice-sized
request, a put ends a parked wait, and an unplug ends one with nothing.
Hand-run like sdl3_raw beside it - tools/audio-suite-check.sh
audio/drivers/sdl3_audio.c builds and runs both, plain and under
ASan+UBSan and TSan.
2026-09-20 20:37:27 +02:00
LibretroAdmin f83c61a68f SDL3: bound the capture wait by the recording device's period
sdl3_microphone_wait_readable() takes its per-wait bound from
sdl3_microphone_wait_ms() - two device periods, clamped to 20-200 ms,
the shape alsa_microphone_wait_ms() uses - and caps what it waits for at
one period. The capture worker re-reads its exit flag only between these
waits, so that bound is also how long closing a microphone holds the
main thread in sthread_join(); and a target above what buffer_size lets
the stream hold can never be reached, the put callback dropping the
backlog past it. The read loop's wait takes the same bound, and
period_frames carries what SDL reports at open for all three streams.
2026-09-20 20:37:27 +02:00
Rob Loach 3f5f41f56a SDL3: mic wait parks on put callback 2026-09-20 20:37:27 +02:00
LibretroAdmin 0c5a699107 opensl: open the layout the frontend asks for
The player was created with two channels whatever the setting said.
The PCM_EX format the float path already uses carries a channel mask
as well, and the frontend's mask is that mask - both are the
WAVEFORMATEXTENSIBLE bits - so it goes across as it is and the frames
arrive in its ascending-bit order either way.

Tried once, ahead of the stereo attempts, and only where PCM_EX is:
API level 21, the gate the float path is already behind. A player that
refuses the wider format falls through to those attempts untouched,
which is also what an older API level does, and layout() then reports
stereo.
2026-09-20 20:28:29 +02:00
LibretroAdmin ebd9f766f5 audren: open the layout the frontend asks for
The renderer was built for two channels whatever the setting said.
audrvDeviceSinkAdd() takes up to six, so 5.1 is the widest this
renderer has, and it now opens that when the layout asks for it. The
mix buffer count follows the channel count, which is why the config
moves into the driver: audrenInitialize() and audrvCreate() have to be
handed the same one. Anything that is not two or six - a four channel
mask - opens as stereo, and layout() reports what was granted.

use_float() says why it is false while it is here. audrvVoiceInit()
takes PcmFormat_Int16 and PcmFormat_Adpcm and rejects the rest,
PcmFormat_Float among them, so this is libnx's limit and not a
setting this driver declined to read.
2026-09-20 20:23:34 +02:00
LibretroAdmin 330b083cfe ps3: open the layout the frontend asks for
The port was opened with two channels whatever the setting said.
libaudio takes six and eight as well, so it now opens what the layout
asks for and falls back to two when the port refuses it or the count
is not one libaudio has - a four channel mask is not. layout() reports
what was granted, so the frontend upmixes to that and no further.

The block the output thread hands to audioAddData() moves to the heap
on the way. That thread is created with 4 KiB of stack, which one
stereo block already half fills; six channels would have run straight
past the end of it.
2026-09-20 20:18:35 +02:00
LibretroAdmin 6feff4679c gx, ctr_dsp: take the counter from libretro-common
Both clock fits read the platform's tick register directly -
gettime() on the one, svcGetSystemTick() on the other.
cpu_features_get_perf_counter() is those two calls, on those two
platforms, and is where a counter read belongs.

The scale stays per-platform: features_cpu.h offers the counter and
no frequency beside it.
2026-09-20 20:18:31 +02:00
LibretroAdmin e1d848bd71 gx, ctr_dsp: report the device clock
Both know the device's position - the DMA callback counts chunks, the
DSP callback reads the channel's sample cursor - and neither took a
timestamp beside it. With one, the slope of position against time is
what the hardware is really running at, against the rate the driver
announced.

The AI's is not a crystal difference but a divisor: it divides its
source clock by a figure that does not land on the rate it is asked
for, and the gap has never been reported anywhere. The DSP's is a
straight comparison of two domains, the channel's cursor against
svcGetSystemTick().

gx fits on the frontend's thread from a pair the callback publishes
under a sequence counter: that callback is a DMA interrupt, and
floating point does not belong in one. The pair has to come from
there even so - sampling the position from the frontend is a chunk
out, which over any usable window is more than the figure being
measured. ctr_dsp fits in its callback, which is an ordinary thread,
and publishes the result as one int.

ctr_dsp takes svcGetSystemTick() rather than
cpu_features_get_time_usec(), which is osGetTime() on this platform
and resolves in milliseconds - a hundred parts per million of
quantisation on a figure measured in single parts.

Measurements, both. Nothing acts on either.
2026-09-20 20:10:50 +02:00
LibretroAdmin 5c851eef6d ps2: report what audsrv knows, and wait for it with an end
The driver handed audio to audsrv and asked it nothing back, so it had
no write_avail() and rate control stayed off for the session. audsrv
answers all three questions: audsrv_available() is the room in its
ring, audsrv_queued() is what it still holds, and the ring's size is
what it reports free before anything is queued - so buffer_size()
becomes the real ring rather than a constant, write_avail() a
measurement, and frames_consumed() what was handed over less what is
waiting.

wait_writable() polls that room with a bounded sleep and a lap cap
rather than calling audsrv_wait_audio(), which blocks with no bound of
its own and would park the audio thread on a server that stopped
draining.

nonblock is still not implemented; the frontend's own policy now has
the room figure to work from.
2026-09-20 19:59:33 +02:00
LibretroAdmin b72e9d374d pulse: report the sink's clock
The latency-update callback already holds the server's timing info and
read only sink_usec from it. read_index is what the sink has played,
in bytes, and timestamp is when that was true - the pairing of
position and time device_clock_ppm asks for, arriving on its own
without a call to fetch it.

Fitted over the window rather than between two points, and restarted
when read_index steps back on a rewind or a flush. A measurement:
nothing acts on it.
2026-09-20 19:33:43 +02:00
LibretroAdmin eec1a9dbe2 tinyalsa, rsound, sdl3, opensl, ps3, gx, wiiu: count the silence they already see
The rest of the drivers that detect a starved device and do nothing
with it. tinyalsa recovers from EPIPE; rsound's callback returns a
short count librsound zero-fills; SDL3 is handed the shortfall as the
get-callback's additional_amount and ignores both its integer
arguments; OpenSL's buffer-queue callback decrements to zero and drops
the result; the PS3 output thread memsets a block it had nothing for;
the GX DMA callback retires each chunk by erasing it, so a chunk the
writer has not reached goes out as the silence left behind; the AX
frame callback already stops the voice when the buffer is short.

Each adds one to a counter where it happens and reports it through
underruns(). GX and AX use the plain volatile counters beside them
rather than atomics, as those files do for their frame counts.

rsound, ps3, gx and opensl stopped short of the slot, so the members
before it are named NULL rather than left to position.
2026-09-20 19:33:39 +02:00
LibretroAdmin d12c42ffd0 ctr_dsp: one rate for the channel and the frontend
init reported 32730 Hz to the frontend and set the channel to
32728 Hz. The channel interpolates with NDSP_INTERP_NONE, so the
frontend resampled to a rate the DSP was not playing at and the ~60
ppm between them had nothing to correct it. Both now come from one
constant.

Also drops the second `if (!ctr)`: ctr is checked, with ndspExit(),
where it is allocated, and the callback and the event have already
been given it by the time the second check is reached.
2026-09-20 19:33:35 +02:00
LibretroAdmin b8e4e9d0de rsound: include verbosity.h for the log it makes
rs_init() calls RARCH_LOG without the header that declares it, so a
build with HAVE_RSOUND=1 stopped on an implicit declaration under
C89. It reached the file transitively on no include path this tree
uses.
2026-09-20 19:33:32 +02:00
LibretroAdmin 5b10ad98d0 thread_read_audit: the coverage note counts the audio callbacks now
The device callbacks, with JACK, PipeWire and PulseAudio built, take a
wide Linux build from 59 audited entries to 76, all clean. The note's job
is to stop a narrow build's zero being read as a clean tree, so the
figure in it should be the one a wide build actually reaches.
2026-09-20 19:18:50 +02:00
LibretroAdmin 7d1185f2d9 overlay_on_screen_test: look at the screen once it holds still
The grab waits for two polls to agree on a screen something has been
drawn to, up to a deadline, instead of trusting a fixed wait. A
frontend that is still starting has drawn nothing, or has drawn into a
window short of its final size, and either is a picture of the startup
rather than of the overlay.

A wrong picture settles at once and fails as it always did: a driver
squeezing the quads is caught on the first pair of polls, and an
overlay that never arrives fails on the menu screen beneath it. A run
that stays in motion to the deadline says so and is checked anyway. On
a healthy machine both lanes now finish sooner than the old wait alone
took.
2026-09-20 19:09:48 +02:00
LibretroAdmin c576919a1f alsa, pulse, pipewire, dsound, openal: count the silence they already see
Each of these drivers already knows when the device went without
audio and did nothing with it. ALSA recovers from -EPIPE and drops the
fact; PulseAudio registers an underflow callback whose body is #if 0'd
around a log line; PipeWire fills the quantum with silence when the
ring is empty; DirectSound's mixer thread has a branch commented
"nothing in FIFO (underrun)"; OpenAL finds the source STOPPED and
restarts it. All five now add one to a counter where it happens and
report it through underruns().

The frontend shows the count on the statistics overlay and says it
once at teardown, which is where a line about audio belongs - pulse's
disabled callback was disabled because it logged from the mainloop's
thread while audio played.

Only the event each backend really reports: -EPIPE and not the -EINTR
and -ESTRPIPE beside it, a PipeWire quantum with nothing at all rather
than a short one, and AL_STOPPED rather than any state that is not
AL_PLAYING, so the first write of a session does not count itself.
2026-09-20 19:06:56 +02:00
LibretroAdmin 3c63e45d8b thread_read_audit: audit the audio device callbacks
The gate walked from the threads this tree creates and from the audio
pipeline's callback. A driver's device callback belongs to the backend
- SDL's device thread, libpulse's and PipeWire's loop, JACK's process
thread, the ASIO driver thread, CoreAudio's listeners, the DSP and AX
frame interrupts - so no create call here names it and the binary walk
has no edge to reach it. Those callbacks are worker threads by every
other measure, and nothing was auditing them.

They are declared, for the same reason the driver frame-context
functions are. A driver's init, open and free stay off the list: they
run on the main thread, where a settings read is legal and every one
of them would be a false finding.
2026-09-20 19:06:52 +02:00
LibretroAdmin 69bdd44f93 asio, jack, coreaudio: take the reinit latch on the main thread
The device callbacks reached for audio_state_get_ptr() to raise
reinit_request: the ASIO driver thread on a reset or rate change,
JACK's server threads on a rate or period change, the HAL's listener
when the default output moves. The singleton is static and the field
is an atomic, so nothing was unsafe, but a worker reaching into
frontend state is the shape the thread-read audit exists to reject,
and wasapi already avoids it - it captures the same pointer on the
main thread at sthread_create and hands it to the worker.

Each driver now takes the pointer at init and its callbacks store
through it.
2026-09-20 19:06:46 +02:00
LibretroAdmin 4bfeb65066 audren, ctr_dsp: bound the waits on the device's frame event
Both waited without a timeout. audrenWaitFrame() is eventWait() with
UINT64_MAX, and LightEvent_Wait() has no bound at all, so a renderer
or a DSP that stops signalling - stopped, reset, or the channel
dropped coming back from sleep - parks the caller for good. On
wait_writable that is the threaded pipeline's audio thread; audren's
blocking write could hold the core's thread the same way.

Both now take the event with a timeout and a lap cap, as every other
driver's wait does, and report a short count when it expires. The
timeout is never reached while the device runs, so it only decides how
long a stopped one takes to be noticed.
2026-09-20 19:06:43 +02:00
LibretroAdmin 0d7ae0dc9d openal: resolve the device clock through a union
Casting alcGetProcAddress()'s object pointer straight to a function
pointer is not legal C, and `make C89_BUILD=1` with HAVE_AL=1 stopped
on it. al_init_events() a hundred lines above already resolves its two
entry points through a union; the device clock now does the same.
2026-09-20 19:06:34 +02:00
LibretroAdmin 46d69f2c81 (Wii/Wii U) Atomics for the HID poll threads' quit flags
Both drivers park a poll thread on a flag the teardown sets from another
thread, with no lock either side, as volatile bools that ordered nothing
between the two. The flags are initialised before their thread is
created, and on the Wii USB side before the first goto error, since free
stores through it.

wiiusb_hid's device_detected goes the other way. It is raised by
wiiusb_hid_change_cb, which libogc runs in interrupt context, so an
atomic is not what it wants - the writer is not a thread, and this file's
other interrupt-shared state is volatile for that reason. It was a plain
bool, which left the poll thread free to hold it in a register across the
loop; volatile is the smallest thing that fixes that.

Both files build against the headers already in the tree - libogc under
wii/, wut under wiiu/ - on a 32-bit PowerPC compiler and on all five
retro_atomic backends, unmodified first and then with the change.
2026-09-20 19:06:30 +02:00
LibretroAdmin 7752034268 (Android) Add a harness for the deferred keypress haptic
samples/input/android_haptic_defer reproduces the defer protocol and
pins it: the callback never enters Java, repeats inside one iteration
coalesce, a press is not carried across a pause, stop, window teardown,
focus loss or destroy, and the delivery guards catch a press raised
after the command in the same poll batch. Two sabotage modes put the
pre-defer dispatch and the missing clears back and are asserted to be
caught. Run by hand, like samples/input/android_kbd_mailbox.
2026-09-20 19:08:17 +02:00
LibretroAdmin f6e7ece621 (Android) Post keypress haptic feedback to the UI thread
doHapticFeedback() is called from the native app thread, and
View.performHapticFeedback() is not documented as thread safe. Drop the
per-keypress log line with it: it runs on an interactive path.
2026-09-20 19:08:17 +02:00
LibretroAdmin bb478c5935 (Android) Keep the pending-haptic flag with the driver's other deferred state
The flag is input-driver state, set and read only on the app thread.
Hold it as a file static beside android_state_flush_pending rather than
in struct android_app, which is shared with the Java thread under the
app mutex. Clearing it before the delivery guards also stops a request
raised before g_android exists from sticking.
2026-09-20 19:08:17 +02:00
LibretroAdmin 02c79d12a2 (Android) Flush pending haptics from the runloop's deferred-work slot
runloop_iterate() already carries the Android work that must happen
outside the core, next to the APP_CMD_PAUSE save that defers for the
same reason. Dispatch the keypress haptic there instead of from
retroarch.c's outer loop, which is compiled out under HAVE_MAIN.
2026-09-20 19:08:17 +02:00
Starlightbotanist be6d649385 Android: defer keypress haptics to the outer app loop
Record pending keypress feedback in Android app state and dispatch it after runloop_iterate() returns. Coalesce requests per iteration and clear pending feedback on pause, stop, focus loss, window removal, and destruction.

Guard delivery by activity, window, focus, and JNI method availability.
2026-09-20 19:08:17 +02:00
LibretroAdmin c9e5688057 overlay_on_screen_test: a failure says where the overlay actually is
A failing lane prints the rectangles on the screen grab, the
rectangles the config asks for, and the lines of the log that say
which context, driver and resolution drew them. A geometry failure is
then read off the CI output rather than reproduced: two bands of two
spans are the two squares, and their rows and columns say what moved
them. A passing lane prints the line it always did.
2026-09-20 18:53:58 +02:00
LibretroAdmin 3c639f4f15 (PS3) An atomic for quit_thread, which crosses two threads
Set by the thread tearing the driver down and read by the output
thread's loop on every pass, as a volatile bool that ordered nothing
between them. The same flag is already an atomic in the PSP, Vita and
PS4 drivers; this one was left behind.

tools/platform_stubs/ps3 grows enough of PSL1GHT's audio, thread,
event-queue, mutex and cond headers to compile the driver on a host, so
the change is checked rather than read: the driver builds unmodified
against them first, then on all five retro_atomic backends. The headers
ps3_defines.h includes unconditionally are there as empty placeholders,
marked as such.
2026-09-20 18:49:18 +02:00
LibretroAdmin 44c99646f8 libretro-common: retro_atomic.h needs boolean.h for its own fallback
The volatile fallback backend declares retro_atomic_cas_int_fb as
returning bool, the header's only use of the type, and nothing here
provided it. A translation unit that includes retro_atomic.h before
anything else fails to compile on that backend - two lines are enough to
show it - which is every platform without real atomics.

The forced-backend loop in the gfx samples workflow did not catch it
because all four files it compiles reach bool through another header
first. boolean.h is what the sibling headers use, is header-only so the
salamander object sets are unaffected, and is a no-op under C++, where
this header is included inside extern "C".
2026-09-20 18:49:14 +02:00
LibretroAdmin d451414e61 ctx_presentable_check: the checks read code, not comments
The vtable scan, the shared-declaration scan and the driver count run
over a copy with comments and literal contents blanked, so a slot
spelled `NULL, /* presentable */` - the tree's way of naming every
unimplemented slot - is a NULL slot and nothing more. Blanking keeps
every offset, so the brace walks still line up with the text.

Three selftest fixtures hold it: a NULL slot named in a comment, a
comment beside a wired hook naming a different one, and the hook's
name inside the driver ident string. Each of the three reports the
checker's defined-nowhere error without this change.
2026-09-20 18:43:22 +02:00
LibretroAdmin fbdd2c2cae audio: atomics for the two cross-thread flags that were volatile bools
Both are written by one thread and read by another, and volatile orders
nothing between them, so neither reader had a guarantee of seeing the
write at all. This is the same reason capture_running in
microphone_driver.h is an atomic, where ThreadSanitizer reported the
race on the field itself.

pipe_consumer_gone is set by the wrapper thread as it leaves its loop
and read by the producer deciding whether to keep waiting; it takes a
release store and an acquire load.

audio_mixer_wfeed's dead is set by the sound's release, which
audio_mixer runs from the mixer thread as well as the caller's, and read
by the feeder task's handler on the task thread.

Checked on all five retro_atomic backends, C11 through the volatile
fallback, and on a clean C89 build.
2026-09-20 18:39:16 +02:00
libretroadmin dca7a0527f Cleanups 2026-09-20 18:37:33 +02:00
LibretroAdmin c202066f26 gfx: split sdl_gl_ctx.c into sdl1_gl_ctx.c and sdl2_gl_ctx.c
Each unit targets a single SDL major version and exports its own vtable:
gfx_ctx_sdl1_gl and gfx_ctx_sdl2_gl. Makefile.common selects the object
inside the existing HAVE_GL_CONTEXT guard, griffin includes it in the
matching arm of its SDL chain, and gfx_ctx_gl_drivers[] lists whichever
one the build defines - SDL2 first, as the merged file's own #ifdef chain
resolved it. Both keep the "gl_sdl" ident, so video_context_driver settings
carry over untouched.

sdl1_gl_ctx.c holds the SDL 1.2 surface path alone: no GL context or shared
context members, no webOS hints, and no settings lookups, since the monitor
index and windowed-fullscreen flag only ever fed SDL_CreateWindow. Its
presentable slot is NULL, which gfx_ctx_driver_t documents as "always
presentable"; bind_hw_render stays an empty function rather than NULL
because gl3.c calls it unguarded whenever a core requests hardware
rendering.
2026-09-20 18:31:54 +02:00
LibretroAdmin c44adcd1ba input: split sdl_joypad.c into sdl1_joypad.c and sdl2_joypad.c
Each unit targets a single SDL major version and exports its own vtable:
sdl1_joypad (ident "sdl") and sdl2_joypad (ident "sdl2"). Makefile.common
selects the object alongside the matching input driver on HAVE_SDL2,
griffin includes it in the same arm, and joypad_drivers[] lists whichever
one the build defines.

sdl1_joypad.c keeps only the plain joystick API: no GameController, haptic
or sensor members, so the pad accessors collapse into their call sites and
set_rumble, set_sensor_state and get_sensor_input are NULL entries rather
than stubs that always return false - both are null-checked by their
callers. The unused compat/strl.h include goes with it.
2026-09-20 18:05:07 +02:00
libretroadmin 36b9cb7436 sdl_dingux_joypad - cleanups 2026-09-20 17:56:40 +02:00
LibretroAdmin a513a10403 input/sdl2: report the horizontal mouse wheel
The wheel event already records mouse_wl/mouse_wr; hand them back under
RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP/DOWN so cores and the menu see tilt
scrolling, with the same axis orientation sdl3_input.c uses.
2026-09-20 17:51:11 +02:00
LibretroAdmin 78c8fd23e0 input: split sdl_input.c into sdl1_input.c and sdl2_input.c
Each unit targets a single SDL major version and exports its own vtable:
input_sdl1 (ident "sdl") and input_sdl2 (ident "sdl2"). Makefile.common
selects the object on HAVE_SDL2, griffin includes the matching unit in
each arm of its SDL chain, and the driver registry lists whichever one
the build defines. The SDL1 arm of griffin now carries the input driver
and joypad next to the audio driver so it links on its own.

sdl1_input.c omits the KMOD_SCROLL probe: 0x8000 is KMOD_RESERVED under
SDL 1.2, so scroll lock stays unreported there and remains SDL2-only.
2026-09-20 17:51:07 +02:00
LibretroAdmin 016966b9c8 tools: sanction the frame roots' display reads in the thread audit
A build with Vulkan, Qt, ffmpeg, HID and Bluetooth audits 59 worker
entries where a default Linux build audits about 42, and the wider call
graph reaches five gfx_display helpers from gl2_frame, gl3_frame and
vulkan_frame. They carry the same argument as the pairs already
sanctioned through the menu and widgets stack: past gfx_display_flush_as
they touch only blend_on and dispctx. Keyed on the reading function, so
one line each covers all three roots.

video_thread_hw_allowed() goes in with its reasoning written out. The
core's context type and the configured driver name are both settled
before the video thread exists, and a driver change reinits through a
join. The bounded transient between the menu's write and that reinit is
recorded rather than papered over, together with why latching is not the
cheaper answer: the macro calling it runs inside gl2 and gl3 init.

The count itself is worth a note, since an entry absent from the binary
is not a finding and a narrow build reporting none is not a clean tree.

Checked by injecting an unsanctioned reader into one of the five
functions: the pairs are per reader, so the audit still reports it.
2026-09-20 17:50:47 +02:00
LibretroAdmin d35bb7777e sdl2_audio: open the device the user picked, and list capture devices
The Device setting reached both init paths and neither used it, so the
list the menu offered could be browsed but not applied. Both now open
the device by name and fall back to the default with a warning when
the name has gone with the hardware it named, rather than failing init
on a stale setting.

Enumeration no longer assumes SDL_INIT_AUDIO is already up: the
frontend builds the list whether or not the driver came up, which is
how the user picks a different device after the current one failed.
The microphone driver gains the same list; it had none.
2026-09-20 17:43:40 +02:00
LibretroAdmin d9cfd3813f sdl1_audio: order the driver's setup before the device thread sees it
SDL 1.2 opens the device with its audio thread already running and
unpauses it by writing a plain flag, so the ring and the park built
between those two points reach that thread with nothing ordering them:
a period that observes the device unpaused may observe a ring that is
not yet built. Taking SDL's audio lock, which that thread holds around
every period, across the unpause is the edge. SDL2 locks and uses an
atomic for this itself.

The handhelds this driver serves are MIPS and ARM, where the stores
are free to arrive in the order the race allows.

samples/audio/sdl1_counters covers this and the driver's device
accounting: SANITIZER=thread reports the race when the lock is taken
back out. Run by hand, as the sdl_lost_wakeup suite is.
2026-09-20 17:43:36 +02:00
LibretroAdmin c3d201d868 sdl1_audio, sdl2_audio: report device time and silence
Both drivers run on an SDL callback that is the device asking for one
period, so counting what it asks for counts device time, and the short
read it zero-fills is a period of silence for want of audio. Bytes and
periods are counted on that path with one atomic add each and turned
into the frontend's frames_consumed() and underruns() off it.

frames_consumed() gives the frontend the device's real rate against
the host clock, so the resampling ratio can be trimmed by the
difference between two crystals; underruns() feeds the statistics
overlay and the one line the frontend logs at teardown.
2026-09-20 17:43:33 +02:00
LibretroAdmin 9e36f9fca6 rthreads: share the false return in the priority and affinity setters
sthread_raise_current_priority and sthread_set_affinity return false
both from the guard ahead of the call they make and from the #else arm
for a platform with neither. Inverting each guard lets the arm fall
through to a single return after #endif.
2026-09-20 17:43:11 +02:00
LibretroAdmin bfc0b9abbd (PSP/Vita/PS4) Bound the starvation lane against the ring geometry
The 30 Hz lanes sat one period inside their own tolerance. A delivery
needs RING_FREE >= its own size, so at the 2560-frame floor a
1600-frame delivery waits until held <= 959; draining a period at a
time from the previous refill, the values the writer is released at
cycle through 448, 512 ... 896, and the first of those eight is below a
period. One refill in eight has to let the ring dip under a period
before it can be refilled, which is 24 of 600 periods - the 4% the
floor was chosen against, and exact rather than noisy. The bound was
5%, so ordinary jitter on top of a fixed 24 reached 29 and 30 against a
limit of 30. It is now twice the geometric figure, which still fails a
four-period floor and a worker that holds a period back as a reserve;
both were re-checked by injection.

The writer's usleep pacing goes with it. Rate-matched to the device by
construction - 1600 frames per 3125 us against 512 per 1000 us - and
usleep only ever oversleeps, so the ring level could only walk
downwards. It made no difference to the counts either way, because the
refill point is set by the ring geometry and not by the writer's rate,
and the writer already self-paces by blocking for room.

A six-period floor would not dip at all; the released values start at
960. That is a latency decision rather than this lane's to make.
2026-09-20 17:36:53 +02:00
LibretroAdmin 26ebbf70d7 gfx/drivers: delete unused psp1_gfx.h
Nothing in the tree includes it, and every type, extern and macro it
declares is absent everywhere else, including in psp1_gfx.c next to it.
Its "pspgu.h" include is unresolvable from gfx/drivers, so it cannot
compile where it sits.
2026-09-20 17:35:53 +02:00
LibretroAdmin 674cf0d693 samples: stop tracking the generated wasapi_sim.c
Both pacing suites generate wasapi_sim.c from audio/drivers/wasapi.c
with sed. The committed copies had drifted from the driver, and since
make regenerates only when the driver is newer, a fresh clone could
build either text depending on checkout timestamps. Generating it
always is what the suites mean by the driver's text.
2026-09-20 17:29:10 +02:00
LibretroAdmin 9fe27f8da3 Remove break statements that follow an unconditional return
Thirteen switch cases end in a return or a goto and then a break the
case can never reach. Object code is unchanged.
2026-09-20 17:29:07 +02:00
libretroadmin 690687bb60 Another unnecessary comment 2026-09-20 17:25:33 +02:00
libretroadmin ee9e0b5dbc Unnecessary comment 2026-09-20 17:24:58 +02:00
LibretroAdmin 261f3028cc apple: make the iOS and tvOS deployment targets overridable
RA_IPHONEOS_DEPLOYMENT_TARGET and RA_TVOS_DEPLOYMENT_TARGET set the floor
for every target in RetroArch_iOS13.xcodeproj, so a runner on an Xcode that
has raised its minimum can build the store packages without a tree edit.
Unset, the targets keep the values they build with today.
2026-09-20 17:21:26 +02:00
LibretroAdmin 250584f48c apple: raise Mac App Store deployment target to macOS 12.0
Xcode 27 accepts macOS deployment targets in the 12.0 - 27.0.x range, so
the store package targets 12.0. RA_MACOSX_DEPLOYMENT_TARGET overrides it
for builds on machines carrying an older Xcode.
2026-09-20 17:19:41 +02:00
LibretroAdmin 4ff07a1798 memmap: bounded name formatting in the area sample
The Windows job builds this with cl /W3 /WX, where sprintf raises C4996
and C2220 turns it into an error, so that lane has never been green.
snprintf takes the buffer size and MSVC does not deprecate it; the
lane's other translation unit is already clean.
2026-09-20 17:16:08 +02:00
LibretroAdmin 060ff93b15 cheevos: drop the blank lines at the end of cheevos_menu.c 2026-09-20 17:10:01 +02:00
LibretroAdmin d4293fb038 (PSP/Vita/PS4) Hold the audio drivers to the contracts they assume
Two things the three drivers took on trust.

A worker that fails to start is not an initialisation failure: running
is set before the create, so the driver comes back reporting itself
alive with nothing consuming its ring. Writes then wait out their laps
and return zero, and free() skips the stop because it is nested under
the thread pointer. init now releases the port, the ring and the
eventcount and reports none, and start() puts running back and answers
false, which is the same hole on resume.

write() derived its frame count as uint16_t from a byte length. Above
65535 frames the count wrapped small, the room check waved it through
on the space the wrapped value asked for, and the non-wrapping memcpy
then copied the original byte length - 262144 bytes into a 12 KiB ring
under the harness. The frontend's output scratch is sized at
max_buffer_samples * AUDIO_MAX_RATIO * slowmotion_ratio and written in
one call, so the length is reachable from an extreme resampler ratio.
The count is a size_t, a length that is not whole frames carries the
frames it has, and anything past what the ring holds is taken short
rather than waiting out 800 ms for room that cannot come.

psp_ring grows a lane for each, over all three drivers: device_mock
supplies the drivers' sthread_create through a rename so one can be
refused, and the write lane walks 0, 1, 65535, 65536, 65537 and 131072
frames plus a six-byte write. Against the previous code the first lane
reports FAIL with alive=1 on all three and the second is an ASan
heap-buffer-overflow of 262144 bytes in psp_audio_write.
2026-09-20 17:09:58 +02:00
LibretroAdmin 335b027672 memmap: hold memshm_area_map/unmap to the reservation they advertise
The contract says @at lies within the reservation, and neither function
checked it. On POSIX the map is MAP_FIXED, so an address outside the
area does not fail - it replaces whatever is mapped there. The offset
was also taken by subtracting base from an address not yet known to
belong to the area, the legacy slot check could overflow in both the
rounding and the addition, and the placeholder path confirmed only that
the first byte fell in a free range, so a length reaching past the end
recorded a range running backwards.

memshm_area_offset carries both arguments as uintptr_t and orders the
comparisons so neither the addition nor the subtraction can wrap; the
POSIX paths round the length up to a page first, because that is what
mmap replaces. unmap refuses an unbalanced call rather than wrapping the
mapping count.

On Windows the range table was also edited before the OS calls that
make the split, and a failure returned without putting it back, leaving
the software map and the VADs describing different things. Each step now
calls the OS first and records the split only once it succeeds, and a
table too full to record the result is refused before anything is
touched.

The test grows negative lanes for each of these: an address below the
base, the first address past the end, a length crossing it, a length
that would wrap, and an unbalanced unmap - plus a mapping made outside
the area, checked byte-wise to still be there afterwards. Against the
previous code the first two report FAIL and the third takes a bus error.
2026-09-20 17:09:54 +02:00
libretroadmin 175bbfded5 Cleanups 2026-09-20 17:09:39 +02:00
LibretroAdmin 645be4f7ad audio: split sdl_audio.c into sdl1_audio.c and sdl2_audio.c
One file per SDL generation, each with its own vtable: audio_sdl1
("sdl") in sdl1_audio.c and audio_sdl2 ("sdl2") in sdl2_audio.c. The
SDL2 file keeps the microphone driver, which is SDL2-only. The SDL1
file drops the SDL2 device-API stubs it no longer needs and checks
SDL_OpenAudio()'s return properly - the shared file compared an
SDL_AudioDeviceID (unsigned) against < 0, so a failed open was taken
for a good one.

Makefile.common builds each object from its own SDL branch instead of
from HAVE_SDL_COMMON, griffin picks the file matching the SDL version,
and the samples/audio suites follow the renamed symbols.
2026-09-20 17:01:29 +02:00
LibretroAdmin 084dbf3e35 audio-suite-check: pass the suite's TSan suppression file
A samples/audio suite that ships tsan.supp does so because its reports
come from an uninstrumented dependency; latency_contract's covers
OpenAL Soft, whose own threads and atomics TSan cannot see into. The
workflow passes the file, so the hand check reports a race the CI job
does not.
2026-09-20 17:01:14 +02:00
LibretroAdmin 635cc12f9b Remove the dead thread handle, and stop SceUID being declared three times
The SceUID member in the three console audio drivers was never
assigned or read. On Vita it was the only thing those drivers wanted
from psp2/kernel/processmgr.h, threadmgr.h and sysmem.h, and
psp2/audioout.h includes psp2/types.h itself, so those three go with
it. PSP1 keeps pspkernel.h: pspaudio.h has no includes of its own and
relies on it for the types in its prototypes.

The Vita compile-only stubs each declared their own SceUID, which
collided under -pedantic in any translation unit taking more than one
of them - features_cpu.c takes processmgr.h and, through
retro_timers.h, threadmgr.h. The typedef moves to a psp2/types.h stub
where the real headers keep it, and the two that need it include it.
The sysmem.h stub held nothing else and no lane includes it now.
2026-09-20 16:39:59 +02:00
LibretroAdmin 488bd8d402 (PSP/Vita/PS4) Size the ring floor for a 30 Hz delivery
The ring has to hold a delivery beside the period the worker is
playing, and a delivery is a video frame of audio: 800 frames for
60 Hz content at 48 kHz, but 1600 for 30 Hz. The four-period floor
fitted the first and not the second, so at settings below the
default a 30 Hz core starved - 14% of periods handed silence through
the threaded pipeline, 93% through the inline one, where the whole
delivery goes in a single write.

Five periods is the floor, which is the ring the default 64 ms
setting already asks for. So no setting at or above the default
moves, 60 Hz content is unchanged at either floor, and 30 Hz content
at the low end goes to 0.3% and 4%. Settings below 64 ms now report
64 ms rather than 53 ms, which is the whole of the visible change.

Content slower than 30 Hz is not covered: 20 Hz wants a sixth
period, which would push the default itself to 75 ms. Left alone
rather than moved for content that rare.

samples/audio/psp_ring drives the starvation check at both delivery
sizes; the floor at four fails the 30 Hz one on all three and leaves
60 Hz green.
2026-09-20 16:32:04 +02:00
LibretroAdmin af297573df Share the NULL return in the SDL audio device list
sdl_audio_list_new returns NULL both from the guard on the list it
allocates and from the SDL1 arm. Wrapping the body in that guard lets
both paths fall through to a single return after #endif.
2026-09-20 16:31:04 +02:00
libretroadmin ecc7ad0c49 (wiiu_audio.c) Update 2026-09-20 16:23:17 +02:00
LibretroAdmin b414ab8ee0 (PSP/Vita/PS4) Play a period as soon as there is one
The worker handed the device silence unless two periods were held,
so at the low end of the latency range - where the ring is at its
four-period floor and the frontend delivers 800 frames at a time -
it spent a quarter of every period playing silence with audio
sitting in the ring. Measured against a device taking a period every
10.67 ms and a writer delivering a video frame of audio at 60 Hz:
27% of periods silent at the floor, against 0.3% when a single held
period is enough. At 64 ms and above the two are the same, so
nothing above the floor changes.

PSP1 needed the window it hands over to change with it. It was
giving the device the window after read_pos rather than the one
read_pos names, which is whole only when two periods are held; it
now takes the window read_pos names and publishes the index after
the call returns, as the other two already did. That also retires
the period it used to discard unplayed at startup.

samples/audio/psp_ring gains a starvation check at the lowest
setting, with the writer paced to the device rather than run flat
out. Putting the reserve back fails it on all three; leaving the
reserve out but giving PSP1 its old window back fails PSP1 alone on
a discontinuous window, which is the coupling between the two.

A failed check in the suite now releases the driver, so one failure
cannot leave a worker running against the next.
2026-09-20 16:20:24 +02:00
LibretroAdmin cbdcfbbdb5 psp1: keep every blit inside the GE's transfer limits
The GE's block transfer takes each side at 1023 at most and both
strides as a multiple of 8, no wider than 1024. The frame and menu
paths pass the core's width and pitch straight through, so a core
outside those limits gets a malformed transfer: vecx hands over
330-pixel rows and o2em 340-wide ones.

Round the destination stride, which is ours, up to a multiple of 8 and
carry it into sceGuTexImage; crop either side at 1023. A source stride
the transfer cannot express is copied a row at a time, where the
stride is not read, through a call list grown to the geometry - the
CPU writes 8 command words a row instead of repacking the frame
itself, which at 340x240 is 1920 uncached stores against 40800.

The harness holds all four limits on every blit and checks the frame
arrives in the texture for both stride paths.
2026-09-20 16:09:25 +02:00
LibretroAdmin 5198a6cb5a (PSP/Vita/PS4) Size the ring from the latency setting
The ring was a fixed 8192 frames on all three, which at 48 kHz is
170 ms of buffering whatever the user asked for, and half of that is
what rate control steers to. Size it from the latency setting at the
rate the device opened at, less the period the device itself holds,
so buffer_size() and the latency it implies are what was asked for.

The size stops being a power of two, so the wrap stops being a mask:
indices stay inside the ring and RING_HELD/RING_FREE compare instead.
The ring is a whole number of periods, which is what keeps every
window the worker hands over inside it, and the floor is four - the
worker wants two periods held before it hands one over, and the
writer needs room past that. At 48 kHz that floor is 53 ms reported,
so settings under it get the floor; above it the figure is exact.

The rate the device took is now kept, since the sizing needs it.

samples/audio/psp_ring gains a check that buffer_size() tracks the
setting across 8..256 ms rather than returning a constant: pinning
the ring back to 8192 fails it on all three. The ring's own checks
hold across the rewritten wrap - giving the reserved frame back still
fails all three with a discontinuous window.
2026-09-20 16:08:38 +02:00
LibretroAdmin 58aa63ebb7 Share the NULL return across the mixer loaders
Each audio_mixer_load_* function returns NULL both from the guard on its
allocation and from the #else arm for a codec that is not built. Wrapping
the body in that guard lets both paths fall through to a single return
after #endif.
2026-09-20 16:08:29 +02:00
LibretroAdmin b23aa20f28 (PSP/Vita/PS4) A harness for the console audio ring
The three drivers had no suite, so the two races fixed in ba5339c4
and d36404a1 had nothing holding them down.

samples/audio/psp_ring builds all three drivers for their own
platforms against the compile-only SDK stubs and supplies the bodies:
a device that reads the window it is handed and takes a period over
it. The writer's frames carry a running count, so a window whose
frames do not continue the last one means the writer reached data the
device had not finished with - a lapped ring, or a period published
free before the device took it - and the check holds without a
sanitizer.

Reintroducing either fix's absence fails it, and each fails its own
platforms: the room check that cannot tell a full ring from an empty
one fails all three, publishing read_pos before the output call fails
Vita and PS4 while PSP1 stays green, which is right - it hands over
the window after read_pos and frees one only on the iteration after
it played. Clean under ASan+UBSan+LSan and TSan.

tools/platform_stubs/orbis carries the sceAudioOut declarations, the
set the psp and vita stubs already had.

Not wired into Linux-samples-audio.yml; run it by hand, or through
tools/audio-suite-check.sh, which now finds it for all three drivers.
2026-09-20 16:01:16 +02:00
LibretroAdmin d36404a114 (PSP/Vita/PS4) The room check could not tell a full ring from an empty one
write_pos and read_pos are both masked into the ring, so the writer
having filled every frame and the writer having written nothing both
read as a distance of zero, and the free-space computation answered
with the whole ring. A writer that caught up exactly lapped: it wrote
from read_pos forward, over the window the device was reading, while
the worker - reading the same distance - took the full ring for an
empty one and played silence.

Hold a frame back, so the two states differ.

Found with a threaded model of the mainloop against the write path
using the tree's own retro_atomic ops, under TSan: the race reproduces
with the frame returned and does not with it held back, and it is
independent of the read_pos publication order - each fix is needed and
neither covers the other.
2026-09-20 15:54:45 +02:00
LibretroAdmin ba5339c419 (Vita/PS4) Publish read_pos after the device has taken the window
The worker advanced read_pos and released it before handing the same
window to sceAudioOutOutput, so the period the device was reading was
already the writer's to fill. A write landing in it while the call
was in flight put the writer's samples into audio being played.

The output call returns once the device has taken the window, which
is where the period becomes free: move the release store there, and
the notify with it. PSP1 does not have this - it outputs the window
after read_pos, which is still-unread data, and frees a window only
on the iteration after it played.
2026-09-20 15:54:42 +02:00
LibretroAdmin 0127835abb psp1: let a build pick the frame slice grid
The 4x16 split makes each slice tall and narrow, which is the wrong
shape for an unswizzled texture's cache lines; what another split
costs can only be measured on the device. Take both counts from the
build when it sets them, and add a check-grids target that holds the
coordinate contracts for each candidate so only speed separates them.
2026-09-20 15:54:13 +02:00
LibretroAdmin 4853a53946 psp1: take blue from the high bits of the display formats again
Red sits in the low bits of every PSP display format, which is what
the CLUT passes psp_init() builds convert the core's frame into: the
pass indexed at red_shift writes the low channel, the pass indexed at
0 writes the high one. So read_viewport's 565 and 5551 arms take blue
from the top of the pixel, as its 4444 and 8888 arms already do.

Record the layout next to both, and in the harness, whose expected
bytes for the two 16-bit cases disagreed with its own 4444 and 8888
cases.
2026-09-20 15:51:20 +02:00
LibretroAdmin 32315f6a74 (PSP/Vita/PS4) Report consumed frames and underruns
These three reported neither, so the sink rate estimate had nothing
to trim the resampling ratio with and the statistics overlay showed
no starvation for any of them.

Neither SDK offers an underrun signal or a queued-sample query the
drivers can use - the PSP SRC's output call returns the sample count
it was reserved with, and sceAudioOut has no rest-sample equivalent
on PS4 - but both are available from the worker's own cadence. The
output call returns once the device has taken the period before it,
so a completed call is a period consumed; the branch that hands the
device silence is the starved one. One atomic add each, where it
happens, read by the frontend.

Vita is the one that could do better: sceAudioOutGetRestSample()
reports a main port's unplayed samples, so frames_consumed() there
could be what was released less what the device still holds rather
than the cadence. Left for someone who can watch it against the
overlay on hardware, since that number steers rate control.

buffer_size() left out the period the output call holds while the
device plays it, which is buffering the driver controls; the
reported size and the latency it implies now agree.
2026-09-20 15:47:47 +02:00
LibretroAdmin c036af814a psp1: give the vblank handler the signature the kernel calls it with
sceKernelRegisterSubIntrHandler() invokes the handler as
void (*)(int, void *), so declaring it otherwise leaves it called
through an incompatible type. Take the driver from the void* argument,
and move the psp_set_texture_enable() declaration ahead of the
statement before it; the compile lane now rejects both.
2026-09-20 15:46:55 +02:00
LibretroAdmin 9c258b507a psp1: add a contract harness for the PSP video driver
Links gfx/drivers/psp1_gfx.c against a stand-in for the machine it
depends on: main RAM and VRAM each mapped twice so the cached and
uncached aliases alias, a no-access page after VRAM, display lists
that stay the GE's until waited on, and a vblank sub-interrupt that
can fire inside free().

Holds the read_viewport byte order and buffer fill, the teardown
order, GE ownership of lists and framebuffers, both blit bounds,
cache-line ownership of what the GE writes, the coordinate grid
across all four rotations, vsync pacing, and init under allocation
failure.
2026-09-20 15:46:49 +02:00
LibretroAdmin 5d16bb5c26 psp1: wait for the GE before drawing the OSD message
The message goes into the framebuffer through the CPU. With a
hardware-rendering core the driver deliberately leaves the GE alone,
so it has to be waited on here, where the two would otherwise be
writing the same buffer.
2026-09-20 15:46:45 +02:00
LibretroAdmin cf4bc9dd1e psp1: fill the whole read_viewport buffer
The screenshot task sizes its buffer from the viewport and encodes all
of it, so a viewport reaching past the framebuffer has to come back
black there rather than leave the encoder reading uninitialised heap
with every row shifted. Clear the buffer, place each row at the offset
the viewport gives it, and read the framebuffer through the uncached
alias the GE wrote it through.
2026-09-20 15:46:42 +02:00
LibretroAdmin a5c0adc3c7 psp1: emit BGR from read_viewport in the 16-bit display modes
video_driver.h specifies read_viewport as 24bpp BGR, which the 4444
and 8888 arms answer with; the 565 and 5551 arms emit the channels the
other way round, and those are the modes the PSP runs in, so every
screenshot comes out with red and blue exchanged.
2026-09-20 15:46:38 +02:00
LibretroAdmin 68c202cbd4 psp1: rebuild texture coordinates only when the geometry changes
psp->frame_coords is written through the uncached alias, so rebuilding
it is 256 stores straight to memory every frame. Keep the source
geometry they were built for and skip the rebuild while it holds.
2026-09-20 15:46:33 +02:00
LibretroAdmin 24ac206e51 psp1: bound the core frame and menu blits to their buffers
VRAM holds the display buffers and the colour tables before the core
frame, and the menu texture buffer holds one screen of 4444; a core or
menu framebuffer larger than what is left runs sceGuCopyImage() off
the end of either. Record what VRAM is left at init and clamp the
copied height in both paths.
2026-09-20 15:46:29 +02:00
LibretroAdmin a0a7a33354 psp1: fail init when an allocation does
psp_init() dereferences the coordinate buffers straight after
allocating them, and hands back a driver whose display lists the
frontend will pass to sceGuStart(). Check every allocation and tear
the partial driver down through psp_free() instead.
2026-09-20 15:46:26 +02:00
LibretroAdmin 4cef57434c psp1: mark the vblank flag volatile
psp_on_vblank() clears it from interrupt context while psp_frame()
reads and sets it, so the compiler has to reload it rather than keep
it in a register. On PSP this is what retro_atomic's own backend
reduces to.
2026-09-20 15:46:22 +02:00
LibretroAdmin 3e67043bd6 psp1: wait for the GE before rebuilding the main display list
sceGuFinish() hands the list to the GE and returns, so the buffer is
still the GE's when the menu pushes a texture between frames.
Synchronise before refilling it, and ignore a call with no driver or
no frame.
2026-09-20 15:46:19 +02:00
LibretroAdmin b7ee4aac47 psp1: reserve whole cache lines for the buffers the GE writes
The GE context save area and the menu texture are written by the GE,
so a cache line either of them shares with data the CPU writes is lost
to the next writeback. Give the context storage its own 64-byte
aligned allocation instead of a slot in the driver struct, and raise
the menu texture's alignment to a cache line.
2026-09-20 15:46:15 +02:00
LibretroAdmin 7b874f570a psp1: release the vblank handler before freeing the driver
psp_on_vblank() writes through the driver pointer it was registered
with, so the sub-interrupt has to be disabled and released before
psp_free() releases anything it reaches. Free the driver struct itself
by that pointer, and drop the main_dList guard so a driver whose first
allocation failed is still torn down.
2026-09-20 15:46:11 +02:00
LibretroAdmin 4a2c2bde57 psp1: compile the PSP video driver in the compile matrix
gfx/drivers/psp1_gfx.c is reachable only through the griffin unity
build, so nothing in CI compiles it. Give it a platform_video lane,
and extend the PSP stub header set with the kernel, display and GU
shapes it names.
2026-09-20 15:46:08 +02:00
LibretroAdmin 4ac1c11a77 Share the fallback return instead of duplicating it per arm
Three functions carry a #else arm whose only job is to return the same
constant the enabled arms already fall back to. Inverting the guard that
precedes each arm's final return lets the arm fall through, so the
constant is returned in one place after #endif.
2026-09-20 15:45:06 +02:00
LibretroAdmin 6d416e7a41 Remove empty preprocessor conditionals
Sixteen #if/#endif pairs across twelve files enclose nothing, so they
select no code in any configuration. Removing them leaves preprocessor
output identical.
2026-09-20 15:29:54 +02:00
LibretroAdmin 814b4cf6cb (PSP/Vita/PS4) One arena for the ring and its silence
The ring and the period of silence the worker outputs when the ring
runs short were two allocations of the same lifetime, neither ever
resized and the silence never written after init. Put them in one
uint32_t arena, the silence past the ring's end, so init has one
allocation to check and free one pointer to release.

The window handed to the output syscall stays inside the ring:
read_pos starts at zero and only ever advances by AUDIO_OUT_COUNT,
which divides AUDIO_BUFFER_SIZE, so it is always a multiple of the
period and the last window ends exactly at the ring's end. The
producer's wrap splits at that same boundary.
2026-09-20 15:29:37 +02:00
LibretroAdmin c68798c71d (PSP/Vita/PS4) Negotiate the output rate, harden init, implement PS4 stop
Each of these backends takes a fixed set of sample rates, and a
setting outside it was passed straight to the hardware: the port
open failed, init returned NULL and the session ran with no audio
at all. Report the rate actually opened through new_rate instead, as
gx_audio does, so the frontend resamples to it.

 - PSP1: the SRC's rate list is fixed, so fall back to 48 kHz when
   the requested rate is refused.
 - Vita: the main port opens at 48 kHz only.
 - PS4: sceAudioOut opens at 48 kHz only.

The ring and silence buffers were allocated without checking the
result, and neither allocation failure nor a failed eventcount init
released the port that had already been opened. Allocate with calloc,
check both, and release the port on every failure path.

PS4: sceAudioOutInit() ran after the port it initialises was already
open; it now comes first. stop() returned false unconditionally, so
CMD_EVENT_AUDIO_STOP always failed on Orbis - pause, sleep/wake and
the shutdown path with it. It stops the worker and reports the
resulting state, as the vtable's contract asks and the other two do.
2026-09-20 15:15:36 +02:00
LibretroAdmin fa553ea47c Split psp_audio.c into per-platform audio drivers
The PSP1, Vita and PS4 backends share no SDK: each opens its port
with a different call, outputs through a different syscall and
releases differently. Give each one its own file and vtable.

audio/drivers/psp_audio.c  - PSP1,  audio_psp,  "psp"
audio/drivers/psp2_audio.c - Vita,  audio_psp2, "vita"
audio/drivers/ps4_audio.c  - Orbis, audio_ps4,  "orbis"

Driver idents are unchanged, so existing configs keep working.
Makefile.common, Makefile.orbis and griffin select the file for the
platform being built; the matrix's Vita lane now compiles
psp2_audio.c.

ps4_audio.c drops the unused verbosity.h include, which on ORBIS
pulls in debugnet.h, and declares malloc through stdlib.h.
2026-09-20 15:07:21 +02:00
LibretroAdmin 02fbc9f3c7 (Vulkan) A check holds the order teardown retires driver objects in
tools/vulkan_teardown_order_check.py fails when vulkan_free() or vulkan_check_swapchain() retires the command pools, the descriptor pools and the pipelines in any order but innermost first, when a descriptor set is freed by hand rather than by the pool destroy that owns it, and when the pool asks for per-set free that nothing needs. The order is legal Vulkan and the validation layer passes it, so the lavapipe harness beside it cannot see this class; a driver that walks its own back references as a pool is destroyed faults at exit instead. Runs as its own job in the Vulkan workflow, self-test first, like the idle-wait check.
2026-09-20 14:55:55 +02:00
LibretroAdmin 0682d0c6e5 (Vulkan) Descriptor pool blocks are created without per-set free
The sets in a block live and die with the pool, and VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT is required only by vkFreeDescriptorSets, which nothing calls. Drivers that provide a per-set allocator for the flag no longer hand one out. The slang filter chain's pools are created this way already.
2026-09-20 14:55:52 +02:00
LibretroAdmin 17fe7a8157 (Vulkan) Retire command pools and descriptor pools before the pipelines
A command buffer references the descriptor sets and the pipelines it was recorded with, and a descriptor set references the layout it was allocated from, so teardown runs innermost first: command pools, then descriptor pools, then the pipelines that own the set layout. Destroying a descriptor pool releases every set allocated from it, so that one call frees them, and the per-frame command buffer and pool handles are cleared as they go, like the descriptor managers and buffer chains beside them.

A driver that keeps its own references along that chain walks them as each pool is destroyed - MoltenVK does, and the fault lands inside vkDestroyDescriptorPool on the video thread as RetroArch exits.
2026-09-20 14:44:48 +02:00
libretroadmin c75c752edb Cleanup 2026-09-20 14:34:01 +02:00
LibretroAdmin b98eefe946 (Metal) A streaming texture stages through one slot per frame in flight
With a single staging buffer, the busy flag set by frame N's blit is
only cleared when the GPU finishes that blit's command buffer. With up
to MAX_INFLIGHT frames queued ahead of it, that has usually not
happened by the time frame N+1's update arrives, so a texture updated
every frame dropped most of its frames and streamed at a fraction of
its rate.

The staging buffer now holds MAX_INFLIGHT frame-sized slots, each with
its own busy flag, used round-robin - the same shape as Vulkan's
VK_STREAM_SLOTS. A frame is dropped only when the slot it would use
is still being read, which with one slot per frame in flight is rare.
The flags are plain ints behind __atomic stores and loads: each is set
only by the updating thread and cleared only by the completed handler,
so no read-modify-write is shared between them.
2026-09-20 14:16:24 +02:00
LibretroAdmin 5dc2d285fa (Metal) A streaming update runs on the thread that owns the blit buffer
metal_update_texture now encodes a blit into Context.blitCommandBuffer,
but it still ran inline on the caller's thread whatever threaded said.
Under threaded video that is the main thread, while the video thread
encodes into and commits the same command buffer at frame end.
MTLCommandBuffer is single-thread-only, and a completed handler added
after the video thread has committed the buffer is an exception, not
just a race.

metal_load_texture already routes mipmapped loads to the video thread
for exactly this reason, and video_driver.h says update_texture is
marshalled onto the video thread; Vulkan, D3D11 and D3D12 all do it.
Metal now does the same through video_thread_texture_handle, with the
result coming back through cmd.handle as in metal_texture_load_wrap.
2026-09-20 14:16:24 +02:00
libretroadmin 2421ce7bb9 (Metal) The staging buffer is not written while the GPU still reads it
The blit reads the staging buffer on the GPU and the next frame's
memcpy writes it from the CPU: hazard tracking orders one GPU access
against another and says nothing about that, so the race moved from
the texture to the buffer rather than going away. The texture carries
a flag now, set when a blit that reads the buffer is committed and
cleared from that command buffer's completion handler, and a frame
arriving while it is set is dropped - the same policy Vulkan, D3D11
and D3D12 keep, with the buffer rather than the texture as the thing
being protected. The flag is atomic because Metal finishes on
whatever thread it likes, and the handler holds the texture so the
flag is still there to clear.
2026-09-20 14:16:24 +02:00
libretroadmin daa9703f7d (Metal) A streaming update is a blit the GPU orders, not a write over its shoulder
metal_update_texture wrote the new contents with replaceRegion:, which
lands from the CPU the moment it is called - while the command buffers
already committed may still be sampling that texture. The comment
above it justified this by saying a command buffer retains what it
samples, which is true of the object's lifetime and says nothing about
its contents. Every other backend refuses exactly this: Vulkan checks
its fences, D3D11 maps with DO_NOT_WAIT, D3D12 compares a fence tag,
and each drops the frame rather than race.

The pixels go through a staging buffer per texture and a blit on the
shared blit command buffer instead. It executes in commit order with
the draws around it and Metal's hazard tracking orders the write
against the reads, so a streaming frame is neither raced nor dropped -
better than the drop the others settle for. The buffer is made on the
first update and kept, since a texture that streams updates every
frame, and it is released with the texture.
2026-09-20 14:16:24 +02:00
LibretroAdmin 26a1a303d0 ci: stop overlay-on-screen runs on master cancelling each other
The concurrency group was keyed on the ref with cancel-in-progress
set, so every push to master that touched one of the watched paths
cancelled the run still going for the previous commit ("Canceling
since a higher priority waiting request ... refs/heads/master
exists").  With several commits pushed close together, most of them
never got the test run to completion.

Key the group on the commit SHA for pushes and only cancel in
progress runs for pull requests, where a new push really does
supersede the old one.
2026-09-20 13:49:40 +02:00
LibretroAdmin 46973bd825 videocore: read the EDID, and use bcm_host's gencmd client
dispserv_videocore gets get_edid, reading the display's EDID over
HDMI DDC with vc_tv_hdmi_ddc_read() the way tvservice -d does: the
base block, then the extension blocks it announces, as far as the
buffer goes. The edid CRT preset (mode 5) takes its ranges from it
instead of warning and falling back to generic_15, and the EDID
entry under System Information shows it. Composite and DPI outputs
have no DDC; the read fails there and the fallback stays. Behind an
HDMI-to-VGA adapter the block is whatever the adapter reports, which
may be its own rather than the monitor's.

bcm_host_init() already connects the gencmd and tvservice clients.
The server now calls it at init (it returns at once when the video
driver made the call first) and sends hdmi_timings through that
client. The per-call connect and vc_gencmd_stop() it replaces, carried
over from crt_rpi_switch, stopped the gencmd client bcm_host shares.
2026-09-20 13:45:52 +02:00
LibretroAdmin b6f5b2d27d crt: VideoCore becomes a display server
The legacy Raspberry Pi firmware stack had its own CRT switching path
inside video_crt_switch.c: a hardcoded timing heuristic
(crt_rpi_switch) that bypassed the modeline engine, with every engine
call site fenced off by #if !defined(HAVE_VIDEOCORE). Keeping those
fences in step with the call sites is how 94d23651 broke the link on
Lakka's Pi Zero builds (fixed in 4e057a5).

VideoCore now has a display server like KMS and X11 do.
dispserv_videocore implements the modeline ops: the engine generates
the mode, and set converts it to hdmi_timings (porches and pulse
widths, inverted-sync flags, frame rate for interlaced modes), sends
it through gencmd, selects it with tvservice -e "DMT 87", resizes the
framebuffer with fbset and reinits the video driver, the same sequence
crt_rpi_switch ran. The vc_egl context and the dispmanx video driver
report RARCH_DISPLAY_VIDEOCORE so the server is picked up.

With that, video_crt_switch.c has no HAVE_VIDEOCORE left:
crt_rpi_switch and the crt_switch_write_edid stub are gone, VideoCore
builds get switchres.ini overrides and the CRT menu entry, and the
guard on crt_switch_display_server_lost() in video_driver.c goes back
to HAVE_MODELINE alone.

Behaviour differences on VideoCore:
- Timings come from the engine and the monitor preset instead of the
  old heuristic.
- Without a super width set by the user, the policy locks a 1920
  super resolution, as the old path always widened the mode near 1920
  to keep the HDMI pixel clock up. It scales fractionally where the
  old path picked an integer multiple of the core width.
- Doublescan is disabled; hdmi_timings has no way to express it.
- The last mode still stays on the wire at exit; the firmware has no
  desktop mode to restore.
2026-09-20 13:43:00 +02:00
Shigeaki Asai 4e057a5194 video_driver: guard crt_switch_display_server_lost with HAVE_VIDEOCORE
crt_switch_display_server_lost() (video_crt_switch.c) is defined
inside #if !defined(HAVE_VIDEOCORE) - VideoCore keeps its own
tvservice path and doesn't use the modeline ops table. The call
added in 94d23651 (\"crt: rebind the modeline ops when the display
server is rebuilt under a live engine\") only checked HAVE_MODELINE,
so any build with both HAVE_MODELINE and HAVE_VIDEOCORE (e.g. Lakka
RPiZero/RPiZero2 with the bcm2835-driver GLES stack) fails to link:

  video_driver.c:(.text+0x20e8): undefined reference to
  \`crt_switch_display_server_lost'

Add the same !defined(HAVE_VIDEOCORE) guard at the call site.
2026-09-20 13:33:46 +02:00
LibretroAdmin 055383c3de d3d11 sample: the norebind control is a control again
The last patch folded version 3 into version 2 and renamed the test's
modes with it, which broke the run that has to fail:

  == a core that ignores what lock_context tells it (must fail)
  d3d11_hw_interface_v2: ok
  FAIL: it passed; the frontend in the test no longer disturbs the
  context

The frame bound everything under "if (rebind || v3)", because in version
3 the render target changes with the sync index and must be set every
frame whatever lock_context said. With version 3 now being version 2,
that condition is always true: the core rebinds even when it is
pretending not to, so the control could not fail.

The render target is now bound every frame on its own, outside the
rebind block where it belongs - it is the sync index's, not the
context's - and everything else is bound only when lock_context says the
frontend has had the context. A core that ignores lock_context draws
into the right texture and draws it wrong, which is the point.

run.sh went with it: the version 3 run is the version 2 run now, so it
is gone, and v3-nowait is v2-nowait. Three runs, one that must pass and
two that must fail.

Ran here under Wine on llvmpipe, the same way CI does: the contract run
passes, both controls fail.
2026-09-20 13:26:54 +02:00
LibretroAdmin 35a3daa09f android: only alias DPAD_CENTER to ENTER on the keyboard row
cf3c678de5 (#18405) rewrote every AKEYCODE_DPAD_CENTER press to
AKEYCODE_ENTER before storing it in android_key_state[], regardless
of which row it was written to. That fixed Enter on external
keyboards (rarch_key_map_android can only map RETROK_RETURN to one
keycode), but pad rows are read by raw keycode against joypad binds,
so any device whose OK/Center button is autoconfigured as "23" - the
Amazon Fire TV Remote, and other remotes/pads - could never trigger
that bind again: the press always landed in bit 66.

Restrict the alias to ANDROID_KEYBOARD_PORT, the only row read
through rarch_keysym_lut. Scoping on the row rather than on
AINPUT_SOURCE_DPAD matters: the hotplug code classifies
KEYBOARD|DPAD (0x301) devices as keyboards too, and ordinary
keyboards with arrow keys report exactly that, so a source check
would reintroduce #18395 for them.

Fixes #19593
2026-09-20 12:52:35 +02:00
LibretroAdmin 81744eb571 d3d11: version 3 becomes version 2, and the old version 2 is gone
The D3D11 hardware render interface had two negotiated versions above 1:
version 2, where the frontend is finished with the frame's texture by
the time video_refresh returns, and version 3, which adds a sync index
so the texture is handed over without a copy. Version 2 was never used
by anything - no core shipped against it - and keeping it meant the
frontend carried a copy path for a case that does not arise.

So version 3 is version 2 now, and what version 2 was is deleted. There
is one negotiated version above 1 and it is the one with the sync index,
which is also how libretro_d3d12.h version 2 and the Vulkan interface
work. RETRO_HW_RENDER_INTERFACE_D3D11_VERSION_3 is gone; a core that
named 3 as its maximum gets 2, and behaves exactly as before.

What went with it:

  - The copy branch in d3d11_gfx_hw_ring_capture, taken when the ring
    passed D3D11_HW_RING_CAPTURE_TEXTURE, and the macro itself.
  - The threaded wrapper's d3d11_v3 flag and the two paths behind it.
    hw_ring_install is now required for version 2 at all, since handing
    the texture over without a copy is what version 2 is; a frontend
    without it gives the core version 1.
  - The sample test's separate version 2 mode; "v3" and "v3-nowait"
    become "v2" and "v2-nowait".

Not built here - this is Windows-only code and needs a Windows build
before it is trusted.
2026-09-20 12:35:14 +02:00
LibretroAdmin 7410f9e8af d3d12: stock pipelines are built for the swapchain's actual format
A 10-bit swapchain with HDR off renders nothing. The game's audio and
input run, the log is clean, and the screen is black.

d3d12_gfx_init_pipelines hardcoded RTVFormats[0] to R8G8B8A8_UNORM for
every pipeline that draws to the swapchain. That is right whenever the
swapchain is 8-bit, and it is not always 8-bit: with HDR disabled and
video_swapchain_bit_depth set to 10, chain.bit_depth becomes
DXGI_SWAPCHAIN_BIT_DEPTH_10 and the back buffer is R10G10B10A2_UNORM. A
draw whose pipeline disagrees with its render target is invalid, so
every frame is thrown away.

It only bites with HDR off, which is why it looks backwards: with HDR on
the frame is drawn with the HDR pipes, and those have always been built
from the real swapchain format and recreated when it changes. The SDR
ones never were. So they are now built the same way, from
chain.formats[chain.bit_depth], which is set well before the pipelines
are created.

The debug layer says it plainly once it is asked:

  ID3D12CommandList::DrawInstanced: The render target format in slot 0
  does not match that specified by the current pipeline state.
  (pipeline state = R8G8B8A8_UNORM, render target format =
  R10G10B10A2_UNORM)

repeated for every draw of every frame.

Not built here; this needs a Windows build before it is trusted.
2026-09-20 05:54:55 +02:00
LibretroAdmin 0e661e7be4 input/overlay: compose two-frame APNGs once, retry busy frame swaps
Two fixes to the press-driven frame swap:

- A swap that could not happen - the surface still in flight on the
  video thread, or the submit failing - was recorded as done anyway.
  Under threaded video a quick tap submits the pressed frame, the
  release lands while it is still in flight, and the button then stays
  pressed-looking until the next tap. The swap now reports whether it
  happened and the current frame only changes when it did, so the
  next poll retries.

- Every press and release rewound the stream and inflated one or two
  full frames on the main thread (the second frame is composed over
  the first, so it cannot be decoded alone). Both frames are now
  composed once at load into a back-to-back pair and a swap is a copy.
  An APNG whose pair cannot be composed, or whose canvas does not
  match its decoded image, stays an ordinary looping animation.

Also reset the current frame when the surface is re-created, since the
new texture starts on the unpressed frame.
2026-09-20 02:40:44 +02:00
LibretroAdmin 4905b828b3 input/overlay: index two-frame APNG press state by pack image
desc->image_index is the desc's position in its page's image list:
every desc with an image gets its own entry there, even when several
share one file. The anim_2frame arrays are sized num_images, one entry
per unique image of the pack, deduplicated by path. Indexing them with
image_index read (and, when the stray byte was nonzero, wrote) past
their end on any pack whose page has more image entries than the pack
has unique files - e.g. a dozen buttons sharing one png - and on
smaller packs toggled the wrong image, since page index N is not
unique image N and the page background shifts everything by one.

Record the unique index at load time, where the loader already has it
from its dedup lookup, as desc->pack_image_index, and use that. Also
skip image-less descs, whose calloc'd index of 0 would otherwise press
whichever file is unique image 0, and keep the block under HAVE_RPNG
with the rest of the animation code.
2026-09-20 02:40:44 +02:00
hunterk 2de98cc73b add on-press functionality for 2-frame apngs 2026-09-20 02:40:44 +02:00
github-actions 687836cfa3 Fetch translations from Crowdin 2026-09-20 00:27:49 +00:00
libretroadmin 2bd0cf51ac Threaded video: the poke interface is published under the lock
video_thread_get_poke_interface() had the driver write thr->poke
directly, on the main thread, while the video thread was already
running and reading that field - the asynchronous upload list takes it
under thr->lock every time it drains, so the read was guarded and the
write was not. A reinit while uploads are in flight is where the two
meet, which is the shape the font-marshal lane drives and where TSan
caught it.

The driver fills a local now and the lock publishes it, so both sides
of the field are under the same lock. Nothing else writes it: the
driver_data assignments beside it happen in the command handler, on
the thread that reads them.
2026-09-20 02:05:52 +02:00
Quan Wei b601ad56e2 ci: package Windows ARM64 builds for GitHub releases (#19586) 2026-09-19 23:53:36 +02:00
LibretroAdmin 8b4b3bfcc4 Threaded video: the hardware ring reports the interface version once
[Video] Threaded video: D3D11 hardware render interface version 3, ...
  [Video] Threaded video: D3D11 hardware render interface version 3, ...

A core may ask for the hardware render interface more than once - when
it first needs the context, and again when it builds its device - and
the ring logged its answer every time. The answer is the same each
time, so it is logged once per ring, and again only if it ever differs.
A new ring, after a reinit, says it again, which is when it is worth
reading. Same for the D3D12 line.

Passes tools/mingw_syntax_check.sh.
2026-09-19 22:16:21 +02:00
LibretroAdmin 0b83e4f881 (D3D12) Interface version 2: the frame is drawn from the core's texture, not from a copy of it
As d804bcb did for D3D11. The driver copies every hardware frame into
frame.texture[0] and draws from that, so that shader passes, history and
feedback read one texture the driver owns; with no shader preset active
nothing needs it, and it is a full-frame copy every frame.

For a core on interface version 2 it goes. Such a core keeps a texture
per sync index and leaves one alone until the frontend is done with it,
so the texture stays the frame it was handed over as for as long as the
driver draws from it, repeats and the menu's redraws included.

What D3D12 needs that D3D11 did not:

  - a descriptor for the core's resource in the driver's own heap. A
    core rotates a few textures, so one is made per resource and found
    again; four are kept, with their resources, until another wants the
    place. That only happens when a core's textures change - a change of
    resolution - and work recorded with the old descriptor may still be
    on the queue, so the queue is drained before it is written over;
  - the resource's state. The core leaves its texture in required_state,
    COPY_SOURCE, and expects to find it there. It is made
    PIXEL_SHADER_RESOURCE for the one draw that reads it and put back,
    in the same command list, on every frame that draws from it.

That draw is now what reads the core's texture, so it is what arms the
fence wait_sync_index waits for, where the copy did. SDR and HDR draw
the frame through the same one binding, so both get this. A shader
preset that is active, a frame from interface version 1 or a software
core, or a descriptor that cannot be had, all take the copy as before;
only a core that negotiated version 2 reaches any of this.

With the core handing its merge target over without a copy (lrps2
000ca3eb5), a D3D12 frame now goes from the core to the screen with
none at all.

Passes tools/mingw_syntax_check.sh (win32 gnu99 and the C89 lane). Not
run.
2026-09-19 22:05:49 +02:00
LibretroAdmin d804bcbfb3 (D3D11) Interface version 3: the frame is drawn from the core's texture, not from a copy of it
The driver copies every hardware frame into frame.texture[0] and draws
from that, so that shader passes, history and feedback all read one
texture the driver owns. With no shader preset active nothing needs it:
it is a full-frame copy, every frame, to draw a picture that is already
in a texture.

For a core on interface version 3 it goes. Version 3 is what makes it
safe: such a core keeps a texture per sync index and leaves one alone
until the frontend is done with it, so the texture stays the frame it
was handed over as for as long as the driver draws from it - repeats
and the menu's redraws included. The driver makes a shader resource view
of the core's texture (a core rotates a few, so a view per texture is
made once and found again; four are kept, with their textures, until
another wants the place) and binds it where it bound frame.texture[0]'s.
SDR and HDR draw the frame through that same one binding, so both get
it. With version 3 already handing the texture over without a copy, a
frame now goes from the core to the screen with none at all.

Everything else is as it was: a shader preset that is active, a frame
from any other interface version or from a software core, or a view that
cannot be made, all take the copy. Only a core that negotiated version 3
reaches any of this.

Passes tools/mingw_syntax_check.sh (win32 gnu99 and the C89 lane). Not
run.
2026-09-19 22:00:12 +02:00
libretroadmin 6957451cf2 CI: the Metal behaviour the surface and overlay paths assume
The driver's Metal paths have only ever been compiled: no job runs
them and no hardware here can. Two assumptions carry them, and this
asserts both on the macOS runner's own device.

A streaming surface replaces a texture's contents while the GPU may
still be sampling it. Vulkan checks a fence, D3D11 maps with
DO_NOT_WAIT, D3D12 compares a fence tag, and each drops the frame
instead of racing; Metal's replaceRegion: has nothing of the kind, so
the path rests on Apple's rule that a managed or shared texture may be
written from the CPU. The lane writes sixty-four frames with work in
flight against each and reads the texture back.

An overlay page holds textures the pack owns. The page keeps them in
an array, a switch replaces that array, and dropping a page must not
take the pack's textures with it - the case that broke gl2 on Android.
The lane builds two pages over one set, switches, drops, and uses the
textures afterwards.

The sample does not link metal.m, which would drag in the frontend; it
reproduces the two patterns. A runner without a Metal device reports
77 and the step says so rather than passing quietly.
2026-09-19 21:50:07 +02:00
LibretroAdmin 3b45449ca3 libretro_d3d11.h version 3: a sync index, so the frame is handed over without a copy
Version 2 says the frontend is finished with the frame's texture when
video_refresh returns. A frontend that presents from another thread can
only honour that by copying the texture there, which RetroArch's
threaded ring does: a full-frame copy every frame, to move a picture the
core already has in a texture into one the ring owns.

Version 3 is version 2 with the sync index the Vulkan interface and
libretro_d3d12.h version 2 have: get_sync_index, get_sync_index_mask and
wait_sync_index. The core keeps one present texture per index; the
frontend reads the texture itself, when its own thread gets to it; and
wait_sync_index returns once the frontend is done with what the index
carried last time. No fence, unlike D3D12: the device has one context,
and what the frontend has issued on it is ahead of whatever the core
issues next, so "the frontend's frame call has returned" is all that
done has to mean. wait_sync_index is not called with the lock held, for
the reason video_refresh is not. It is a new version number and not new
members on version 2 because version 2 has shipped: a core built against
a longer struct must be able to tell a frontend that has it from one
that does not.

The driver, with no wrapper in front, reads the texture inside
video_refresh: one index, nothing to wait for.

The threaded ring holds a reference to the core's texture in the slot
where version 2 made a copy, and the video thread hands it to the driver
through hw_ring_install, which D3D11 now implements; the driver takes
its frame's reference under the lock, as it does for version 2's copies.
The wait that covers a frame queued but not yet claimed, or claimed and
still being recorded - written for D3D12 version 2, where a slot is the
core's own texture for the same reason - is lifted out of the D3D12
block and used by both.

samples/gfx/d3d11_hw_interface_v2 gains two runs: version 3 with a
frontend slower than the core, which laps it and has to wait, and a core
that skips wait_sync_index, which is caught ("index 0 should hold frame
0, holds frame 18"). All four runs behave; run.sh requires the two
controls to fail. d3d11.c and video_thread_hw.c pass
tools/mingw_syntax_check.sh (win32 gnu99 and the C89 lane). The driver
and ring code itself is not run: it needs the core's half.
2026-09-19 21:48:24 +02:00
LibretroAdmin d0fb3a8ef0 (D3D11) Interface version 2: video_refresh without the lock, and the frontend's turn comes first
Version 2 as first written cost a D3D11 core frame rate under threaded
video instead of giving it anything: about 50 fps from a core that runs
at 60, the wrapper's drains piling up, 20 ms and more to vblank. Two
things in the contract and the lock, both fixed here; the core's half of
the first follows in its own tree.

video_refresh was to be called with the lock held. video_refresh is
where the threaded wrapper paces the core: it waits, up to a refresh,
for the video thread to take the frame before. The video thread cannot
finish the frame it is on without the lock. So every paced wait ran to
its timeout with the core holding the one thing that would have ended
it. The contract now says what it should have: set_texture with the
lock held, release it, then video_refresh, and never hold it across
video_refresh. The frontend takes the lock itself for the copy it makes
there - it always did, the lock being recursive - and the core cannot
draw into the texture meanwhile because it is inside video_refresh. The
paragraph about carrying on after video_refresh within the same lock
goes, since there is no such thing any more.

A Windows critical section is not fair. A thread that releases it and
asks again at once usually gets it back, over one that has been waiting;
a core that locks around each piece of GS work, thousands of times a
frame, keeps the frontend's thread out for the whole frame that way. The
frontend now says when it is waiting (an interlocked count around its
EnterCriticalSection), and the core's lock_context lets it go first:
while the count is up it yields the processor instead of entering. A
nested lock_context does not do that - the core would be waiting for a
frontend it is itself blocking - which is what core_depth is for. With
no wrapper in front the frontend is the core's own thread and never
waits.

samples/gfx/d3d11_hw_interface_v2 follows the contract as changed and
still passes, control included. d3d11.c passes
tools/mingw_syntax_check.sh (win32 gnu99 and the C89 lane). Not run on
the driver itself.
2026-09-19 21:17:40 +02:00
LibretroAdmin c69cbc61f8 (D3D11) Interface version 2: the repeat of the last frame gives the context up around its waits too
A D3D11 core on interface version 2 under threaded video, 120 Hz display,
60 fps core: stuck near 48 fps, 8.33 ms reported for the driver's render,
20 ms and more to vblank.

cb758f6 stopped the frame holding the lock the core and the driver share
the context with through the frame's waits. It left the other entry
point that takes that lock as it was: d3d11_present_last, which puts the
retained frame on screen again when the core has nothing new by the
deadline. With a core slower than the display that is not a rare path -
at 120 Hz under a 60 fps core it runs every other refresh - and it waits
for the display exactly as the frame does: the waitable swapchain's
latency wait, then a Present with the swap interval. It held the lock
through both, so for one refresh in two the core could not touch the
context at all, and a core that needs about five milliseconds of it per
frame came out at two frames in five refreshes.

Both loops in it - the light frames and black frame insertion's dark ones
- now give the lock up around the latency wait, and around Present where
the runtime's multithread protection is on, with the helpers the frame
uses. Neither wait depends on pipeline state, and the copy or clear
between them is issued with the lock held as before.

Passes tools/mingw_syntax_check.sh (win32 gnu99 and the C89 lane). Not
run.
2026-09-19 21:17:36 +02:00
libretroadmin e5b109aed6 (Win32) The window title is set with a deadline, not an open-ended wait
A core driving a hardware context calls the frame callback from whichever
thread it renders on, and the frame updates the window title. SetWindowText
sends WM_SETTEXT, and a send does not return until the window's own thread
takes it off its queue -- but that thread is inside retro_run, waiting for
the core, and is not pumping messages. Each waits for the other. A PS2 core
presenting from its EE thread hangs at boot this way, with the frontend
thread parked in the core's own wait and the EE blocked in SendMessageW.

The same message with a timeout: the title is cosmetic and a frame that
cannot set it loses nothing, where a frame that blocks on it loses the
session. ABORTIFHUNG returns at once when the target is already known to
be stuck rather than waiting the deadline out. Both flags and
SendMessageTimeout predate NT 4, so the oldest target still builds; checked
at _WIN32_WINNT 0x0400, 0x0501 and 0x0600.
2026-09-19 21:12:21 +02:00
libretroadmin 89da82e3cc The video thread reads the frame's snapshot, and a check that says so
The null driver's present_last() read a setting the main thread was
writing, and TSan found it rather than anyone noticing by reading. Two
more were doing the same from frame(): xvideo and sdl2 took the OSD
message position out of the live settings while the video thread was
drawing with it. xvideo takes it from the video_frame_info_t its
caller already holds; sdl2 has two callers, frame() and the OSD poke,
so the position arrives as a pair of floats - the snapshot's from the
frame, the setting's from the poke, where the wrapper holds the main
thread inside the command and nothing can be writing.

tools/video_thread_state_check.py looks for the rest of the class
instead of waiting for the next one: it seeds from frame() and
present_last(), walks what they call, and fails on config_get_ptr(),
video_state_get_ptr() or runloop_state_get_ptr() anywhere it reaches.
It cannot follow a call through a function pointer or into another
file, so a clean run means no path it can follow, not no path at all.
The drivers for platforms with no video thread are listed with that
reason - a read that is latent today and a finding the day the
platform runs threaded - and an entry whose driver has stopped
reading fails too, so the list shrinks. It carries a self-test for
both directions and runs beside the gfx samples.
2026-09-19 21:07:30 +02:00
LibretroAdmin cb758f6733 (D3D11) Interface version 2: the frame does not hold the context through its waits
A D3D11 core on interface version 2 under threaded video ran at 48 fps
where it runs at 60 otherwise, with 16 ms to vblank and 34 ms worst.

Under version 2 the driver's frame takes the lock the core and the
driver share the immediate context with, and it took it for the whole of
the frame. The frame blocks in three places: the waitable swapchain's
latency wait at the top, the wait for vertical blank, and Present. The
first two do not touch the context at all, and holding the lock through
them kept the core off the context for most of a refresh, every refresh.

The lock is given up around the two waits and taken again after, which
marks the context used by the frontend as any other turn does. What the
core does to the pipeline state meanwhile cannot matter at either point:
the latency wait comes before the frame has bound anything, and the wait
for vertical blank after it has issued its last draw and flushed.

Present does touch the context. With the runtime's own multithread
protection on - ID3D10Multithread, which a D3D11 immediate context has
answered for since Windows 7 - the runtime serialises Present against
the core's calls itself, for as long as it needs the context, so the
lock is given up around a waiting Present as well. Protection is turned
on when version 2 is handed out; where that is not possible, Present
stays under the lock as before.

With no wrapper in front the frame runs on the core's thread inside the
core's own lock; giving up one level of a recursive lock there changes
nothing, as it should. Version 1 cores never reach any of this.

Passes tools/mingw_syntax_check.sh (win32 gnu99 and the C89 lane). Not
run.
2026-09-19 20:55:16 +02:00
LibretroAdmin ef32d1f4a2 (D3D11) libretro_d3d11.h version 2, in the driver and in the threaded ring
The contract went in with 357a049. This is the frontend's half of it.
Nothing changes for any core until one asks: version 2 is handed out
only to a core that negotiated it, and every existing core still gets
version 1 - under threaded video, the deferred context behind the proxy,
exactly as before.

Negotiation. GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT answers
version 1 for the D3D11 type, and
d3d11_hw_interface_negotiated_version() (gfx/common/d3d11_hw_interface.h)
is where the driver and the ring both learn what the core asked for.

The lock. A critical section in the driver, recursive, which is what the
contract needs: with no wrapper in front the driver's frame runs inside
the video_refresh the core called with the lock held, and the frame
calls itself for black frame insertion. Only two of the driver's entry
points can run while the core is running - the frame, and the repeat of
the last one - so those two take it; everything else reaches the video
thread as a command the main thread waits for, and the main thread is
not inside the core while it waits. Taking it marks the context as used
by the frontend, which is what lock_context reports to the core and
clears.

The frame. With no wrapper, set_texture's texture is what the frame
reads, in place of whatever is bound at pixel shader slot 0. Under the
threaded ring the core gets the immediate context itself - no deferred
context, no proxy, no command list to replay - and at video_refresh the
driver copies the named texture into the slot's own: one call on the
context the core's thread holds the lock for, ahead of anything the core
does next. The slot's texture is replaced (a change of frame size) and
the video thread takes its reference to it under that same lock.

The header gains one paragraph: video_refresh is the frontend's use of
the context too, so a core that carries on after it within the same lock
treats the state as disturbed.

d3d11.c, video_thread_hw.c and runloop.c pass tools/mingw_syntax_check.sh
(win32 gnu99 and the C89 lane). Not run: it needs a core that asks for
version 2, which is the next change, and a Windows GPU.
samples/gfx/d3d11_hw_interface_v2 still runs the contract itself.
2026-09-19 20:40:15 +02:00
LibretroAdmin 357a049bf6 libretro_d3d11.h: version 2, where the core and the frontend take turns on the immediate context
A D3D11 device has one immediate context, and it cannot be used from two
threads at once. Version 1 hands it to the core and takes the frame from
whatever is bound at pixel shader slot 0, which works only while the
frontend draws on the core's thread. A frontend that presents from
another thread cannot share it under those rules; all it can do is give
the core a deferred context, and a deferred context records but cannot
read back - no Map for reading, no query results. A core that reads its
render targets back loses that silently, and has to be written around a
context whose state and object lifetimes do not behave like the one it
was written for.

Version 2 shares the immediate context by taking turns:

  lock_context / unlock_context
      whoever holds the lock has the context to itself and everything
      D3D11 can do with it, reading back included. lock_context returns
      true when the frontend has used the context since the core last
      released it, and the core then binds again whatever it relies on;
      false means the context is exactly as it left it. Recursive. A
      core may lock and unlock as often as it likes within a frame.
  set_texture(texture)
      the frame, named outright. The frontend is finished with it by
      the time video_refresh returns - one context orders everything -
      so there is no sync index, and the slot 0 convention is not needed.

context is always the immediate one in version 2. The struct is version
1's with members added at the end; RETRO_HW_RENDER_INTERFACE_D3D11_VERSION
stays 1 because existing cores reject anything else, and a core gets
version 2 only by asking through the negotiation interface libretro
already has (RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_D3D11 is new
in libretro.h), exactly as for D3D12. How the lock is made is the
frontend's business: a lock of its own is enough, since both sides take
it for every use, so nothing here needs ID3D11Multithread or Windows 10.

This is the contract and a proof that it is sufficient; no driver
implements it yet and no core asks for it, so nothing changes for anyone
until they do.

samples/gfx/d3d11_hw_interface_v2 runs it on a real D3D11 device: a core
that draws through cached state, reads its own render target back every
frame and hands the texture over, against a frontend thread that takes
the lock and draws with its own target, shaders and viewport before
checking the frame it was given. 200 frames drawn, read back and seen.
A core that ignores lock_context's answer is the control and is caught
at once ("the core drew frame 1 and read back 0"); run.sh requires that.
Built with mingw-w64, run under Wine, added to the gfx samples lane,
which fails rather than skips if it finds no D3D11 device. The header
compiles as C89 and as C++.
2026-09-19 20:18:30 +02:00
LibretroAdmin 2903a3a9c9 video_null: present_last reads the frame's snapshot, not the live setting
ThreadSanitizer, threaded_video harness, present-repeat lane:

  data race gfx/video_driver.c:451 in video_null_present_last
    read  by 'ra-video'  video_null_present_last <- video_thread_loop
    write by main thread lane_present_repeat (threaded_video_test.c:373)

The null driver's present_last asked config_get_ptr() for
video_black_frame_insertion. Under the threaded wrapper that is the
video thread reading a setting the main thread writes, which is the
read the wrapper exists to keep off that thread; the lane writes the
setting to test black frame insertion and the two met.

The count now comes from the frame: video_null_frame keeps
video_info->black_frame_insertion, the snapshot the main thread took
for that frame, and present_last returns it plus one. frame and
present_last run on the same thread with or without the wrapper, so
there is nothing left to race, and it is how the real drivers learn the
value. The lane still sees what it checks - it runs frames after
changing the setting, so the repeat that follows carries the new count.

Passes tools/mingw_syntax_check.sh. The harness was not rebuilt here
(a full sanitizer build of the frontend); the CI lane that reported
this is the check.
2026-09-19 19:52:12 +02:00
LibretroAdmin 00e0963ec0 (D3D12) libretro_d3d12.h version 2, in the driver and in the threaded ring
The contract went in with 75f55ed. This is the frontend's half of it.
Nothing changes for any core until one asks: version 2 is handed out
only to a core that negotiated it, and every existing core still gets
version 1 and the copy that goes with it.

Negotiation. GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT answers
version 1 for the D3D12 type. d3d12_hw_interface_negotiated_version()
(gfx/common/d3d12_hw_interface.h) reads what the core then set and gives
min(what it asked for, 2), or 1; the driver and the ring both ask there,
so they cannot disagree.

The driver, with no wrapper in front. Its frame runs inside
video_refresh and reads the core's texture there, so there is one sync
index. set_texture_fenced queues a Wait on the core's fence ahead of the
frame's command list - a wait on the GPU's timeline, none on the CPU -
and a fence signalled behind that list is what wait_sync_index waits.

The threaded ring. A version 2 slot is the core's own texture: publish
records and submits nothing on the core's thread, which is where version
1's cost was (a command list, two barriers, a copy, a submit and a
signal, every frame). The video thread gives the driver the texture and
the core's fence through hw_ring_install, which D3D12 now implements;
the driver queues the Wait ahead of its frame and reads the texture
where it used to read the slot's copy. The slot holds a reference to the
texture and the fence until it is handed over again. A core that calls
version 1's set_texture on a version 2 interface gets version 1's path
for that frame.

One thing version 2 cannot share with the copy path. A slot's fence says
the video thread has driven the driver with the slot and the GPU is
done; it says nothing while a frame is queued and not yet claimed, or
claimed and still being recorded. With a copy per slot that window is
harmless - the copy is replaced by a newer one. With version 2 the slot
is the core's texture, and a core let back in then would draw into what
the frame about to be recorded reads. So for version 2 slots the wait
also covers the window: until no queued frame names the slot, on the
condition the video thread already broadcasts when it claims a frame and
when it finishes one. Other APIs' waits are untouched.

d3d12.c, video_thread_hw.c and runloop.c pass tools/mingw_syntax_check.sh
(win32 gnu99 and the C89 lane). Not run: it needs a core that asks for
version 2, which is the next change, and a Windows GPU to run it on.
samples/gfx/d3d12_hw_interface_v2 still runs the contract itself.
2026-09-19 19:50:07 +02:00
LibretroAdmin 75f55ed57d libretro_d3d12.h: version 2, where each side says when it is done with the frame
Version 1's whole handoff is set_texture(texture, format). It does not
say when the texture is complete or when the core may draw into it
again, so the only safe thing a frontend can do is copy it before
video_refresh returns - and one that presents from another thread has to
record and submit that copy on the core's thread. That is the cost the
threaded video statistics show for a D3D12 hardware core next to a
Vulkan one (about 23 us a frame against 5): not D3D12, whose queues are
free-threaded and whose fences do what Vulkan's semaphores do, but an
interface that exposes none of it.

Version 2 adds what the Vulkan interface has had all along:

  get_sync_index / get_sync_index_mask
      the core keeps one present texture per index and is told which to
      draw into
  wait_sync_index
      blocks until the frontend has finished with the texture last
      handed over for the current index; until then it is not the core's
  set_texture_fenced(texture, format, fence, value)
      the frame, complete once the fence reaches the value; the frontend
      does not read it before then, on whatever queue or thread it reads

The struct is version 1's with members added at the end, so one object
serves both, and RETRO_HW_RENDER_INTERFACE_D3D12_VERSION stays 1 because
existing cores compare against it and reject anything else. A core gets
version 2 only by asking, through the negotiation interface libretro
already has: RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_D3D12 is new
in libretro.h, and retro_hw_render_context_negotiation_interface_d3d12
carries the highest version the core can use. An older frontend answers
GET_..._NEGOTIATION_INTERFACE_SUPPORT with 0 for the type and the core
carries on with version 1, having asked for nothing.

This is the contract and a proof that it is sufficient; no driver
implements it yet and no core asks for it, so nothing changes for
anyone until they do.

samples/gfx/d3d12_hw_interface_v2 runs it: a core and a frontend that
presents from its own thread, sharing a device and a queue, with the
handoff above and no copy on the core's thread. The frontend is slower
than the core, so the core laps it and has to block in wait_sync_index;
every frame must reach the frontend holding the frame number it was
published with. 240 of 240 do. A core that skips wait_sync_index is
built in as the control and is caught on its first lap ("index 0 should
hold frame 0, holds frame 237"); run.sh requires that. Built with
mingw-w64, run under Wine with vkd3d over Mesa's software Vulkan driver,
and added to the gfx samples lane, which fails rather than skips if it
finds no D3D12 device. The header compiles as C89 and as C++.
2026-09-19 19:24:51 +02:00
libretroadmin 6af2e74598 Don't mention specific cores 2026-09-19 19:22:31 +02:00
libretroadmin 79b7ddb9cf d3d11_hw_ring: the runner the workflow calls, and the rule that swallowed it
.gitignore carried a bare run.sh, which git matches at every depth, so
the script the gfx samples job calls could never be added and the job
has been failing on a file that is not in the tree. The rule is for
the checkout's own run.sh and now says so.

run.sh builds the oracle and the deferred proxy it exercises with
mingw-w64 - dxguid and uuid carry the interface GUIDs the proxy names
- brings up an Xvfb display when the job has none, and runs the three
cases the test documents: the ring and the immediate context must
pass, and the ring as it used to be, resetting the context every
frame, must fail. A build where that last one passes has lost the
regression the sample exists for.
2026-09-19 19:10:45 +02:00
LibretroAdmin 89912eb6c9 (D3D12) HDR: the frame's trip from the back buffer to the swapchain is opaque
The PS2 core on D3D12 with its hardware renderer showed a black picture
with HDR on: audio, input, widgets, statistics and the overlay all fine.
With HDR off it showed its picture. With the core's software renderer it
showed its picture either way.

With HDR on, and a menu or an overlay up, the frame is drawn into
chain.back_buffer and then composited onto the swapchain image. That
composite used pipes[VIDEO_SHADER_STOCK_HDR], whose blend state is
SRC_ALPHA / INV_SRC_ALPHA, and the HDR shader returns the sampled alpha
with the colour. So the frame was blended over the freshly cleared
swapchain image at whatever alpha the core's texture holds.

For a software frame that is 1: XRGB8888 has no alpha to read. A
hardware core hands over its own render target. The PS2 core's holds the
guest's alpha - the GS keeps destination alpha in the frame buffer, and
for most of a frame it is zero or 0x80 - so its picture was composited at
zero or half over black. The software renderer's output is opaque, which
is why the same core showed a picture in that mode; and the overlay the
reporter runs is what put the frame through the back buffer at all.

The composite now uses VIDEO_SHADER_STOCK_NOBLEND_HDR: same shader, same
target format, blending off. There is nothing under the frame to blend
with - the swapchain image is cleared immediately before - and the SDR
path has always drawn the frame with blending off. The second use of
STOCK_HDR further down, which lays the back buffer under the menu and
widgets, is left as it is.

Found with the D3D12 debug layer forced on from dxcpl, which showed the
core's own draws to be clean, and the reporter's HDR on/off and
software/hardware results. Passes tools/mingw_syntax_check.sh. Not run:
no HDR output here.
2026-09-19 19:09:33 +02:00
LibretroAdmin b178b90772 (D3D12) Overlays pick the HDR blend pipeline when the back buffer is HDR
With HDR on, the D3D12 debug layer reports, for every overlay image of
every frame:

  ID3D12CommandList::DrawInstanced: The render target format in slot 0
  does not match that specified by the current pipeline state.
  (pipeline state = R8G8B8A8_UNORM, render target format =
  R10G10B10A2_UNORM) [RENDER_TARGET_FORMAT_MISMATCH_PIPELINE_STATE]

Thousands in a ten-second capture with an input overlay loaded, and
nothing else.

d3d12_render_overlay() binds sprites.pipe_blend unconditionally. That
pipeline is built for an R8G8B8A8 target; the overlay is drawn onto the
swapchain's back buffer, which under HDR is R10G10B10A2 or
R16G16B16A16_FLOAT. A draw whose pipeline disagrees with the bound render
target is invalid, and what it does without the debug layer is up to the
driver. Every other sprite draw in the file - blend_begin, blend_end,
the font, the menu sprites on either side of the overlay call - already
picks the _hdr pipeline by testing chain.current_rt_format. The overlay
now does the same. It is drawn between the same root CBV and sprite
setup the menu sprites use, so PSMainHDR sees the state it expects.

Passes tools/mingw_syntax_check.sh. Not run: no HDR output here, and
Wine has none to offer.
2026-09-19 19:09:29 +02:00
libretroadmin 86b3de3205 CI: the samples no other workflow runs
A sample that builds, runs and asserts something, and that no
workflow names, is a test nobody runs. font_lifecycle sat that way
long enough for its expectation to drift away from the font driver it
was written against, and it is not alone: fifteen others are runnable
and unclaimed, from the audio pipeline's format and layout oracles to
the overlay alpha-flag and viewport seqlock ones.

This job takes whatever is left over, under the address and undefined
sanitizers with leak detection on. The list is computed from the
sample tree and the workflows rather than written down, so a new
sample is covered by existing rather than by being remembered, and a
sample that gets a workflow of its own drops out of here on its own.
Samples needing a library the runner does not carry are skipped by
name; their own workflows install what they need.
2026-09-19 19:07:26 +02:00
libretroadmin 0acf4dbe11 font_lifecycle: a rebuild marshals whatever the hint says, and the sweep asks make what it can run
The sample held the older rule - that a font made without a threading
hint rebuilds on the calling thread - and nothing ran it, so it sat
there disagreeing with the code. A rebuild goes through the video
thread whenever the wrapper is up, whatever thread made the font: a
font size change rebuilds the OSD font from the settings path on the
main thread, and a GL backend making its context current there while
the video thread holds it is an X BadAccess. The hint decides where a
font is created, not where it is rebuilt, and the sample now says so
and runs beside the other gfx samples.

The sweep found it, but only after it stopped inventing failures:
"does this sample have a check target" was a grep for check:, which a
comment satisfies, so samples whose runnable target is called
something else were sent to a rule that does not exist. make -n
answers the question properly.
2026-09-19 18:58:10 +02:00
LibretroAdmin 403f441f95 menu: fold the console/mobile widget scale range and handler into its row
The console/mobile Graphics Widgets Scale Override row is a plain
S_FLOAT row, which has range and OK handler slots, but it left them
empty and relied on two builder lines after ADD_DESC to supply the
0.2-5.0 range and setting_action_ok_uint. That dependency is what
broke in fb4a17e562 (fixed in #19584): the builder lines moved into
the desktop branch and the console row was left with nothing.

Put the range and handler in the row itself, the same way the
windowed row already does, and move the two builder lines back into
the desktop branch where only the fullscreen LV row still needs them
(the LV float grammar has no range or handler slots). The comment
explaining that is restored.

No behavior change: SDESC_RANGE_MINMAX with 0.2/5.0/0.01 is the same
range, enforced at both ends, that add_range(..., true, true) applied,
and the handler is the same. Desktop rows are untouched.
2026-09-19 18:54:35 +02:00
Artem Lytkin 365b4ba082 menu: restore widget scale override range on console and mobile (#19584)
fb4a17e562 moved the OK handler and the 0.2-5.0 range of the widget
scale row into the desktop branch, so on console and mobile builds
Graphics Widgets Scale Override was left with no range and no OK
action and could not be changed. Apply them after either row again.
2026-09-19 18:55:39 +02:00
LibretroAdmin fa4879f8fe (D3D12) C89: hoist loop counters out of for-init in the HDR setters
Six d3d12_set_hdr_* setters declared their counter inside the for
statement (for (unsigned i = 0; ...)), which is C99 and does not build
as C89. Declare it at the top of the block instead. No functional
change.
2026-09-19 18:47:25 +02:00
LibretroAdmin 7364d29509 (D3D11/D3D12) Screenshot: keep the read window inside the backbuffer
read_viewport and read_viewport_hdr clamp the viewport width and height
to the full output size, but never check x + width or y + height. The
viewport origin is user-controlled (custom aspect ratio), so a viewport
pushed off the right or bottom edge made the CPU copy loops - and the
shared dxgi_hdr_readback_* decoders, and the GPU HDR readback loops -
read past the end of each source row and, on the last rows, past the
end of the mapped staging/readback memory.

Add dxgi_readback_clamp_window() and call it in all four readback
entry points, against the real source size (the staging desc on D3D11,
the swapchain size on D3D12). Width and height double as the stride and
row count of the caller's buffer, which task_screenshot clamps the same
way, so they are left alone and the window is slid back inside the
surface instead. Vulkan already clamps its copy extent; the D3D drivers
did not.
2026-09-19 18:47:21 +02:00
LibretroAdmin 6929c9048e (D3D11) Threaded video: the core's deferred context keeps its state across frames
A D3D11 hardware core under threaded video showed one frame and then
black, with widgets, overlays and statistics still drawn over it. With
threaded video off the same core showed its picture. Reported on the PS2
core.

The hardware ring gives the core a deferred context and closes its
recording at every video_refresh with FinishCommandList(..., FALSE, ...).
FALSE resets the deferred context to the default state each time. A core
written against the immediate context does not expect that: what it
binds stays bound there, because the frontend's own frame does not touch
those slots, so it binds some things once and never again. The PS2 core
binds its vertex-expansion buffer to VS slot 0 once, at device creation,
and the state it rebinds after each present does not include it. From
the second frame on that slot was empty, every sprite, line and point
collapsed to nothing - the screen and depth clears games draw as sprites
among them - and the picture was black.

TRUE keeps the deferred context's state across lists, which is how the
immediate context the core was written for behaves. The value lives in
d3d11_deferred_proxy.h as D3D11_HW_RING_KEEP_CONTEXT_STATE so the test
below uses what the driver uses.

samples/gfx/d3d11_hw_ring runs the ring's sequence - record through the
real proxy, take the texture at PS slot 0, FinishCommandList, replay on
the immediate context, copy - against a core that binds through a cache,
rebinds a fixed set after every present and has one binding it makes
once. It draws a different colour each frame over a black clear and
reads the frontend's copy back. Built with mingw-w64 and run under Wine,
where wined3d implements deferred contexts over OpenGL: the ring and the
immediate context show all four frames; the ring with FALSE shows the
first and then black, and run.sh requires that it still does. Added to
the gfx samples lane, which already has mingw and Wine. Nothing on Linux
exercised the D3D11 ring before this.

d3d11.c passes tools/mingw_syntax_check.sh. Not run on real Windows.
2026-09-19 18:39:20 +02:00
LibretroAdmin ba12bd21cf (D3D12) Screenshot: propagate CPU HDR decode failure
d3d12_gfx_read_viewport() ignored the return value of
dxgi_hdr_readback_to_bgr24() on the CPU HDR fallback path and always
returned true, so a failed decode was reported as a successful
screenshot with an unwritten buffer. D3D11 already returns false here;
make D3D12 match.
2026-09-19 18:37:19 +02:00
LibretroAdmin 27114685e1 (D3D11) HDR screenshot: clear staging MiscFlags, init BackBuffer
Follow-up to #19585. d3d11_gfx_read_viewport_hdr() builds its staging
desc from the swapchain backbuffer desc the same way the SDR path did,
but never cleared MiscFlags. Backbuffer misc flags are not valid on a
D3D11_USAGE_STAGING texture, so strict drivers (seen on Qualcomm
Adreno X2-90 for the SDR path) reject CreateTexture2D. The HDR path
already NULL-checks the result so it did not crash, but the HDR
screenshot silently failed.

Also initialise BackBuffer to NULL so the !BackBuffer check after
GetBuffer is not testing an uninitialised pointer when GetBuffer
fails.
2026-09-19 18:34:54 +02:00
Quan Wei e6b62c0e81 Handle D3D11 screenshot staging creation and map failures (#19585) 2026-09-19 18:35:46 +02:00
LibretroAdmin a47f2cd0dc threaded_video harness: the dupe lane does not free a frame the cache still holds
The dupe lane added with 8ae3e66882 failed the ASan lanes:

  heap-use-after-free ... READ of size 64
    #1 video_thread_frame gfx/video_thread_wrapper.c
    #3 video_driver_cached_frame
    #6 run_frames
    #7 lane_dupe_under_wrapper
  freed by thread T0 here:
    #1 lane_dupe_under_wrapper

The test's fault, not the wrapper's. video_driver_frame() keeps the
pointer it is given as the cached frame - borrowed, exactly as it
keeps a core's framebuffer - and the menu re-presents it on the next
frame it draws. The lane malloc'd its test pattern, pushed it, freed
it, and then ran two more frames with the menu up, which went back
for it.

The pattern is now a static buffer, so what the cache borrows stays
valid the way a core's framebuffer does, and the lane invalidates the
cached frame before it hands back, so the lanes after it do not find a
test pattern there.

I had only run the lane on a plain build, which is how this got
through. Run this time the way CI runs it, on trees built for it:
SAN=address (ASan+UBSan) and SAN=thread, CYCLES=2, all lanes pass.
2026-09-19 17:22:52 +02:00
LibretroAdmin 050200bb62 settings: regenerate settings_table.reference
tools/settings_check.py table --check fails on master: two settings
were added without the reference being regenerated alongside, so the
"Settings table drift" CI job reports drift for every change that
follows, whatever it touches.

  bool  audio_fastforward_callback
  uint  menu_thumbnail_preview_threads

Regenerated with

  python3 tools/settings_check.py table --write tools/settings_table.reference

Those two rows, in each of the three configurations, are the whole
difference. table --check, unity (desktop and mobile) and
settings_migrate_selftest.py all pass.
2026-09-19 17:00:12 +02:00
LibretroAdmin 6081dd9790 configuration: audio_asio_output_channel is an ASIO setting, not a WASAPI one
A Windows build configured with --disable-wasapi did not compile:

  configuration.c: error: 'struct <anonymous>' has no member named
  'audio_asio_output_channel'

The field was declared in settings_t inside the #ifdef HAVE_WASAPI
block, next to audio_wasapi_sh_buffer_length. Everything that uses it -
its row in settings_def_audio_asio_action.h, the descriptor and
configuration tables that row expands into, the menu representation in
menu_setting.c, and asio.c itself - is under HAVE_ASIO, and ASIO and
WASAPI are switched independently. With ASIO on and WASAPI off the row
was emitted and the field was not there.

It gets a HAVE_ASIO block of its own. Builds with both drivers, or
with neither, see no difference.

Checked with a mingw cross build's own compile lines, HAVE_ASIO
defined and HAVE_WASAPI not: configuration.c fails before and compiles
after; menu_setting.c and asio.c compile.
2026-09-19 17:00:08 +02:00
LibretroAdmin 8ae3e66882 video_thread: a duped frame reaches the driver as NULL, not as an old frame
With threaded video on, the ffmpeg core ghosted on Vulkan and D3D12 -
every frame doubled with an older one - and on D3D11 faded to grey and
back. gl and glcore were clean, because there the core renders with
OpenGL itself and never sends pixels at all.

A core with nothing new calls video_refresh with NULL, and the driver
repeats the frame it has. The ffmpeg core does that for every display
refresh that falls between two video frames: every other call for a
30 fps clip on a 60 Hz display.

video_thread_frame() treated that push like any other. It picked a
free ring slot, copied nothing into it because there was nothing to
copy, and published it. The video thread then gave the driver that
slot's buffer as the frame. What the buffer held was whatever had last
been put there: the frame from two pushes before, or - when the real
frames all arrive zero-copy in the other slot, as the ffmpeg core's
now do - something older still, down to the 0x80 fill the slots are
allocated with. The driver uploaded it and showed it as new. So the
display went new, old, new, old at the refresh rate.

A slot now records that its push carried no pixels, and for such a
slot the video thread hands the driver NULL, which is what the core
sent and what every driver already gets without the wrapper. The
pixel-format conversion and the video filter are skipped for it, as
they have nothing to work on. A hardware frame the ring could not
publish takes the same route; the comment there already said it was
to be treated as a dupe.

Regression test: a new lane in the threaded_video harness pushes
pixels, NULL, pixels, NULL through the wrapper with the driver's
frame() interposed, and requires the driver to see exactly that. On
the old code it fails with the driver given stale pixels for every
dupe; with this change it passes.

Also run on a real build under Xvfb, ffmpeg core on the software path:
Vulkan and gl1, threaded video on and off.
2026-09-19 16:40:57 +02:00
LibretroAdmin a38053c8f8 glcore: gl3_t::flags is 32 bits, so the flags above bit 15 exist
Loading a hardware-rendered OpenGL core (the ffmpeg core, any GL core)
on Windows with the glcore driver and threaded video on crashed at
load: a call through NULL out of the core's context_reset(), with

  [WGL] wglMakeCurrent(core context) failed: 0x000000aa.

just before it. 0xaa is ERROR_BUSY: the core's context was current on
another thread. It was - the video thread had taken it.

gl3_t::flags is a uint16_t, and two flags are defined past the end of
it:

  GL3_FLAG_HW_RING_EXPECTED = (1 << 16)
  GL3_FLAG_OVERLAY_BORROWED = (1 << 17)

`gl->flags |= GL3_FLAG_HW_RING_EXPECTED` therefore stores nothing, and
every test of either flag is false, with no warning from any compiler.

HW_RING_EXPECTED is what gl3_core_context_is_mains() reads between
init and the ring coming up, to keep the video thread from taking the
core's context while the main thread is about to. With the flag lost,
the bind at the end of gl3_init() ran, the video thread left init
holding the core's context, and the main thread's wglMakeCurrent() for
it failed. With no context current, every GL function the core then
looked up came back NULL, and it called the first.

OVERLAY_BORROWED is what stops glcore deleting overlay textures that
belong to the overlay pack (load_textures). With the flag lost, glcore
deleted textures it had only been lent.

The field is now a uint32_t, as gl2's already is. Nothing else
changes: every other flag keeps its bit.

Found with a mingw build under wine, which fails the same way as a
real Windows box: before, the 0xaa error and no frames; after, the
core's context goes current on the main thread, 117 frames are pushed
and the process exits 0. glcore on Linux unchanged, threaded video on
and off.
2026-09-19 16:27:34 +02:00
LibretroAdmin 7a533d7d68 libretro-common: fastjmp_buf carries the alignment its Win64 stores need
The Win64 fastjmp_set saves xmm6-xmm15 with movaps, which faults on an
address that is not a multiple of 16. The header says the buffer is
"16-byte aligned for the movaps stores" in a comment over a plain
unsigned char array, whose alignment is 1. A file-scope static gets away
with it because compilers put a 240-byte static on a 16-byte boundary
anyway; a fastjmp_buf that is a struct member after a char sits at +1,
and the first fastjmp_set dies at "movaps %xmm6, 0x50(%rcx)".

The member is declared aligned to 16 with the MSVC or GNU spelling, both
valid in C89, so alignof(fastjmp_buf) is 16 on Win64. The System V and
AArch64 forms store through general-purpose moves and stp and are
unchanged, as is the setjmp fallback.

fastjmp_test gains that case: a buffer placed after a char, its address
checked on Win64, and a set and jump through it everywhere. Linux has no
aligned stores, so the Linux samples job could never have seen this; the
MSYS2 job in Cross-libc-vfs-samples builds and runs fastjmp_test too
now, under a second, and triggers on the fastjmp paths. Built with
mingw-w64 and run under Wine: the new case fails on the old header at
the address check and then faults, and passes on this one.
2026-09-19 16:14:13 +02:00
Craig 161c36628e windows: switch to new gcc 16 docker (#19579) 2026-09-19 16:04:34 +02:00
LibretroAdmin d0b1630ff9 video_thread: the ring's GL context is given back at free, not dropped
Closing content with threaded video on, the glcore driver and an OpenGL
hardware core (the ffmpeg core is the easy one to hit it with) killed
the process on X11:

  X Error of failed request:  BadAccess
  Major opcode of failed request:  150 (GLX)
  Minor opcode of failed request:  26 (X_GLXMakeContextCurrent)

The hardware ring takes the core's GL context on the main thread at
init and has to give it up there at free - only the thread a context
is current on can release it. video_thread_hw_free() runs on the video
thread, inside CMD_FREE, so it hands that release to the main thread
through video_thread_call_on_waiter().

video_thread_call_on_waiter() finds the wrapper through
video_thread_thr_capture, and video_thread_free() clears the capture
before it sends CMD_FREE. So by the time the ring asked, there was no
wrapper to be found, and the function returned without calling
anything. The core's context stayed current on the main thread, and
gl3_deinit_hw_render(), a few lines later on the video thread, bound
it: BadAccess on GLX, which Xlib answers with exit(1). On WGL the same
bind is a failed wglMakeCurrent, the context cannot be deleted while
another thread holds it, and the main thread goes into the next
session still holding a dead context.

video_thread_free() now names the instance it is freeing for the
length of CMD_FREE, and video_thread_call_on_waiter() falls back to it
when the capture is gone. The capture itself still clears first, so
the font and texture entry points stop taking the instance exactly
when they did. And with no wrapper to be found at all the call is made
in place, as it is in a build without threads, instead of vanishing.

Verified on a real build under Xvfb (Mesa): glcore + threaded video +
ffmpeg core exits 1 with the X error before, exits 0 after.
2026-09-19 15:55:46 +02:00
LibretroAdmin 6c9817e510 gl2: a hardware core under the threaded wrapper always gets its context
Loading a video in the ffmpeg core with the gl driver and threaded
video on crashed at load. On Windows it is a call through NULL out of
the core's context_reset(), from drivers_init(); on Linux a segfault in
the same place, after

  [Video] Threaded video: the core's GL context could not be taken on
  the main thread; the core will have no context.

Under the wrapper an OpenGL core renders on the main thread while the
driver draws on the video thread, so the core cannot borrow the
driver's context as it does unthreaded. It needs one of its own that
shares objects with the driver's: the shared context, which the
hardware ring makes current on the main thread before context_reset()
runs. gl2_hw_ring_context_new() refuses when there is no shared
context to take.

gl2 only ever made a shared context when the setting was on or the
core had asked for one with SET_HW_SHARED_CONTEXT. A core that did
neither - the ffmpeg core is one - was therefore sent into
context_reset() with no context current at all. On WGL every function
it looks up without a context comes back NULL, and it called the
first one.

glcore has always forced the shared context for hardware cores. gl2
now does the same whenever the wrapper is running and the ring is
allowed for the session, which is exactly when it cannot work without
it. Unthreaded behaviour is unchanged.

Verified on a real build under Xvfb (Mesa), gl + threaded video +
ffmpeg core: crashes at load before, plays and exits 0 after; threaded
off unchanged.
2026-09-19 15:55:41 +02:00
kaysedwardsandLibretroAdmin e8f9fec4c7 Support More Video Files (#19583)
* I'm pleased to release a new variant of my changes
to the FFMPEG core. I have, I believe, fixed the
issues with the current code as well as my old
version. The core should now play files containing
many more video codecs--including HEVC and AV1 files.

The issue stemmed from how avcodec_send_packet
and avcodec_receive_frame must be mutually looped
when using the modern FFMPEG interfaces; the current
code essentially disregards the returned value so
does the wrong thing in certain scenarios.

I've also changed the `seek_adjust` function slightly
so that the time adjustment is nearer to exactly five
minutes for the common input.

I've also cleared the SEEK_L2_STR and SEEK_R2_STR
variables in a few more places to fix a bug where
seeking could get stuck on the wrong adjustment.

I've also fixed the description of the controls;
they are currently misreporting a 60 second seek
instead of the implemented 30 seconds.

A few more bugs, unfortunately, remain: the audio
decoder has a bug not unlike the bug in the video
decoder; changing the audio to a stream having a
different rate will increase or decrease the speed
of the video being played; the implementation is
somewhat fragile in the face of broken files in
that stuttering can accumulate and even crash
the decoder.

A note on reporting bugs: please, before you report
a bug with a certan file formats, run your sample file
through FFMPEG to make sure the file itself contains
no errors. As I mentioned above, the implementation
is somewhat fragile; the core, as is, really needs
files that contain no errors for proper function.

* ffmpeg core: review fixes for the send/receive decode loop change

Four things found reading the change:

* A comment in seek_frame() picked up half of another line in an
  edit ("taken us to thestrength without using"). Put back.

* select_decoder() cleared FF_THREAD_FRAME with ^=. It works there,
  because the bit was set two lines up, but it is a toggle, not a
  clear, and the next edit that moves the assignment turns it into
  a bug. Use &= ~.

* sws_worker_thread() now builds its scaler from the decoded frame's
  own size and format instead of the container's, which is the right
  thing, but it means a frame from a damaged file can be one that
  sws_getCachedContext() refuses - and the NULL went straight into
  sws_setColorspaceDetails() and sws_scale(). Check it, log it, and
  still finish the slot so the ring keeps its order and the main
  thread is not left waiting for a frame that never completes.

* The rewrite left two things behind that now warn: decode_thread()
  still computed a frame_size nothing takes any more, and
  video_buffer_has_open_slot() lost its only caller. Both removed.

* ffmpeg core: software video path stops copying frames it does not have to

A frame on the software path (any driver that is not GL - Vulkan,
D3D, Metal) was copied twice after the scaler was done with it: row
by row out of its ring slot into a buffer of the core's own, and then
by the frontend out of that buffer into its texture. At 3840x2160
that is 33 MB a copy, 60 times a second.

The first copy bought nothing. The frame is already sitting in a slot
in the layout retro_video_refresh_t takes, so now the slot is what
the frontend is given.

The second is what RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER
is for. The core asks for the frontend's framebuffer every frame, as
the API requires; a driver that has one (Vulkan, D3D12) hands out the
mapped texture it draws from, the frame is copied into that, and the
frontend's own copy does not happen. One copy from scaler to GPU
instead of two. Where the frontend has nothing to offer, or offers a
format that is not XRGB8888, the slot is passed directly and it is
one copy that way too.

Showing a frame out of its slot means the slot cannot go back to the
decoder the moment it has been shown: the frontend may come back for
the last frame it was given (a paused menu redraws it), and by then
the decoder would be halfway through writing the next picture into
it. So the slot is held until the frame after it is due. Holding is
simply not opening - the slot stays finished at the tail, where the
decoder cannot go.

video_buffer_clear() (a seek) takes every slot back whether it is
held or not, and the same index can be finished again with a frame
nobody has seen by the time the holder lets go. The ring now counts
its clears, and a held slot is given back only if none happened since
it was taken; otherwise releasing it would drop that frame and push
the tail past it.

The software path's ring gets a fifth slot, so the decoder still has
four to run ahead with. That slot is the memory the temporary buffer
used to be: the buffer is now only allocated where it is still used,
which is the GLES texture upload.

Also checks the slot it is handed is not NULL, which the old code
did not: a clear between the wait and the take left ctx unset and
the next line read ctx->pts.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-09-19 15:40:54 +02:00
libretroadmin 0d7a5b4b1c The sample sweep runs what it builds, and two things that found
tools/sample_link_check.sh linked every sample a change touches and
called that a check. A sample that builds and passes plainly can
still be the one holding a leak or a use-after-free, and only the
sanitized run says so, so a sample with a check target is now built
and run the way its CI job runs it: address and undefined sanitizers,
leak detection on. Both modes go through one routine - the --all path
had a loop of its own, which is how the full sweep went on linking
while the selected one checked. A sample that cross-builds a Windows
target is built and not run, since this host cannot run it.

Two things it found on the first pass. pipeline_clocked builds a sinc
resampler per run through retro_resampler_realloc and never released
it, leaving seven behind and 232 KB with them; the teardown releases
it as audio_driver_deinit does. And input_overlay_textures matched a
page's entry to the pack's unique image by the pixel buffer they
share, which is NULL on both sides once the pixels are released - and
NULL matches the first image every time. A released buffer matches
nothing now, so the pack is declined and reloaded from its path
instead of drawing one image everywhere.
2026-09-19 15:23:12 +02:00
LibretroAdmin 52babd3fef qb: only enable XCB when x11-xcb is present
HAVE_XCB was enabled by the libxcb check alone, but the code it gates
(gfx/common/vulkan_common.c) includes X11/Xlib-xcb.h and calls
XGetXCBConnection, and Makefile.common links -lX11-xcb. Those come
from the separate x11-xcb package (libx11-xcb-dev on Debian/Ubuntu),
so a box with libx11 + libxcb but no x11-xcb failed with:

  gfx/common/vulkan_common.c:30:10: fatal error: X11/Xlib-xcb.h:
  No such file or directory

Check for x11-xcb in configure and turn HAVE_XCB off when it is
missing, and link via X11_XCB_LIBS instead of a hardcoded flag.

The X11 Vulkan context only ever creates an XCB surface, so build and
register gfx_ctx_vk_x only when HAVE_XCB is set rather than shipping a
context that cannot create a surface.
2026-09-19 15:07:28 +02:00
LibretroAdmin df0f7e7635 CI: a test that runs retroarch and looks at the overlay on the screen
Every overlay test in the tree stubs the driver or builds the pack by
hand, and every one of them passed while an enabled overlay covered
the whole screen. A person with threaded video off saw it in a second.
This test does what that person did.

samples/input/overlay_on_screen/overlay_on_screen_test.py takes a real
retroarch binary, starts it under Xvfb on Mesa's software GL with the
menu up and an input overlay enabled, grabs the root window with xwd
and reads the pixels - once with video_threaded off, once with it on.
The overlay is made on the spot: two solid magenta squares, top left
and bottom right, on a full-screen page at full opacity; magenta is no
colour the menu draws. It requires that

- the middle of each square is magenta (the overlay is up, in place);
- the middle of the screen is not (no image is stretched over it);
- magenta covers about the area of the two squares (8% here).

Against the previous commit's parent it fails in both modes with
"overlay covers 100.0% of the screen, its two squares are 8.0%"; with
the fix it passes at exactly 8.0% in both.

New workflow CI Linux overlay on screen: a slim X11 + OpenGL build
(shader compilers, the big menu drivers, networking and codecs off)
behind ccache, then the test, some twenty seconds. It runs when the
overlay frontend, the loader, gfx_surface, the video driver core, the
threaded wrapper or gl2 change.
2026-09-19 13:28:05 +02:00
LibretroAdmin 13c652060e Overlays: "has an image" is asked of the size, not of pixels that were released
An enabled input overlay covered the whole screen: every image of the
page drawn full-screen, one over the other (sonninnos; reproduced here
on the gl driver under Xvfb, threaded video off AND on).

The loader does not give the pack private image structs. For the first
overlay::image or overlay_desc::image that names a file,
task_overlay_load_image_texture() registers that struct's ADDRESS as
the pack's unique image, so input_overlay::images[] points into the
pages. Since 86fbf34e01 the pack's pixels are released once the driver
has the textures, with image_texture_free(images[i]) - which therefore
cleared .pixels inside the descs themselves. And "does this desc have
an image" was asked of .pixels, in four places: the page's and the
descs' vertex_geom, the movable-desc update, and the pressed-button
alpha. The very descs that had just been given textures answered no,
their geometry was never sent to the driver, and the driver drew each
image where a page load leaves it: over the whole screen.

e9468dedf8 moved that release later and kept it, and said of these
structs that they were "copies ... only ever tested ... and are left
to say so". They are not copies; that comment was wrong, and no test
could tell, because overlay_textures_test built its pack by hand with
private image structs.

- OVERLAY_HAS_IMAGE() asks the width. A desc or page without an image
  is calloc()ed and has none; one with an image keeps it for good
  (input_overlay_drop_pixels puts the sizes back after the free). The
  four tests use it.
- With nothing reading .pixels as a flag any more, every copy of a
  released pointer is cleared - the pages' load_images as before, and
  now the page and desc image structs that share the file - both when
  the pixels are dropped and when a surface in flight adopts them.
  Nothing is left pointing at freed memory.
- overlay_textures_test lays its pack out as the loader does: descs
  with images, images[] pointing into them, later users holding
  copies. A new "has image" lane requires every desc to still report
  its image once the page has gone as textures, and to hold no stale
  pointer. With the old question put back it fails for the four descs.
2026-09-19 13:27:59 +02:00
LibretroAdmin 946ef57971 Overlays: audit of the video drivers' overlay paths (frontend, GL, Vulkan, D3D9-12, Metal)
One audit, one commit. Every item below was found by reading the
overlay code of each driver after the gl2 blackout (78fda8c573); what
could be run here was, and what could not is said at the end.

Frontend
- input_overlay_load_active() always went on to set a page's per-image
  alpha and geometry after loading it. Twice the page may not be in
  the driver: load() can fail - its answer was thrown away - and a
  pack with neither textures nor pixels left is never handed to load()
  at all. The setters then ran with the new page's indices against
  whatever the driver held before. input_overlay_load_page() now
  answers none / pixels / textures, counts a load() that returned
  false as none, and the geometry is skipped for none. Every driver's
  load() ends in true on success and the threaded wrapper passes the
  answer back.

gl1, gl2, gl3 (glcore), vulkan
- 7661ab06e7 bounded vertex_geom and tex_geom in the three GL drivers
  and missed set_alpha: NULL overlay_color_coord with no page loaded,
  and a write past the page's block for an index off its end.
- Vulkan's three setters had no check at all; vk->overlay.vertex is
  NULL after vulkan_overlay_free().
  All now return for a NULL array or an index at or past the count.

d3d9 (cg, hlsl)
- load() set overlays_size whether or not the calloc() behind it had
  worked. It fails cleanly now; a page with no images is no page.
- set_alpha, vertex_geom, tex_geom: bounded, and NULL-checked.
- hlsl ignored LockRect()'s result and copied to whatever d3dlr held;
  cg already checked it.
- enable() set its flag inside a loop over the page's images, so never
  for a page with none. Set once.
- full_screen(): NULL driver or array is a return.

d3d10
- d3d10_free_overlays() released each texture and never freed the
  array holding them, while every load() made a new one over it: a
  leak per page switch and one at teardown. Freed; count and buffer
  pointer cleared with it.
- load() called through an unchecked calloc(), CreateBuffer() and
  Map(). A page that cannot be built is freed and reported.
- The setters mapped a buffer that might not exist and indexed it
  without a bound. One helper refuses a NULL buffer, an index at or
  past the count, and a Map() that failed.

d3d11
- The sprite buffer is "reused across pages while it is big enough" -
  but d3d11_free_overlays(), run at the top of every page load, had
  already released it, so every page switch was a CreateBuffer. The
  page and the buffer now go separately: the page at a page load, the
  buffer at teardown.
- A page whose sprite buffer could not be made or mapped kept its
  count; it is freed and reported.
- The setters ignored Map()'s result and wrote through pData, which a
  failed Map() leaves uninitialised, with no bound. Same helper as
  d3d10's.

d3d12
- A load_textures page copied each of the pack's d3d12_texture_t. The
  deferred upload's state (dirty, upload_fence, the upload buffer)
  lives in that struct, so copy and original part company at the
  first draw: the pack's struct stays dirty for good and every later
  page re-records an upload of every image it shares; and an animated
  image's next frame marks the pack's struct dirty while the driver
  draws from a copy that is not, so the animation stands still. The
  page now holds pointers (overlays.refs): into its own array for a
  load() page, to the pack's structs for a load_textures page.
- load_textures released the previous page's own textures and the
  sprite buffer without draining the queue. The comment's reason - the
  queue is drained at the top of every frame, so nothing reads them
  between frames - has it backwards: the last frame may still be in
  flight. The page free drains when, and only when, it has textures
  of its own to release.
- The sprite buffer was released at the top of every page load, so
  its reuse had nothing to reuse and every page switch was a drain
  and a new buffer. Kept across pages; released, behind a drain, at
  teardown.
- A page that cannot be built is freed and reported; a zero handle is
  refused rather than dereferenced; the setters go through the same
  kind of helper as d3d10's and d3d11's.

metal
- loadImages: stored -newTexture:'s result straight into an
  NSMutableArray; nil there is an exception. The page ends at that
  image and reports failure.
- -_getForIndex: answered &contents[index * 4] whatever the index and
  whether or not there was a buffer. NULL for both now, and the three
  updates return on NULL.
- Ownership needed nothing: the array retains the pack's textures.

CI
- tools/overlay_setter_bounds.py, beside the ownership check in CI
  Linux libretro-common tests: for each audited driver, set_alpha,
  vertex_geom and tex_geom - or the one helper they map through - must
  compare the index with the page's count before writing; for Metal,
  -_getForIndex: must check the buffer's length and its callers take
  NULL. Against the drivers as they were: 25 failures across all ten.
- overlay_textures_test asks load_page for its exact answer and gains
  a lane where the driver's load() fails.

Tested: both CI scripts; mingw gnu99 + linux-c89 syntax gate on all
eleven C files; gl1, gl2, gl3, vulkan and the frontend compiled in a
configured Linux build; overlay_textures_test plain and THREADS=1
under ASan + UBSan with leak detection. Not tested: metal.m cannot be
compiled here, and none of the D3D or Metal code has been run - no
runner and no machine here has those drivers.
2026-09-19 12:08:48 +02:00
LibretroAdmin d588459f53 CI: no video driver deletes overlay textures it was lent
No runner can run a GL or D3D driver, so the gl2 overlay blackout got
through with four correct siblings beside it. tools/
overlay_texture_ownership.py is the check at source level: every file
in gfx/drivers that implements load_textures must set its "borrowed"
marker when it takes the pack's handles, test it (negated) where it
frees a page, and clear it so a later load() page is not leaked.
metal.m is exempt, with the reason in the script: its textures are ARC
references. vulkan.c clears its marker by wiping the overlay struct,
which the script is told about.

Against gl2.c as it was, it fails with "the marker is never tested"
and "never cleared"; d3d11, d3d12, gl3 and vulkan pass. Runs beside
the VFS backend parity check in CI Linux libretro-common tests.
2026-09-19 11:52:50 +02:00
LibretroAdmin 78fda8c573 gl2: a page switch does not delete the overlay pack's textures
On the GL driver, rotating an Android device turned the on-screen
overlay into semi-transparent black boxes that stayed until the core
was restarted (reported by parkerlreed, Galaxy Fold, Adreno 840,
GLES 2 context, threaded video off).

Since df4b5a7e0b a page of the overlay is handed to the driver as the
pack's own texture names (load_textures). gl2_overlay_alloc() marks
such a page GL2_FLAG_OVERLAY_BORROWED - and nothing ever read the
flag. gl2_free_overlay() runs at the top of every page load and called
glDeleteTextures() on whatever names the previous page held, so the
first page switch deleted the pack's textures. A rotation is a page
switch (landscape to portrait and back), most names are on both pages,
and the pack - still cached, still sure it has its textures - went on
handing out dead names: an unbound texture samples as black, the
overlay's alpha does the rest. gl3, d3d11, d3d12 and vulkan honour
their marker; gl2 alone did not.

gl2_free_overlay() now deletes only names the driver made itself
(load()), and clears the flag so the next such page is not leaked.
2026-09-19 11:52:46 +02:00
LibretroAdmin f7393f3cc6 verbosity: flush every log line again
4552975f37 stopped flushing INFO/DEBUG lines to the log file and only
flushed [ERROR] and [WARN], to save the per-line fflush cost. In
practice that costs more than it saves:

- When RetroArch crashes, the lines that explain the crash are
  usually the informational ones written just before it. Those were
  exactly the lines left sitting in stdio's buffer and lost, which
  makes crash reports much harder to read in the cases where gdb
  does not help.
- Anything following the log live (tail -f, monitoring scripts) saw
  nothing until the buffer happened to fill (#19571).

Go back to the previous behaviour: one write and one flush per line,
for every tag. rarch_log_tag_wants_flush() and the flush parameter on
rarch_log_line_emit() are removed. The strcmp tag matching for the
Android log priority from afd57d615e stays - that overread fix is
independent of the flush policy.

The log-atomicity harness's flush lane now asserts the promise that
matters: a RARCH_LOG (informational) line must be readable from the
file as soon as the call returns, before any deinit.

Fixes #19571
2026-09-19 11:43:44 +02:00
Davey Hughes f9c96e80d9 vulkan: pass a failed mailbox fence wait on as the acquire's result (#19582)
The emulated mailbox checked its fence wait only for VK_TIMEOUT, so
VK_ERROR_DEVICE_LOST or an out-of-memory error handed the main thread
the image as VK_SUCCESS. A lost device was still caught, by the next
frame's submit, but only after that frame had been recorded and
submitted against it.

Carry the wait's result instead. An error is handed over the same way
as an image, so the main thread's existing acquire error path takes it
from there: it drops the swapchain and, for a lost device, raises
VIDEO_FLAG_GPU_DEVICE_LOST.
2026-09-19 10:55:46 +02:00
LibretroAdmin 405c7aad2c CI: cheevos_login_test runs against a TLS 1.3 capable Mbed TLS
The runner's libmbedtls-dev is 2.28, which has no TLS 1.3, so the mbed
leg could not see the session-ticket read failure (#19580) or the
missing psa_crypto_init(). Add a leg that builds Mbed TLS 3.6.0 - the
one release that shows both - and runs the harness against it. Library
only, about 20 s; a tarball download that still fails after retries
skips the leg with a warning instead of failing the job.

3.6.0 also ignores MBEDTLS_SSL_VERIFY_OPTIONAL under TLS 1.3, so the
harness can no longer lean on it: mbed_seed.c wraps the real backend's
ssl_socket_init and adds the test CA, as bear_seed.c does for BearSSL.
The handshake now verifies on every Mbed TLS version.

Makefile: MBEDTLS_DIR=<built tree> selects the library, and clean
removes both seed objects whichever SSL_BACKEND it is run with - a
stale mbed_seed.o built against other headers links and then fails
every TLS case.

Verified on 3.6.0: without the psa_crypto_init() change 4 cases fail,
without the ticket change case 6 fails, with both all pass (ASan+UBSan).
2026-09-19 10:44:23 +02:00
LibretroAdmin 9e09ed3783 net_socket_ssl_mbed: call psa_crypto_init() on Mbed TLS 3.x
Mbed TLS 3.x runs TLS 1.3 on top of PSA and leaves psa_crypto_init()
to the application. The backend only called it for 4.x. 3.6 compiles
TLS 1.3 in by default, so with a system 3.6 the result was:

  3.6.0   every handshake fails with MBEDTLS_ERR_SSL_INTERNAL_ERROR
          (-0x6C00) while building the ClientHello key share - no
          HTTPS at all, RetroAchievements login included
  3.6.1+  the library quietly stops offering TLS 1.3 when PSA is not
          initialised, so connections fell back to TLS 1.2

Call it on 3.x whenever TLS 1.3 or MBEDTLS_USE_PSA_CRYPTO is compiled
in. The bundled 2.x and the 4.x path are unchanged.
2026-09-19 10:44:23 +02:00
Davey Hughes 1e1300cca4 net_socket_ssl_mbed: read past TLS 1.3 session tickets (#19580)
A TLS 1.3 server sends NewSessionTicket messages after the handshake.
Mbed TLS 3.6.0 and 4.x report each from mbedtls_ssl_read() with
MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET, asking to be called again;
3.6.1 and later 3.6 releases drop tickets by default. Neither receive
path knew the code. The nonblocking one only got past it when isagain()
happened to find EAGAIN left in errno, and otherwise failed the
transfer, so an HTTPS request to a TLS 1.3 server such as
retroachievements.org failed at random with receive_header_failed: the
cause of intermittent "Login failed: No response" with those versions
as the system Mbed TLS. Keep reading instead.

cheevos_login_test gains a case that reads with the tickets already
buffered and errno cleared, which fails without this change on Mbed TLS
3.6.0 or 4.x.
2026-09-19 10:40:24 +02:00
LibretroAdmin 8fda74c6b5 CI: late handles, promotion, and a pack freed mid-upload are pinned
overlay_textures_test, THREADS=1:

- late handles now holds the whole sequence: both pages through load()
  with live pixels while the uploads are pending, the uploads posted
  exactly once, the page on screen promoted to textures when the
  handles are in, the pixels gone after that and not before, a second
  promote doing nothing, and the next page switch uploading nothing;
- late and declined / late and failed: the handles come in and the
  driver declines them, or the thread could not make them - load()
  from then on, pixels kept, no texture held for nothing;
- freed in flight: the pack is freed, and separately the driver torn
  down, with the uploads still queued, and the video thread runs
  afterwards. Before gfx_surface_free_adopt() this was a
  heap-use-after-free on the video thread's read of the pixels; the
  torn-down pack that lives on reports nothing to come back from and
  is never handed to load().

Already in the CI step (ASan + UBSan with leak detection), so the
adopted pixels are checked for being freed as well as for not being
freed early.
2026-09-19 10:36:45 +02:00
LibretroAdmin 7d27f3a9d2 Overlays: threaded video gets its textures, and a pack freed mid-upload is safe
Two things the threaded lanes of overlay_textures_test turned up.

Under threaded video the pack's textures were never used.
input_overlay_upload_textures() posts the uploads and polls for the
handles in the same breath; the video thread has not woken yet, so the
handles are not there, and the function released everything and had
the page shown through load(). The uploads still ran, and their
textures were unloaded as they completed. The next page switch did it
all again: three page loads of a three-image pack measured nine
texture uploads thrown away and three load() calls, which is more work
than before the pack's textures existed.

Late handles are now a state, not a failure. The surfaces are kept,
the page goes through load() this once, and
input_overlay_promote_textures() - one call per input poll, a few
pointer tests when nothing is pending - hands the page on screen to
the driver as textures when the last handle is in, after which the
pixels go as they do unthreaded. A page switch in the meantime posts
nothing. A pack that will not be shown as textures (upload failed,
driver declined) keeps its pixels and gives back what was uploaded.

And a use-after-free, narrow but real. A still image's upload borrows
the pack's pixels (gfx_surface_submit_external). Freeing the pack, or
tearing the driver down, with that upload still queued left the video
thread to read freed memory: the surface knew to outlive its flight
(dying), the pixels did not. gfx_surface_free_adopt() lets such a
surface take the pixels with it and free them at the completion;
input_overlay_release_textures() uses it, and the image is left with
its size and no pixels, exactly as after the pixels are dropped.
input_overlay_has_source() now asks every image for its pixels, not
the first, since a release in flight takes only some.
2026-09-19 10:36:45 +02:00
LibretroAdmin e8817a997e CI: the overlay's textures under threaded video, both ways the race goes
overlay_textures_test was built without HAVE_THREADS, so what happens
under the threaded wrapper was read off the code and never run. With
THREADS=1 the sample now links a stub of the wrapper's async upload
path: video_thread_async_post() queues the node as the real one does,
video_thread_async_poll() delivers completions on the caller, and the
video thread is a function the test calls at the point in the sequence
where it wants that thread to have got round to its queue. The race
between the posts and the poll is therefore the test's to decide, and
both outcomes are lanes:

- prompt handles: the uploads finish before the poll. Both pages go as
  textures, each with its own images, and the pixels go afterwards.
  Against 86fbf34e01's code this lane fails exactly as the unthreaded
  one does - threaded video was never immune, only slow enough;
- late handles: the poll finds nothing, the page goes through load()
  with live pixels, the pack keeps them, and when the thread runs it
  reads pixels that are still there. Nothing outlives the pack.

Runs under ASan + UBSan in CI Linux samples/input beside the existing
two builds; well under a second.
2026-09-19 10:36:45 +02:00
Davey Hughes fffaa74e2f vulkan: keep waiting on the mailbox acquire fence instead of giving up (#19581)
vkAcquireNextImageKHR has already succeeded by the time the emulated
mailbox waits on the fence, so a VK_TIMEOUT there means only that the
presentation engine has not released the image yet. Treating it as a
failure left the thread idle without re-arming REQUEST_ACQUIRE, while
the main thread kept HAS_PENDING_REQUEST set, so nothing asked for
another acquire and video stayed frozen until fast-forward ended and
the mailbox was torn down.

Re-arming is not an option: the acquired image cannot be handed back,
and the next acquire would pass a fence that is still pending. Keep
waiting on the same fence, checking the DEAD flag between waits so
teardown stays prompt.

Drivers that let the fence rather than the acquire carry the wait reach
this. AMD's Windows driver holds the fence for tens of milliseconds
during a display stall, and minimizing a fast-forwarding exclusive
fullscreen window there froze video in 3 of 4 runs before this change
and in none after.

Leaving the wait on DEAD leaves the fence pending, and a pending fence
must not be destroyed, so wait on it once more first, bounded by the
same refresh-derived timeout as the mailbox's other waits so a driver
that never signals cannot hang the join.
2026-09-19 10:20:14 +02:00
LibretroAdmin 427de41229 CI: every route an overlay page takes to the driver is pinned
samples/input/overlay_textures compiles input/input_overlay_textures.c
as it is against the real gfx_surface and a stub driver that reads
every pixel it is handed, so a stale pointer is an ASan report rather
than a picture nobody looks at. The pack is two pages over three
images, one on both pages. Lanes:

- textures: a driver with load_textures gets each page's own images'
  textures and load() is never reached;
- page switch: the second page uploads nothing;
- pixels go: once the driver has the textures the pixels are
  released and the sizes stay;
- declined / no such path / upload fails: a driver that answers
  load_textures with false, has none, or cannot make a texture gets
  live pixels through load() on every page, and the pack stays whole;
- spent pack: textures gone with the driver and pixels gone before
  them - nothing is shown, and the pack says it cannot come back;
- no images: a pack of hitboxes alone is always reusable.

Against 86fbf34e01's code the first lane is a heap-use-after-free in
load(); with the pixels dropped before load_textures() has answered,
the declined lane fails. Runs plain and under ASan + UBSan in CI Linux
samples/input, well under a second each.
2026-09-19 10:10:16 +02:00
LibretroAdmin 75359a9e4a Overlays: whoever takes the loader's payload frees the animated list
dac760d996 added anim_list to the payload a finished overlay load hands
over; the loader forgets it at the hand-off, as it does the image
list. Two takers did not free it:

- overlay_budget_test's consumer, which is why the ASan lane of CI
  Linux samples/tasks reports 4752 bytes leaked in 12 allocations,
  one string_list per successful load
  (task_push_overlay_load_default, task_overlay.c:1500);
- input_overlay_loaded() when it cannot allocate the pack, which
  freed the image list and leaked this one with its file buffers.

Both now free the entries' buffers and then the list.
2026-09-19 10:10:12 +02:00
LibretroAdmin e9468dedf8 Overlays: the pixels go after the driver has the textures, not before
With threaded video off, enabling an input overlay showed one
oversized blob across the screen or took RetroArch down (reported by
sonninnos, bisected to 86fbf34e01).

86fbf34e01 freed the pack's decoded pixels inside
input_overlay_upload_textures(), as soon as every image had a handle.
The very next loop builds each page's handle list by matching the
page's pixel pointers against the unique images' - which had just been
set to NULL. No entry matched, the upload released everything and
returned false, and input_overlay_load_page() fell back to load() with
the pages' own copies of those pointers: freed memory, handed to the
driver as images. With threaded video the handles are not there at the
poll, the function returns before the free, and the page goes through
load() with live pixels, which is why only the unthreaded case showed.

There was a second way in. The wrapper's overlay table answers
load_textures for any driver, and the one beneath it may have no such
path (d3d9, d3d10, gl1, ...): load_textures() returns false after the
upload succeeded, and the fallback is load() again - so freeing at the
upload leaves that route nothing to read either.

The pixels are now released by input_overlay_load_page(), and only
once load_textures() has answered true. The pages' copies of the
pointers are cleared with them, so nothing is left that looks like an
image and is not one. The sizes are kept, as 86fbf34e01 meant them to
be: image_texture_free() zeroes them. A pack left with neither
textures nor pixels shows nothing instead of reaching load(), and the
cache check in input_overlay_init() becomes input_overlay_has_source(),
which also stops a pack with no images at all from being reloaded from
its path on every toggle.
2026-09-19 10:10:08 +02:00
LibretroAdmin d5e7337b48 Overlays: the pack's textures get a file of their own
The upload of a pack's images, the page lists built from them, their
release, and the choice between showing a page as textures
(load_textures) or as pixels (load) move from input_driver.c to
input/input_overlay_textures.c, unchanged. input_overlay_load_active()
keeps the per-page alpha and geometry and calls the new
input_overlay_load_page() for the rest.

Nothing behaves differently. input_driver.c cannot be compiled outside
the frontend, so none of this could be put under a test; the new file
needs gfx_surface and four driver entry points, which a sample can
stub. The next commits fix what 86fbf34e01 broke here and then pin it.
2026-09-19 10:10:04 +02:00
LibretroAdmin b4e5db3c56 cheevos widgets: no placeholder, and no gap, for a badge a few frames away
A badge that is on disk reaches a widget a few frames after it is
asked for - decoded by a task, uploaded by the video thread, never
waited on - and the progress tracker and the challenge indicators drew
the placeholder trophy for those frames. The tracker is shared by
every achievement with measured progress, so a game advancing two of
them at once has it change hands every few frames, and it threw the
old badge away and loaded the new one each time: a flicker without
end.

rcheevos_get_badge_texture_ex() says why a 0 is a 0: pending while the
file is on disk and loading, not pending while it is being downloaded
or after a failure.

Progress tracker:
- it keeps the last four badges it has shown, so going back to one is
  immediate and loads nothing
- an update whose badge is not there yet is not applied until the
  badge arrives: what is on screen - the previous progress, or nothing
  on a first appearance - stays exactly as it is meanwhile, text and
  badge together. The wait is at most CHEEVO_BADGE_HOLD_FRAMES frames,
  and none at all when the badge is not coming soon; then the
  placeholder is drawn, as before
- the hide timer does not fire under a waiting update
- its textures are unloaded on context destroy and on free, which
  used to leave the handle behind

Challenge indicators: one that has just asked for its badge is not
drawn while the answer is pending, under the same bound, and keeps its
place so its neighbours do not move. They now ask before they draw
instead of after, so the badge shows a frame sooner.

The holds count frames; no clock is read for them.
2026-09-19 10:01:53 +02:00
github-actions 1089625974 Fetch translations from Crowdin 2026-09-19 00:23:43 +00:00
libretroadmin 2f31b9e11a Overlays: an animated image needs one slot, not two
A surface with a submit in flight refuses every slot, not just the
one it is using, so the second slot an animated overlay image was
given could never be written: its frames are composed on the main
thread and submitted at once, which leaves no window for a slot to
fill while another is out. That is a frame of memory per animated
image - eight megabytes for a 1080p animated border - for something
unreachable. The slot count goes to one, and the branch that picked
between slots says what it always did: a frame arriving while the
last is still out is dropped, and the next poll tries again.

Two slots remain right where a worker thread decodes into one while
the other is with the video thread, which is the animated thumbnail's
case and not this one. The sample holds the contract and records the
reason.
2026-09-19 00:05:44 +02:00
libretroadmin dac760d996 Overlays: an animated PNG in the pack plays
An overlay image that carries an acTL animation chunk keeps its file
bytes and gets a stream over them; the pack gives it a surface with
slots rather than a static one, and its already-decoded first frame
goes into the first slot, so nothing decodes twice. Each poll composes
whatever frame is due into a free slot and submits it, which updates
the texture in place: the pages' handles stand, so a page switch is
still an upload of nothing and an animation costs no texture of its
own. A slot the video thread has not finished with means the frame is
skipped rather than waited for - an overlay is a control surface, and
a late button image is worse than a dropped one - and the end of a
pass rewinds, since a control surface that animates should keep doing
it. A hidden overlay does not advance: composing frames nobody can
see is work with no observer, and a resumed loop is invisible on
button art.

An animated image keeps its compressed bytes and its slots where a
still releases its pixels once the texture exists, which is what a
stream needs to decode from. samples/gfx/overlay_apng builds its own
APNG and holds the contract the pages depend on: frames that differ,
a pass that ends and replays, one texture across the submits, and a
free with a frame still in flight.
2026-09-18 22:42:40 +02:00
libretroadmin 247519d69c Overlays: a 16-bit PNG in the pack keeps its ten bits where the driver samples them
The still loader could never be asked for the extra precision, so a
16-bit PNG was flattened to eight bits a channel whatever the driver
could sample. texture_image::pix10 is an ask on the way in and an
answer on the way out now: set it and a decoder with a 10-bit path -
rpng for 16-bit PNG, the video stills - emits XRGB2101010 and reports
what it produced; leave it clear and the decode is the 8-bit one it
always was. A failed decode clears it, so nothing mistakes a stale
request for an answer.

The overlay loader asks when the surface layer says the driver
samples XRGB2101010, which is how a pack's artwork reaches an HDR
swapchain without a pass that throws its low bits away first. A
driver that later cannot sample it narrows the image as before.
2026-09-18 21:06:19 +02:00
libretroadmin 86fbf34e01 Overlays: the pack's pixels go once they are on the GPU
A loaded pack held its decoded pixels for the whole session against
one event: a video reinit, where the textures die with the driver.
Measured against libretro/common-overlays that is 0.9 MiB for a
gamepad pack and up to 40 MiB for a keyboard, a 4K border or a 6x
landscape pad - held the entire time to save a decode at a moment
that tears down and rebuilds a graphics driver anyway, and one that
reloads the overlay from its path regardless
(video_driver_init_internal). So the pixels are freed as soon as
every image of the pack has its texture, and the width and height
stay, which is what the layout reads.

A cached pack is reusable only while it still has something to
upload from, so the cache check asks for live textures or live
pixels and a pack that has neither is reloaded from its path; an
upload with nothing to upload declines, which the existing
TEXTURES_DECLINED fallback already handles.
2026-09-18 20:54:48 +02:00
libretroadmin 807d9f1ad3 gfx: count what a loaded overlay pack keeps in memory, and let the harness print its numbers
An overlay pack keeps its decoded pixels after the textures exist, in
case a video reinit has to upload them again. Whether that is worth
having is a question about how many megabytes it is, so the counters
answer it: overlay_pixel_kib rises with each image uploaded for a pack
and falls when the pack's textures are released, which also makes the
accounting checkable.

The Vulkan harness target filtered its log down to the pass and
failure lines, so the baseline lines the counters print - the loads,
updates, posts and copies a run actually made - never reached the job
meant to report them. It passes those through now, and the overlay
lane prints and checks the pack figure beside the rest.
2026-09-18 20:20:24 +02:00
libretroadmin 61f2f72964 gfx_display: the white texture says which channel order it is
The one-by-one white texture is built on the stack and handed to the
driver with its channel order never written, so the drivers read
whatever the stack held there. Four 0xff bytes come out the same
either way, which is why it has gone unnoticed, but the field is the
caller's to set and every other producer sets it: it comes from the
surface layer now, like the rest.
2026-09-18 18:22:09 +02:00
2112 changed files with 169948 additions and 279556 deletions

No files matched your search

+20 -2
View File
@@ -21,11 +21,12 @@ jobs:
timeout-minutes: 30
steps:
- name: Dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
# The gl2 arms compile against the desktop GL and EGL headers,
# and the Dingux and RS90 video drivers against SDL 1.2's.
pkgs="build-essential libgl-dev libegl-dev libgles-dev libsdl1.2-dev"
# The Direct3D and WASAPI lanes cross-compile with mingw-w64.
pkgs="build-essential libgl-dev libegl-dev libgles-dev libsdl1.2-dev gcc-mingw-w64-x86-64 clang gcc-aarch64-linux-gnu gcc-mipsel-linux-gnu"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
@@ -36,6 +37,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -35,6 +35,9 @@ on:
- 'libretro-common/include/vfs/**'
- 'libretro-common/include/streams/file_stream.h'
- 'libretro-common/samples/file/vfs/**'
- 'libretro-common/fastjmp/**'
- 'libretro-common/include/fastjmp.h'
- 'libretro-common/samples/fastjmp/**'
- '.github/workflows/Cross-libc-vfs-samples.yml'
pull_request:
paths:
@@ -43,6 +46,9 @@ on:
- 'libretro-common/include/vfs/**'
- 'libretro-common/include/streams/file_stream.h'
- 'libretro-common/samples/file/vfs/**'
- 'libretro-common/fastjmp/**'
- 'libretro-common/include/fastjmp.h'
- 'libretro-common/samples/fastjmp/**'
- '.github/workflows/Cross-libc-vfs-samples.yml'
workflow_dispatch:
@@ -169,3 +175,15 @@ jobs:
# Exercises the Win32 CreateFileMapping/MapViewOfFile
# backend, which no other job in the tree reaches.
./vfs_mapped_ptr_test.exe
# The Win64 fastjmp_set stores xmm6-xmm15 with movaps and faults
# on a misaligned buffer. Linux has no such stores, so the Linux
# samples job cannot see a fastjmp_buf that lost its alignment;
# this is the only lane that can. Under a second to build and run.
- name: fastjmp on Win64
working-directory: libretro-common/samples/fastjmp/fastjmp_test
run: |
set -euo pipefail
make clean
make
./fastjmp_test.exe
@@ -0,0 +1,48 @@
name: CI extern "C" prototype parity
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
extern-c-prototype-check:
name: extern "C" definitions match their C prototypes
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Checker self-test
shell: bash
run: |
set -eu
# Fixture pass: a matching definition is clean, a definition
# with a parameter more than its prototype (the shape of the
# 0b2613d Xbox boot crash) is flagged, and a parameter of
# another type is flagged.
python3 tools/extern_c_prototype_check.py --selftest
- name: Tree check
shell: bash
run: |
set -eu
# A .cpp file that defines a win32_common.h function under
# extern "C" without including the header (uwp_main.cpp) is
# never checked against the prototype by any compiler: the
# linker matches on the name alone. This is the check that
# compares the two, for every such definition in the tree.
python3 tools/extern_c_prototype_check.py
+91
View File
@@ -0,0 +1,91 @@
name: CI FreeBSD
# Builds RetroArch inside a FreeBSD VM (GitHub has no FreeBSD runners)
# with the same feature set the games/retroarch port enables by default,
# minus Qt (very slow to install). This is what kept the port carrying
# patches on top of each release: libudev is FreeBSD's libudev-devd, the
# input headers come from dev/evdev/, and there is no linux_common.o to
# link against.
#
# Deliberately NOT installed: evdev-proto and v4l_compat (the Linux
# compatibility headers). A stock FreeBSD box must build udev + Wayland
# from the dev/evdev/ headers in base, and configure must turn V4L2 off
# on its own when libv4l is present but linux/videodev2.h is not.
on:
push:
branches: [master]
pull_request:
repository_dispatch:
types: [run_build]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Check Out Repo
uses: actions/checkout@v4
- name: Build in FreeBSD
uses: vmactions/freebsd-vm@v1
with:
release: "14.3"
usesh: true
copyback: false
prepare: |
pkg install -y \
gmake pkgconf \
ffmpeg freetype2 openal-soft mesa-libs \
libudev-devd libxkbcommon wayland wayland-protocols \
libX11 libXext libXxf86vm libXinerama libXrandr libXv \
sdl2 pipewire alsa-lib pulseaudio libdrm libv4l
run: |
set -e
freebsd-version
NCPU=$(sysctl -n hw.ncpu)
# --disable-pipewire must not leave HAVE_PIPEWIRE_STABLE
# behind just because libpipewire is installed.
./configure --disable-qt --disable-discord --disable-pipewire
grep -q '^HAVE_PIPEWIRE_STABLE = 0' config.mk
# libv4l2.pc is installed but linux/videodev2.h is not
# (v4l_compat): V4L2 must be auto-disabled, not left on to
# fail at compile time.
grep -q '^HAVE_V4L2 = 0' config.mk
# Real build: force the FreeBSD-specific paths on so a missing
# dependency fails configure instead of silently skipping them.
./configure --disable-qt --disable-discord \
--enable-udev --enable-x11 --enable-wayland --enable-sdl2 \
--enable-ffmpeg --enable-pipewire --enable-alsa \
--enable-pulse --enable-oss
gmake -j$NCPU clean
gmake -j$NCPU info all
./retroarch --version
# udev input must multiplex with kqueue here, so the binary
# must not need libepoll-shim's epoll_* at all.
if nm -u retroarch | grep -q 'epoll_'; then
echo "retroarch still references epoll (libepoll-shim)"; exit 1
fi
# A fresh RetroArch must pick up data sets the ports install
# under /usr/local/share/libretro (assets, info, shaders,
# autoconfig) without any retroarch.cfg edits.
sh samples/frontend/packaged_dirs/run.sh
# With v4l_compat the V4L2 camera and video processor core
# must still compile; only those two objects are rebuilt.
pkg install -y v4l_compat
./configure --disable-qt --disable-discord --enable-v4l2
grep -q '^HAVE_V4L2 = 1' config.mk
gmake -j$NCPU camera/drivers/video4linux2.o \
cores/libretro-video-processor/video_processor_v4l2.o
+18 -1
View File
@@ -24,7 +24,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev zlib1g-dev libfreetype6-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev nvidia-cg-toolkit nvidia-cg-dev libavcodec-dev libsdl2-dev libsdl-image1.2-dev libxml2-dev yasm"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -37,6 +37,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+79 -1
View File
@@ -72,7 +72,7 @@ jobs:
# comment for why SDL2 over xvideo. No sanitizer-specific
# packages required; libasan / libubsan ship with the gcc
# that ubuntu-latest has installed by default.
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev nvidia-cg-toolkit nvidia-cg-dev libavcodec-dev libsdl2-dev libsdl-image1.2-dev libxml2-dev yasm"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -85,6 +85,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -383,3 +400,64 @@ jobs:
| sort -u | head -20
fi
echo "[done] imageviewer headless smoke (soft-fail)"
- name: Screenshot on exit under LeakSanitizer (strict)
# The smoke above runs with detect_leaks=0, which is how a
# synchronous screenshot leaking its task state and the whole
# captured frame on every call went unnoticed: --max-frames-ss
# and save-state thumbnails both take that path. This run
# takes one and checks for leaks with only the system libraries
# suppressed - Mesa, X11, SDL, D-Bus and friends keep caches
# alive to exit that are not RetroArch's - so anything left is
# ours and fails the job.
env:
ASAN_OPTIONS: abort_on_error=1:detect_leaks=1:print_stacktrace=1
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
LSAN_OPTIONS: suppressions=/tmp/lsan-system.supp:print_suppressions=0
run: |
set -eu
cat > /tmp/lsan-system.supp <<EOF
leak:libGL
leak:libGLX
leak:libGLdispatch
leak:libgallium
leak:libLLVM
leak:_dri.so
leak:libEGL
leak:libgbm
leak:libX11
leak:libxcb
leak:libSDL2
leak:libdbus-1
leak:libfontconfig
leak:libfreetype
leak:libasound
leak:libpulse
EOF
mkdir -p /tmp/lsan-cfg /tmp/lsan-shots
cat > /tmp/lsan-cfg/retroarch.cfg <<EOF
video_driver = "sdl2"
audio_driver = "null"
video_threaded = "false"
video_fullscreen = "false"
video_windowed_fullscreen = "false"
screenshot_directory = "/tmp/lsan-shots"
EOF
set +e
timeout 120 xvfb-run -a -s "-screen 0 320x240x24" ./retroarch \
-c /tmp/lsan-cfg/retroarch.cfg \
-L cores/libretro-imageviewer/image_core.so \
/tmp/test.png \
--max-frames=60 \
--max-frames-ss \
--max-frames-ss-path=/tmp/lsan-shots/exit.png \
> /tmp/lsan.log 2>&1
rc=$?
set -e
if [ "$rc" != 0 ] || grep -qE "leak of|AddressSanitizer:|runtime error:" /tmp/lsan.log; then
echo "exit=${rc}"
grep -E -A 12 "leak of|AddressSanitizer:|runtime error:" /tmp/lsan.log | head -120
exit 1
fi
test -s /tmp/lsan-shots/exit.png
echo "[pass] screenshot on exit, no leaks outside the system libraries"
+46 -5
View File
@@ -21,13 +21,26 @@ concurrency:
jobs:
autoconf-tests:
name: Build and run the autoconfig regression suite
name: Build and run the autoconfig regression suite (${{ matrix.lane }})
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: ${{ matrix.sanitizer == '' && 15 || 40 }}
strategy:
fail-fast: false
matrix:
# The suite fails on any sanitizer report in any run, so the
# sanitized lanes hold the whole frontend it drives - runloop,
# task queue, autodetect - to the same bar as the assertions.
include:
- lane: plain
sanitizer: ''
- lane: ASan + UBSan
sanitizer: address,undefined
- lane: TSan
sanitizer: thread
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential zlib1g-dev libfreetype6-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -40,6 +53,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -59,11 +89,22 @@ jobs:
grep -q '^HAVE_TEST_DRIVERS = 1' config.mk
- name: Build
run: make -j"$(nproc)"
run: |
if [ -n "${{ matrix.sanitizer }}" ]; then
make -j"$(nproc)" SANITIZER=${{ matrix.sanitizer }} DEBUG=1
else
make -j"$(nproc)"
fi
- name: Run autoconfig regression suite
shell: bash
run: |
set -eu
test -x ./retroarch
./tests-other/test_autoconf.sh ./retroarch
if [ "${{ matrix.sanitizer }}" = thread ]; then
# TSan cannot map its shadow under the runner's ASLR
# entropy; setarch -R carries to every child the suite runs.
setarch "$(uname -m)" -R ./tests-other/test_autoconf.sh ./retroarch
else
./tests-other/test_autoconf.sh ./retroarch
fi
@@ -0,0 +1,79 @@
name: CI Linux filter plugins
# Builds every video filter (gfx/video_filters) and audio DSP filter
# (libretro-common/audio/dsp_filters) plugin with its own Makefile and
# runs samples/gfx/filter_plugins over all of them: each plugin is
# loaded with RTLD_NOW into a host that does not link libm, so a
# symbol it cannot resolve fails the load, then fed a frame or a run
# of audio blocks at its default settings. Every video filter that
# takes more than one worker must give the single-worker output byte for
# byte with 2, 3 and 4 workers running at once. The same run is repeated
# with the plugins and the harness built under ASan, UBSan and
# LeakSanitizer, and under ThreadSanitizer.
on:
push:
branches:
- master
paths:
- 'gfx/video_filters/**'
- 'libretro-common/audio/dsp_filters/**'
- 'libretro-common/include/libretro_dspfilter.h'
- 'samples/gfx/filter_plugins/**'
- '.github/workflows/Linux-filter-plugins.yml'
pull_request:
paths:
- 'gfx/video_filters/**'
- 'libretro-common/audio/dsp_filters/**'
- 'libretro-common/include/libretro_dspfilter.h'
- 'samples/gfx/filter_plugins/**'
- '.github/workflows/Linux-filter-plugins.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
filter-plugins:
name: Load and run every filter plugin
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Build the plugins and the harness
run: |
set -eu
make -C samples/gfx/filter_plugins
# The harness must not bring libm in on the plugins' behalf.
if readelf -d samples/gfx/filter_plugins/filter_plugins_test \
| grep -q 'libm\.so'; then
echo "filter_plugins_test links libm; it would hide a plugin that does not"
exit 1
fi
- name: Load and run every plugin
run: make -C samples/gfx/filter_plugins check
- name: Load and run every plugin under ASan, UBSan and LeakSanitizer
env:
ASAN_OPTIONS: abort_on_error=1:detect_leaks=1:print_stacktrace=1
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
run: |
set -eu
make -C samples/gfx/filter_plugins clean
make -C samples/gfx/filter_plugins check SANITIZER=address,undefined
- name: Load and run every plugin under ThreadSanitizer
env:
TSAN_OPTIONS: halt_on_error=1
run: |
set -eu
make -C samples/gfx/filter_plugins clean
make -C samples/gfx/filter_plugins check SANITIZER=thread
@@ -0,0 +1,92 @@
name: CI Linux frontend overhead [frame-path harness]
# What one runloop_iterate() costs when the core costs nothing, and
# whether anything on the frame path allocates. The harness links the
# shipping release objects with main() replaced, boots on the null
# drivers, runs a core that pushes one frame, one audio batch and one
# input read per retro_run, and counts heap calls over the frame path
# by glibc interposition - which is why this is a release build with
# no allocator sanitizer in the way. It also prints microseconds per
# frame under fast-forward and trips on a loose bound, so a blocking
# call or a sleep that lands on the frame path fails here rather than
# in a bug report about stutter.
#
# Everything a default desktop build compiles in is compiled in, so a
# product that starts costing the frame while idle is measured, not
# excluded.
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
linux-frontend-overhead:
name: Release build, frame-path heap and cost
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Install dependencies
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev zlib1g-dev libfreetype6-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libavcodec-dev libsdl2-dev libxml2-dev yasm"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
fi
for i in 1 2 3; do
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 && break
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
- name: Checkout
uses: actions/checkout@v5
- name: Configure
# Non-Qt, as the harness requires (with Qt, main() lives in
# ui_qt.o). Everything else at its default.
run: ./configure --disable-qt
- name: Build (release)
run: make -j$(getconf _NPROCESSORS_ONLN)
- name: Run the frontend-overhead harness
shell: bash
run: |
set -o pipefail
SAN=none make -C samples/runloop/frontend_overhead check
make -C samples/runloop/frontend_overhead clean
+125
View File
@@ -0,0 +1,125 @@
name: CI Linux gl1 upload
# Runs the gl1 video driver under Xvfb with a test core and checks the
# screen against the frame the core drew, pixel for pixel, down each of
# the driver's upload paths: frames taken straight from the core's
# buffer, the no-GL_EXT_bgra swizzle, native RGB565, the GL 1.1 CPU
# expansion, and texture storage respecified across frame size changes.
on:
push:
branches:
- master
paths:
- 'gfx/drivers/gl1.c'
- 'libretro-common/gfx/scaler/pixconv.c'
- 'libretro-common/include/gfx/scaler/pixconv.h'
- 'samples/gfx/gl1_upload/**'
- '.github/workflows/Linux-gl1-upload.yml'
pull_request:
paths:
- 'gfx/drivers/gl1.c'
- 'libretro-common/gfx/scaler/pixconv.c'
- 'libretro-common/include/gfx/scaler/pixconv.h'
- 'samples/gfx/gl1_upload/**'
- '.github/workflows/Linux-gl1-upload.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
gl1-upload:
name: Run retroarch on gl1 under Xvfb and check the frame
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Install dependencies
timeout-minutes: 20
run: |
for i in 1 2 3; do
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 && break
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential zlib1g-dev ccache python3 \
xvfb libgl1-mesa-dri \
libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev \
libxxf86vm-dev libxrandr-dev libxinerama-dev \
libgl-dev libegl-dev && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential zlib1g-dev ccache python3 \
xvfb libgl1-mesa-dri \
libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev \
libxxf86vm-dev libxrandr-dev libxinerama-dev \
libgl-dev libegl-dev
- name: Checkout
uses: actions/checkout@v5
- name: Restore ccache
uses: actions/cache@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 500M
ccache -z
- name: Build RetroArch (X11 + OpenGL, the rest off)
shell: bash
run: |
set -eu
./configure --disable-qt --disable-wayland --disable-sdl \
--disable-sdl2 --disable-ffmpeg --disable-vulkan \
--disable-pulse --disable-discord \
--disable-networking --disable-cheevos \
--disable-glslang --disable-slang \
--disable-spirv_cross --disable-xmb \
--disable-materialui --disable-ozone \
--disable-7zip --disable-flac \
--disable-audiomixer --disable-translate \
--disable-accessibility --disable-crtswitchres \
--disable-langextra --disable-libretrodb \
--disable-video_filter --disable-dsp_filter
grep -q '^HAVE_OPENGL1 = 1' config.mk
make -j$(nproc)
ccache -s
- name: Run gl1_upload_test
shell: bash
run: |
set -eu
make -C samples/gfx/gl1_upload
timeout 600 python3 samples/gfx/gl1_upload/gl1_upload_test.py ./retroarch
@@ -0,0 +1,139 @@
name: CI Linux library network backends
# The built-in TLS, SMB and NFS clients are the defaults and nothing is
# bundled. A system mbedTLS or BearSSL can stand in for the TLS client
# (--enable-mbedtls, --enable-bearssl) and a system libsmb2 for the SMB
# client (--enable-libsmb). Each lane configures with its option, builds
# RetroArch to the link, checks the binary carries that library's
# backend and not the built-in client, and then operates the backend:
# the TLS lanes run the cheevos login transport against a local HTTPS
# server, the libsmb2 lane runs the SMB VFS tests against a local smbd.
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
library-backends:
name: ${{ matrix.lane }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- lane: mbedtls
configure: --enable-mbedtls
present: mbedtls_ssl_handshake
absent: ' T ssl_socket_retro_was_resumed'
- lane: bearssl
configure: --enable-bearssl
present: br_ssl_client_init_full
absent: ' T ssl_socket_retro_was_resumed'
- lane: libsmb2
configure: --enable-libsmb
present: ' U smb2_connect_share'
absent: ' T rsmb_connect'
steps:
- name: Dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y build-essential pkg-config cmake \
libgl1-mesa-dev libx11-dev libxext-dev libasound2-dev libsdl2-dev \
zlib1g-dev libfreetype6-dev python3 openssl \
libmbedtls-dev libbearssl-dev samba
- name: Checkout
uses: actions/checkout@v5
- name: libsmb2 from upstream (no Ubuntu package)
if: matrix.lane == 'libsmb2'
run: |
set -eu
git clone -q --depth 1 https://github.com/sahlberg/libsmb2.git "$RUNNER_TEMP/libsmb2"
cmake -S "$RUNNER_TEMP/libsmb2" -B "$RUNNER_TEMP/libsmb2/build" \
-DCMAKE_INSTALL_PREFIX=/usr/local -DBUILD_SHARED_LIBS=ON >/dev/null
make -C "$RUNNER_TEMP/libsmb2/build" -j"$(nproc)" >/dev/null
sudo make -C "$RUNNER_TEMP/libsmb2/build" install >/dev/null
sudo ldconfig
echo "PKG_CONFIG_PATH=/usr/local/lib/pkgconfig" >> "$GITHUB_ENV"
pkg-config --modversion libsmb2 || PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion libsmb2
- name: Configure
run: |
set -eu
./configure ${{ matrix.configure }} --disable-qt --disable-wayland --disable-ffmpeg
grep -E '^HAVE_(RETROSSL|MBEDTLS|BEARSSL|RETROSMB|SMBCLIENT|SSL) ' config.mk
case "${{ matrix.lane }}" in
mbedtls) grep -q '^HAVE_MBEDTLS = 1' config.mk; grep -q '^HAVE_RETROSSL = 0' config.mk ;;
bearssl) grep -q '^HAVE_BEARSSL = 1' config.mk; grep -q '^HAVE_RETROSSL = 0' config.mk ;;
libsmb2) grep -q '^HAVE_SMBCLIENT = 1' config.mk; grep -q '^HAVE_RETROSMB = 0' config.mk ;;
esac
- name: Build
run: make -j"$(nproc)"
- name: Check which backend linked
run: |
set -eu
nm retroarch | grep -q -- '${{ matrix.present }}'
if nm retroarch | grep -q -- '${{ matrix.absent }}'; then
echo "the built-in client is still linked in the ${{ matrix.lane }} build"; exit 1
fi
echo "[pass] ${{ matrix.lane }} backend linked, built-in client absent"
- name: Operate the TLS backend (cheevos login transport, ASan + UBSan)
if: matrix.lane != 'libsmb2'
working-directory: samples/network/cheevos_login
run: |
set -eu
case "${{ matrix.lane }}" in mbedtls) be=mbed ;; bearssl) be=bear ;; esac
make clean all SSL_BACKEND=$be SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 \
make check SSL_BACKEND=$be
echo "[pass] ${{ matrix.lane }}: cheevos login transport over a local HTTPS server"
- name: Operate the libsmb2 backend (SMB VFS against a local smbd)
if: matrix.lane == 'libsmb2'
run: |
set -eu
sudo -E env PATH="$PATH" PKG_CONFIG_PATH="$PKG_CONFIG_PATH" WITH_LIBSMB2=1 \
samples/network/smb_retro/local_samba_test.sh
sudo make -C samples/network/smb_retro clean
# libsmb2 keeps process-wide state (its list of live contexts) with
# no lock of its own; the VFS backend must serialise it. A race
# there needs two cores to show up by timing, so the same tests run
# again under TSan against a TSan-built libsmb2, which reports it on
# any runner, every time.
- name: Operate the libsmb2 backend under TSan (instrumented libsmb2)
if: matrix.lane == 'libsmb2'
run: |
set -eu
cmake -S "$RUNNER_TEMP/libsmb2" -B "$RUNNER_TEMP/libsmb2/build-tsan" \
-DCMAKE_INSTALL_PREFIX=/opt/libsmb2-tsan -DBUILD_SHARED_LIBS=ON \
-DCMAKE_C_FLAGS="-O1 -g -fsanitize=thread" \
-DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=thread" >/dev/null
make -C "$RUNNER_TEMP/libsmb2/build-tsan" -j"$(nproc)" >/dev/null
sudo make -C "$RUNNER_TEMP/libsmb2/build-tsan" install >/dev/null
sudo -E env PATH="$PATH" \
PKG_CONFIG_PATH=/opt/libsmb2-tsan/lib/pkgconfig \
LD_LIBRARY_PATH=/opt/libsmb2-tsan/lib \
TSAN_OPTIONS="halt_on_error=1 exitcode=66" \
CFLAGS="-O1 -g -fsanitize=thread" LDLIBS="-fsanitize=thread" \
WITH_LIBSMB2=1 samples/network/smb_retro/local_samba_test.sh
sudo make -C samples/network/smb_retro clean
@@ -27,13 +27,14 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
# All three packages ship on the stock ubuntu-latest image, so
# this step is a no-op there and apt is only contacted if the
# image ever drops one of them.
# ffmpeg (with libx264): formats/h264/rh264_lossless_test builds
# its lossless corpus and its reference decodes with it.
# ffmpeg (with libx264 and libx265): formats/h264/rh264_lossless_test
# and formats/h265/rh265_lossless_test build their corpora and
# their reference decodes with it.
if dpkg -s build-essential zlib1g-dev clang ffmpeg >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
@@ -44,6 +45,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential zlib1g-dev clang ffmpeg && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential zlib1g-dev clang ffmpeg
@@ -92,6 +110,7 @@ jobs:
# self-contained test and exits 0 on success / non-zero on
# failure. These are built AND executed.
declare -a RUN_TARGETS=(
float_to_s32_test
compat_fnmatch_test
snprintf
unbase64_test
@@ -102,17 +121,20 @@ jobs:
config_file_test
path_resolve_realpath_test
fill_pathname_test
nbio_test
rpng
rzip_chunk_size_test
rzip_matches_buf_test
rzip_codec_test
rzip_parallel_test
data_transfer_source_test
data_transfer_prefix_test
data_transfer_window_test
audio_transfer_window_test
rjpeg_test
rh264_lossless_test
rh264_lossless_test_nothreads
rh265_lossless_test
rh265_lossless_test_nothreads
r7z_lzma_props_test
vcdiff_test
net_ifinfo
@@ -135,6 +157,11 @@ jobs:
rtga_test
rbmp_test
rbmp_encode_test
# Every 16-bit input through each pixconv converter, with
# SSE2, with MMX only, and with no SIMD at all.
pixconv_test
pixconv_test_nosimd
pixconv_test_scalar
rpng_chunk_overflow_test
rpng_roundtrip_test
rpng_decode_corpus_test
@@ -146,11 +173,18 @@ jobs:
screenshot_io_bench
word_wrap_overflow_test
task_queue_title_error_test
task_queue_snapshot_test
task_queue_init_failure_test
# Stops the worker with a bound when a detachable
# handler is stuck; waits ~1.5 s by design.
task_queue_detach_test
task_queue_main_thread_test
task_queue_budget_test
image_blit_bands_test
tpool_wait_test
# Four CPU-burning jobs on a pool of four must overlap;
# on a one-core runner it notes that and still passes.
tpool_parallel_test
data_transfer_pool_scrub_test
retro_atomic_test
retro_atomic_extern_c_linkage_test
@@ -166,13 +200,12 @@ jobs:
# microsecond clock; the late bound is loose for a loaded
# runner.
retro_sleep_until_test
# Runs its natural tier only here; Makefile.test's
# threads-samples forces each tier in turn. On the
# runner's cores the fence check is load-bearing, which
# this box cannot make it.
# Runs its natural tier only here; procbarrier-tiers.yml
# forces every tier on Linux, Windows and macOS runners.
retro_procbarrier_test
retro_asym_eventcount_test
sthread_ex_test
fast_cores_test
memshm_test
fastjmp_test
rdds_block_test
@@ -554,6 +587,26 @@ jobs:
make clean
CC=clang make check SANITIZER=thread
- name: Run rzip_parallel_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/streams/rzip
run: |
# rzip_stream.c's chunk-writer pool hands each slot between the
# writer and one worker on an acquire/release status word and
# parks both sides on retro_eventcount, with no lock anywhere.
# The value checks prove in-order emission; a weakened store
# or a lost wakeup still passes them on x86 TSO, and only TSan
# sees the missing barrier. The test drives the pool at every
# width and through two hundred short lifetimes so the
# start / park / shutdown edges are exercised often.
set -u
set -o pipefail
make clean
CC=clang make rzip_parallel_test SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 ./rzip_parallel_test
- name: Run crc32_test under Clang + ThreadSanitizer
shell: bash
working-directory: libretro-common/samples/encodings/crc32
@@ -690,7 +743,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
# clang ships on the stock ubuntu-latest image; apt is only
# contacted if the image ever drops it.
@@ -704,6 +757,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
clang && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
clang
@@ -793,7 +863,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
for i in 1 2 3; do
sudo timeout 90 apt-get update -y \
@@ -801,6 +871,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential ${{ matrix.apt_pkgs }} qemu-user-static && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential ${{ matrix.apt_pkgs }} qemu-user-static
@@ -928,7 +1015,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
for i in 1 2 3; do
sudo timeout 90 apt-get update -y \
@@ -936,6 +1023,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential ${{ matrix.apt_pkgs }} qemu-user-static && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential ${{ matrix.apt_pkgs }} qemu-user-static
@@ -980,6 +1084,27 @@ jobs:
set -o pipefail
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./crc32_test
- name: Build and run pixconv_test under qemu-user
shell: bash
working-directory: libretro-common/samples/gfx/pixconv
run: |
set -euo pipefail
# The ARM legs are the only place the NEON converters run;
# PowerPC runs the scalar paths big-endian. The armhf
# toolchain defaults to VFP without NEON, so armv7 builds a
# second time with NEON on.
make clean
CC="${{ matrix.cc }}" CFLAGS="${{ matrix.cflags }}" make all
for t in pixconv_test pixconv_test_nosimd pixconv_test_scalar; do
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./$t
done
if [ "${{ matrix.name }}" = armv7 ]; then
make clean
CC="${{ matrix.cc }}" CFLAGS="-mfpu=neon" make all
${{ matrix.qemu }} -L ${{ matrix.sysroot }} ./pixconv_test
fi
make clean
- name: Run vfs_mapped_ptr_test without mappings (MMAP=0)
shell: bash
working-directory: libretro-common/samples/file/vfs
@@ -33,11 +33,11 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 8
timeout-minutes: 20
run: |
# ffmpeg: tools/companion_thumbs_test.sh builds its video-hover
# fixture (a 2 KB Baseline MP4) with it.
pkgs="build-essential check pkg-config zlib1g-dev lcov gcc-mingw-w64-x86-64 gcc-mingw-w64-i686 wine wine64 ffmpeg"
pkgs="build-essential check pkg-config zlib1g-dev lcov gcc-mingw-w64-x86-64 gcc-mingw-w64-i686 wine wine64 ffmpeg gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-mipsel-linux-gnu qemu-user"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
@@ -48,6 +48,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -107,6 +124,15 @@ jobs:
LIBCHECK_CFLAGS="$(pkg-config --cflags check)" \
CFLAGS="$(pkg-config --cflags check)"
echo "[pass] libretro-common unit test suites"
# The ARMv8 AES + PMULL path executes only on ARM; this
# runs the crypto suite under qemu-aarch64 in both the
# dispatched and the baseline-crypto shape.
make -f Makefile.test crypto-aarch64-check
make -f Makefile.test crypto-arm-check
make -f Makefile.test crypto-mips-check
# every lazily set up table or CPU check, first used by
# eight threads at once under TSan
make -f Makefile.test crypto-tsan-check
- name: Check VFS backend symbol parity
shell: bash
@@ -123,6 +149,44 @@ jobs:
# free, on Linux.
python3 tools/vfs_backend_parity.py
- name: Check every command describes itself
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# HELP prints command.h's tables, and interfaces that hand the
# commands to a client are generated from them: a command
# added without a description reaches all of those blank.
python3 tools/command_table_check.py
- name: Check no video driver deletes overlay textures it was lent
shell: bash
run: |
set -eu
# load_textures lends a driver the overlay pack's handles for
# one page; they are on the pack's other pages too and are
# the pack's to delete. gl2 marked them as borrowed and then
# deleted them anyway at the next page, so rotating an
# Android device blacked the overlay out until the core was
# restarted. No runner can run a GL driver, so this is the
# check at source level: every driver with load_textures
# sets, tests and clears its borrowed marker.
python3 tools/overlay_texture_ownership.py
- name: Check overlay setters refuse an image the driver does not hold
shell: bash
run: |
set -eu
# set_alpha, vertex_geom and tex_geom are called whenever the
# layout, opacity or page changes, not only after a load()
# that worked: with no page the driver's arrays are NULL, and
# after a smaller page or a failed load the index is off the
# end. 7661ab06e7 bounded the GL geometry setters and missed
# set_alpha; D3D, Vulkan and Metal had no check at all. No
# runner can run these drivers, so this is the check at
# source level for the ones that have been audited.
python3 tools/overlay_setter_bounds.py
- name: Check companion backend feature parity
shell: bash
working-directory: ${{ github.workspace }}
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential zlib1g-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+134 -2
View File
@@ -22,6 +22,15 @@ on:
- 'playlist.h'
- 'tasks/task_content*'
- 'samples/menu/playlist_nav/**'
- 'samples/menu/nav_repeat_stall/**'
- 'runloop.c'
- 'samples/menu/network_settings/**'
- 'intl/msg_hash_lbl.h'
- 'msg_hash_lbl_str.h'
- 'samples/menu/resolution_reset/**'
- 'samples/settings/packed_rows/**'
- 'setting_list.h'
- 'gfx/video_defines.h'
- '.github/workflows/Linux-menu-harness.yml'
pull_request:
paths:
@@ -30,6 +39,15 @@ on:
- 'playlist.h'
- 'tasks/task_content*'
- 'samples/menu/playlist_nav/**'
- 'samples/menu/nav_repeat_stall/**'
- 'runloop.c'
- 'samples/menu/network_settings/**'
- 'intl/msg_hash_lbl.h'
- 'msg_hash_lbl_str.h'
- 'samples/menu/resolution_reset/**'
- 'samples/settings/packed_rows/**'
- 'setting_list.h'
- 'gfx/video_defines.h'
- '.github/workflows/Linux-menu-harness.yml'
workflow_dispatch:
@@ -45,7 +63,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential zlib1g-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -58,6 +76,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -98,10 +133,12 @@ jobs:
#
# Qt in particular has to go: with it enabled main() lives
# in ui_qt.o, and the harness replaces main().
# Networking stays on: the network settings harness below
# needs the SMB and NFS client screens compiled in.
./configure --disable-qt --disable-x11 \
--disable-opengl --disable-opengl_core \
--disable-opengl1 --disable-vulkan \
--disable-cheevos --disable-networking \
--disable-cheevos \
--disable-ffmpeg
make -j$(nproc)
@@ -146,6 +183,64 @@ jobs:
./samples/menu/playlist_nav/menu_playlist_nav_test
make -C samples/menu/playlist_nav clean
- name: Build and run menu_nav_repeat_stall_test
shell: bash
run: |
set -eu
# One press on a menu entry whose frame stalls - Ozone and
# XMB build a large playlist synchronously when the selection
# lands on it - has to move the selection once. The menu acts
# on input one poll old, so the hold time has to be measured
# between polls or the stall fires an auto-repeat. Drives the
# real runloop with a scripted input driver; also checks that
# a hold across the stall still repeats and that a steady hold
# waits out the initial scroll delay. Shares the non-Qt build
# made above.
samples/menu/nav_repeat_stall/build.sh
./samples/menu/nav_repeat_stall/menu_nav_repeat_stall_test
make -C samples/menu/nav_repeat_stall clean
- name: Build and run menu_network_settings_test
shell: bash
run: |
set -eu
# The NFS Network Settings entry opened an empty "null"
# browser: the label its deferred list is pushed with had no
# row in msg_hash_lbl.h, so it resolved to "null" and the
# push found nothing. The harness resolves the SMB and NFS
# labels, opens Settings > Network and both screens through
# the real dispatch, and checks the Quick Menu no longer
# carries the two entries.
samples/menu/network_settings/build.sh
./samples/menu/network_settings/menu_network_settings_test
make -C samples/menu/network_settings clean
- name: Build and run resolution_reset_test
shell: bash
run: |
set -eu
# Start on the Resolution entry reinits and sets the video
# mode at the output size; it must keep the window state the
# frontend is in rather than take a windowed desktop session
# full screen. Shares the non-Qt build made above.
samples/menu/resolution_reset/build.sh
./samples/menu/resolution_reset/resolution_reset_test
make -C samples/menu/resolution_reset clean
- name: Build and run packed_rows_test
shell: bash
run: |
set -eu
# Settings rows that each own one half of a packed settings_t
# word - the custom viewport's origin and size, the window
# size pair - driven through their display strings, a right
# press and the dropdown list. Each row has to show and set
# its own axis and leave its partner as it stands. Shares the
# non-Qt build made above.
samples/settings/packed_rows/build.sh
./samples/settings/packed_rows/packed_rows_test
make -C samples/settings/packed_rows clean
- name: Hot function budget
shell: bash
run: |
@@ -157,6 +252,17 @@ jobs:
# this job already built.
.github/workflows/scripts/hot-function-budget.sh
- name: Lock budget
shell: bash
run: |
set -eu
# Lock acquisitions on the paths meant to take none, or a
# counted few, against tools/lock_acquire_budget.ref: a lock
# put back on a per-frame path passes every lane and shows up
# only as a slower frame. Same release objects as above; this
# build has no Vulkan, so the Vulkan entries report SKIP.
tools/lock_acquire_budget.sh
- name: Build and run threaded_video_test
shell: bash
run: |
@@ -174,3 +280,29 @@ jobs:
samples/gfx/threaded_video/build.sh
./samples/gfx/threaded_video/threaded_video_test 3
make -C samples/gfx/threaded_video clean
- name: Build and run content_load_test
shell: bash
run: |
set -eu
# The content load as a chain of main-thread tasks, against the
# real runloop on the same non-Qt menu build: frames presented
# between stages, one load at a time, a core's reinit deferred
# to the load's driver stage, the dummy-core fallback, a
# hardware-render request carried across the driver rebuild,
# the task queue surviving the load, and the close waiting out
# a save state from the frame loop.
samples/tasks/content_load/build.sh
./samples/tasks/content_load/content_load_test
make -C samples/tasks/content_load clean
- name: Build and run explore_build_test
shell: bash
run: |
set -eu
# The Explore index built in steps: one item per step hashes
# the same as the build in one go, and the explore task returns
# unfinished across checks with Threaded Tasks off.
samples/menu/explore_build/build.sh
./samples/menu/explore_build/explore_build_test
make -C samples/menu/explore_build clean
+222
View File
@@ -0,0 +1,222 @@
name: CI Linux network clients
# The built-in TLS, SMB2/3 and NFSv3 clients against real servers:
# openssl s_server, Samba, unfs3, nfs-ganesha and a MIT KDC on the runner, natively, as Windows
# builds under Wine, under ASan + UBSan + LSan, plus libFuzzer over the
# parsers. Its own job so the libretro-common unit-test job keeps its
# short budget.
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
network-clients:
name: TLS, SMB and NFS clients against local servers
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Install dependencies
timeout-minutes: 25
run: |
sudo apt-get update -y
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
# tshark's dumpcap asks whether to be setuid: no, runs are root
echo "wireshark-common wireshark-common/install-setuid boolean false" | sudo debconf-set-selections
export DEBIAN_FRONTEND=noninteractive
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only --no-install-recommends build-essential clang \
gcc-mingw-w64-x86-64 openssl samba libtirpc-dev flex bison autoconf automake wine wine64 \
nfs-ganesha nfs-ganesha-vfs krb5-kdc krb5-admin-server krb5-user winbind \
samba-dsdb-modules samba-ad-provision samba-vfs-modules acl smbclient tshark && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y --no-install-recommends build-essential clang \
gcc-mingw-w64-x86-64 openssl samba libtirpc-dev flex bison autoconf automake wine wine64 \
nfs-ganesha nfs-ganesha-vfs krb5-kdc krb5-admin-server krb5-user winbind \
samba-dsdb-modules samba-ad-provision samba-vfs-modules acl smbclient tshark
- name: Checkout
uses: actions/checkout@v5
- name: Built-in TLS client against a local openssl server
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# net_socket_ssl_retro.c end to end: every cipher suite, both
# curves, RSA / P-256 / P-384 certificates under a CA made on
# the spot, verify required / optional / disabled, and the
# refusals that must happen (wrong hostname, untrusted CA). An
# IP address matches only an iPAddress SAN entry, never the
# same address written as a DNS name.
samples/network/tls_retro/local_server_test.sh
# The same tool cross-linked for Windows: the sample's link
# line has to carry Winsock, which nothing on Linux checks.
make -C samples/network/tls_retro clean
- name: Built-in SMB2/3 client against a local smbd
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# net_smb2.c end to end against Samba on the loopback: every
# dialect 2.0.2 to 3.1.1 with signing mandatory, AES-CCM and
# AES-GCM sealing required, and a refused logon. Root for
# port 445; the script itself skips without smbd.
sudo samples/network/smb_retro/local_samba_test.sh
# and with Kerberos against a Samba AD DC provisioned on the
# spot, NTLM disabled on it so only a ticket can log in
sudo samples/network/smb_retro/local_ad_dc_test.sh
sudo make -C samples/network/smb_retro clean
- name: Windows builds under Wine
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# The MinGW builds run, not just link: the crypto and keychain
# suites, then the three server matrices with the Windows tools
# talking to the same local openssl, Samba and unfs3.
export WINEDEBUG=-all WINEPREFIX=$RUNNER_TEMP/wineprefix
# the tools below are run through wine, so it must be there
command -v wine >/dev/null
make -C libretro-common -f Makefile.test win32-wine-check
export CC=x86_64-w64-mingw32-gcc RUNNER=wine EXE=.exe
make -C samples/network/tls_retro clean
samples/network/tls_retro/local_server_test.sh
make -C samples/network/tls_retro clean
make -C samples/network/smb_retro clean
# smbd needs root for port 445, and Wine refuses both a prefix
# owned by someone else and creating one inside a directory it
# does not own ($RUNNER_TEMP belongs to the runner user), so the
# root run gets a prefix under /tmp with root's own HOME
sudo -E env HOME=/root WINEPREFIX=/tmp/wineprefix-root wineboot -u
sudo -E env HOME=/root WINEPREFIX=/tmp/wineprefix-root samples/network/smb_retro/local_samba_test.sh
sudo -E env HOME=/root WINEPREFIX=/tmp/wineprefix-root samples/network/smb_retro/local_ad_dc_test.sh
sudo make -C samples/network/smb_retro clean
make -C samples/network/nfs_retro clean
samples/network/nfs_retro/local_unfs3_test.sh
make -C samples/network/nfs_retro clean
sudo -E env HOME=/root WINEPREFIX=/tmp/wineprefix-root samples/network/nfs_retro/local_ganesha_test.sh
sudo make -C samples/network/nfs_retro clean
sudo -E env HOME=/root WINEPREFIX=/tmp/wineprefix-root samples/network/nfs_retro/local_read_plus_test.sh
sudo make -C samples/network/nfs_retro clean
make -C samples/network/krb5_retro clean
samples/network/krb5_retro/local_kdc_test.sh
make -C samples/network/krb5_retro clean
- name: Fuzz the network parsers
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# libFuzzer over the X.509 parser and the TLS, SMB and NFS
# clients' receive paths (a listener in the harness answers the
# client with the fuzzer's input), under ASan + UBSan, from a
# seed corpus recorded off real servers. Any crash, timeout or
# leak artifact fails the step.
make -C libretro-common/test/fuzz fuzz-run FUZZ_SECONDS=30
if ls libretro-common/test/fuzz/crash-* libretro-common/test/fuzz/leak-* libretro-common/test/fuzz/timeout-* >/dev/null 2>&1; then
echo "fuzzer artifacts found"; exit 1
fi
make -C libretro-common/test/fuzz clean
- name: Network client matrices under ASan + UBSan + LSan
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# The three server matrices again, with the clients built under
# the sanitizers: every code path a real server drives, checked
# for memory errors, undefined behaviour and leaks. UBSan found
# a NULL memcpy in the SHA-2 stream updates on the SHA-384 PRF
# path the first time this ran.
export CFLAGS="-O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer"
export LDFLAGS="-fsanitize=address,undefined"
export ASAN_OPTIONS=detect_leaks=1:strict_string_checks=1
export UBSAN_OPTIONS=print_stacktrace=1
make -C samples/network/tls_retro clean
samples/network/tls_retro/local_server_test.sh
make -C samples/network/tls_retro clean
make -C samples/network/smb_retro clean
sudo -E samples/network/smb_retro/local_samba_test.sh
sudo -E samples/network/smb_retro/local_ad_dc_test.sh
make -C samples/network/smb_retro clean
make -C samples/network/nfs_retro clean
samples/network/nfs_retro/local_unfs3_test.sh
make -C samples/network/nfs_retro clean
sudo -E samples/network/nfs_retro/local_ganesha_test.sh
sudo make -C samples/network/nfs_retro clean
sudo -E samples/network/nfs_retro/local_read_plus_test.sh
sudo make -C samples/network/nfs_retro clean
make -C samples/network/krb5_retro clean
samples/network/krb5_retro/local_kdc_test.sh
make -C samples/network/krb5_retro clean
- name: Built-in NFSv3 client against unfs3
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# net_nfs3.c end to end against a user-space NFSv3 server built
# from unfs3's repository (Ubuntu no longer packages it), on
# fixed ports without a portmapper: mount, write, read, seek,
# stat, mkdir, rename, truncate, list, unlink, refused export.
samples/network/nfs_retro/local_unfs3_test.sh
# NFSv4.0 (one COMPOUND per operation, no portmapper or MOUNT,
# pseudo-filesystem export) against nfs-ganesha from apt
sudo -E samples/network/nfs_retro/local_ganesha_test.sh
# NFSv4.2 READ_PLUS against a fake server reporting the holes
# the filesystem really holds, its replies checked by
# Wireshark's own dissector
sudo make -C samples/network/nfs_retro clean
sudo -E samples/network/nfs_retro/local_read_plus_test.sh
# and its Windows link line, cross-linked
sudo make -C samples/network/nfs_retro clean
CC=x86_64-w64-mingw32-gcc make -C samples/network/nfs_retro nfs_test
make -C samples/network/nfs_retro clean
CC=x86_64-w64-mingw32-gcc make -C samples/network/tls_retro tls_fetch
make -C samples/network/tls_retro clean
- name: Built-in Kerberos client against a MIT KDC
shell: bash
working-directory: ${{ github.workspace }}
run: |
set -eu
# net_krb5.c against krb5kdc on a fixed port: AS with and
# without PA-ENC-TIMESTAMP, TGS for a cifs/ service with the
# ticket checked against the service's keytab key, wrong
# password and unknown service refused.
samples/network/krb5_retro/local_kdc_test.sh
make -C samples/network/krb5_retro clean
CC=x86_64-w64-mingw32-gcc make -C samples/network/krb5_retro krb5_test
make -C samples/network/krb5_retro clean
+107
View File
@@ -0,0 +1,107 @@
name: CI Linux without pkg-config
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
no-pkg-config:
name: Configure and build with pkg-config unavailable
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Install dependencies
shell: bash
run: |
set -eu
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends \
build-essential zlib1g-dev libfreetype-dev libdrm-dev \
libasound2-dev libpulse-dev libpipewire-0.3-dev \
libdbus-1-dev libwayland-dev libwayland-bin libxkbcommon-dev \
libdecor-0-dev libssl-dev qt6-base-dev qt6-base-dev-tools
- name: Checkout
uses: actions/checkout@v5
- name: Configure
shell: bash
run: |
set -eu
# PKG_CONF_PATH=none makes configure behave as it does on a
# system with no pkg-config binary: every check_val falls
# back to a plain link test and takes its cflags from the
# include directories it is given. A library that links but
# whose headers are not on the include path must end up
# disabled, never enabled with empty cflags.
PKG_CONF_PATH=none ./configure --enable-dbus
grep -q '^HAVE_PIPEWIRE = 1' config.mk
grep -q '^HAVE_PIPEWIRE_STABLE = 1' config.mk
grep '^PIPEWIRE_CFLAGS = ' config.mk | grep -q -- '-I/usr/include/pipewire-0.3'
grep '^PIPEWIRE_CFLAGS = ' config.mk | grep -q -- '-I/usr/include/spa-0.2'
# Found without pkg-config: D-Bus by its headers, one of them
# under the library directory; Wayland by its libraries and the
# scanner's own version; Qt through qmake -query.
for have in DBUS WAYLAND WAYLAND_SCANNER LIBDECOR OPENSSL QT QT6; do
grep -q "^HAVE_$have = 1" config.mk \
|| { echo "::error::HAVE_$have not found without pkg-config"; exit 1; }
done
grep '^DBUS_CFLAGS = ' config.mk | grep -q -- '-I/usr/include/dbus-1.0'
grep '^DBUS_CFLAGS = ' config.mk | grep -q -- 'dbus-1.0/include'
- name: Build
shell: bash
run: |
set -eu
make -j"$(nproc)"
test -x ./retroarch
test -f obj-unix/release/audio/drivers/pipewire.o
test -f obj-unix/release/camera/drivers/pipewire.o
test -f obj-unix/release/gfx/common/dbus_common.o
test -f obj-unix/release/gfx/common/wayland_common.o
test -f obj-unix/release/ui/drivers/ui_qt.o
- name: The run-time libdbus mirror matches libdbus
shell: bash
run: |
set -eu
# gfx/common/dbus_runtime.h mirrors libdbus's public structs and
# constants so RetroArch can load it without its headers; the
# D-Bus CFLAGS configure found above lead to the real ones.
make -C samples/frontend/dbus_runtime check \
DBUS_CFLAGS="$(sed -n 's/^DBUS_CFLAGS = //p' config.mk)"
- name: A package forced by name fails when nothing can find it
shell: bash
run: |
set -u
# rsound is found only through pkg-config. Asked for by name with
# no pkg-config, configure must stop and say so rather than
# quietly build without it.
if PKG_CONF_PATH=none ./configure --enable-rsound > forced.log 2>&1; then
cat forced.log
echo "::error::--enable-rsound without pkg-config configured instead of failing"
exit 1
fi
grep -q 'Forced to build with package rsound, but pkg-config was not found' forced.log \
|| { cat forced.log; echo "::error::configure failed for another reason"; exit 1; }
# A forced package with a library check behind it still configures.
PKG_CONF_PATH=none ./configure --enable-pulse > forced.log 2>&1 \
|| { cat forced.log; echo "::error::--enable-pulse failed without pkg-config"; exit 1; }
grep -q '^HAVE_PULSE = 1' config.mk
@@ -0,0 +1,178 @@
name: CI Linux overlay on screen
# The one overlay test that looks at the screen. It builds a real
# retroarch with X11 and OpenGL, runs it under Xvfb on Mesa's software
# rasteriser with an input overlay enabled, threaded video off and then
# on, grabs the screen and checks the overlay is where its config puts
# it and nowhere else. It then checks the menu's mouse cursor with an
# overlay up: a page of "nul" buttons (an LED overlay) must leave the
# cursor to the menu, a page that takes input must not. Last, with the
# overlay LED driver on, it checks that only LED images are hidden and
# that a hidden one stays hidden while its button is held.
#
# Every stubbed overlay test passed while an enabled overlay covered the
# whole screen (2026-09-19). Nothing that stubs the driver, or builds
# the pack by hand, can see that; this can.
on:
push:
branches:
- master
paths:
- 'input/input_driver.c'
- 'input/input_overlay*'
- 'tasks/task_overlay.c'
- 'led/drivers/led_overlay.c'
- 'gfx/gfx_surface.*'
- 'gfx/video_driver.c'
- 'gfx/video_thread_wrapper.*'
- 'gfx/drivers/gl2.c'
- 'menu/menu_driver.c'
- 'input/drivers/x11_input.c'
- 'samples/input/overlay_on_screen/**'
- '.github/workflows/Linux-overlay-on-screen.yml'
pull_request:
paths:
- 'input/input_driver.c'
- 'input/input_overlay*'
- 'tasks/task_overlay.c'
- 'led/drivers/led_overlay.c'
- 'gfx/gfx_surface.*'
- 'gfx/video_driver.c'
- 'gfx/video_thread_wrapper.*'
- 'gfx/drivers/gl2.c'
- 'menu/menu_driver.c'
- 'input/drivers/x11_input.c'
- 'samples/input/overlay_on_screen/**'
- '.github/workflows/Linux-overlay-on-screen.yml'
workflow_dispatch:
permissions:
contents: read
# Only pull requests cancel a superseded run. On master every push
# gets its own group (keyed by the commit), so a burst of pushes queues
# instead of cancelling the run for the commit before it.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
overlay-on-screen:
name: Run retroarch under Xvfb and look at the overlay
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Install dependencies
timeout-minutes: 20
run: |
for i in 1 2 3; do
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 && break
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential zlib1g-dev ccache python3 \
xvfb x11-apps libgl1-mesa-dri \
libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev libxtst6 \
libxxf86vm-dev libxrandr-dev libxinerama-dev \
libgl-dev libegl-dev && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
build-essential zlib1g-dev ccache python3 \
xvfb x11-apps libgl1-mesa-dri \
libx11-dev libx11-xcb-dev libxcb1-dev libxext-dev libxtst6 \
libxxf86vm-dev libxrandr-dev libxinerama-dev \
libgl-dev libegl-dev
- name: Checkout
uses: actions/checkout@v5
- name: Restore ccache
uses: actions/cache@v4
with:
path: ~/.ccache
key: ccache-${{ github.workflow }}-${{ github.job }}-${{ github.sha }}
restore-keys: |
ccache-${{ github.workflow }}-${{ github.job }}-
- name: Enable ccache
run: |
set -eu
export CCACHE_DIR="$HOME/.ccache"
echo "CCACHE_DIR=$CCACHE_DIR" >> "$GITHUB_ENV"
echo "/usr/lib/ccache" >> "$GITHUB_PATH"
ccache -M 500M
ccache -z
- name: Build RetroArch (X11 + OpenGL, the rest off)
shell: bash
run: |
set -eu
# What draws the overlay has to be real: X11, the gl driver,
# the menu, threads. Everything else is build time.
./configure --disable-qt --disable-wayland --disable-sdl \
--disable-sdl2 --disable-ffmpeg --disable-vulkan \
--disable-pulse --disable-discord \
--disable-networking --disable-cheevos \
--disable-glslang --disable-slang \
--disable-spirv_cross --disable-xmb \
--disable-materialui --disable-ozone \
--disable-7zip --disable-flac \
--disable-audiomixer --disable-translate \
--disable-accessibility --disable-crtswitchres \
--disable-langextra --disable-libretrodb \
--disable-video_filter --disable-dsp_filter
make -j$(nproc)
ccache -s
- name: Run overlay_on_screen_test (threaded video off, then on)
shell: bash
run: |
set -eu
# Two solid magenta squares on a full-screen page. Each must
# be where the config puts it, the middle of the screen must
# not be overlay, and the overlay must cover about the area of
# its two squares. With "does this desc have an image" asked
# of pixels that had just been released, no desc's geometry
# was set and the overlay covered 100% of the screen, in both
# modes.
timeout 180 python3 samples/input/overlay_on_screen/overlay_on_screen_test.py ./retroarch
- name: Run overlay_menu_mouse_test (display and input packs, threaded off and on)
shell: bash
run: |
set -eu
# The menu switched its mouse off whenever any overlay was
# alive, so an LED overlay - nothing on it to press - left a
# fullscreen desktop user with no menu cursor. A pack of
# "nul" buttons must now show the cursor where the pointer is
# moved; the same pack bound to "a" must still not.
timeout 180 python3 samples/input/overlay_on_screen/overlay_menu_mouse_test.py ./retroarch
- name: Run overlay_led_hidden_test (mapped and named LED packs, threaded off and on)
shell: bash
run: |
set -eu
# With the overlay LED driver on and Up held through XTEST: an
# image at a ledN_map slot, "nul" or a control, stays hidden,
# as does one named with _led, and a pack that names its LEDs
# is not mapped.
timeout 300 python3 samples/input/overlay_on_screen/overlay_led_hidden_test.py ./retroarch
+18 -1
View File
@@ -39,7 +39,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libavcodec-dev libsdl2-dev libsdl-image1.2-dev libxml2-dev yasm"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -52,6 +52,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+18 -1
View File
@@ -35,7 +35,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential pkg-config zlib1g-dev qt6-base-dev libgl-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -48,6 +48,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+66 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libasound2-dev libsdl2-dev libopenal-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -555,6 +572,24 @@ jobs:
./device_list_driver_test
echo "[pass] device_list_driver_test (ASan)"
- name: Build and run device_label_path_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/device_label_path
run: |
set -eu
# A device list entry with the hardware's name after the path,
# "/dev/dsp0 (HDA Intel)", is stored by the menu as shown and
# comes back to init as the device string; oss opened it as a
# path and failed, so a device picked from the list could not
# be used. Compiles oss and audioio whole with open() redirected
# to a recorder, and checks each reaches it with the path.
make clean && make -j$(nproc) all SANITIZER=address,undefined
test -x device_label_path_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./device_label_path_test
echo "[pass] device_label_path_test (ASan)"
- name: Build and run rate_control_bounds_test (ASan + UBSan)
shell: bash
working-directory: samples/audio/rate_control_bounds
@@ -743,6 +778,36 @@ jobs:
WINEDEBUG=-all timeout 300 make check
echo "[pass] dsound_pump_test (wine)"
- name: Build and run wdmks_loop_test (MinGW, under wine)
shell: bash
working-directory: samples/audio/wdmks_loop
run: |
set -eu
# audio/drivers/wdmks.c compiled whole, its WaveRT loop driven
# by hand: the test owns the driver's state, points the
# position register at a word of its own and moves the
# hardware itself, so nothing reaches the kernel and wine
# needs no sound card. Pins that every byte the cursor passes
# is cleared, so the loop plays silence when the frontend
# stops writing - a pause, a menu, a stall - instead of the
# last loop of audio round and round; that a write after
# that lands a margin ahead of the hardware rather than a
# loop behind it; that a full loop is not taken for an
# overrun; and that a start puts the model back at the top
# of the loop with the hardware; and that the resync margin is
# the FIFO the pin reports plus half a millisecond, two
# milliseconds where it reports none; and that gaps in the
# audio are counted once each, neither a start nor every
# sample of a pause.
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 \
|| ! command -v wine >/dev/null 2>&1; then
sudo apt-get install -y -qq gcc-mingw-w64-x86-64 wine64 \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30
fi
make clean all
WINEDEBUG=-all timeout 120 make check
echo "[pass] wdmks_loop_test (wine)"
- name: Build and run wasapi_exclusive_pacing_test (ASan + UBSan, then TSan)
shell: bash
working-directory: samples/audio/wasapi_exclusive_pacing
+18 -1
View File
@@ -41,7 +41,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential pkg-config ffmpeg libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -54,6 +54,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+18 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential ffmpeg"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+533 -100
View File
@@ -20,6 +20,31 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
video-thread-state:
name: The video thread reads its snapshot
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Tree check
shell: bash
run: |
set -eu
# The main thread builds a video_frame_info_t and hands it
# over; a driver that reaches back into config_get_ptr() from
# frame() or present_last() reads settings the main thread
# may be writing. TSan found one of those in the null driver
# rather than anyone noticing it by reading, so this looks
# for the rest: it walks what those two entry points call
# and fails on live-state access it can reach. The drivers
# for platforms with no video thread are listed in the
# checker with that reason, and the list has to shrink when
# one of them stops doing it.
python3 tools/video_thread_state_check.py
surface-requirements:
name: Producers ask the surface layer
runs-on: ubuntu-latest
@@ -62,10 +87,11 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libx11-dev libxrandr-dev libdrm-dev"
pkgs="$pkgs xserver-xorg-core xserver-xorg-video-dummy x11-xserver-utils libsdl2-dev libxinerama-dev"
pkgs="$pkgs libwayland-dev"
pkgs="$pkgs gcc-mingw-w64-x86-64 wine64 openbox cmake ninja-build"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
@@ -77,6 +103,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -180,51 +223,6 @@ jobs:
done
echo "[pass] atomic backend portability"
- name: Build and run vblank_clock_ddraw_test (MinGW, under wine)
shell: bash
working-directory: samples/gfx/vblank_clock_ddraw
run: |
set -eu
# Scanline Sync's Windows 2000/XP tier against a real DirectDraw
# (wine's, which reports the counter as an XDDM driver does):
# GetScanLine readings into vblank_sampler_feed and the vblank
# clock, and the clock's beam checked against GetScanLine.
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1 \
|| ! command -v wine >/dev/null 2>&1 \
|| ! command -v xvfb-run >/dev/null 2>&1; then
sudo apt-get install -y -qq gcc-mingw-w64-x86-64 wine64 xvfb \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30
fi
make clean all
WINEDEBUG=-all timeout 300 xvfb-run -a -s "-screen 0 1280x1024x24" make check
echo "[pass] vblank_clock_ddraw_test (wine)"
- name: Build and run vblank_clock_test and vblank_clock_pub_test (C89, ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/vblank_clock
run: |
set -eu
# gfx/common/vblank_clock.h: the arithmetic Scanline Sync on
# Windows uses in place of polling D3DKMTGetScanLine - a clock
# fed vblank timestamps from a waiter that only ever wakes
# late, and the beam position it implies. Against 50 simulated
# displays up to 600 ppm off their nominal rate: no beam
# before it settles, beam and wait landing within 6 lines,
# missed and duplicate vblanks, a stale clock, and variable
# refresh never reported.
make clean all SANITIZER=address,undefined
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 60 ./vblank_clock_test
echo "[pass] vblank_clock_test"
# The seqlock handing the clock from the kernel-event thread to
# the frame thread: one writer publishing flat out, three
# readers checking every snapshot is one whole publish and
# none goes backwards - under ASan+UBSan, then TSan, which
# also checks the pattern is free of formal data races.
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 ./vblank_clock_pub_test
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./vblank_clock_pub_test
echo "[pass] vblank_clock_pub_test (ASan, TSan)"
- name: Build and run vulkan_extension_count_test (ASan)
shell: bash
working-directory: samples/gfx/vulkan_extension_count
@@ -378,20 +376,20 @@ jobs:
set -eu
# Regression test for the partial-init leak fix in
# gfx/common/vulkan_common.c::vulkan_emulated_mailbox_init.
# Pre-fix the function had three sequential allocations
# (scond_new, slock_new, sthread_create) and on any of the
# latter two failures returned `false` directly, leaking
# the already-allocated cond and/or lock. Both production
# call sites in vulkan_create_swapchain ignore the return
# value, so an init failure also left vk->mailbox.lock ==
# NULL and vk->mailbox.cond == NULL while VK_DATA_FLAG_
# EMULATING_MAILBOX was still set, setting up a NULL-deref
# the next time vulkan_acquire_next_image routed into
# vulkan_emulated_mailbox_acquire_next_image (slock_lock
# on a NULL pointer). Fix routes every early failure
# through `goto error` to a single deinit call, which is
# null-safe and ends with a memset, leaving the struct in
# the same shape the deinit-on-shutdown path produces and
# The function brings up three things in sequence (two
# eventcounts and the acquire thread; a condition variable,
# a mutex and the thread when this was written) and used to
# return `false` directly when the second or third failed,
# leaking what the earlier ones had allocated. Both
# production call sites in vulkan_create_swapchain ignore
# the return value, so an init failure also left the
# mailbox half built while VK_DATA_FLAG_EMULATING_MAILBOX
# was still set, setting up a crash the next time
# vulkan_acquire_next_image routed into the emulated path.
# Fix routes every early failure through `goto error` to a
# single deinit call, which is safe on a partly built
# mailbox and ends with a memset, leaving the struct in the
# same shape the deinit-on-shutdown path produces and
# tripping the existing `mailbox.swapchain == VK_NULL_
# HANDLE` guard at vulkan_acquire_next_image. Build under
# AddressSanitizer with leak detection so any
@@ -485,42 +483,19 @@ jobs:
working-directory: samples/gfx/gfx_widgets_msg_queue_race
run: |
set -eu
# Regression test for the producer-producer race fix on
# dispgfx_widget_t::msg_queue in gfx/gfx_widgets.c.
# Pre-fix the FIFO had no dedicated lock: producers could
# be invoked from any thread (the threaded task system at
# libretro-common/queues/task_queue.c runs a worker
# thread; gfx/video_driver.c::video_driver_frame released
# RUNLOOP_MSG_QUEUE_LOCK before calling the producer, so
# main-thread producers ran unlocked) yet there was no
# lock dedicated to msg_queue itself. Post-fix a
# msg_queue_lock is acquired around every fifo_write (in
# gfx_widgets_msg_queue_push), every fifo_read (in
# gfx_widgets_iterate), and the avail-check that gates
# them. The outer fast-path FIFO_*_AVAIL checks that
# used to wrap the function bodies were dropped: reading
# the FIFO cursors outside the lock is itself a data
# race against the locked writes (TSan-detectable;
# benign on x86 TSO but real on weak-memory hardware).
#
# ThreadSanitizer is the right validator: it instruments
# every memory access on fifo_buffer_t::end and
# ::first and would flag a missing-lock regression as a
# data race on the producer side, the consumer side, or
# the avail check. ASan/UBSan would not catch the lock
# removal at all. halt_on_error=1 makes TSan exit
# non-zero on the first observed race, which is what we
# want for CI: any race here means the production
# locking discipline is broken.
#
# The test mirrors the post-fix locking protocol of
# gfx_widgets_msg_queue_push and gfx_widgets_iterate's
# FIFO interaction; the widget allocation, font
# measurement, and animation push that wrap the actual
# FIFO calls in production are not part of the race and
# are stripped from the test. If gfx_widgets.c amends
# the locking around msg_queue, the verbatim copies in
# gfx_widgets_msg_queue_race_test.c must follow.
# The widget message ring in gfx/gfx_widgets.c is a lock-free
# single-producer / single-consumer queue: the main thread
# publishes through msg_queue_tail, the widgets' owner pops
# through msg_queue_head, each cursor released by its writer
# and acquired by the other side, and the slot pointer rides
# that pairing. The test builds the production push and pop
# out of gfx_widgets.c itself and drives them from one producer
# thread and one consumer thread, checking order, loss, and the
# full-ring refusal. ThreadSanitizer is the validator that
# matters: a store to the slot array outside the release /
# acquire pairing, or a cursor published before its slot is
# written, is a race it reports and the value checks may not.
# halt_on_error=1 fails the run on the first race.
make clean all SANITIZER=thread
test -x gfx_widgets_msg_queue_race_test
TSAN_OPTIONS=halt_on_error=1 timeout 60 \
@@ -601,6 +576,16 @@ jobs:
TSAN_OPTIONS=halt_on_error=1 timeout 120 \
./gfx_widgets_sanitize_test
echo "[pass] gfx_widgets_sanitize_test (TSan)"
- name: Build and run widget snapshot ownership test
shell: bash
working-directory: tests-other
run: |
set -eu
make -f Makefile.widget_snapshot clean
make -f Makefile.widget_snapshot
timeout 60 ./widget_snapshot_ownership_test
make -f Makefile.widget_snapshot clean
- name: Build and run gfx_widgets_task_bar_stack_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_widgets_task_bar_stack
@@ -652,6 +637,79 @@ jobs:
timeout 60 ./display_servers_test
echo "[pass] display_servers_test"
- name: Build and run dispserv_gx_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/dispserv_gx
run: |
set -eu
# The GameCube/Wii display server (dispserv_gx.c and the mode
# rules in dispserv_gx_modes.c), linked as they ship against
# stubbed libogc queries, on every TV standard: the list holds
# only modes the standard shows, one of them current, and each
# entry - through the label the menu prints and the parse the
# menu does - selects exactly that mode, stores its config id
# and reports the refresh rate it listed.
#
# The menu's parse used to run strtoul() inside
# VIDEO_SCALE_PUT_H, whose PACK evaluates its argument twice;
# the second call returned 0 and every pick from the list
# went out as Wx0. Guard the shape as well as the behaviour.
if grep -nE 'VIDEO_SCALE_(PUT_[WH]|PACK)\([^;]*strto' \
../../../menu/cbs/menu_cbs_ok.c; then
echo "::error::strtoul inside a VIDEO_SCALE macro runs twice"
exit 1
fi
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./dispserv_gx_test
make clean
- name: Build and run dispserv_ps3_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/dispserv_ps3
run: |
set -eu
# The PS3 display server (dispserv_ps3.c and the mode rules in
# dispserv_ps3_modes.c), linked as they ship against stubbed
# PSL1GHT video output queries, for displays taking different
# sets of modes with the system menu in each: the list holds
# exactly what the display takes with the mode in use marked,
# each entry - through the menu's label and parse - selects
# that mode and reinitialises the driver once, and
# ps3_display_server_resolution(), which the RSX driver
# configures the output from, follows the choice (before, the
# RSX driver ignored the setting and kept the system menu's
# mode) and falls back to the system mode when the display
# does not take it.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./dispserv_ps3_test
make clean
- name: X11 window poll sends no requests (Xvfb, ASan + UBSan)
shell: bash
run: |
set -eu
# x11_check_window() and x11_has_focus() run every frame; a
# reply-bearing request there waits behind whatever else is
# queued on the connection (a software Vulkan WSI's whole
# PutImage frame). Focus comes from FocusIn/FocusOut, so the
# X contexts must select FocusChangeMask.
for f in x_ctx.c x_vk_ctx.c xegl_ctx.c; do
grep -q FocusChangeMask gfx/drivers_context/$f \
|| { echo "::error::$f does not select FocusChangeMask"; exit 1; }
done
sudo apt-get install -y -qq xvfb > /dev/null
Xvfb :97 -screen 0 1024x768x24 > /tmp/xvfb97.log 2>&1 &
XVFB_PID=$!
sleep 1
(cd samples/gfx/x11_frame_poll \
&& make clean all SANITIZER=address,undefined \
&& DISPLAY=:97 ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./x11_frame_poll_test)
kill $XVFB_PID
echo "[pass] x11_frame_poll_test"
- name: Live mode switching on an Xorg dummy server (X11 + SDL)
shell: bash
run: |
@@ -664,7 +722,10 @@ jobs:
# cannot stand in). The X11 sample runs the modeline engine
# end to end - generate a 15 kHz timing, put it on the wire
# through dispserv_x11, read the crtc back and compare every
# field, switch again, restore - and the SDL sample switches
# field, switch again, restore - then the Screen Resolution
# callbacks: the listed modes, a smaller, a larger and a
# rate-only switch with the framebuffer following, an
# unlisted size refused, and back. The SDL sample switches
# a real SDL2 window among the listed modes through
# dispserv_sdl2 (and dispserv_sdl3 on an SDL3 build), for the
# modeline path and the menu picker.
@@ -689,6 +750,18 @@ jobs:
&& ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 120 ./display_servers_x11_live_test)
echo "[pass] display_servers_x11_live_test"
# The refresh rate a core polls every frame: answered from
# memory once the X event pump drains the connection, with no
# request to the server, and changed by the next pump after
# another client switches the mode - under a reader on a
# second thread, once under ASan+UBSan and once under TSan.
(cd samples/gfx/x11_refresh_rate \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 120 ./x11_refresh_rate_test \
&& make clean all SANITIZER=thread \
&& TSAN_OPTIONS=halt_on_error=1 timeout 120 ./x11_refresh_rate_test)
echo "[pass] x11_refresh_rate_test"
(cd samples/gfx/display_servers_sdl2_live \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
@@ -746,6 +819,80 @@ jobs:
pkill openbox || true
sudo kill "$XORG_PID" || true
- name: Screen Resolution through Mutter's DisplayConfig (ASan + UBSan)
shell: bash
run: |
set -eu
# Under GNOME's Wayland session the X11 driver runs on
# XWayland, which lists every mode at the desktop's current
# rate and only scales the window when "switched", and the
# Wayland driver had no resolution list at all; Mutter's
# org.gnome.Mutter.DisplayConfig D-Bus interface is the only
# way to the monitor's real modes. mock_mutter.py carries
# Mutter's signatures and its ApplyMonitorsConfig checks; the
# test drives gfx/common/mutter_displayconfig.c directly and
# through dispserv_x11 (real Xorg dummy server, and Xwayland
# under a headless Weston) and dispserv_wl, with and without
# Mutter on the bus. Without it every path must behave
# exactly as before: no session bus, a bus without Mutter, a
# real X server (Mutter is never asked), XWayland's own list,
# and no Wayland resolution list. Last, a real headless Mutter
# and its XWayland: Mutter names XWayland's outputs after its
# connectors (DP-1, Meta-0), so only the XWAYLAND extension
# says what the server is.
sudo apt-get install -y -qq libdbus-1-dev dbus python3-dbus \
python3-gi weston xwayland mutter > /dev/null
(cd samples/gfx/mutter_displayconfig \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
XORG="sudo Xorg" timeout 300 ./run.sh)
echo "[pass] mutter_displayconfig_test"
- name: D3D11 hardware ring under threaded video (mingw + Wine, headless)
shell: bash
run: |
set -o pipefail
# The threaded wrapper hands a D3D11 hardware core a deferred
# context and replays its frames on the video thread. Nothing
# on Linux exercises that, and it shipped showing one frame
# and then black. The real proxy and the driver's own
# FinishCommandList argument, run under Wine, where wined3d
# implements deferred contexts over OpenGL. Requires the ring
# and the immediate context to show every frame, and the ring
# as it used to be to fail.
sudo apt-get install -y -qq xvfb > /dev/null
samples/gfx/d3d11_hw_ring/run.sh
- name: libretro_d3d11.h version 2, the contract run (mingw + Wine, headless)
shell: bash
run: |
set -o pipefail
# Version 2 of the D3D11 hardware render interface lets a core
# and a frontend that presents from another thread share the
# device's one immediate context by taking turns, so the core
# keeps everything a deferred context cannot do - reading back
# above all. This runs the two against each other on a real
# D3D11 device (wined3d), and requires every frame and every
# readback to be right, and a core that ignores what
# lock_context tells it to be caught.
sudo apt-get install -y -qq xvfb > /dev/null
REQUIRE_D3D11=1 samples/gfx/d3d11_hw_interface_v2/run.sh
- name: libretro_d3d12.h version 2, the contract run (mingw + Wine + vkd3d, headless)
shell: bash
run: |
set -o pipefail
# Version 2 of the D3D12 hardware render interface lets a core
# hand a frame to a frontend that presents from another thread
# without the copy version 1 forces. This runs a core and such a
# frontend against each other on a real D3D12 device - vkd3d
# over Mesa's software Vulkan driver - and requires every frame
# to arrive intact, and a core that ignores wait_sync_index to
# be caught.
sudo apt-get install -y -qq xvfb libvkd3d1 mesa-vulkan-drivers > /dev/null
REQUIRE_D3D12=1 samples/gfx/d3d12_hw_interface_v2/run.sh
- name: Build and run display_servers_kms_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/display_servers_kms
@@ -782,6 +929,125 @@ jobs:
timeout 60 ./display_servers_kms_test
echo "[pass] display_servers_kms_test"
- name: Build and run drm_hdr_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/drm_hdr
run: |
set -eu
# gfx/common/drm_hdr.c: which EDIDs take HDR10 and what they
# declare, malformed ones refused without an overread, and the
# HDR_OUTPUT_METADATA blob, whose layout must be the kernel's.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./drm_hdr_test
echo "[pass] drm_hdr_test"
- name: Build and run wayland_wlr_output_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_wlr_output
run: |
set -eu
# gfx/common/wayland_wlr_output.c, the Wayland display server's
# resolution list and mode switch on wlroots compositors, against
# an in-process compositor that checks every configuration names
# each head exactly once, as the protocol requires.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_wlr_output_test
echo "[pass] wayland_wlr_output_test"
- name: Build and run wayland_cursor_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_cursor
run: |
set -eu
# input/common/wayland_cursor.h: the size and scale the Wayland
# context loads its cursor theme at when the compositor has no
# cursor-shape-v1 - XCURSOR_SIZE honoured, fractional scales
# rounded up to a whole buffer scale.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_cursor_test
echo "[pass] wayland_cursor_test"
- name: Build and run wayland_resize_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_resize
run: |
set -eu
# gfx/common/wayland_resize.h, what the Wayland GL and Vulkan
# contexts do once their buffers are resized, against an
# in-process compositor: configures count again whatever the
# scaling, and the buffer scale goes only to a surface on the
# whole-scale path that is new enough to take it.
export XDG_RUNTIME_DIR=$(mktemp -d)
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_resize_test
echo "[pass] wayland_resize_test"
- name: Build and run video_mode_select_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/video_mode_select
run: |
set -eu
# gfx/common/video_mode_select.c: which listed mode a size and
# rate name, and the order a resolution list is shown in - the
# code every display server's mode switch goes through.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./video_mode_select_test
echo "[pass] video_mode_select_test"
- name: Build and run wayland_kwin_output_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_kwin_output
run: |
set -eu
# gfx/common/wayland_kwin_output.c, the Wayland display server's
# resolution list and mode switch on KWin, against an in-process
# compositor speaking KWin's output protocols in both the
# registry form and the older per-output globals.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_kwin_output_test
echo "[pass] wayland_kwin_output_test"
- name: Build and run wayland_color_management_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_color_management
run: |
set -eu
# gfx/common/wayland_color.c, which tags an HDR GL surface as
# Windows-scRGB, against an in-process compositor that records
# every fatal wp_color_manager_v1 rule the client could break.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wayland_color_management_test
echo "[pass] wayland_color_management_test"
- name: Build and run wayland_drm_lease_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/wayland_drm_lease
run: |
set -eu
# The DRM lease path in gfx/common/wayland_drm_lease.c and
# gfx/display_servers/dispserv_wl.c, driven by a test
# compositor on libwayland-server: what is offered, which
# connector an index picks, an index past the offer, a
# refusal, a revoke arriving while the client is idle, and
# no Wayland session at all. The leased descriptor and the
# device's non-master fd are the ones a mistake leaks, so
# the lane runs under ASan. Both protocol halves are
# generated here from deps/wayland-protocols, which needs
# wayland-scanner and the client and server libraries -
# libwayland-dev carries all three.
make clean all SANITIZER=address,undefined
test -x wayland_drm_lease_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./wayland_drm_lease_test
echo "[pass] wayland_drm_lease_test"
- name: Build and run modeline_edid_parse_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/modeline_edid_parse
@@ -802,6 +1068,73 @@ jobs:
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./modeline_edid_parse_test
- name: Build and run integer_scale_axis_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/integer_scale_axis
run: |
set -eu
# video_viewport_get_scaled_integer(), pulled out of
# gfx/video_driver.c by extract_integer.awk, swept over
# viewport sizes, frame sizes, every axis mode that touches
# X and every scaling mode. Y+X has to be integer on both
# axes: the X axis was once multiplied from the
# aspect-corrected width (256x224 at 64:49 is 293), giving
# 1172x896 at 1080p - a 4.58x horizontal scale and uneven
# pixel columns - on every video driver.
make clean all SANITIZER=address,undefined
test -x integer_scale_axis_test
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 60 ./integer_scale_axis_test
make clean
- name: Build and run font_lifecycle_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/font_lifecycle
run: |
set -eu
# Where a font is created follows the threading hint; where
# it is REBUILT does not - a rebuild marshals whenever the
# wrapper is up, because a GL backend making its context
# current on the main thread while the video thread holds it
# is an X BadAccess. Nothing ran this sample, and it had the
# older rule written into it.
# The fallback tests read DejaVu in place of the assets'
# fallback fonts: Sans for fallback-font.ttf, and Sans Mono
# and Sans as a general and a second face for the chain test.
sudo apt-get install -y -qq fonts-dejavu-core fonts-dejavu-mono \
> /dev/null 2>&1 || sudo apt-get install -y -qq fonts-dejavu-core > /dev/null
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 make check SANITIZER=address,undefined
make clean
# A fallback font is read on a thread of its own while two
# fonts on two threads ask for it, and a second face behind
# the first.
make fallback chain SAN=thread
TSAN_OPTIONS=halt_on_error=1 setarch "$(uname -m)" -R \
timeout 120 ./fallback
TSAN_OPTIONS=halt_on_error=1 setarch "$(uname -m)" -R \
timeout 120 ./chain
make clean
- name: Build and run overlay_apng_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/overlay_apng
run: |
set -eu
# An animated overlay image is a surface with slots whose
# texture is updated in place, so the pages' handles stand
# while its frames advance. The sample builds its own APNG
# fixture and holds that contract: distinct frames, a pass
# that ends and replays, one texture across the submits, and
# a free with a frame still in flight.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 120 ./overlay_apng_test fixtures/anim.png
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./overlay_apng_test fixtures/anim.png
make clean
- name: Build and run gfx_thumbnail_anim_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_anim
@@ -876,13 +1209,50 @@ jobs:
# unloaded, never handed to whatever reused the slot), an
# off-main-thread ask only recorded until the main thread's
# service starts it, the default badge lent rather than
# given, and a refused async upload treated as a failure.
# given, a 0 reported as a short wait only while a file on
# disk is loading, and a refused async upload treated as a
# failure.
make clean all SANITIZER=address,undefined
test -x badge_cache_test
ASAN_OPTIONS=detect_leaks=1 timeout 60 ./badge_cache_test
echo "[pass] badge_cache_test (ASan)"
make clean
- name: Build and run psp1_gfx_contracts_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/psp1_gfx_contracts
run: |
set -eu
# gfx/drivers/psp1_gfx.c is reached only by the griffin unity
# build for PSP, so nothing else here compiles it, let alone
# runs it. This links the real driver against a stand-in for
# the parts of the machine its correctness turns on: main RAM
# and VRAM each mapped twice so the cached and uncached
# aliases genuinely alias, a no-access page after VRAM so a
# blit off the end faults, display lists that stay the GE's
# until they are waited on, and a vblank sub-interrupt that
# can be fired from inside free().
#
# What it holds: the 24bpp BGR byte order video_driver.h
# specifies for read_viewport, filling the whole buffer the
# screenshot task hands over, releasing the interrupt handler
# before the driver struct it writes through, keeping the GE
# off a display list or framebuffer that is in use, both blits
# staying inside the buffers they target, cache-line ownership
# of everything the GE writes, and init failing cleanly when
# an allocation does.
make clean all SANITIZER=address,undefined
rc=0
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 120 ./psp1_gfx_contracts_test || rc=$?
# 77: the runner would not hand over the PSP address windows.
if [ "$rc" = 77 ]; then
echo "[skip] PSP address windows unavailable"
exit 0
fi
[ "$rc" = 0 ]
make clean
- name: Build and run gfx_thumbnail_preview_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/gfx_thumbnail_preview
@@ -952,6 +1322,15 @@ jobs:
AUDIO_BYTES_PER_TICK=65536 FRAMES=120 timeout 300 \
./gfx_thumbnail_preview_test fixtures/*.mp4 fixtures/anim_lossless.webp
echo "[pass] gfx_thumbnail_preview_test (ASan)"
# The same session with HAVE_THREADS: the thumbnail decodes on
# its worker and the pool, the app's own path, paced at a
# display's rate with a bound on the longest stall.
test -x gfx_thumbnail_preview_test_threaded
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 \
THREADS=4 FRAMES=180 PACE_HZ=60 PACE_MAX_STALL_MS=800 NOAUDIO=1 timeout 300 \
./gfx_thumbnail_preview_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4
echo "[pass] gfx_thumbnail_preview_test_threaded (ASan, 4 threads)"
# The frame-indexed types (animated WEBP, APNG) through the
# same session, windowed: animated WEBP had no progressive
# open, so the session read the whole file into the
@@ -1005,8 +1384,29 @@ jobs:
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 bash -c '
./preview_blit_bands_test fixtures/long_video.mp4
./preview_blit_bands_test fixtures/vp9_tiles.webm
./preview_blit_bands_test fixtures/hevc_wpp.mp4'
./preview_blit_bands_test fixtures/hevc_wpp.mp4
./preview_blit_bands_test fixtures/bframes_hevc.mp4
./preview_blit_bands_test fixtures/hevc_nowpp.mp4 fixtures/mbaff_h264.mp4
RH265_PUBLISH_DELAY=200 ./preview_blit_bands_test fixtures/hevc_wpp.mp4 fixtures/bframes_hevc.mp4 fixtures/hevc_nowpp.mp4'
echo "[pass] preview_blit_bands_test (ASan)"
# Catching up: behind the clock the stream drops only what
# nothing references, and every picture still shown is the
# full decode's; the threaded build does the same with four
# pictures in flight, the drops taken at submit.
test -x preview_catchup_test && test -x preview_catchup_test_threaded
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 bash -c '
./preview_catchup_test fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4
./preview_catchup_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4'
echo "[pass] preview_catchup_test (ASan, one picture at a time and four in flight)"
# The audio chain against ffmpeg: the track's rate exact, the
# resampled one within a fraction of a percent, from the first
# half second.
test -x preview_audio_oracle_test
ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 300 \
./preview_audio_oracle_test fixtures/trailing_small.mp4 fixtures/trailing_large.mp4
echo "[pass] preview_audio_oracle_test (ASan)"
# clean keeps fixtures/ by design; regenerate anyway so the
# step cannot silently run against an empty glob.
make clean all SANITIZER=thread
@@ -1014,15 +1414,48 @@ jobs:
TSAN_OPTIONS=halt_on_error=1:allocator_may_return_null=1 FRAMES=60 timeout 300 \
./gfx_thumbnail_preview_test fixtures/*.mp4 fixtures/anim_lossless.webp
echo "[pass] gfx_thumbnail_preview_test (TSan)"
# and threaded under TSan, which is the run that can see a
# race between the worker, the pool and the poll
THREADS=4 FRAMES=180 PACE_HZ=60 PACE_MAX_STALL_MS=1500 NOAUDIO=1 timeout 300 \
./gfx_thumbnail_preview_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4
echo "[pass] gfx_thumbnail_preview_test_threaded (TSan, 4 threads)"
# The pool and the tile-column threads under TSan: the
# shared frame buffers and contexts are written in disjoint
# columns, the counts merged after the join.
TSAN_OPTIONS=halt_on_error=1:allocator_may_return_null=1 timeout 300 bash -c '
./preview_blit_bands_test fixtures/long_video.mp4
./preview_blit_bands_test fixtures/vp9_tiles.webm
./preview_blit_bands_test fixtures/hevc_wpp.mp4'
./preview_blit_bands_test fixtures/hevc_wpp.mp4
./preview_blit_bands_test fixtures/bframes_hevc.mp4
./preview_blit_bands_test fixtures/hevc_nowpp.mp4 fixtures/mbaff_h264.mp4
RH265_PUBLISH_DELAY=200 ./preview_blit_bands_test fixtures/hevc_wpp.mp4 fixtures/bframes_hevc.mp4 fixtures/hevc_nowpp.mp4'
echo "[pass] preview_blit_bands_test (TSan)"
TSAN_OPTIONS=halt_on_error=1:allocator_may_return_null=1 timeout 300 bash -c '
./preview_catchup_test_threaded fixtures/bframes_h264.mp4 fixtures/bframes_hevc.mp4'
echo "[pass] preview_catchup_test_threaded (TSan)"
TSAN_OPTIONS=halt_on_error=1 timeout 600 bash -c '
./preview_audio_decode_test fixtures/trailing_large.mp4 1024 island
./preview_audio_decode_test fixtures/trailing_large.mp4 262144 island'
echo "[pass] preview_audio_decode_test (TSan)"
- name: Build and run hw_ring_fence_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/gfx/hw_ring_fence
run: |
set -u
set -o pipefail
# The hardware ring's fence handoff between the video thread
# and the core's thread (gfx/video_thread_hw_fence.h), the
# tree's own header, with two threads at one slot: a fence is
# signalled only while idle, waited only once signalled, and
# never inside two calls at once - with no lock. The single
# re-signalled fence it replaced is built too and must fail.
make clean all SANITIZER=address,undefined
timeout 120 make check SANITIZER=address,undefined
echo "[pass] hw_ring_fence_test (ASan + UBSan)"
# The two threads share nothing but atomics, and TSan is
# what says so.
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 make check SANITIZER=thread
echo "[pass] hw_ring_fence_test (TSan)"
make clean
+136 -3
View File
@@ -23,13 +23,13 @@ jobs:
samples-input:
name: Build and run samples/input
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 20
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libusb-1.0-0-dev"
pkgs="build-essential libusb-1.0-0-dev libx11-dev libxtst-dev libxi-dev xvfb gcc-mingw-w64-x86-64 wine64"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -65,6 +82,35 @@ jobs:
ccache -M 500M
ccache -z
- name: X11 input poll sends no requests (Xvfb, ASan + UBSan, TSan)
shell: bash
working-directory: samples/input/x11_input_poll
run: |
set -eu
# The X input driver's poll runs every frame. Keys, and with
# XInput 2 the pointer, come from events on the driver's own
# connection, drained at the poll, so it sends no request on
# either connection; a query on the shared one waits behind
# whatever the video thread presents through it. XTEST drives
# the keyboard (held before init, auto-repeat, focus changes,
# another client's keyboard grab) and the pointer (a press
# without motion, buttons 1, 3 and 8, the wheel, a drag out of
# the window, a new driver over a held button, and grabbed
# motion across a warp the server holds back behind a server
# grab - where any wait on the server would hang the test).
Xvfb :96 -screen 0 1024x768x24 > /tmp/xvfb96.log 2>&1 &
XVFB_PID=$!
sleep 1
make clean all SANITIZER=address,undefined
DISPLAY=:96 ASAN_OPTIONS=detect_leaks=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 120 ./x11_input_poll_test
make clean all SANITIZER=thread
DISPLAY=:96 TSAN_OPTIONS=halt_on_error=1 timeout 120 ./x11_input_poll_test
make clean
kill $XVFB_PID
echo "[pass] x11_input_poll_test (ASan, TSan)"
- name: Build and run illuminance_sensor_test (ASan + UBSan)
shell: bash
working-directory: samples/input/illuminance_sensor
@@ -134,3 +180,90 @@ jobs:
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./wii_handshake_test
echo "[pass] wii_handshake_test (ASan)"
- name: Build and run android_lifecycle_flush_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/input/android_lifecycle_flush
run: |
set -eu
# The Android background flush handshake, a UI thread against
# an app thread. Pins that onPause() and onStop() return only
# once SRAM, core options and the config are on disk - a
# swiped-away task is killed as soon as they have returned -
# that the save is written once per trip to the background,
# and that the startup pump acknowledges without saving.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./android_lifecycle_flush_test
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 60 ./android_lifecycle_flush_test
make clean
echo "[pass] android_lifecycle_flush_test (ASan, TSan)"
- name: Build and run android_kbd_keys_test (ASan + UBSan)
shell: bash
working-directory: samples/input/android_kbd_keys
run: |
set -eu
# Key events from the Android system keyboard, through the
# native input queue to the text field that owns the menu
# line. Pins that a Backspace the soft keyboard sends as a
# key event is handed back to the field instead of being
# consumed natively - consumed, the field kept the
# characters and the next snapshot put them back on the line.
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./android_kbd_keys_test
make clean
echo "[pass] android_kbd_keys_test (ASan)"
- name: Build and run overlay_textures_test (plain, ASan + UBSan, threaded)
shell: bash
working-directory: samples/input/overlay_textures
run: |
set -eu
# input/input_overlay_textures.c as it is, against a stub
# driver that reads every pixel it is handed. Pins every
# route a page takes to the driver: as textures (each page
# gets its own images' textures, a switch uploads nothing,
# the pixels go once the driver has them and the sizes
# stay), and as pixels through load() for a driver that
# declines the textures, has no such path, or fails an
# upload - live pixels on every page. 86fbf34e01 freed the
# pixels before the page lists were matched by them, so
# with threaded video off the fallback showed freed memory;
# under ASan that is a use-after-free here.
make clean all
timeout 60 ./overlay_textures_test
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./overlay_textures_test
# THREADS=1: the threaded wrapper as a stub whose video
# thread runs when the test says so, so both outcomes of
# the race between the uploads and the poll are lanes. The
# quick thread is 86fbf34e01's bug again under threaded
# video; the slow one must find the pack's pixels alive.
make clean all THREADS=1 SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
timeout 60 ./overlay_textures_test
make clean
echo "[pass] overlay_textures_test (plain, ASan, threaded ASan)"
- name: Build and run winraw_abs_pos_test (mingw + Wine)
shell: bash
working-directory: samples/input/winraw_abs_pos
run: |
set -eu
# input/drivers/winraw_input.c, cross-built with mingw-w64 and
# run under Wine: an absolute pointer report's position is
# kept as one word, and every read must give back a position
# a report carried, including a report that lands during poll.
# mingw-w64 and Wine come from "Install dependencies", whose
# download is bounded and retried; an unbounded install here
# stalled silently until the job timeout. Stop wineserver on
# exit so it cannot hold the step's output open.
trap 'wineserver -k >/dev/null 2>&1 || true' EXIT
make clean all
WINEDEBUG=-all timeout 300 make check
make clean
echo "[pass] winraw_abs_pos_test (wine)"
+239 -3
View File
@@ -27,9 +27,9 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libmbedtls-dev"
pkgs="build-essential libmbedtls-dev libbearssl-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -92,6 +109,136 @@ jobs:
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./webdav_options_test
echo "[pass] webdav_options_test (TSan)"
- name: Build and run webdav_reauth_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/network/webdav_reauth
run: |
set -eu
# Includes network/cloud_sync/webdav.c and plays the server
# for every request type the driver sends (OPTIONS, GET, PUT,
# DELETE, MOVE, MKCOL). A server that keeps answering 401 with
# a Digest challenge - a wrong password - must get one retry
# and then a reported failure; the driver used to retry every
# challenge, so a mistyped password looped the first request
# of a sync forever. A single stale-nonce challenge must still
# be retried and succeed. A retried PUT must send the file:
# it used to re-read it from the end the first attempt left
# it at and upload uninitialised memory in place of the save.
# A malformed challenge (a value with no closing quote, a
# trailing unquoted parameter) must be refused, not crash the
# parser or send it reading past the end of the line.
make clean all SANITIZER=address,undefined
test -x webdav_reauth_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./webdav_reauth_test
echo "[pass] webdav_reauth_test (ASan)"
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./webdav_reauth_test
echo "[pass] webdav_reauth_test (TSan)"
- name: Build and run webdav_backup_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/network/webdav_backup
run: |
set -eu
# Includes network/cloud_sync/webdav.c and plays the server,
# request by request. With destructive sync off, an upload
# must COPY the server's file to deleted/<path>-<time> before
# the PUT replaces it - it used to overwrite it in place, so
# the setting protected against deletes but not overwrites.
# Covers the order (backup first, into an existing deleted/
# collection, named as a delete names it), 404 as "nothing to
# back up", a failed backup stopping the upload, destructive
# sync and the server manifest skipping the backup, and COPY
# and PUT each getting their own Digest retry.
make clean all SANITIZER=address,undefined
test -x webdav_backup_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./webdav_backup_test
echo "[pass] webdav_backup_test (ASan)"
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./webdav_backup_test
echo "[pass] webdav_backup_test (TSan)"
- name: Build and run s3_backup_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/network/s3_backup
run: |
set -eu
# Includes network/cloud_sync/s3.c and plays the server,
# request by request. The S3 driver never read the
# destructive sync setting: deletes removed objects for good
# and uploads overwrote them. With it off, both must first
# CopyObject the object to deleted/<path>-<time>. Covers the
# copy source for virtual-hosted and path-style endpoints,
# x-amz-copy-source being signed, 404 as "nothing to keep", a
# failed copy (including a 200 with an <Error> body) stopping
# the delete or upload, and destructive sync and the server
# manifest skipping the copy.
make clean all SANITIZER=address,undefined
test -x s3_backup_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./s3_backup_test
echo "[pass] s3_backup_test (ASan)"
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./s3_backup_test
echo "[pass] s3_backup_test (TSan)"
- name: Build and run smb_backup_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/network/smb_backup
run: |
set -eu
# Includes network/cloud_sync/smb.c and runs it against an
# in-memory libsmb2 share (fake_smb.c), checking what the
# share holds after each upload. The driver used to truncate
# the server file and then write, so a write failing part way
# left a truncated save; and with destructive sync off it
# overwrote the file without keeping the old copy a delete
# keeps. Uploads now go through <path>.rauploading and the
# replaced file is kept as <path>-<time> (non-destructive).
# Covers a failed write, backup and final rename each leaving
# the old file intact with no temporary file left, no backup
# for destructive sync, a new file or the manifest, an empty
# upload and the configured subdirectory.
make clean all SANITIZER=address,undefined
test -x smb_backup_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./smb_backup_test
echo "[pass] smb_backup_test (ASan)"
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./smb_backup_test
echo "[pass] smb_backup_test (TSan)"
- name: Build and run gdrive_backup_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/network/gdrive_backup
run: |
set -eu
# Includes network/cloud_sync/google_drive.c and plays the
# Drive API, request by request. With destructive sync off, a
# delete kept the old file (renamed <name>-<time>) but an
# upload replaced its content outright. An upload to an
# existing file must now files.copy it to that name in the
# same folder before the PATCH. Covers the order and the copy
# request, a failed copy stopping the upload, a 401 on the
# copy and on the PATCH each refreshing the token once (the
# PATCH retry repeating only the PATCH, not the backup), and
# destructive sync, a new file and the manifest skipping it.
make clean all SANITIZER=address,undefined
test -x gdrive_backup_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1 timeout 60 \
./gdrive_backup_test
echo "[pass] gdrive_backup_test (ASan)"
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 120 ./gdrive_backup_test
echo "[pass] gdrive_backup_test (TSan)"
- name: Build and run cheevos_login_test (bear ASan+UBSan, bear TSan, mbed ASan+UBSan)
shell: bash
working-directory: samples/network/cheevos_login
@@ -106,7 +253,12 @@ jobs:
# from a realistic messy CA bundle (BearSSL), keep-alive pool
# reuse, one-shot replay of a request on a pooled connection
# the server closed while idle, and no replay once response
# bytes have been seen.
# bytes have been seen. Each TLS backend's non-blocking send
# must push a 12 MiB body past a server that stops reading for
# a second without ever blocking, and deliver every byte; and
# net_http must send the same upload a slice per update, plain
# and over TLS - it used to go out in one blocking send, a call
# of a second or more (spinning at 100% CPU on a plain socket).
make clean all SANITIZER=address,undefined
test -x cheevos_login_test
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
@@ -121,3 +273,87 @@ jobs:
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 \
make check SSL_BACKEND=mbed
echo "[pass] cheevos_login_test (mbedTLS, ASan)"
# The built-in client, RetroArch's default backend.
make clean all SSL_BACKEND=retro SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 \
make check SSL_BACKEND=retro
echo "[pass] cheevos_login_test (built-in TLS, ASan)"
# The runner's libmbedtls-dev is 2.28, which has no TLS 1.3,
# so the leg above cannot see either TLS 1.3 failure. Mbed
# TLS 3.6.0 is the one release that shows both: it fails
# every handshake unless psa_crypto_init() was called, and it
# reports session tickets from mbedtls_ssl_read(). Library
# only, ~20 s to build. A download that still fails after
# the retries skips the leg rather than failing the job.
mbed_ver=3.6.0
mbed_sha=3ecf94fcfdaacafb757786a01b7538a61750ebd85c4b024f56ff8ba1490fcd38
mbed_tar="$RUNNER_TEMP/mbedtls-$mbed_ver.tar.bz2"
if curl -fsSL --retry 3 --retry-delay 5 --max-time 120 -o "$mbed_tar" \
"https://github.com/Mbed-TLS/mbedtls/releases/download/v$mbed_ver/mbedtls-$mbed_ver.tar.bz2"; then
echo "$mbed_sha $mbed_tar" | sha256sum -c -
tar -xjf "$mbed_tar" -C "$RUNNER_TEMP"
make -C "$RUNNER_TEMP/mbedtls-$mbed_ver" -j"$(nproc)" lib >/dev/null
make clean all SSL_BACKEND=mbed SANITIZER=address,undefined \
MBEDTLS_DIR="$RUNNER_TEMP/mbedtls-$mbed_ver"
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 \
make check SSL_BACKEND=mbed
echo "[pass] cheevos_login_test (mbedTLS $mbed_ver TLS 1.3, ASan)"
else
echo "::warning::Mbed TLS $mbed_ver download failed; TLS 1.3 leg skipped"
fi
- name: Build and run net_http_semantics_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/network/net_http_semantics
run: |
set -eu
# HTTP/1.1 semantics of libretro-common/net/net_http.c against
# an in-process scripted server on 127.0.0.1, which logs which
# connection every request arrived on. Covers GET 200 framing
# and pool reuse, and responses without a body: HEAD, 204 and
# 304 on keep-alive used to sit waiting for a close that never
# came, a 1xx other than 100 was taken as the final status,
# and a 101 or stray bytes after a bodyless response left the
# socket in the pool. Connection and Transfer-Encoding are
# read as lists ("Keep-Alive, Close", "gzip, chunked"),
# HTTP/1.0 without keep-alive is not pooled, and a length
# after "chunked" does not reframe the body. Redirects: an
# absolute-path Location used to go out as "GET //foo", a
# 302 loop was followed forever (now 8 hops), a streamed PUT
# redirected by a 307 is rewound and resent whole, and a 3xx
# body no longer lands in the caller's sink. Bounds: a header
# block (1xx blocks included) over 64 KiB and a chunk size
# with no hex digits fail instead of growing the buffer or
# passing as the last chunk, a Content-Length buffer never
# grows past the body, and a pooled connection that dies
# after the status line is not replayed. A URL shorter than
# its scheme is rejected without an overread. Unit matrix for
# net_http_url_join() and net_http_urldecode(), including
# buffer bounds and in-place decoding; redirects resolved
# through it ("../", "?q", "//host", fragments dropped,
# %20 left encoded). The response header block: lookup and
# iteration, the legacy string_list accessor, and 64 header
# lines costing no per-line allocations. A Content-Length or
# "chunked" on a 1xx block does not frame the final response.
# A followed 3xx's body is dropped as it arrives, not
# buffered to its Content-Length, and bytes past a body's
# Content-Length end the connection, not the transfer.
# Every case has a
# deadline, so a hang fails the case instead of the job.
# net_http is lock-free by design (DNS cache and pool owned by
# the driving thread, resolvers publish with an atomic store);
# keep it that way.
if grep -nE '\b(slock|scond)_[a-z_]+\(' ../../../libretro-common/net/net_http.c; then
echo "::error title=net_http lock::net_http.c must not take locks"
exit 1
fi
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 120 \
./net_http_semantics_test
echo "[pass] net_http_semantics_test (ASan)"
make clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./net_http_semantics_test
echo "[pass] net_http_semantics_test (TSan)"
+18 -1
View File
@@ -41,7 +41,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential pkg-config libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -54,6 +54,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+18 -1
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+203 -23
View File
@@ -27,7 +27,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential zlib1g-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -40,6 +40,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -117,6 +134,38 @@ jobs:
timeout 60 ./archive_name_safety_test
echo "[pass] archive_name_safety_test"
- name: Build and run cloudsync_path_safety_test
shell: bash
working-directory: samples/tasks/cloudsync
run: |
set -eu
# Regression test for the manifest-key check in
# tasks/task_cloudsync.c::task_cloud_sync_fetch_server_file.
# A hostile sync server's manifest key could escape the
# cloud-sync directory via ".." or an absolute component, or
# crash the fetch with a key that has no '/'. The test
# #includes the real predicate (tasks/task_cloudsync_path.c).
make clean all
test -x cloudsync_path_safety_test
timeout 60 ./cloudsync_path_safety_test
echo "[pass] cloudsync_path_safety_test"
- name: Build and run natt_desc_parse_test
shell: bash
working-directory: samples/tasks/natt_desc
run: |
set -eu
# Regression test for the UPnP IGD description parser
# (network/natt_desc.c::natt_parse_desc_node). A router's
# description XML is untrusted; the old parser crashed on a
# leaf or empty root node and never reached a nested
# <service>. The test #includes the real parser and feeds it
# hand-built hostile trees.
make clean all
test -x natt_desc_parse_test
timeout 60 ./natt_desc_parse_test
echo "[pass] natt_desc_parse_test"
- name: Build and run http_method_match_test (ASan)
shell: bash
working-directory: samples/tasks/http
@@ -230,6 +279,32 @@ jobs:
timeout 60 ./bsv_replay_bounds_test
echo "[pass] bsv_replay_bounds_test"
- name: Build and run bsv_replay_init_test (plain, ASan + UBSan)
shell: bash
working-directory: samples/tasks/bsv_replay_init
run: |
set -eu
# Regression oracle for replay start-up and shutdown in
# tasks/task_movie.c and input/bsv/bsvmovie.c (#19622).
# Links the shipping units against a stub core and the real
# task queue / intfstream / VFS, in the iOS configuration
# (RZSTD on, STATESTREAM off). Pre-fix a replay truncated
# inside its first checkpoint, or halted before its first
# frame, left BSV_FLAG_MOVIE_END set with no handle: the
# runloop then paused after every frame, for every content,
# until the process was restarted. Also covers halting a
# recording before its first frame leaving the session stuck
# "recording". If runloop.c changes its per-frame replay
# protocol, frame() in the test must follow.
make clean all
test -x bsv_replay_init_test
timeout 120 ./bsv_replay_init_test
echo "[pass] bsv_replay_init_test"
make clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 \
timeout 120 ./bsv_replay_init_test
echo "[pass] bsv_replay_init_test (ASan + UBSan)"
- name: Build and run bps_patch_bounds_test (ASan)
shell: bash
working-directory: samples/tasks/bps_patch
@@ -727,16 +802,14 @@ jobs:
working-directory: samples/tasks/overlay
run: |
set -eu
# The overlay visibility index does not come from the overlay:
# led/drivers/led_overlay.c forwards settings->uints.led_map[],
# which configuration.c reads from the config file with no
# range of its own. Unbounded, "led1_map = 500" wrote past
# overlay_visibility[MAX_VISIBILITY] during drivers_init() and
# handed the same index to the video driver's set_alpha().
# This builds the shipping LED driver to pin that it really
# does forward config values unaltered, and checks the bound
# covers the array exactly. If input_driver.c changes that
# bound, the verbatim copy in the test must follow.
# Builds the shipping input/input_overlay_alpha.c and
# led/drivers/led_overlay.c against a stub driver. Which images
# the overlay LED driver hides: a pack's _led images, else the
# image at a ledN_map slot whatever its desc does, and never an
# index off the page - led_map comes from the config with no
# range of its own. And the per-poll alpha pass: an image is set only when
# its alpha changes, a page load sets them all again, and an
# LED going out and back on between polls is shown again.
make -f Makefile.visibility clean >/dev/null
make -f Makefile.visibility all SANITIZER=address,undefined -j$(nproc)
ASAN_OPTIONS=detect_leaks=1 \
@@ -956,22 +1029,39 @@ jobs:
# by cancellation plus a stale-completion generation guard
# (menu_explore_cancel_init_task / menu_dbinfo_cancel_task).
#
# Scope is deliberately narrow and mechanical: menu/ and the
# two task translation units the menu drives directly. The
# remaining wait sites elsewhere (core deinit save/load, NAT
# traversal, netplay MITM) are tracked separately and are
# NOT covered here - widening this guard is what should
# happen as each of those is converted.
hits=$(grep -rn 'task_queue_wait' menu/ \
# Scope is mechanical: menu/, the task translation units the
# menu drives directly, and the content load - a chain of
# main-thread tasks that closes content from the frame loop.
# The remaining wait sites (NAT traversal, the CLI database
# scan, the exit path) are tracked separately; widening this
# guard is what should happen as each of those is converted.
hits=$(grep -rn 'task_queue_wait' menu/ network/ command.c \
tasks/task_menu_explore.c \
tasks/task_database_info.c \
tasks/task_netplay_nat_traversal.c || true)
tasks/task_netplay_nat_traversal.c \
tasks/task_content.c || true)
if [[ -n "$hits" ]]; then
printf '%s\n' "$hits"
printf '::error title=Blocking wait on a menu path::task_queue_wait() must not be reachable from the menu. Use cancellation with a stale-completion guard, or a deferred continuation.\n'
exit 1
fi
printf 'No task_queue_wait on the guarded menu paths.\n'
# The close's blocking helpers wait inside task_save.c, where
# the grep above cannot see them: the menu and the tasks must
# not call them. The load closes through
# retroarch_main_deinit_begin/pending/finish instead.
hits=$(grep -rn 'content_wait_for_\(save\|load\)_state_task' \
menu/ tasks/ --include='*.c' \
| grep -v '^tasks/task_save.c:' || true)
if [[ -n "$hits" ]]; then
printf '%s\n' "$hits"
printf '::error title=Blocking close on a menu path::content_wait_for_*_state_task() blocks the frame loop. Close through retroarch_main_deinit_begin/pending/finish from a main-thread task.\n'
exit 1
fi
grep -q 'retroarch_main_deinit_pending' tasks/task_content.c || {
printf '::error title=Blocking close on a menu path::the content load no longer waits for state tasks from the frame loop.\n'
exit 1
}
printf 'No task_queue_wait or blocking close on the guarded menu paths.\n'
- name: Build and run playlist_parity_test (plain, ASan + UBSan)
shell: bash
@@ -1232,7 +1322,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential zlib1g-dev"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -1245,6 +1335,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -1364,8 +1471,9 @@ jobs:
# sits between two representations: RetroArch's raw
# CRLF-delimited blob and the NULL-terminated key/value array
# emscripten_fetch wants, plus the response blob that has to
# become the same "Name: Value" string_list net_http.c
# produces. Neither conversion existed while the backend was
# become the same "Name: Value" header block net_http.c
# produces - through net_http_headers_compact(), which the test
# links rather than copies. Neither conversion existed while the backend was
# on emscripten_async_wget2_data, which has no header support
# at all - the `headers` argument was accepted and dropped on
# every entry point, so cloud sync could not authenticate.
@@ -1540,6 +1648,78 @@ jobs:
make -f Makefile.transport_fail clean
echo "[pass] http_transport_failure_test (TSan)"
- name: Build and run http_resume_test (ASan + UBSan, TSan)
shell: bash
working-directory: samples/tasks/http
run: |
set -eu
# An interrupted download to a file (task_push_http_download_file,
# as the core updater and thumbnails use) resumes with
# "Range: bytes=N-" and If-Range on the first response's strong
# ETag. A 206 from the right offset continues the file; a 200
# (resource changed) starts it over; a 206 from the wrong offset,
# a first response without Accept-Ranges or a validator, and a
# connection that keeps dropping (more than three resumes) fail
# and remove the partial file. Every interrupted download used
# to fail outright.
make -f Makefile.resume clean all SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 timeout 300 \
./http_resume_test
echo "[pass] http_resume_test (ASan)"
make -f Makefile.resume clean all SANITIZER=thread
TSAN_OPTIONS=halt_on_error=1 timeout 300 ./http_resume_test
make -f Makefile.resume clean
echo "[pass] http_resume_test (TSan)"
- name: Build and run HTTP initialization failure test
shell: bash
working-directory: samples/tasks/http
run: |
set -eu
for threads in 0 1; do
make -f Makefile.init_failure clean
make -f Makefile.init_failure THREADS=$threads
timeout 60 ./http_init_failure_test
done
make -f Makefile.init_failure clean
- name: Build and run http_limits_test (ASan + UBSan, threaded and not)
shell: bash
working-directory: samples/tasks/http
run: |
set -eu
# net_http.c included whole, with the clock and the socket
# calls stubbed. The DNS cache is capped (it grew by one entry
# per distinct host:port for the whole session), evicts least
# recently used first, and never evicts a lookup still in
# flight. The pool keeps at most 8 idle connections, closing
# the longest-idle first, and closes rather than reuses one
# idle for more than 30 s. On a system without IPv6, an IPv6
# address listed first no longer fails the transfer, and one
# mid-list no longer hides the addresses after it. Bracketed
# IPv6 literals ([::1]:8080) parse in URLs and redirects and
# reach the resolver without their brackets.
for threads in 1 0; do
make -f Makefile.limits clean
make -f Makefile.limits THREADS=$threads SANITIZER=address,undefined
ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 \
timeout 60 ./http_limits_test
done
make -f Makefile.limits clean
- name: Build and run HTTP DNS connect lifetime test
shell: bash
working-directory: samples/tasks/http
run: |
set -eu
for ssl in 0 1; do
make -f Makefile.dns_lifetime clean
make -f Makefile.dns_lifetime SSL=$ssl
timeout 60 ./http_dns_lifetime_test
done
make -f Makefile.dns_lifetime clean
- name: Build and run http_dns_evict_deadlock_test (ASan, TSan)
shell: bash
working-directory: samples/tasks/http
@@ -0,0 +1,117 @@
name: CI Linux samples [nothing else runs these]
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
samples-unclaimed:
name: Samples no other job runs
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Install dependencies
timeout-minutes: 20
run: |
pkgs="build-essential"
if dpkg -s $pkgs >/dev/null 2>&1; then
echo "All packages already present on runner image; skipping apt."
exit 0
fi
for i in 1 2 3; do
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 && break
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only -o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 $pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y -o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 $pkgs
- name: Checkout
uses: actions/checkout@v5
- name: Run them
shell: bash
run: |
set -eu
# A sample that builds and runs, and that no workflow names,
# is a test nobody runs: font_lifecycle sat that way while
# its expectation drifted away from the font driver it was
# written against. This job takes whatever is left over -
# the list is computed, so a new sample joins it by existing
# rather than by being remembered.
#
# Samples that need a library the runner does not carry
# (SDL, FFmpeg, libusb) are skipped: their own workflows
# install what they need. A sample cross-built with mingw-w64
# and run under Wine gets both installed here, the first time
# one turns up, so it runs rather than fails for want of a
# compiler.
fails=0
wine_ready=0
for mk in $(find samples libretro-common/samples -name Makefile | sort); do
dir=$(dirname "$mk")
name=$(basename "$dir")
(cd "$dir" && make -n check >/dev/null 2>&1) || continue
if grep -rqlF "$name" .github/workflows/ 2>/dev/null; then
continue
fi
if grep -qiE 'sdl|ffmpeg|libav|libusb' "$mk"; then
echo "-- skip $dir (needs a library this job does not install)"
continue
fi
if [ $wine_ready -eq 0 ] && grep -qE 'mingw32-|wine' "$mk"; then
sudo apt-get update -y -qq -o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 >/dev/null || true
sudo apt-get install -y -qq gcc-mingw-w64-x86-64 wine64 \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30
wine_ready=1
fi
echo "== $dir"
if (cd "$dir" && make clean >/dev/null 2>&1 \
&& ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 \
UBSAN_OPTIONS=print_stacktrace=1 \
timeout 300 make SANITIZER=address,undefined check); then
echo " ok"
else
echo " FAIL $dir"
fails=$((fails + 1))
fi
(cd "$dir" && make clean >/dev/null 2>&1) || true
done
if [ "$fails" -ne 0 ]; then
echo "$fails sample(s) failed"
exit 1
fi
+18 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libavcodec-dev libsdl2-dev libsdl-image1.2-dev libxml2-dev yasm"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -51,6 +51,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -49,7 +49,7 @@ jobs:
- name: Install dependencies
# Same set as Linux-asan-ubsan.yml. libtsan ships with the gcc
# on the runner image, so nothing sanitizer-specific is needed.
timeout-minutes: 5
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libavcodec-dev libsdl2-dev libsdl-image1.2-dev libxml2-dev yasm"
if dpkg -s $pkgs >/dev/null 2>&1; then
@@ -62,6 +62,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+168 -10
View File
@@ -46,6 +46,16 @@ name: CI Vulkan/GL/D3D11 [idle waits + driver harness]
# frontend does against the driver from the wrong thread is an X
# BadAccess here rather than a corruption elsewhere; the first run
# caught the OSD font rebuild doing exactly that.
#
# 6. Teardown-order check: tools/vulkan_teardown_order_check.py holds
# the order the driver retires a frame's objects in - command
# pools, then descriptor pools, then the pipelines that own the set
# layout - and the rule that destroying a descriptor pool is what
# frees its sets. The other order is legal Vulkan and the
# validation layer passes it, so the harness lanes above cannot see
# it; a driver that walks its own back references as a pool is
# destroyed faults inside vkDestroyDescriptorPool at exit, which is
# what MoltenVK did.
on:
push:
@@ -85,6 +95,27 @@ jobs:
set -eu
python3 tools/vulkan_idle_wait_check.py
vulkan-teardown-order:
name: Vulkan teardown order
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Checker self-test
shell: bash
run: |
set -eu
python3 tools/vulkan_teardown_order_check.py --selftest
- name: Tree check
shell: bash
run: |
set -eu
python3 tools/vulkan_teardown_order_check.py
vulkan-validation-harness:
name: Threaded-video harness on Vulkan (lavapipe, validation layer)
runs-on: ubuntu-latest
@@ -92,7 +123,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 8
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev libxcb1-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libxv-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libvulkan-dev mesa-vulkan-drivers vulkan-validationlayers vulkan-tools libasound2-dev"
for i in 1 2 3; do
@@ -101,6 +132,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -121,8 +169,14 @@ jobs:
# debug messenger that reports through RARCH_ERR. It is a
# tree-wide define: vulkan_context_t carries a member under it,
# so every TU that includes vulkan_common.h must see it.
# GFX_INSTRUMENT counts what the image and surface paths do -
# uploads, in-place updates, allocating posts, copies - so the
# harness lanes below can hold the budgets the paths are
# written to and print what a run cost. This is the one leg
# that carries it: under a sanitizer the timings mean nothing,
# and the counters change no behaviour.
run: |
make -j$(getconf _NPROCESSORS_ONLN) VULKAN_DEBUG=1
make -j$(getconf _NPROCESSORS_ONLN) VULKAN_DEBUG=1 GFX_INSTRUMENT=1
test -x retroarch
- name: Run the harness on Vulkan under the validation layer
@@ -142,8 +196,35 @@ jobs:
# lines, so it has to name the same build: VK_DEBUG=1. Its
# surface and overlay-page lanes run the driver's real
# in-place texture update and its borrowed-texture overlay
# page here, under the validation layer.
SAN=none VK_DEBUG=1 CYCLES=2 make -C samples/gfx/threaded_video check-vulkan
# page here, under the validation layer, and with the tree
# built GFX_INSTRUMENT=1 they print their baselines and hold
# the budgets: one texture per streaming surface, no
# allocation per post, no copy, a 4K submit that stays a
# descriptor hand-off.
SAN=none VK_DEBUG=1 GFX_INSTRUMENT=1 CYCLES=2 make -C samples/gfx/threaded_video check-vulkan
# Once more with the harness core pushing XRGB8888: the
# driver samples a linear 32-bit image itself where it
# converts RGB565 with a compute pass, so a lent frame and
# a window into it take the other upload path (the one
# beetle-psx takes).
HARNESS_CORE_XRGB8888=1 SAN=none VK_DEBUG=1 GFX_INSTRUMENT=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
# And with the harness core as a Vulkan hardware core, which
# runs its own lane alone: the core's image must not be drawn
# from once the core has destroyed it - at a content close,
# unthreaded and under the wrapper, and after a set_image(NULL)
# - and a dupe of a hardware frame must not re-submit a fence
# that is already signalled. Either is a validation error
# here, and on lavapipe a crash.
HARNESS_CORE_HW_VULKAN=1 SAN=none VK_DEBUG=1 GFX_INSTRUMENT=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
make -C samples/gfx/threaded_video clean
# And as a core that creates the device itself, through the
# negotiation interface's create_device - Beetle PSX, for one.
# That device has the extensions the frontend requires and
# none of the optional ones it adds to a device of its own, so
# anything the driver or the filter chain does that leans on
# an optional extension is a validation error here and nowhere
# else. The filter chain's mirror-clamp samplers were.
HARNESS_CORE_HW_VULKAN=2 SAN=none VK_DEBUG=1 GFX_INSTRUMENT=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
make -C samples/gfx/threaded_video clean
vulkan-validation-sample:
@@ -153,7 +234,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 8
timeout-minutes: 20
run: |
pkgs="build-essential libx11-dev libx11-xcb-dev libxcb1-dev xvfb x11-utils libvulkan-dev mesa-vulkan-drivers vulkan-validationlayers vulkan-tools"
for i in 1 2 3; do
@@ -162,6 +243,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -200,7 +298,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 8
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev libxcb1-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libxv-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libvulkan-dev mesa-vulkan-drivers vulkan-validationlayers vulkan-tools libasound2-dev"
for i in 1 2 3; do
@@ -209,6 +307,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -240,6 +355,7 @@ jobs:
export DISPLAY=:99
export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json
SAN=address VK_DEBUG=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
HARNESS_CORE_XRGB8888=1 SAN=address VK_DEBUG=1 CYCLES=1 make -C samples/gfx/threaded_video check-vulkan
make -C samples/gfx/threaded_video clean
vulkan-tsan-harness:
@@ -249,7 +365,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 8
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev libxcb1-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libxv-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libvulkan-dev mesa-vulkan-drivers vulkan-validationlayers vulkan-tools libasound2-dev"
for i in 1 2 3; do
@@ -258,6 +374,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -303,7 +436,7 @@ jobs:
steps:
- name: Install dependencies
timeout-minutes: 8
timeout-minutes: 20
run: |
pkgs="build-essential libxkbcommon-dev libx11-xcb-dev libxcb1-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libxv-dev xvfb x11-utils zlib1g-dev libfreetype6-dev libgl-dev libglx-dev libegl-dev libgles-dev libgl1-mesa-dri libglx-mesa0 mesa-utils libasound2-dev"
for i in 1 2 3; do
@@ -312,6 +445,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
@@ -362,8 +512,15 @@ jobs:
# d3d12 runs the same lanes on the other explicit-API backend:
# its in-place texture update is fence-tagged against the frame
# that last copied from the upload buffer, which only a running
# driver exercises.
# driver exercises. It runs with the harness core pushing
# XRGB8888: d3d12 lends its framebuffer only for a row pitch
# on a 256-byte boundary, which 320 pixels reach at 4 bytes and
# not at 2, so that is the run in which a frame is lent and a
# window into the loan is read out of it (lent-window lane).
driver: [d3d11, d3d12]
include:
- driver: d3d12
core_xrgb8888: 1
steps:
- name: Checkout
@@ -395,5 +552,6 @@ jobs:
run: |
set -eu
samples/gfx/threaded_video/build.sh
SAN=none DRIVER=${{ matrix.driver }} CYCLES=1 GDB=1 make -C samples/gfx/threaded_video check-driver
HARNESS_CORE_XRGB8888=${{ matrix.core_xrgb8888 || '' }} \
SAN=none DRIVER=${{ matrix.driver }} CYCLES=1 GDB=1 make -C samples/gfx/threaded_video check-driver
make -C samples/gfx/threaded_video clean
+6
View File
@@ -33,6 +33,12 @@ jobs:
run: |
./configure --disable-qt --enable-xdelta
- name: Include order
run: |
# The tree's own headers are found before same-named ones in the
# system include dirs configure or pkg-config put on the line.
sh tools/include_order_check.sh
- name: Compile RA
run: |
make -j$(getconf _NPROCESSORS_ONLN) clean
+79
View File
@@ -32,6 +32,58 @@ jobs:
# companion, the grid, the tables, the dialogs - as AppKit does them.
tools/companion_cocoa_test_macos.sh 2>&1 | grep -v "^$"
- name: Metal texture behaviour the surface, overlay and lent-framebuffer paths assume
run: |
set -o pipefail
# The driver's Metal paths have only ever been compiled. This
# asserts the two behaviours they are built on, on the runner's
# real device: a texture replaced in place while GPU work that
# samples it is in flight - the animated preview's steady
# state, and the one path with no fence check or DO_NOT_WAIT to
# drop a frame with - and an overlay page holding textures the
# pack owns, where a page switch must not take them with it.
#
# 77 means the runner exposed no Metal device; the sample says
# so rather than passing quietly.
cd samples/gfx/metal_texture_update
make check; rc=$?
if [ "$rc" = 77 ]; then
echo "::warning::no Metal device on this runner; the check did not run"
exit 0
fi
exit "$rc"
- name: iCloud Drive cloud sync driver
run: |
set -o pipefail
# Includes network/cloud_sync/icloud_drive.m, with the iCloud
# container redirected to a temporary directory. Cloud sync
# waits for every request to report exactly once: a read whose
# local file could not be created never reported (the sync
# hung), and a cloud file that could not be loaded or a local
# file that could not be read reported twice. With iCloud
# unavailable, read and delete reported success. Also covers
# the ordinary read, missing file, upload and delete paths.
cd samples/network/icloud_drive
make clean check SANITIZER=address,undefined
- name: iCloud (CloudKit) cloud sync driver
run: |
set -o pipefail
# Includes network/cloud_sync/icloud.m, with CloudKit replaced by
# an in-memory database (+[CKContainer defaultContainer] is
# overridden in a category; real CKRecord/CKAsset/CKQuery). A
# read whose local file could not be written, or whose record
# held no readable data, reported success; an upload whose
# lookup failed created a duplicate record; a delete reported
# success before its deletes were sent, whatever became of
# them; and non-destructive sync was ignored. Checks each case,
# that deletes and uploads keep <path>-<time> with destructive
# sync off (and not for destructive sync or the manifest), and
# that every request reports exactly once.
cd samples/network/icloud_cloudkit
make clean check SANITIZER=address,undefined
- name: Compile RA
run: |
set -o pipefail
@@ -46,3 +98,30 @@ jobs:
name: RetroArch-${{ steps.slug.outputs.sha8 }}
path: |
build/Build/Products/Release
# The threaded-video harness on the Metal driver: the real frontend
# under Cocoa's own main (see samples/gfx/threaded_video/harness_cocoa.m),
# every lane the Vulkan and D3D legs run, on the runner's GPU. A
# Makefile build, since the harness links the tree's objects.
metal-harness:
runs-on: macos-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Configure and build (Makefile, no Qt)
run: |
set -o pipefail
./configure --disable-qt --disable-ffmpeg --disable-discord --disable-cheevos \
--disable-networking --disable-sdl2 --disable-sdl --disable-translate \
--disable-update_cores --disable-update_assets --disable-online_updater \
--disable-cdrom --disable-7zip --disable-ssl 2>&1 | tail -5
make -j$(sysctl -n hw.ncpu) GFX_INSTRUMENT=1 2>&1 | grep -E "error|warning: unused" || true
test -x retroarch
- name: Harness on Metal (RGB565 core, then XRGB8888)
run: |
set -o pipefail
SAN=none GFX_INSTRUMENT=1 DRIVER=metal CYCLES=1 make -C samples/gfx/threaded_video check-driver
HARNESS_CORE_XRGB8888=1 SAN=none GFX_INSTRUMENT=1 DRIVER=metal CYCLES=1 make -C samples/gfx/threaded_video check-driver
+15 -16
View File
@@ -21,23 +21,27 @@ jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: reallibretroretroarch/libretro-build-orbis:latest
options: --user root
# The image is Alpine 3.15, whose musl cannot load the runner's
# Node 24, so JavaScript actions run on the host and only the
# build steps run inside the image.
env:
ORBIS_IMAGE: reallibretroretroarch/libretro-build-orbis:latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
apk add ncurses-dev make bash python2
apk add libintl icu-dev wget
wget https://dot.net/v1/dotnet-install.sh && chmod 755 dotnet-install.sh && ./dotnet-install.sh -c 3.0 --install-dir ~/cli
- name: Compile RA
run: |
export PATH=~/cli:$PATH # .net cli
make -f Makefile.orbis -j$(getconf _NPROCESSORS_ONLN) HAVE_STATIC_DUMMY=1 info all
docker run --rm --user root -v "$PWD":/src -w /src "$ORBIS_IMAGE" bash -c '
set -e
apk add ncurses-dev make bash python2
apk add libintl icu-dev wget
wget https://dot.net/v1/dotnet-install.sh && chmod 755 dotnet-install.sh && ./dotnet-install.sh -c 3.0 --install-dir ~/cli
export PATH=~/cli:$PATH # .net cli
make -f Makefile.orbis -j$(getconf _NPROCESSORS_ONLN) HAVE_STATIC_DUMMY=1 info all
clang --version
orbis-ld --version
'
- name: Get short SHA
id: slug
@@ -50,8 +54,3 @@ jobs:
retroarch_orbis.elf
retroarch_orbis.oelf
retroarch_orbis.self
- name: Version of binaries
run: |
clang --version
orbis-ld --version
+108
View File
@@ -0,0 +1,108 @@
name: CI removed struct members
# When a struct member goes away, the compiler finds every reader it
# compiles. The readers it does not compile are the ones no Linux box
# sees - the Objective-C drivers above all, and anything behind a
# platform #ifdef - so each of those is found by whichever job builds
# that platform, one round trip at a time.
#
# window_position_x and window_position_y went that way. Packing the
# pair into one word updated the Win32 and SDL3 users, which is what a
# grep of the .c and .h files turns up; ui_cocoa.m kept reading both
# and took out the macOS job and both PowerPC cross jobs at once. Those
# three are among the slowest jobs in the tree, and the answer they
# eventually gave was four lines in one file.
#
# tools/packed_field_check.py does not cover this. It knows the six
# names a size or origin pair is spelled with, and window_position_x is
# not one of them; on the broken tree it printed 1184 lines, none of
# them ui_cocoa.m. The question here is not what a member is called,
# it is whether anything still reaches for one the change took away.
#
# So this lane reads the change rather than the tree's vocabulary: the
# members a push or a pull request removes, minus the ones something
# still declares, against every reader in every language the tree is
# written in. It reads files rather than building them, so one job
# covers what the platform jobs between them would only find one at a
# time.
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
removed-member-check:
name: readers of a removed member
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v5
with:
# Both ends of the range have to be present, and a pull
# request's base can be behind by any amount.
fetch-depth: 0
- name: Checker self-test
shell: bash
run: |
set -eu
# Fixture pass: a member removed along with its readers is
# clean, one left behind in a language no lane here compiles
# is caught, and a member that merely moved, one whose name
# another struct still declares, one that was renamed, one
# named only in a comment or a log string, one declared in a
# declarator list, and a name too common to reason about are
# each left alone.
#
# The fixtures are handed to the checker directly rather than
# taken from the tree, so this reports the same thing whether
# or not the tree currently passes - it measures the checker.
python3 tools/removed_member_check.py --selftest
- name: Range check
shell: bash
run: |
set -eu
# A pull request is measured against its base; a push against
# what was there before it. A force push or a first push
# leaves 'before' as zeroes, and a merge has more than one
# parent, so fall back to the first parent in either case.
if [ -n "${PR_BASE:-}" ]; then
base="$PR_BASE"; head="$PR_HEAD"
else
base="${EVENT_BEFORE:-}"; head="$GITHUB_SHA"
case "$base" in
''|*[!0]*) ;;
*) base= ;;
esac
if [ -z "$base" ] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base="$(git rev-parse "$head^" 2>/dev/null || true)"
fi
fi
if [ -z "$base" ]; then
echo "no base commit to compare against; nothing to check"
exit 0
fi
echo "range: $base..$head"
# Every struct member the range removes, minus the ones the
# tree still declares somewhere, against every .c .h .m .mm
# .cpp .hpp and .inc reader of one.
python3 tools/removed_member_check.py --range "$base..$head"
env:
PR_BASE: ${{ github.event.pull_request.base.sha }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
EVENT_BEFORE: ${{ github.event.before }}
+118 -4
View File
@@ -6,6 +6,18 @@ on:
pull_request:
repository_dispatch:
types: [run_build]
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: Existing release tag to build (empty builds the selected ref)
type: string
default: ''
publish_release:
description: Upload the ARM64 package to the existing release
type: boolean
default: false
permissions:
contents: read
@@ -14,13 +26,15 @@ env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.release_tag || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
msvc:
runs-on: windows-11-arm
timeout-minutes: 90
outputs:
release_tag: ${{ steps.source.outputs.release_tag }}
strategy:
matrix:
version: [2022]
@@ -29,6 +43,29 @@ jobs:
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.release.tag_name || inputs.release_tag || github.ref }}
- name: Record and verify the source revision
id: source
shell: pwsh
env:
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
PUBLISH_RELEASE: ${{ github.event_name == 'release' || inputs.publish_release == true }}
run: |
$revision = git rev-parse HEAD
if ($LASTEXITCODE) { throw 'Could not resolve the source revision.' }
"revision=$revision" >> $env:GITHUB_OUTPUT
if ($env:PUBLISH_RELEASE -eq 'true' -and !$env:RELEASE_TAG) {
throw 'Publishing requires an existing release tag.'
}
if ($env:RELEASE_TAG) {
$tagRevision = git rev-parse --verify "refs/tags/$($env:RELEASE_TAG)^{commit}"
if ($LASTEXITCODE -or $tagRevision -ne $revision) {
throw 'The checkout is not the requested release tag.'
}
}
"release_tag=$env:RELEASE_TAG" >> $env:GITHUB_OUTPUT
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1
@@ -40,11 +77,88 @@ jobs:
- name: Get short SHA
id: slug
shell: powershell
run: echo "sha8=$('${{ github.sha }}'.Substring(0,8))" >> $env:GITHUB_OUTPUT
shell: pwsh
env:
SOURCE_REVISION: ${{ steps.source.outputs.revision }}
run: echo "sha8=$($env:SOURCE_REVISION.Substring(0,8))" >> $env:GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: retroarch-${{ matrix.version }}-${{ matrix.configuration }}-${{ matrix.platform }}-${{ steps.slug.outputs.sha8 }}
path: |
pkg/msvc/${{ matrix.platform }}/${{ matrix.configuration }}/RetroArch-msvc${{ matrix.version }}.exe
- name: Checkout packaging tools from the workflow revision
if: matrix.configuration == 'Release'
uses: actions/checkout@v5
with:
ref: ${{ github.workflow_sha }}
path: .arm64-packaging
sparse-checkout: pkg/msvc/packaging
- name: Checkout pinned Ozone assets
if: matrix.configuration == 'Release'
uses: actions/checkout@v5
with:
repository: libretro/retroarch-assets
ref: 73106363e14e34c08a5854b4cfbc29f184e3b783
path: .arm64-assets
sparse-checkout: |
ozone
pkg
fonts
- name: Checkout pinned core information
if: matrix.configuration == 'Release'
uses: actions/checkout@v5
with:
repository: libretro/libretro-super
ref: 0532bd63ce3072f50ca4f4ada40d42aef565bd3f
path: .arm64-info
sparse-checkout: dist/info
- name: Verify and package ARM64 frontend
if: matrix.configuration == 'Release'
shell: pwsh
env:
SOURCE_REVISION: ${{ steps.source.outputs.revision }}
run: |
./.arm64-packaging/pkg/msvc/packaging/package-arm64.ps1 `
-Executable ./pkg/msvc/ARM64/Release/RetroArch-msvc2022.exe `
-SourceDirectory . -AssetsDirectory ./.arm64-assets -InfoDirectory ./.arm64-info `
-OutputDirectory ./arm64-dist -SourceRevision $env:SOURCE_REVISION `
-Repository $env:GITHUB_REPOSITORY `
-AssetsRevision 73106363e14e34c08a5854b4cfbc29f184e3b783 `
-InfoRevision 0532bd63ce3072f50ca4f4ada40d42aef565bd3f
- name: Upload ARM64 release package
if: matrix.configuration == 'Release'
uses: actions/upload-artifact@v4
with:
name: retroarch-windows-arm64-package
path: |
arm64-dist/RetroArch-WinARM64.zip
arm64-dist/RetroArch-WinARM64.zip.sha256
if-no-files-found: error
upload-release:
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.publish_release == true)
needs: msvc
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: retroarch-windows-arm64-package
path: arm64-dist
- name: Attach ARM64 package to the existing release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.msvc.outputs.release_tag }}
run: |
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" > /dev/null
gh release upload "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --clobber \
arm64-dist/RetroArch-WinARM64.zip arm64-dist/RetroArch-WinARM64.zip.sha256
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: reallibretroretroarch/libretro-build-mxe-win32-cross:gcc10
image: reallibretroretroarch/libretro-build-mxe-win32-cross:gcc16
options: --user root
steps:
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: reallibretroretroarch/libretro-build-mxe-win64-cross:gcc10
image: reallibretroretroarch/libretro-build-mxe-win64-cross:gcc16
options: --user root
steps:
+141
View File
@@ -0,0 +1,141 @@
name: CI procbarrier tiers
# retro_procbarrier picks one of several ways to force a memory barrier
# on every running thread, and which one depends on the OS and the CPU.
# The Linux samples job only ever runs the one its runner picks
# (membarrier). This runs every tier each platform has, on a multi-core
# runner so the fence checks can actually catch a missing barrier, and
# fails if a forced tier quietly fell back to another one.
#
# Linux x86_64 membarrier, page-flip, signal
# Linux arm64 membarrier, signal
# Windows x64 FlushProcessWriteBuffers, page-flip
# Windows arm64 FlushProcessWriteBuffers
# macOS arm64 signal
# macOS x86_64 page-flip, signal
on:
push:
branches: [master]
paths:
- 'libretro-common/rthreads/**'
- 'libretro-common/include/rthreads/**'
- 'libretro-common/include/retro_atomic.h'
- 'libretro-common/samples/rthreads/retro_procbarrier_test/**'
- 'libretro-common/samples/rthreads/retro_asym_eventcount_test/**'
- '.github/workflows/procbarrier-tiers.yml'
pull_request:
paths:
- 'libretro-common/rthreads/**'
- 'libretro-common/include/rthreads/**'
- 'libretro-common/include/retro_atomic.h'
- 'libretro-common/samples/rthreads/retro_procbarrier_test/**'
- 'libretro-common/samples/rthreads/retro_asym_eventcount_test/**'
- '.github/workflows/procbarrier-tiers.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
posix:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: linux-x86_64
os: ubuntu-latest
libs: -lpthread -lrt
tiers: natural=membarrier membarrier=membarrier pageflip=page-flip signal=signal
- name: linux-arm64
os: ubuntu-24.04-arm
libs: -lpthread -lrt
tiers: natural=membarrier membarrier=membarrier signal=signal
- name: macos-arm64
os: macos-latest
libs: -lpthread
tiers: natural=signal signal=signal
- name: macos-x86_64
os: macos-15-intel
libs: -lpthread
tiers: natural=page-flip pageflip=page-flip signal=signal
steps:
- uses: actions/checkout@v5
- name: Build
working-directory: libretro-common
run: |
set -e
cc -std=gnu99 -O1 -g -Wall -DHAVE_THREADS -Iinclude \
samples/rthreads/retro_procbarrier_test/retro_procbarrier_test.c \
rthreads/retro_procbarrier.c rthreads/rthreads.c \
-o retro_procbarrier_test ${{ matrix.libs }}
cc -std=gnu99 -O1 -g -Wall -DHAVE_THREADS -Iinclude \
samples/rthreads/retro_asym_eventcount_test/retro_asym_eventcount_test.c \
rthreads/retro_asym_eventcount.c \
rthreads/retro_procbarrier.c rthreads/rthreads.c \
-o retro_asym_eventcount_test ${{ matrix.libs }}
- name: Run every tier
working-directory: libretro-common
env:
REQUIRE_SMP: 1
run: |
bash samples/rthreads/retro_procbarrier_test/run_tiers.sh \
./retro_procbarrier_test ./retro_asym_eventcount_test \
${{ matrix.tiers }}
windows:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: windows-x64
os: windows-2022
vcvars: vcvars64.bat
tiers: natural=FlushProcessWriteBuffers fpwb=FlushProcessWriteBuffers pageflip=page-flip
- name: windows-arm64
os: windows-11-arm
vcvars: vcvarsarm64.bat
tiers: natural=FlushProcessWriteBuffers fpwb=FlushProcessWriteBuffers
steps:
- uses: actions/checkout@v5
- name: Build
shell: cmd
working-directory: libretro-common
run: |
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath`) do set VSPATH=%%i
if not defined VSPATH exit /b 1
call "%VSPATH%\VC\Auxiliary\Build\${{ matrix.vcvars }}"
cl /nologo /W3 /O1 /DHAVE_THREADS /Iinclude ^
samples\rthreads\retro_procbarrier_test\retro_procbarrier_test.c ^
rthreads\retro_procbarrier.c rthreads\rthreads.c ^
/Fe:retro_procbarrier_test.exe
if errorlevel 1 exit /b 1
cl /nologo /W3 /O1 /DHAVE_THREADS /Iinclude ^
samples\rthreads\retro_asym_eventcount_test\retro_asym_eventcount_test.c ^
rthreads\retro_asym_eventcount.c ^
rthreads\retro_procbarrier.c rthreads\rthreads.c ^
/Fe:retro_asym_eventcount_test.exe
if errorlevel 1 exit /b 1
- name: Run every tier
shell: bash
working-directory: libretro-common
env:
REQUIRE_SMP: 1
run: |
bash samples/rthreads/retro_procbarrier_test/run_tiers.sh \
./retro_procbarrier_test.exe ./retro_asym_eventcount_test.exe \
${{ matrix.tiers }}
+18 -1
View File
@@ -26,7 +26,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Dependencies
timeout-minutes: 5
timeout-minutes: 20
run: |
# libvulkan-dev brings the Vulkan driver, the slang shader stack
# (shader_vulkan.c, slang_process.c, glslang_util.c, slang_cache.c)
@@ -43,6 +43,23 @@ jobs:
echo "apt-get update failed (attempt $i); retrying in 10s"
sleep 10
done
# Fetch first, under a bound: a download is safe to stop and
# resumes, an install is not. A mirror that stalls is left for
# the main archive on the next attempt.
for i in 1 2 3; do
sudo timeout 240 apt-get install -y --download-only \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs && break
[ $i = 3 ] && exit 1
echo "package download failed or stalled (attempt $i); retrying from archive.ubuntu.com"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*; do
[ -f "$f" ] || continue
sudo sed -i -e 's#mirror+file:/etc/apt/apt-mirrors.txt#http://archive.ubuntu.com/ubuntu/#g' \
-e 's#http://azure\.archive\.ubuntu\.com/ubuntu#http://archive.ubuntu.com/ubuntu#g' "$f"
done
sudo timeout 90 apt-get update -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 || true
done
sudo apt-get install -y \
-o Acquire::Retries=3 -o Acquire::http::Timeout=30 \
$pkgs
+681 -14
View File
@@ -19,10 +19,29 @@ cd "$(dirname "$0")/../../.."
CC=${CC:-gcc}
INC="-I. -Ilibretro-common/include -Ideps -Ideps/rcheevos/include -Iinput/include"
WARN="-Wall -Wno-unused-function -Werror=implicit-function-declaration"
# gcc 14 rejects these outright and older gcc only warns; a lane on
# an older compiler has to reject them too, or a mismatched callback
# passes here and fails on the toolchains that build the platform.
WARN="$WARN -Werror=incompatible-pointer-types -Werror=int-conversion"
BASE="-DRARCH_INTERNAL -DHAVE_AUDIOMIXER -DHAVE_THREADS -DHAVE_CONFIGFILE -DHAVE_MENU"
# A console lane that keeps the host identity takes the Linux branch
# of every #if ladder and proves nothing, so this sits above the
# lanes rather than among them.
HOSTOFF="-U__linux__ -U__gnu_linux__ -Ulinux -U__unix__ -U__unix -Uunix"
fail=0
# A failing compile's output, errors first: a file with a page of
# warnings ahead of its one error pushed the error past the cut, which
# left a failing lane reporting nothing to act on.
show_out() {
if echo "$1" | grep -q ": error: "; then
echo "$1" | grep ": error: " | sed 's/^/ /' | head -8
else
echo "$1" | sed 's/^/ /' | head -12
fi
}
check() {
name="$1"; shift
defs="$1"; shift
@@ -31,7 +50,7 @@ check() {
if ! out=$($CC $WARN $INC $BASE $defs -fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
echo "$out" | sed 's/^/ /' | head -12
show_out "$out"
fail=1; bad=1
fi
done
@@ -98,14 +117,16 @@ ARMWARN="$WARN -Wno-deprecated-declarations"
# a lane whose headers are absent says so and is not a failure.
platform_video() {
name="$1"; defs="$2"; extra="$3"; tu="$4"; probe="$5"
if [ -n "$probe" ] && [ ! -f "$probe" ]; then
echo "skip $name (no $probe)"
return
fi
for pr in $probe; do
if [ ! -f "$pr" ]; then
echo "skip $name (no $pr)"
return
fi
done
if ! out=$($CC $WARN $extra $INC $BASE $defs -fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
printf '%s\n' "$out" | sed 's/^/ /' | head -10
show_out "$out"
fail=1
else
echo "ok $name"
@@ -140,6 +161,118 @@ arm "ps3" ps3 "-D__PSL1GHT__ -DHAVE_MEMINFO"
arm "ps2" "" "-DPS2"
arm "emscripten" emscripten "-D__EMSCRIPTEN__"
# The consoles that carry the cleanroom crypto, keychain and TLS client
# (3DS, Vita, Switch), each in its own shape with the SDK stand-ins the
# random source and the socket layer need. Vita's and the 3DS's socket
# layers are the SDKs' own, so the TLS client itself is compiled only
# in the Switch shape here; those two builds are their workflows' to
# link.
CRYPTO_SET="libretro-common/crypto/crypto.c libretro-common/crypto/kdf.c libretro-common/crypto/pk.c libretro-common/crypto/x509.c libretro-common/file/keychain.c"
TLS_SET="$CRYPTO_SET libretro-common/net/net_socket_ssl_retro.c network/tls_log.c"
CONSOLE_NET="-DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_SSL -DHAVE_RETROSSL -DHAVE_NETWORKING -DHAVE_THREADS -DHAVE_CONFIGFILE -DRARCH_INTERNAL"
console_set() {
cs_name="$1"; cs_inc="$2"; cs_defs="$3"; shift 3
for cs_tu in "$@"; do
check "$cs_name: $(basename $cs_tu)" "$HOSTOFF $cs_inc $CONSOLE_NET $cs_defs $CDECL" "$cs_tu"
done
}
console_set "3ds crypto" "-Itools/platform_stubs/ctr" "-D_3DS -DARM11" $CRYPTO_SET
console_set "vita crypto" "-Itools/platform_stubs/vita" "-DVITA" $CRYPTO_SET
console_set "switch tls" "-I$STUBS/libnx" "-DHAVE_LIBNX -DSWITCH -D__SWITCH__" $TLS_SET
# The built-in NFS client, which the 3DS, Vita, Switch and Wii U
# build, under the defines their Makefiles set (RARCH_CONSOLE for the
# console sleep and the 3DS transfer cap, Vita's legacy socket layer):
# Wii U against its own wiiu/include, Vita against stubs of vitasdk's
# socket headers.
NFS_SET="libretro-common/net/net_nfs3.c libretro-common/vfs/vfs_implementation_nfs.c libretro-common/vfs/vfs_prefetch.c"
NFSD="-DHAVE_RETRONFS -DHAVE_NFSCLIENT"
console_set "3ds nfs" "-Itools/platform_stubs/ctr $NFSD" "-D_3DS -DARM11 -DRARCH_CONSOLE" $NFS_SET
console_set "vita nfs" "-Itools/platform_stubs/vita $NFSD" "-DVITA -DRARCH_CONSOLE -DHAVE_SOCKET_LEGACY" $NFS_SET
console_set "switch nfs" "-I$STUBS/libnx $NFSD" "-DHAVE_LIBNX -DSWITCH -D__SWITCH__ -DRARCH_CONSOLE" $NFS_SET
console_set "wiiu nfs" "-Iwiiu/include -Itools/platform_stubs/wiiu $NFSD" "-DWIIU -D__WUT__ -DHW_WUP -D__wiiu__ -DRARCH_CONSOLE" $NFS_SET
# The built-in SMB and Kerberos clients, which the 3DS, Vita and Switch
# build on their crypto (the Wii U has no kernel RNG for it to draw on).
SMB_SET="libretro-common/net/net_smb2.c libretro-common/net/net_krb5.c libretro-common/vfs/vfs_implementation_smb.c"
SMBD="-DHAVE_RETROSMB -DHAVE_SMBCLIENT"
console_set "3ds smb" "-Itools/platform_stubs/ctr $SMBD" "-D_3DS -DARM11 -DRARCH_CONSOLE" $SMB_SET
console_set "vita smb" "-Itools/platform_stubs/vita $SMBD" "-DVITA -DRARCH_CONSOLE -DHAVE_SOCKET_LEGACY" $SMB_SET
console_set "switch smb" "-I$STUBS/libnx $SMBD" "-DHAVE_LIBNX -DSWITCH -D__SWITCH__ -DRARCH_CONSOLE" $SMB_SET
# The network stack in the old MSVC projects (2005 - 2017), which no job
# here builds: 32-bit Windows, C89, at the Windows floors those projects
# target - VS2005's _WIN32_WINNT 0x0410 and XP's 0x0501. The stack keeps
# to what those compilers and SDKs have: C89, the LL/ULL suffixes code
# they already build uses, Windows entry points resolved at run time,
# and a CA bundle split into parts under MSVC's 64 KiB string limit.
MSVC_OLD_CC=${MSVC_OLD_CC:-i686-w64-mingw32-gcc}
if command -v "$MSVC_OLD_CC" > /dev/null 2>&1; then
MSVC_OLD_STACK="$CRYPTO_SET libretro-common/net/net_socket_ssl_retro.c network/tls_log.c libretro-common/net/net_smb2.c libretro-common/net/net_krb5.c libretro-common/net/net_nfs3.c libretro-common/vfs/vfs_implementation_smb.c libretro-common/vfs/vfs_implementation_nfs.c libretro-common/vfs/vfs_prefetch.c"
MSVC_OLD_DEFS="-DRARCH_INTERNAL -DHAVE_NETWORKING -DHAVE_THREADS -DHAVE_CONFIGFILE -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_SSL -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS -DLEGACY_WIN32_NO_MIGRATION_NOTE"
for winnt in 0x0410 0x0501; do
mo_bad=0
for tu in $MSVC_OLD_STACK; do
if ! out=$($MSVC_OLD_CC -std=c89 -ansi -pedantic -Werror=pedantic -Werror=declaration-after-statement -Wno-long-long -Wno-variadic-macros -Werror=implicit-function-declaration -Wall -Wno-unused-function -Wno-overlength-strings \
-I. -Ilibretro-common/include -D_WIN32_WINNT=$winnt $MSVC_OLD_DEFS -fsyntax-only "$tu" 2>&1) \
|| echo "$out" | grep -q "warning:"; then
echo "FAIL msvc-old network stack, _WIN32_WINNT=$winnt"
echo " $tu"
show_out "$out"
fail=1; mo_bad=1
fi
done
[ "$mo_bad" = 1 ] || echo "ok msvc-old network stack, _WIN32_WINNT=$winnt"
done
# and no single string literal in the CA bundle reaches MSVC's limit
if python3 - << 'PYEOF'
import re, sys
t = open("libretro-common/net/cacert.h", encoding="latin-1").read()
for body in re.findall(r"\[\] = \{(.*?)\};", t, re.S):
lits = re.findall(r'"((?:[^"\\]|\\.)*)"', body)
size = sum(len(l.encode("latin-1").decode("unicode_escape")) for l in lits) + len(lits)
if size >= 65535:
sys.exit(1)
PYEOF
then
echo "ok cacert.h parts under MSVC's 64 KiB string limit"
else
echo "FAIL cacert.h has a part at or over MSVC's 64 KiB string limit"; fail=1
fi
else
echo "skip msvc-old network stack (no $MSVC_OLD_CC)"
fi
# Every platform makefile on Makefile.common, without its toolchain: the
# graph's own DEFINES checked for consistency (TLS, SMB, NFS need the
# networking they are built on) and the frontend compiled under exactly
# them. Makefile.retrofw asked for the TLS client with no networking
# and only its own workflow saw the undefined reference.
pm_out=$(tools/platform_makefile_check.sh 2>&1); pm_rc=$?
printf '%s\n' "$pm_out" | sed 's/^ok /ok platform makefile: /'
[ $pm_rc -eq 0 ] || { echo "FAIL platform makefile check"; fail=1; }
# What each console Makefile actually builds, from its own object list:
# no MCP server on any console (a desktop HAVE_MCP option; listed once
# with the networking objects it broke the 3DS), and no crypto library
# on the 24/32 MiB machines.
co_out=$(tools/console_objects_check.sh 2>&1); co_rc=$?
printf '%s\n' "$co_out" | sed 's/^ok /ok console objects: /'
[ $co_rc -eq 0 ] || { echo "FAIL console object check"; fail=1; }
# The OpenDingux family builds with a MIPS toolchain; the crypto and the
# TLS client compile with a MIPS cross compiler when one is installed.
MIPSEL_CC=${MIPSEL_CC:-mipsel-linux-gnu-gcc}
if command -v "$MIPSEL_CC" >/dev/null 2>&1; then
for tu in $TLS_SET; do
if out=$($MIPSEL_CC -march=mips32r2 $INC $CONSOLE_NET -DDINGUX -D_GNU_SOURCE -Wall -Werror -Wno-unused-function -fsyntax-only "$tu" 2>&1); then
echo "ok dingux mips: $(basename $tu)"
else
echo "FAIL dingux mips: $(basename $tu)"; show_out "$out"; fail=1
fi
done
else
echo "skip dingux mips (no $MIPSEL_CC)"
fi
platform_video "odroidgo2 video" \
"-DHAVE_ODROIDGO2 -DHAVE_OPENGL -DHAVE_GLSL" "" \
gfx/drivers/gl2.c ""
@@ -148,14 +281,192 @@ platform_video "gx video" "-DGEKKO -DHW_RVL" "-I$STUBS/gx" \
platform_video "switch video" \
"-DHAVE_LIBNX -DSWITCH -D__SWITCH__" "-I$STUBS/libnx" \
gfx/drivers/switch_nx_gfx.c ""
# The PSP driver folds its VRAM and cache-alias addresses into pointers,
# which a 64-bit host narrows; the arithmetic is 32-bit on the target.
platform_video "psp1 video" "-DPSP" \
"-Itools/platform_stubs/psp -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast \
-Wdeclaration-after-statement -Werror=declaration-after-statement" \
gfx/drivers/psp1_gfx.c ""
# The Vita driver, against psp2 stubs carrying the SceGxm declarations it
# names in the shapes the real headers give them. Same narrowing as the
# PSP driver above: its pool addresses are 32-bit on the target.
platform_video "gxm video" "-DVITA -DRARCH_CONSOLE $HOSTOFF" \
"-Itools/platform_stubs/vita -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast" \
gfx/drivers/gxm_gfx.c ""
# gl1 on Vita, against a vitaGL stub over the host GL headers. The
# driver's Vita branches are compiled nowhere else, and one of them
# carried a declaration after a statement that only the Vita job saw.
platform_video "vita gl1 video" \
"-DVITA -DRARCH_CONSOLE -DHAVE_OPENGL1 -DHAVE_OVERLAY -DHAVE_GFX_WIDGETS $HOSTOFF" \
"-Itools/platform_stubs/vita -Wdeclaration-after-statement \
-Werror=declaration-after-statement" \
gfx/drivers/gl1.c /usr/include/GL/gl.h
# The PS2 driver, against gsKit and PS2SDK stubs carrying what it
# names in the shapes the real headers give them. Makefile.ps2 turns on
# the window offset, which the driver reads. This lane is also the only
# one that compiles retro_atomic.h's EE interrupt-mask backend, which
# no other platform selects.
platform_video "ps2 video" \
"-DPS2 -DRARCH_CONSOLE -DHAVE_WINDOW_OFFSET -DHAVE_RGUI $HOSTOFF" \
"-Itools/platform_stubs/ps2" gfx/drivers/ps2_gfx.c ""
# The DOS VGA driver, against DJGPP stubs for the DPMI and port I/O it
# uses. Only the DJGPP job compiled it before; DJGPP defines __unix__.
platform_video "dos vga video" \
"-D__DJGPP__ -DDJGPP -DRARCH_CONSOLE $HOSTOFF -D__unix__" \
"-Itools/platform_stubs/dos -Wdeclaration-after-statement \
-Werror=declaration-after-statement" \
gfx/drivers/vga_gfx.c ""
# The two SDL video drivers. Nothing else here compiled them, which is
# how a field they read through video_info_t went on being read after it
# had been packed away. Each skips where its headers are absent, as the
# dingux lanes below do.
platform_video "sdl2 video" "-DHAVE_SDL2" "-I/usr/include/SDL2" \
gfx/drivers/sdl2_gfx.c /usr/include/SDL2/SDL.h
platform_video "sdl3 video" "-DHAVE_SDL3" "-I/usr/include/SDL3" \
gfx/drivers/sdl3_gfx.c /usr/include/SDL3/SDL.h
platform_video "dingux video" "-DDINGUX" "-I/usr/include/SDL" \
gfx/drivers/sdl_dingux_gfx.c /usr/include/SDL/SDL.h
platform_video "rs90 video" "-DDINGUX -DRS90" "-I/usr/include/SDL" \
gfx/drivers/sdl_rs90_gfx.c /usr/include/SDL/SDL.h
# libdrm's exynos module and its G2D headers, which distros ship only
# where the hardware exists. A runner building libdrm with -Dexynos=true
# turns this on.
platform_video "wiiu gx2 video" "-DWIIU $HOSTOFF" \
"-Iwiiu/include -Iwiiu" gfx/drivers/gx2_gfx.c ""
platform_video "hub75 video" "" "" gfx/drivers/hub75_gfx.c ""
platform_video "openvg video" "-DHAVE_VG -DHAVE_EGL" \
"-Itools/platform_stubs/openvg" gfx/drivers/vg.c ""
platform_video "exynos video" "-DHAVE_EXYNOS" "-I/usr/include/libdrm" \
gfx/drivers/exynos_gfx.c /usr/include/exynos/exynos_fimg2d.h
# The Direct3D drivers, against the DirectX headers the tree vendors in
# gfx/include/dxsdk. Six drivers no other lane compiles, and they read
# the same frame and draw descriptors every other video driver does.
#
# A mingw-w64 cross compiler is all these need beyond the tree; where
# there is none the lanes say so rather than fail.
MINGW_CC=${MINGW_CC:-x86_64-w64-mingw32-gcc}
d3d_video() {
name="$1"; defs="$2"; tu="$3"
if ! command -v "$MINGW_CC" > /dev/null 2>&1; then
echo "skip $name (no $MINGW_CC)"
return
fi
if ! out=$($MINGW_CC $WARN -isystemgfx/include/dxsdk $INC $BASE $defs \
-fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
show_out "$out"
fail=1
else
echo "ok $name"
fi
}
D3DDEFS="-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY"
d3d_video "d3d8 video" "$D3DDEFS -DHAVE_D3D8" gfx/drivers/d3d8.c
d3d_video "d3d9 video, Cg" "$D3DDEFS -DHAVE_D3D9" gfx/drivers/d3d9cg.c
d3d_video "d3d9 video, HLSL" "$D3DDEFS -DHAVE_D3D9" gfx/drivers/d3d9hlsl.c
d3d_video "d3d10 video" "$D3DDEFS -DHAVE_D3D10" gfx/drivers/d3d10.c
d3d_video "d3d11 video" "$D3DDEFS -DHAVE_D3D11" gfx/drivers/d3d11.c
d3d_video "d3d12 video" "$D3DDEFS -DHAVE_D3D12" gfx/drivers/d3d12.c
d3d_video "gdi video" "-DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_GDI" \
gfx/drivers/gdi_gfx.c
# The context drivers, which no job here compiles either. Each answers
# the frontend's window and size questions, so a change to what those
# hand back reaches all of them at once. Every lane names the headers it
# wants and says so when the runner has none.
platform_video "ctx: null" "" "" gfx/drivers_context/gfx_null_ctx.c ""
platform_video "ctx: x11 gl" "-DHAVE_X11 -DHAVE_OPENGL -DHAVE_EGL" "" \
gfx/drivers_context/x_ctx.c /usr/include/X11/Xlib.h
platform_video "ctx: x11 egl" "-DHAVE_X11 -DHAVE_EGL -DHAVE_OPENGLES" "" \
gfx/drivers_context/xegl_ctx.c /usr/include/X11/Xlib.h
platform_video "ctx: x11 vulkan" "-DHAVE_VULKAN -DHAVE_X11 -DHAVE_XCB" "" \
gfx/drivers_context/x_vk_ctx.c \
"/usr/include/vulkan/vulkan.h /usr/include/X11/Xlib-xcb.h"
platform_video "ctx: khr display" "-DHAVE_VULKAN" "" \
gfx/drivers_context/khr_display_ctx.c /usr/include/vulkan/vulkan.h
platform_video "ctx: kms/gbm" "-DHAVE_EGL -DHAVE_OPENGL -DHAVE_KMS -DHAVE_GBM" \
"-I/usr/include/libdrm" gfx/drivers_context/drm_ctx.c /usr/include/gbm.h
# vivante_fbdev is left out: it calls Vivante's own fbCreateWindow and
# fbGetDisplayByIndex, which only that vendor's EGL headers declare, and
# a lane that let those go implicit would pass what the SDK rejects.
platform_video "ctx: mali fbdev" "-DHAVE_EGL -DHAVE_OPENGLES" "" \
gfx/drivers_context/mali_fbdev_ctx.c /usr/include/EGL/egl.h
platform_video "ctx: opendingux fbdev" "-DHAVE_EGL -DHAVE_OPENGLES -DDINGUX" "" \
gfx/drivers_context/opendingux_fbdev_ctx.c /usr/include/EGL/egl.h
platform_video "ctx: sdl1 gl" "-DHAVE_SDL -DHAVE_OPENGL" "-I/usr/include/SDL" \
gfx/drivers_context/sdl1_gl_ctx.c /usr/include/SDL/SDL.h
platform_video "ctx: sdl2 gl" "-DHAVE_SDL2 -DHAVE_OPENGL" "-I/usr/include/SDL2" \
gfx/drivers_context/sdl2_gl_ctx.c /usr/include/SDL2/SDL.h
# osmesa is the software context a headless build selects, and
# qb/config.libs.sh can enable it from a pkg-config probe alone.
platform_video "ctx: osmesa" "-DHAVE_OSMESA -DHAVE_OPENGL" "" \
gfx/drivers_context/osmesa_ctx.c /usr/include/GL/osmesa.h
# The Wayland protocol headers are generated into gfx/common/wayland by
# configure, so these lanes run only in a tree that has been configured
# with Wayland.
WL_H="/usr/include/wayland-client.h gfx/common/wayland/content-type-v1.h"
platform_video "ctx: wayland gl" "-DHAVE_WAYLAND -DHAVE_EGL -DHAVE_OPENGL" "" \
gfx/drivers_context/wayland_ctx.c "$WL_H"
platform_video "ctx: wayland vulkan" "-DHAVE_WAYLAND -DHAVE_VULKAN" "" \
gfx/drivers_context/wayland_vk_ctx.c "$WL_H /usr/include/vulkan/vulkan.h"
# The shared Wayland code the two contexts above call into. It was the
# only Wayland unit no lane compiled, which is how four declarations
# after a statement sat in it. Its libdecor half is a second lane: the
# function pointers that half calls through are declared under
# HAVE_LIBDECOR_H *and* HAVE_DYLIB, so a lane naming only the first
# proves nothing, and the header libdecor.h itself comes from a
# libdecor-0 include directory the probe checks for.
WLC89="-Wdeclaration-after-statement -Werror=declaration-after-statement"
platform_video "wayland common" "-DHAVE_WAYLAND -DHAVE_EGL -DHAVE_OPENGL" \
"$WLC89" gfx/common/wayland_common.c "$WL_H"
platform_video "wayland common: libdecor" \
"-DHAVE_WAYLAND -DHAVE_EGL -DHAVE_OPENGL -DHAVE_LIBDECOR_H -DHAVE_DYLIB" \
"-I/usr/include/libdecor-0 $WLC89" \
gfx/common/wayland_common.c "$WL_H /usr/include/libdecor-0/libdecor.h"
platform_video "wayland common: backport" \
"-DHAVE_WAYLAND -DHAVE_EGL -DHAVE_OPENGL -DHAVE_WAYLAND_BACKPORT=1" \
"$WLC89" gfx/common/wayland_common.c "$WL_H"
# webOS swaps its own shim in for half of the common Wayland functions,
# and the Wayland context reaches it through prototypes of its own, so
# the shim and the context each get a lane. The shim's webOS protocol
# headers are wayland-scanner output of LG's webos-wayland-extensions
# (Apache-2.0), which the SDK generates at build time; stubs/webos keeps
# a copy laid out so the shim's ../../gfx/common/wayland includes find it.
WEBOS="-DWEBOS -DWEBOS_APP_ID=\"com.retroarch\" -DHAVE_WAYLAND -DHAVE_EGL"
WEBOS="$WEBOS -DHAVE_OPENGLES"
platform_video "ctx: wayland gl (webos)" "$WEBOS" "" \
gfx/drivers_context/wayland_ctx.c "$WL_H"
platform_video "webos wayland shim" "$WEBOS" \
"-I$STUBS/webos/gfx/common" \
input/common/wayland_common_webos.c "$WL_H"
arm "win32" win32 "-D_WIN32 -D_WIN32_WINNT=0x0600"
arm "win32-old" win32 "-D_WIN32 -D_WIN32_WINNT=0x0400"
arm "macos" apple "-D__APPLE__"
arm "ios" apple "-D__APPLE__ -DTARGET_OS_IPHONE=1"
# The GL core driver without slang: its HDR encode is slang's, and state
# for it once sat outside the slang guards, which only the C89 lane -
# built without slang - ever compiled.
platform_video "glcore without slang" "-DHAVE_OPENGL -DHAVE_OPENGL_CORE" \
"" gfx/drivers/gl3.c "/usr/include/GL/gl.h"
platform_video "glcore with slang" \
"-DHAVE_OPENGL -DHAVE_OPENGL_CORE -DHAVE_SLANG -DHAVE_SHADERPIPELINE" \
"" gfx/drivers/gl3.c "/usr/include/GL/gl.h"
# The CoreText rasterizer and the font driver's Apple branch, against
# stand-ins for the SDK headers it includes: no runner builds it but the
# Apple jobs, so a change to the rasterizer interface reached them first.
CORETEXT="$HOSTOFF -D__APPLE__ -D__MACH__ -DHAVE_CORETEXT"
platform_video "macos coretext rasterizer" "$CORETEXT" "-I$STUBS/apple" \
gfx/drivers_font_renderer/coretext.c ""
platform_video "ios coretext rasterizer" "$CORETEXT -DTARGET_OS_IPHONE=1" \
"-I$STUBS/apple" gfx/drivers_font_renderer/coretext.c ""
platform_video "macos font driver" "$CORETEXT" "-I$STUBS/apple" \
gfx/font_driver.c ""
arm "dos/djgpp" "" "-D__DJGPP__ -D__unix__"
arm "linux" "" ""
@@ -190,6 +501,51 @@ check "gl3: GLES3" \
C89="-std=c89 -ansi -pedantic -Werror=pedantic -Werror=declaration-after-statement -Wno-long-long -Wno-variadic-macros -D_GNU_SOURCE -DC89_BUILD"
check "gl3: desktop, C89" \
"$GL3DEFS $GLINC $C89" gfx/drivers/gl3.c
# Without threads, in C89: audio_driver.h and rthreads.h both reach the
# tasks and the audio driver, and a typedef each declared on its own is
# one C89 refuses twice. make C89_BUILD=1 --disable-threads stopped at
# task_save.c on it; no job builds that shape. check() always adds
# HAVE_THREADS, so this lane spells its defines out.
NOTHREADS_BASE=$(echo "$BASE" | sed 's/-DHAVE_THREADS//')
nt_bad=0
for tu in tasks/task_save.c tasks/task_audio_mixer.c \
audio/audio_driver.c audio/audio_thread_wrapper.c; do
if ! out=$($CC $WARN $INC $NOTHREADS_BASE $C89 -fsyntax-only "$tu" 2>&1); then
echo "FAIL audio + tasks: no threads, C89"
echo " $tu"
show_out "$out"
fail=1; nt_bad=1
fi
done
[ "$nt_bad" = 1 ] || echo "ok audio + tasks: no threads, C89"
# WASAPI is in the Windows C89_BUILD lane, which no job here builds, and
# its error-string helper serves only the microphone path, so both halves
# are compiled: C89 flags, with and without HAVE_MICROPHONE, unused
# functions as errors. A real compile rather than -fsyntax-only, which
# stops before GCC looks for unused static functions.
win32_audio() {
name="$1"; defs="$2"; tu="$3"
if ! command -v "$MINGW_CC" > /dev/null 2>&1; then
echo "skip $name (no $MINGW_CC)"
return
fi
if ! out=$($MINGW_CC $WARN -Wunused-function -Werror=unused-function \
$INC $BASE $C89 \
$defs -c -o /dev/null "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
show_out "$out"
fail=1
else
echo "ok $name"
fi
}
win32_audio "wasapi: C89, microphone" "-DHAVE_WASAPI -DHAVE_MICROPHONE" \
audio/drivers/wasapi.c
win32_audio "wasapi: C89, no microphone" "-DHAVE_WASAPI" \
audio/drivers/wasapi.c
check "hw ring: OpenGL only" \
"-DHAVE_OPENGL $GLINC" gfx/video_thread_hw.c
check "hw ring: GLES only" \
@@ -216,7 +572,7 @@ check_nothreads() {
if ! out=$($CC $WARN $INC $NOTHREADS $defs -fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
echo "$out" | sed 's/^/ /' | head -12
show_out "$out"
fail=1; bad=1
fi
done
@@ -235,10 +591,10 @@ check_nothreads() {
# eventcount park path (threads on).
check "audio: openal" "-Itools/platform_stubs/openal -DHAVE_AL -DHAVE_THREADS -Wdeclaration-after-statement -Werror=declaration-after-statement" audio/drivers/openal.c
# psp_audio: index-pair SPSC + eventcount park; compile-only sce
# stubs, PSP and Vita arms.
# psp/psp2 audio: index-pair SPSC + eventcount park, one driver per
# platform; compile-only sce stubs.
check "psp: psp_audio" "-Itools/platform_stubs/psp -DPSP -DHAVE_THREADS -Wdeclaration-after-statement -Werror=declaration-after-statement" audio/drivers/psp_audio.c
check "vita: psp_audio" "-Itools/platform_stubs/vita -DVITA -DHAVE_THREADS -Wdeclaration-after-statement -Werror=declaration-after-statement" audio/drivers/psp_audio.c
check "vita: psp2_audio" "-Itools/platform_stubs/vita -DVITA -DHAVE_THREADS -Wdeclaration-after-statement -Werror=declaration-after-statement" audio/drivers/psp2_audio.c
# features_cpu.c on every statically linked platform. It owns
# cpu_features_get_time_usec() and retro_sleep_until_us(); the wait's
@@ -251,7 +607,6 @@ check "vita: psp_audio" "-Itools/platform_stubs/vita -DVITA -DHAVE_THREADS -Wdec
# them. PSP gets _POSIX_C_SOURCE for the host libc's nanosleep, which
# pspsdk's newlib declares unconditionally.
FCPU_TU=libretro-common/features/features_cpu.c
HOSTOFF="-U__linux__ -U__gnu_linux__ -Ulinux -U__unix__ -U__unix -Uunix"
check "features_cpu: 3ds" "$HOSTOFF -Itools/platform_stubs/ctr -D_3DS -D__3DS__ -DARM11 -DRARCH_CONSOLE" $FCPU_TU
check "features_cpu: gekko" "$HOSTOFF -Itools/platform_stubs/gekko -DGEKKO -DHW_RVL -DRARCH_CONSOLE" $FCPU_TU
check "features_cpu: wiiu" "$HOSTOFF -Itools/platform_stubs/wiiu -DWIIU -DRARCH_CONSOLE" $FCPU_TU
@@ -261,6 +616,118 @@ check "features_cpu: ps3" "$HOSTOFF -Itools/platform_stubs/ps3 -D__PS3__
check "features_cpu: psl1ght" "$HOSTOFF -Itools/platform_stubs/psl1ght -D__PS3__ -D__PSL1GHT__ -DRARCH_CONSOLE" $FCPU_TU
check "features_cpu: emscripten" "$HOSTOFF -Itools/platform_stubs/emscripten -D__EMSCRIPTEN__ -DEMSCRIPTEN" $FCPU_TU
# Shared menu and video code under the console platform macros. These
# files are compiled by every desktop job, but the blocks inside them
# that a console selects are not, and a change to a struct those blocks
# touch is green everywhere until a console job runs. The frame and
# draw descriptors they read need no SDK header, so these lanes take
# the stubs already here and shed the host identity like the ones
# above.
MENUGFX="gfx/video_driver.c gfx/gfx_display.c gfx/gfx_widgets.c \
gfx/gfx_thumbnail.c gfx/gfx_surface.c gfx/video_thread_wrapper.c \
menu/drivers/xmb.c menu/drivers/ozone.c menu/drivers/materialui.c"
MG_BASE="-DHAVE_XMB -DHAVE_OZONE -DHAVE_MATERIALUI -DHAVE_RGUI \
-DHAVE_GFX_WIDGETS -DHAVE_OVERLAY -DHAVE_CONFIGFILE -DRARCH_CONSOLE \
-DRARCH_INTERNAL -Iinput/include"
check "console menu+gfx: vita" "$HOSTOFF $MG_BASE -Itools/platform_stubs/vita -DVITA" $MENUGFX
check "console menu+gfx: wiiu" "$HOSTOFF $MG_BASE -Itools/platform_stubs/wiiu -DWIIU" $MENUGFX
check "console menu+gfx: psl1ght" "$HOSTOFF $MG_BASE -Itools/platform_stubs/psl1ght -D__PS3__ -D__PSL1GHT__" $MENUGFX
# video_thread_wrapper.c reaches for libctru's own allocator on 3DS,
# and a guessed linearMemAlign() would let this lane pass what the real
# SDK rejects, so the 3DS lane takes the rest.
check "console menu+gfx: 3ds" "$HOSTOFF $MG_BASE -Itools/platform_stubs/ctr -D_3DS -D__3DS__" \
$(echo "$MENUGFX" | tr ' ' '\n' | grep -v video_thread_wrapper)
check "console menu+gfx: gekko" "$HOSTOFF $MG_BASE -Itools/platform_stubs/gekko -DGEKKO -DHW_RVL" $MENUGFX
# Platform drivers no other job compiles. Each of these reaches a
# compiler only inside its own console toolchain - griffin includes it
# behind that platform's #ifdef and the desktop makefiles never build
# it - so a C89 slip or a missing declaration in one sits until the
# console job runs, which is how a mixed declaration lived in
# gx_joypad.c. These are the ones the stubs already in the tree can
# reach, with one stub added for the pair of PSP input drivers - the
# 3ds audio drivers want fifteen more symbols whose libctru signatures
# cannot be checked from here, and a stub that guessed one would let a
# lane pass what the real SDK rejects, so they are left out.
# The rest need headers no stub here supplies. psp1_gfx and
# dispserv_android already have lanes of their own - the latter gains
# the C89 declaration check below rather than a second lane.
#
# Same warnings as the driver lanes above, C89 declarations included,
# because that is the rule these files are furthest from anyone
# checking. GEKKO takes the vendored libogc headers before its stub:
# the stub carries only what libogc does not.
CDECL="-Wdeclaration-after-statement -Werror=declaration-after-statement"
GEKKO_INC="-Iwii/libogc/include -Itools/platform_stubs/gekko"
check "gekko: gx_input" "$HOSTOFF $GEKKO_INC -DGEKKO -DHW_RVL -DRARCH_CONSOLE $CDECL" input/drivers/gx_input.c
check "gekko: gx_joypad" "$HOSTOFF $GEKKO_INC -DGEKKO -DHW_RVL -DRARCH_CONSOLE $CDECL" input/drivers_joypad/gx_joypad.c
check "gekko: mem2_manager" "$HOSTOFF $GEKKO_INC -DGEKKO -DHW_RVL -DRARCH_CONSOLE $CDECL" libretro-common/memory/mem2_manager.c
check "gekko: dispserv_gx" "$HOSTOFF $GEKKO_INC -DGEKKO -DHW_RVL -DRARCH_CONSOLE $CDECL" gfx/display_servers/dispserv_gx.c
check "gamecube: dispserv_gx" "$HOSTOFF $GEKKO_INC -DGEKKO -DRARCH_CONSOLE $CDECL" gfx/display_servers/dispserv_gx.c
check "gekko: dispserv_gx_modes" "$HOSTOFF $GEKKO_INC -DGEKKO -DHW_RVL -DRARCH_CONSOLE $CDECL" gfx/display_servers/dispserv_gx_modes.c
check "psl1ght: dispserv_ps3" "$HOSTOFF -Itools/platform_stubs/ps3 -D__PS3__ -D__PSL1GHT__ -DRARCH_CONSOLE $CDECL" gfx/display_servers/dispserv_ps3.c gfx/display_servers/dispserv_ps3_modes.c
check "3ds: ctr_input" "$HOSTOFF -Itools/platform_stubs/ctr -D_3DS -D__3DS__ -DARM11 -DRARCH_CONSOLE $CDECL" input/drivers/ctr_input.c
PSP_DEFS="$HOSTOFF -Itools/platform_stubs/psp -DPSP -D_POSIX_C_SOURCE=199309L -DRARCH_CONSOLE"
check "psp: psp_input" "$PSP_DEFS $CDECL" input/drivers/psp_input.c
check "psp: psp_joypad" "$PSP_DEFS $CDECL" input/drivers_joypad/psp_joypad.c
check "orbis: ps4_audio" "$HOSTOFF -Itools/platform_stubs/orbis -DORBIS $CDECL" audio/drivers/ps4_audio.c
check "qnx: alsa_qsa" "$HOSTOFF -Itools/platform_stubs/qnx -D__QNX__ $CDECL" audio/drivers/alsa_qsa.c
check "android: vfs saf" "$HOSTOFF -Itools/platform_stubs/android -DANDROID $CDECL" libretro-common/vfs/vfs_implementation_saf.c
check "android: play delivery" "$HOSTOFF -Itools/platform_stubs/android -DANDROID $CDECL" play_feature_delivery/play_feature_delivery.c
# the Android build carries the built-in NFS client and its backend
# (HAVE_RETRONFS in pkg/android/phoenix-common/jni/Android.mk) and the
# prefetcher both network backends read through
check "android: nfs client" "$HOSTOFF -Itools/platform_stubs/android -DANDROID -DHAVE_NFSCLIENT -DHAVE_RETRONFS -DHAVE_THREADS -DHAVE_NETWORKING $CDECL" libretro-common/net/net_nfs3.c
check "android: nfs backend" "$HOSTOFF -Itools/platform_stubs/android -DANDROID -DHAVE_NFSCLIENT -DHAVE_RETRONFS -DHAVE_THREADS -DHAVE_NETWORKING -DRARCH_INTERNAL $CDECL" libretro-common/vfs/vfs_implementation_nfs.c
check "android: vfs prefetch" "$HOSTOFF -Itools/platform_stubs/android -DANDROID -DHAVE_THREADS $CDECL" libretro-common/vfs/vfs_prefetch.c
# The Android build carries the cleanroom network stack (Android.mk:
# HAVE_CRYPTO, keychain, RETROSSL, RETROSMB, RETRONFS) and is compiled
# by the NDK's clang for aarch64. The same compiler family and target
# here, with the aarch64 cross headers standing in for the NDK's, over
# every translation unit of the stack: the AES/PMULL path takes the
# +crypto target attribute this way, which gcc lanes never exercise.
ANDROID_CLANG=${ANDROID_CLANG:-clang}
ANDROID_SYSINC=${ANDROID_SYSINC:-/usr/aarch64-linux-gnu/include}
android_clang() {
name="$1"; defs="$2"; tu="$3"
if ! command -v "$ANDROID_CLANG" > /dev/null 2>&1 || [ ! -d "$ANDROID_SYSINC" ]; then
echo "skip $name (no $ANDROID_CLANG or $ANDROID_SYSINC)"
return
fi
if ! out=$($ANDROID_CLANG --target=aarch64-linux-gnu -isystem "$ANDROID_SYSINC" \
-Wall -Werror -Wno-unused-function -D__ANDROID__ -DANDROID $INC $defs \
-fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
show_out "$out"
fail=1
else
echo "ok $name"
fi
}
ANDROID_NET="-DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_SSL -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS -DHAVE_NETWORKING -DHAVE_THREADS -DHAVE_CONFIGFILE -DRARCH_INTERNAL -D_GNU_SOURCE"
for tu in libretro-common/crypto/crypto.c libretro-common/crypto/kdf.c libretro-common/crypto/pk.c \
libretro-common/crypto/x509.c libretro-common/file/keychain.c libretro-common/net/net_socket_ssl_retro.c \
libretro-common/net/net_smb2.c libretro-common/net/net_krb5.c libretro-common/net/net_nfs3.c \
libretro-common/vfs/vfs_implementation_smb.c libretro-common/vfs/vfs_implementation_nfs.c \
libretro-common/vfs/vfs_prefetch.c network/tls_log.c; do
android_clang "android clang/aarch64: $(basename $tu)" "$ANDROID_NET" "$tu"
done
# rwebaudio is its own translation unit in the emscripten build, not
# part of griffin's, and nothing else compiles it at all.
check "emscripten: rwebaudio" "$HOSTOFF -Itools/platform_stubs/emscripten -D__EMSCRIPTEN__ -DEMSCRIPTEN -DHAVE_RWEBAUDIO $CDECL" audio/drivers/rwebaudio.c
check "emscripten: rwebcam" "$HOSTOFF -Itools/platform_stubs/emscripten -D__EMSCRIPTEN__ -DEMSCRIPTEN $CDECL" camera/drivers/rwebcam.c
# Two more that no job here compiles: the S3 cloud-sync backend, which
# only griffin includes and which nothing defines HAVE_S3 for, and the
# Lakka wifi driver, which needs HAVE_LAKKA. Both had a mixed
# declaration. The C89 build cannot see either, because neither is in
# its object list.
NETDEFS="-DHAVE_NETWORKING -DHAVE_CONFIGFILE -DHAVE_OVERLAY -DHAVE_CHEATS"
check "cloudsync: s3" "$NETDEFS -DHAVE_CLOUDSYNC -DHAVE_S3 $CDECL" network/cloud_sync/s3.c
check "lakka: connmanctl" "$NETDEFS -DHAVE_LAKKA -DHAVE_WIFI $CDECL" network/drivers_wifi/connmanctl.c
# The salamander launchers link a hand-picked subset of libretro-common
# with -DIS_SALAMANDER: rtime.c for rtime_localtime, no features_cpu.c.
# A syntax pass cannot see the link error that a symbol from an
@@ -272,7 +739,7 @@ salamander_link() {
if ! out=$($CC $WARN $INC $BASE $defs -o /tmp/salamander_link_check \
tools/platform_stubs/salamander_main.c "$@" 2>&1); then
echo "FAIL $name"
echo "$out" | sed 's/^/ /' | head -12
show_out "$out"
fail=1
else
echo "ok $name"
@@ -288,7 +755,7 @@ salamander_link "salamander link: rtime.c" \
# (jni, looper, native_activity, sensor, configuration, window) with
# the host identity shed.
check "android: runloop" "$HOSTOFF -DANDROID -Itools/platform_stubs/android" runloop.c
check "android: dispserv" "$HOSTOFF -DANDROID -Itools/platform_stubs/android" gfx/display_servers/dispserv_android.c
check "android: dispserv" "$HOSTOFF -DANDROID -Itools/platform_stubs/android $CDECL" gfx/display_servers/dispserv_android.c
# The Android arms of rthreads: thread affinity goes to the kernel
# directly there (bionic keeps cpu_set_t behind _GNU_SOURCE), and the
@@ -296,6 +763,17 @@ check "android: dispserv" "$HOSTOFF -DANDROID -Itools/platform_stubs/android" gf
check "android: rthreads (API 21)" "-DHAVE_THREADS -D__ANDROID__ -D__ANDROID_API__=21 -include tools/platform_stubs/android/bionic_pthread_stub.h -Itools/platform_stubs/android" libretro-common/rthreads/rthreads.c
check "android: rthreads (API 19)" "-DHAVE_THREADS -D__ANDROID__ -D__ANDROID_API__=19 -Itools/platform_stubs/android" libretro-common/rthreads/rthreads.c
# The D-Bus, Mutter and RealtimeKit units, and the elevation chain,
# are built on Linux and BSD only, but griffin and other build systems
# may still see the files elsewhere: as a target with no POSIX headers
# at all (MSVC, consoles), each must reduce to its gate and include
# none of them.
check "no-posix: D-Bus/Mutter/RealtimeKit units" "$HOSTOFF -DHAVE_DYLIB -Itools/platform_stubs/no_posix" \
gfx/common/dbus_runtime.c gfx/common/dbus_common.c \
gfx/common/mutter_displayconfig.c \
frontend/thread_elevation.c frontend/thread_elevation/rtkit.c \
frontend/thread_elevation/eevdf.c
check "android: opensl" "-DANDROID -DHAVE_OPENSL -Itools/platform_stubs/android -Wdeclaration-after-statement -Werror=declaration-after-statement" audio/drivers/opensl.c
check "gekko: rgui" "-DGEKKO -DHAVE_MENU -DHAVE_RGUI -Itools/platform_stubs/gekko" menu/drivers/rgui.c
@@ -349,7 +827,7 @@ check_gates() {
if ! out=$($CC $WARN $INC $NOMENU $defs -fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
echo "$out" | sed 's/^/ /' | head -12
show_out "$out"
fail=1; bad=1
fi
done
@@ -359,6 +837,11 @@ UITU="retroarch.c runloop.c gfx/video_driver.c gfx/gfx_display.c"
UIDEFS="$GLINC -DHAVE_COMMAND -DHAVE_STDIN_CMD"
check_gates "gates: menu + widgets" "$UIDEFS -DHAVE_MENU -DHAVE_GFX_WIDGETS" $UITU
check_gates "gates: menu only" "$UIDEFS -DHAVE_MENU" $UITU
# The keychain without networking: its menu entry and task must be
# declared outside the networking-only parts of tasks_internal.h.
check_gates "gates: keychain menu, no networking" \
"$UIDEFS -DHAVE_MENU -DHAVE_KEYCHAIN -DHAVE_CRYPTO -DHAVE_CONFIGFILE" \
menu/menu_setting.c tasks/task_keychain.c
check_gates "gates: widgets only" "$UIDEFS -DHAVE_GFX_WIDGETS" $UITU
check_gates "gates: neither" "$UIDEFS" $UITU
@@ -376,6 +859,37 @@ check_gates "gates: no networking" "$UIDEFS $(without -DHAVE_NETWORKING)"
check_gates "gates: no run-ahead" "$UIDEFS $(without -DHAVE_RUNAHEAD)" $FETU
check_gates "gates: no subsystems" "$UIDEFS" $FETU
# Each video API owns a GPU index setting and its default, under that
# API's own gate, and a build has any mix of APIs: a default defined
# under another API's gate builds only where both are on. Each API that
# a Linux build can have alone is checked alone.
check_gates "gates: GPU index, EGL only" "$UIDEFS -DHAVE_EGL -DHAVE_OPENGL" configuration.c
check_gates "gates: GPU index, Vulkan only" "$UIDEFS -DHAVE_VULKAN" configuration.c
# griffin is one translation unit, so a file-local name that is fine on
# its own collides there: p256's fmul against gfx's did, and only the
# MSVC job builds griffin. Both crypto shapes, on Linux, for free.
check_gates "griffin: HAVE_CRYPTO + keychain" "$UIDEFS -DRARCH_INTERNAL -DHAVE_GRIFFIN=1 -DHAVE_MENU -DHAVE_RGUI -DHAVE_CONFIGFILE -DHAVE_CRYPTO -DHAVE_KEYCHAIN -D_GNU_SOURCE" griffin/griffin.c
check_gates "griffin: HAVE_RETROSSL" "$UIDEFS -DRARCH_INTERNAL -DHAVE_GRIFFIN=1 -DHAVE_MENU -DHAVE_RGUI -DHAVE_CONFIGFILE -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_NETWORKING -DHAVE_SSL -DHAVE_RETROSSL -D_GNU_SOURCE" griffin/griffin.c
check_gates "griffin: HAVE_RETROSMB + cloud sync" "$UIDEFS -DRARCH_INTERNAL -DHAVE_GRIFFIN=1 -DHAVE_MENU -DHAVE_RGUI -DHAVE_CONFIGFILE -DHAVE_CRYPTO -DHAVE_NETWORKING -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_CLOUDSYNC -D_GNU_SOURCE" griffin/griffin.c
# The unity build the Android, Apple, webOS and MSVC targets ship: the
# whole network stack beside every codec, image reader and subsystem
# griffin can carry. A static helper in one translation unit that
# another one also defines (get32 in net_smb2.c and rvorbis.c) only
# collides here, so this lane carries as much of griffin as a host can.
GRIFFIN_FULL="-DRARCH_INTERNAL -DHAVE_GRIFFIN=1 -DHAVE_MENU -DHAVE_RGUI -DHAVE_XMB -DHAVE_OZONE -DHAVE_MATERIALUI -DHAVE_GFX_WIDGETS -DHAVE_OVERLAY -DHAVE_CONFIGFILE -DHAVE_CHEEVOS -DRC_DISABLE_LUA -DHAVE_RUNAHEAD -DHAVE_REWIND -DHAVE_PATCH -DHAVE_XDELTA -DHAVE_CHEATS -DHAVE_SCREENSHOTS -DHAVE_TRANSLATE -DHAVE_LIBRETRODB -DHAVE_DYNAMIC -DHAVE_DYLIB -DHAVE_THREADS -DHAVE_AUDIOMIXER -DHAVE_DSP_FILTER -DHAVE_VIDEO_FILTER -DHAVE_RWAV -DHAVE_RFLAC -DHAVE_RVORBIS -DHAVE_RMP3 -DHAVE_RAC3 -DHAVE_RPNG -DHAVE_RJPEG -DHAVE_RBMP -DHAVE_RTGA -DHAVE_RWEBP -DHAVE_RDDS -DHAVE_RZSTD -DHAVE_7ZIP -DHAVE_CHD -DHAVE_RCHD -DHAVE_IMAGEVIEWER -DHAVE_STB_FONT -DHAVE_NETWORKING -DHAVE_IFINFO -DHAVE_NETWORK_CMD -DHAVE_NETPLAYDISCOVERY -DHAVE_ONLINE_UPDATER -DHAVE_UPDATE_ASSETS -DHAVE_UPDATE_CORES -DHAVE_UPDATE_CORE_INFO -DHAVE_CORE_INFO_CACHE -DHAVE_CLOUDSYNC -DHAVE_S3 -DHAVE_CRYPTO -DHAVE_KEYCHAIN -DHAVE_SSL -DHAVE_RETROSSL -DHAVE_SMBCLIENT -DHAVE_RETROSMB -DHAVE_NFSCLIENT -DHAVE_RETRONFS -DHAVE_LANGEXTRA -DHAVE_SHADERPIPELINE -DHAVE_GLSL -DHAVE_OPENGL -D_GNU_SOURCE"
check_gates "griffin: full unity (every codec + network stack)" "$UIDEFS $GRIFFIN_FULL" griffin/griffin.c
# The SMB show-settings row read a HAVE_MENU-only default under
# HAVE_SMBCLIENT alone; with the built-in client on by default that
# broke every --disable-menu build. configuration.c with SMB and no
# menu, and the same with no menu at all.
check_gates "gates: SMB client, no menu" "$UIDEFS -DHAVE_CONFIGFILE -DHAVE_NETWORKING -DHAVE_CRYPTO -DHAVE_SMBCLIENT -DHAVE_RETROSMB -D_GNU_SOURCE" configuration.c
check_gates "gates: NFS client, no menu" "$UIDEFS -DHAVE_CONFIGFILE -DHAVE_NETWORKING -DHAVE_NFSCLIENT -DHAVE_RETRONFS -D_GNU_SOURCE" configuration.c
check_gates "gates: NFS client + menu" "$UIDEFS -DHAVE_CONFIGFILE -DHAVE_NETWORKING -DHAVE_NFSCLIENT -DHAVE_RETRONFS -DHAVE_MENU -DHAVE_RGUI -D_GNU_SOURCE" configuration.c menu/menu_setting.c menu/menu_displaylist.c menu/cbs/menu_cbs_ok.c menu/cbs/menu_cbs_deferred_push.c menu/cbs/menu_cbs_sublabel.c menu/cbs/menu_cbs_title.c
check_gates "gates: no menu, no SMB" "$UIDEFS -DHAVE_CONFIGFILE -DHAVE_NETWORKING -D_GNU_SOURCE" configuration.c
check_gates "griffin: HAVE_RETRONFS" "$UIDEFS -DRARCH_INTERNAL -DHAVE_GRIFFIN=1 -DHAVE_MENU -DHAVE_RGUI -DHAVE_CONFIGFILE -DHAVE_NETWORKING -DHAVE_NFSCLIENT -DHAVE_RETRONFS -D_GNU_SOURCE" griffin/griffin.c
check_gates "griffin: no crypto (console shape)" "$UIDEFS -DRARCH_INTERNAL -DHAVE_GRIFFIN=1 -DHAVE_MENU -DHAVE_RGUI -DHAVE_CONFIGFILE -DHAVE_KEYCHAIN -D_GNU_SOURCE" griffin/griffin.c
# A subsystem's own unit is built only when its gate is on, so each is
# checked with that gate on and the user interface off: the achievement
# and netplay widgets, and the menu entries either drives, are the edges
@@ -399,6 +913,47 @@ check "dsp filters: C89" "$C89 -DHAVE_FILTERS_BUILTIN" $DSP
check "dsp filters: C89, no C99 math declarations" \
"$(echo "$C89" | sed 's/ -D_GNU_SOURCE//') -DHAVE_FILTERS_BUILTIN" $DSP
# The UI companion's shared core, which only a Qt or Cocoa build puts
# in OBJ. A tree configured --disable-qt on a Linux host - which is what
# a desktop job builds - compiles none of it, so a change to a struct it
# reads is green everywhere until one of those jobs runs.
echo "== ui companion: the shared core =="
COMPANION="ui/companion/companion_core.c ui/companion/companion_thumbs.c \
ui/companion/companion_dock.c"
check "ui companion" "-DHAVE_RGUI -DHAVE_CONFIGFILE" $COMPANION
check "ui companion: no threads" \
"$(echo "$BASE" | sed 's/ -DHAVE_THREADS//') -DHAVE_RGUI -DHAVE_CONFIGFILE" \
$COMPANION
# Its own tests, which link the same objects and read the same structs.
# They live outside ui/companion and a change that stops at the three
# files above leaves them behind.
check "ui companion: tests" "-DHAVE_RGUI -DHAVE_CONFIGFILE -Iui/companion" \
ui/companion/test/companion_core_test.c \
ui/companion/test/companion_thumbs_test.c \
ui/companion/test/companion_core_stubs.c
# Video and shader back ends a Linux desktop job does not select. Each
# reads the frame descriptor and the shader parameter block every other
# driver does, and nothing else here compiles them: this configuration
# has no Vulkan, no Cg and none of the framebuffer drivers, so a change
# to those structs is green until a job that does have them runs.
echo "== video back ends this configuration does not build =="
GFXDEFS="-DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_GFX_WIDGETS"
VK_H=/usr/include/vulkan/vulkan.h
platform_video "vulkan back end" "$GFXDEFS -DHAVE_VULKAN" "" \
gfx/drivers/vulkan.c "$VK_H"
platform_video "vulkan shader back end" "$GFXDEFS -DHAVE_VULKAN" "" \
gfx/drivers_shader/shader_vulkan.c "$VK_H"
check "cg shader back end" "$GFXDEFS -DHAVE_CG -DHAVE_OPENGL" \
gfx/drivers_shader/shader_gl_cg.c
check "framebuffer back ends" "$GFXDEFS" \
gfx/drivers/fpga_gfx.c gfx/drivers/hub75_gfx.c gfx/drivers/sunxi_gfx.c
X11_H=/usr/include/X11/Xlib.h
platform_video "x11 shm back end" "$GFXDEFS -DHAVE_X11" "" \
gfx/drivers/xshm_gfx.c "$X11_H"
platform_video "xvideo back end" "$GFXDEFS -DHAVE_X11 -DHAVE_XVIDEO" "" \
gfx/drivers/xvideo.c "$X11_H"
echo "== run-ahead: the dynamic-library gates =="
# The secondary instance exists only with HAVE_DYNAMIC; a build that
# can load libraries but links its core statically (HAVE_DYLIB alone)
@@ -409,4 +964,116 @@ check "runahead: HAVE_DYLIB only" "$RADEFS -DHAVE_DYLIB" runahead
check "runahead: HAVE_DYNAMIC" "$RADEFS -DHAVE_DYNAMIC" runahead.c
check "runahead: both" "$RADEFS -DHAVE_DYNAMIC -DHAVE_DYLIB" runahead.c
# The Metal driver, as Objective-C against stand-ins for the parts of the
# macOS SDK it uses (tools/platform_stubs/metal). No runner here has the
# SDK, so the Apple jobs used to be the first to compile it. A selector
# the stand-ins do not declare is an error: a new Metal call needs its
# declaration added there.
if command -v clang > /dev/null 2>&1; then
if ! out=$(clang -x objective-c -fobjc-runtime=macosx-10.13 -fblocks \
-fsyntax-only -Wall -Wno-unused-function -Werror=objc-method-access \
-Werror=unused-but-set-variable $HOSTOFF -D__APPLE__ -D__MACH__ \
-DHAVE_METAL -Itools/platform_stubs/metal $INC $BASE \
gfx/drivers/metal.m 2>&1); then
echo "FAIL metal video (stubs)"
echo " gfx/drivers/metal.m"
show_out "$out"
fail=1
else
echo "ok metal video (stubs)"
fi
else
echo "skip metal video (stubs) (no clang)"
fi
# gfx/font_layout.h expands inside each driver's render function, and a
# hook that reads a value before declaring its own locals is C99 the C89
# lane rejects, so these lanes reject it too (the PS2 and Wii U drivers,
# built only by C99 toolchains, opt out). And a
# layout counter a driver has no use for is set but never read. GCC 15
# and clang warn about that; the runner's GCC does not, so these lanes
# ask clang, with the warning made an error.
font_layout() {
name="$1"; target="$2"; defs="$3"; tu="$4"
if ! command -v clang > /dev/null 2>&1; then
echo "skip $name (no clang)"
return
fi
if ! out=$(clang $target -Werror=unused-but-set-variable \
-Werror=declaration-after-statement \
$INC $BASE $defs -fsyntax-only "$tu" 2>&1); then
echo "FAIL $name"
echo " $tu"
show_out "$out"
fail=1
else
echo "ok $name"
fi
}
# Vulkan from the Khronos headers the tree carries (gfx/include), as the
# real builds use, so the lane runs on a runner without the SDK headers
font_layout "font layout: vulkan" "" "-DHAVE_VULKAN -Igfx/include" gfx/drivers/vulkan.c
font_layout "font layout: gl" "" "-DHAVE_OPENGL" gfx/drivers/gl2.c
font_layout "font layout: gl1" "" "-DHAVE_OPENGL -DHAVE_OPENGL1" \
gfx/drivers/gl1.c
if command -v sdl2-config > /dev/null 2>&1; then
font_layout "font layout: sdl2" "" "-DHAVE_SDL2 $(sdl2-config --cflags)" \
gfx/drivers/sdl2_gfx.c
else
echo "skip font layout: sdl2 (no sdl2-config)"
fi
if [ -f /usr/include/SDL3/SDL.h ]; then
font_layout "font layout: sdl3" "" "-DHAVE_SDL3 -I/usr/include/SDL3" \
gfx/drivers/sdl3_gfx.c
else
echo "skip font layout: sdl3 (no SDL3 headers)"
fi
# The software OSD blitters, where their headers are installed
if [ -f /usr/include/X11/extensions/Xvlib.h ]; then
font_layout "font layout: xvideo" "" "-DHAVE_X11 -DHAVE_XVIDEO" \
gfx/drivers/xvideo.c
fi
if [ -f /usr/include/SDL/SDL.h ]; then
font_layout "font layout: sdl" "" "-DHAVE_SDL -I/usr/include/SDL" \
gfx/drivers/sdl_gfx.c
fi
if [ -f /usr/include/linux/omapfb.h ]; then
font_layout "font layout: omap" "" "-DHAVE_OMAP" gfx/drivers/omap_gfx.c
fi
font_layout "font layout: switch" "" \
"-DHAVE_LIBNX -DSWITCH -D__SWITCH__ -I$STUBS/libnx" \
gfx/drivers/switch_nx_gfx.c
font_layout "font layout: gxm" "" \
"-DVITA -DRARCH_CONSOLE $HOSTOFF -Itools/platform_stubs/vita" \
gfx/drivers/gxm_gfx.c
font_layout "font layout: ps2" "" \
"-DPS2 -DRARCH_CONSOLE -DHAVE_WINDOW_OFFSET -DHAVE_RGUI $HOSTOFF -Itools/platform_stubs/ps2 \
-Wno-declaration-after-statement" \
gfx/drivers/ps2_gfx.c
font_layout "font layout: gx2" "" \
"-DWIIU $HOSTOFF -Iwiiu/include -Iwiiu -Wno-declaration-after-statement" \
gfx/drivers/gx2_gfx.c
font_layout "font layout: glcore" "" \
"-DHAVE_OPENGL -DHAVE_OPENGL_CORE -DHAVE_SLANG" gfx/drivers/gl3.c
if [ -d /usr/x86_64-w64-mingw32/include ]; then
MINGW_CLANG="--target=x86_64-w64-mingw32 -isystemgfx/include/dxsdk -isystem /usr/x86_64-w64-mingw32/include"
font_layout "font layout: gdi" "$MINGW_CLANG" \
"-DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_GDI -D_WIN32_WINNT=0x0601" \
gfx/drivers/gdi_gfx.c
font_layout "font layout: d3d8" "$MINGW_CLANG" \
"-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_D3D8" gfx/drivers/d3d8.c
font_layout "font layout: d3d9 hlsl" "$MINGW_CLANG" \
"-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_D3D9" gfx/drivers/d3d9hlsl.c
font_layout "font layout: d3d9 cg" "$MINGW_CLANG" \
"-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_D3D9" gfx/drivers/d3d9cg.c
font_layout "font layout: d3d10" "$MINGW_CLANG" \
"-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_D3D10" gfx/drivers/d3d10.c
font_layout "font layout: d3d11" "$MINGW_CLANG" \
"-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_D3D11" gfx/drivers/d3d11.c
font_layout "font layout: d3d12" "$MINGW_CLANG" \
"-DHAVE_D3D -DHAVE_RGUI -DHAVE_OVERLAY -DHAVE_D3D12" gfx/drivers/d3d12.c
else
echo "skip font layout: gdi, d3d8/9/10/11/12 (no MinGW headers)"
fi
exit $fail
@@ -0,0 +1,43 @@
/* Stand-in for the macOS SDK umbrella header: only the CoreGraphics
* and CoreText declarations gfx/drivers_font_renderer/coretext.c uses. */
#ifndef STUB_AS_H
#define STUB_AS_H
#include <CoreFoundation/CFString.h>
typedef double CGFloat;
typedef unsigned short CGGlyph;
typedef struct { CGFloat x, y; } CGPoint;
typedef struct { CGFloat width, height; } CGSize;
typedef struct { CGPoint origin; CGSize size; } CGRect;
typedef struct { CGFloat a,b,c,d,tx,ty; } CGAffineTransform;
extern const CGAffineTransform CGAffineTransformIdentity;
typedef struct CGContext *CGContextRef;
typedef struct CGColorSpace *CGColorSpaceRef;
typedef struct CGDataProvider *CGDataProviderRef;
typedef struct CGFont *CGFontRef;
typedef const struct __CTFont *CTFontRef;
typedef const struct __CTLine *CTLineRef;
typedef uint32_t CTFontOrientation;
typedef uint32_t CGBitmapInfo;
enum { kCGImageAlphaNone = 0, kCGImageAlphaOnly = 7, kCGBitmapByteOrder16Host = 0x1000 };
extern const CFStringRef kCTFontAttributeName;
extern const CFStringRef kCTForegroundColorFromContextAttributeName;
typedef void (*CGDataProviderReleaseDataCallback)(void *info, const void *data, size_t size);
CGDataProviderRef CGDataProviderCreateWithData(void *, const void *, size_t, CGDataProviderReleaseDataCallback);
CGFontRef CGFontCreateWithDataProvider(CGDataProviderRef);
CTFontRef CTFontCreateWithGraphicsFont(CGFontRef, CGFloat, const CGAffineTransform *, void *);
bool CTFontGetGlyphsForCharacters(CTFontRef, const UniChar *, CGGlyph *, CFIndex);
CGRect CTFontGetBoundingRectsForGlyphs(CTFontRef, CTFontOrientation, const CGGlyph *, CGRect *, CFIndex);
double CTFontGetAdvancesForGlyphs(CTFontRef, CTFontOrientation, const CGGlyph *, CGSize *, CFIndex);
CGFloat CTFontGetAscent(CTFontRef);
CGFloat CTFontGetDescent(CTFontRef);
CGFloat CTFontGetLeading(CTFontRef);
CGColorSpaceRef CGColorSpaceCreateDeviceGray(void);
void CGColorSpaceRelease(CGColorSpaceRef);
CGContextRef CGBitmapContextCreate(void *, size_t, size_t, size_t, size_t, CGColorSpaceRef, uint32_t);
void CGContextRelease(CGContextRef);
void CGContextSetGrayFillColor(CGContextRef, CGFloat, CGFloat);
void CGContextSetTextMatrix(CGContextRef, CGAffineTransform);
void CGContextSetTextPosition(CGContextRef, CGFloat, CGFloat);
CTLineRef CTLineCreateWithAttributedString(CFAttributedStringRef);
void CTLineDraw(CTLineRef, CGContextRef);
#endif
@@ -0,0 +1,27 @@
/* Stand-in for the macOS/iOS SDK header: only the CoreFoundation
* declarations gfx/drivers_font_renderer/coretext.c uses, so the
* compile matrix can check that file in an Apple shape. */
#ifndef STUB_CF_H
#define STUB_CF_H
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>
typedef long CFIndex;
typedef unsigned short UniChar;
typedef const void *CFTypeRef;
typedef const struct __CFString *CFStringRef;
typedef const struct __CFDictionary *CFDictionaryRef;
typedef const struct __CFAllocator *CFAllocatorRef;
typedef const struct __CFBoolean *CFBooleanRef;
typedef const struct __CFAttributedString *CFAttributedStringRef;
typedef struct { int v; } CFDictionaryKeyCallBacks;
typedef struct { int v; } CFDictionaryValueCallBacks;
extern const CFBooleanRef kCFBooleanTrue;
extern const CFDictionaryKeyCallBacks kCFTypeDictionaryKeyCallBacks;
extern const CFDictionaryValueCallBacks kCFTypeDictionaryValueCallBacks;
void CFRelease(CFTypeRef);
CFTypeRef CFRetain(CFTypeRef);
CFStringRef CFStringCreateWithCharacters(CFAllocatorRef, const UniChar *, CFIndex);
CFDictionaryRef CFDictionaryCreate(CFAllocatorRef, const void **, const void **, CFIndex, const CFDictionaryKeyCallBacks *, const CFDictionaryValueCallBacks *);
CFAttributedStringRef CFAttributedStringCreate(CFAllocatorRef, CFStringRef, CFDictionaryRef);
#endif
@@ -0,0 +1,2 @@
/* iOS spelling of the same stand-in */
#include <ApplicationServices/ApplicationServices.h>
@@ -0,0 +1,2 @@
/* iOS spelling of the same stand-in */
#include <ApplicationServices/ApplicationServices.h>
@@ -51,4 +51,8 @@ void *framebufferBegin(Framebuffer *fb, u32 *out_stride);
void framebufferEnd(Framebuffer *fb);
void framebufferClose(Framebuffer *fb);
/* the kernel random source the crypto's random path draws on */
void randomGet(void *buf, size_t len);
#endif
@@ -0,0 +1,636 @@
/* Generated by wayland-scanner 1.22.0 */
#ifndef WEBOS_FOREIGN_CLIENT_PROTOCOL_H
#define WEBOS_FOREIGN_CLIENT_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include "wayland-client.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @page page_webos_foreign The webos_foreign protocol
* @section page_ifaces_webos_foreign Interfaces
* - @subpage page_iface_wl_webos_foreign - interface for handling foreign surfaces
* - @subpage page_iface_wl_webos_exported - interface for handling exported window
* - @subpage page_iface_wl_webos_imported - interface for imported content or punchthrough region
* @section page_copyright_webos_foreign Copyright
* <pre>
*
* Copyright (c) 2018-2019 LG Electronics, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* </pre>
*/
struct wl_region;
struct wl_surface;
struct wl_webos_exported;
struct wl_webos_foreign;
struct wl_webos_imported;
#ifndef WL_WEBOS_FOREIGN_INTERFACE
#define WL_WEBOS_FOREIGN_INTERFACE
/**
* @page page_iface_wl_webos_foreign wl_webos_foreign
* @section page_iface_wl_webos_foreign_desc Description
*
* This interface defines a way to associate one or more foreign surfaces from
* different clients or a punch-through region to a surface that acts the main
* window.
*
* You can set a region on the main window where a foreign surface or a
* punch-through region can be placed. It is called "exported" region or window
* in this protocol. Then you can request the compositor to punch through that
* region or attach another surface as an "imported" content.
* @section page_iface_wl_webos_foreign_api API
* See @ref iface_wl_webos_foreign.
*/
/**
* @defgroup iface_wl_webos_foreign The wl_webos_foreign interface
*
* This interface defines a way to associate one or more foreign surfaces from
* different clients or a punch-through region to a surface that acts the main
* window.
*
* You can set a region on the main window where a foreign surface or a
* punch-through region can be placed. It is called "exported" region or window
* in this protocol. Then you can request the compositor to punch through that
* region or attach another surface as an "imported" content.
*/
extern const struct wl_interface wl_webos_foreign_interface;
#endif
#ifndef WL_WEBOS_EXPORTED_INTERFACE
#define WL_WEBOS_EXPORTED_INTERFACE
/**
* @page page_iface_wl_webos_exported wl_webos_exported
* @section page_iface_wl_webos_exported_desc Description
*
* This interface defines operations and notifications on an exported window.
* @section page_iface_wl_webos_exported_api API
* See @ref iface_wl_webos_exported.
*/
/**
* @defgroup iface_wl_webos_exported The wl_webos_exported interface
*
* This interface defines operations and notifications on an exported window.
*/
extern const struct wl_interface wl_webos_exported_interface;
#endif
#ifndef WL_WEBOS_IMPORTED_INTERFACE
#define WL_WEBOS_IMPORTED_INTERFACE
/**
* @page page_iface_wl_webos_imported wl_webos_imported
* @section page_iface_wl_webos_imported_desc Description
*
* This interface defines operations and notifications on an imported object.
* @section page_iface_wl_webos_imported_api API
* See @ref iface_wl_webos_imported.
*/
/**
* @defgroup iface_wl_webos_imported The wl_webos_imported interface
*
* This interface defines operations and notifications on an imported object.
*/
extern const struct wl_interface wl_webos_imported_interface;
#endif
#ifndef WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_ENUM
#define WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_ENUM
/**
* @ingroup iface_wl_webos_foreign
* type of webos_exported
*
* This indicates the type of the content that the exported window can show.
*/
enum wl_webos_foreign_webos_exported_type {
/**
* exported window for video object
*/
WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_VIDEO_OBJECT = 0,
/**
* exported window for subtitle object
*/
WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_SUBTITLE_OBJECT = 1,
/**
* exported window for transparent object that does not need to punch through other windows
*/
WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_TRANSPARENT_OBJECT = 2,
/**
* exported window for opaque object that always needs to punch through other windows
*/
WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_OPAQUE_OBJECT = 3,
};
#endif /* WL_WEBOS_FOREIGN_WEBOS_EXPORTED_TYPE_ENUM */
#define WL_WEBOS_FOREIGN_DESTROY 0
#define WL_WEBOS_FOREIGN_EXPORT_ELEMENT 1
#define WL_WEBOS_FOREIGN_IMPORT_ELEMENT 2
/**
* @ingroup iface_wl_webos_foreign
*/
#define WL_WEBOS_FOREIGN_DESTROY_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_foreign
*/
#define WL_WEBOS_FOREIGN_EXPORT_ELEMENT_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_foreign
*/
#define WL_WEBOS_FOREIGN_IMPORT_ELEMENT_SINCE_VERSION 1
/** @ingroup iface_wl_webos_foreign */
static inline void
wl_webos_foreign_set_user_data(struct wl_webos_foreign *wl_webos_foreign, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_foreign, user_data);
}
/** @ingroup iface_wl_webos_foreign */
static inline void *
wl_webos_foreign_get_user_data(struct wl_webos_foreign *wl_webos_foreign)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_foreign);
}
static inline uint32_t
wl_webos_foreign_get_version(struct wl_webos_foreign *wl_webos_foreign)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_foreign);
}
/**
* @ingroup iface_wl_webos_foreign
*
* Destroys the foreign object.
*/
static inline void
wl_webos_foreign_destroy(struct wl_webos_foreign *wl_webos_foreign)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_foreign,
WL_WEBOS_FOREIGN_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_foreign), WL_MARSHAL_FLAG_DESTROY);
}
/**
* @ingroup iface_wl_webos_foreign
*
* Requests to create an exported window on a given surface.
*/
static inline struct wl_webos_exported *
wl_webos_foreign_export_element(struct wl_webos_foreign *wl_webos_foreign, struct wl_surface *surface, uint32_t exported_type)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_foreign,
WL_WEBOS_FOREIGN_EXPORT_ELEMENT, &wl_webos_exported_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_foreign), 0, NULL, surface, exported_type);
return (struct wl_webos_exported *) id;
}
/**
* @ingroup iface_wl_webos_foreign
*
* Requests an import object that can be associated with a given exported window.
*/
static inline struct wl_webos_imported *
wl_webos_foreign_import_element(struct wl_webos_foreign *wl_webos_foreign, const char *window_id, uint32_t exported_type)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_foreign,
WL_WEBOS_FOREIGN_IMPORT_ELEMENT, &wl_webos_imported_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_foreign), 0, NULL, window_id, exported_type);
return (struct wl_webos_imported *) id;
}
/**
* @ingroup iface_wl_webos_exported
* @struct wl_webos_exported_listener
*/
struct wl_webos_exported_listener {
/**
* window is associated with this wl_webos_exported
*
* Notifies window_id of the exported window.
* @param window_id window_id of associated exported window
*/
void (*window_id_assigned)(void *data,
struct wl_webos_exported *wl_webos_exported,
const char *window_id,
uint32_t exported_type);
};
/**
* @ingroup iface_wl_webos_exported
*/
static inline int
wl_webos_exported_add_listener(struct wl_webos_exported *wl_webos_exported,
const struct wl_webos_exported_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_exported,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_EXPORTED_DESTROY 0
#define WL_WEBOS_EXPORTED_SET_EXPORTED_WINDOW 1
#define WL_WEBOS_EXPORTED_SET_CROP_REGION 2
#define WL_WEBOS_EXPORTED_SET_PROPERTY 3
/**
* @ingroup iface_wl_webos_exported
*/
#define WL_WEBOS_EXPORTED_WINDOW_ID_ASSIGNED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_exported
*/
#define WL_WEBOS_EXPORTED_DESTROY_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_exported
*/
#define WL_WEBOS_EXPORTED_SET_EXPORTED_WINDOW_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_exported
*/
#define WL_WEBOS_EXPORTED_SET_CROP_REGION_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_exported
*/
#define WL_WEBOS_EXPORTED_SET_PROPERTY_SINCE_VERSION 2
/** @ingroup iface_wl_webos_exported */
static inline void
wl_webos_exported_set_user_data(struct wl_webos_exported *wl_webos_exported, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_exported, user_data);
}
/** @ingroup iface_wl_webos_exported */
static inline void *
wl_webos_exported_get_user_data(struct wl_webos_exported *wl_webos_exported)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_exported);
}
static inline uint32_t
wl_webos_exported_get_version(struct wl_webos_exported *wl_webos_exported)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_exported);
}
/**
* @ingroup iface_wl_webos_exported
*
* Destroys the exported object.
*/
static inline void
wl_webos_exported_destroy(struct wl_webos_exported *wl_webos_exported)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_exported,
WL_WEBOS_EXPORTED_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_exported), WL_MARSHAL_FLAG_DESTROY);
}
/**
* @ingroup iface_wl_webos_exported
*
* Sets the exported window with source and destination regions.
*/
static inline void
wl_webos_exported_set_exported_window(struct wl_webos_exported *wl_webos_exported, struct wl_region *source_region, struct wl_region *destination_region)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_exported,
WL_WEBOS_EXPORTED_SET_EXPORTED_WINDOW, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_exported), 0, source_region, destination_region);
}
/**
* @ingroup iface_wl_webos_exported
*
* Sets a region to be cropped.
*/
static inline void
wl_webos_exported_set_crop_region(struct wl_webos_exported *wl_webos_exported, struct wl_region *original_input, struct wl_region *source_region, struct wl_region *destination_region)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_exported,
WL_WEBOS_EXPORTED_SET_CROP_REGION, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_exported), 0, original_input, source_region, destination_region);
}
/**
* @ingroup iface_wl_webos_exported
*
* Sets a property for this webos_exported. The property is given by a string pair (name, value).
*/
static inline void
wl_webos_exported_set_property(struct wl_webos_exported *wl_webos_exported, const char *name, const char *value)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_exported,
WL_WEBOS_EXPORTED_SET_PROPERTY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_exported), 0, name, value);
}
#ifndef WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_ENUM
#define WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_ENUM
/**
* @ingroup iface_wl_webos_imported
* surface alignment
*
* The alignment of an imported surface on the associated exported window.
*/
enum wl_webos_imported_surface_alignment {
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_STRETCH = 0,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_NORTH = 1,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_WEST = 2,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_SOUTH = 4,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_EAST = 8,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_CENTER = 16,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_CROP = 32,
WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_FIT = 64,
};
#endif /* WL_WEBOS_IMPORTED_SURFACE_ALIGNMENT_ENUM */
/**
* @ingroup iface_wl_webos_imported
* @struct wl_webos_imported_listener
*/
struct wl_webos_imported_listener {
/**
* destination region changed
*
* Notifies the change of the destination region of associated
* exported window.
* @param width destination width of exported window
* @param height destination height of exported window
*/
void (*destination_region_changed)(void *data,
struct wl_webos_imported *wl_webos_imported,
uint32_t width,
uint32_t height);
/**
* punchthrough attached
*
* Notifies that a punchthrough is attached to webos_imported.
* @since 2
*/
void (*punchthrough_attached)(void *data,
struct wl_webos_imported *wl_webos_imported,
const char *context_id);
/**
* punchthrough detached
*
* Notifies that a punchthrough is detached from webos_imported.
* @since 2
*/
void (*punchthrough_detached)(void *data,
struct wl_webos_imported *wl_webos_imported,
const char *context_id);
/**
* surface attached
*
* Notifies that a surface is attached to webos_imported.
* @since 2
*/
void (*surface_attached)(void *data,
struct wl_webos_imported *wl_webos_imported,
struct wl_surface *surface);
/**
* surface detached
*
* Notifies that a surface is detached from webos_imported.
* @since 2
*/
void (*surface_detached)(void *data,
struct wl_webos_imported *wl_webos_imported,
struct wl_surface *surface);
};
/**
* @ingroup iface_wl_webos_imported
*/
static inline int
wl_webos_imported_add_listener(struct wl_webos_imported *wl_webos_imported,
const struct wl_webos_imported_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_imported,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_IMPORTED_SET_SURFACE_ALIGNMENT 0
#define WL_WEBOS_IMPORTED_DESTROY 1
#define WL_WEBOS_IMPORTED_ATTACH_PUNCHTHROUGH 2
#define WL_WEBOS_IMPORTED_SET_PUNCHTHROUGH 3
#define WL_WEBOS_IMPORTED_DETACH_PUNCHTHROUGH 4
#define WL_WEBOS_IMPORTED_ATTACH_SURFACE 5
#define WL_WEBOS_IMPORTED_DETACH_SURFACE 6
#define WL_WEBOS_IMPORTED_ATTACH_PUNCHTHROUGH_WITH_CONTEXT 7
#define WL_WEBOS_IMPORTED_SET_Z_INDEX 8
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_DESTINATION_REGION_CHANGED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_PUNCHTHROUGH_ATTACHED_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_PUNCHTHROUGH_DETACHED_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_SURFACE_ATTACHED_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_SURFACE_DETACHED_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_SET_SURFACE_ALIGNMENT_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_DESTROY_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_ATTACH_PUNCHTHROUGH_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_SET_PUNCHTHROUGH_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_DETACH_PUNCHTHROUGH_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_ATTACH_SURFACE_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_DETACH_SURFACE_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_ATTACH_PUNCHTHROUGH_WITH_CONTEXT_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_imported
*/
#define WL_WEBOS_IMPORTED_SET_Z_INDEX_SINCE_VERSION 2
/** @ingroup iface_wl_webos_imported */
static inline void
wl_webos_imported_set_user_data(struct wl_webos_imported *wl_webos_imported, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_imported, user_data);
}
/** @ingroup iface_wl_webos_imported */
static inline void *
wl_webos_imported_get_user_data(struct wl_webos_imported *wl_webos_imported)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_imported);
}
static inline uint32_t
wl_webos_imported_get_version(struct wl_webos_imported *wl_webos_imported)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_imported);
}
/**
* @ingroup iface_wl_webos_imported
*
* Sets the alignment of the imported surface.
*/
static inline void
wl_webos_imported_set_surface_alignment(struct wl_webos_imported *wl_webos_imported, uint32_t format)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_SET_SURFACE_ALIGNMENT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0, format);
}
/**
* @ingroup iface_wl_webos_imported
*
* Destroys the imported object.
*/
static inline void
wl_webos_imported_destroy(struct wl_webos_imported *wl_webos_imported)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), WL_MARSHAL_FLAG_DESTROY);
}
/**
* @ingroup iface_wl_webos_imported
*
* Requests a punch-through on the exporetd window associated with
* this import object.
*/
static inline void
wl_webos_imported_attach_punchthrough(struct wl_webos_imported *wl_webos_imported)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_ATTACH_PUNCHTHROUGH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0);
}
/**
* @ingroup iface_wl_webos_imported
*/
static inline void
wl_webos_imported_set_punchthrough(struct wl_webos_imported *wl_webos_imported, const char *context_id)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_SET_PUNCHTHROUGH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0, context_id);
}
/**
* @ingroup iface_wl_webos_imported
*
* Requests to detatch the punch-through from the exported window
* associated with this import object.
*/
static inline void
wl_webos_imported_detach_punchthrough(struct wl_webos_imported *wl_webos_imported)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_DETACH_PUNCHTHROUGH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0);
}
/**
* @ingroup iface_wl_webos_imported
*
* Requests to attach the surface as imported to the associated
* exported window.
*/
static inline void
wl_webos_imported_attach_surface(struct wl_webos_imported *wl_webos_imported, struct wl_surface *surface)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_ATTACH_SURFACE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0, surface);
}
/**
* @ingroup iface_wl_webos_imported
*
* Requests to detach the surface as imported from the associated
* exported window.
*/
static inline void
wl_webos_imported_detach_surface(struct wl_webos_imported *wl_webos_imported, struct wl_surface *surface)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_DETACH_SURFACE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0, surface);
}
/**
* @ingroup iface_wl_webos_imported
*
* Requests a punch-through on the exporetd window associated with
* this import object for a given context id.
*/
static inline void
wl_webos_imported_attach_punchthrough_with_context(struct wl_webos_imported *wl_webos_imported, const char *context_id)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_ATTACH_PUNCHTHROUGH_WITH_CONTEXT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0, context_id);
}
/**
* @ingroup iface_wl_webos_imported
*
* Set the z index for webos_imported. The compositor will move the surface,
* that is attached to webos_imported, to this layer. If no surface is attached to
* webos_imported, nothing will happen.
*/
static inline void
wl_webos_imported_set_z_index(struct wl_webos_imported *wl_webos_imported, int32_t z_index)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_imported,
WL_WEBOS_IMPORTED_SET_Z_INDEX, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_imported), 0, z_index);
}
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,530 @@
/* Generated by wayland-scanner 1.22.0 */
#ifndef WEBOS_INPUT_MANAGER_CLIENT_PROTOCOL_H
#define WEBOS_INPUT_MANAGER_CLIENT_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include "wayland-client.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @page page_webos_input_manager The webos_input_manager protocol
* @section page_ifaces_webos_input_manager Interfaces
* - @subpage page_iface_wl_webos_input_manager - get additional input interface
* - @subpage page_iface_wl_webos_seat - interface for additional input data
* - @subpage page_iface_wl_webos_gyroscope - interface for gyroscope device
* - @subpage page_iface_wl_webos_accelerometer - interface for accelerometer device
* @section page_copyright_webos_input_manager Copyright
* <pre>
*
* Copyright (c) 2014-2020 LG Electronics, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* </pre>
*/
struct wl_seat;
struct wl_webos_accelerometer;
struct wl_webos_gyroscope;
struct wl_webos_input_manager;
struct wl_webos_seat;
#ifndef WL_WEBOS_INPUT_MANAGER_INTERFACE
#define WL_WEBOS_INPUT_MANAGER_INTERFACE
/**
* @page page_iface_wl_webos_input_manager wl_webos_input_manager
* @section page_iface_wl_webos_input_manager_desc Description
*
* wl_webos_seat has more information than wl_seat. User can get it using
* get_webos_seat(seat).
* @section page_iface_wl_webos_input_manager_api API
* See @ref iface_wl_webos_input_manager.
*/
/**
* @defgroup iface_wl_webos_input_manager The wl_webos_input_manager interface
*
* wl_webos_seat has more information than wl_seat. User can get it using
* get_webos_seat(seat).
*/
extern const struct wl_interface wl_webos_input_manager_interface;
#endif
#ifndef WL_WEBOS_SEAT_INTERFACE
#define WL_WEBOS_SEAT_INTERFACE
/**
* @page page_iface_wl_webos_seat wl_webos_seat
* @section page_iface_wl_webos_seat_desc Description
*
* This is a webOS extension for additional input data such as gyroscope or
* accelerometer. If the seat has additional input data, user can request it
* using get_*.
* @section page_iface_wl_webos_seat_api API
* See @ref iface_wl_webos_seat.
*/
/**
* @defgroup iface_wl_webos_seat The wl_webos_seat interface
*
* This is a webOS extension for additional input data such as gyroscope or
* accelerometer. If the seat has additional input data, user can request it
* using get_*.
*/
extern const struct wl_interface wl_webos_seat_interface;
#endif
#ifndef WL_WEBOS_GYROSCOPE_INTERFACE
#define WL_WEBOS_GYROSCOPE_INTERFACE
/**
* @page page_iface_wl_webos_gyroscope wl_webos_gyroscope
* @section page_iface_wl_webos_gyroscope_desc Description
*
* This is an interface for gyroscope device.
* @section page_iface_wl_webos_gyroscope_api API
* See @ref iface_wl_webos_gyroscope.
*/
/**
* @defgroup iface_wl_webos_gyroscope The wl_webos_gyroscope interface
*
* This is an interface for gyroscope device.
*/
extern const struct wl_interface wl_webos_gyroscope_interface;
#endif
#ifndef WL_WEBOS_ACCELEROMETER_INTERFACE
#define WL_WEBOS_ACCELEROMETER_INTERFACE
/**
* @page page_iface_wl_webos_accelerometer wl_webos_accelerometer
* @section page_iface_wl_webos_accelerometer_desc Description
*
* This is an interface for accelerometer device.
* @section page_iface_wl_webos_accelerometer_api API
* See @ref iface_wl_webos_accelerometer.
*/
/**
* @defgroup iface_wl_webos_accelerometer The wl_webos_accelerometer interface
*
* This is an interface for accelerometer device.
*/
extern const struct wl_interface wl_webos_accelerometer_interface;
#endif
/**
* @ingroup iface_wl_webos_input_manager
* @struct wl_webos_input_manager_listener
*/
struct wl_webos_input_manager_listener {
/**
* current cursor visibility
*
* If the current cursor visibility is chagned, notify it.
* @param visibility boolean vaule for visibility of current cursor
* @param webos_seat webos_seat cursor visibility changed, if this value is Null All cursors are hidden
*/
void (*cursor_visibility)(void *data,
struct wl_webos_input_manager *wl_webos_input_manager,
uint32_t visibility,
struct wl_webos_seat *webos_seat);
};
/**
* @ingroup iface_wl_webos_input_manager
*/
static inline int
wl_webos_input_manager_add_listener(struct wl_webos_input_manager *wl_webos_input_manager,
const struct wl_webos_input_manager_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_input_manager,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_INPUT_MANAGER_GET_WEBOS_SEAT 0
#define WL_WEBOS_INPUT_MANAGER_SET_CURSOR_VISIBILITY 1
/**
* @ingroup iface_wl_webos_input_manager
*/
#define WL_WEBOS_INPUT_MANAGER_CURSOR_VISIBILITY_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_input_manager
*/
#define WL_WEBOS_INPUT_MANAGER_GET_WEBOS_SEAT_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_input_manager
*/
#define WL_WEBOS_INPUT_MANAGER_SET_CURSOR_VISIBILITY_SINCE_VERSION 1
/** @ingroup iface_wl_webos_input_manager */
static inline void
wl_webos_input_manager_set_user_data(struct wl_webos_input_manager *wl_webos_input_manager, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_input_manager, user_data);
}
/** @ingroup iface_wl_webos_input_manager */
static inline void *
wl_webos_input_manager_get_user_data(struct wl_webos_input_manager *wl_webos_input_manager)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_input_manager);
}
static inline uint32_t
wl_webos_input_manager_get_version(struct wl_webos_input_manager *wl_webos_input_manager)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_input_manager);
}
/** @ingroup iface_wl_webos_input_manager */
static inline void
wl_webos_input_manager_destroy(struct wl_webos_input_manager *wl_webos_input_manager)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_input_manager);
}
/**
* @ingroup iface_wl_webos_input_manager
*
* Returns a webos_seat associated with the seat.
*/
static inline struct wl_webos_seat *
wl_webos_input_manager_get_webos_seat(struct wl_webos_input_manager *wl_webos_input_manager, struct wl_seat *seat)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_input_manager,
WL_WEBOS_INPUT_MANAGER_GET_WEBOS_SEAT, &wl_webos_seat_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_input_manager), 0, NULL, seat);
return (struct wl_webos_seat *) id;
}
/**
* @ingroup iface_wl_webos_input_manager
*
* Sets the cursor visibility.
*/
static inline void
wl_webos_input_manager_set_cursor_visibility(struct wl_webos_input_manager *wl_webos_input_manager, uint32_t visibility)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_input_manager,
WL_WEBOS_INPUT_MANAGER_SET_CURSOR_VISIBILITY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_input_manager), 0, visibility);
}
#ifndef WL_WEBOS_SEAT_CAPABILITY_ENUM
#define WL_WEBOS_SEAT_CAPABILITY_ENUM
/**
* @ingroup iface_wl_webos_seat
* webos seat capability bitmask
*
* This is a bitmask of capabilities this webOS seat has; if a member is
* set, then it is present on the seat.
*/
enum wl_webos_seat_capability {
/**
* this seat does not provide additional input
*/
WL_WEBOS_SEAT_CAPABILITY_DEFAULT = 0,
/**
* this seat can provide gyroscope data, wl_webos_gyroscope
*/
WL_WEBOS_SEAT_CAPABILITY_GYROSCOPE = 1,
/**
* this seat can provide accelerometer data, wl_webos_accelerometer
*/
WL_WEBOS_SEAT_CAPABILITY_ACCELEROMETER = 2,
};
#endif /* WL_WEBOS_SEAT_CAPABILITY_ENUM */
/**
* @ingroup iface_wl_webos_seat
* @struct wl_webos_seat_listener
*/
struct wl_webos_seat_listener {
/**
* info about the seat
*
* This event will be sent immediately after the client as
* requested the webOS seat from wl_webos_input_manager.
*
* The designator [0, 1, ... N] indicates if this is the
* primary(=0), secondary, etc seat.
* @param id an unique identifier for the seat
* @param name a human readable name for the seat
* @param designator a value for the designation of this seat
*/
void (*info)(void *data,
struct wl_webos_seat *wl_webos_seat,
uint32_t id,
const char *name,
uint32_t designator,
uint32_t capabilities);
};
/**
* @ingroup iface_wl_webos_seat
*/
static inline int
wl_webos_seat_add_listener(struct wl_webos_seat *wl_webos_seat,
const struct wl_webos_seat_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_seat,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_SEAT_GET_GYROSCOPE 0
#define WL_WEBOS_SEAT_GET_ACCELEROMETER 1
/**
* @ingroup iface_wl_webos_seat
*/
#define WL_WEBOS_SEAT_INFO_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_seat
*/
#define WL_WEBOS_SEAT_GET_GYROSCOPE_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_seat
*/
#define WL_WEBOS_SEAT_GET_ACCELEROMETER_SINCE_VERSION 1
/** @ingroup iface_wl_webos_seat */
static inline void
wl_webos_seat_set_user_data(struct wl_webos_seat *wl_webos_seat, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_seat, user_data);
}
/** @ingroup iface_wl_webos_seat */
static inline void *
wl_webos_seat_get_user_data(struct wl_webos_seat *wl_webos_seat)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_seat);
}
static inline uint32_t
wl_webos_seat_get_version(struct wl_webos_seat *wl_webos_seat)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_seat);
}
/** @ingroup iface_wl_webos_seat */
static inline void
wl_webos_seat_destroy(struct wl_webos_seat *wl_webos_seat)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_seat);
}
/**
* @ingroup iface_wl_webos_seat
*
* If the seat has gyroscope sensor, user can get this interface.
*/
static inline struct wl_webos_gyroscope *
wl_webos_seat_get_gyroscope(struct wl_webos_seat *wl_webos_seat)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_seat,
WL_WEBOS_SEAT_GET_GYROSCOPE, &wl_webos_gyroscope_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_seat), 0, NULL);
return (struct wl_webos_gyroscope *) id;
}
/**
* @ingroup iface_wl_webos_seat
*
* If the seat has accelerometer sensor, user can get this interface.
*/
static inline struct wl_webos_accelerometer *
wl_webos_seat_get_accelerometer(struct wl_webos_seat *wl_webos_seat)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_seat,
WL_WEBOS_SEAT_GET_ACCELEROMETER, &wl_webos_accelerometer_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_seat), 0, NULL);
return (struct wl_webos_accelerometer *) id;
}
/**
* @ingroup iface_wl_webos_gyroscope
* @struct wl_webos_gyroscope_listener
*/
struct wl_webos_gyroscope_listener {
/**
* sensor data from the seat
*
* The gyroscope sensor data will be delivered only if the client
* explicitly requested it via "requeest_data".
* @param x data for x-axis
* @param y data for y-axis
* @param z data for z-axis
*/
void (*sensor_data)(void *data,
struct wl_webos_gyroscope *wl_webos_gyroscope,
wl_fixed_t x,
wl_fixed_t y,
wl_fixed_t z);
};
/**
* @ingroup iface_wl_webos_gyroscope
*/
static inline int
wl_webos_gyroscope_add_listener(struct wl_webos_gyroscope *wl_webos_gyroscope,
const struct wl_webos_gyroscope_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_gyroscope,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_GYROSCOPE_REQUEST_DATA 0
/**
* @ingroup iface_wl_webos_gyroscope
*/
#define WL_WEBOS_GYROSCOPE_SENSOR_DATA_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_gyroscope
*/
#define WL_WEBOS_GYROSCOPE_REQUEST_DATA_SINCE_VERSION 1
/** @ingroup iface_wl_webos_gyroscope */
static inline void
wl_webos_gyroscope_set_user_data(struct wl_webos_gyroscope *wl_webos_gyroscope, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_gyroscope, user_data);
}
/** @ingroup iface_wl_webos_gyroscope */
static inline void *
wl_webos_gyroscope_get_user_data(struct wl_webos_gyroscope *wl_webos_gyroscope)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_gyroscope);
}
static inline uint32_t
wl_webos_gyroscope_get_version(struct wl_webos_gyroscope *wl_webos_gyroscope)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_gyroscope);
}
/** @ingroup iface_wl_webos_gyroscope */
static inline void
wl_webos_gyroscope_destroy(struct wl_webos_gyroscope *wl_webos_gyroscope)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_gyroscope);
}
/**
* @ingroup iface_wl_webos_gyroscope
*
* Requests to enable/disable sending data for gyroscope device.
*/
static inline void
wl_webos_gyroscope_request_data(struct wl_webos_gyroscope *wl_webos_gyroscope, uint32_t enabled)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_gyroscope,
WL_WEBOS_GYROSCOPE_REQUEST_DATA, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_gyroscope), 0, enabled);
}
/**
* @ingroup iface_wl_webos_accelerometer
* @struct wl_webos_accelerometer_listener
*/
struct wl_webos_accelerometer_listener {
/**
* sensor data from the seat
*
* The accelerometer sensor data will be delivered only if the
* client explicitly requested it via "request_data".
* @param x data for x-axis
* @param y data for y-axis
* @param z data for z-axis
*/
void (*sensor_data)(void *data,
struct wl_webos_accelerometer *wl_webos_accelerometer,
wl_fixed_t x,
wl_fixed_t y,
wl_fixed_t z);
};
/**
* @ingroup iface_wl_webos_accelerometer
*/
static inline int
wl_webos_accelerometer_add_listener(struct wl_webos_accelerometer *wl_webos_accelerometer,
const struct wl_webos_accelerometer_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_accelerometer,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_ACCELEROMETER_REQUEST_DATA 0
/**
* @ingroup iface_wl_webos_accelerometer
*/
#define WL_WEBOS_ACCELEROMETER_SENSOR_DATA_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_accelerometer
*/
#define WL_WEBOS_ACCELEROMETER_REQUEST_DATA_SINCE_VERSION 1
/** @ingroup iface_wl_webos_accelerometer */
static inline void
wl_webos_accelerometer_set_user_data(struct wl_webos_accelerometer *wl_webos_accelerometer, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_accelerometer, user_data);
}
/** @ingroup iface_wl_webos_accelerometer */
static inline void *
wl_webos_accelerometer_get_user_data(struct wl_webos_accelerometer *wl_webos_accelerometer)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_accelerometer);
}
static inline uint32_t
wl_webos_accelerometer_get_version(struct wl_webos_accelerometer *wl_webos_accelerometer)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_accelerometer);
}
/** @ingroup iface_wl_webos_accelerometer */
static inline void
wl_webos_accelerometer_destroy(struct wl_webos_accelerometer *wl_webos_accelerometer)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_accelerometer);
}
/**
* @ingroup iface_wl_webos_accelerometer
*
* Requests to enable/disable sending data for accelerometer device.
*/
static inline void
wl_webos_accelerometer_request_data(struct wl_webos_accelerometer *wl_webos_accelerometer, uint32_t enabled)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_accelerometer,
WL_WEBOS_ACCELEROMETER_REQUEST_DATA, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_accelerometer), 0, enabled);
}
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,506 @@
/* Generated by wayland-scanner 1.22.0 */
#ifndef WEBOS_SHELL_CLIENT_PROTOCOL_H
#define WEBOS_SHELL_CLIENT_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include "wayland-client.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @page page_webos_shell The webos_shell protocol
* @section page_ifaces_webos_shell Interfaces
* - @subpage page_iface_wl_webos_shell - interface for webOS shell
* - @subpage page_iface_wl_webos_shell_surface - interface to handle webos shell surfaces
* @section page_copyright_webos_shell Copyright
* <pre>
*
* Copyright (c) 2013-2020 LG Electronics, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* </pre>
*/
struct wl_surface;
struct wl_webos_shell;
struct wl_webos_shell_surface;
#ifndef WL_WEBOS_SHELL_INTERFACE
#define WL_WEBOS_SHELL_INTERFACE
/**
* @page page_iface_wl_webos_shell wl_webos_shell
* @section page_iface_wl_webos_shell_desc Description
*
* This interface provides handlings on webOS shell surfaces.
* @section page_iface_wl_webos_shell_api API
* See @ref iface_wl_webos_shell.
*/
/**
* @defgroup iface_wl_webos_shell The wl_webos_shell interface
*
* This interface provides handlings on webOS shell surfaces.
*/
extern const struct wl_interface wl_webos_shell_interface;
#endif
#ifndef WL_WEBOS_SHELL_SURFACE_INTERFACE
#define WL_WEBOS_SHELL_SURFACE_INTERFACE
/**
* @page page_iface_wl_webos_shell_surface wl_webos_shell_surface
* @section page_iface_wl_webos_shell_surface_desc Description
*
* This allows the client to associate an normal surface with a
* shell surface. The shell surface provides webOS specific functionality
* to surfaces.
* @section page_iface_wl_webos_shell_surface_api API
* See @ref iface_wl_webos_shell_surface.
*/
/**
* @defgroup iface_wl_webos_shell_surface The wl_webos_shell_surface interface
*
* This allows the client to associate an normal surface with a
* shell surface. The shell surface provides webOS specific functionality
* to surfaces.
*/
extern const struct wl_interface wl_webos_shell_surface_interface;
#endif
#define WL_WEBOS_SHELL_GET_SYSTEM_PIP 0
#define WL_WEBOS_SHELL_GET_SHELL_SURFACE 1
/**
* @ingroup iface_wl_webos_shell
*/
#define WL_WEBOS_SHELL_GET_SYSTEM_PIP_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell
*/
#define WL_WEBOS_SHELL_GET_SHELL_SURFACE_SINCE_VERSION 1
/** @ingroup iface_wl_webos_shell */
static inline void
wl_webos_shell_set_user_data(struct wl_webos_shell *wl_webos_shell, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_shell, user_data);
}
/** @ingroup iface_wl_webos_shell */
static inline void *
wl_webos_shell_get_user_data(struct wl_webos_shell *wl_webos_shell)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_shell);
}
static inline uint32_t
wl_webos_shell_get_version(struct wl_webos_shell *wl_webos_shell)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_shell);
}
/** @ingroup iface_wl_webos_shell */
static inline void
wl_webos_shell_destroy(struct wl_webos_shell *wl_webos_shell)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_shell);
}
/**
* @ingroup iface_wl_webos_shell
*
* This request is deprecated. If you want to create a new request,
* you can reuse this one. FYI, in that case, you also need to modify
* get_system_pip method in luna-surfacemanager as well.
*/
static inline void
wl_webos_shell_get_system_pip(struct wl_webos_shell *wl_webos_shell)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell,
WL_WEBOS_SHELL_GET_SYSTEM_PIP, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell), 0);
}
/**
* @ingroup iface_wl_webos_shell
*
* Creates an webOS shell surface for an existing surface.
*/
static inline struct wl_webos_shell_surface *
wl_webos_shell_get_shell_surface(struct wl_webos_shell *wl_webos_shell, struct wl_surface *surface)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell,
WL_WEBOS_SHELL_GET_SHELL_SURFACE, &wl_webos_shell_surface_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell), 0, NULL, surface);
return (struct wl_webos_shell_surface *) id;
}
#ifndef WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_ENUM
#define WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_ENUM
/**
* @ingroup iface_wl_webos_shell_surface
* location mask for the surface
*
* Using this mask the surface can be positioned onto
* different locations on screen. By not using absolute
* coordinates we can create greater flexibility in the
* compositor and the clients.
*/
enum wl_webos_shell_surface_location_hint {
WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_NORTH = 1,
WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_WEST = 2,
WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_SOUTH = 4,
WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_EAST = 8,
WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_CENTER = 16,
};
#endif /* WL_WEBOS_SHELL_SURFACE_LOCATION_HINT_ENUM */
#ifndef WL_WEBOS_SHELL_SURFACE_STATE_ENUM
#define WL_WEBOS_SHELL_SURFACE_STATE_ENUM
/**
* @ingroup iface_wl_webos_shell_surface
* the state of the surface
*
* The state provides info to the client on how the compositor has placed
* the surface.
*
* The default state will indicate to the client that it is windowed. The
* "position_changed" event will tell the position in screen coordinates
* when the surface is in this state.
*/
enum wl_webos_shell_surface_state {
WL_WEBOS_SHELL_SURFACE_STATE_DEFAULT = 0,
WL_WEBOS_SHELL_SURFACE_STATE_MINIMIZED = 1,
WL_WEBOS_SHELL_SURFACE_STATE_MAXIMIZED = 2,
WL_WEBOS_SHELL_SURFACE_STATE_FULLSCREEN = 3,
};
#endif /* WL_WEBOS_SHELL_SURFACE_STATE_ENUM */
#ifndef WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_ENUM
#define WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_ENUM
/**
* @ingroup iface_wl_webos_shell_surface
* the key that the surface handles
*
* The client uses wl_webos_shell_surface.set_key_mask to notify server
* that it will handle specific keys only.
*/
enum wl_webos_shell_surface_webos_key {
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_HOME = 1,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_BACK = 2,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_EXIT = 4,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_NAVIGATION_LEFT = 8,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_NAVIGATION_RIGHT = 16,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_NAVIGATION_UP = 32,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_NAVIGATION_DOWN = 64,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_NAVIGATION_OK = 128,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_NUMERIC_KEYS = 256,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_COLOR_RED = 512,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_COLOR_GREEN = 1024,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_COLOR_YELLOW = 2048,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_COLOR_BLUE = 4096,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_PROGRAMME_GROUP = 8192,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_PLAYBACK_GROUP = 16384,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_TELETEXT_GROUP = 32768,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_LOCAL_LEFT = 65536,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_LOCAL_RIGHT = 131072,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_LOCAL_UP = 262144,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_LOCAL_DOWN = 524288,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_LOCAL_OK = 1048576,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_REMOTE_MAGNIFIER_GROUP = 2097152,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_MINIMAL_PLAYBACK_GROUP = 4194304,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_GUIDE = 8388608,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_TELETEXT_ACTIVE_GROUP = 16777216,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_DATA = 33554432,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_INFO = 67108864,
WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_DEFAULT = 0xFFFFFFF8,
};
#endif /* WL_WEBOS_SHELL_SURFACE_WEBOS_KEY_ENUM */
#ifndef WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_ENUM
#define WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_ENUM
/**
* @ingroup iface_wl_webos_shell_surface
* the status of the add-on
*
* Status of the add-on for the given shell surface.
* null: neither server-side add-on set nor loaded
* loaded: the server-side add-on is loaded and visible
* denied: the server-side add-on is not allowed to load
* error: the server-side add-on is not loaded due to an error
*/
enum wl_webos_shell_surface_addon_status {
WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_NULL = 0,
WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_LOADED = 1,
WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_DENIED = 2,
WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_ERROR = 3,
};
#endif /* WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_ENUM */
/**
* @ingroup iface_wl_webos_shell_surface
* @struct wl_webos_shell_surface_listener
*/
struct wl_webos_shell_surface_listener {
/**
* the surface state was changed
*
* The compositor or the user has taken action that has resulted
* in this surface to change state.
*/
void (*state_changed)(void *data,
struct wl_webos_shell_surface *wl_webos_shell_surface,
uint32_t state);
/**
* the surface position on screen changed
*
* This event will only be sent the surfaces that are in their
* default state. It will be not sent for fullscreen surfaces nor
* for surfaces that are minimized.
*/
void (*position_changed)(void *data,
struct wl_webos_shell_surface *wl_webos_shell_surface,
int32_t x,
int32_t y);
/**
* request closing of window
*
* The close event is sent when the compositor requests closing
* of a window, e.g. through some gesture or other interaction,
* without explicitly quitting or killing the client.
*/
void (*close)(void *data,
struct wl_webos_shell_surface *wl_webos_shell_surface);
/**
* indicates what areas are exposed
*
* The array contains non-overlapping rectangles that define
* areas that are visible in this surface. The array elements are a
* series of ints arranged as follows x,y,w,h,x,y,w,h,-1. The
* values will not contain negative numbers and the -1 is treated
* as a terminating marker.
*/
void (*exposed)(void *data,
struct wl_webos_shell_surface *wl_webos_shell_surface,
struct wl_array *rectangles);
/**
* the surface state is about to change
*
* Be sure that the actual state should not be changed with this.
* Client can only prepare that state will be changed.
*/
void (*state_about_to_change)(void *data,
struct wl_webos_shell_surface *wl_webos_shell_surface,
uint32_t state);
/**
* notify the add-on status when changed
*
* Notifies when the add-on status changes.
* @since 2
*/
void (*addon_status_changed)(void *data,
struct wl_webos_shell_surface *wl_webos_shell_surface,
uint32_t addon_status);
};
/**
* @ingroup iface_wl_webos_shell_surface
*/
static inline int
wl_webos_shell_surface_add_listener(struct wl_webos_shell_surface *wl_webos_shell_surface,
const struct wl_webos_shell_surface_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_shell_surface,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_SHELL_SURFACE_SET_LOCATION_HINT 0
#define WL_WEBOS_SHELL_SURFACE_SET_STATE 1
#define WL_WEBOS_SHELL_SURFACE_SET_PROPERTY 2
#define WL_WEBOS_SHELL_SURFACE_SET_KEY_MASK 3
#define WL_WEBOS_SHELL_SURFACE_SET_ADDON 4
#define WL_WEBOS_SHELL_SURFACE_RESET_ADDON 5
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_STATE_CHANGED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_POSITION_CHANGED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_CLOSE_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_EXPOSED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_STATE_ABOUT_TO_CHANGE_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_ADDON_STATUS_CHANGED_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_SET_LOCATION_HINT_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_SET_STATE_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_SET_PROPERTY_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_SET_KEY_MASK_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_SET_ADDON_SINCE_VERSION 2
/**
* @ingroup iface_wl_webos_shell_surface
*/
#define WL_WEBOS_SHELL_SURFACE_RESET_ADDON_SINCE_VERSION 2
/** @ingroup iface_wl_webos_shell_surface */
static inline void
wl_webos_shell_surface_set_user_data(struct wl_webos_shell_surface *wl_webos_shell_surface, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_shell_surface, user_data);
}
/** @ingroup iface_wl_webos_shell_surface */
static inline void *
wl_webos_shell_surface_get_user_data(struct wl_webos_shell_surface *wl_webos_shell_surface)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_shell_surface);
}
static inline uint32_t
wl_webos_shell_surface_get_version(struct wl_webos_shell_surface *wl_webos_shell_surface)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface);
}
/** @ingroup iface_wl_webos_shell_surface */
static inline void
wl_webos_shell_surface_destroy(struct wl_webos_shell_surface *wl_webos_shell_surface)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_shell_surface);
}
/**
* @ingroup iface_wl_webos_shell_surface
*
* The location hint is a guideline to the compositor where
* to place the surface. The compositor will do its best to
* place it there, however this is not guaranteed. Currently
* The location hint is only applied to _WEBOS_WINDOW_TYPE_POPUP
* and _WEBOS_WINDOW_TYPE_SYSTEM_UI surfaces.
*/
static inline void
wl_webos_shell_surface_set_location_hint(struct wl_webos_shell_surface *wl_webos_shell_surface, uint32_t hint)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell_surface,
WL_WEBOS_SHELL_SURFACE_SET_LOCATION_HINT, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface), 0, hint);
}
/**
* @ingroup iface_wl_webos_shell_surface
*
* The client requests its surface state to the compositor.
*/
static inline void
wl_webos_shell_surface_set_state(struct wl_webos_shell_surface *wl_webos_shell_surface, uint32_t state)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell_surface,
WL_WEBOS_SHELL_SURFACE_SET_STATE, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface), 0, state);
}
/**
* @ingroup iface_wl_webos_shell_surface
*
* The client sets the surface property to the compositor. The property
* is given by a string pair (name, value).
*/
static inline void
wl_webos_shell_surface_set_property(struct wl_webos_shell_surface *wl_webos_shell_surface, const char *name, const char *value)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell_surface,
WL_WEBOS_SHELL_SURFACE_SET_PROPERTY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface), 0, name, value);
}
/**
* @ingroup iface_wl_webos_shell_surface
*
* A value of '1' in the mask indicates that the respective key event (both press and release)
* will be delivered to the client. Bitwise XOR operation against the default mask value and
* a supported individual values will result in that key not being delivered to the client.
* Notice that the default value omits certain keys.
*
* The compositor is free to ignore certain keys even if set to '1' depending on the overall
* system UI behaviour, most commonly this will mean the HOME key.
*
* NOTE: This protocol will eventually replace the string property based key filtering.
*/
static inline void
wl_webos_shell_surface_set_key_mask(struct wl_webos_shell_surface *wl_webos_shell_surface, uint32_t webos_key)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell_surface,
WL_WEBOS_SHELL_SURFACE_SET_KEY_MASK, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface), 0, webos_key);
}
/**
* @ingroup iface_wl_webos_shell_surface
*
* Sets the path to the server-side add-on. The compositor loads the add-on
* on receiving this request if the add-on is identified as a valid one.
*/
static inline void
wl_webos_shell_surface_set_addon(struct wl_webos_shell_surface *wl_webos_shell_surface, const char *path)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell_surface,
WL_WEBOS_SHELL_SURFACE_SET_ADDON, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface), 0, path);
}
/**
* @ingroup iface_wl_webos_shell_surface
*
* Requests to unload the add-on if it is loaded.
* If there is no add-on loaded this request is silently ignored.
*/
static inline void
wl_webos_shell_surface_reset_addon(struct wl_webos_shell_surface *wl_webos_shell_surface)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_shell_surface,
WL_WEBOS_SHELL_SURFACE_RESET_ADDON, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_shell_surface), 0);
}
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,597 @@
/* Generated by wayland-scanner 1.22.0 */
#ifndef WEBOS_SURFACE_GROUP_CLIENT_PROTOCOL_H
#define WEBOS_SURFACE_GROUP_CLIENT_PROTOCOL_H
#include <stdint.h>
#include <stddef.h>
#include "wayland-client.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @page page_webos_surface_group The webos_surface_group protocol
* @section page_ifaces_webos_surface_group Interfaces
* - @subpage page_iface_wl_webos_surface_group_compositor - a way to group multi client surfaces together
* - @subpage page_iface_wl_webos_surface_group -
* - @subpage page_iface_wl_webos_surface_group_layer - the handle for the group owner to manipulate the layer
* @section page_copyright_webos_surface_group Copyright
* <pre>
*
* Copyright (c) 2014-2020 LG Electronics, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* </pre>
*/
struct wl_surface;
struct wl_webos_surface_group;
struct wl_webos_surface_group_compositor;
struct wl_webos_surface_group_layer;
#ifndef WL_WEBOS_SURFACE_GROUP_COMPOSITOR_INTERFACE
#define WL_WEBOS_SURFACE_GROUP_COMPOSITOR_INTERFACE
/**
* @page page_iface_wl_webos_surface_group_compositor wl_webos_surface_group_compositor
* @section page_iface_wl_webos_surface_group_compositor_desc Description
*
* A surface group is a mechanism to group surfaces together that are
* not part of the same client, hence transiency, nor subsurfaces will
* fit the bill. However the compositor will treat surfaces belonging
* to a group very similarly as to surfaces that are transient.
*
* The group will be controlled by the process that creates it. The group
* will have a root surface and the grouped surfaces will be added as
* layers on top or below it. The creator of the group will designate a z order
* and a name for each layer. Other processes wanting to add their
* surface into the group need to know the both the name of the group and
* the name of the layer.
*
* A group can allow anonymous surfaces to be attached to them as well.
*
* Practical suggestion, the name of the group should match the appId of
* parent surface.
* @section page_iface_wl_webos_surface_group_compositor_api API
* See @ref iface_wl_webos_surface_group_compositor.
*/
/**
* @defgroup iface_wl_webos_surface_group_compositor The wl_webos_surface_group_compositor interface
*
* A surface group is a mechanism to group surfaces together that are
* not part of the same client, hence transiency, nor subsurfaces will
* fit the bill. However the compositor will treat surfaces belonging
* to a group very similarly as to surfaces that are transient.
*
* The group will be controlled by the process that creates it. The group
* will have a root surface and the grouped surfaces will be added as
* layers on top or below it. The creator of the group will designate a z order
* and a name for each layer. Other processes wanting to add their
* surface into the group need to know the both the name of the group and
* the name of the layer.
*
* A group can allow anonymous surfaces to be attached to them as well.
*
* Practical suggestion, the name of the group should match the appId of
* parent surface.
*/
extern const struct wl_interface wl_webos_surface_group_compositor_interface;
#endif
#ifndef WL_WEBOS_SURFACE_GROUP_INTERFACE
#define WL_WEBOS_SURFACE_GROUP_INTERFACE
/**
* @page page_iface_wl_webos_surface_group wl_webos_surface_group
* @section page_iface_wl_webos_surface_group_api API
* See @ref iface_wl_webos_surface_group.
*/
/**
* @defgroup iface_wl_webos_surface_group The wl_webos_surface_group interface
*/
extern const struct wl_interface wl_webos_surface_group_interface;
#endif
#ifndef WL_WEBOS_SURFACE_GROUP_LAYER_INTERFACE
#define WL_WEBOS_SURFACE_GROUP_LAYER_INTERFACE
/**
* @page page_iface_wl_webos_surface_group_layer wl_webos_surface_group_layer
* @section page_iface_wl_webos_surface_group_layer_desc Description
*
* The handle can be used to change the z order of a particular layer
* @section page_iface_wl_webos_surface_group_layer_api API
* See @ref iface_wl_webos_surface_group_layer.
*/
/**
* @defgroup iface_wl_webos_surface_group_layer The wl_webos_surface_group_layer interface
*
* The handle can be used to change the z order of a particular layer
*/
extern const struct wl_interface wl_webos_surface_group_layer_interface;
#endif
#define WL_WEBOS_SURFACE_GROUP_COMPOSITOR_CREATE_SURFACE_GROUP 0
#define WL_WEBOS_SURFACE_GROUP_COMPOSITOR_GET_SURFACE_GROUP 1
/**
* @ingroup iface_wl_webos_surface_group_compositor
*/
#define WL_WEBOS_SURFACE_GROUP_COMPOSITOR_CREATE_SURFACE_GROUP_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group_compositor
*/
#define WL_WEBOS_SURFACE_GROUP_COMPOSITOR_GET_SURFACE_GROUP_SINCE_VERSION 1
/** @ingroup iface_wl_webos_surface_group_compositor */
static inline void
wl_webos_surface_group_compositor_set_user_data(struct wl_webos_surface_group_compositor *wl_webos_surface_group_compositor, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_surface_group_compositor, user_data);
}
/** @ingroup iface_wl_webos_surface_group_compositor */
static inline void *
wl_webos_surface_group_compositor_get_user_data(struct wl_webos_surface_group_compositor *wl_webos_surface_group_compositor)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_surface_group_compositor);
}
static inline uint32_t
wl_webos_surface_group_compositor_get_version(struct wl_webos_surface_group_compositor *wl_webos_surface_group_compositor)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_compositor);
}
/** @ingroup iface_wl_webos_surface_group_compositor */
static inline void
wl_webos_surface_group_compositor_destroy(struct wl_webos_surface_group_compositor *wl_webos_surface_group_compositor)
{
wl_proxy_destroy((struct wl_proxy *) wl_webos_surface_group_compositor);
}
/**
* @ingroup iface_wl_webos_surface_group_compositor
*
* The group will be identified by the 'name' parameter. If a group already
* exists with the given name an error will be reported to the client.
*/
static inline struct wl_webos_surface_group *
wl_webos_surface_group_compositor_create_surface_group(struct wl_webos_surface_group_compositor *wl_webos_surface_group_compositor, struct wl_surface *parent, const char *name)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group_compositor,
WL_WEBOS_SURFACE_GROUP_COMPOSITOR_CREATE_SURFACE_GROUP, &wl_webos_surface_group_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_compositor), 0, NULL, parent, name);
return (struct wl_webos_surface_group *) id;
}
/**
* @ingroup iface_wl_webos_surface_group_compositor
*
* A handle to an existing group.
*/
static inline struct wl_webos_surface_group *
wl_webos_surface_group_compositor_get_surface_group(struct wl_webos_surface_group_compositor *wl_webos_surface_group_compositor, const char *name)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group_compositor,
WL_WEBOS_SURFACE_GROUP_COMPOSITOR_GET_SURFACE_GROUP, &wl_webos_surface_group_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_compositor), 0, NULL, name);
return (struct wl_webos_surface_group *) id;
}
#ifndef WL_WEBOS_SURFACE_GROUP_Z_HINT_ENUM
#define WL_WEBOS_SURFACE_GROUP_Z_HINT_ENUM
/**
* @ingroup iface_wl_webos_surface_group
* hint for the z position of the anonymous surfaces
*
* The position of anonymous layer is always relative to the root element
* and the named layers in this group.
*
* If there are multiple entries with the same index designator they will
* get positioned in the order of which they get attached.
*/
enum wl_webos_surface_group_z_hint {
/**
* below the root surface and the lowest named layer
*/
WL_WEBOS_SURFACE_GROUP_Z_HINT_BELOW = 0,
/**
* above the root surface and above the highest named layer
*/
WL_WEBOS_SURFACE_GROUP_Z_HINT_ABOVE = 1,
/**
* above all of the anonymous surfaces
*/
WL_WEBOS_SURFACE_GROUP_Z_HINT_TOP = 2,
};
#endif /* WL_WEBOS_SURFACE_GROUP_Z_HINT_ENUM */
/**
* @ingroup iface_wl_webos_surface_group
* @struct wl_webos_surface_group_listener
*/
struct wl_webos_surface_group_listener {
/**
* sent to clients attached to this group
*
* If the owner crashes or normally destroys this group the
* attached client will receive this notification.
*
* Since compositor does not know what to do with client surface
* still attached to this group it will not show them. A well
* behaving client will 'detach' from this group and release its
* assosicated resource
*/
void (*owner_destroyed)(void *data,
struct wl_webos_surface_group *wl_webos_surface_group);
};
/**
* @ingroup iface_wl_webos_surface_group
*/
static inline int
wl_webos_surface_group_add_listener(struct wl_webos_surface_group *wl_webos_surface_group,
const struct wl_webos_surface_group_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_surface_group,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_SURFACE_GROUP_CREATE_LAYER 0
#define WL_WEBOS_SURFACE_GROUP_ATTACH 1
#define WL_WEBOS_SURFACE_GROUP_ATTACH_ANONYMOUS 2
#define WL_WEBOS_SURFACE_GROUP_ALLOW_ANONYMOUS_LAYERS 3
#define WL_WEBOS_SURFACE_GROUP_DETACH 4
#define WL_WEBOS_SURFACE_GROUP_DESTROY 5
#define WL_WEBOS_SURFACE_GROUP_FOCUS_OWNER 6
#define WL_WEBOS_SURFACE_GROUP_FOCUS_LAYER 7
#define WL_WEBOS_SURFACE_GROUP_COMMIT_KEY_INDEX 8
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_OWNER_DESTROYED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_CREATE_LAYER_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_ATTACH_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_ATTACH_ANONYMOUS_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_ALLOW_ANONYMOUS_LAYERS_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_DETACH_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_DESTROY_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_FOCUS_OWNER_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_FOCUS_LAYER_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group
*/
#define WL_WEBOS_SURFACE_GROUP_COMMIT_KEY_INDEX_SINCE_VERSION 1
/** @ingroup iface_wl_webos_surface_group */
static inline void
wl_webos_surface_group_set_user_data(struct wl_webos_surface_group *wl_webos_surface_group, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_surface_group, user_data);
}
/** @ingroup iface_wl_webos_surface_group */
static inline void *
wl_webos_surface_group_get_user_data(struct wl_webos_surface_group *wl_webos_surface_group)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_surface_group);
}
static inline uint32_t
wl_webos_surface_group_get_version(struct wl_webos_surface_group *wl_webos_surface_group)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* The layer will be identified by the 'name' parameter. The compositor
* will place the layer in the given z-index relative to the root layer.
* If negative z-indecies are provided they will be placed underneath the
* root surface accordingly
*
* The creator will get a handle 'wl_webos_surface_group_layer' to the
* newly created group. The handle can be used to manipulate the layer.
*
* If a layer group already exists with the given parameters an error
* will be reported to the client.
*/
static inline struct wl_webos_surface_group_layer *
wl_webos_surface_group_create_layer(struct wl_webos_surface_group *wl_webos_surface_group, const char *name, int32_t z_index)
{
struct wl_proxy *id;
id = wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_CREATE_LAYER, &wl_webos_surface_group_layer_interface, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, NULL, name, z_index);
return (struct wl_webos_surface_group_layer *) id;
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Processes that want to attach a surface to an existing group and layer
* will call this function.
*
* An error will be reported if either the group, the layer or the layer
* already has a surface attached to it.
*/
static inline void
wl_webos_surface_group_attach(struct wl_webos_surface_group *wl_webos_surface_group, struct wl_surface *surface, const char *layer_name)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_ATTACH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, surface, layer_name);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Clients that have a "multi-purpose" surface that might apply to many use
* cases can try to attach their surface to a named group. The group controls
* if that is allowed or not.
*
* An error will be reported if the group is not available or it does not
* allow anonymous surfaces
*/
static inline void
wl_webos_surface_group_attach_anonymous(struct wl_webos_surface_group *wl_webos_surface_group, struct wl_surface *surface, uint32_t z_hint)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_ATTACH_ANONYMOUS, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, surface, z_hint);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Anonymous layers are layers that do not belong to any named layer.
* They do not enjoy the same benefits as named layers. Their actucal
* z-index might change if there are named layers or a z index of a
* named layer is updated.
*/
static inline void
wl_webos_surface_group_allow_anonymous_layers(struct wl_webos_surface_group *wl_webos_surface_group, uint32_t allow)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_ALLOW_ANONYMOUS_LAYERS, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, allow);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* When a client detaches from a group the compositor will treat is as any other surface.
* In practice this means that if the client does not want the surface to appear "dangling"
* or visible in recents the client should hide/destroy the surface, then detach it from
* the group to avoid any unwanted behavior.
*/
static inline void
wl_webos_surface_group_detach(struct wl_webos_surface_group *wl_webos_surface_group, struct wl_surface *surface)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_DETACH, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, surface);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Destroys the surface group object.
*/
static inline void
wl_webos_surface_group_destroy(struct wl_webos_surface_group *wl_webos_surface_group)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), WL_MARSHAL_FLAG_DESTROY);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Using "focus_owner" a surface-group-client or a surface-group-owner can transfer
* keybord-focus to surface-group-owner.
*/
static inline void
wl_webos_surface_group_focus_owner(struct wl_webos_surface_group *wl_webos_surface_group)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_FOCUS_OWNER, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Using "focus_layer" a surface-group-owner or a surface-group-client can transfer
* keybord-focus to specific surface-group-client attached to surface-group-layer "layer".
*/
static inline void
wl_webos_surface_group_focus_layer(struct wl_webos_surface_group *wl_webos_surface_group, const char *layer)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_FOCUS_LAYER, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, layer);
}
/**
* @ingroup iface_wl_webos_surface_group
*
* Using "commit_key_index", the compositor will send key events in key index order
* regardless of z-order in the surface group.
* Key index set to each layer becomes effective after "commit_key_index" request.
* Parameter 'commit' should be 1 when applying the key index order.
* Set parameter 'commit' to 0 not to use the key index order.
* If anonymous layer is allowed, the key index order doesn't work.
*/
static inline void
wl_webos_surface_group_commit_key_index(struct wl_webos_surface_group *wl_webos_surface_group, uint32_t commit)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group,
WL_WEBOS_SURFACE_GROUP_COMMIT_KEY_INDEX, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group), 0, commit);
}
/**
* @ingroup iface_wl_webos_surface_group_layer
* @struct wl_webos_surface_group_layer_listener
*/
struct wl_webos_surface_group_layer_listener {
/**
* notify surface-group-owner that surface is attached to this layer
*
* When any surafce is attached to this layer, the compositor
* needs to notify surface-group-owner.
*/
void (*surface_attached)(void *data,
struct wl_webos_surface_group_layer *wl_webos_surface_group_layer);
/**
* notify surface-group-owner that surface is detached from this layer
*
* When any surafce is detached from this layer, the compositor
* needs to notify surface-group-owner. This will be usefull if
* surface attached to this layer is destroyed by associated
* surface-group-client.
*/
void (*surface_detached)(void *data,
struct wl_webos_surface_group_layer *wl_webos_surface_group_layer);
};
/**
* @ingroup iface_wl_webos_surface_group_layer
*/
static inline int
wl_webos_surface_group_layer_add_listener(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer,
const struct wl_webos_surface_group_layer_listener *listener, void *data)
{
return wl_proxy_add_listener((struct wl_proxy *) wl_webos_surface_group_layer,
(void (**)(void)) listener, data);
}
#define WL_WEBOS_SURFACE_GROUP_LAYER_SET_Z_INDEX 0
#define WL_WEBOS_SURFACE_GROUP_LAYER_SET_KEY_INDEX 1
#define WL_WEBOS_SURFACE_GROUP_LAYER_DESTROY 2
/**
* @ingroup iface_wl_webos_surface_group_layer
*/
#define WL_WEBOS_SURFACE_GROUP_LAYER_SURFACE_ATTACHED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group_layer
*/
#define WL_WEBOS_SURFACE_GROUP_LAYER_SURFACE_DETACHED_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group_layer
*/
#define WL_WEBOS_SURFACE_GROUP_LAYER_SET_Z_INDEX_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group_layer
*/
#define WL_WEBOS_SURFACE_GROUP_LAYER_SET_KEY_INDEX_SINCE_VERSION 1
/**
* @ingroup iface_wl_webos_surface_group_layer
*/
#define WL_WEBOS_SURFACE_GROUP_LAYER_DESTROY_SINCE_VERSION 1
/** @ingroup iface_wl_webos_surface_group_layer */
static inline void
wl_webos_surface_group_layer_set_user_data(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer, void *user_data)
{
wl_proxy_set_user_data((struct wl_proxy *) wl_webos_surface_group_layer, user_data);
}
/** @ingroup iface_wl_webos_surface_group_layer */
static inline void *
wl_webos_surface_group_layer_get_user_data(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer)
{
return wl_proxy_get_user_data((struct wl_proxy *) wl_webos_surface_group_layer);
}
static inline uint32_t
wl_webos_surface_group_layer_get_version(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer)
{
return wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_layer);
}
/**
* @ingroup iface_wl_webos_surface_group_layer
*
* Updates the z index for this layer. The compositor will move the surface
* attached to this layer accordingly. If no surface is attached to this
* layer nothing will happen
*/
static inline void
wl_webos_surface_group_layer_set_z_index(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer, int32_t z_index)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group_layer,
WL_WEBOS_SURFACE_GROUP_LAYER_SET_Z_INDEX, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_layer), 0, z_index);
}
/**
* @ingroup iface_wl_webos_surface_group_layer
*
* Sets the key index for this layer. The compositor will send key events
* to this layer accordingly after "commit_key_index" request.
* If no surface is attached to this layer nothing will happen.
* Key index should be neither redundant nor 0 (0 is onwner's key index).
* The larger the key index, the faster the layer receives the key event.
*/
static inline void
wl_webos_surface_group_layer_set_key_index(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer, int32_t key_index)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group_layer,
WL_WEBOS_SURFACE_GROUP_LAYER_SET_KEY_INDEX, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_layer), 0, key_index);
}
/**
* @ingroup iface_wl_webos_surface_group_layer
*
* Destroys the surface group layer object.
*/
static inline void
wl_webos_surface_group_layer_destroy(struct wl_webos_surface_group_layer *wl_webos_surface_group_layer)
{
wl_proxy_marshal_flags((struct wl_proxy *) wl_webos_surface_group_layer,
WL_WEBOS_SURFACE_GROUP_LAYER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wl_webos_surface_group_layer), WL_MARSHAL_FLAG_DESTROY);
}
#ifdef __cplusplus
}
#endif
#endif
+28 -1
View File
@@ -86,7 +86,7 @@ apple/RetroArch_iOS.xcodeproj/project.xcworkspace/*
.cproject
.settings
libretro-super
run.sh
/run.sh
convert_rumble.awk
*~
assets
@@ -266,21 +266,41 @@ gfx/common/wayland/webos-surface-group.c
gfx/common/wayland/webos-surface-group.h
gfx/common/wayland/tearing-control-v1.h
gfx/common/wayland/tearing-control-v1.c
gfx/common/wayland/drm-lease-v1.h
gfx/common/wayland/drm-lease-v1.c
# samples/: built harness executables and objects. Each directory's
# test is <dir>_test; nothing built there is source.
samples/**/*_test
samples/**/*_test_*
samples/audio/stretch/stretch_bench
samples/**/*.o
libretro-common/samples/**/*_test
libretro-common/samples/**/*_test_*
!libretro-common/samples/**/*_test/
!samples/**/*_test_*.c
!libretro-common/samples/**/*_test_*.c
!samples/**/*_test_*.h
!libretro-common/samples/**/*_test_*.h
# Harness executables that are not named <dir>_test. This one had been
# committed as a built binary, which made any make clean in that
# directory show up as a deletion.
samples/audio/embedded_alsa/abi_check
libretro-common/samples/rthreads/retro_eventcount_bench/retro_eventcount_bench
libretro-common/samples/rthreads/retro_eventcount_bench/retro_eventcount_bench_*
# The driver's text with its Windows includes swapped, produced by each
# suite's Makefile from audio/drivers/wasapi.c. Committed copies went
# stale against the driver they are generated from.
samples/audio/wasapi_exclusive_pacing/wasapi_sim.c
samples/audio/discrete_multichannel/wasapi_sim.c
samples/gfx/font_lifecycle/lifecycle
samples/gfx/font_lifecycle/lifecycle-threaded
samples/gfx/font_lifecycle/create
# Generated by samples/gfx/wayland_drm_lease from the bundled protocol
# XML; the client half of the same pair lands in gfx/common/wayland.
samples/gfx/wayland_drm_lease/drm-lease-v1-client-protocol.h
samples/gfx/wayland_drm_lease/drm-lease-v1-server-protocol.h
samples/gfx/wayland_drm_lease/drm-lease-v1-protocol.c
# libretro-common samples
libretro-common/samples/streams/rzip/rzip
@@ -319,3 +339,10 @@ gfx/drivers/rsx_shaders/modern_opaque.vpo
pkg/psl1ght/pkg/USRDIR/shaders_cg/
!pkg/psl1ght/pkg/USRDIR/system/
samples/audio/embedded_alsa/ealsa_abi.h
# Python bytecode caches, written by tools/*.py whenever they run.
__pycache__/
*.pyc
samples/audio/asio_clock/asio_clock_driver.h
samples/gfx/integer_scale_axis/integer_scale_driver.h
samples/gfx/integer_scale_axis/integer_scale_axis_test
+2 -2
View File
@@ -7,7 +7,7 @@ stages:
- trigger-static-cores
build-retroarch-windows-x64:
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win64-cross:gcc10
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win64-cross:gcc16
stage: build
variables:
MEDIA_PATH: .media
@@ -51,7 +51,7 @@ build-retroarch-windows-x64:
- "rm -f ${MEDIA_PATH}/${CI_PROJECT_NAME}/redist/opengl32.dll"
build-retroarch-windows-i686:
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win32-cross:gcc10
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win32-cross:gcc16
stage: build
variables:
MEDIA_PATH: .media
+17
View File
@@ -90,6 +90,23 @@
- MENU/XMB: Tab change possible by gestures
- MENU/XMB: Horizontal menu is now optional
- NETWORK: New network commands SAVE_STATE_SLOT N and GET_CONFIG_PARAM
- NETWORK: HELP network command lists every command with what it does
- NETWORK: LIST_CORES, LIST_PLAYLISTS and GET_PLAYLIST network commands, to find content and the core to load it with
- NETWORK: MCP server: AI assistants can drive RetroArch through the Model Context Protocol, every network command a tool (off by default, see docs/mcp-server.md)
- NETWORK: Built-in TLS 1.2/1.3 client; mbedTLS and BearSSL are no longer bundled (--enable-mbedtls / --enable-bearssl use a system one)
- NETWORK: Built-in SMB2/3 client with signing, AES-CCM/GCM encryption and Kerberos; libsmb2 is no longer bundled (--enable-libsmb uses a system one)
- NETWORK: Built-in NFSv3 and NFSv4 (4.0, 4.1, 4.2) client for loading content from nfs:// exports
- NETWORK: NFS files stay usable across a server restart, a dropped connection or a long pause: the client reconnects and reopens what it needs
- 3DS/VITA/SWITCH/WIIU: nfs:// content through the built-in NFS client
- 3DS/VITA/SWITCH: smb:// content through the built-in SMB client
- WINDOWS/MSVC: the Visual Studio 2005 - 2017 projects build the TLS client, the keychain, SMB and NFS
- NETWORK: SMB files stay usable across a server restart: the connection is mended and the file opened again where it was
- NETWORK: NFSv4.2 reads use READ_PLUS: holes in sparse files no longer cross the network
- 3DS/SWITCH: nfs:// content through the built-in NFS client
- NETWORK: Optional SMB/NFS read-ahead with background prefetch, for large disc images over a slow link (off by default)
- NETWORK: Saved passwords are encrypted in retroarch-keychain.cfg; an optional passphrase lets the keychain move to another machine (see docs/network-stack.md)
- NETWORK: SMB 3.1.1 signs with AES-GMAC where the server offers it, several times faster than AES-CMAC on signed reads
- NETWORK: HTTPS downloads whose length is set by the server closing the connection now finish instead of hanging
- OVERLAY: Allow overlays to position the viewport
- OVERLAY: Hold button function
- OVERLAY: Allow interaction during next overlay swap
+10
View File
@@ -268,6 +268,16 @@ Debug builds report handlers that overrun a frame - see
task_queue_set_slow_handler_cb() - which is the cheapest way to find
out whether a handler you are adding respects any of this.
The waits that remain are the accepted ones, none of them reachable
while the menu is up or a core runs, each commented where it is:
* the command-line scan without a menu, which exits once done
(retroarch.c);
* the save/load state waits on exit, at startup and when an init
fails (content_wait_for_save_state_task() and
content_wait_for_load_state_task());
* the Wii U main loop's vsync wait (platform_wiiu.c).
Win32 file dialogs are modal by nature and stay so.
Commit message density
----------------------
Keep commit message short and appropriate for the size of the change. One or two
+15 -2
View File
@@ -10,6 +10,12 @@ endif
include config.mk
# The absolute include paths configure found on the system
# (/usr/local/include on the BSDs, /opt/vc/include, ...) are searched after
# the tree's own; the relative ones (gfx/include/ANGLE) are the tree's.
SYSTEM_INCLUDE_DIRS := $(filter -I/%,$(INCLUDE_DIRS))
INCLUDE_DIRS := $(filter-out -I/%,$(INCLUDE_DIRS))
# Put your favorite compile flags in this file, if you want different defaults than upstream.
# Do not attempt to create that file upstream.
# (It'd be better to put this comment in that file, but .gitignore doesn't work on files that exist in the repo.)
@@ -125,7 +131,11 @@ ifneq ($(CXX_BUILD), 1)
CFLAGS += -D_GNU_SOURCE
endif
DEF_FLAGS += $(INCLUDE_DIRS) -Ideps
# The tree's own headers come before any system or pkg-config path, so a
# same-named header installed on the system cannot stand in for one of
# ours.
DEF_FLAGS := -I. $(INCLUDE_DIRS) -Ideps $(filter-out -I.,$(DEF_FLAGS)) \
$(SYSTEM_INCLUDE_DIRS)
CFLAGS += $(DEF_FLAGS)
CXXFLAGS += $(DEF_FLAGS) -D__STDC_CONSTANT_MACROS
@@ -235,7 +245,10 @@ config.mk:
else
-include $(RARCH_OBJ:.o=.d)
ifeq ($(HAVE_CONFIG_MK),)
config.mk: configure qb/*
# The Wayland protocol sources are generated by configure, so a tree
# configured before a protocol was added has no way to make the new
# object: ask for the reconfigure rather than fail on a missing file.
config.mk: configure qb/* gfx/common/wayland/generate_wayland_protos.sh
@echo "config.mk is outdated or non-existing. Run ./configure again."
@exit 1
endif
+148 -301
View File
@@ -325,9 +325,12 @@ OBJ += \
audio/audio_pipeline_stretch.o \
audio/audio_binaural.o \
input/input_driver.o \
input/input_overlay_textures.o \
input/input_overlay_alpha.o \
input/common/input_hid_common.o \
led/led_driver.o \
gfx/video_driver.o \
gfx/common/video_mode_select.o \
gfx/gfx_display.o \
gfx/gfx_animation.o \
gfx/gfx_instrument.o \
@@ -400,6 +403,7 @@ DEFINES += -DHAVE_WINDOW_OFFSET
endif
OBJ += $(LIBRETRO_COMM_DIR)/audio/resampler/audio_resampler.o
OBJ += $(LIBRETRO_COMM_DIR)/audio/resampler/audio_resampler_int16.o
ifeq ($(HAVE_DSP_FILTER), 1)
DEFINES += -DHAVE_DSP_FILTER
@@ -428,8 +432,27 @@ OBJ += \
tasks/task_database.o \
disk_control_interface.o
# The cleanroom crypto library. Off unless a build asks for it: the
# round tables, curve constants and 4 KiB RSA working set are nothing
# for a desktop and too much for GameCube, Wii or PS2, whose Makefiles
# do not set it.
ifeq ($(HAVE_CRYPTO), 1)
DEFINES += -DHAVE_CRYPTO
OBJ += $(LIBRETRO_COMM_DIR)/crypto/crypto.o \
$(LIBRETRO_COMM_DIR)/crypto/kdf.o \
$(LIBRETRO_COMM_DIR)/crypto/pk.o \
$(LIBRETRO_COMM_DIR)/crypto/x509.o
endif
ifeq ($(HAVE_CONFIGFILE), 1)
DEFINES += -DHAVE_CONFIGFILE
ifeq ($(HAVE_CRYPTO), 1)
ifeq ($(HAVE_KEYCHAIN), 1)
DEFINES += -DHAVE_KEYCHAIN
OBJ += $(LIBRETRO_COMM_DIR)/file/keychain.o \
tasks/task_keychain.o
endif
endif
endif
ifeq ($(HAVE_BLISSBOX), 1)
@@ -472,9 +495,6 @@ ifeq ($(HAVE_CC_RESAMPLER), 1)
OBJ += audio/drivers_resampler/cc_resampler.o
OBJ += audio/drivers_resampler/cc_resampler_int16.o
ifeq ($(HAVE_NEON),1)
OBJ += audio/drivers_resampler/cc_resampler_neon.o
endif
endif
ifeq ($(HAVE_LANGEXTRA), 1)
@@ -578,199 +598,48 @@ ifeq ($(HAVE_LIBRETRODB), 1)
endif
endif
ifeq ($(HAVE_BUILTINBEARSSL), 1)
# TLS needs the sockets: a makefile that asks for a client without
# networking (a handheld with the crypto and keychain for the store
# but no network) gets neither the define nor the object.
ifneq ($(HAVE_NETWORKING), 1)
HAVE_SSL = 0
HAVE_RETROSSL = 0
HAVE_BEARSSL = 0
HAVE_MBEDTLS = 0
endif
# TLS: the built-in client by default; a system mbedTLS or BearSSL in
# its place when configure was asked for one. Nothing is bundled.
ifeq ($(HAVE_RETROSSL), 1)
HAVE_SSL = 1
DEFINES += -DHAVE_SSL -DHAVE_RETROSSL
else ifeq ($(HAVE_BEARSSL), 1)
HAVE_SSL = 1
DEFINES += -DHAVE_SSL -DHAVE_BEARSSL
# these -Is are only needed for BearSSL itself
INCLUDE_DIRS += -Ideps/bearssl-0.6/src -Ideps/bearssl-0.6/inc
OBJS_BEAR = deps/bearssl-0.6/src/aead/ccm.o \
deps/bearssl-0.6/src/codec/ccopy.o \
deps/bearssl-0.6/src/codec/dec32be.o \
deps/bearssl-0.6/src/codec/dec32le.o \
deps/bearssl-0.6/src/codec/dec64be.o \
deps/bearssl-0.6/src/codec/enc32be.o \
deps/bearssl-0.6/src/codec/enc32le.o \
deps/bearssl-0.6/src/codec/enc64be.o \
deps/bearssl-0.6/src/ec/ec_all_m31.o \
deps/bearssl-0.6/src/ec/ec_c25519_m31.o \
deps/bearssl-0.6/src/ec/ecdsa_atr.o \
deps/bearssl-0.6/src/ec/ecdsa_i31_bits.o \
deps/bearssl-0.6/src/ec/ecdsa_i31_vrfy_asn1.o \
deps/bearssl-0.6/src/ec/ecdsa_i31_vrfy_raw.o \
deps/bearssl-0.6/src/ec/ec_p256_m31.o \
deps/bearssl-0.6/src/ec/ec_prime_i31.o \
deps/bearssl-0.6/src/ec/ec_secp256r1.o \
deps/bearssl-0.6/src/ec/ec_secp384r1.o \
deps/bearssl-0.6/src/ec/ec_secp521r1.o \
deps/bearssl-0.6/src/hash/ghash_ctmul64.o \
deps/bearssl-0.6/src/hash/ghash_pclmul.o \
deps/bearssl-0.6/src/hash/md5.o \
deps/bearssl-0.6/src/hash/multihash.o \
deps/bearssl-0.6/src/hash/sha1.o \
deps/bearssl-0.6/src/hash/sha2big.o \
deps/bearssl-0.6/src/hash/sha2small.o \
deps/bearssl-0.6/src/int/i31_add.o \
deps/bearssl-0.6/src/int/i31_bitlen.o \
deps/bearssl-0.6/src/int/i31_decmod.o \
deps/bearssl-0.6/src/int/i31_decode.o \
deps/bearssl-0.6/src/int/i31_decred.o \
deps/bearssl-0.6/src/int/i31_encode.o \
deps/bearssl-0.6/src/int/i31_fmont.o \
deps/bearssl-0.6/src/int/i31_iszero.o \
deps/bearssl-0.6/src/int/i31_modpow2.o \
deps/bearssl-0.6/src/int/i31_modpow.o \
deps/bearssl-0.6/src/int/i31_montmul.o \
deps/bearssl-0.6/src/int/i31_muladd.o \
deps/bearssl-0.6/src/int/i31_ninv31.o \
deps/bearssl-0.6/src/int/i31_rshift.o \
deps/bearssl-0.6/src/int/i31_sub.o \
deps/bearssl-0.6/src/int/i31_tmont.o \
deps/bearssl-0.6/src/int/i32_div32.o \
deps/bearssl-0.6/src/int/i62_modpow2.o \
deps/bearssl-0.6/src/mac/hmac_ct.o \
deps/bearssl-0.6/src/mac/hmac.o \
deps/bearssl-0.6/src/rand/hmac_drbg.o \
deps/bearssl-0.6/src/rand/sysrng.o \
deps/bearssl-0.6/src/rsa/rsa_default_pkcs1_vrfy.o \
deps/bearssl-0.6/src/rsa/rsa_default_pub.o \
deps/bearssl-0.6/src/rsa/rsa_i31_pkcs1_vrfy.o \
deps/bearssl-0.6/src/rsa/rsa_i31_pub.o \
deps/bearssl-0.6/src/rsa/rsa_i62_pkcs1_vrfy.o \
deps/bearssl-0.6/src/rsa/rsa_i62_pub.o \
deps/bearssl-0.6/src/rsa/rsa_pkcs1_sig_unpad.o \
deps/bearssl-0.6/src/ssl/prf_md5sha1.o \
deps/bearssl-0.6/src/ssl/prf.o \
deps/bearssl-0.6/src/ssl/prf_sha256.o \
deps/bearssl-0.6/src/ssl/prf_sha384.o \
deps/bearssl-0.6/src/ssl/ssl_client_default_rsapub.o \
deps/bearssl-0.6/src/ssl/ssl_client_full.o \
deps/bearssl-0.6/src/ssl/ssl_client.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_aescbc.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_aesccm.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_aesgcm.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_chapol.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_descbc.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_ecdsa.o \
deps/bearssl-0.6/src/ssl/ssl_engine_default_rsavrfy.o \
deps/bearssl-0.6/src/ssl/ssl_engine.o \
deps/bearssl-0.6/src/ssl/ssl_hs_client.o \
deps/bearssl-0.6/src/ssl/ssl_rec_cbc.o \
deps/bearssl-0.6/src/ssl/ssl_rec_ccm.o \
deps/bearssl-0.6/src/ssl/ssl_rec_chapol.o \
deps/bearssl-0.6/src/ssl/ssl_rec_gcm.o \
deps/bearssl-0.6/src/symcipher/aes_ct64_cbcdec.o \
deps/bearssl-0.6/src/symcipher/aes_ct64_cbcenc.o \
deps/bearssl-0.6/src/symcipher/aes_ct64_ctrcbc.o \
deps/bearssl-0.6/src/symcipher/aes_ct64_ctr.o \
deps/bearssl-0.6/src/symcipher/aes_ct64_dec.o \
deps/bearssl-0.6/src/symcipher/aes_ct64_enc.o \
deps/bearssl-0.6/src/symcipher/aes_ct64.o \
deps/bearssl-0.6/src/symcipher/aes_x86ni_cbcdec.o \
deps/bearssl-0.6/src/symcipher/aes_x86ni_cbcenc.o \
deps/bearssl-0.6/src/symcipher/aes_x86ni_ctrcbc.o \
deps/bearssl-0.6/src/symcipher/aes_x86ni_ctr.o \
deps/bearssl-0.6/src/symcipher/aes_x86ni.o \
deps/bearssl-0.6/src/symcipher/chacha20_ct.o \
deps/bearssl-0.6/src/symcipher/chacha20_sse2.o \
deps/bearssl-0.6/src/symcipher/des_ct_cbcdec.o \
deps/bearssl-0.6/src/symcipher/des_ct_cbcenc.o \
deps/bearssl-0.6/src/symcipher/des_ct.o \
deps/bearssl-0.6/src/symcipher/des_support.o \
deps/bearssl-0.6/src/symcipher/poly1305_ctmul.o \
deps/bearssl-0.6/src/symcipher/poly1305_ctmulq.o \
deps/bearssl-0.6/src/x509/x509_decoder.o \
deps/bearssl-0.6/src/x509/x509_minimal_full.o \
deps/bearssl-0.6/src/x509/x509_minimal.o \
OBJ += $(OBJS_BEAR)
else ifeq ($(HAVE_BUILTINMBEDTLS), 1)
LIBS += $(if $(BEARSSL_LIBS),$(BEARSSL_LIBS),-lbearssl)
DEF_FLAGS += $(BEARSSL_CFLAGS)
else ifeq ($(HAVE_MBEDTLS), 1)
HAVE_SSL = 1
DEFINES += -DHAVE_SSL
ifeq ($(DEBUG), 1)
DEFINES += -DMBEDTLS_SSL_DEBUG_ALL
endif
# MinGW requires this for some reason,
# even though the include paths are relative to the source
INCLUDE_DIRS += -Ideps/mbedtls
OBJS_TLS_CRYPTO = deps/mbedtls/aes.o \
deps/mbedtls/aesni.o \
deps/mbedtls/arc4.o \
deps/mbedtls/asn1parse.o \
deps/mbedtls/asn1write.o \
deps/mbedtls/base64.o \
deps/mbedtls/bignum.o \
deps/mbedtls/blowfish.o \
deps/mbedtls/camellia.o \
deps/mbedtls/ccm.o \
deps/mbedtls/cipher.o \
deps/mbedtls/cipher_wrap.o \
deps/mbedtls/ctr_drbg.o \
deps/mbedtls/des.o \
deps/mbedtls/dhm.o \
deps/mbedtls/ecdh.o \
deps/mbedtls/ecdsa.o \
deps/mbedtls/ecp.o \
deps/mbedtls/ecp_curves.o \
deps/mbedtls/entropy.o \
deps/mbedtls/entropy_poll.o \
deps/mbedtls/gcm.o \
deps/mbedtls/hmac_drbg.o \
deps/mbedtls/md.o \
deps/mbedtls/md5.o \
deps/mbedtls/md_wrap.o \
deps/mbedtls/oid.o \
deps/mbedtls/padlock.o \
deps/mbedtls/pem.o \
deps/mbedtls/pk.o \
deps/mbedtls/pk_wrap.o \
deps/mbedtls/pkcs12.o \
deps/mbedtls/pkcs5.o \
deps/mbedtls/pkparse.o \
deps/mbedtls/pkwrite.o \
deps/mbedtls/ripemd160.o \
deps/mbedtls/rsa.o \
deps/mbedtls/sha1.o \
deps/mbedtls/sha_alt.o \
deps/mbedtls/sha256.o \
deps/mbedtls/sha512.o \
deps/mbedtls/threading.o \
deps/mbedtls/timing.o \
deps/mbedtls/xtea.o
OBJS_TLS_X509 = deps/mbedtls/certs.o \
deps/mbedtls/x509.o \
deps/mbedtls/x509_create.o \
deps/mbedtls/x509_crl.o \
deps/mbedtls/x509_crt.o \
deps/mbedtls/x509_csr.o \
deps/mbedtls/x509write_crt.o \
deps/mbedtls/x509write_csr.o
OBJS_TLS = deps/mbedtls/debug.o \
deps/mbedtls/net_sockets.o \
deps/mbedtls/ssl_cache.o \
deps/mbedtls/ssl_ciphersuites.o \
deps/mbedtls/ssl_cli.o \
deps/mbedtls/ssl_cookie.o \
deps/mbedtls/ssl_srv.o \
deps/mbedtls/ssl_ticket.o \
deps/mbedtls/ssl_tls.o
OBJ += $(OBJS_TLS_CRYPTO) $(OBJS_TLS_X509) $(OBJS_TLS)
else ifeq ($(HAVE_SSL), 1)
DEFINES += -DHAVE_SSL
LIBS += $(SYSTEMMBEDTLS_LIBS) $(SYSTEMMBEDX509_LIBS) $(SYSTEMMBEDCRYPTO_LIBS)
DEF_FLAGS += $(SYSTEMMBEDTLS_CFLAGS) $(SYSTEMMBEDX509_CFLAGS) $(SYSTEMMBEDCRYPTO_CFLAGS)
LIBS += $(MBEDTLS_LIBS) $(MBEDX509_LIBS) $(MBEDCRYPTO_LIBS)
DEF_FLAGS += $(MBEDTLS_CFLAGS) $(MBEDX509_CFLAGS) $(MBEDCRYPTO_CFLAGS)
endif
# Miscellaneous
ifeq ($(HAVE_COMMAND), 1)
DEFINES += -DHAVE_COMMAND
# The MCP server: desktop builds, through ./configure; no console
# Makefile sets HAVE_MCP.
ifeq ($(HAVE_NETWORKING), 1)
ifeq ($(HAVE_MCP), 1)
DEFINES += -DHAVE_MCP
OBJ += network/mcp_server.o
endif
endif
endif
ifeq ($(HAVE_STDIN_CMD), 1)
@@ -997,6 +866,7 @@ endif
OBJ += $(LIBRETRO_COMM_DIR)/audio/conversion/s16_to_float.o \
$(LIBRETRO_COMM_DIR)/audio/conversion/float_to_s16.o \
$(LIBRETRO_COMM_DIR)/audio/conversion/float_to_s32.o \
$(LIBRETRO_COMM_DIR)/audio/conversion/mono_to_stereo_float.o \
$(LIBRETRO_COMM_DIR)/audio/conversion/stereo_to_mono_float.o \
@@ -1223,7 +1093,19 @@ ifeq ($(HAVE_THREADS), 1)
$(LIBRETRO_COMM_DIR)/queues/retro_waitable_spsc.o \
gfx/video_thread_wrapper.o \
gfx/video_thread_hw.o \
audio/audio_thread_wrapper.o
audio/audio_thread_wrapper.o \
frontend/thread_elevation.o \
frontend/thread_elevation/eevdf.o
# RealtimeKit and screensaver inhibit over libdbus loaded at run
# time: no D-Bus headers or link dependency, just the library where
# the desktop has it.
ifneq ($(findstring Linux,$(OS))$(findstring BSD,$(OS)),)
OBJ += gfx/common/dbus_runtime.o \
gfx/common/dbus_common.o \
gfx/common/mutter_displayconfig.o \
frontend/thread_elevation/rtkit.o
endif
DEFINES += -DHAVE_THREADS
ifeq ($(findstring Haiku,$(OS)),)
LIBS += $(THREADS_LIBS)
@@ -1264,13 +1146,13 @@ endif
ifeq ($(TARGET), retroarch_orbis)
OBJ += input/drivers/ps4_input.o \
input/drivers_joypad/ps4_joypad.o \
audio/drivers/psp_audio.o
audio/drivers/ps4_audio.o
endif
ifeq ($(TARGET), retroarch_vita)
OBJ += input/drivers/psp_input.o \
input/drivers_joypad/psp_joypad.o \
audio/drivers/psp_audio.o \
audio/drivers/psp2_audio.o \
frontend/drivers/platform_psp.o
endif
@@ -1335,7 +1217,16 @@ ifeq ($(HAVE_WAYLAND), 1)
gfx/common/wayland/tablet-unstable-v2.o \
gfx/common/wayland/content-type-v1.o \
gfx/common/wayland/single-pixel-buffer-v1.o \
gfx/common/wayland/tearing-control-v1.o
gfx/common/wayland/tearing-control-v1.o \
gfx/common/wayland/color-management-v1.o \
gfx/common/wayland_color.o \
gfx/common/wayland/kde-output-device-v2.o \
gfx/common/wayland/kde-output-management-v2.o \
gfx/common/wayland_kwin_output.o \
gfx/common/wayland/wlr-output-management-unstable-v1.o \
gfx/common/wayland_wlr_output.o \
gfx/common/wayland/drm-lease-v1.o \
gfx/common/wayland_drm_lease.o
ifeq ($(HAVE_VULKAN), 1)
OBJ += gfx/drivers_context/wayland_vk_ctx.o
@@ -1407,7 +1298,7 @@ ifeq ($(HAVE_X11), 1)
DEFINES += -DHAVE_X11
DEF_FLAGS += $(X11_CFLAGS) $(XEXT_CFLAGS) $(XF86VM_CFLAGS) $(XINERAMA_CFLAGS)
ifeq ($(HAVE_XCB),1)
LIBS += -lX11-xcb
LIBS += $(X11_XCB_LIBS)
endif
ifeq ($(HAVE_XI2),1)
LIBS += -lXi
@@ -1420,7 +1311,10 @@ ifeq ($(HAVE_X11), 1)
OBJ += gfx/drivers_context/x_ctx.o
endif
ifeq ($(HAVE_VULKAN), 1)
OBJ += gfx/drivers_context/x_vk_ctx.o
# The X11 Vulkan context creates an XCB surface.
ifeq ($(HAVE_XCB), 1)
OBJ += gfx/drivers_context/x_vk_ctx.o
endif
endif
endif
@@ -1438,7 +1332,6 @@ endif
ifeq ($(HAVE_DBUS), 1)
LIBS += $(DBUS_LIBS)
DEF_FLAGS += $(DBUS_CFLAGS)
OBJ += gfx/common/dbus_common.o
endif
ifeq ($(HAVE_UDEV), 1)
@@ -1537,7 +1430,8 @@ OBJ += gfx/drivers_context/gfx_null_ctx.o
ifeq ($(HAVE_KMS), 1)
HAVE_AND_WILL_USE_DRM = 1
OBJ += gfx/drivers_context/drm_ctx.o \
gfx/display_servers/dispserv_kms.o
gfx/display_servers/dispserv_kms.o \
gfx/common/drm_hdr.o
ifeq ($(HAVE_ODROIDGO2), 1)
OBJ += gfx/drivers_context/drm_go2_ctx.o
@@ -1754,14 +1648,16 @@ else ifeq ($(HAVE_SDL2), 1)
HAVE_SDL_COMMON = 1
OBJ += gfx/drivers/sdl2_gfx.o \
gfx/common/sdl2_common.o \
gfx/display_servers/dispserv_sdl2.o
gfx/display_servers/dispserv_sdl2.o \
audio/drivers/sdl2_audio.o
DEF_FLAGS += $(SDL2_CFLAGS)
# See the SDL3 note above: drop SDL's WinMain shim lib, we set
# SDL_MAIN_HANDLED and provide our own entry point.
LIBS += $(filter-out -lSDL2main -lSDLmain -lSDL_main,$(SDL2_LIBS))
else ifeq ($(HAVE_SDL), 1)
HAVE_SDL_COMMON = 1
OBJ += gfx/drivers/sdl_gfx.o
OBJ += gfx/drivers/sdl_gfx.o \
audio/drivers/sdl1_audio.o
DEF_FLAGS += $(SDL_CFLAGS)
# See the SDL3 note above: drop SDL's WinMain shim lib.
LIBS += $(filter-out -lSDLmain -lSDL_main,$(SDL_LIBS))
@@ -1778,18 +1674,27 @@ else ifeq ($(HAVE_SDL_DINGUX), 1)
endif
OBJ += input/drivers/sdl_dingux_input.o \
input/drivers_joypad/sdl_dingux_joypad.o
input/drivers_joypad/sdl_dingux_joypad.o \
audio/drivers/sdl1_audio.o
DEF_FLAGS += $(SDL_DINGUX_CFLAGS)
LIBS += $(SDL_DINGUX_LIBS)
endif
ifeq ($(HAVE_SDL_COMMON), 1)
OBJ += input/drivers/sdl_input.o \
input/drivers_joypad/sdl_joypad.o \
audio/drivers/sdl_audio.o
ifeq ($(HAVE_SDL2), 1)
OBJ += input/drivers/sdl2_input.o \
input/drivers_joypad/sdl2_joypad.o
else
OBJ += input/drivers/sdl1_input.o \
input/drivers_joypad/sdl1_joypad.o
endif
ifeq ($(HAVE_GL_CONTEXT), 1)
OBJ += gfx/drivers_context/sdl_gl_ctx.o
ifeq ($(HAVE_SDL2), 1)
OBJ += gfx/drivers_context/sdl2_gl_ctx.o
else
OBJ += gfx/drivers_context/sdl1_gl_ctx.o
endif
endif
INCLUDE_DIRS += $(BSD_LOCAL_INC)
@@ -1872,6 +1777,10 @@ ifeq ($(HAVE_DISPMANX), 1)
HAVE_VIDEOCORE = 1
endif
ifeq ($(HAVE_VIDEOCORE), 1)
OBJ += gfx/display_servers/dispserv_videocore.o
endif
ifeq ($(HAVE_SUNXI), 1)
OBJ += gfx/drivers/sunxi_gfx.o \
gfx/include/pixman/pixman-arm-neon-asm.o
@@ -2527,6 +2436,7 @@ ifeq ($(HAVE_NETWORKING), 1)
$(LIBRETRO_COMM_DIR)/net/net_socket.o \
core_updater_list.o \
network/natt.o \
network/natt_desc.o \
tasks/task_netplay_lan_scan.o \
tasks/task_netplay_nat_traversal.o \
tasks/task_pl_thumbnail_download.o \
@@ -2553,20 +2463,25 @@ ifeq ($(HAVE_NETWORKING), 1)
ifeq ($(HAVE_CLOUDSYNC), 1)
DEFINES += -DHAVE_CLOUDSYNC
OBJ += tasks/task_cloudsync.o \
tasks/task_cloudsync_path.o \
network/cloud_sync/webdav.o \
network/cloud_sync_driver.o
ifeq ($(HAVE_SSL), 1)
OBJ += network/cloud_sync/google_drive.o
endif
ifneq (,$(filter 1,$(HAVE_SMBCLIENT) $(HAVE_BUILTINSMBCLIENT)))
ifneq (,$(filter 1,$(HAVE_SMBCLIENT) $(HAVE_RETROSMB)))
OBJ += network/cloud_sync/smb.o
endif
endif
ifeq ($(HAVE_BUILTINBEARSSL), 1)
ifeq ($(HAVE_RETROSSL), 1)
OBJ += $(LIBRETRO_COMM_DIR)/net/net_socket_ssl_retro.o \
network/tls_log.o
else ifeq ($(HAVE_BEARSSL), 1)
OBJ += $(LIBRETRO_COMM_DIR)/net/net_socket_ssl_bear.o
else ifeq ($(HAVE_SSL), 1)
OBJ += $(LIBRETRO_COMM_DIR)/net/net_socket_ssl_mbed.o
else ifeq ($(HAVE_MBEDTLS), 1)
OBJ += $(LIBRETRO_COMM_DIR)/net/net_socket_ssl_mbed.o \
network/tls_log.o
endif
ifeq ($(HAVE_IFINFO), 1)
@@ -2930,109 +2845,41 @@ ifeq ($(HAVE_TEST_DRIVERS), 1)
OBJ += input/drivers/test_input.o
endif
ifneq (,$(filter 1,$(HAVE_SMBCLIENT) $(HAVE_BUILTINSMBCLIENT)))
# The built-in NFSv3 client and its nfs:// VFS backend. Needs only
# networking; the console Makefiles do not set it.
ifeq ($(HAVE_RETRONFS), 1)
DEFINES += -DHAVE_NFSCLIENT -DHAVE_RETRONFS
OBJ += $(LIBRETRO_COMM_DIR)/net/net_nfs3.o \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation_nfs.o
endif
# The background prefetcher the network backends read through; once,
# whichever of them are built.
ifneq (,$(filter 1,$(HAVE_RETRONFS) $(HAVE_RETROSMB) $(HAVE_SMBCLIENT)))
OBJ += $(LIBRETRO_COMM_DIR)/vfs/vfs_prefetch.o
endif
# The built-in SMB2/3 client (libretro-common/net/net_smb2.c) stands in
# for libsmb2: the same HAVE_SMBCLIENT surface for the menu, VFS and
# cloud sync, no bundled or system library, needs HAVE_CRYPTO.
ifeq ($(HAVE_RETROSMB), 1)
DEFINES += -DHAVE_SMBCLIENT -DHAVE_RETROSMB
OBJ += $(LIBRETRO_COMM_DIR)/net/net_smb2.o \
$(LIBRETRO_COMM_DIR)/net/net_krb5.o \
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation_smb.o
else ifeq ($(HAVE_SMBCLIENT), 1)
# a system libsmb2 behind the same HAVE_SMBCLIENT surface
DEFINES += -DHAVE_SMBCLIENT
# The libsmb2 public headers gate their <stdint.h> include on
# HAVE_STDINT_H (autoconf-style), and every TU that includes
# them - the bundled library sources and frontend consumers
# like network/cloud_sync/smb.c alike - needs the define.
# It used to arrive only as a side effect of an unrelated block,
# which broke builds that did not enable that block on hundreds
# of unknown uint*_t types. Own the dependency here.
# HAVE_STDINT_H (autoconf-style); every TU that includes them needs
# the define.
DEFINES += -DHAVE_STDINT_H
ifeq ($(HAVE_SMBCLIENT),1)
# system libsmb2
ifeq ($(shell pkg-config --exists libsmb2 && echo yes),yes)
CFLAGS += $(shell pkg-config --cflags libsmb2)
LIBS += $(shell pkg-config --libs libsmb2)
else
LIBS += -lsmb2
endif
else ifeq ($(HAVE_BUILTINSMBCLIENT),1)
# builtin libsmb2
ifneq ($(findstring Win32,$(OS)),)
# WIN32_LEAN_AND_MEAN is defined with an empty replacement list on
# purpose. It has to be on the command line because the bundled
# libsmb2 needs it, but it lands on every TU in the build, and ~40
# RetroArch headers do a bare "#define WIN32_LEAN_AND_MEAN" of their
# own before including <windows.h>. A plain -DWIN32_LEAN_AND_MEAN
# expands to 1, which does not match those, so each one of them
# warns about a macro redefinition. Nothing tests the value, only
# #ifndef, so an empty definition is equivalent and silent.
CFLAGS += -D_WINDOWS -DWIN32_LEAN_AND_MEAN=
CFLAGS += -DNEED_RANDOM -DNEED_SRANDOM -DNEED_GETLOGIN_R
endif
INCLUDE_DIRS += -I$(DEPS_DIR)/libsmb2/include
INCLUDE_DIRS += -I$(DEPS_DIR)/libsmb2/include/smb2
CFLAGS += "-D_U_=__attribute__((unused))"
CFLAGS += -DHAVE_TIME_H -DHAVE_FCNTL_H -DHAVE_UNISTD_H
CFLAGS += -DHAVE_STDLIB_H -DSTDC_HEADERS
CFLAGS += -DHAVE_STRING_H
CFLAGS += -DHAVE_LINGER
ifeq ($(findstring Win32,$(OS)),)
CFLAGS += -DHAVE_SYS_UIO_H
CFLAGS += -DHAVE_POLL_H -DHAVE_NETDB_H
CFLAGS += -DHAVE_NETINET_TCP_H -DHAVE_NETINET_IN_H
CFLAGS += -DHAVE_SYS_SOCKET_H -DHAVE_ARPA_INET_H
endif
OBJ += \
deps/libsmb2/lib/aes.o \
deps/libsmb2/lib/aes_apple.o \
deps/libsmb2/lib/aes128ccm.o \
deps/libsmb2/lib/asn1-ber.o \
deps/libsmb2/lib/aes_reference.o \
deps/libsmb2/lib/alloc.o \
deps/libsmb2/lib/compat.o \
deps/libsmb2/lib/dcerpc.o \
deps/libsmb2/lib/dcerpc-lsa.o \
deps/libsmb2/lib/dcerpc-srvsvc.o \
deps/libsmb2/lib/errors.o \
deps/libsmb2/lib/hmac.o \
deps/libsmb2/lib/hmac-md5.o \
deps/libsmb2/lib/init.o \
deps/libsmb2/lib/krb5-wrapper.o \
deps/libsmb2/lib/libsmb2.o \
deps/libsmb2/lib/md4c.o \
deps/libsmb2/lib/ntlmssp.o \
deps/libsmb2/lib/pdu.o \
deps/libsmb2/lib/sha1.o \
deps/libsmb2/lib/sha224-256.o \
deps/libsmb2/lib/sha384-512.o \
deps/libsmb2/lib/smb2-cmd-close.o \
deps/libsmb2/lib/smb2-cmd-create.o \
deps/libsmb2/lib/smb2-cmd-echo.o \
deps/libsmb2/lib/smb2-cmd-error.o \
deps/libsmb2/lib/smb2-cmd-flush.o \
deps/libsmb2/lib/smb2-cmd-ioctl.o \
deps/libsmb2/lib/smb2-cmd-lock.o \
deps/libsmb2/lib/smb2-cmd-logoff.o \
deps/libsmb2/lib/smb2-cmd-negotiate.o \
deps/libsmb2/lib/smb2-cmd-oplock-break.o \
deps/libsmb2/lib/smb2-cmd-notify-change.o \
deps/libsmb2/lib/smb2-cmd-query-directory.o \
deps/libsmb2/lib/smb2-cmd-query-info.o \
deps/libsmb2/lib/smb2-cmd-read.o \
deps/libsmb2/lib/smb2-cmd-session-setup.o \
deps/libsmb2/lib/smb2-cmd-set-info.o \
deps/libsmb2/lib/smb2-cmd-tree-connect.o \
deps/libsmb2/lib/smb2-cmd-tree-disconnect.o \
deps/libsmb2/lib/smb2-cmd-write.o \
deps/libsmb2/lib/smb2-data-file-info.o \
deps/libsmb2/lib/smb2-data-filesystem-info.o \
deps/libsmb2/lib/smb2-data-security-descriptor.o \
deps/libsmb2/lib/smb2-data-reparse-point.o \
deps/libsmb2/lib/smb2-share-enum.o \
deps/libsmb2/lib/smb2-signing.o \
deps/libsmb2/lib/smb3-seal.o \
deps/libsmb2/lib/socket.o \
deps/libsmb2/lib/spnego-wrapper.o \
deps/libsmb2/lib/sync.o \
deps/libsmb2/lib/timestamps.o \
deps/libsmb2/lib/usha.o \
deps/libsmb2/lib/unicode.o
ifeq ($(shell pkg-config --exists libsmb2 && echo yes),yes)
CFLAGS += $(shell pkg-config --cflags libsmb2)
LIBS += $(shell pkg-config --libs libsmb2)
else
LIBS += -lsmb2
endif
OBJ += $(LIBRETRO_COMM_DIR)/vfs/vfs_implementation_smb.o
endif
+8 -4
View File
@@ -65,11 +65,8 @@ ifeq ($(GRIFFIN_BUILD), 1)
DEFINES += -DHAVE_OVERLAY
DEFINES += -DHAVE_CORE_INFO_CACHE
#DEFINES += -DHAVE_SOCKET_LEGACY
#-DHAVE_SSL -DHAVE_BUILTINMBEDTLS -DMBEDTLS_SSL_DEBUG_ALL
#ssl is currently incompatible with griffin due to use of the "static" flag on repeating functions that will conflict when included in one file
CFLAGS += -I. \
-Ideps \
-Ideps/mbedtls \
-Ideps/rcheevos/include \
-Ilibretro-common/include
else
@@ -103,7 +100,14 @@ else
HAVE_IFINFO = 1
HAVE_CHEEVOS = 1
HAVE_THREADS = 1
HAVE_BUILTINMBEDTLS = 1
# the cleanroom crypto, keychain and TLS client
HAVE_CRYPTO = 1
HAVE_KEYCHAIN = 1
HAVE_RETROSSL = 1
# the built-in NFS client (nfs://); it needs no crypto
HAVE_RETRONFS = 1
# the built-in SMB client (smb://), on the crypto above
HAVE_RETROSMB = 1
HAVE_CORE_INFO_CACHE = 1
HAVE_CLOUDSYNC = 1
+5 -2
View File
@@ -43,7 +43,10 @@ HAVE_AL = 1
HAVE_ALSA = 0
HAVE_DSP_FILTER = 1
HAVE_VIDEO_FILTER = 1
HAVE_BUILTINMBEDTLS = 1
# the cleanroom crypto, keychain and TLS client
HAVE_CRYPTO = 1
HAVE_KEYCHAIN = 1
HAVE_RETROSSL = 1
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
@@ -77,7 +80,7 @@ HAVE_OPENGL1 = 0
HAVE_OPENGLES = 1
HAVE_OPENGLES3 = 0
HAVE_OPENGL_CORE = 0
HAVE_OPENSSL = 1
HAVE_OPENSSL = 0
HAVE_OVERLAY = 1
HAVE_RBMP = 1
HAVE_RJPEG = 1
+2 -4
View File
@@ -3,7 +3,6 @@ DEPS_DIR := $(ROOT_DIR)/deps
DEBUG = 0
HAVE_LOGGER = 0
HAVE_FILE_LOGGER = 0
HAVE_CC_RESAMPLER = 1
WHOLE_ARCHIVE_LINK = 0
BIG_STACK = 1
PC_DEVELOPMENT_IP_ADDRESS = 255.255.255.255
@@ -328,8 +327,7 @@ else ifeq ($(platform), vita)
libretro-common/audio/conversion/s16_to_float_neon.o \
libretro-common/audio/conversion/float_to_s16_neon.o \
memory/neon/memcpy-neon.o \
libretro-common/audio/resampler/drivers/sinc_resampler_neon.o \
audio/drivers_resampler/cc_resampler_neon.o
libretro-common/audio/resampler/drivers/sinc_resampler_neon.o
LIBDIRS += -L.
LDFLAGS += -Wl,-q
@@ -1009,7 +1007,7 @@ else ifneq (,$(findstring unix,$(platform)))
EXT_TARGET := $(TARGET_NAME)
EXT_INTER_TARGET := $(TARGET_NAME)
INCLUDE += -Ilibretro-common/include -Igfx/include -Ideps -Ideps/rcheevos/include -Ideps/SPIRV-Cross -Ideps/glslang -I.
LIBS += -ldl -lm -lpthread -lGL -ludev -lpulse -lX11 -lX11-xcb -lXxf86vm
LIBS += -ldl -lm -lpthread -lGL -ludev -lpulse -lX11 -lX11-xcb -lxcb -lXxf86vm
endif
ifneq (,$(findstring msvc,$(platform)))
-1
View File
@@ -52,7 +52,6 @@ HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 0
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
+9 -2
View File
@@ -58,7 +58,14 @@ HAVE_RVORBIS = 1
HAVE_RMODTRACKER = 1
HAVE_CORE_INFO_CACHE = 1
HAVE_CLOUDSYNC = 1
HAVE_SSL = 1
# The cleanroom crypto, keychain and TLS 1.2/1.3 client; nothing to link.
HAVE_CRYPTO = 1
HAVE_KEYCHAIN = 1
HAVE_RETROSSL = 1
# The built-in NFS client (nfs://); it needs no crypto.
HAVE_RETRONFS = 1
# The built-in SMB client (smb://), on the crypto above.
HAVE_RETROSMB = 1
# RetroArch libnx useful flags
HAVE_THREADS = 1
@@ -150,7 +157,7 @@ LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs $(ARCH) -Wl,--allow-multiple-de
# add things from Makefile.common
CFLAGS += $(DEF_FLAGS)
LIBS := -lswresample -lavformat -lavcodec -lavutil -lswscale -lstdc++ -lbz2 -lpng -lz -lnx -lvpx -lopus -llzma -lmbedtls -lmbedx509 -lmbedcrypto -lnx -lm
LIBS := -lswresample -lavformat -lavcodec -lavutil -lswscale -lstdc++ -lbz2 -lpng -lz -lnx -lvpx -lopus -llzma -lnx -lm
ifeq ($(HAVE_OPENGL), 1)
LIBS := -lEGL -lglapi -ldrm_nouveau $(LIBS)
-1
View File
@@ -54,7 +54,6 @@ HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 0
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
+19 -6
View File
@@ -122,20 +122,29 @@ LIBS += -logc
CFLAGS += -DGEKKO -U__INT32_TYPE__ -U __UINT32_TYPE__ -D__INT32_TYPE__=int
HAVE_RUNAHEAD := 1
# 24 MB of MEM1 makes resident code weight count, so features that cannot
# do their job on this hardware are compiled out: runahead (the second
# savestate never fits beside a running core), the webp/dds thumbnail
# decoders, the mod/s3m/xm tracker, 7z (LZMA dictionaries alone outsize
# the console's free RAM, so 7z archives could never extract anyway) and
# libretrodb (defaulted on by Makefile.common; database scanning is
# impractical here, and the explore menu goes with it). Together these
# return about 640 KB of MEM1 to content.
HAVE_LIBRETRODB := 0
HAVE_RUNAHEAD := 0
HAVE_DSP_FILTER := 1
HAVE_VIDEO_FILTER := 1
HAVE_FILTERS_BUILTIN := 1
HAVE_THREADS := 1
HAVE_RPNG := 1
HAVE_RWEBP := 1
HAVE_RDDS := 1
HAVE_RWEBP := 0
HAVE_RDDS := 0
HAVE_RJPEG := 1
HAVE_RBMP := 1
HAVE_RTGA := 1
HAVE_RMODTRACKER := 1
HAVE_RMODTRACKER := 0
HAVE_OVERLAY := 1
HAVE_7ZIP := 1
HAVE_7ZIP := 0
HAVE_RZSTD := 1
HAVE_CONFIGFILE := 1
HAVE_PATCH := 1
@@ -161,9 +170,13 @@ INCLUDE += -I./libretro-common/include \
-Ideps
CFLAGS += -Wall -std=gnu99 $(MACHDEP) $(PLATCFLAGS) $(INCLUDE)
OBJ = $(PLATOBJS) audio/drivers/gx_audio.o gfx/drivers/gx_gfx.o input/drivers/gx_input.o frontend/drivers/platform_gx.o input/drivers_joypad/gx_joypad.o
OBJ = $(PLATOBJS) audio/drivers/gx_audio.o gfx/drivers/gx_gfx.o gfx/display_servers/dispserv_gx.o gfx/display_servers/dispserv_gx_modes.o input/drivers/gx_input.o frontend/drivers/platform_gx.o input/drivers_joypad/gx_joypad.o
ifeq ($(EXTERNAL_LIBOGC), 0)
# Bridge the prebuilt in-tree libogc's __syscalls table onto the
# per-symbol __syscall_* hooks modern devkitPPC libsysbase dispatches
# through (see wii/libogc_syscalls_compat.c).
OBJ += wii/libogc_syscalls_compat.o
OBJ += wii/libogc/libfat/libfat.o wii/libogc/libfat/fatfile.o wii/libogc/libfat/file_allocation_table.o wii/libogc/libfat/cache.o wii/libogc/libfat/fatdir.o wii/libogc/libfat/partition.o wii/libogc/libfat/directory.o wii/libogc/libfat/filetime.o wii/libogc/libfat/disc.o
endif
+1 -1
View File
@@ -19,7 +19,7 @@ AUTH_INFO = 000000000000000000000000001C004000FF00000000008000000000000000000000
OBJ += deps/xxHash/xxhash.o \
# input/drivers/ps4_input.o \
# input/drivers_joypad/ps4_joypad.o \
# audio/drivers/psp_audio.o \
# audio/drivers/ps4_audio.o \
# gfx/drivers_context/orbis_ctx.o \
# frontend/drivers/platform_orbis.o
+6 -2
View File
@@ -54,7 +54,11 @@ HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 0
# the cleanroom crypto, keychain and TLS client (HAVE_OPENSSL linked a
# library no backend used)
HAVE_CRYPTO = 1
HAVE_KEYCHAIN = 1
HAVE_RETROSSL = 1
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
@@ -85,7 +89,7 @@ HAVE_OPENGL1 = 0
HAVE_OPENGLES = 0
HAVE_OPENGLES3 = 0
HAVE_OPENGL_CORE = 0
HAVE_OPENSSL = 1
HAVE_OPENSSL = 0
HAVE_OVERLAY = 0
HAVE_RBMP = 1
HAVE_RJPEG = 1
+6 -2
View File
@@ -51,7 +51,11 @@ HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 0
# the cleanroom crypto, keychain and TLS client (HAVE_OPENSSL linked a
# library no backend used)
HAVE_CRYPTO = 1
HAVE_KEYCHAIN = 1
HAVE_RETROSSL = 1
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
@@ -83,7 +87,7 @@ HAVE_OPENGL1 = 0
HAVE_OPENGLES = 0
HAVE_OPENGLES3 = 0
HAVE_OPENGL_CORE = 0
HAVE_OPENSSL = 1
HAVE_OPENSSL = 0
HAVE_OVERLAY = 0
HAVE_RBMP = 1
HAVE_RJPEG = 1
-1
View File
@@ -51,7 +51,6 @@ HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 0
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
-1
View File
@@ -53,7 +53,6 @@ HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 0
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
+8 -1
View File
@@ -75,10 +75,17 @@ else
HAVE_VITAGL := 0
endif
HAVE_NETWORKING := 1
HAVE_BUILTINMBEDTLS := 1
# the cleanroom crypto, keychain and TLS client
HAVE_CRYPTO := 1
HAVE_KEYCHAIN := 1
HAVE_RETROSSL := 1
HAVE_IFINFO := 1
HAVE_NETPLAYDISCOVERY := 1
HAVE_SOCKET_LEGACY := 1
# the built-in NFS client (nfs://); it needs no crypto
HAVE_RETRONFS := 1
# the built-in SMB client (smb://), on the crypto above
HAVE_RETROSMB := 1
HAVE_MENU := 1
HAVE_MENU_COMMON := 1
HAVE_GFX_WIDGETS := 1
+8 -3
View File
@@ -49,15 +49,20 @@ HAVE_AUDIOIO = 0
HAVE_AUDIOMIXER = 1
HAVE_BLISSBOX = 0
HAVE_BSV_MOVIE = 1
HAVE_BUILTINBEARSSL = 0
HAVE_RFLAC = 1
HAVE_DSP_FILTER = 1
HAVE_VIDEO_FILTER = 1
HAVE_STATIC_VIDEO_FILTERS = 1
HAVE_STATIC_AUDIO_FILTERS = 1
HAVE_FILTERS_BUILTIN = 1
HAVE_BUILTINMBEDTLS = 1
HAVE_BUILTINSMBCLIENT := 1
# The cleanroom network stack, as on the desktop builds: the crypto,
# the encrypted keychain, the TLS 1.2/1.3 client and the SMB2/3 client
# with Kerberos; plus the NFSv3/v4 client. Needs nothing but sockets.
HAVE_CRYPTO = 1
HAVE_KEYCHAIN = 1
HAVE_RETROSSL = 1
HAVE_RETROSMB = 1
HAVE_RETRONFS = 1
HAVE_C99 = 1
HAVE_CC = 1
HAVE_CC_RESAMPLER = 1
+1 -1
View File
@@ -179,7 +179,7 @@ INCLUDE += -I./libretro-common/include \
-Ideps/rcheevos/include
CFLAGS += -Wall -std=gnu99 $(MACHDEP) $(PLATCFLAGS) $(INCLUDE)
OBJ = $(PLATOBJS) audio/drivers/gx_audio.o gfx/drivers/gx_gfx.o input/drivers/gx_input.o frontend/drivers/platform_gx.o input/drivers_joypad/gx_joypad.o libretro-common/memory/mem2_manager.o gfx/drivers/gx_gfx_vi_encoder.o frontend/drivers/platform_wii.o input/drivers_hid/wiiusb_hid.o
OBJ = $(PLATOBJS) audio/drivers/gx_audio.o gfx/drivers/gx_gfx.o gfx/display_servers/dispserv_gx.o gfx/display_servers/dispserv_gx_modes.o input/drivers/gx_input.o frontend/drivers/platform_gx.o input/drivers_joypad/gx_joypad.o libretro-common/memory/mem2_manager.o gfx/drivers/gx_gfx_vi_encoder.o frontend/drivers/platform_wii.o input/drivers_hid/wiiusb_hid.o
ifeq ($(EXTERNAL_LIBOGC), 0)
OBJ += wii/libogc/libfat/libfat.o wii/libogc/libfat/fatfile.o wii/libogc/libfat/file_allocation_table.o wii/libogc/libfat/cache.o wii/libogc/libfat/fatdir.o wii/libogc/libfat/partition.o wii/libogc/libfat/directory.o wii/libogc/libfat/filetime.o wii/libogc/libfat/disc.o
+2
View File
@@ -161,6 +161,8 @@ endif
HAVE_LANGEXTRA = 1
HAVE_LIBRETRODB = 1
HAVE_NETWORKING = 1
# the built-in NFS client (nfs://); it needs no crypto
HAVE_RETRONFS = 1
HAVE_IFINFO = 1
HAVE_NETPLAYDISCOVERY = 1
HAVE_CHEEVOS = 1
+44
View File
@@ -0,0 +1,44 @@
/* 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 __AUDIO_DEVICE_LABEL_H
#define __AUDIO_DEVICE_LABEL_H
#include <stddef.h>
#include <string.h>
#include <retro_inline.h>
#include <compat/strl.h>
/* A device list entry can be a path with the hardware's name after
* it in brackets - "/dev/dsp0 (HDA Intel)" - which is how oss and
* audioio list their nodes so the user can tell them apart. The menu
* stores the entry as shown, and it comes back to init as the device
* string, so a driver that opens the string as a path takes the path
* off the front first: everything before the first " (". A string
* without one is a path already and is copied as it is. Returns the
* length of the path. */
static INLINE size_t audio_device_label_path(char *dst, size_t len,
const char *entry)
{
const char *cut = strstr(entry, " (");
size_t n = cut ? (size_t)(cut - entry) : strlen(entry);
if (n >= len)
n = len - 1;
memcpy(dst, entry, n);
dst[n] = '\0';
return n;
}
#endif
+341 -209
View File
@@ -118,6 +118,22 @@ static bool audio_driver_pipeline_transport_publish(uint32_t tempo_q16,
* out of pipe_ring per pass (one batch slice), and the producer stages
* at most this many when it has to convert a float batch first. */
#define AUDIO_PIPE_SLICE_INT16S AUDIO_CHUNK_SIZE_NONBLOCKING
/* Multichannel fold staging (audio_driver_state_t::multi_fold), in
* frames. Every multichannel entry slices its batch to at most this
* many frames before folding, so the region is sized once at init and
* the fold never grows it on the batch callback. Shared by the float
* and int16 paths; sized for float, the larger. */
#define AUDIO_MULTI_FOLD_FRAMES (AUDIO_CHUNK_SIZE_NONBLOCKING >> 1)
/* Recorder remap staging (audio_driver_state_t::record_remap): two
* halves of one batch slice at the widest frame either side can carry
* (a multichannel core is bounded at 2 + 2 * extra pairs, a recorder
* layout at the same), so it too is a fixed region carved at init. In
* int16 samples per half. */
#define AUDIO_RECORD_REMAP_CHANNELS 16
#define AUDIO_RECORD_REMAP_INT16S ((AUDIO_CHUNK_SIZE_NONBLOCKING >> 1) \
* AUDIO_RECORD_REMAP_CHANNELS)
/* The canonical wide frame on a multi-channel core's ring: a slot per
* speaker position bit, FL and FR first. */
#define AUDIO_PIPE_CANON_CHANNELS 11
@@ -248,8 +264,6 @@ audio_driver_t audio_null = {
audio_driver_t *audio_drivers[] = {
#ifdef HAVE_ALSA
&audio_alsa,
#if !defined(__QNX__) && !defined(MIYOO) && defined(HAVE_THREADS)
#endif
#endif
#ifdef HAVE_TINYALSA
&audio_tinyalsa,
@@ -293,8 +307,11 @@ audio_driver_t *audio_drivers[] = {
#ifdef HAVE_DSOUND
&audio_dsound,
#endif
#if defined(HAVE_SDL) || defined(HAVE_SDL2)
&audio_sdl,
#ifdef HAVE_SDL
&audio_sdl1,
#endif
#ifdef HAVE_SDL2
&audio_sdl2,
#endif
#ifdef HAVE_SDL3
&audio_sdl3,
@@ -323,9 +340,15 @@ audio_driver_t *audio_drivers[] = {
#if defined(HAVE_AUDIOWORKLET)
&audio_audioworklet,
#endif
#if defined(PSP) || defined(VITA) || defined(ORBIS)
#if defined(PSP)
&audio_psp,
#endif
#if defined(VITA)
&audio_psp2,
#endif
#if defined(ORBIS)
&audio_ps4,
#endif
#if defined(PS2)
&audio_ps2,
#endif
@@ -350,7 +373,6 @@ microphone_driver_t microphone_null = {
NULL,
NULL,
NULL,
NULL,
"null",
NULL,
NULL,
@@ -361,12 +383,13 @@ microphone_driver_t microphone_null = {
microphone_driver_t *microphone_drivers[] = {
#ifdef HAVE_ALSA
&microphone_alsa,
#if !defined(__QNX__) && !defined(MIYOO) && defined(HAVE_THREADS)
#endif
#endif
#ifdef HAVE_WASAPI
&microphone_wasapi,
#endif
#ifdef HAVE_WDMKS
&microphone_wdmks,
#endif
#ifdef HAVE_SDL2
&microphone_sdl, /* Microphones are not supported in SDL 1 */
#endif
@@ -588,27 +611,11 @@ static bool audio_driver_resampler_realloc(audio_driver_state_t *audio_st,
audio_st->resampler_quality, audio_st->src_ratio_orig, hq_oversampling);
audio_st->resampler_hq = initialized && hq_oversampling
&& audio_st->src_ratio_orig >= 2.0
&& audio_st->resampler == &sinc_resampler;
&& audio_st->resampler
&& (audio_st->resampler->caps & RESAMPLER_CAP_HQ_OVERSAMPLE);
return initialized;
}
/* Map the shared resampler quality enum onto the integer sinc driver's own
* quality enum (they use different orderings). */
static enum sinc_int16_quality audio_sinc_int16_quality_map(
enum resampler_quality q)
{
switch (q)
{
case RESAMPLER_QUALITY_LOWEST: return SINC_INT16_QUALITY_LOWEST;
case RESAMPLER_QUALITY_LOWER: return SINC_INT16_QUALITY_LOWER;
case RESAMPLER_QUALITY_HIGHER: return SINC_INT16_QUALITY_HIGHER;
case RESAMPLER_QUALITY_HIGHEST: return SINC_INT16_QUALITY_HIGHEST;
case RESAMPLER_QUALITY_NORMAL:
case RESAMPLER_QUALITY_DONTCARE:
default: return SINC_INT16_QUALITY_NORMAL;
}
}
size_t audio_driver_get_underruns(void)
{
audio_driver_state_t *audio_st = &audio_driver_st;
@@ -650,21 +657,13 @@ static void audio_driver_extra_free(audio_driver_state_t *audio_st)
/* An int16 resampler instance of the kind the front pair uses. */
static void *audio_driver_int16_resampler_new(audio_driver_state_t *audio_st)
{
const char *rs_ident = (audio_st->resampler && audio_st->resampler->short_ident)
? audio_st->resampler->short_ident : "";
if (string_is_equal(rs_ident, "sinc"))
return sinc_resampler_int16_init_hq(audio_st->src_ratio_orig,
audio_sinc_int16_quality_map(audio_st->resampler_quality),
audio_st->resampler_hq);
#ifdef HAVE_NEAREST_RESAMPLER
if (string_is_equal(rs_ident, "nearest"))
return nearest_resampler_int16_init();
#endif
#ifdef HAVE_CC_RESAMPLER
if (string_is_equal(rs_ident, "cc"))
return cc_resampler_int16_init(audio_st->src_ratio_orig);
#endif
return NULL;
retro_resampler_int16_t rs;
retro_resampler_int16_new(&rs,
(audio_st->resampler && audio_st->resampler->short_ident)
? audio_st->resampler->short_ident : NULL,
audio_st->resampler_quality, audio_st->src_ratio_orig,
audio_st->resampler_hq);
return rs.data;
}
/* Room for a batch of extras: the buffers, and resampler instances of
@@ -985,12 +984,50 @@ static INLINE bool audio_driver_pipe_ahead(const audio_driver_state_t *audio_st,
return d && d <= audio_st->pipe_ring.capacity;
}
/* The ring's positions restart with it; so do the marks in them. */
/* The main thread's side of the resume mark: a new sequence, armed at
* @at or not armed at all. The main thread is the mark's only writer. */
static void audio_driver_pipe_fade_publish(audio_driver_state_t *audio_st,
bool armed, size_t at)
{
unsigned seq = ((unsigned)retro_atomic_load_relaxed_int(
&audio_st->pipe_fade_in_mark) >> 1) + 1;
if (armed)
retro_atomic_store_release_size(&audio_st->pipe_fade_in_at, at);
retro_atomic_store_release_int(&audio_st->pipe_fade_in_mark,
(int)((seq << 1) | (armed ? 1u : 0u)));
}
/* The consumer's side: the resume point a mark still asks for, and the
* mark's sequence to record once it is acted on. The position is taken
* between two reads of the mark that agree, so it is that mark's. */
static bool audio_driver_pipe_fade_pending(audio_driver_state_t *audio_st,
size_t *at, unsigned *seq)
{
for (;;)
{
unsigned mark = (unsigned)retro_atomic_load_acquire_int(
&audio_st->pipe_fade_in_mark);
if (!(mark & 1u) || (mark >> 1) == audio_st->pipe_fade_in_seen)
return false;
*at = retro_atomic_load_acquire_size(&audio_st->pipe_fade_in_at);
if ((unsigned)retro_atomic_load_acquire_int(
&audio_st->pipe_fade_in_mark) == mark)
{
*seq = mark >> 1;
return true;
}
}
}
/* The ring's positions restart with it; so do the marks in them. Both
* owners are parked. */
static void audio_driver_pipe_marks_clear(audio_driver_state_t *audio_st)
{
audio_st->pipe_discard_seen =
retro_atomic_load_acquire_int(&audio_st->pipe_discard_gen);
retro_atomic_store_release_int(&audio_st->pipe_fade_in_set, 0);
audio_st->pipe_fade_in_seen =
(unsigned)retro_atomic_load_acquire_int(
&audio_st->pipe_fade_in_mark) >> 1;
audio_st->pipe_arm_fade = false;
}
#endif
@@ -1016,6 +1053,23 @@ static bool audio_driver_deinit_internal(bool audio_enable)
if (n)
RARCH_LOG("[Audio] Driver \"%s\": %u period%s of silence for want of audio this session.\n",
audio_driver_get_ident(), (unsigned)n, n == 1 ? "" : "s");
#ifdef HAVE_THREADS
/* And the consumer's side of it: whether the pipe ever ran
* short of the core, which is the other place silence comes
* from, and how low it got. */
if (audio_st->pipe_threaded
&& retro_atomic_load_acquire_size(&audio_st->pipe_held_min1))
{
size_t waits = retro_atomic_load_acquire_size(&audio_st->pipe_source_waits);
size_t low = retro_atomic_load_acquire_size(&audio_st->pipe_held_min1) - 1;
RARCH_LOG("[Audio] Pipeline: waited for the core %u time%s after priming,"
" %.2f ms at the longest; the pipe held %u frames (%.1f ms) at its lowest.\n",
(unsigned)waits, waits == 1 ? "" : "s",
(double)retro_atomic_load_acquire_size(&audio_st->pipe_source_wait_max_us) / 1000.0,
(unsigned)low,
audio_st->input > 0.0f ? (double)low * 1000.0 / (double)audio_st->input : 0.0);
}
#endif
if (audio_st->context_audio_data)
audio->free(audio_st->context_audio_data);
audio_st->context_audio_data = NULL;
@@ -1067,10 +1121,10 @@ static bool audio_driver_deinit_internal(bool audio_enable)
audio_st->pipe_wide = NULL;
audio_st->pipe_wide_bytes = 0;
audio_st->pipe_channels = 2;
free(audio_st->multi_fold);
/* multi_fold is a region of arena_float, freed with it below. */
audio_st->multi_fold = NULL;
audio_st->multi_fold_frames = 0;
free(audio_st->record_remap);
/* record_remap is a region of arena_int16, freed with it above. */
audio_st->record_remap = NULL;
audio_st->record_remap_frames = 0;
audio_st->core_layout = AUDIO_LAYOUT_STEREO;
@@ -3404,18 +3458,14 @@ static void audio_driver_flush(audio_driver_state_t *audio_st,
* batch. The resampler sees identical bytes either way, so output is
* bit-exact.
*
* The pointer must still be suitably aligned: the CC resampler's ARM
* NEON assembly loads its input with an explicit alignment hint
* (`vld1.f32 d16, [r1, :64]!` in cc_resampler_neon.S), which faults on
* an under-aligned address. input_data is memalign_alloc(64); a
* core-owned buffer carries no such guarantee, so fall back to the copy
* unless the pointer is at least 16-byte aligned. */
* Nothing downstream requires the pointer to be aligned: every
* resampler reads data_in scalar-wise or with unaligned loads, and so
* does the bypass clamp. */
{
bool synth_on = midi_driver_synth_active() && audio_st->synth_buf;
bool copy_input = !is_float
|| (audio_volume_gain != 1.0f)
|| synth_on
|| (((uintptr_t)data & 0xf) != 0)
#ifdef HAVE_DSP_FILTER
|| (audio_st->dsp != NULL)
#endif
@@ -4054,10 +4104,13 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
* in addition to its own. */
size_t i16_rewind = AUDIO_ARENA_NEXT(i16_in_scratch,
max_buffer_samples, AUDIO_ARENA_ALIGN_INT16);
size_t i16_total = i16_rewind + max_buffer_samples;
size_t i16_record = AUDIO_ARENA_NEXT(i16_rewind,
max_buffer_samples, AUDIO_ARENA_ALIGN_INT16);
#else
size_t i16_total = i16_in_scratch + max_buffer_samples;
size_t i16_record = AUDIO_ARENA_NEXT(i16_in_scratch,
max_buffer_samples, AUDIO_ARENA_ALIGN_INT16);
#endif
size_t i16_total = i16_record + 2 * AUDIO_RECORD_REMAP_INT16S;
size_t f32_input = 0;
size_t f32_synth = AUDIO_ARENA_NEXT(f32_input,
max_buffer_samples, AUDIO_ARENA_ALIGN_FLOAT);
@@ -4066,10 +4119,13 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
#ifdef HAVE_REWIND
size_t f32_rewind = AUDIO_ARENA_NEXT(f32_out,
outsamples_max, AUDIO_ARENA_ALIGN_FLOAT);
size_t f32_total = f32_rewind + max_buffer_samples;
size_t f32_fold = AUDIO_ARENA_NEXT(f32_rewind,
max_buffer_samples, AUDIO_ARENA_ALIGN_FLOAT);
#else
size_t f32_total = f32_out + outsamples_max;
size_t f32_fold = AUDIO_ARENA_NEXT(f32_out,
outsamples_max, AUDIO_ARENA_ALIGN_FLOAT);
#endif
size_t f32_total = f32_fold + AUDIO_MULTI_FOLD_FRAMES * 2;
int16_t *arena_int16 = (int16_t*)memalign_alloc(64,
i16_total * sizeof(int16_t));
float *arena_float = (float*)memalign_alloc(64,
@@ -4130,6 +4186,10 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
audio_driver_st.rewind_buf_f = arena_float + f32_rewind;
#endif
audio_driver_st.output_samples_buf_length = outsamples_max * sizeof(float);
audio_driver_st.multi_fold = arena_float + f32_fold;
audio_driver_st.multi_fold_frames = AUDIO_MULTI_FOLD_FRAMES;
audio_driver_st.record_remap = arena_int16 + i16_record;
audio_driver_st.record_remap_frames = AUDIO_RECORD_REMAP_INT16S;
if (!audio_enable)
{
@@ -4229,6 +4289,9 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
retro_atomic_store_release_int(&audio_driver_st.pipe_ctrl_avail, -1);
audio_driver_st.pipe_underruns_seen = 0;
audio_driver_st.pipe_priming = true;
retro_atomic_size_init(&audio_driver_st.pipe_source_waits, 0);
retro_atomic_size_init(&audio_driver_st.pipe_source_wait_max_us, 0);
retro_atomic_size_init(&audio_driver_st.pipe_held_min1, 0);
if (audio_driver_st.pipe_channels > 2)
{
size_t wide = audio_driver_st.pipe_pass_frames * AUDIO_PIPE_CANON_CHANNELS * sizeof(float);
@@ -4268,7 +4331,7 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
audio_driver_st.pipe_park_ready = true;
AUDIO_FLAGS_SET(&audio_driver_st, AUDIO_FLAG_PIPELINE_THREADED);
audio_driver_st.pipe_threaded = true;
audio_driver_st.pipe_consumer_gone = false;
retro_atomic_int_init(&audio_driver_st.pipe_consumer_gone, 0);
RARCH_LOG("[Audio] Threaded pipeline: ring holds %u frames of %s core audio.\n",
(unsigned)(audio_driver_st.pipe_ring.capacity / audio_driver_st.pipe_frame_bytes),
audio_driver_st.pipe_float ? "float" : "int16");
@@ -4468,35 +4531,14 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
&& audio_driver_st.resampler->short_ident)
{
const char *rs_ident = audio_driver_st.resampler->short_ident;
if (string_is_equal(rs_ident, "sinc"))
{
audio_driver_st.resampler_data_int16 = sinc_resampler_int16_init_hq(
audio_driver_st.src_ratio_orig,
audio_sinc_int16_quality_map(audio_driver_st.resampler_quality),
audio_driver_st.resampler_hq);
audio_driver_st.resampler_int16_process = sinc_resampler_int16_process;
audio_driver_st.resampler_int16_free = sinc_resampler_int16_free;
audio_driver_st.resampler_int16_reset = sinc_resampler_int16_reset;
}
#ifdef HAVE_NEAREST_RESAMPLER
else if (string_is_equal(rs_ident, "nearest"))
{
audio_driver_st.resampler_data_int16 = nearest_resampler_int16_init();
audio_driver_st.resampler_int16_process = nearest_resampler_int16_process;
audio_driver_st.resampler_int16_free = nearest_resampler_int16_free;
audio_driver_st.resampler_int16_reset = nearest_resampler_int16_reset;
}
#endif
#ifdef HAVE_CC_RESAMPLER
else if (string_is_equal(rs_ident, "cc"))
{
audio_driver_st.resampler_data_int16 = cc_resampler_int16_init(
audio_driver_st.src_ratio_orig);
audio_driver_st.resampler_int16_process = cc_resampler_int16_process;
audio_driver_st.resampler_int16_free = cc_resampler_int16_free;
audio_driver_st.resampler_int16_reset = cc_resampler_int16_reset;
}
#endif
retro_resampler_int16_t rs;
retro_resampler_int16_new(&rs, rs_ident,
audio_driver_st.resampler_quality,
audio_driver_st.src_ratio_orig, audio_driver_st.resampler_hq);
audio_driver_st.resampler_data_int16 = rs.data;
audio_driver_st.resampler_int16_process = rs.process;
audio_driver_st.resampler_int16_free = rs.free;
audio_driver_st.resampler_int16_reset = rs.reset;
if (audio_driver_st.resampler_int16_process)
RARCH_LOG("[Audio] %s resampler: integer s16 path %s.\n",
rs_ident,
@@ -4562,18 +4604,7 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
? (double)audio_driver_st.buffer_size / frame_bytes
* 1000.0 / out_rate
: 0.0;
const char *ident = audio_driver_st.current_audio->ident;
#ifdef HAVE_THREADS
/* Name the driver the user chose, not the wrapper it runs
* under. */
if (string_is_equal(ident, "audio-thread"))
{
const audio_driver_t *inner = audio_thread_wrapped_driver(
audio_driver_st.context_audio_data);
if (inner)
ident = inner->ident;
}
#endif
const char *ident = audio_driver_get_ident();
RARCH_LOG("[Audio] Driver \"%s\" reports a %u-byte buffer: "
"%.1f ms of %u-channel %s at %u Hz against a %u ms latency setting%s; "
"rate control %s it near %.1f ms.\n",
@@ -4611,7 +4642,8 @@ bool audio_driver_init_internal(void *settings_data, bool audio_cb_inited)
* SIGFPE" workaround for the same hazard. Check it once, centrally,
* and fall back to no rate control rather than making every driver
* defend itself. */
if (!audio_driver_st.current_audio->buffer_size)
if ( !audio_driver_st.current_audio->write_avail
|| !audio_driver_st.current_audio->buffer_size)
RARCH_WARN("[Audio] Rate control was desired, but driver does not support needed features.\n");
else if (audio_driver_st.buffer_size == 0)
RARCH_WARN("[Audio] Rate control was desired, but the driver "
@@ -4668,7 +4700,8 @@ error:
void audio_driver_pipeline_consumer_exit(void)
{
audio_driver_st.pipe_consumer_gone = true;
/* Release: the consumer's loop is done before the word that says so. */
retro_atomic_store_release_int(&audio_driver_st.pipe_consumer_gone, 1);
}
void audio_driver_publish_runloop(void)
@@ -5020,7 +5053,7 @@ void audio_driver_pause_fade(bool paused)
retro_atomic_store_release_size(&audio_st->pipe_discard_to,
retro_atomic_load_acquire_size(&audio_st->pipe_ring.head));
retro_atomic_fetch_add_int(&audio_st->pipe_discard_gen, 1);
retro_atomic_store_release_int(&audio_st->pipe_fade_in_set, 0);
audio_driver_pipe_fade_publish(audio_st, false, 0);
#endif
/* Only when something was playing is there a step to smooth. */
@@ -5263,9 +5296,19 @@ static size_t audio_driver_pipe_target_frames(audio_driver_state_t *audio_st)
}
/* Never under one publish: the core delivers a frame at a time, and
* a pipe holding less than that between publishes is a device that
* runs dry between them whatever its own buffer holds. */
* runs dry between them whatever its own buffer holds.
*
* And where nothing regulates the fill - no rate control - one
* publish is no margin at all: the pipe holds a frame just after
* the core hands one over and nothing just before the next, so a
* frame late by any amount reaches the device as silence. Two
* publishes there, so a frame's worth is held at the worst phase,
* which is what a late frame costs. */
if (target < audio_st->pipe_pass_frames)
target = audio_st->pipe_pass_frames;
if ( !(AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_CONTROL)
&& target < 2 * audio_st->pipe_pass_frames)
target = 2 * audio_st->pipe_pass_frames;
/* Within the ring less two publishes: one arriving, one of swing
* in the fill between the core's publish and the consumer's pass. */
ring_max = audio_st->pipe_ring.capacity / audio_st->pipe_frame_bytes;
@@ -5275,6 +5318,29 @@ static size_t audio_driver_pipe_target_frames(audio_driver_state_t *audio_st)
ring_max = 0;
return target < ring_max ? target : ring_max;
}
/* What priming fills the pipe to: the target and the device's buffer
* on top, since the device starts empty and the pipe is to hold a
* buffer's worth ahead of it. The same figure is the line above which
* audio is late after an underrun: trimming to the target alone threw
* the device's share away on the first pass after every prime, and the
* pipe then ran a device buffer short of what it had just been filled
* to. Zero when there is no target. */
static size_t audio_driver_pipe_prime_frames(audio_driver_state_t *audio_st)
{
size_t target = audio_driver_pipe_target_frames(audio_st);
size_t frame_bytes, device, room, limit;
if (!target)
return 0;
frame_bytes = audio_driver_dev_frame_bytes(audio_st);
device = (size_t)((double)audio_st->buffer_size / frame_bytes
/ audio_st->src_ratio_orig);
room = audio_st->pipe_ring.capacity / audio_st->pipe_frame_bytes;
limit = room > audio_st->pipe_pass_frames
? room - audio_st->pipe_pass_frames : 1;
target += device;
return target > limit ? limit : target;
}
#endif
/* Input and output do not overlap. Convert only the two populated slots. */
@@ -5546,9 +5612,8 @@ static void audio_driver_submit_width(audio_driver_state_t *audio_st,
* whatever the consumer flushes next: mark where that starts. */
if (from_core && audio_st->fade_in_pending)
{
retro_atomic_store_release_size(&audio_st->pipe_fade_in_at,
audio_driver_pipe_fade_publish(audio_st, true,
retro_atomic_load_relaxed_size(&audio_st->pipe_ring.head));
retro_atomic_store_release_int(&audio_st->pipe_fade_in_set, 1);
audio_st->fade_in_pending = false;
}
@@ -5587,7 +5652,8 @@ static void audio_driver_submit_width(audio_driver_state_t *audio_st,
: (AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_NONBLOCK) != 0)
break;
if ( !(AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_STARTED)
|| audio_st->pipe_consumer_gone)
|| retro_atomic_load_acquire_int(
&audio_st->pipe_consumer_gone))
break;
/* Sleep until the consumer has completed a pass. The
* generation is taken first, then the wait window is opened
@@ -6075,6 +6141,27 @@ bool audio_driver_pipeline_transport_step(struct audio_pipeline_stretch *stage,
output_budget, finishing, complete, &progress);
}
/* The consumer's account of the core, kept for the teardown line:
* a pass that starts with the pipe at its lowest so far, and a wait
* for source that ran after priming, with how long it took. */
static void audio_driver_pipe_note_held(audio_driver_state_t *st, size_t held)
{
size_t cur = retro_atomic_load_relaxed_size(&st->pipe_held_min1);
if (!cur || held + 1 < cur)
retro_atomic_store_release_size(&st->pipe_held_min1, held + 1);
}
static void audio_driver_pipe_note_wait(audio_driver_state_t *st,
retro_time_t began)
{
retro_time_t took = cpu_features_get_time_usec() - began;
retro_atomic_fetch_add_size(&st->pipe_source_waits, 1);
if (took > 0 && (size_t)took
> retro_atomic_load_relaxed_size(&st->pipe_source_wait_max_us))
retro_atomic_store_release_size(&st->pipe_source_wait_max_us,
(size_t)took);
}
static bool audio_driver_transport_wait(audio_driver_state_t *st, size_t need,
unsigned metadata_threshold)
{
@@ -6103,8 +6190,13 @@ static bool audio_driver_transport_wait(audio_driver_state_t *st, size_t need,
|| retro_atomic_load_acquire_int(&st->pipe_wake))
retro_eventcount_cancel_wait(&st->pipe_data);
else
{
retro_time_t began = st->pipe_priming ? 0 : cpu_features_get_time_usec();
retro_eventcount_commit_wait_timeout(&st->pipe_data, key,
AUDIO_PIPE_WAIT_MAX_US);
if (began)
audio_driver_pipe_note_wait(st, began);
}
if (retro_atomic_load_acquire_int(&st->pipe_wake))
{
retro_atomic_store_release_int(&st->pipe_wake, 0);
@@ -6137,26 +6229,28 @@ static void audio_driver_transport_consume(audio_driver_state_t *st)
size_t seen = audio->underruns(st->context_audio_data);
if (seen != st->pipe_underruns_seen)
{
size_t target = audio_driver_pipe_target_frames(st);
size_t target = audio_driver_pipe_prime_frames(st);
if (!audio_driver_pipeline_transport_discard(held > target ? held - target : 0))
return;
st->pipe_underruns_seen = seen;
held = retro_spsc_read_avail(&st->pipe_ring) / st->pipe_frame_bytes;
/* Silence with the pipe short is the core having been late,
* and the cushion the pipe held ahead of the device is what
* the stall burned. Nothing rebuilds it on this path - no
* rate control, and a core paced to the display never runs
* ahead - so from here every ordinary bit of jitter would be
* a hole. Prime again: the same wait the first pass made,
* while the device is silent anyway, so the next stall meets
* a full cushion. */
if (held < target)
st->pipe_priming = true;
}
}
if (st->pipe_priming)
{
size_t target = audio_driver_pipe_target_frames(st);
size_t need = 1;
if (target)
{
size_t room = st->pipe_ring.capacity / st->pipe_frame_bytes;
size_t device = (size_t)((double)st->buffer_size
/ audio_driver_dev_frame_bytes(st) / st->src_ratio_orig);
size_t limit = room > st->pipe_pass_frames ? room - st->pipe_pass_frames : 1;
need = target + device;
if (need > limit) need = limit;
}
size_t need = audio_driver_pipe_prime_frames(st);
if (!need)
need = 1;
if (!audio_driver_transport_wait(st, need * st->pipe_frame_bytes,
AUDIO_PIPELINE_LAYOUT_CAPACITY))
return;
@@ -6166,6 +6260,8 @@ static void audio_driver_transport_consume(audio_driver_state_t *st)
&& audio_pipeline_stretch_needs_input(st->pipe_transport)
&& !audio_driver_transport_wait(st, st->pipe_frame_bytes, 1))
return;
if (!st->pipe_priming)
audio_driver_pipe_note_held(st, held);
/* What a pause left in the ring is stale behind its tail, and so is
* what the stage holds: taken out unplayed, up to where the pause was
* published. */
@@ -6191,20 +6287,23 @@ static void audio_driver_transport_consume(audio_driver_state_t *st)
/* Never across the point the core's audio resumes at: there the
* stage drops the silence it holds and the ramp is armed for what
* it renders next. */
if (retro_atomic_load_acquire_int(&st->pipe_fade_in_set))
{
size_t at = retro_atomic_load_acquire_size(&st->pipe_fade_in_at);
if (audio_driver_pipe_ahead(st, tail, at))
size_t at;
unsigned seq;
if (audio_driver_pipe_fade_pending(st, &at, &seq))
{
size_t upto = (at - tail) / st->pipe_frame_bytes;
if (input_budget > upto)
input_budget = upto;
}
else
{
audio_driver_transport_discard(0, false);
st->pipe_arm_fade = true;
retro_atomic_store_release_int(&st->pipe_fade_in_set, 0);
if (audio_driver_pipe_ahead(st, tail, at))
{
size_t upto = (at - tail) / st->pipe_frame_bytes;
if (input_budget > upto)
input_budget = upto;
}
else
{
audio_driver_transport_discard(0, false);
st->pipe_arm_fade = true;
st->pipe_fade_in_seen = seq;
}
}
}
}
@@ -6271,18 +6370,9 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
size_t need = audio_st->pipe_frame_bytes;
if (audio_st->pipe_priming)
{
size_t target = audio_driver_pipe_target_frames(audio_st);
if (target)
{
size_t frame_bytes = audio_driver_dev_frame_bytes(audio_st);
size_t device = (size_t)((double)audio_st->buffer_size / frame_bytes
/ audio_st->src_ratio_orig);
size_t room = audio_st->pipe_ring.capacity / audio_st->pipe_frame_bytes;
target += device;
if (target > room - audio_st->pipe_pass_frames)
target = room - audio_st->pipe_pass_frames;
need = target * audio_st->pipe_frame_bytes;
}
size_t prime = audio_driver_pipe_prime_frames(audio_st);
if (prime)
need = prime * audio_st->pipe_frame_bytes;
}
while (retro_spsc_read_avail(&audio_st->pipe_ring) < need)
{
@@ -6319,15 +6409,25 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
retro_eventcount_cancel_wait(&audio_st->pipe_data);
continue;
}
if (!retro_eventcount_commit_wait_timeout(&audio_st->pipe_data, key,
AUDIO_PIPE_WAIT_MAX_US))
{
/* Nothing came within the bound. A producer that has stopped
* is the wrapper's business, not this pass's. */
if (retro_atomic_load_acquire_int(&audio_st->pipe_data_gen) == gen)
return;
retro_time_t began = audio_st->pipe_priming
? 0 : cpu_features_get_time_usec();
bool woken = retro_eventcount_commit_wait_timeout(&audio_st->pipe_data,
key, AUDIO_PIPE_WAIT_MAX_US);
if (began)
audio_driver_pipe_note_wait(audio_st, began);
if (!woken)
{
/* Nothing came within the bound. A producer that has
* stopped is the wrapper's business, not this pass's. */
if (retro_atomic_load_acquire_int(&audio_st->pipe_data_gen) == gen)
return;
}
}
}
if (!audio_st->pipe_priming)
audio_driver_pipe_note_held(audio_st,
retro_spsc_read_avail(&audio_st->pipe_ring) / audio_st->pipe_frame_bytes);
audio_st->pipe_priming = false;
}
@@ -6399,7 +6499,7 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
size_t seen = audio->underruns(audio_st->context_audio_data);
if (seen != audio_st->pipe_underruns_seen)
{
size_t target = audio_driver_pipe_target_frames(audio_st) * audio_st->pipe_frame_bytes;
size_t target = audio_driver_pipe_prime_frames(audio_st) * audio_st->pipe_frame_bytes;
size_t held = retro_spsc_read_avail(&audio_st->pipe_ring);
bool discarded = false;
audio_st->pipe_underruns_seen = seen;
@@ -6421,6 +6521,17 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
audio_driver_reset_resamplers(audio_st);
audio_driver_state_unlock();
}
/* Silence with the pipe short is the core having been late,
* and what the stall burned is the cushion the pipe held
* ahead of the device; nothing on this path rebuilds it.
* Prime again on the next pass - the wait the first pass
* made, while the device is silent anyway - so the next
* stall meets a full cushion rather than an empty pipe. */
if (held < target)
{
audio_st->pipe_priming = true;
return;
}
if (have > held / audio_st->pipe_frame_bytes)
have = held / audio_st->pipe_frame_bytes;
}
@@ -6466,19 +6577,22 @@ static void audio_driver_pipeline_consume(audio_driver_state_t *audio_st)
}
/* Never across the point the core's audio resumes at: the ramp is
* armed for the chunk that starts there. */
if (retro_atomic_load_acquire_int(&audio_st->pipe_fade_in_set))
{
size_t at = retro_atomic_load_acquire_size(&audio_st->pipe_fade_in_at);
if (audio_driver_pipe_ahead(audio_st, tail, at))
size_t at;
unsigned seq;
if (audio_driver_pipe_fade_pending(audio_st, &at, &seq))
{
size_t upto = (at - tail) / audio_st->pipe_frame_bytes;
if (have > upto)
have = upto;
}
else
{
audio_st->pipe_arm_fade = true;
retro_atomic_store_release_int(&audio_st->pipe_fade_in_set, 0);
if (audio_driver_pipe_ahead(audio_st, tail, at))
{
size_t upto = (at - tail) / audio_st->pipe_frame_bytes;
if (have > upto)
have = upto;
}
else
{
audio_st->pipe_arm_fade = true;
audio_st->pipe_fade_in_seen = seq;
}
}
}
}
@@ -6551,13 +6665,11 @@ static void audio_driver_record_push(audio_driver_state_t *audio_st,
capacity = frames > (AUDIO_CHUNK_SIZE_NONBLOCKING >> 1)
? (AUDIO_CHUNK_SIZE_NONBLOCKING >> 1) : frames;
need = capacity * (channels > rchannels ? channels : rchannels);
if (need > audio_st->record_remap_frames)
{
int16_t *nb = (int16_t*)realloc(audio_st->record_remap, need * 2 * sizeof(int16_t));
if (!nb) return;
audio_st->record_remap = nb;
audio_st->record_remap_frames = need;
}
/* The region is carved out of arena_int16 at init for
* AUDIO_RECORD_REMAP_CHANNELS per frame; wider than that is not a
* layout either side can declare, so this is a check, not a grow. */
if (!audio_st->record_remap || need > audio_st->record_remap_frames)
return;
while (frames)
{
size_t n = frames > capacity ? capacity : frames;
@@ -6733,18 +6845,11 @@ size_t audio_driver_sample_batch(const int16_t *data, size_t frames)
static bool audio_driver_multi_fold_room(audio_driver_state_t *audio_st,
size_t frames, size_t sample)
{
if (frames > audio_st->multi_fold_frames)
{
/* float and int16 batches share the buffer: sized for float,
* the larger */
void *n = realloc(audio_st->multi_fold, frames * 2 * sizeof(float));
if (!n)
return false;
audio_st->multi_fold = n;
audio_st->multi_fold_frames = frames;
}
/* The region is carved out of arena_float by
* audio_driver_init_internal() at AUDIO_MULTI_FOLD_FRAMES frames;
* callers slice to that bound, so this is a check, never a grow. */
(void)sample;
return true;
return audio_st->multi_fold && frames <= audio_st->multi_fold_frames;
}
static bool audio_driver_multi_layout_ok(unsigned channels, unsigned layout)
@@ -7766,6 +7871,7 @@ bool audio_driver_mixer_add_stream(audio_mixer_stream_params_t *params)
if (audio_driver_mixer_use_s16(audio_driver_st.stat_core_is_float))
voice = audio_mixer_play_s16(handle, looped,
GAIN_TO_Q16(params->volume),
audio_driver_st.resampler_ident,
audio_driver_st.resampler_quality, stop_cb);
else
voice = audio_mixer_play(handle, looped, params->volume,
@@ -7856,6 +7962,7 @@ static void audio_driver_mixer_play_stream_internal(
audio_mixer_play_s16(audio_driver_st.mixer_streams[i].handle,
(type == AUDIO_STREAM_STATE_PLAYING_LOOPED) ? true : false,
AUDIO_MIXER_GAIN_UNITY,
audio_driver_st.resampler_ident,
audio_driver_st.resampler_quality,
audio_driver_st.mixer_streams[i].stop_cb);
else
@@ -8301,7 +8408,7 @@ bool audio_driver_start(bool is_shutdown)
}
RARCH_DBG("[Audio] Started audio driver \"%s\" (is_shutdown=%s)\n",
audio->ident, is_shutdown ? "true" : "false");
audio_driver_get_ident(), is_shutdown ? "true" : "false");
return true;
@@ -8735,7 +8842,8 @@ bool audio_driver_stop(void)
audio_driver_state_unlock();
}
AUDIO_FLAGS_CLEAR(audio_st, AUDIO_FLAG_STARTED);
RARCH_DBG("[Audio] Stopped audio driver \"%s\".\n", audio->ident);
RARCH_DBG("[Audio] Stopped audio driver \"%s\".\n",
audio_driver_get_ident());
}
return stopped;
@@ -8904,7 +9012,7 @@ bool audio_compute_buffer_statistics(audio_statistics_t *stats)
}
#ifdef HAVE_MENU
void audio_driver_menu_sample(void)
void audio_driver_menu_sample(bool by_clock)
{
static int16_t samples_buf[1024] = {0};
settings_t *settings = config_get_ptr();
@@ -8930,10 +9038,40 @@ void audio_driver_menu_sample(void)
|| info->sample_rate <= 0.0 || info->fps <= 0.0)
return;
frames = floor(info->sample_rate / info->fps);
if (by_clock)
{
/* What has played since the last feed, carrying the part of a
* frame that did not make a whole one. The first feed is one
* content frame; a gap longer than a tenth of a second is a
* stall or a return to the menu, not time to make up. */
retro_time_t now = cpu_features_get_time_usec();
if (audio_st->menu_feed_last)
{
retro_time_t elapsed = now - audio_st->menu_feed_last;
double owed;
if (elapsed < 0)
elapsed = 0;
else if (elapsed > 100000)
elapsed = 100000;
owed = info->sample_rate
* (double)elapsed / 1000000.0 + audio_st->menu_feed_frac;
frames = floor(owed);
audio_st->menu_feed_frac = owed - frames;
}
audio_st->menu_feed_last = now;
}
else
{
audio_st->menu_feed_last = 0;
audio_st->menu_feed_frac = 0.0;
}
/* Validate before conversion and the stereo sample-count multiply. */
if (!(frames >= 0.0 && frames <= UINT_MAX / 2))
return;
sample_count = (unsigned)frames * 2;
/* Less than a frame has played since the last feed */
if (by_clock && !sample_count)
return;
if ((AUDIO_FLAGS_GET(audio_st) & AUDIO_FLAG_SUSPENDED))
check_flush = false;
@@ -9422,32 +9560,12 @@ static bool mic_driver_open_mic_internal(retro_microphone_t* microphone)
&& microphone->resampler
&& microphone->resampler->short_ident)
{
const char *rs_ident = microphone->resampler->short_ident;
if (string_is_equal(rs_ident, "sinc"))
{
microphone->resampler_data_int16 = sinc_resampler_int16_init(
microphone->orig_ratio,
audio_sinc_int16_quality_map(mic_st->resampler_quality));
microphone->resampler_int16_process = sinc_resampler_int16_process;
microphone->resampler_int16_free = sinc_resampler_int16_free;
}
#ifdef HAVE_NEAREST_RESAMPLER
else if (string_is_equal(rs_ident, "nearest"))
{
microphone->resampler_data_int16 = nearest_resampler_int16_init();
microphone->resampler_int16_process = nearest_resampler_int16_process;
microphone->resampler_int16_free = nearest_resampler_int16_free;
}
#endif
#ifdef HAVE_CC_RESAMPLER
else if (string_is_equal(rs_ident, "cc"))
{
microphone->resampler_data_int16 = cc_resampler_int16_init(
microphone->orig_ratio);
microphone->resampler_int16_process = cc_resampler_int16_process;
microphone->resampler_int16_free = cc_resampler_int16_free;
}
#endif
retro_resampler_int16_t rs;
retro_resampler_int16_new(&rs, microphone->resampler->short_ident,
mic_st->resampler_quality, microphone->orig_ratio, false);
microphone->resampler_data_int16 = rs.data;
microphone->resampler_int16_process = rs.process;
microphone->resampler_int16_free = rs.free;
if (!microphone->resampler_data_int16)
{
microphone->resampler_int16_process = NULL;
@@ -9825,9 +9943,23 @@ static void microphone_driver_capture_thread(void *data)
continue;
}
if (!mic_st->driver->wait_readable(mic_st->driver_context,
microphone->microphone_context, slice * sample_size))
continue;
/* What the device has now, which can be less than was asked for:
* a driver returns short whenever its bounded wait expires with
* something in hand, and alsa returns a period even when the
* slice spans several. read() blocks until it has everything it
* was asked for, so reading the whole slice on a short count
* parks the worker inside the driver - and teardown joins this
* thread unconditionally. Read what is there. */
{
size_t want = slice * sample_size;
size_t got = mic_st->driver->wait_readable(mic_st->driver_context,
microphone->microphone_context, want);
if (got < sample_size)
continue;
if (got < want)
slice = got / sample_size;
}
/* The flush - device read, up-channel, resample, ring write -
* runs without the lock: SPSC, this thread is the producer. The
+52 -14
View File
@@ -31,12 +31,10 @@
* calls into it - so the type has to exist there even though nothing
* uses it. */
#include <rthreads/retro_eventcount.h>
#ifdef HAVE_THREADS
/* Declarations only, with or without threads: the state holds lock and
* condition pointers either way. Declaring the types here instead would
* repeat typedefs a C89 compiler refuses once a file includes both. */
#include <rthreads/rthreads.h>
#else
typedef struct slock slock_t;
typedef struct scond scond_t;
#endif
#include <retro_inline.h>
#include <libretro.h>
#include <retro_miscellaneous.h>
@@ -413,6 +411,12 @@ typedef struct audio_driver
bool (*device_clock_ppm)(void *data, double *ppm);
} audio_driver_t;
/* What a driver's device-clock word holds until it has an estimate.
* Every estimate is kept inside +-100000 ppm, so the estimate and
* whether there is one share one int: one store publishes, one load
* reads, and no reader can pair a flag with a stale value. */
#define AUDIO_CLOCK_PPM_NONE (-0x7FFFFFFF - 1)
/* The layout the user asked for, one of the AUDIO_LAYOUT_ masks. A
* driver opening a device reads this; a driver without the layout
* hook is not asked. */
@@ -546,8 +550,10 @@ typedef struct
/* Written once by the wrapper thread as it leaves its loop, read by
* the producer's wait. Its own field, not a bit in flags: the main
* thread read-modify-writes flags and a second writer would lose
* bits. */
volatile bool pipe_consumer_gone;
* bits. An atomic and not a volatile bool: volatile orders nothing
* between the two, so the producer had no guarantee of seeing the
* consumer's last word at all. */
retro_atomic_int_t pipe_consumer_gone;
/* Throttle channel for audio_sync without vsync: the consumer bumps
* pipe_gen after every pass and notifies pipe_space; a producer that
* found the ring full waits for the generation to change. No lock:
@@ -685,6 +691,15 @@ typedef struct
* pipe's target then, not just a frame. Consumer thread only after
* init. */
bool pipe_priming;
/* How the consumer fared against the core after priming, said once
* at teardown beside the driver's silence count: the passes that
* found the pipe short and had to wait for the core, the longest
* such wait, and the least the pipe held at the start of a pass
* (kept as frames + 1, so zero is no pass yet). Consumer writes,
* the main thread reads at teardown. */
retro_atomic_size_t pipe_source_waits;
retro_atomic_size_t pipe_source_wait_max_us;
retro_atomic_size_t pipe_held_min1;
/* The audio thread's own copy of AUDIO_FLAG_PIPELINE_THREADED. Set
* before the wrapper thread is released and cleared after it is
* joined, so the thread never reads the flags word - which the main
@@ -754,6 +769,13 @@ typedef struct
retro_time_t avg_flush_delta;
double avg_expected_delta;
/* The menu's silence at Menu Frame Rate 'Display Rate' is measured
* out by the clock rather than by the content's frame: when it was
* last fed (0 = not feeding by the clock), and the fraction of a
* frame the last feed left owing. */
retro_time_t menu_feed_last;
double menu_feed_frac;
/* Rate-limit state for the DRC compute.
*
* The DRC ratio is updated approximately once per game-frame's worth
@@ -895,7 +917,7 @@ typedef struct
unsigned out_channels;
/* The multi-channel batch entry (RETRO_ENVIRONMENT_GET_AUDIO_
* SAMPLE_BATCH_MULTI): the layout the core last delivered, and
* the stereo fold of a batch, grown to the largest batch seen. The
* the stereo fold of a batch slice, a fixed region of arena_float. The
* pipeline carries stereo; a core's wider frame is folded here at
* the boundary, and the device's upmix widens the stereo again.
* core_layout is stereo until the core delivers something else,
@@ -980,12 +1002,16 @@ typedef struct
* has been published since. The core's first audio after a resume
* starts at pipe_fade_in_at: the consumer never takes a chunk across
* it and arms the resume ramp on reaching it, through pipe_arm_fade,
* consumer thread only. */
* consumer thread only. pipe_fade_in_mark is (sequence << 1) | armed,
* written by the main thread alone; the consumer records the sequence
* it has acted on in pipe_fade_in_seen instead of clearing the mark,
* so a mark published while it reads is never cleared unseen. */
retro_atomic_size_t pipe_discard_to;
retro_atomic_int_t pipe_discard_gen;
int pipe_discard_seen;
retro_atomic_size_t pipe_fade_in_at;
retro_atomic_int_t pipe_fade_in_set;
retro_atomic_int_t pipe_fade_in_mark;
unsigned pipe_fade_in_seen;
bool pipe_arm_fade;
uint8_t pipe_transport_follow;
/* Mutually exclusive with pipe_transport; shares its output storage. */
@@ -1390,7 +1416,18 @@ size_t audio_driver_sample_batch_rewind(
#endif
#ifdef HAVE_MENU
void audio_driver_menu_sample(void);
/**
* audio_driver_menu_sample:
* @by_clock : feed the silence the clock says has played since the last
* feed, rather than one content frame of it.
*
* Feeds the device silence while the core is not running behind the
* menu, with the menu sounds, the mixer and thumbnail audio mixed in.
* One content frame per call holds a blocking writer to the content's
* rate; by the clock, with a non-blocking writer, the device is kept fed
* at whatever rate the menu runs.
**/
void audio_driver_menu_sample(bool by_clock);
#endif
extern audio_driver_t audio_rsound;
@@ -1402,7 +1439,8 @@ extern audio_driver_t audio_roar;
extern audio_driver_t audio_openal;
extern audio_driver_t audio_opensl;
extern audio_driver_t audio_jack;
extern audio_driver_t audio_sdl;
extern audio_driver_t audio_sdl1;
extern audio_driver_t audio_sdl2;
extern audio_driver_t audio_sdl3;
extern audio_driver_t audio_xa;
extern audio_driver_t audio_pulse;
@@ -1425,11 +1463,11 @@ extern audio_driver_t audio_ps3;
extern audio_driver_t audio_gx;
extern audio_driver_t audio_ax;
extern audio_driver_t audio_psp;
extern audio_driver_t audio_psp2;
extern audio_driver_t audio_ps4;
extern audio_driver_t audio_ps2;
extern audio_driver_t audio_ctr_csnd;
extern audio_driver_t audio_ctr_dsp;
#ifdef HAVE_THREADS
#endif
extern audio_driver_t audio_switch;
extern audio_driver_t audio_switch_libnx_audren;
extern audio_driver_t audio_rwebaudio;
+189 -124
View File
@@ -18,12 +18,15 @@
#include <string.h>
#include <lists/string_list.h>
#include <retro_atomic.h>
#include <rthreads/retro_eventcount.h>
#include <rthreads/rthreads.h>
#include <features/features_cpu.h>
#include "audio_thread_wrapper.h"
#include "audio_driver.h"
#include "../verbosity.h"
#include "../frontend/thread_elevation.h"
/* How long a handshake between the main thread and the audio thread
* may run before it is reported. Both are sub-millisecond on a device
@@ -43,29 +46,48 @@
* request signals the condition and cuts the wait short. */
#define AUDIO_THREAD_IDLE_WAIT_US 1000
enum audio_thread_init_state
{
AUDIO_THREAD_INIT_PENDING = 0,
AUDIO_THREAD_INIT_OK,
AUDIO_THREAD_INIT_FAILED,
AUDIO_THREAD_INIT_ABANDONED
};
typedef struct audio_thread
{
const audio_driver_t *driver;
void *driver_data;
sthread_t *thread;
slock_t *lock;
scond_t *cond;
const char *device;
unsigned *new_rate;
int inited;
/* The frontend stopped waiting for init(): this thread owns its own
* state from there and frees it when init() finally returns. */
bool abandoned;
/* Both threads park here: the audio thread between passes it has
* nothing to do on and while stopped, the main thread for the
* stop acknowledgement and for init() to return. Every change to
* the words below is written first and notified after, so a waiter's
* re-check between prepare and commit sees it. */
retro_eventcount_t ec;
/* The thread's two loop conditions, tested on every pass without
* any lock. alive only ever goes false. */
retro_atomic_int_t alive;
retro_atomic_int_t stopped;
/* The thread is parked in a stopped loop and the driver is stopped;
* block() waits for it. */
retro_atomic_int_t stopped_ack;
/* How init() ended, and who owns this struct: the thread moves it
* from PENDING to OK or FAILED, the frontend from PENDING to
* ABANDONED when it gives up waiting. One compare-and-swap each, so
* exactly one side wins - the other reads what happened and acts on
* that. An abandoned thread frees the struct itself when init()
* finally returns. */
retro_atomic_int_t init_state;
/* Initialization options. */
unsigned out_rate;
unsigned latency;
bool alive;
bool stopped;
bool stopped_ack;
bool is_paused;
bool is_shutdown;
bool use_float;
@@ -77,6 +99,38 @@ typedef struct audio_thread
bool prefer_fast_cores;
} audio_thread_t;
/* Audio thread: parks while stopped, acknowledging the stop so block()
* can return. block() clears the ack before it raises the flag, so an
* ack found clear inside the wait window means a start and a second
* stop landed since this pass acknowledged, and that stop is answered
* before parking. The ack is stored and notified before the window,
* never inside it: a notify from inside would cut the thread's own
* wait short. */
static void audio_thread_park_stopped(audio_thread_t *thr)
{
for (;;)
{
int key;
if (!retro_atomic_load_acquire_int(&thr->stopped))
return;
retro_atomic_store_release_int(&thr->stopped_ack, 1);
retro_eventcount_notify(&thr->ec);
key = retro_eventcount_prepare_wait(&thr->ec);
if (!retro_atomic_load_acquire_int(&thr->stopped))
{
retro_eventcount_cancel_wait(&thr->ec);
return;
}
if (!retro_atomic_load_acquire_int(&thr->stopped_ack))
{
retro_eventcount_cancel_wait(&thr->ec);
continue;
}
retro_eventcount_commit_wait(&thr->ec, key);
}
}
/**
* The thread that manages the life of the audio driver.
* The wrapped audio driver lives and dies with this function.
@@ -86,15 +140,31 @@ static void audio_thread_loop(void *data)
bool is_shutdown;
audio_thread_t *thr = (audio_thread_t*)data;
if (!thr)
return;
sthread_setname("ra-audio");
/* Best effort and never fatal: a refusal leaves the default. */
if (thr->raise_priority)
{
if (sthread_raise_current_priority())
RARCH_LOG("[Audio] Audio thread priority raised.\n");
else
RARCH_LOG("[Audio] Audio thread priority not raised; the system refused or has no such class.\n");
const char *via = NULL;
const char *added = NULL;
switch (thread_elevation_raise_current(&via, &added))
{
case THREAD_ELEVATION_GRANTED:
RARCH_LOG("[Audio] Audio thread priority raised.\n");
break;
case THREAD_ELEVATION_PENDING:
/* Finishing on a thread of its own; this one does not wait. */
RARCH_LOG("[Audio] Audio thread priority not raised directly; asking %s.\n", via);
break;
default:
RARCH_LOG("[Audio] Audio thread priority not raised; the system refused or has no such class.\n");
break;
}
if (added)
RARCH_LOG("[Audio] Audio thread runs on %s.\n", added);
}
if (thr->prefer_fast_cores)
@@ -103,38 +173,32 @@ static void audio_thread_loop(void *data)
RARCH_LOG("[Audio] Audio thread placed on the performance cores.\n");
}
if (!thr)
return;
thr->driver_data = thr->driver->init(
thr->device, thr->out_rate, thr->latency,
thr->new_rate);
slock_lock(thr->lock);
thr->inited = thr->driver_data ? 1 : -1;
if (thr->inited > 0 && thr->driver->use_float)
/* Read by the frontend once it sees the outcome below */
thr->use_float = false;
if (thr->driver_data && thr->driver->use_float)
thr->use_float = thr->driver->use_float(thr->driver_data);
thr->layout = AUDIO_LAYOUT_STEREO;
if (thr->inited > 0 && thr->driver->layout)
if (thr->driver_data && thr->driver->layout)
thr->layout = thr->driver->layout(thr->driver_data);
scond_signal(thr->cond);
/* Publish the outcome, unless the frontend gave up waiting first:
* then nobody is waiting for this any more and nobody else will
* free it. */
if (!retro_atomic_cas_int(&thr->init_state, AUDIO_THREAD_INIT_PENDING,
thr->driver_data ? AUDIO_THREAD_INIT_OK : AUDIO_THREAD_INIT_FAILED))
{
bool abandoned = thr->abandoned;
slock_unlock(thr->lock);
/* Nobody is waiting for this any more, and nobody else will free
* it: init() took longer than the frontend was willing to wait. */
if (abandoned)
{
if (thr->driver_data && thr->driver->free)
thr->driver->free(thr->driver_data);
slock_free(thr->lock);
scond_free(thr->cond);
free(thr);
return;
}
if (thr->driver_data && thr->driver->free)
thr->driver->free(thr->driver_data);
retro_eventcount_free(&thr->ec);
free(thr);
return;
}
retro_eventcount_notify(&thr->ec);
if (thr->inited < 0)
if (!thr->driver_data)
return;
/* Wait until we start to avoid calling
@@ -144,15 +208,8 @@ static void audio_thread_loop(void *data)
* this loop rather than the one below. block() waits for the
* acknowledgement whichever loop the thread is in, so this one
* gives it too. */
slock_lock(thr->lock);
while (thr->stopped)
{
thr->stopped_ack = true;
scond_signal(thr->cond);
scond_wait(thr->cond, thr->lock);
}
audio_thread_park_stopped(thr);
is_shutdown = thr->is_shutdown;
slock_unlock(thr->lock);
/* The loop below only calls the driver's start() when it comes out
* of a stop; the initial start has to be made here, on this thread,
@@ -161,41 +218,37 @@ static void audio_thread_loop(void *data)
for (;;)
{
slock_lock(thr->lock);
if (!thr->alive)
/* Tested without the lock: a pass that is neither leaving nor
* parking has nothing to say to the main thread, and that is
* every pass while audio is playing. A request that lands just
* after either test is taken on the next pass, which is where it
* was taken before. */
if (!retro_atomic_load_acquire_int(&thr->alive))
{
scond_signal(thr->cond);
thr->stopped_ack = true;
slock_unlock(thr->lock);
retro_atomic_store_release_int(&thr->stopped_ack, 1);
retro_eventcount_notify(&thr->ec);
break;
}
if (thr->stopped)
if (retro_atomic_load_acquire_int(&thr->stopped))
{
thr->driver->stop(thr->driver_data);
while (thr->stopped)
{
/* If we stop right after start,
* we might not be able to properly ack.
* Signal in the loop instead. */
thr->stopped_ack = true;
scond_signal(thr->cond);
scond_wait(thr->cond, thr->lock);
}
audio_thread_park_stopped(thr);
thr->driver->start(thr->driver_data, thr->is_shutdown);
}
slock_unlock(thr->lock);
if (!audio_driver_callback())
{
slock_lock(thr->lock);
if (thr->alive && !thr->stopped)
scond_wait_timeout(thr->cond, thr->lock,
/* The re-check between prepare and commit is what keeps a
* request that lands here from going unseen until the
* timeout */
int key = retro_eventcount_prepare_wait(&thr->ec);
if ( retro_atomic_load_acquire_int(&thr->alive)
&& !retro_atomic_load_acquire_int(&thr->stopped))
retro_eventcount_commit_wait_timeout(&thr->ec, key,
AUDIO_THREAD_IDLE_WAIT_US);
slock_unlock(thr->lock);
else
retro_eventcount_cancel_wait(&thr->ec);
}
}
@@ -212,23 +265,19 @@ static void audio_thread_block(audio_thread_t *thr)
if (!thr)
return;
if (thr->stopped)
if (retro_atomic_load_acquire_int(&thr->stopped))
return;
slock_lock(thr->lock);
/* alive goes false when the thread is told to leave its loop, or
* leaves it on its own after a failed write. Either way it will not
* acknowledge anything again, and the wait below would never end.
* There is nothing running to park, so there is nothing to wait
* for. */
if (!thr->alive)
{
slock_unlock(thr->lock);
if (!retro_atomic_load_acquire_int(&thr->alive))
return;
}
thr->stopped_ack = false;
thr->stopped = true;
scond_signal(thr->cond);
retro_atomic_store_release_int(&thr->stopped_ack, 0);
retro_atomic_store_release_int(&thr->stopped, 1);
retro_eventcount_notify(&thr->ec);
/* The thread may be asleep in the pipeline waiting for data; wake it
* so it comes back to the loop and acknowledges now rather than
* after its timeout. */
@@ -238,12 +287,22 @@ static void audio_thread_block(audio_thread_t *thr)
* abandoned: callers past it act as though the thread is parked,
* and free() joins the thread either way, so a device that never
* returns from a write still stops here. It is reported, so the log
* names what the frontend is waiting on. */
* names what the frontend is waiting on. A thread that leaves its
* loop meanwhile acks on the way out. */
{
bool warned = false;
while (!thr->stopped_ack)
for (;;)
{
if (scond_wait_timeout(thr->cond, thr->lock,
int key;
if (retro_atomic_load_acquire_int(&thr->stopped_ack))
break;
key = retro_eventcount_prepare_wait(&thr->ec);
if (retro_atomic_load_acquire_int(&thr->stopped_ack))
{
retro_eventcount_cancel_wait(&thr->ec);
break;
}
if (retro_eventcount_commit_wait_timeout(&thr->ec, key,
AUDIO_THREAD_HANDSHAKE_WARN_US))
continue;
if (!warned)
@@ -255,8 +314,6 @@ static void audio_thread_block(audio_thread_t *thr)
}
}
}
slock_unlock(thr->lock);
}
/**
@@ -268,10 +325,8 @@ static void audio_thread_unblock(audio_thread_t *thr)
if (!thr)
return;
slock_lock(thr->lock); /* Prevent the audio thread from touching this flag... */
thr->stopped = false; /* ...so that the main thread can do it. */
scond_signal(thr->cond); /* Then let the audio thread know that it's okay to resume. */
slock_unlock(thr->lock); /* "As you were." */
retro_atomic_store_release_int(&thr->stopped, 0);
retro_eventcount_notify(&thr->ec);
}
static void audio_thread_free(void *data)
@@ -283,11 +338,11 @@ static void audio_thread_free(void *data)
if (thr->thread)
{
slock_lock(thr->lock); /* Let the audio thread finish what it's doing... */
thr->stopped = false; /* Then stop it. "You're fired." */
thr->alive = false;
scond_signal(thr->cond); /* Let the thread know it's okay to continue */
slock_unlock(thr->lock); /* At this point, it will exit its loop. */
/* Let the thread out of whichever wait it is in; it leaves its
* loop on seeing alive false. */
retro_atomic_store_release_int(&thr->stopped, 0);
retro_atomic_store_release_int(&thr->alive, 0);
retro_eventcount_notify(&thr->ec);
/* It may be asleep in the pipeline waiting for data; wake it so
* it sees alive == false now rather than after its timeout. */
@@ -297,10 +352,7 @@ static void audio_thread_free(void *data)
* (It will call the wrapped driver's free() function.) */
}
if (thr->lock)
slock_free(thr->lock);
if (thr->cond)
scond_free(thr->cond);
retro_eventcount_free(&thr->ec);
free(thr);
/* The audio driver is done, clean up the thread itself. */
}
@@ -316,7 +368,7 @@ static bool audio_thread_alive(void *data)
/* A thread that has ended after a failed write reports the device
* as not alive, which is what it is; block() below is a no-op then,
* and the answer has to come from somewhere. */
if (!thr->alive)
if (!retro_atomic_load_acquire_int(&thr->alive))
return false;
audio_thread_block(thr);
@@ -333,9 +385,7 @@ void audio_thread_apply_control(void *data,
bool running;
if (!thr || !control)
return;
slock_lock(thr->lock);
running = !thr->stopped;
slock_unlock(thr->lock);
running = !retro_atomic_load_acquire_int(&thr->stopped);
audio_thread_block(thr);
control(userdata);
if (running)
@@ -369,10 +419,10 @@ static bool audio_thread_start(void *data, bool is_shutdown)
audio_driver_enable_callback();
slock_lock(thr->lock);
/* Written before the release store that clears stopped, which the
* thread acquire-loads before it reads is_shutdown */
thr->is_paused = false;
thr->is_shutdown = is_shutdown;
slock_unlock(thr->lock);
audio_thread_unblock(thr);
return true;
@@ -490,10 +540,8 @@ static ssize_t audio_thread_write(void *data, const void *s, size_t len)
_len = thr->driver->write(thr->driver_data, s, len);
if (_len < 0)
{
slock_lock(thr->lock);
thr->alive = false;
scond_signal(thr->cond);
slock_unlock(thr->lock);
retro_atomic_store_release_int(&thr->alive, 0);
retro_eventcount_notify(&thr->ec);
}
return _len;
}
@@ -600,13 +648,13 @@ bool audio_init_thread(const audio_driver_t **out_driver,
thr->new_rate = new_rate;
thr->latency = latency;
if (!(thr->cond = scond_new()))
goto error;
if (!(thr->lock = slock_new()))
if (!retro_eventcount_init(&thr->ec))
goto error;
thr->alive = true;
thr->stopped = true;
retro_atomic_int_init(&thr->alive, 1);
retro_atomic_int_init(&thr->stopped, 1);
retro_atomic_int_init(&thr->stopped_ack, 0);
retro_atomic_int_init(&thr->init_state, AUDIO_THREAD_INIT_PENDING);
if (!(thr->thread = sthread_create(audio_thread_loop, thr)))
goto error;
@@ -617,27 +665,44 @@ bool audio_init_thread(const audio_driver_t **out_driver,
* with no way out. Past the deadline the thread is told it owns its
* own state and this returns without it - freeing it here would pull
* it out from under a thread still using it. */
slock_lock(thr->lock);
{
bool warned = false;
retro_time_t giveup = cpu_features_get_time_usec()
+ AUDIO_THREAD_HANDSHAKE_GIVEUP_US;
while (!thr->inited)
for (;;)
{
retro_time_t now = cpu_features_get_time_usec();
int key;
retro_time_t now;
if (retro_atomic_load_acquire_int(&thr->init_state)
!= AUDIO_THREAD_INIT_PENDING)
break;
now = cpu_features_get_time_usec();
if (now >= giveup)
{
thr->abandoned = true;
slock_unlock(thr->lock);
RARCH_ERR("[Audio] Driver \"%s\" did not return from init after %d seconds; going on without audio.\n",
thr->driver->ident ? thr->driver->ident : "?",
(int)(AUDIO_THREAD_HANDSHAKE_GIVEUP_US / 1000000));
sthread_detach(thr->thread);
*out_driver = NULL;
*out_data = NULL;
return false;
/* The thread may publish in this same instant: the
* exchange decides, and a lost one means init() has just
* returned and the state is this thread's after all. */
if (retro_atomic_cas_int(&thr->init_state,
AUDIO_THREAD_INIT_PENDING, AUDIO_THREAD_INIT_ABANDONED))
{
RARCH_ERR("[Audio] Driver \"%s\" did not return from init after %d seconds; going on without audio.\n",
thr->driver->ident ? thr->driver->ident : "?",
(int)(AUDIO_THREAD_HANDSHAKE_GIVEUP_US / 1000000));
sthread_detach(thr->thread);
*out_driver = NULL;
*out_data = NULL;
return false;
}
continue;
}
if (scond_wait_timeout(thr->cond, thr->lock,
key = retro_eventcount_prepare_wait(&thr->ec);
if (retro_atomic_load_acquire_int(&thr->init_state)
!= AUDIO_THREAD_INIT_PENDING)
{
retro_eventcount_cancel_wait(&thr->ec);
break;
}
if (retro_eventcount_commit_wait_timeout(&thr->ec, key,
AUDIO_THREAD_HANDSHAKE_WARN_US))
continue;
if (!warned)
@@ -649,9 +714,9 @@ bool audio_init_thread(const audio_driver_t **out_driver,
}
}
}
slock_unlock(thr->lock);
if (thr->inited < 0) /* Thread failed. */
if (retro_atomic_load_acquire_int(&thr->init_state)
!= AUDIO_THREAD_INIT_OK) /* Thread failed. */
goto error;
*out_driver = &audio_thread;
+111 -110
View File
@@ -29,6 +29,7 @@
#include <stdlib.h>
#include <lists/string_list.h>
#include <retro_atomic.h>
#ifdef HAVE_CONFIG_H
#include "../../config.h"
@@ -60,37 +61,25 @@ typedef struct alsa_microphone_handle
alsa_stream_info_t stream_info;
} alsa_microphone_handle_t;
typedef struct alsa_microphone
{
bool nonblock;
} alsa_microphone_t;
/* The microphone driver context carries nothing of its own: what
* init() and free() set up and tear down is ALSA's global state, and
* the frontend only needs a handle to hand back. The driver itself is
* that handle. */
extern microphone_driver_t microphone_alsa;
static void *alsa_microphone_init(void)
{
alsa_microphone_t *alsa = (alsa_microphone_t*)calloc(1, sizeof(alsa_microphone_t));
if (!alsa)
{
RARCH_ERR("[ALSA] Failed to allocate driver context.\n");
return NULL;
}
RARCH_LOG("[ALSA] Using ALSA version %s.\n", snd_asoundlib_version());
return alsa;
return (void*)&microphone_alsa;
}
static void alsa_microphone_close_mic(void *driver_context, void *mic_context);
static void alsa_microphone_free(void *driver_context)
{
alsa_microphone_t *alsa = (alsa_microphone_t*)driver_context;
/* The mic frontend should've closed all mics before calling free(). */
if (alsa)
{
if (driver_context)
snd_config_update_free_global();
free(alsa);
}
}
static bool alsa_microphone_start_mic(void *driver_context, void *mic_context);
@@ -117,13 +106,15 @@ static int alsa_microphone_read(void *driver_context, void *mic_context, void *s
{
snd_pcm_sframes_t size;
snd_pcm_state_t state;
alsa_microphone_t *alsa = (alsa_microphone_t*)driver_context;
alsa_microphone_handle_t *mic = (alsa_microphone_handle_t*)mic_context;
uint8_t *buf = (uint8_t*)s;
snd_pcm_sframes_t read = 0;
int errnum = 0;
bool eagain_retry = true;
int laps = ALSA_WAIT_READABLE_LAPS;
int timeout_ms;
if (!alsa || !mic || !buf)
if (!driver_context || !mic || !buf)
return -1;
size = BYTES_TO_FRAMES(len, mic->stream_info.frame_bits);
@@ -146,88 +137,55 @@ static int alsa_microphone_read(void *driver_context, void *mic_context, void *s
}
}
if (alsa->nonblock)
timeout_ms = alsa_microphone_wait_ms(mic);
while (size)
{
while (size)
snd_pcm_sframes_t frames;
int rc = snd_pcm_wait(mic->pcm, timeout_ms);
/* Nothing delivered in the time two periods take. A device that
* has stopped must not park the caller, so give up after a
* bounded number of these and return what there is. */
if (rc == 0)
{
snd_pcm_sframes_t frames = snd_pcm_readi(mic->pcm, buf, size);
if (frames == -EPIPE || frames == -EINTR || frames == -ESTRPIPE)
{
errnum = snd_pcm_recover(mic->pcm, frames, 0);
if (errnum < 0)
{
RARCH_ERR("[ALSA] Failed to read from microphone: %s.\n", snd_strerror(frames));
RARCH_ERR("[ALSA] Additionally, recovery failed with: %s.\n", snd_strerror(errnum));
return -1;
}
if (--laps < 0)
break;
}
else if (frames == -EAGAIN)
break;
else if (frames < 0)
continue;
}
if (rc == -EPIPE || rc == -ESTRPIPE || rc == -EINTR)
{
if (snd_pcm_recover(mic->pcm, rc, 1) < 0)
return -1;
continue;
}
frames = snd_pcm_readi(mic->pcm, buf, size);
if (frames == -EPIPE || frames == -EINTR || frames == -ESTRPIPE)
{
if (snd_pcm_recover(mic->pcm, frames, 1) < 0)
return -1;
read += frames;
buf += FRAMES_TO_BYTES(frames, mic->stream_info.frame_bits);
size -= frames;
break;
}
}
else
{
bool eagain_retry = true;
int laps = ALSA_WAIT_READABLE_LAPS;
int timeout_ms = alsa_microphone_wait_ms(mic);
while (size)
else if (frames == -EAGAIN)
{
snd_pcm_sframes_t frames;
int rc = snd_pcm_wait(mic->pcm, timeout_ms);
/* Nothing delivered in the time two periods take. A device that
* has stopped must not park the caller, so give up after a
* bounded number of these and return what there is. */
if (rc == 0)
/* Definitely not supposed to happen. */
if (eagain_retry)
{
if (--laps < 0)
break;
eagain_retry = false;
continue;
}
if (rc == -EPIPE || rc == -ESTRPIPE || rc == -EINTR)
{
if (snd_pcm_recover(mic->pcm, rc, 1) < 0)
return -1;
continue;
}
frames = snd_pcm_readi(mic->pcm, buf, size);
if (frames == -EPIPE || frames == -EINTR || frames == -ESTRPIPE)
{
if (snd_pcm_recover(mic->pcm, frames, 1) < 0)
return -1;
break;
}
else if (frames == -EAGAIN)
{
/* Definitely not supposed to happen. */
if (eagain_retry)
{
eagain_retry = false;
continue;
}
break;
}
else if (frames < 0)
return -1;
read += frames;
buf += FRAMES_TO_BYTES(frames, mic->stream_info.frame_bits);
size -= frames;
break;
}
else if (frames < 0)
return -1;
read += frames;
buf += FRAMES_TO_BYTES(frames, mic->stream_info.frame_bits);
size -= frames;
}
return FRAMES_TO_BYTES(read, mic->stream_info.frame_bits);
@@ -244,12 +202,6 @@ static bool alsa_microphone_mic_alive(const void *driver_context, const void *mi
return snd_pcm_state(mic->pcm) == SND_PCM_STATE_RUNNING;
}
static void alsa_microphone_set_nonblock_state(void *driver_context, bool nonblock)
{
alsa_microphone_t *alsa = (alsa_microphone_t*)driver_context;
alsa->nonblock = nonblock;
}
static struct string_list *alsa_microphone_device_list_new(const void *data)
{
return alsa_device_list_type_new("Input");
@@ -267,10 +219,9 @@ static void *alsa_microphone_open_mic(void *driver_context,
unsigned latency,
unsigned *new_rate)
{
alsa_microphone_t *alsa = (alsa_microphone_t*)driver_context;
alsa_microphone_handle_t *mic = NULL;
if (!alsa) /* If we weren't given a valid ALSA context... */
if (!driver_context) /* If we weren't given a valid ALSA context... */
return NULL;
/* If the microphone context couldn't be allocated... */
@@ -287,7 +238,7 @@ static void *alsa_microphone_open_mic(void *driver_context,
error:
RARCH_ERR("[ALSA] Failed to initialize microphone.\n");
alsa_microphone_close_mic(alsa, mic);
alsa_microphone_close_mic(driver_context, mic);
return NULL;
@@ -400,7 +351,6 @@ microphone_driver_t microphone_alsa = {
alsa_microphone_init,
alsa_microphone_free,
alsa_microphone_read,
alsa_microphone_set_nonblock_state,
"alsa",
alsa_microphone_device_list_new,
alsa_microphone_device_list_free,
@@ -422,6 +372,10 @@ typedef struct alsa
/* Frames snd_pcm_writei() accepted since open; less what the device
* still holds, it is what the device has consumed. */
uint64_t frames_written;
/* Xruns the device reported on the write path: -EPIPE is the
* device having run out of audio, which -EINTR and -ESTRPIPE
* beside it are not. */
retro_atomic_size_t underruns;
bool nonblock;
/* Stopped, as the frontend sees it: alive() is its inverse. Held
* says how: the stream paused with its buffer kept, or dropped,
@@ -466,8 +420,20 @@ typedef struct alsa
double clk_a_sx, clk_a_sy, clk_a_sxx, clk_a_sxy, clk_a_n;
int clk_a_ppm;
int clk_a_valid;
/* Raised wherever the device stops and starts again - a pause, a
* resume, an xrun recovery - from whichever thread that happens on.
* Its position stood still meanwhile while time did not, so both
* fits start again from the writer's next sample; the last
* published estimate stands until the new window publishes one. */
retro_atomic_int_t clk_rebase;
} alsa_t;
static int alsa_recover(alsa_t *alsa, int err)
{
retro_atomic_store_release_int(&alsa->clk_rebase, 1);
return snd_pcm_recover(alsa->pcm, err, 1);
}
/* The layout the device actually has, from its channel map; the
* requested one where the map could not be read; stereo where the
* device would not open wider. */
@@ -549,6 +515,7 @@ static bool alsa_start(void *data, bool is_shutdown)
}
alsa->is_paused = false;
alsa->held = false;
retro_atomic_store_release_int(&alsa->clk_rebase, 1);
return true;
}
@@ -573,7 +540,9 @@ static ssize_t alsa_write(void *data, const void *buf_, size_t len)
if (frames == -EPIPE || frames == -EINTR || frames == -ESTRPIPE)
{
if (snd_pcm_recover(alsa->pcm, frames, 1) < 0)
if (frames == -EPIPE)
retro_atomic_fetch_add_size(&alsa->underruns, 1);
if (alsa_recover(alsa, frames) < 0)
return -1;
break;
@@ -611,7 +580,9 @@ static ssize_t alsa_write(void *data, const void *buf_, size_t len)
if (frames == -EPIPE || frames == -EINTR || frames == -ESTRPIPE)
{
if (snd_pcm_recover(alsa->pcm, frames, 1) < 0)
if (frames == -EPIPE)
retro_atomic_fetch_add_size(&alsa->underruns, 1);
if (alsa_recover(alsa, frames) < 0)
return -1;
break;
}
@@ -623,7 +594,7 @@ static ssize_t alsa_write(void *data, const void *buf_, size_t len)
rc = snd_pcm_wait(alsa->pcm, wait_ms);
if (rc == -EPIPE || rc == -ESTRPIPE || rc == -EINTR)
{
if (snd_pcm_recover(alsa->pcm, rc, 1) < 0)
if (alsa_recover(alsa, rc) < 0)
return -1;
}
continue;
@@ -662,6 +633,7 @@ static bool alsa_stop(void *data)
{
alsa->is_paused = true;
alsa->held = true;
retro_atomic_store_release_int(&alsa->clk_rebase, 1);
return true;
}
ret = snd_pcm_drop(alsa->pcm);
@@ -672,6 +644,7 @@ static bool alsa_stop(void *data)
}
alsa->is_paused = true;
alsa->held = false;
retro_atomic_store_release_int(&alsa->clk_rebase, 1);
return true;
}
@@ -770,7 +743,7 @@ static size_t alsa_wait_writable(void *data, size_t len)
if (avail == -EPIPE || avail == -ESTRPIPE || avail == -EINTR)
{
if (snd_pcm_recover(alsa->pcm, (int)avail, 1) < 0)
if (alsa_recover(alsa, (int)avail) < 0)
return 0;
if (--laps < 0)
return 0;
@@ -786,7 +759,7 @@ static size_t alsa_wait_writable(void *data, size_t len)
rc = snd_pcm_start(alsa->pcm);
if (rc == -EPIPE || rc == -ESTRPIPE || rc == -EINTR)
{
if (snd_pcm_recover(alsa->pcm, rc, 1) < 0)
if (alsa_recover(alsa, rc) < 0)
return 0;
}
else if (rc < 0)
@@ -798,7 +771,7 @@ static size_t alsa_wait_writable(void *data, size_t len)
return 0;
if (rc == -EPIPE || rc == -ESTRPIPE || rc == -EINTR)
{
if (snd_pcm_recover(alsa->pcm, rc, 1) < 0)
if (alsa_recover(alsa, rc) < 0)
return 0;
}
else if (rc < 0)
@@ -876,6 +849,16 @@ static void alsa_clock_sample(alsa_t *alsa)
if (!sys_ns)
return;
/* A load and a store rather than an exchange, which the volatile
* fallback of retro_atomic lacks: a raise landing between the two
* is one that has just been served. */
if (retro_atomic_load_acquire_int(&alsa->clk_rebase))
{
retro_atomic_store_relaxed_int(&alsa->clk_rebase, 0);
alsa->clk_have_anchor = 0;
alsa->clk_a_have_anchor = 0;
}
/* The hardware's own clock against the system's, where the driver
* counts one. This needs no rate and no delay - it is two clocks
* compared directly. */
@@ -990,6 +973,12 @@ static size_t alsa_frames_consumed(void *data)
return (size_t)(alsa->frames_written - (uint64_t)delay);
}
static size_t alsa_underruns(void *data)
{
alsa_t *alsa = (alsa_t*)data;
return alsa ? retro_atomic_load_acquire_size(&alsa->underruns) : 0;
}
audio_driver_t audio_alsa = {
alsa_init,
alsa_write,
@@ -1007,7 +996,7 @@ audio_driver_t audio_alsa = {
NULL, /* write_raw */
alsa_wait_writable,
alsa_frames_consumed,
NULL, /* underruns */
alsa_underruns,
alsa_layout,
NULL, /* frames_consumed_fallback */
alsa_device_clock_ppm
@@ -1178,6 +1167,10 @@ typedef struct ealsa
size_t period_frames;
size_t buffer_frames;
uint64_t frames_written; /* handed to the device since it opened */
/* Xruns the device reported on the write path: EPIPE is the device
* having run out of audio, which the ESTRPIPE beside it - a resume
* after a suspend - is not. */
retro_atomic_size_t underruns;
bool nonblock;
bool has_float;
bool can_pause;
@@ -1599,6 +1592,8 @@ static ssize_t tinyalsa_write(void *data, const void *buf, size_t len)
{
/* An underrun, or a resume after a suspend: prepared
* again and the frames go out on the next turn. */
if (errno == EPIPE)
retro_atomic_fetch_add_size(&ea->underruns, 1);
if (!ealsa_recover(ea))
return -1;
continue;
@@ -1782,6 +1777,12 @@ static void tinyalsa_device_list_free(void *data, void *array_list_data)
string_list_free(s);
}
static size_t tinyalsa_underruns(void *data)
{
tinyalsa_t *ea = (tinyalsa_t*)data;
return ea ? retro_atomic_load_acquire_size(&ea->underruns) : 0;
}
audio_driver_t audio_tinyalsa = {
tinyalsa_init,
tinyalsa_write,
@@ -1799,7 +1800,7 @@ audio_driver_t audio_tinyalsa = {
NULL, /* write_raw */
tinyalsa_wait_writable,
tinyalsa_frames_consumed,
NULL, /* underruns */
tinyalsa_underruns,
tinyalsa_layout
};
+24 -2
View File
@@ -16,6 +16,12 @@
#define ALSA_PCM_NEW_HW_PARAMS_API
#define ALSA_PCM_NEW_SW_PARAMS_API
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <retro_atomic.h>
#include <sys/asoundlib.h>
#include <retro_math.h>
@@ -38,6 +44,12 @@ typedef struct alsa
unsigned buf_size;
unsigned buf_count;
unsigned rate;
/* Times the device reported it had run out of audio. Counted where
* the status says so, on the write path, which is why it is counted
* and not said: a log line there costs part of the margin it is
* reporting on. The frontend shows it and logs the total once at
* teardown. */
retro_atomic_size_t underruns;
bool nonblock;
bool has_float;
bool can_pause;
@@ -170,6 +182,7 @@ static void *alsa_qsa_init(const char *device,
alsa->buffer[i] = alsa->buffer_chunk + i * alsa->buf_size;
alsa->has_float = false;
retro_atomic_size_init(&alsa->underruns, 0);
alsa->can_pause = true;
RARCH_LOG("[ALSA QSA] Can pause: %s.\n",
alsa->can_pause ? "yes" : "no");
@@ -206,7 +219,7 @@ static int check_pcm_status(void *data, int channel_type)
}
else if (status.status == SND_PCM_STATUS_UNDERRUN)
{
RARCH_LOG("[ALSA QSA] check_pcm_status: SNDP_CM_STATUS_UNDERRUN.\n");
retro_atomic_fetch_add_size(&alsa->underruns, 1);
if ((ret = snd_pcm_channel_prepare(alsa->pcm, channel_type)) < 0)
{
RARCH_ERR("[ALSA QSA] Invalid state detected for underrun on snd_pcm_channel_prepare: %s.\n",
@@ -422,6 +435,12 @@ static size_t alsa_qsa_buffer_size(void *data)
return alsa->buf_size * alsa->buf_count;
}
static size_t alsa_qsa_underruns(void *data)
{
alsa_qsa_t *alsa = (alsa_qsa_t*)data;
return alsa ? retro_atomic_load_acquire_size(&alsa->underruns) : 0;
}
audio_driver_t audio_alsa = {
alsa_qsa_init,
alsa_qsa_write,
@@ -436,5 +455,8 @@ audio_driver_t audio_alsa = {
NULL,
alsa_qsa_write_avail,
alsa_qsa_buffer_size,
NULL /* write_raw */
NULL, /* write_raw */
NULL, /* wait_writable */
NULL, /* frames_consumed */
alsa_qsa_underruns
};
+31 -14
View File
@@ -802,8 +802,7 @@ typedef struct ra_asio
double clk_sxx;
double clk_sxy;
double clk_n;
retro_atomic_int_t clk_ppm;
retro_atomic_int_t clk_valid;
retro_atomic_int_t clk_ppm; /* AUDIO_CLOCK_PPM_NONE until known */
/* Frames the device has taken: a period per callback, silence
* included. Written by the callback thread, read by the writer, so
* atomic: an aligned word is what the machine will not tear, which
@@ -811,6 +810,12 @@ typedef struct ra_asio
* - and the underrun counter beside it is already atomic for the
* same reason. */
retro_atomic_size_t consumed;
/* The frontend's reinit latch, taken on the main thread at init.
* The ASIO callbacks run on the driver's own thread, where reaching
* for a frontend singleton is the thing the worker-read audit
* exists to catch; wasapi captures the same pointer at
* sthread_create. */
retro_atomic_int_t *reinit_request;
/* Set by the message callback on kAsioResetRequest or
* kAsioBufferSizeChange: the buffers are to be disposed and created
* anew - the driver's preferred size may have changed - when the
@@ -1050,8 +1055,11 @@ static void asio_cb_sample_rate_changed(ASIOSampleRate rate)
RARCH_WARN("[ASIO] The device changed its sample rate to %.0f Hz; audio will reinitialise.\n",
(double)rate);
if (g_asio)
{
retro_atomic_store_release_int(&g_asio->rebuild_pending, 1);
retro_atomic_store_release_int(&audio_state_get_ptr()->reinit_request, 1);
if (g_asio->reinit_request)
retro_atomic_store_release_int(g_asio->reinit_request, 1);
}
}
static long asio_cb_message(long selector, long value,
@@ -1085,8 +1093,11 @@ static long asio_cb_message(long selector, long value,
RARCH_WARN("[ASIO] Driver requests %s; audio will reinitialise.\n",
selector == kAsioResetRequest ? "a reset" : "a buffer size change");
if (g_asio)
{
retro_atomic_store_release_int(&g_asio->rebuild_pending, 1);
retro_atomic_store_release_int(&audio_state_get_ptr()->reinit_request, 1);
if (g_asio->reinit_request)
retro_atomic_store_release_int(g_asio->reinit_request, 1);
}
return 1L;
case kAsioResyncRequest:
/* The driver lost its place - a system pause, a clock that
@@ -1181,7 +1192,6 @@ static ASIOTime * asio_cb_buffer_switch_time_info(
if (ppm > -100000.0 && ppm < 100000.0)
{
retro_atomic_store_release_int(&ad->clk_ppm, (int)ppm);
retro_atomic_store_release_int(&ad->clk_valid, 1);
}
}
}
@@ -1603,6 +1613,8 @@ static void *ra_asio_init(const char *device, unsigned rate,
bool rate_moved = false;
g_asio_persistent = NULL;
ad->reinit_request = &audio_state_get_ptr()->reinit_request;
/* Nothing was freed when this was parked, so a callback still
* in flight from then was harmless - it read a cleared g_asio
* and left. What follows here is not harmless: the ring is
@@ -1625,8 +1637,7 @@ static void *ra_asio_init(const char *device, unsigned rate,
* drift. */
ad->clk_have_anchor = 0;
ad->clk_n = 0.0;
retro_atomic_store_release_int(&ad->clk_valid, 0);
retro_atomic_store_release_int(&ad->clk_ppm, 0);
retro_atomic_store_release_int(&ad->clk_ppm, AUDIO_CLOCK_PPM_NONE);
/* The rate, settled before anything is sized from it.
*
@@ -1727,9 +1738,10 @@ static void *ra_asio_init(const char *device, unsigned rate,
ad = (ra_asio_t *)calloc(1, sizeof(ra_asio_t));
if (!ad)
return NULL;
/* Taken here, on the main thread, for the driver's callbacks. */
ad->reinit_request = &audio_state_get_ptr()->reinit_request;
retro_atomic_size_init(&ad->underruns, 0);
retro_atomic_int_init(&ad->clk_ppm, 0);
retro_atomic_int_init(&ad->clk_valid, 0);
retro_atomic_int_init(&ad->clk_ppm, AUDIO_CLOCK_PPM_NONE);
ad->clk_have_anchor = 0;
retro_atomic_size_init(&ad->consumed, 0);
@@ -2048,6 +2060,7 @@ static void ra_asio_set_nonblock_state(void *data, bool state)
static void ra_asio_free(void *data)
{
int clk_ppm;
ra_asio_t *ad = (ra_asio_t *)data;
if (!ad)
return;
@@ -2056,10 +2069,10 @@ static void ra_asio_free(void *data)
* was doing, against the rate this driver asked for. Logged beside
* the underrun count rather than acted on: until this has been read
* off real interfaces it is a measurement, not an input. */
if (retro_atomic_load_acquire_int(&ad->clk_valid))
clk_ppm = retro_atomic_load_acquire_int(&ad->clk_ppm);
if (clk_ppm != AUDIO_CLOCK_PPM_NONE)
RARCH_LOG("[ASIO] Device clock, from the driver's time information:"
" %+d ppm against %u Hz.\n",
retro_atomic_load_acquire_int(&ad->clk_ppm), ad->sample_rate);
" %+d ppm against %u Hz.\n", clk_ppm, ad->sample_rate);
else
RARCH_LOG("[ASIO] Device clock: the driver reported no usable time"
" information.\n");
@@ -2187,9 +2200,13 @@ static size_t ra_asio_underruns(void *data)
static bool ra_asio_device_clock_ppm(void *data, double *ppm)
{
ra_asio_t *ad = (ra_asio_t*)data;
if (!ad || !retro_atomic_load_acquire_int(&ad->clk_valid))
int v;
if (!ad)
return false;
*ppm = (double)retro_atomic_load_acquire_int(&ad->clk_ppm);
v = retro_atomic_load_acquire_int(&ad->clk_ppm);
if (v == AUDIO_CLOCK_PPM_NONE)
return false;
*ppm = (double)v;
return true;
}
+106 -5
View File
@@ -20,6 +20,7 @@
#include <sys/ioctl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/audioio.h>
@@ -27,7 +28,12 @@
#include "../../config.h"
#endif
#include <compat/strl.h>
#include <lists/string_list.h>
#include <retro_miscellaneous.h>
#include "../audio_driver.h"
#include "../audio_device_label.h"
#include "../../verbosity.h"
#define DEFAULT_DEV "/dev/audio"
@@ -36,12 +42,19 @@ static void *audioio_init(const char *device, unsigned rate, unsigned latency,
unsigned *new_out_rate)
{
struct audio_info info;
const char *audiodev = device ? device : DEFAULT_DEV;
char audiodev[PATH_MAX_LENGTH];
int *fd = (int*)calloc(1, sizeof(int));
if (!fd)
return NULL;
/* The device may be a list entry, "/dev/audio0 (name)": the path
* is what is opened. */
if (device)
audio_device_label_path(audiodev, sizeof(audiodev), device);
else
strlcpy(audiodev, DEFAULT_DEV, sizeof(audiodev));
AUDIO_INITINFO(&info);
#ifdef AUMODE_PLAY_ALL
@@ -178,20 +191,104 @@ static size_t audioio_buffer_size(void *data)
int *fd = (int*)data;
if (ioctl(*fd, AUDIO_GETINFO, &info) < 0)
return false;
return 0;
return info.play.buffer_size;
}
#ifdef AUDIO_GETBUFINFO
/* audio(4)'s play.seek is the count of bytes pending, so what the
* device will take is its buffer less what is still queued in it.
*
* AUDIO_GETBUFINFO is NetBSD's, and so is seek: the Solaris
* audio_prinfo carries buffer_size and samples and no seek, which is
* why this is not offered there. Reporting the whole buffer instead -
* which is what happened before - reads to rate control as a device
* that never drains, and it answers a device that is always empty
* with its full upward correction, for as long as it is on. The
* interface asks for nothing rather than a constant, and nothing is
* what the other branch gives. */
static size_t audioio_write_avail(void *data)
{
return audioio_buffer_size(data);
struct audio_info info;
int *fd = (int*)data;
if (ioctl(*fd, AUDIO_GETBUFINFO, &info) < 0)
return 0;
if (info.play.seek >= info.play.buffer_size)
return 0;
return info.play.buffer_size - info.play.seek;
}
#endif
/* The device is opened with AUDIO_ENCODING_SLINEAR at 16 bits; the
* audio(4) encodings are integer PCM, mu-law and A-law, none float. */
static bool audioio_use_float(void *data) { return false; }
/* The nodes an audioio kernel offers: /dev/audio and /dev/audio0..15,
* as oss.c walks /dev/dsp*. AUDIO_GETDEV names the hardware behind a
* node on every audioio kernel, so the label carries it. A node that
* cannot be opened for output is not offered; a busy one is, since it
* exists and may be free later. Needs no driver instance, which is
* what the menu has when init failed and the user needs to pick
* another device. */
static void *audioio_device_list_new(void *data)
{
int i;
union string_list_elem_attr attr;
struct string_list *sl = string_list_new();
(void)data;
attr.i = 0;
if (!sl)
return NULL;
for (i = -1; i < 16; i++)
{
char path[32];
char label[160];
int fd;
if (i < 0)
strlcpy(path, DEFAULT_DEV, sizeof(path));
else
snprintf(path, sizeof(path), "/dev/audio%d", i);
fd = open(path, O_WRONLY | O_NONBLOCK);
if (fd < 0 && errno != EBUSY)
continue;
strlcpy(label, path, sizeof(label));
if (fd >= 0)
{
audio_device_t dev;
if (ioctl(fd, AUDIO_GETDEV, &dev) == 0 && dev.name[0])
{
size_t _len = strlcat(label, " (", sizeof(label));
_len += strlcpy(label + _len, dev.name, sizeof(label) - _len);
strlcpy(label + _len, ")", sizeof(label) - _len);
}
close(fd);
}
string_list_append(sl, label, attr);
}
if (!sl->size)
{
string_list_free(sl);
return NULL;
}
return sl;
}
static void audioio_device_list_free(void *data, void *array_list_data)
{
struct string_list *sl = (struct string_list*)array_list_data;
(void)data;
if (sl)
string_list_free(sl);
}
audio_driver_t audio_audioio = {
audioio_init,
audioio_write,
@@ -202,9 +299,13 @@ audio_driver_t audio_audioio = {
audioio_free,
audioio_use_float,
"audioio",
NULL,
NULL,
audioio_device_list_new,
audioio_device_list_free,
#ifdef AUDIO_GETBUFINFO
audioio_write_avail,
#else
NULL, /* write_avail - no way to measure the fill here */
#endif
audioio_buffer_size,
NULL /* write_raw */
};
+29 -2
View File
@@ -16,7 +16,9 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <malloc.h>
#include <boolean.h>
#include <retro_atomic.h>
#include <emscripten/wasm_worker.h>
#include <emscripten/webaudio.h>
@@ -65,6 +67,11 @@ typedef struct audioworklet_data
* ring_init false and the driver does not come up. */
retro_spsc_t ring;
size_t ring_size;
/* Quanta the worklet had to fill with silence because the ring came
* up short, or because it could not get at the ring at all. Counted
* here and not said: this is the render callback, and a line from
* it costs part of the quantum it is reporting on. */
retro_atomic_size_t underruns;
bool ring_init;
unsigned rate;
unsigned latency;
@@ -112,7 +119,9 @@ static bool audioworklet_process_cb(int numInputs, const AudioSampleFrame *input
/* busyspin is safe as of emscripten 4.0.4 */
if (!emscripten_lock_busyspin_wait_acquire(&audioworklet->buffer_lock, 2.5))
{
printf("[WARN] [AudioWorklet] Worklet: could not acquire lock\n");
/* The output arrays arrive zeroed, so this quantum goes out
* as silence. */
retro_atomic_fetch_add_size(&audioworklet->underruns, 1);
return true;
}
@@ -136,6 +145,10 @@ static bool audioworklet_process_cb(int numInputs, const AudioSampleFrame *input
if (writing_frames < outputs[0].samplesPerChannel)
{
int zero_frames = outputs[0].samplesPerChannel - writing_frames;
/* Only where audio was expected: a driver that is not running is
* not being starved. */
if (audioworklet->driver_running)
retro_atomic_fetch_add_size(&audioworklet->underruns, 1);
memset(outputs[0].data + writing_frames, 0, zero_frames * sizeof(float));
memset(outputs[0].data + writing_frames + outputs[0].samplesPerChannel, 0, zero_frames * sizeof(float));
}
@@ -287,7 +300,10 @@ static void *audioworklet_init(const char *device, unsigned rate,
return NULL;
audioworklet->worklet_stack = memalign(16, WORKLET_STACK_SIZE);
if (!audioworklet->worklet_stack)
{
free(audioworklet);
return NULL;
}
audioworklet_static_data = audioworklet;
audioworklet->latency = latency;
@@ -301,6 +317,7 @@ static void *audioworklet_init(const char *device, unsigned rate,
* below take their existing error exits. */
if (!audioworklet->ring_init)
audioworklet->init_error = true;
retro_atomic_size_init(&audioworklet->underruns, 0);
emscripten_lock_init(&audioworklet->buffer_lock);
#ifdef PROXY_TO_PTHREAD
emscripten_lock_init(&audioworklet->trywrite_lock);
@@ -607,6 +624,13 @@ static size_t audioworklet_buffer_size(void *data)
static bool audioworklet_use_float(void *data) { return true; }
static size_t audioworklet_underruns(void *data)
{
audioworklet_data_t *audioworklet = (audioworklet_data_t*)data;
return audioworklet
? retro_atomic_load_acquire_size(&audioworklet->underruns) : 0;
}
audio_driver_t audio_audioworklet = {
audioworklet_init,
audioworklet_write,
@@ -621,5 +645,8 @@ audio_driver_t audio_audioworklet = {
NULL,
audioworklet_write_avail,
audioworklet_buffer_size,
NULL /* write_raw */
NULL, /* write_raw */
NULL, /* wait_writable */
NULL, /* frames_consumed */
audioworklet_underruns
};
+58 -66
View File
@@ -328,6 +328,12 @@ typedef struct coreaudio
* on that path, read by the frontend's overlay, and by it once at
* teardown for the log. Never logged from here. */
retro_atomic_size_t underruns;
/* The frontend's reinit latch, taken on the main thread at init:
* the default-device listeners run on HAL threads, where reaching
* for a frontend singleton is the thing the worker-read audit
* exists to catch. wasapi captures the same pointer at
* sthread_create. */
retro_atomic_int_t *reinit_request;
/* The largest number_frames the callback may be handed, as the
* device and the unit describe themselves, and the largest it was
@@ -347,19 +353,19 @@ typedef struct coreaudio
/* Render callbacks handed invalid output buffers. */
retro_atomic_size_t format_errors;
/* The worst a callback came up short by, in samples, and how full
* the ring was that time. Written by the render thread with two
* atomic stores and read once at teardown - never logged from
* there. A buzz is a callback finding less than a period and
* padding the rest with silence, so these two numbers say whether
* that is what is happening and by how much, which "it buzzes"
* cannot. Out here rather than beside period_frames, which is the
/* The worst a callback came up short by and how full the ring was
* that time, in frames: the shortfall in the high 16 bits, the ring
* in the low 16, each saturating at 0xFFFF. One word, so the two
* always come from the same callback. Written by the render thread
* and read once at teardown - never logged from there. A buzz is a
* callback finding less than a period and padding the rest with
* silence, so these two numbers say whether that is what is
* happening and by how much, which "it buzzes" cannot. Out here rather than beside period_frames, which is the
* HAL's and macOS-only: the callback and the teardown log read
* these on every Apple platform, so declaring them under
* !TARGET_OS_IPHONE left the iOS and tvOS builds referring to
* members that were not there. */
retro_atomic_size_t worst_short;
retro_atomic_size_t worst_short_avail;
retro_atomic_int_t worst_short;
/* The output unit: ComponentInstance or AudioComponentInstance,
* both of which are this type on every SDK. */
@@ -414,14 +420,12 @@ typedef struct coreaudio
uint64_t ct_anchor_host;
int ct_have_anchor;
double ct_sx, ct_sy, ct_sxx, ct_sxy, ct_n;
retro_atomic_int_t ct_ppm;
retro_atomic_int_t ct_valid;
retro_atomic_int_t ct_ppm; /* AUDIO_CLOCK_PPM_NONE until known */
/* What the HAL says itself, where it fills it in: mRateScalar is
* the device clock against nominal, already computed by someone
* with better information than this fit has. Kept beside it rather
* than instead of it, so the two can be compared on hardware. */
retro_atomic_int_t ct_scalar_ppm;
retro_atomic_int_t ct_scalar_valid;
retro_atomic_int_t ct_scalar_ppm; /* AUDIO_CLOCK_PPM_NONE until known */
unsigned output_rate; /* Hardware output rate */
/* The layout the output unit's input bus was set to, as the
* frontend's mask, and its channel count: the unit routes each
@@ -574,6 +578,8 @@ static void coreaudio_free(void *data)
{
coreaudio_t *dev = (coreaudio_t*)data;
size_t n;
int clk_ppm;
unsigned worst;
if (!dev)
return;
@@ -583,11 +589,12 @@ static void coreaudio_free(void *data)
* shortfall against the ring and the period, which is what says
* whether a buzz is the ring being too small for the device's
* period rather than the source being late. */
worst = (unsigned)retro_atomic_load_acquire_int(&dev->worst_short);
if ((n = retro_atomic_load_acquire_size(&dev->underruns)))
RARCH_LOG("[CoreAudio] The callback came up short %u time%s; at worst it wanted %u samples more than the %u it found, against a %u-sample ring and a %u-frame device period.\n",
(unsigned)n, n == 1 ? "" : "s",
(unsigned)retro_atomic_load_acquire_size(&dev->worst_short),
(unsigned)retro_atomic_load_acquire_size(&dev->worst_short_avail),
(worst >> 16) * dev->channels,
(worst & 0xFFFFu) * dev->channels,
(unsigned)dev->usable,
#if !TARGET_OS_IPHONE
(unsigned)dev->period_frames
@@ -603,17 +610,18 @@ static void coreaudio_free(void *data)
* measurements, and a clock estimate that is wrong is worse than
* one that is absent. Where both appear they should agree, and a
* disagreement is the interesting result. */
if (retro_atomic_load_acquire_int(&dev->ct_valid))
clk_ppm = retro_atomic_load_acquire_int(&dev->ct_ppm);
if (clk_ppm != AUDIO_CLOCK_PPM_NONE)
RARCH_LOG("[CoreAudio] Device clock, fitted from the callback's"
" timestamps: %+d ppm against %u Hz.\n",
retro_atomic_load_acquire_int(&dev->ct_ppm), dev->output_rate);
clk_ppm, dev->output_rate);
else
RARCH_LOG("[CoreAudio] Device clock: not enough usable timestamps"
" to fit one.\n");
if (retro_atomic_load_acquire_int(&dev->ct_scalar_valid))
clk_ppm = retro_atomic_load_acquire_int(&dev->ct_scalar_ppm);
if (clk_ppm != AUDIO_CLOCK_PPM_NONE)
RARCH_LOG("[CoreAudio] Device clock, as the HAL reports it"
" (mRateScalar): %+d ppm.\n",
retro_atomic_load_acquire_int(&dev->ct_scalar_ppm));
" (mRateScalar): %+d ppm.\n", clk_ppm);
/* What the device was said to be able to ask for, and what it did
* ask for. Said whether or not anything went wrong, because the
@@ -675,7 +683,6 @@ static OSStatus coreaudio_audio_write_cb(void *userdata,
{
retro_atomic_store_release_int(&dev->ct_scalar_ppm,
(int)((time_stamp->mRateScalar - 1.0) * 1000000.0));
retro_atomic_store_release_int(&dev->ct_scalar_valid, 1);
}
if ( (f & kAudioTimeStampSampleTimeValid)
@@ -720,7 +727,6 @@ static OSStatus coreaudio_audio_write_cb(void *userdata,
if (ppm > -100000.0 && ppm < 100000.0)
{
retro_atomic_store_release_int(&dev->ct_ppm, (int)ppm);
retro_atomic_store_release_int(&dev->ct_valid, 1);
}
}
}
@@ -813,12 +819,18 @@ static OSStatus coreaudio_audio_write_cb(void *userdata,
*action_flags |= kAudioUnitRenderAction_OutputIsSilence;
retro_atomic_fetch_add_size(&dev->underruns, 1);
{
size_t shortfall = frames_needed - avail;
if (shortfall > retro_atomic_load_acquire_size(&dev->worst_short))
{
retro_atomic_store_release_size(&dev->worst_short, shortfall);
retro_atomic_store_release_size(&dev->worst_short_avail, avail);
}
/* Whole frames both: avail was rounded down above, and
* frames_needed is a multiple of the channel count. */
size_t shortfall = (frames_needed - avail) / dev->channels;
size_t have = avail / dev->channels;
if (shortfall > 0xFFFF)
shortfall = 0xFFFF;
if (have > 0xFFFF)
have = 0xFFFF;
if (shortfall > ((unsigned)retro_atomic_load_acquire_int(
&dev->worst_short) >> 16))
retro_atomic_store_release_int(&dev->worst_short,
(int)((shortfall << 16) | have));
}
}
else
@@ -1371,11 +1383,11 @@ static void *coreaudio_init(const char *device,
}
dev->ct_have_anchor = 0;
dev->ct_n = 0.0;
retro_atomic_int_init(&dev->ct_ppm, 0);
retro_atomic_int_init(&dev->ct_valid, 0);
retro_atomic_int_init(&dev->ct_scalar_ppm, 0);
retro_atomic_int_init(&dev->ct_scalar_valid, 0);
retro_atomic_int_init(&dev->ct_ppm, AUDIO_CLOCK_PPM_NONE);
retro_atomic_int_init(&dev->ct_scalar_ppm, AUDIO_CLOCK_PPM_NONE);
retro_atomic_size_init(&dev->underruns, 0);
/* Taken here, on the main thread, for the HAL's listeners. */
dev->reinit_request = &audio_state_get_ptr()->reinit_request;
/* What coreaudio_run() waits for before starting; the HAL's pull on
* macOS, and on iOS the ring's own quarter, which is what the unit
* was asked to use. */
@@ -1387,8 +1399,7 @@ static void *coreaudio_init(const char *device,
retro_atomic_size_init(&dev->max_pull_observed, 0);
retro_atomic_size_init(&dev->oversized_pulls, 0);
retro_atomic_size_init(&dev->format_errors, 0);
retro_atomic_size_init(&dev->worst_short, 0);
retro_atomic_size_init(&dev->worst_short_avail, 0);
retro_atomic_int_init(&dev->worst_short, 0);
dev->write_ptr = 0;
dev->read_ptr = 0;
@@ -1790,19 +1801,16 @@ static void coreaudio_device_list_free(void *data, void *array_list_data)
static bool coreaudio_device_clock_ppm(void *data, double *ppm)
{
coreaudio_t *dev = (coreaudio_t*)data;
int v;
if (!dev)
return false;
if (retro_atomic_load_acquire_int(&dev->ct_scalar_valid))
{
*ppm = (double)retro_atomic_load_acquire_int(&dev->ct_scalar_ppm);
return true;
}
if (retro_atomic_load_acquire_int(&dev->ct_valid))
{
*ppm = (double)retro_atomic_load_acquire_int(&dev->ct_ppm);
return true;
}
return false;
v = retro_atomic_load_acquire_int(&dev->ct_scalar_ppm);
if (v == AUDIO_CLOCK_PPM_NONE)
v = retro_atomic_load_acquire_int(&dev->ct_ppm);
if (v == AUDIO_CLOCK_PPM_NONE)
return false;
*ppm = (double)v;
return true;
}
static size_t coreaudio_frames_consumed(void *data)
@@ -1896,9 +1904,8 @@ audio_driver_t audio_coreaudio = {
* built again against whatever the default now is. */
static void coreaudio_output_default_moved(coreaudio_t *dev)
{
if (dev && dev->follows_default)
retro_atomic_store_release_int(
&audio_state_get_ptr()->reinit_request, 1);
if (dev && dev->follows_default && dev->reinit_request)
retro_atomic_store_release_int(dev->reinit_request, 1);
}
static OSStatus coreaudio_output_default_listener(ca_obj_id_t obj,
@@ -1998,7 +2005,7 @@ typedef struct coreaudio_mic
/* The reader's park, notified by the capture callback after each
* slice it writes (and on the ones it cannot: a full ring still
* moves the epoch, so a parked reader re-checks). An eventcount,
* as in sdl_audio's mic: the callback's notify is an atomic bump
* as in sdl2_audio's mic: the callback's notify is an atomic bump
* and a load with no reader parked - no lock ever on the capture
* thread - and the reader's timed waits re-check the ring inside
* the prepare/commit window, so a slice landing between the check
@@ -2010,7 +2017,6 @@ typedef struct coreaudio_mic
void *cb_buf;
size_t cb_buf_size;
unsigned sample_rate;
bool nonblock;
bool use_float;
#if !TARGET_OS_IPHONE
AudioDeviceID device;
@@ -2022,12 +2028,10 @@ typedef struct coreaudio_mic
/* Driver-wide context: created by init(), destroyed by free(). The
* frontend holds it for as long as the driver is selected, longer than
* any microphone opened through it, so it is never the same allocation
* as one. It latches the non-blocking state so a mic opened later
* inherits it. */
* as one. It tracks the one microphone open through it. */
typedef struct coreaudio_mic_driver
{
coreaudio_mic_t *mic;
bool nonblock;
} coreaudio_mic_driver_t;
static void coreaudio_mic_close(void *driver_context, void *mic_context);
@@ -2356,7 +2360,7 @@ static int coreaudio_mic_read(void *driver_context, void *mic_context,
avail = retro_spsc_read_avail(&mic->ring);
n = avail < len ? avail : len;
if (!n && !mic->nonblock)
if (!n)
{
/* Blocking: one slice's worth of wait for the callback, timed,
* with the ring re-checked inside the window. */
@@ -2373,16 +2377,6 @@ static int coreaudio_mic_read(void *driver_context, void *mic_context,
return (int)n;
}
static void coreaudio_mic_set_nonblock_state(void *driver_context, bool state)
{
coreaudio_mic_driver_t *drv = (coreaudio_mic_driver_t*)driver_context;
if (!drv)
return;
drv->nonblock = state;
if (drv->mic)
drv->mic->nonblock = state;
}
static struct string_list *coreaudio_mic_device_list_new(const void *driver_context)
{
#if TARGET_OS_IPHONE
@@ -2473,7 +2467,6 @@ static void *coreaudio_mic_open(void *driver_context, const char *device,
retro_atomic_int_init(&mic->running, 0);
retro_atomic_int_init(&mic->initialized, 0);
mic->sample_rate = rate;
mic->nonblock = drv->nonblock;
if (!retro_eventcount_init(&mic->park))
goto error;
@@ -2687,7 +2680,6 @@ microphone_driver_t microphone_coreaudio = {
coreaudio_mic_init,
coreaudio_mic_free,
coreaudio_mic_read,
coreaudio_mic_set_nonblock_state,
"coreaudio",
coreaudio_mic_device_list_new,
coreaudio_mic_device_list_free,
+156 -11
View File
@@ -17,6 +17,7 @@
#include <string.h>
#include <retro_atomic.h>
#include <features/features_cpu.h>
#include <malloc.h>
#include "../audio_driver.h"
@@ -24,6 +25,17 @@
#include "../../retroarch.h"
#include "../../verbosity.h"
/* libctru defines this from 1.5 on; it is the SoC clock by sixteen. */
#ifndef SYSCLOCK_ARM11
#define SYSCLOCK_ARM11 (16756991u * 8u * 2u)
#endif
/* The rate the channel is set to and the rate the frontend is told,
* which have to be the one number: the channel interpolates with
* NDSP_INTERP_NONE, so anything the frontend resamples to that the
* channel is not playing at is error with nothing to correct it. */
#define CTR_DSP_AUDIO_RATE 32728
typedef struct
{
ndspWaveBuf dsp_buf; /* TODO/FIXME - find out alignment */
@@ -42,6 +54,27 @@ typedef struct
* that once every four seconds and would miss ninety wraps. */
uint32_t last_pos;
retro_atomic_size_t consumed;
/* The DSP's clock against the rate the channel is set to, fitted
* from the sample position this callback already reads and
* svcGetSystemTick(), which counts in the ARM11's domain rather
* than the DSP's. Accumulated in the callback, which owns all of
* it, and published as one int in ppm.
*
* cpu_features_get_perf_counter() and not
* cpu_features_get_time_usec(): the counter is svcGetSystemTick()
* here, the ARM11's own tick; the clock is osGetTime(), which
* resolves in milliseconds - a hundred parts per million of
* quantisation on a ten second window, against a figure measured
* in single parts. SYSCLOCK_ARM11 is the counter's scale, since
* features_cpu.h offers no frequency beside it.
*
* A measurement. Nothing acts on it. */
uint64_t clk_pos;
uint64_t clk_anchor_pos;
uint64_t clk_anchor_tick;
int clk_have_anchor;
double clk_sx, clk_sy, clk_sxx, clk_sxy, clk_n;
retro_atomic_int_t clk_ppm; /* AUDIO_CLOCK_PPM_NONE until known */
bool nonblock;
bool playing;
} ctr_dsp_audio_t;
@@ -50,18 +83,75 @@ typedef struct
#define CTR_DSP_AUDIO_COUNT_MASK (CTR_DSP_AUDIO_COUNT - 1u)
#define CTR_DSP_AUDIO_SIZE (CTR_DSP_AUDIO_COUNT * sizeof(int16_t) * 2)
/* One (position, tick) pair into the fit. Runs on the DSP callback's
* own thread, which owns every field it touches here. */
static void ctr_dsp_audio_clock_sample(ctr_dsp_audio_t *ctr, uint32_t frames)
{
uint64_t tick = (uint64_t)cpu_features_get_perf_counter();
double x, y, denom;
ctr->clk_pos += frames;
/* Taken on the first frame, on a position that went backwards, and
* again once the window has run long enough to be worth
* restarting. */
if ( !ctr->clk_have_anchor
|| tick <= ctr->clk_anchor_tick
|| (tick - ctr->clk_anchor_tick) > (uint64_t)SYSCLOCK_ARM11 * 30)
{
ctr->clk_anchor_pos = ctr->clk_pos;
ctr->clk_anchor_tick = tick;
ctr->clk_have_anchor = 1;
ctr->clk_sx = ctr->clk_sy = ctr->clk_sxx = ctr->clk_sxy
= ctr->clk_n = 0.0;
retro_atomic_store_release_int(&ctr->clk_ppm, AUDIO_CLOCK_PPM_NONE);
return;
}
/* Seconds and frames from the anchor: a fit on the raw values
* loses its answer to cancellation. */
x = (double)(tick - ctr->clk_anchor_tick) / (double)SYSCLOCK_ARM11;
y = (double)(ctr->clk_pos - ctr->clk_anchor_pos);
ctr->clk_sx += x;
ctr->clk_sy += y;
ctr->clk_sxx += x * x;
ctr->clk_sxy += x * y;
ctr->clk_n += 1.0;
/* A second of window at least, as the interface asks. */
if (ctr->clk_n < 4.0 || x < 1.0)
return;
denom = ctr->clk_n * ctr->clk_sxx - ctr->clk_sx * ctr->clk_sx;
if (denom <= 0.0)
return;
{
double slope = (ctr->clk_n * ctr->clk_sxy - ctr->clk_sx * ctr->clk_sy)
/ denom;
double ppm = (slope / (double)CTR_DSP_AUDIO_RATE - 1.0) * 1000000.0;
if (ppm > -100000.0 && ppm < 100000.0)
{
retro_atomic_store_release_int(&ctr->clk_ppm, (int)ppm);
}
}
}
static void ctr_dsp_audio_frame_cb(void *data)
{
ctr_dsp_audio_t *ctr = (ctr_dsp_audio_t*)data;
uint32_t pos = ndspChnGetSamplePos(ctr->channel);
/* Unsigned subtraction and the mask do the unwrap: the difference is
* right across the wrap without a comparison. Only this callback
* writes either field. */
retro_atomic_fetch_add_size(&ctr->consumed,
(size_t)((pos - ctr->last_pos) & CTR_DSP_AUDIO_COUNT_MASK));
uint32_t frames = (pos - ctr->last_pos) & CTR_DSP_AUDIO_COUNT_MASK;
retro_atomic_fetch_add_size(&ctr->consumed, (size_t)frames);
ctr->last_pos = pos;
ctr_dsp_audio_clock_sample(ctr, frames);
LightEvent_Signal(&ctr->frame_event);
}
@@ -94,13 +184,11 @@ static void *ctr_dsp_audio_init(const char *device, unsigned rate, unsigned late
ndspExit();
return NULL;
}
retro_atomic_int_init(&ctr->clk_ppm, AUDIO_CLOCK_PPM_NONE);
LightEvent_Init(&ctr->frame_event, RESET_ONESHOT);
ndspSetCallback(ctr_dsp_audio_frame_cb, ctr);
if (!ctr)
return NULL;
*new_rate = 32730;
*new_rate = CTR_DSP_AUDIO_RATE;
ctr->channel = 0;
@@ -110,7 +198,7 @@ static void *ctr_dsp_audio_init(const char *device, unsigned rate, unsigned late
ndspChnReset(ctr->channel);
ndspChnSetFormat(ctr->channel, NDSP_FORMAT_STEREO_PCM16);
ndspChnSetInterp(ctr->channel, NDSP_INTERP_NONE);
ndspChnSetRate(ctr->channel, 32728.0f);
ndspChnSetRate(ctr->channel, (float)CTR_DSP_AUDIO_RATE);
ndspChnWaveBufClear(ctr->channel);
ctr->dsp_buf.data_pcm16 = linearAlloc(CTR_DSP_AUDIO_SIZE);
@@ -144,6 +232,38 @@ static void ctr_dsp_audio_free(void *data)
* advance before giving up on it. */
#define CTR_DSP_AUDIO_WAIT_LAPS 20
/* LightEvent_Wait() has no timeout, so a DSP that has stopped
* signalling its frame event - reset, or the channel dropped coming
* back from sleep - never returns from it. Never reached while the DSP
* runs: it signals every frame, so this only decides how long a
* stopped one takes to be noticed. */
#define CTR_DSP_AUDIO_STALL_TIMEOUT_NS 256000000LL
#define CTR_DSP_AUDIO_WAIT_WRITABLE_LAPS 8
/* LightEvent_WaitTimeout() is libctru 2's; on 1.x the wait is the
* try-wait polled in slices short against the DSP's frame. Non-zero is
* the timeout either way. */
#ifdef USE_CTRULIB_2
#define ctr_dsp_audio_frame_wait(ctr, ns) \
LightEvent_WaitTimeout(&(ctr)->frame_event, (ns))
#else
#define CTR_DSP_AUDIO_POLL_SLICE_NS 500000LL
static int ctr_dsp_audio_frame_wait(ctr_dsp_audio_t *ctr, s64 timeout_ns)
{
s64 left = timeout_ns;
while (left > 0)
{
if (LightEvent_TryWait(&ctr->frame_event))
return 0;
svcSleepThread(CTR_DSP_AUDIO_POLL_SLICE_NS);
left -= CTR_DSP_AUDIO_POLL_SLICE_NS;
}
return 1;
}
#endif
static ssize_t ctr_dsp_audio_write(void *data, const void *buf, size_t len)
{
u32 pos;
@@ -265,6 +385,9 @@ static size_t ctr_dsp_audio_wait_writable(void *data, size_t len)
{
ctr_dsp_audio_t *ctr = (ctr_dsp_audio_t*)data;
uint32_t want = (uint32_t)(len >> 2);
/* Each wait ends on a timeout; this ends the loop when the DSP
* keeps calling back but never frees enough. */
int laps = CTR_DSP_AUDIO_WAIT_WRITABLE_LAPS;
if (want > (CTR_DSP_AUDIO_COUNT >> 1))
want = CTR_DSP_AUDIO_COUNT >> 1;
@@ -273,13 +396,18 @@ static size_t ctr_dsp_audio_wait_writable(void *data, size_t len)
{
uint32_t avail;
if (!ctr->playing)
return 0;
break;
avail = (ndspChnGetSamplePos(ctr->channel) - ctr->pos)
& CTR_DSP_AUDIO_COUNT_MASK;
if (avail >= want)
return avail * 2 * sizeof(int16_t);
LightEvent_Wait(&ctr->frame_event);
if (--laps < 0)
break;
/* Non-zero is the timeout. */
if (ctr_dsp_audio_frame_wait(ctr, CTR_DSP_AUDIO_STALL_TIMEOUT_NS))
break;
}
return 0;
}
/* Both in bytes of int16 stereo, as the interface asks: the ring is
@@ -291,6 +419,19 @@ static size_t ctr_dsp_audio_buffer_size(void *data)
return CTR_DSP_AUDIO_COUNT * 2 * sizeof(int16_t);
}
static bool ctr_dsp_audio_device_clock_ppm(void *data, double *ppm)
{
ctr_dsp_audio_t *ctr = (ctr_dsp_audio_t*)data;
int v;
if (!ctr)
return false;
v = retro_atomic_load_acquire_int(&ctr->clk_ppm);
if (v == AUDIO_CLOCK_PPM_NONE)
return false;
*ppm = (double)v;
return true;
}
audio_driver_t audio_ctr_dsp = {
ctr_dsp_audio_init,
ctr_dsp_audio_write,
@@ -307,5 +448,9 @@ audio_driver_t audio_ctr_dsp = {
ctr_dsp_audio_buffer_size,
NULL, /* write_raw */
ctr_dsp_audio_wait_writable,
ctr_dsp_audio_frames_consumed
ctr_dsp_audio_frames_consumed,
NULL, /* underruns */
NULL, /* layout */
NULL, /* frames_consumed_fallback */
ctr_dsp_audio_device_clock_ppm
};
+11 -1
View File
@@ -151,6 +151,9 @@ typedef struct dsound
* 48 kHz it holds twelve hours, and the estimate treats a wrap as a
* restart. */
retro_atomic_int_t frames_played;
/* Blocks the mixer thread had room for and nothing to put in, so
* the device played the silence written into them instead. */
retro_atomic_size_t underruns;
DWORD last_read_ptr;
} dsound_t;
@@ -382,6 +385,7 @@ static DWORD CALLBACK dsound_thread(PVOID data)
* fill block with silence. */
memset(region.chunk1, 0, region.size1);
memset(region.chunk2, 0, region.size2);
retro_atomic_fetch_add_size(&ds->underruns, 1);
}
else
{
@@ -982,6 +986,12 @@ static uint32_t dsound_layout(void *data)
return ds ? ds->layout : AUDIO_LAYOUT_STEREO;
}
static size_t dsound_underruns(void *data)
{
dsound_t *ds = (dsound_t*)data;
return ds ? retro_atomic_load_acquire_size(&ds->underruns) : 0;
}
audio_driver_t audio_dsound = {
dsound_init,
dsound_write,
@@ -999,6 +1009,6 @@ audio_driver_t audio_dsound = {
NULL, /* write_raw */
dsound_wait_writable,
dsound_frames_consumed,
NULL, /* underruns */
dsound_underruns,
dsound_layout
};
+143 -1
View File
@@ -21,11 +21,13 @@
#ifdef GEKKO
#include <gccore.h>
#include <ogcsys.h>
#include <ogc/lwp_watchdog.h>
#else
#include <cafe/ai.h>
#endif
#include <boolean.h>
#include <features/features_cpu.h>
#include <retro_inline.h>
#include <defines/gx_defines.h>
@@ -55,6 +57,40 @@ typedef struct
* way. 32-bit so the read cannot tear: at 48 kHz it wraps in about
* a day, which the estimator's differences handle. */
volatile uint32_t consumed;
/* Chunks the DMA started on that the writer had not reached: the
* callback zeroes each chunk as it retires it, so what goes out is
* silence. Volatile rather than atomic for the reason consumed is. */
volatile uint32_t underruns;
/* One (position, time) pair per chunk for the device clock, taken
* in the callback because that is where the position is exact:
* sampling consumed from the frontend would be a chunk out, which
* over any usable window is more error than the figure being
* measured. clk_seq brackets the pair so the reader can tell it
* was not caught mid-write.
*
* The fit itself is done in device_clock_ppm(), on the frontend's
* thread. This callback is a DMA interrupt, and floating point
* there is not a thing to be doing.
*
* clk_tb is the PPC timebase, which is what
* cpu_features_get_perf_counter() reads here - a counter, not a
* clock. ticks_to_microsecs() is its scale; features_cpu.h offers
* the counter and no frequency beside it. */
volatile uint32_t clk_pos;
volatile uint32_t clk_tb;
volatile uint32_t clk_seq;
/* The fit, touched only by device_clock_ppm(). Sums in seconds and
* frames from the anchor, because a fit on raw values loses its
* answer to cancellation. The anchor is retaken well inside the
* 32-bit timebase's wrap. */
uint32_t clk_last_seq;
uint32_t clk_anchor_pos;
uint32_t clk_anchor_tb;
int clk_have_anchor;
int clk_valid;
int clk_ppm;
double clk_sx, clk_sy, clk_sxx, clk_sxy, clk_n;
unsigned rate;
bool nonblock;
bool is_paused;
} gx_audio_t;
@@ -80,11 +116,23 @@ static void gx_audio_dma_callback(void)
wa->dma_busy = wa->dma_next;
wa->dma_next = (wa->dma_next + 1) & (BLOCKS - 1);
/* The chunk the DMA is about to take is one the writer has not
* filled, and the callback erased it as it retired. */
if (wa->dma_next == wa->dma_write)
wa->underruns++;
DCFlushRange(wa->data[wa->dma_next], CHUNK_SIZE);
AIInitDMA((uint32_t)wa->data[wa->dma_next], CHUNK_SIZE);
/* A chunk the DMA has finished with: device time. */
wa->consumed += CHUNK_FRAMES;
/* The pair the clock fit reads, published between two bumps of the
* sequence so a reader can tell a torn one. */
wa->clk_seq++;
wa->clk_pos = wa->consumed;
wa->clk_tb = (uint32_t)cpu_features_get_perf_counter();
wa->clk_seq++;
OSSignalCond(wa->dma_cond);
}
@@ -119,11 +167,13 @@ static void *gx_audio_init(const char *device,
{
AISetDSPSampleRate(AI_SAMPLERATE_32KHZ);
*new_rate = 32000;
wa->rate = 32000;
}
else /* Ranges 32001-39999 (in settings going up from 32000) and 48000-max (default high) -> set to 48000 hz */
{
AISetDSPSampleRate(AI_SAMPLERATE_48KHZ);
*new_rate = 48000;
wa->rate = 48000;
}
wa->dma_write = BLOCKS - 1;
@@ -312,6 +362,94 @@ static size_t gx_audio_wait_writable(void *data, size_t len)
* so the slot cannot be NULL either. */
static bool gx_audio_use_float(void *data) { return false; }
static size_t gx_audio_underruns(void *data)
{
gx_audio_t *wa = (gx_audio_t*)data;
return wa ? (size_t)wa->underruns : 0;
}
/* The AI's own clock against the rate this driver reports, in parts
* per million. What the hardware does with the rate it was asked for
* is not the rate it was asked for: the AI divides its source clock by
* a figure that does not land on 48000, and nothing downstream has
* ever been told. A measurement; nothing acts on it. */
static bool gx_audio_device_clock_ppm(void *data, double *ppm)
{
gx_audio_t *wa = (gx_audio_t*)data;
uint32_t s1 = 0, s2 = 0, pos = 0, tb = 0;
int tries = 4;
if (!wa || !wa->rate)
return false;
/* A pair the callback was not in the middle of writing. */
do
{
s1 = wa->clk_seq;
pos = wa->clk_pos;
tb = wa->clk_tb;
s2 = wa->clk_seq;
} while ((s1 != s2 || (s1 & 1u)) && --tries > 0);
if (s1 == s2 && !(s1 & 1u) && s1 != wa->clk_last_seq)
{
wa->clk_last_seq = s1;
/* Retaken on the first pair, when the DMA has restarted and the
* position went backwards, and well inside the wrap of the
* 32-bit timebase this samples. The frame bound catches the
* case the tick bound cannot: nothing reads this while the
* statistics are off, and a gap long enough to wrap the
* timebase comes back looking like a short interval. */
if ( !wa->clk_have_anchor
|| pos - wa->clk_anchor_pos > 0x80000000u
|| (uint32_t)(pos - wa->clk_anchor_pos) > wa->rate * 31u
|| (uint64_t)ticks_to_microsecs(
(uint64_t)(uint32_t)(tb - wa->clk_anchor_tb)) > 30000000ull)
{
wa->clk_anchor_pos = pos;
wa->clk_anchor_tb = tb;
wa->clk_have_anchor = 1;
wa->clk_valid = 0;
wa->clk_sx = wa->clk_sy = wa->clk_sxx = wa->clk_sxy = wa->clk_n = 0.0;
}
else
{
double x = (double)(uint64_t)ticks_to_microsecs(
(uint64_t)(uint32_t)(tb - wa->clk_anchor_tb)) / 1000000.0;
double y = (double)(uint32_t)(pos - wa->clk_anchor_pos);
wa->clk_sx += x;
wa->clk_sy += y;
wa->clk_sxx += x * x;
wa->clk_sxy += x * y;
wa->clk_n += 1.0;
/* A second of window at least, as the interface asks. */
if (wa->clk_n >= 4.0 && x >= 1.0)
{
double denom = wa->clk_n * wa->clk_sxx - wa->clk_sx * wa->clk_sx;
if (denom > 0.0)
{
double slope = (wa->clk_n * wa->clk_sxy
- wa->clk_sx * wa->clk_sy) / denom;
double est = (slope / (double)wa->rate - 1.0) * 1000000.0;
if (est > -100000.0 && est < 100000.0)
{
wa->clk_ppm = (int)est;
wa->clk_valid = 1;
}
}
}
}
}
if (!wa->clk_valid)
return false;
*ppm = (double)wa->clk_ppm;
return true;
}
audio_driver_t audio_gx = {
gx_audio_init,
gx_audio_write,
@@ -328,5 +466,9 @@ audio_driver_t audio_gx = {
gx_audio_buffer_size,
NULL, /* write_raw */
gx_audio_wait_writable,
gx_audio_frames_consumed
gx_audio_frames_consumed,
gx_audio_underruns,
NULL, /* layout */
NULL, /* frames_consumed_fallback */
gx_audio_device_clock_ppm
};
+23 -14
View File
@@ -43,6 +43,11 @@ typedef struct jack
unsigned channels; /* ports, and samples a frame in the ring */
uint32_t layout; /* the frontend's mask, one port a position */
jack_ringbuffer_t *buffer;
/* The frontend's reinit latch, taken on the main thread at init:
* the server's callbacks run on its threads, where reaching for a
* frontend singleton is the thing the worker-read audit exists to
* catch. wasapi does the same at sthread_create. */
retro_atomic_int_t *reinit_request;
#ifdef HAVE_THREADS
/* Parked on by the writer; notified from JACK's process and
* shutdown callbacks. An eventcount, not a lock and condition
@@ -102,8 +107,7 @@ typedef struct jack
int clk_have_anchor;
double clk_sx, clk_sy, clk_sxx, clk_sxy, clk_n;
unsigned clk_rate;
retro_atomic_int_t clk_ppm;
retro_atomic_int_t clk_valid;
retro_atomic_int_t clk_ppm; /* AUDIO_CLOCK_PPM_NONE until known */
/* The server saying its rate or its period has changed. Both make
* everything downstream wrong - what the resampler produces, what
* the ring was sized for, how long a write waits - so both ask the
@@ -145,9 +149,13 @@ static size_t ja_read_deinterleaved(jack_t *jd, float *dst[JACK_MAX_PORTS], jack
static bool ja_device_clock_ppm(void *data, double *ppm)
{
jack_t *jd = (jack_t*)data;
if (!jd || !retro_atomic_load_acquire_int(&jd->clk_valid))
int v;
if (!jd)
return false;
*ppm = (double)retro_atomic_load_acquire_int(&jd->clk_ppm);
v = retro_atomic_load_acquire_int(&jd->clk_ppm);
if (v == AUDIO_CLOCK_PPM_NONE)
return false;
*ppm = (double)v;
return true;
}
@@ -221,7 +229,6 @@ static int ja_process_cb(jack_nframes_t nframes, void *data)
if (ppm > -100000.0 && ppm < 100000.0)
{
retro_atomic_store_release_int(&jd->clk_ppm, (int)ppm);
retro_atomic_store_release_int(&jd->clk_valid, 1);
}
}
}
@@ -287,8 +294,8 @@ static int ja_sample_rate_cb(jack_nframes_t nframes, void *data)
if (jd)
{
retro_atomic_store_release_int(&jd->reconfigure, 1);
retro_atomic_store_release_int(
&audio_state_get_ptr()->reinit_request, 1);
if (jd->reinit_request)
retro_atomic_store_release_int(jd->reinit_request, 1);
}
return 0;
}
@@ -299,8 +306,8 @@ static int ja_buffer_size_cb(jack_nframes_t nframes, void *data)
if (jd)
{
retro_atomic_store_release_int(&jd->reconfigure, 1);
retro_atomic_store_release_int(
&audio_state_get_ptr()->reinit_request, 1);
if (jd->reinit_request)
retro_atomic_store_release_int(jd->reinit_request, 1);
}
return 0;
}
@@ -441,14 +448,15 @@ static void *ja_init(const char *device,
goto error;
#endif
jd->reinit_request = &audio_state_get_ptr()->reinit_request;
jd->client = jack_client_open("RetroArch", JackNullOption, NULL);
if (!jd->client)
goto error;
*new_rate = jack_get_sample_rate(jd->client);
jd->clk_rate = *new_rate;
retro_atomic_int_init(&jd->clk_ppm, 0);
retro_atomic_int_init(&jd->clk_valid, 0);
retro_atomic_int_init(&jd->clk_ppm, AUDIO_CLOCK_PPM_NONE);
jack_set_process_callback(jd->client, ja_process_cb, jd);
/* Registered before activation, as JACK requires of a client that
@@ -697,6 +705,7 @@ static void ja_free(void *data)
{
jack_t *jd = (jack_t*)data;
size_t x;
int clk_ppm;
retro_atomic_store_release_int(&jd->shutdown, 1);
@@ -705,10 +714,10 @@ static void ja_free(void *data)
* are measurements, and a clock estimate that is wrong is worse
* than one that is absent. With a device driving the graph this is
* that device; with something else driving it, it is that. */
if (retro_atomic_load_acquire_int(&jd->clk_valid))
clk_ppm = retro_atomic_load_acquire_int(&jd->clk_ppm);
if (clk_ppm != AUDIO_CLOCK_PPM_NONE)
RARCH_LOG("[JACK] Server clock, fitted from the cycle times:"
" %+d ppm against %u Hz.\n",
retro_atomic_load_acquire_int(&jd->clk_ppm), jd->clk_rate);
" %+d ppm against %u Hz.\n", clk_ppm, jd->clk_rate);
else
RARCH_LOG("[JACK] Server clock: not enough usable cycle times to"
" fit one.\n");
+23 -3
View File
@@ -137,6 +137,10 @@ typedef struct al
/* Frames the source has finished playing, for the sink rate
* estimate; see al_frames_consumed(). */
retro_atomic_size_t consumed;
/* Times the source was found STOPPED with a write to make: it had
* played its queue out and the device went quiet for want of
* audio. INITIAL is the first write, which has not started it. */
retro_atomic_size_t underruns;
} al_t;
static void al_free(void *data)
@@ -327,8 +331,14 @@ static void *al_init(const char *device, unsigned rate, unsigned latency,
* OpenAL Soft extension, so asked for rather than assumed: core
* OpenAL has nothing of the kind and Apple's does not carry it. */
if (alcIsExtensionPresent(al->handle, "ALC_SOFT_device_clock"))
al->clk_get = (al_get_integer64v_t)alcGetProcAddress(
al->handle, "alcGetInteger64vSOFT");
{
/* Through a union, as al_init_events() resolves its own: an
* object pointer cast to a function pointer is not legal C. */
union { void *p; al_get_integer64v_t f; } clk;
clk.p = alcGetProcAddress(al->handle, "alcGetInteger64vSOFT");
if (clk.p)
al->clk_get = clk.f;
}
RARCH_LOG("[OpenAL] Device clock: %s.\n",
al->clk_get ? "reported by the implementation (ALC_SOFT_device_clock)"
: "not offered by this implementation");
@@ -604,7 +614,11 @@ static ssize_t al_write(void *data, const void *s, size_t len)
ALint val;
alGetSourcei(al->source, AL_SOURCE_STATE, &val);
if (val != AL_PLAYING)
{
if (val == AL_STOPPED)
retro_atomic_fetch_add_size(&al->underruns, 1);
alSourcePlay(al->source);
}
}
return _len;
@@ -784,6 +798,12 @@ static void al_device_list_free(void *u, void *slp)
string_list_free(sl);
}
static size_t al_underruns(void *data)
{
al_t *al = (al_t*)data;
return al ? retro_atomic_load_acquire_size(&al->underruns) : 0;
}
audio_driver_t audio_openal = {
al_init,
al_write,
@@ -801,7 +821,7 @@ audio_driver_t audio_openal = {
NULL, /* write_raw */
al_wait_writable,
al_frames_consumed,
NULL, /* underruns */
al_underruns,
al_layout,
NULL, /* frames_consumed_fallback */
al_device_clock_ppm
+77 -3
View File
@@ -34,7 +34,7 @@
* reached in normal operation - the callback fires as each enqueued
* block finishes - so the value only decides how long a device that
* has stopped consuming takes to be noticed. Matches the flat
* bail-out timeouts in sdl_audio.c and wasapi.c. */
* bail-out timeouts in sdl2_audio.c and wasapi.c. */
#define OPENSL_STALL_TIMEOUT_US 256000
/* Helper macros, COM-style. */
@@ -69,6 +69,10 @@ typedef struct sl
* level 21+); blocks then hold 32-bit float frames and the frontend
* skips its float-to-int16 pass. */
bool use_float;
/* The layout asked for, and the channels the player was created
* with. They agree unless the wider format was refused. */
uint32_t layout;
unsigned channels;
bool nonblock;
bool is_paused;
} sl_t;
@@ -104,6 +108,9 @@ typedef struct sl_shared
* and the ASIO callback count. Written only by the callback, read by
* the frontend through sl_frames_consumed(). */
retro_atomic_size_t consumed;
/* Callbacks that left the queue empty: the device has nothing to
* play next and goes quiet until the writer enqueues again. */
retro_atomic_size_t underruns;
unsigned frames_per_block;
bool park_ready;
} sl_shared_t;
@@ -128,7 +135,8 @@ static void opensl_callback(SLAndroidSimpleBufferQueueItf bq, void *ctx)
/* A player torn down, or replaced, since this was dispatched. */
if (!retro_atomic_load_acquire_int(&sh->live) || bq != sh->bq)
return;
retro_atomic_fetch_sub_int(&sh->buffered_blocks, 1);
if (retro_atomic_fetch_sub_int(&sh->buffered_blocks, 1) == 1)
retro_atomic_fetch_add_size(&sh->underruns, 1);
/* A block the device has played: device time, whatever the writer
* managed to supply. */
retro_atomic_fetch_add_size(&sh->consumed, sh->frames_per_block);
@@ -219,6 +227,7 @@ static void *sl_init(const char *device, unsigned rate, unsigned latency,
{
unsigned i;
unsigned frames_per_block;
unsigned channels;
unsigned frame_size = 2 * sizeof(int16_t);
SLDataFormat_PCM fmt_pcm = {0};
#if defined(ANDROID) && defined(__ANDROID_API__) && (__ANDROID_API__ >= 21)
@@ -254,6 +263,14 @@ static void *sl_init(const char *device, unsigned rate, unsigned latency,
RARCH_LOG("[OpenSL] Requested audio latency: %u ms.\n", latency);
/* Anything wider than stereo needs the PCM_EX format below, which
* is API level 21 and up; without it the count is not one this
* driver can ask for and the stereo path is what runs. */
sl->layout = audio_driver_requested_layout();
channels = audio_layout_channels(sl->layout);
if (channels < 2 || channels > 8)
channels = 2;
GOTO_IF_FAIL(slCreateEngine(&sl->engine_object, 0, NULL, 0, NULL, NULL));
GOTO_IF_FAIL(SLObjectItf_Realize(sl->engine_object, SL_BOOLEAN_FALSE));
GOTO_IF_FAIL(SLObjectItf_GetInterface(sl->engine_object, SL_IID_ENGINE, &sl->engine));
@@ -299,10 +316,46 @@ static void *sl_init(const char *device, unsigned rate, unsigned latency,
audio_sink.pLocator = &loc_outmix;
#if defined(ANDROID) && defined(__ANDROID_API__) && (__ANDROID_API__ >= 21)
/* The layout the frontend wants, where the player takes it. The
* frontend's mask is the channel mask OpenSL asks for - both are
* the WAVEFORMATEXTENSIBLE bits - so it goes across as it is, and
* the frames arrive in its ascending-bit order either way. Refused,
* this falls through to the stereo attempts below unchanged. */
if (channels > 2)
{
fmt_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
fmt_pcm_ex.numChannels = channels;
fmt_pcm_ex.sampleRate = rate * 1000; /* milli-Hz */
fmt_pcm_ex.bitsPerSample = 32;
fmt_pcm_ex.containerSize = 32;
fmt_pcm_ex.channelMask = sl->layout;
fmt_pcm_ex.endianness = SL_BYTEORDER_LITTLEENDIAN;
fmt_pcm_ex.representation = SL_ANDROID_PCM_REPRESENTATION_FLOAT;
audio_src.pFormat = &fmt_pcm_ex;
res = SLEngineItf_CreateAudioPlayer(sl->engine, &sl->buffer_queue_object,
&audio_src, &audio_sink, 1, &id, &req);
if (res == SL_RESULT_SUCCESS)
{
sl->use_float = true;
sl->channels = channels;
frame_size = channels * sizeof(float);
RARCH_LOG("[OpenSL] Float output, %u channels.\n", channels);
}
else
{
sl->buffer_queue_object = NULL;
RARCH_LOG("[OpenSL] %u channels refused (0x%x), using stereo.\n",
channels, (unsigned)res);
}
}
/* Float first. API level 21 added SLAndroidDataFormat_PCM_EX; a
* runtime that refuses it (older device, or a libOpenSLES built
* without it) fails player creation, and the 16-bit format below is
* tried in that case. */
if (!sl->use_float)
{
fmt_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX;
fmt_pcm_ex.numChannels = 2;
fmt_pcm_ex.sampleRate = rate * 1000; /* milli-Hz */
@@ -318,6 +371,7 @@ static void *sl_init(const char *device, unsigned rate, unsigned latency,
if (res == SL_RESULT_SUCCESS)
{
sl->use_float = true;
sl->channels = 2;
frame_size = 2 * sizeof(float);
RARCH_LOG("[OpenSL] Float output.\n");
}
@@ -327,6 +381,7 @@ static void *sl_init(const char *device, unsigned rate, unsigned latency,
RARCH_LOG("[OpenSL] Float output refused (0x%x), using 16-bit.\n",
(unsigned)res);
}
}
#endif
if (!sl->use_float)
@@ -343,6 +398,7 @@ static void *sl_init(const char *device, unsigned rate, unsigned latency,
GOTO_IF_FAIL(SLEngineItf_CreateAudioPlayer(sl->engine, &sl->buffer_queue_object,
&audio_src, &audio_sink,
1, &id, &req));
sl->channels = 2;
frame_size = 2 * sizeof(int16_t);
}
GOTO_IF_FAIL(SLObjectItf_Realize(sl->buffer_queue_object, SL_BOOLEAN_FALSE));
@@ -579,6 +635,22 @@ static bool sl_use_float(void *data)
return sl->use_float;
}
static size_t sl_underruns(void *data)
{
sl_t *sl = (sl_t*)data;
if (!sl)
return 0;
return retro_atomic_load_acquire_size(&sl_shared.underruns);
}
static uint32_t sl_layout(void *data)
{
sl_t *sl = (sl_t*)data;
if (!sl || sl->channels != audio_layout_channels(sl->layout))
return AUDIO_LAYOUT_STEREO;
return sl->layout;
}
audio_driver_t audio_opensl = {
sl_init,
sl_write,
@@ -595,5 +667,7 @@ audio_driver_t audio_opensl = {
sl_buffer_size,
NULL, /* write_raw */
sl_wait_writable,
sl_frames_consumed
sl_frames_consumed,
sl_underruns,
sl_layout
};
+10 -1
View File
@@ -37,7 +37,9 @@
#include "../../config.h"
#endif
#include <retro_miscellaneous.h>
#include "../audio_driver.h"
#include "../audio_device_label.h"
#include "../../verbosity.h"
#ifdef HAVE_OSS_BSD
@@ -81,12 +83,19 @@ static void *oss_init(const char *device,
unsigned *new_out_rate)
{
int frags, frag, channels, format, new_rate;
char oss_device[PATH_MAX_LENGTH];
oss_audio_t *ossaudio = (oss_audio_t*)calloc(1, sizeof(oss_audio_t));
const char *oss_device = device ? device : DEFAULT_OSS_DEV;
if (!ossaudio)
return NULL;
/* The device may be a list entry, "/dev/dsp0 (card)": the path is
* what is opened. */
if (device)
audio_device_label_path(oss_device, sizeof(oss_device), device);
else
strlcpy(oss_device, DEFAULT_OSS_DEV, sizeof(oss_device));
if ((ossaudio->fd = open(oss_device, O_WRONLY)) < 0)
{
free(ossaudio);
+14 -10
View File
@@ -61,6 +61,10 @@ typedef struct pipewire_audio
* way. Written only by the callback, read by the frontend through
* pwire_frames_consumed(). */
retro_atomic_size_t consumed;
/* Quanta the graph asked for and this driver had nothing at all
* for, so the whole buffer went out as silence. A short read is
* not one: that hands the graph fewer frames, not silence. */
retro_atomic_size_t underruns;
/* The device clock, fitted from the time report the graph already
* hands over.
@@ -105,7 +109,6 @@ static size_t pwire_calc_frame_size(enum spa_audio_format fmt, uint32_t nchannel
case SPA_AUDIO_FORMAT_F32_BE:
case SPA_AUDIO_FORMAT_F32_LE:
return 4 * nchannels;
break;
default:
RARCH_ERR("[PipeWire] Bad spa_audio_format %d.\n", fmt);
break;
@@ -406,13 +409,6 @@ static bool pwire_microphone_mic_alive(const void *driver_context, const void *m
return pw_stream_get_state(mic->stream, &error) == PW_STREAM_STATE_STREAMING;
}
static void pwire_microphone_set_nonblock_state(void *driver_context, bool nonblock)
{
pipewire_core_t *pw = (pipewire_core_t*)driver_context;
if (pw)
pw->nonblock = nonblock;
}
static struct string_list *pwire_microphone_device_list_new(const void *driver_context)
{
pipewire_core_t *pw = (pipewire_core_t*)driver_context;
@@ -608,7 +604,6 @@ microphone_driver_t microphone_pipewire = {
pwire_microphone_init,
pwire_microphone_free,
pwire_microphone_read,
pwire_microphone_set_nonblock_state,
"pipewire",
pwire_microphone_device_list_new,
pwire_microphone_device_list_free,
@@ -662,8 +657,11 @@ static void pwire_playback_process_cb(void *data)
avail = spa_ringbuffer_get_read_index(&audio->ring, &idx);
if (avail <= 0)
{
/* fill rest buffer with silence */
memset(p, 0x00, n_bytes);
retro_atomic_fetch_add_size(&audio->underruns, 1);
}
else
{
if (avail < (int32_t)n_bytes)
@@ -1264,6 +1262,12 @@ static size_t pwire_wait_writable(void *data, size_t len)
return (size_t)avail;
}
static size_t pwire_underruns(void *data)
{
pipewire_audio_t *audio = (pipewire_audio_t*)data;
return audio ? retro_atomic_load_acquire_size(&audio->underruns) : 0;
}
audio_driver_t audio_pipewire = {
pwire_init,
pwire_write,
@@ -1281,7 +1285,7 @@ audio_driver_t audio_pipewire = {
NULL, /* write_raw */
pwire_wait_writable,
pwire_frames_consumed,
NULL, /* underruns */
pwire_underruns,
pwire_layout,
NULL, /* frames_consumed_fallback */
pwire_device_clock_ppm
+96 -11
View File
@@ -20,14 +20,32 @@
#include <kernel.h>
#include <audsrv.h>
#include <retro_timers.h>
#include "../audio_driver.h"
#define AUDIO_BUFFER 128 * 1024
#define AUDIO_CHANNELS 2
#define AUDIO_BITS 16
#define AUDIO_FRAME_BYTES (AUDIO_CHANNELS * (AUDIO_BITS / 8))
/* audsrv_wait_audio() is the SDK's own wait and has no bound, so a
* server that stops draining parks the caller for good. Polling what
* audsrv reports free ends either way: the laps cap the case where it
* keeps draining but never frees enough, and the case where it has
* stopped. Never reached while audio plays - a period frees long
* before this does. */
#define AUDIO_WAIT_LAPS 8
#define AUDIO_WAIT_US 4000
typedef struct ps2_audio
{
/* Bytes handed to audsrv since the format was set. Less what its
* ring still holds, that is what the SPU has played. */
uint64_t bytes_written;
/* What audsrv reported free before anything was queued, which is
* its ring. write_avail() is measured against it. */
size_t ring_size;
/* TODO/FIXME - nonblock is not implemented */
bool nonblock;
bool running;
@@ -37,6 +55,7 @@ static void *ps2_audio_init(const char *device,
unsigned rate, unsigned latency,
unsigned *new_rate)
{
int avail;
struct audsrv_fmt_t format;
ps2_audio_t *ps2 = (ps2_audio_t*)calloc(1, sizeof(ps2_audio_t));
@@ -50,6 +69,10 @@ static void *ps2_audio_init(const char *device,
audsrv_set_format(&format);
audsrv_set_volume(MAX_VOLUME);
/* Asked with nothing queued, so what is free is the whole ring. */
avail = audsrv_available();
ps2->ring_size = (avail > 0) ? (size_t)avail : (size_t)AUDIO_BUFFER;
return ps2;
}
@@ -66,10 +89,14 @@ static void ps2_audio_free(void *data)
static ssize_t ps2_audio_write(void *data, const void *s, size_t len)
{
int ret;
ps2_audio_t* ps2 = (ps2_audio_t*)data;
if (!ps2->running)
return -1;
return audsrv_play_audio(s, len);
ret = audsrv_play_audio(s, len);
if (ret > 0)
ps2->bytes_written += (uint64_t)ret;
return ret;
}
static bool ps2_audio_alive(void *data)
@@ -107,16 +134,72 @@ static void ps2_audio_set_nonblock_state(void *data, bool toggle)
ps2->nonblock = toggle;
}
/* No write_avail(): the driver hands audio to audsrv and has nothing
* to measure its fill with. It used to report the whole buffer as free
* whenever it was running, which the rate control read as an empty
* device every frame and answered with its full upward correction,
* permanently - audio a half percent fast, and sharp, for as long as
* rate control was on. With no write_avail() the frontend disables
* rate control for this driver and says so, which is the truth. */
/* What audsrv says is free in its ring: a measurement, not the whole
* buffer reported as free every frame, which is what this used to do
* and what rate control answered with its full upward correction for
* as long as it was on. */
static size_t ps2_audio_write_avail(void *data)
{
int avail;
ps2_audio_t *ps2 = (ps2_audio_t*)data;
if (!ps2 || !ps2->running)
return 0;
if ((avail = audsrv_available()) <= 0)
return 0;
if ((size_t)avail > ps2->ring_size)
return ps2->ring_size;
return (size_t)avail;
}
/* Frames the SPU has played: what was handed over, less what audsrv
* still holds for it. */
static size_t ps2_audio_frames_consumed(void *data)
{
int queued;
uint64_t played;
ps2_audio_t *ps2 = (ps2_audio_t*)data;
if (!ps2)
return 0;
if ((queued = audsrv_queued()) < 0)
queued = 0;
played = ((uint64_t)queued >= ps2->bytes_written)
? 0 : ps2->bytes_written - (uint64_t)queued;
return (size_t)(played / AUDIO_FRAME_BYTES);
}
static size_t ps2_audio_wait_writable(void *data, size_t len)
{
int laps = AUDIO_WAIT_LAPS;
ps2_audio_t *ps2 = (ps2_audio_t*)data;
if (!ps2 || !ps2->running)
return 0;
/* Capped at half the ring, so the wait always has an end within a
* draining server's reach. */
if (len > ps2->ring_size / 2)
len = ps2->ring_size / 2;
for (;;)
{
size_t avail = ps2_audio_write_avail(ps2);
if (avail >= len)
return avail;
if (--laps < 0)
break;
retro_sleep_us(AUDIO_WAIT_US);
}
return 0;
}
static bool ps2_audio_use_float(void *data) { return false; }
static size_t ps2_audio_buffer_size(void *data) { return AUDIO_BUFFER; }
static size_t ps2_audio_buffer_size(void *data)
{
ps2_audio_t *ps2 = (ps2_audio_t*)data;
return ps2 ? ps2->ring_size : (size_t)AUDIO_BUFFER;
}
audio_driver_t audio_ps2 = {
ps2_audio_init,
@@ -130,7 +213,9 @@ audio_driver_t audio_ps2 = {
"ps2",
NULL,
NULL,
NULL, /* write_avail */
ps2_audio_write_avail,
ps2_audio_buffer_size,
NULL /* write_raw */
NULL, /* write_raw */
ps2_audio_wait_writable,
ps2_audio_frames_consumed
};
+92 -14
View File
@@ -16,7 +16,9 @@
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <retro_atomic.h>
#include <retro_spsc.h>
#include <defines/ps3_defines.h>
@@ -50,7 +52,24 @@ typedef struct
uint32_t audio_port;
bool nonblock;
bool started;
volatile bool quit_thread;
/* Set by the thread tearing the driver down, read by the output
* thread's loop. An atomic and not a volatile bool: volatile orders
* nothing between the two. */
retro_atomic_int_t quit_thread;
/* Blocks the output thread had nothing for and sent as silence. */
retro_atomic_size_t underruns;
/* One block, handed to audioAddData() as it comes out of the ring.
* On the heap and not the output thread's stack: that thread is
* created with 4 KiB, which a stereo block already half fills and
* a six channel one would run straight past. Aligned as the stack
* copy was. */
float *out_block;
size_t out_block_size;
/* The layout asked for, and the channels the port was opened with.
* They agree unless the port refused the count, which is what
* layout() reports on. */
uint32_t layout;
unsigned channels;
} ps3_audio_t;
@@ -60,27 +79,30 @@ static void ps3_event_loop(void *data)
static void ps3_event_loop(uint64_t data)
#endif
{
float out_tmp[AUDIO_BLOCK_SAMPLES * AUDIO_CHANNELS]
__attribute__((aligned(16)));
sys_event_queue_t id;
sys_ipc_key_t key;
sys_event_t event;
ps3_audio_t *aud = (ps3_audio_t*)(uintptr_t)data;
float *out_block = aud->out_block;
size_t block = aud->out_block_size;
audioCreateNotifyEventQueue(&id, &key);
audioSetNotifyEventQueue(key);
while (!aud->quit_thread)
while (!retro_atomic_load_acquire_int(&aud->quit_thread))
{
sysEventQueueReceive(id, &event, PS3_SYS_NO_TIMEOUT);
if (retro_spsc_read_avail(&aud->ring) >= sizeof(out_tmp))
retro_spsc_read(&aud->ring, out_tmp, sizeof(out_tmp));
if (retro_spsc_read_avail(&aud->ring) >= block)
retro_spsc_read(&aud->ring, out_block, block);
else
memset(out_tmp, 0, sizeof(out_tmp));
{
memset(out_block, 0, block);
retro_atomic_fetch_add_size(&aud->underruns, 1);
}
sysLwCondSignal(&aud->cond);
audioAddData(aud->audio_port, out_tmp,
audioAddData(aud->audio_port, out_block,
AUDIO_BLOCK_SAMPLES, 1.0);
}
@@ -92,6 +114,7 @@ static void *ps3_audio_init(const char *device,
unsigned rate, unsigned latency,
unsigned *new_rate)
{
unsigned channels;
audioPortParam params;
ps3_audio_t *data = NULL;
#ifdef __PSL1GHT__
@@ -112,7 +135,16 @@ static void *ps3_audio_init(const char *device,
audioInit();
params.numChannels = AUDIO_CHANNELS;
/* The layout the frontend wants, where the port takes that many
* channels: libaudio opens two, six or eight, so a four channel
* mask is not one of them and opens as stereo. layout() reports
* what was granted. */
data->layout = audio_driver_requested_layout();
channels = audio_layout_channels(data->layout);
if (channels != 2 && channels != 6 && channels != 8)
channels = AUDIO_CHANNELS;
params.numChannels = channels;
params.numBlocks = AUDIO_BLOCKS;
params.param_attrib = 0;
#if 0
@@ -124,16 +156,40 @@ static void *ps3_audio_init(const char *device,
if (audioPortOpen(&params, &data->audio_port) != CELL_OK)
{
/* A port that would not take the wider count still takes two. */
if (channels == AUDIO_CHANNELS)
{
audioQuit();
free(data);
return NULL;
}
channels = AUDIO_CHANNELS;
params.numChannels = channels;
if (audioPortOpen(&params, &data->audio_port) != CELL_OK)
{
audioQuit();
free(data);
return NULL;
}
}
data->channels = channels;
data->out_block_size = AUDIO_BLOCK_SAMPLES * channels * sizeof(float);
data->out_block = (float*)memalign(16, data->out_block_size);
if (!data->out_block)
{
audioPortClose(data->audio_port);
audioQuit();
free(data);
return NULL;
}
data->ring_size = AUDIO_BLOCK_SAMPLES *
AUDIO_CHANNELS * AUDIO_BLOCKS * sizeof(float);
channels * AUDIO_BLOCKS * sizeof(float);
data->ring_init = retro_spsc_init(&data->ring, data->ring_size);
if (!data->ring_init)
{
free(data->out_block);
audioPortClose(data->audio_port);
audioQuit();
free(data);
@@ -145,6 +201,8 @@ static void *ps3_audio_init(const char *device,
audioPortStart(data->audio_port);
data->started = true;
retro_atomic_int_init(&data->quit_thread, 0);
retro_atomic_size_init(&data->underruns, 0);
sysThreadCreate(&data->thread, ps3_event_loop,
#ifdef __PSL1GHT__
data,
@@ -190,7 +248,8 @@ static ssize_t ps3_audio_write(void *data, const void *s, size_t len)
int laps = PS3_AUDIO_WAIT_LAPS;
while (ps3_audio_write_avail(aud) < len)
{
if (!aud->started || aud->quit_thread)
if ( !aud->started
|| retro_atomic_load_acquire_int(&aud->quit_thread))
return 0;
ps3_audio_wait_block(aud);
if (--laps < 0)
@@ -244,7 +303,7 @@ static void ps3_audio_free(void *data)
uint64_t val;
ps3_audio_t *aud = data;
aud->quit_thread = true;
retro_atomic_store_release_int(&aud->quit_thread, 1);
ps3_audio_start(aud, false);
sysThreadJoin(aud->thread, &val);
@@ -257,6 +316,7 @@ static void ps3_audio_free(void *data)
sysLwMutexDestroy(&aud->cond_lock);
sysLwCondDestroy(&aud->cond);
free(aud->out_block);
free(data);
}
@@ -293,7 +353,8 @@ static size_t ps3_audio_wait_writable(void *data, size_t len)
for (;;)
{
if (!aud->started || aud->quit_thread)
if ( !aud->started
|| retro_atomic_load_acquire_int(&aud->quit_thread))
return 0;
avail = ps3_audio_write_avail(aud);
if (avail >= len)
@@ -307,6 +368,20 @@ static size_t ps3_audio_wait_writable(void *data, size_t len)
}
}
static uint32_t ps3_audio_layout(void *data)
{
ps3_audio_t *aud = (ps3_audio_t*)data;
if (!aud || aud->channels != audio_layout_channels(aud->layout))
return AUDIO_LAYOUT_STEREO;
return aud->layout;
}
static size_t ps3_audio_underruns(void *data)
{
ps3_audio_t *aud = (ps3_audio_t*)data;
return aud ? retro_atomic_load_acquire_size(&aud->underruns) : 0;
}
audio_driver_t audio_ps3 = {
ps3_audio_init,
ps3_audio_write,
@@ -322,5 +397,8 @@ audio_driver_t audio_ps3 = {
ps3_audio_write_avail,
ps3_audio_buffer_size,
NULL, /* write_raw */
ps3_audio_wait_writable
ps3_audio_wait_writable,
NULL, /* frames_consumed */
ps3_audio_underruns,
ps3_audio_layout
};
+527
View File
@@ -0,0 +1,527 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2014-2017 - Ali Bouhlel
*
* 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/>.
*/
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <rthreads/rthreads.h>
#include <rthreads/retro_eventcount.h>
#include <retro_atomic.h>
#include <libSceAudioOut.h>
#include <defines/ps4_defines.h>
#include "../audio_driver.h"
typedef struct ps4_audio
{
/* The ring, followed by one period of silence for the worker
* to output when the ring runs short. */
uint32_t* buffer_u32;
sthread_t *worker_thread;
/* The writer's park; the worker notifies after every period it
* frees. Gated: the steady state, writer ahead and never waiting,
* costs the worker nothing per period, where the old
* signal-every-iteration took cond_lock each time. */
retro_eventcount_t park;
/* For the sink rate estimate and the statistics overlay.
* Only the worker writes either. */
retro_atomic_size_t consumed;
retro_atomic_size_t underruns;
int port;
int rate;
/* Frames the ring holds. Set before the worker starts, and
* fixed for the life of the driver. */
unsigned ring;
/* The ring's index pair, the SPSC discipline by hand because the
* worker consumes in place - ps4->buffer_u32 + read_pos goes straight
* into the output syscall, and a retro_spsc would add a bounce
* copy per period. Producer publishes write_pos with release
* after the samples land; consumer publishes read_pos with
* release after the syscall returns; each side reads the other's
* index with acquire and its own relaxed. fifo_lock guarded
* nothing else, and is gone. */
retro_atomic_int_t read_pos;
retro_atomic_int_t write_pos;
retro_atomic_int_t running;
bool nonblock;
} ps4_audio_t;
#define AUDIO_OUT_COUNT 512u
/* The ring is what the latency setting asks for, so it is a whole
* number of periods rather than a power of two and the wrap is a
* compare. The window the device is playing stays inside the ring
* as held until the output call after it returns (see the worker),
* so the writer's room is the ring less that window less one frame.
* Six periods is the floor: five of room, which holds a delivery of
* a video frame of audio - 1600 frames for 30 Hz content at 48 kHz -
* beside the window in flight without dipping under a period each
* refill. Anything at or above the default is unaffected. */
#define AUDIO_RING_MIN (AUDIO_OUT_COUNT * 6u)
#define AUDIO_RING_MAX (AUDIO_OUT_COUNT * 64u)
/* Frames held, and frames the writer may still place - one short of
* the ring, so a full ring and an empty one do not both read as
* write_pos == read_pos. */
#define RING_HELD(w, r, n) \
((unsigned)((w) >= (r) ? (w) - (r) : (n) - (r) + (w)))
#define RING_FREE(w, r, n) ((n) - 1u - RING_HELD((w), (r), (n)))
/* Bound on any wait for the audio thread to consume: one wait, and how
* many of them before the caller gets the pass back. The thread
* consumes a period every period while the device runs. */
#define PS4_AUDIO_WAIT_US 100000
#define PS4_AUDIO_WAIT_LAPS 8
/* The only rate sceAudioOut opens at. */
#define PS4_AUDIO_RATE 48000
/* The ring the latency setting asks for: that many milliseconds at
* the rate the device opened at, in whole periods so no window the
* worker hands over crosses the ring's end. The window the device is
* playing is inside it, so this is the whole of the driver's
* buffering and what buffer_size() reports. */
static unsigned ps4_ring_frames(unsigned rate, unsigned latency)
{
unsigned frames = rate * latency / 1000u;
frames = ((frames + AUDIO_OUT_COUNT - 1u) / AUDIO_OUT_COUNT)
* AUDIO_OUT_COUNT;
if (frames < AUDIO_RING_MIN)
return AUDIO_RING_MIN;
if (frames > AUDIO_RING_MAX)
return AUDIO_RING_MAX;
return frames;
}
/* Return port used */
static int ps4_configure_audio(unsigned rate, unsigned *new_rate)
{
/* Every other rate is refused, which leaves the session with no
* audio at all; open at the one it takes and tell the frontend to
* resample to it. */
if (rate != PS4_AUDIO_RATE)
*new_rate = PS4_AUDIO_RATE;
return sceAudioOutOpen(0xff,
SCE_AUDIO_OUT_PORT_TYPE_MAIN, 0, AUDIO_OUT_COUNT,
PS4_AUDIO_RATE, SCE_AUDIO_OUT_MODE_STEREO);
}
static void ps4_audio_mainloop(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
/* The worker's own cursor: the next window to hand over. The
* published read_pos runs one window behind it while a ring
* window is in flight. The output call returns once the window
* handed to the previous call has been output, and with this
* call's window queued and still to be read by the hardware -
* the SDKs document passing NULL as the way to wait for that
* last window - so the window a call hands over is the device's
* until the call after it returns, and is released there. */
uint16_t play_pos = (uint16_t)
retro_atomic_load_relaxed_int(&ps4->read_pos);
while (retro_atomic_load_acquire_int(&ps4->running))
{
bool cond = false;
uint16_t next_pos = play_pos;
uint16_t write_pos = (uint16_t)
retro_atomic_load_acquire_int(&ps4->write_pos);
/* A period in hand is a period to play. Holding one back
* as a reserve only hands the device silence while the
* ring has audio in it. */
cond = RING_HELD(write_pos, play_pos, ps4->ring)
< AUDIO_OUT_COUNT;
if (!cond)
{
next_pos += AUDIO_OUT_COUNT;
if (next_pos >= ps4->ring)
next_pos = 0;
}
else
retro_atomic_fetch_add_size(&ps4->underruns, 1);
sceAudioOutOutput(ps4->port,
ps4->buffer_u32
+ (cond ? ps4->ring : play_pos));
retro_atomic_fetch_add_size(&ps4->consumed, AUDIO_OUT_COUNT);
/* The previous call's window has been output; this call's is
* the one in flight, so everything before play_pos is the
* writer's. When this call handed over silence, play_pos is
* where read_pos already stands. */
retro_atomic_store_release_int(&ps4->read_pos, play_pos);
play_pos = next_pos;
retro_eventcount_notify(&ps4->park);
}
return;
}
static void *ps4_audio_init(const char *device,
unsigned rate, unsigned latency,
unsigned *new_rate)
{
int port;
ps4_audio_t *ps4 = (ps4_audio_t*)calloc(1, sizeof(ps4_audio_t));
if (!ps4)
return NULL;
/* The library is what opens the port, so it comes up first. */
sceAudioOutInit();
if ((port = ps4_configure_audio(rate, new_rate)) < 0)
{
free(ps4);
return NULL;
}
ps4->rate = *new_rate ? *new_rate : rate;
ps4->ring = ps4_ring_frames(ps4->rate, latency);
/* Cache aligned, not necessary but helpful. */
/* The ring, and one period of silence past its end. */
ps4->buffer_u32 = (uint32_t*)calloc(ps4->ring + AUDIO_OUT_COUNT,
sizeof(uint32_t));
retro_atomic_size_init(&ps4->consumed, 0);
retro_atomic_size_init(&ps4->underruns, 0);
retro_atomic_int_init(&ps4->read_pos, 0);
retro_atomic_int_init(&ps4->write_pos, 0);
ps4->port = port;
if ( !ps4->buffer_u32
|| !retro_eventcount_init(&ps4->park))
{
sceAudioOutClose(port);
free(ps4->buffer_u32);
free(ps4);
return NULL;
}
ps4->nonblock = false;
/* running has to be set before the worker starts, so a worker that
* never starts leaves a driver reporting itself alive with nothing
* consuming the ring: writes wait out their laps and return zero,
* and teardown acts on a state that was never true. */
retro_atomic_int_init(&ps4->running, 1);
if (!(ps4->worker_thread = sthread_create(ps4_audio_mainloop, ps4)))
{
retro_eventcount_free(&ps4->park);
sceAudioOutClose(port);
free(ps4->buffer_u32);
free(ps4);
return NULL;
}
return ps4;
}
static void ps4_audio_free(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (!ps4)
return;
if (retro_atomic_load_acquire_int(&ps4->running))
{
if (ps4->worker_thread)
{
retro_atomic_store_release_int(&ps4->running, 0);
sthread_join(ps4->worker_thread);
}
}
retro_eventcount_free(&ps4->park);
free(ps4->buffer_u32);
ps4->worker_thread = NULL;
sceAudioOutClose(ps4->port);
free(ps4);
}
static ssize_t ps4_audio_write(void *data, const void *s, size_t len)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
uint16_t write_pos = (uint16_t)
retro_atomic_load_relaxed_int(&ps4->write_pos);
/* Frames, and wide enough for them: narrowed to uint16_t, a write of
* more than 65535 frames wrapped to a small count that the room check
* then waved through, and the copy below took len bytes for it. The
* ring holds one frame short of its length, so no wait can free room
* for more than that at once - take what fits and report the short
* write rather than spending the laps on room that will not come. A
* len that is not whole frames carries the frames it has. */
size_t sample_count = len / sizeof(uint32_t);
if (!retro_atomic_load_acquire_int(&ps4->running))
return -1;
if (!sample_count)
return 0;
if (sample_count > (size_t)(ps4->ring - 1u))
sample_count = ps4->ring - 1u;
len = sample_count * sizeof(uint32_t);
/* The ring is counted in uint32_t frames (write_pos, read_pos,
* ring); len is bytes. Both room checks below used to
* compare the frame count against len, i.e. demanded four times the
* room actually needed: non-blocking writes were refused - the audio
* dropped - with plenty of space free, and blocking ones waited for
* space that rate control was not trying to free. ps4_write_avail()
* and ps4_wait_writable() already convert; compare frames to
* frames here too. */
if (ps4->nonblock)
{
if (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&ps4->read_pos),
ps4->ring) < sample_count)
return 0;
}
{
/* The audio thread notifies every period it consumes. One that
* has stopped consuming - the device suspended, the thread gone
* - notifies nothing; each wait is bounded, the loop is
* lap-bounded, and the write then returns having written
* nothing rather than holding the caller. Room and liveness
* are re-checked inside the eventcount's window, so a period
* freed between the check and the park costs nothing. */
int laps = PS4_AUDIO_WAIT_LAPS;
while (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&ps4->read_pos),
ps4->ring) < sample_count)
{
int key;
if (--laps < 0 || !retro_atomic_load_acquire_int(&ps4->running))
return 0;
key = retro_eventcount_prepare_wait(&ps4->park);
if ( (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&ps4->read_pos),
ps4->ring) >= sample_count)
|| !retro_atomic_load_acquire_int(&ps4->running))
{
retro_eventcount_cancel_wait(&ps4->park);
continue;
}
if (!retro_eventcount_commit_wait_timeout(&ps4->park, key,
PS4_AUDIO_WAIT_US))
continue;
}
}
if ((write_pos + sample_count) > ps4->ring)
{
memcpy(ps4->buffer_u32 + write_pos, s,
(ps4->ring - write_pos) * sizeof(uint32_t));
memcpy(ps4->buffer_u32, (uint32_t*)s +
(ps4->ring - write_pos),
(write_pos + sample_count - ps4->ring) * sizeof(uint32_t));
}
else
memcpy(ps4->buffer_u32 + write_pos, s, len);
write_pos = (uint16_t)(write_pos + sample_count);
if (write_pos >= ps4->ring)
write_pos -= ps4->ring;
/* Release: the samples land before the index that publishes
* them. */
retro_atomic_store_release_int(&ps4->write_pos, write_pos);
return len;
}
static bool ps4_audio_alive(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (!ps4)
return false;
return retro_atomic_load_acquire_int(&ps4->running) != 0;
}
static bool ps4_audio_stop(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (ps4)
{
retro_atomic_store_release_int(&ps4->running, 0);
/* A writer parked on the ring must see the flag drop; the
* worker may already be gone and notify nothing further. */
retro_eventcount_notify(&ps4->park);
if (ps4->worker_thread)
{
sthread_join(ps4->worker_thread);
ps4->worker_thread = NULL;
}
}
return true;
}
static bool ps4_audio_start(void *data, bool is_shutdown)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (ps4 && !retro_atomic_load_acquire_int(&ps4->running))
{
if (!ps4->worker_thread)
{
retro_atomic_store_release_int(&ps4->running, 1);
if (!(ps4->worker_thread = sthread_create(ps4_audio_mainloop, ps4)))
{
retro_atomic_store_release_int(&ps4->running, 0);
return false;
}
}
}
return true;
}
static void ps4_audio_set_nonblock_state(void *data, bool toggle)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (ps4)
ps4->nonblock = toggle;
}
static size_t ps4_write_avail(void *data)
{
size_t _len;
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (!ps4 || !retro_atomic_load_acquire_int(&ps4->running))
return 0;
_len = RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&ps4->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&ps4->read_pos), ps4->ring);
return _len * sizeof(uint32_t);
}
/* Sleep on the condition the output thread signals after every block
* until the fifo has room for len, in the same units ps4_audio_write()
* compares against, capped at half the fifo so the wait always ends.
* Returns the free space as ps4_write_avail() reports it, or 0 when
* the output thread is not running. */
static size_t ps4_wait_writable(void *data, size_t len)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
size_t avail;
int laps = PS4_AUDIO_WAIT_LAPS;
/* len arrives in bytes; the ring is counted in uint32_t frames. */
size_t want = len / sizeof(uint32_t);
if (want > ps4->ring / 2)
want = ps4->ring / 2;
for (;;)
{
int key;
if (!retro_atomic_load_acquire_int(&ps4->running))
return 0;
avail = RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&ps4->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&ps4->read_pos), ps4->ring);
if (avail >= want)
break;
/* Bounded per wait and overall: a thread that has stopped
* consuming hands the pass back as no space coming from this
* call. The room is re-checked inside the window. */
if (--laps < 0)
return 0;
key = retro_eventcount_prepare_wait(&ps4->park);
if (RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&ps4->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&ps4->read_pos),
ps4->ring) >= want
|| !retro_atomic_load_acquire_int(&ps4->running))
{
retro_eventcount_cancel_wait(&ps4->park);
continue;
}
if (!retro_eventcount_commit_wait_timeout(&ps4->park, key,
PS4_AUDIO_WAIT_US))
continue;
}
return avail * sizeof(uint32_t);
}
/* sceAudioOut is opened in SCE_AUDIO_OUT_MODE_STEREO, which is 16-bit
* PCM; float output would need a different port mode. */
static bool ps4_audio_use_float(void *data) { return false; }
static size_t ps4_buffer_size(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (!ps4)
return 0;
/* In bytes: the ring, in uint32_t frames of int16 stereo. The
* window the device is playing is held inside it. */
return ps4->ring * sizeof(uint32_t);
}
/* Frames the device has taken since init: the output call returns
* when it has taken the one before, so the worker counts a period
* per completed call. */
static size_t ps4_frames_consumed(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (!ps4)
return 0;
return retro_atomic_load_acquire_size(&ps4->consumed);
}
/* Periods played as silence for want of audio. */
static size_t ps4_underruns(void *data)
{
ps4_audio_t* ps4 = (ps4_audio_t*)data;
if (!ps4)
return 0;
return retro_atomic_load_acquire_size(&ps4->underruns);
}
audio_driver_t audio_ps4 = {
ps4_audio_init,
ps4_audio_write,
ps4_audio_stop,
ps4_audio_start,
ps4_audio_alive,
ps4_audio_set_nonblock_state,
ps4_audio_free,
ps4_audio_use_float,
"orbis",
NULL,
NULL,
ps4_write_avail,
ps4_buffer_size,
NULL, /* write_raw */
ps4_wait_writable,
ps4_frames_consumed,
ps4_underruns
};
+523
View File
@@ -0,0 +1,523 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2014-2017 - Ali Bouhlel
*
* 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/>.
*/
#include <stdint.h>
#include <malloc.h>
#include <stdio.h>
#include <string.h>
#include <rthreads/rthreads.h>
#include <rthreads/retro_eventcount.h>
#include <retro_atomic.h>
#include <psp2/audioout.h>
#include "../audio_driver.h"
typedef struct psp2_audio
{
/* The ring, followed by one period of silence for the worker
* to output when the ring runs short. */
uint32_t* buffer_u32;
sthread_t *worker_thread;
/* The writer's park; the worker notifies after every period it
* frees. Gated: the steady state, writer ahead and never waiting,
* costs the worker nothing per period, where the old
* signal-every-iteration took cond_lock each time. */
retro_eventcount_t park;
/* For the sink rate estimate and the statistics overlay.
* Only the worker writes either. */
retro_atomic_size_t consumed;
retro_atomic_size_t underruns;
int port;
int rate;
/* Frames the ring holds. Set before the worker starts, and
* fixed for the life of the driver. */
unsigned ring;
/* The ring's index pair, the SPSC discipline by hand because the
* worker consumes in place - psp->buffer_u32 + read_pos goes straight
* into the output syscall, and a retro_spsc would add a bounce
* copy per period. Producer publishes write_pos with release
* after the samples land; consumer publishes read_pos with
* release after the syscall returns; each side reads the other's
* index with acquire and its own relaxed. fifo_lock guarded
* nothing else, and is gone. */
retro_atomic_int_t read_pos;
retro_atomic_int_t write_pos;
retro_atomic_int_t running;
bool nonblock;
} psp2_audio_t;
#define AUDIO_OUT_COUNT 512u
/* The ring is what the latency setting asks for, so it is a whole
* number of periods rather than a power of two and the wrap is a
* compare. The window the device is playing stays inside the ring
* as held until the output call after it returns (see the worker),
* so the writer's room is the ring less that window less one frame.
* Six periods is the floor: five of room, which holds a delivery of
* a video frame of audio - 1600 frames for 30 Hz content at 48 kHz -
* beside the window in flight without dipping under a period each
* refill. Anything at or above the default is unaffected. */
#define AUDIO_RING_MIN (AUDIO_OUT_COUNT * 6u)
#define AUDIO_RING_MAX (AUDIO_OUT_COUNT * 64u)
/* Frames held, and frames the writer may still place - one short of
* the ring, so a full ring and an empty one do not both read as
* write_pos == read_pos. */
#define RING_HELD(w, r, n) \
((unsigned)((w) >= (r) ? (w) - (r) : (n) - (r) + (w)))
#define RING_FREE(w, r, n) ((n) - 1u - RING_HELD((w), (r), (n)))
/* Bound on any wait for the audio thread to consume: one wait, and how
* many of them before the caller gets the pass back. The thread
* consumes a period every period while the device runs. */
#define PSP2_AUDIO_WAIT_US 100000
#define PSP2_AUDIO_WAIT_LAPS 8
/* The only rate the main port opens at. */
#define PSP2_AUDIO_RATE 48000
/* The ring the latency setting asks for: that many milliseconds at
* the rate the device opened at, in whole periods so no window the
* worker hands over crosses the ring's end. The window the device is
* playing is inside it, so this is the whole of the driver's
* buffering and what buffer_size() reports. */
static unsigned psp2_ring_frames(unsigned rate, unsigned latency)
{
unsigned frames = rate * latency / 1000u;
frames = ((frames + AUDIO_OUT_COUNT - 1u) / AUDIO_OUT_COUNT)
* AUDIO_OUT_COUNT;
if (frames < AUDIO_RING_MIN)
return AUDIO_RING_MIN;
if (frames > AUDIO_RING_MAX)
return AUDIO_RING_MAX;
return frames;
}
/* Return port used */
static int psp2_configure_audio(unsigned rate, unsigned *new_rate)
{
/* The main port refuses every other rate, which leaves the session
* with no audio at all; open at the one it takes and tell the
* frontend to resample to it. */
if (rate != PSP2_AUDIO_RATE)
*new_rate = PSP2_AUDIO_RATE;
return sceAudioOutOpenPort(
SCE_AUDIO_OUT_PORT_TYPE_MAIN, AUDIO_OUT_COUNT,
PSP2_AUDIO_RATE, SCE_AUDIO_OUT_MODE_STEREO);
}
static void psp2_audio_mainloop(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
/* The worker's own cursor: the next window to hand over. The
* published read_pos runs one window behind it while a ring
* window is in flight. The output call returns once the window
* handed to the previous call has been output, and with this
* call's window queued and still to be read by the hardware -
* the SDKs document passing NULL as the way to wait for that
* last window - so the window a call hands over is the device's
* until the call after it returns, and is released there. */
uint16_t play_pos = (uint16_t)
retro_atomic_load_relaxed_int(&psp->read_pos);
while (retro_atomic_load_acquire_int(&psp->running))
{
bool cond = false;
uint16_t next_pos = play_pos;
uint16_t write_pos = (uint16_t)
retro_atomic_load_acquire_int(&psp->write_pos);
/* A period in hand is a period to play. Holding one back
* as a reserve only hands the device silence while the
* ring has audio in it. */
cond = RING_HELD(write_pos, play_pos, psp->ring)
< AUDIO_OUT_COUNT;
if (!cond)
{
next_pos += AUDIO_OUT_COUNT;
if (next_pos >= psp->ring)
next_pos = 0;
}
else
retro_atomic_fetch_add_size(&psp->underruns, 1);
sceAudioOutOutput(psp->port,
psp->buffer_u32
+ (cond ? psp->ring : play_pos));
retro_atomic_fetch_add_size(&psp->consumed, AUDIO_OUT_COUNT);
/* The previous call's window has been output; this call's is
* the one in flight, so everything before play_pos is the
* writer's. When this call handed over silence, play_pos is
* where read_pos already stands. */
retro_atomic_store_release_int(&psp->read_pos, play_pos);
play_pos = next_pos;
retro_eventcount_notify(&psp->park);
}
return;
}
static void *psp2_audio_init(const char *device,
unsigned rate, unsigned latency,
unsigned *new_rate)
{
int port;
psp2_audio_t *psp = (psp2_audio_t*)calloc(1, sizeof(psp2_audio_t));
if (!psp)
return NULL;
if ((port = psp2_configure_audio(rate, new_rate)) < 0)
{
free(psp);
return NULL;
}
psp->rate = *new_rate ? *new_rate : rate;
psp->ring = psp2_ring_frames(psp->rate, latency);
/* Cache aligned, not necessary but helpful. */
/* The ring, and one period of silence past its end. */
psp->buffer_u32 = (uint32_t*)calloc(psp->ring + AUDIO_OUT_COUNT,
sizeof(uint32_t));
retro_atomic_size_init(&psp->consumed, 0);
retro_atomic_size_init(&psp->underruns, 0);
retro_atomic_int_init(&psp->read_pos, 0);
retro_atomic_int_init(&psp->write_pos, 0);
psp->port = port;
if ( !psp->buffer_u32
|| !retro_eventcount_init(&psp->park))
{
sceAudioOutReleasePort(port);
free(psp->buffer_u32);
free(psp);
return NULL;
}
psp->nonblock = false;
/* running has to be set before the worker starts, so a worker that
* never starts leaves a driver reporting itself alive with nothing
* consuming the ring: writes wait out their laps and return zero,
* and teardown acts on a state that was never true. */
retro_atomic_int_init(&psp->running, 1);
if (!(psp->worker_thread = sthread_create(psp2_audio_mainloop, psp)))
{
retro_eventcount_free(&psp->park);
sceAudioOutReleasePort(port);
free(psp->buffer_u32);
free(psp);
return NULL;
}
return psp;
}
static void psp2_audio_free(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (!psp)
return;
if (retro_atomic_load_acquire_int(&psp->running))
{
if (psp->worker_thread)
{
retro_atomic_store_release_int(&psp->running, 0);
sthread_join(psp->worker_thread);
}
}
retro_eventcount_free(&psp->park);
free(psp->buffer_u32);
psp->worker_thread = NULL;
sceAudioOutReleasePort(psp->port);
free(psp);
}
static ssize_t psp2_audio_write(void *data, const void *s, size_t len)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
uint16_t write_pos = (uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos);
/* Frames, and wide enough for them: narrowed to uint16_t, a write of
* more than 65535 frames wrapped to a small count that the room check
* then waved through, and the copy below took len bytes for it. The
* ring holds one frame short of its length, so no wait can free room
* for more than that at once - take what fits and report the short
* write rather than spending the laps on room that will not come. A
* len that is not whole frames carries the frames it has. */
size_t sample_count = len / sizeof(uint32_t);
if (!retro_atomic_load_acquire_int(&psp->running))
return -1;
if (!sample_count)
return 0;
if (sample_count > (size_t)(psp->ring - 1u))
sample_count = psp->ring - 1u;
len = sample_count * sizeof(uint32_t);
/* The ring is counted in uint32_t frames (write_pos, read_pos,
* ring); len is bytes. Both room checks below used to
* compare the frame count against len, i.e. demanded four times the
* room actually needed: non-blocking writes were refused - the audio
* dropped - with plenty of space free, and blocking ones waited for
* space that rate control was not trying to free. psp2_write_avail()
* and psp2_wait_writable() already convert; compare frames to
* frames here too. */
if (psp->nonblock)
{
if (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) < sample_count)
return 0;
}
{
/* The audio thread notifies every period it consumes. One that
* has stopped consuming - the device suspended, the thread gone
* - notifies nothing; each wait is bounded, the loop is
* lap-bounded, and the write then returns having written
* nothing rather than holding the caller. Room and liveness
* are re-checked inside the eventcount's window, so a period
* freed between the check and the park costs nothing. */
int laps = PSP2_AUDIO_WAIT_LAPS;
while (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) < sample_count)
{
int key;
if (--laps < 0 || !retro_atomic_load_acquire_int(&psp->running))
return 0;
key = retro_eventcount_prepare_wait(&psp->park);
if ( (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) >= sample_count)
|| !retro_atomic_load_acquire_int(&psp->running))
{
retro_eventcount_cancel_wait(&psp->park);
continue;
}
if (!retro_eventcount_commit_wait_timeout(&psp->park, key,
PSP2_AUDIO_WAIT_US))
continue;
}
}
if ((write_pos + sample_count) > psp->ring)
{
memcpy(psp->buffer_u32 + write_pos, s,
(psp->ring - write_pos) * sizeof(uint32_t));
memcpy(psp->buffer_u32, (uint32_t*)s +
(psp->ring - write_pos),
(write_pos + sample_count - psp->ring) * sizeof(uint32_t));
}
else
memcpy(psp->buffer_u32 + write_pos, s, len);
write_pos = (uint16_t)(write_pos + sample_count);
if (write_pos >= psp->ring)
write_pos -= psp->ring;
/* Release: the samples land before the index that publishes
* them. */
retro_atomic_store_release_int(&psp->write_pos, write_pos);
return len;
}
static bool psp2_audio_alive(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (!psp)
return false;
return retro_atomic_load_acquire_int(&psp->running) != 0;
}
static bool psp2_audio_stop(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (psp)
{
retro_atomic_store_release_int(&psp->running, 0);
/* A writer parked on the ring must see the flag drop; the
* worker may already be gone and notify nothing further. */
retro_eventcount_notify(&psp->park);
if (psp->worker_thread)
{
sthread_join(psp->worker_thread);
psp->worker_thread = NULL;
}
}
return true;
}
static bool psp2_audio_start(void *data, bool is_shutdown)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (psp && !retro_atomic_load_acquire_int(&psp->running))
{
if (!psp->worker_thread)
{
retro_atomic_store_release_int(&psp->running, 1);
if (!(psp->worker_thread = sthread_create(psp2_audio_mainloop, psp)))
{
retro_atomic_store_release_int(&psp->running, 0);
return false;
}
}
}
return true;
}
static void psp2_audio_set_nonblock_state(void *data, bool toggle)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (psp)
psp->nonblock = toggle;
}
static size_t psp2_write_avail(void *data)
{
size_t _len;
psp2_audio_t* psp = (psp2_audio_t*)data;
if (!psp || !retro_atomic_load_acquire_int(&psp->running))
return 0;
_len = RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos), psp->ring);
return _len * sizeof(uint32_t);
}
/* Sleep on the condition the output thread signals after every block
* until the fifo has room for len, in the same units psp2_audio_write()
* compares against, capped at half the fifo so the wait always ends.
* Returns the free space as psp2_write_avail() reports it, or 0 when
* the output thread is not running. */
static size_t psp2_wait_writable(void *data, size_t len)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
size_t avail;
int laps = PSP2_AUDIO_WAIT_LAPS;
/* len arrives in bytes; the ring is counted in uint32_t frames. */
size_t want = len / sizeof(uint32_t);
if (want > psp->ring / 2)
want = psp->ring / 2;
for (;;)
{
int key;
if (!retro_atomic_load_acquire_int(&psp->running))
return 0;
avail = RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos), psp->ring);
if (avail >= want)
break;
/* Bounded per wait and overall: a thread that has stopped
* consuming hands the pass back as no space coming from this
* call. The room is re-checked inside the window. */
if (--laps < 0)
return 0;
key = retro_eventcount_prepare_wait(&psp->park);
if (RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) >= want
|| !retro_atomic_load_acquire_int(&psp->running))
{
retro_eventcount_cancel_wait(&psp->park);
continue;
}
if (!retro_eventcount_commit_wait_timeout(&psp->park, key,
PSP2_AUDIO_WAIT_US))
continue;
}
return avail * sizeof(uint32_t);
}
/* sceAudioOut takes 16-bit PCM only; there is no float output on the
* Vita hardware or in the kernel API. */
static bool psp2_audio_use_float(void *data) { return false; }
static size_t psp2_buffer_size(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (!psp)
return 0;
/* In bytes: the ring, in uint32_t frames of int16 stereo. The
* window the device is playing is held inside it. */
return psp->ring * sizeof(uint32_t);
}
/* Frames the device has taken since init: the output call returns
* when it has taken the one before, so the worker counts a period
* per completed call. */
static size_t psp2_frames_consumed(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (!psp)
return 0;
return retro_atomic_load_acquire_size(&psp->consumed);
}
/* Periods played as silence for want of audio. */
static size_t psp2_underruns(void *data)
{
psp2_audio_t* psp = (psp2_audio_t*)data;
if (!psp)
return 0;
return retro_atomic_load_acquire_size(&psp->underruns);
}
audio_driver_t audio_psp2 = {
psp2_audio_init,
psp2_audio_write,
psp2_audio_stop,
psp2_audio_start,
psp2_audio_alive,
psp2_audio_set_nonblock_state,
psp2_audio_free,
psp2_audio_use_float,
"vita",
NULL,
NULL,
psp2_write_avail,
psp2_buffer_size,
NULL, /* write_raw */
psp2_wait_writable,
psp2_frames_consumed,
psp2_underruns
};
+204 -128
View File
@@ -16,9 +16,7 @@
*/
#include <stdint.h>
#if defined(VITA) || defined(PSP)
#include <malloc.h>
#endif
#include <stdio.h>
#include <string.h>
@@ -26,26 +24,16 @@
#include <rthreads/retro_eventcount.h>
#include <retro_atomic.h>
#if defined(VITA)
#include <psp2/kernel/processmgr.h>
#include <psp2/kernel/threadmgr.h>
#include <psp2/kernel/sysmem.h>
#include <psp2/audioout.h>
#elif defined(PSP)
#include <pspkernel.h>
#include <pspaudio.h>
#elif defined(ORBIS)
#include <libSceAudioOut.h>
#include <defines/ps4_defines.h>
#include <verbosity.h>
#endif
#include "../audio_driver.h"
typedef struct psp_audio
{
uint32_t* buffer;
uint32_t* zeroBuffer;
/* The ring, followed by one period of silence for the worker
* to output when the ring runs short. */
uint32_t* buffer_u32;
sthread_t *worker_thread;
/* The writer's park; the worker notifies after every period it
@@ -54,13 +42,20 @@ typedef struct psp_audio
* signal-every-iteration took cond_lock each time. */
retro_eventcount_t park;
SceUID thread;
/* For the sink rate estimate and the statistics overlay.
* Only the worker writes either. */
retro_atomic_size_t consumed;
retro_atomic_size_t underruns;
int port;
int rate;
/* Frames the ring holds. Set before the worker starts, and
* fixed for the life of the driver. */
unsigned ring;
/* The ring's index pair, the SPSC discipline by hand because the
* worker consumes in place - psp->buffer + read_pos goes straight
* worker consumes in place - psp->buffer_u32 + read_pos goes straight
* into the output syscall, and a retro_spsc would add a bounce
* copy per period. Producer publishes write_pos with release
* after the samples land; consumer publishes read_pos with
@@ -75,8 +70,25 @@ typedef struct psp_audio
} psp_audio_t;
#define AUDIO_OUT_COUNT 512u
#define AUDIO_BUFFER_SIZE (1u<<13u)
#define AUDIO_BUFFER_SIZE_MASK (AUDIO_BUFFER_SIZE-1)
/* The ring is what the latency setting asks for, so it is a whole
* number of periods rather than a power of two and the wrap is a
* compare. The window the device is playing stays inside the ring
* as held until the output call after it returns (see the worker),
* so the writer's room is the ring less that window less one frame.
* Six periods is the floor: five of room, which holds a delivery of
* a video frame of audio - 1600 frames for 30 Hz content at 48 kHz -
* beside the window in flight without dipping under a period each
* refill. Anything at or above the default is unaffected. */
#define AUDIO_RING_MIN (AUDIO_OUT_COUNT * 6u)
#define AUDIO_RING_MAX (AUDIO_OUT_COUNT * 64u)
/* Frames held, and frames the writer may still place - one short of
* the ring, so a full ring and an empty one do not both read as
* write_pos == read_pos. */
#define RING_HELD(w, r, n) \
((unsigned)((w) >= (r) ? (w) - (r) : (n) - (r) + (w)))
#define RING_FREE(w, r, n) ((n) - 1u - RING_HELD((w), (r), (n)))
/* Bound on any wait for the audio thread to consume: one wait, and how
* many of them before the caller gets the pass back. The thread
@@ -84,62 +96,92 @@ typedef struct psp_audio
#define PSP_AUDIO_WAIT_US 100000
#define PSP_AUDIO_WAIT_LAPS 8
/* Return port used */
static int psp_configure_audio(unsigned rate)
/* The rate the SRC always takes, for a setting it will not. */
#define PSP_AUDIO_RATE_FALLBACK 48000
/* The ring the latency setting asks for: that many milliseconds at
* the rate the device opened at, in whole periods so no window the
* worker hands over crosses the ring's end. The window the device is
* playing is inside it, so this is the whole of the driver's
* buffering and what buffer_size() reports. */
static unsigned psp_ring_frames(unsigned rate, unsigned latency)
{
#if defined(VITA)
return sceAudioOutOpenPort(
SCE_AUDIO_OUT_PORT_TYPE_MAIN, AUDIO_OUT_COUNT,
rate, SCE_AUDIO_OUT_MODE_STEREO);
#elif defined(ORBIS)
return sceAudioOutOpen(0xff,
SCE_AUDIO_OUT_PORT_TYPE_MAIN, 0, AUDIO_OUT_COUNT,
rate, SCE_AUDIO_OUT_MODE_STEREO);
#else
return sceAudioSRCChReserve(AUDIO_OUT_COUNT, rate, 2);
#endif
unsigned frames = rate * latency / 1000u;
frames = ((frames + AUDIO_OUT_COUNT - 1u) / AUDIO_OUT_COUNT)
* AUDIO_OUT_COUNT;
if (frames < AUDIO_RING_MIN)
return AUDIO_RING_MIN;
if (frames > AUDIO_RING_MAX)
return AUDIO_RING_MAX;
return frames;
}
/* Return port used */
static int psp_configure_audio(unsigned rate, unsigned *new_rate)
{
int port = sceAudioSRCChReserve(AUDIO_OUT_COUNT, rate, 2);
/* The SRC takes a fixed set of rates and refuses the rest outright,
* which leaves the session with no audio at all. Open at a rate it
* takes and tell the frontend to resample to that instead. */
if (port < 0)
{
if ((port = sceAudioSRCChReserve(AUDIO_OUT_COUNT,
PSP_AUDIO_RATE_FALLBACK, 2)) >= 0)
*new_rate = PSP_AUDIO_RATE_FALLBACK;
}
return port;
}
static void psp_audio_mainloop(void *data)
{
psp_audio_t* psp = (psp_audio_t*)data;
/* The worker's own cursor: the next window to hand over. The
* published read_pos runs one window behind it while a ring
* window is in flight. The output call returns once the window
* handed to the previous call has been output, and with this
* call's window queued and still to be read by the hardware -
* the SDKs document passing NULL as the way to wait for that
* last window - so the window a call hands over is the device's
* until the call after it returns, and is released there. */
uint16_t play_pos = (uint16_t)
retro_atomic_load_relaxed_int(&psp->read_pos);
while (retro_atomic_load_acquire_int(&psp->running))
{
bool cond = false;
uint16_t read_pos = (uint16_t)
retro_atomic_load_relaxed_int(&psp->read_pos);
#if defined(VITA) || defined(ORBIS)
uint16_t read_pos_2 = read_pos;
#endif
uint16_t next_pos = play_pos;
uint16_t write_pos = (uint16_t)
retro_atomic_load_acquire_int(&psp->write_pos);
cond = ((uint16_t)(write_pos - read_pos) & AUDIO_BUFFER_SIZE_MASK)
< (AUDIO_OUT_COUNT * 2);
/* A period in hand is a period to play. Holding one back
* as a reserve only hands the device silence while the
* ring has audio in it. */
cond = RING_HELD(write_pos, play_pos, psp->ring)
< AUDIO_OUT_COUNT;
if (!cond)
{
read_pos += AUDIO_OUT_COUNT;
read_pos &= AUDIO_BUFFER_SIZE_MASK;
/* Release: the period is free for the writer only after
* this store; the syscall below reads the old window, which
* the writer cannot touch until it sees the new index. */
retro_atomic_store_release_int(&psp->read_pos, read_pos);
next_pos += AUDIO_OUT_COUNT;
if (next_pos >= psp->ring)
next_pos = 0;
}
else
retro_atomic_fetch_add_size(&psp->underruns, 1);
sceAudioSRCOutputBlocking(PSP_AUDIO_VOLUME_MAX,
psp->buffer_u32
+ (cond ? psp->ring : play_pos));
retro_atomic_fetch_add_size(&psp->consumed, AUDIO_OUT_COUNT);
/* The previous call's window has been output; this call's is
* the one in flight, so everything before play_pos is the
* writer's. When this call handed over silence, play_pos is
* where read_pos already stands. */
retro_atomic_store_release_int(&psp->read_pos, play_pos);
play_pos = next_pos;
retro_eventcount_notify(&psp->park);
#if defined(VITA) || defined(ORBIS)
sceAudioOutOutput(psp->port,
cond ? (psp->zeroBuffer)
: (psp->buffer + read_pos_2));
#else
sceAudioSRCOutputBlocking(PSP_AUDIO_VOLUME_MAX,
cond
? (psp->zeroBuffer)
: (psp->buffer + read_pos));
#endif
}
return;
@@ -155,37 +197,49 @@ static void *psp_audio_init(const char *device,
if (!psp)
return NULL;
if ((port = psp_configure_audio(rate)) < 0)
if ((port = psp_configure_audio(rate, new_rate)) < 0)
{
free(psp);
return NULL;
}
#if defined(ORBIS)
sceAudioOutInit();
#endif
psp->rate = *new_rate ? *new_rate : rate;
psp->ring = psp_ring_frames(psp->rate, latency);
/* Cache aligned, not necessary but helpful. */
psp->buffer = (uint32_t*)malloc(AUDIO_BUFFER_SIZE * sizeof(uint32_t));
memset(psp->buffer, 0, AUDIO_BUFFER_SIZE * sizeof(uint32_t));
psp->zeroBuffer = (uint32_t*)malloc(AUDIO_OUT_COUNT * sizeof(uint32_t));
memset(psp->zeroBuffer, 0, AUDIO_OUT_COUNT * sizeof(uint32_t));
/* The ring, and one period of silence past its end. */
psp->buffer_u32 = (uint32_t*)calloc(psp->ring + AUDIO_OUT_COUNT,
sizeof(uint32_t));
retro_atomic_size_init(&psp->consumed, 0);
retro_atomic_size_init(&psp->underruns, 0);
retro_atomic_int_init(&psp->read_pos, 0);
retro_atomic_int_init(&psp->write_pos, 0);
psp->port = port;
if (!retro_eventcount_init(&psp->park))
if ( !psp->buffer_u32
|| !retro_eventcount_init(&psp->park))
{
free(psp->buffer);
free(psp->zeroBuffer);
sceAudioSRCChRelease();
free(psp->buffer_u32);
free(psp);
return NULL;
}
psp->nonblock = false;
/* running has to be set before the worker starts, so a worker that
* never starts leaves a driver reporting itself alive with nothing
* consuming the ring: writes wait out their laps and return zero,
* and teardown acts on a state that was never true. */
retro_atomic_int_init(&psp->running, 1);
psp->worker_thread = sthread_create(psp_audio_mainloop, psp);
if (!(psp->worker_thread = sthread_create(psp_audio_mainloop, psp)))
{
retro_eventcount_free(&psp->park);
sceAudioSRCChRelease();
free(psp->buffer_u32);
free(psp);
return NULL;
}
return psp;
}
@@ -205,17 +259,10 @@ static void psp_audio_free(void *data)
}
}
retro_eventcount_free(&psp->park);
free(psp->buffer);
free(psp->buffer_u32);
psp->worker_thread = NULL;
free(psp->zeroBuffer);
#if defined(VITA)
sceAudioOutReleasePort(psp->port);
#elif defined(ORBIS)
sceAudioOutClose(psp->port);
#else
sceAudioSRCChRelease();
#endif
sceAudioSRCChRelease();
free(psp);
@@ -226,13 +273,25 @@ static ssize_t psp_audio_write(void *data, const void *s, size_t len)
psp_audio_t* psp = (psp_audio_t*)data;
uint16_t write_pos = (uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos);
uint16_t sample_count = len / sizeof(uint32_t);
/* Frames, and wide enough for them: narrowed to uint16_t, a write of
* more than 65535 frames wrapped to a small count that the room check
* then waved through, and the copy below took len bytes for it. The
* ring holds one frame short of its length, so no wait can free room
* for more than that at once - take what fits and report the short
* write rather than spending the laps on room that will not come. A
* len that is not whole frames carries the frames it has. */
size_t sample_count = len / sizeof(uint32_t);
if (!retro_atomic_load_acquire_int(&psp->running))
return -1;
if (!sample_count)
return 0;
if (sample_count > (size_t)(psp->ring - 1u))
sample_count = psp->ring - 1u;
len = sample_count * sizeof(uint32_t);
/* The ring is counted in uint32_t frames (write_pos, read_pos,
* AUDIO_BUFFER_SIZE); len is bytes. Both room checks below used to
* ring); len is bytes. Both room checks below used to
* compare the frame count against len, i.e. demanded four times the
* room actually needed: non-blocking writes were refused - the audio
* dropped - with plenty of space free, and blocking ones waited for
@@ -241,9 +300,9 @@ static ssize_t psp_audio_write(void *data, const void *s, size_t len)
* frames here too. */
if (psp->nonblock)
{
if (AUDIO_BUFFER_SIZE - ((uint16_t)(write_pos - (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos))
& AUDIO_BUFFER_SIZE_MASK) < sample_count)
if (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) < sample_count)
return 0;
}
@@ -256,17 +315,17 @@ static ssize_t psp_audio_write(void *data, const void *s, size_t len)
* are re-checked inside the eventcount's window, so a period
* freed between the check and the park costs nothing. */
int laps = PSP_AUDIO_WAIT_LAPS;
while (AUDIO_BUFFER_SIZE - ((uint16_t)(write_pos - (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos))
& AUDIO_BUFFER_SIZE_MASK) < sample_count)
while (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) < sample_count)
{
int key;
if (--laps < 0 || !retro_atomic_load_acquire_int(&psp->running))
return 0;
key = retro_eventcount_prepare_wait(&psp->park);
if ( (AUDIO_BUFFER_SIZE - ((uint16_t)(write_pos - (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos))
& AUDIO_BUFFER_SIZE_MASK) >= sample_count)
if ( (RING_FREE(write_pos, (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) >= sample_count)
|| !retro_atomic_load_acquire_int(&psp->running))
{
retro_eventcount_cancel_wait(&psp->park);
@@ -278,19 +337,20 @@ static ssize_t psp_audio_write(void *data, const void *s, size_t len)
}
}
if ((write_pos + sample_count) > AUDIO_BUFFER_SIZE)
if ((write_pos + sample_count) > psp->ring)
{
memcpy(psp->buffer + write_pos, s,
(AUDIO_BUFFER_SIZE - write_pos) * sizeof(uint32_t));
memcpy(psp->buffer, (uint32_t*)s +
(AUDIO_BUFFER_SIZE - write_pos),
(write_pos + sample_count - AUDIO_BUFFER_SIZE) * sizeof(uint32_t));
memcpy(psp->buffer_u32 + write_pos, s,
(psp->ring - write_pos) * sizeof(uint32_t));
memcpy(psp->buffer_u32, (uint32_t*)s +
(psp->ring - write_pos),
(write_pos + sample_count - psp->ring) * sizeof(uint32_t));
}
else
memcpy(psp->buffer + write_pos, s, len);
memcpy(psp->buffer_u32 + write_pos, s, len);
write_pos += sample_count;
write_pos &= AUDIO_BUFFER_SIZE_MASK;
write_pos = (uint16_t)(write_pos + sample_count);
if (write_pos >= psp->ring)
write_pos -= psp->ring;
/* Release: the samples land before the index that publishes
* them. */
retro_atomic_store_release_int(&psp->write_pos, write_pos);
@@ -309,9 +369,6 @@ static bool psp_audio_stop(void *data)
{
psp_audio_t* psp = (psp_audio_t*)data;
#if defined(ORBIS)
return false;
#else
if (psp)
{
retro_atomic_store_release_int(&psp->running, 0);
@@ -326,7 +383,6 @@ static bool psp_audio_stop(void *data)
}
}
return true;
#endif
}
static bool psp_audio_start(void *data, bool is_shutdown)
@@ -338,7 +394,11 @@ static bool psp_audio_start(void *data, bool is_shutdown)
if (!psp->worker_thread)
{
retro_atomic_store_release_int(&psp->running, 1);
psp->worker_thread = sthread_create(psp_audio_mainloop, psp);
if (!(psp->worker_thread = sthread_create(psp_audio_mainloop, psp)))
{
retro_atomic_store_release_int(&psp->running, 0);
return false;
}
}
}
@@ -359,10 +419,9 @@ static size_t psp_write_avail(void *data)
if (!psp || !retro_atomic_load_acquire_int(&psp->running))
return 0;
_len = AUDIO_BUFFER_SIZE - ((uint16_t)((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos) - (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos))
& AUDIO_BUFFER_SIZE_MASK);
_len = RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos), psp->ring);
return _len * sizeof(uint32_t);
}
@@ -379,18 +438,17 @@ static size_t psp_wait_writable(void *data, size_t len)
/* len arrives in bytes; the ring is counted in uint32_t frames. */
size_t want = len / sizeof(uint32_t);
if (want > AUDIO_BUFFER_SIZE / 2)
want = AUDIO_BUFFER_SIZE / 2;
if (want > psp->ring / 2)
want = psp->ring / 2;
for (;;)
{
int key;
if (!retro_atomic_load_acquire_int(&psp->running))
return 0;
avail = AUDIO_BUFFER_SIZE - ((uint16_t)((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos) - (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos))
& AUDIO_BUFFER_SIZE_MASK);
avail = RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos), psp->ring);
if (avail >= want)
break;
/* Bounded per wait and overall: a thread that has stopped
@@ -399,10 +457,10 @@ static size_t psp_wait_writable(void *data, size_t len)
if (--laps < 0)
return 0;
key = retro_eventcount_prepare_wait(&psp->park);
if ((AUDIO_BUFFER_SIZE - ((uint16_t)((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos) - (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos))
& AUDIO_BUFFER_SIZE_MASK)) >= want
if (RING_FREE((uint16_t)
retro_atomic_load_relaxed_int(&psp->write_pos), (uint16_t)
retro_atomic_load_acquire_int(&psp->read_pos),
psp->ring) >= want
|| !retro_atomic_load_acquire_int(&psp->running))
{
retro_eventcount_cancel_wait(&psp->park);
@@ -420,10 +478,32 @@ static size_t psp_wait_writable(void *data, size_t len)
static bool psp_audio_use_float(void *data) { return false; }
static size_t psp_buffer_size(void *data)
{
/* In bytes: the ring holds AUDIO_BUFFER_SIZE uint32_t frames of int16
* stereo. The comment beside this had the multiplication and left it
* out. */
return AUDIO_BUFFER_SIZE * sizeof(uint32_t);
psp_audio_t* psp = (psp_audio_t*)data;
if (!psp)
return 0;
/* In bytes: the ring, in uint32_t frames of int16 stereo. The
* window the device is playing is held inside it. */
return psp->ring * sizeof(uint32_t);
}
/* Frames the device has taken since init: the output call returns
* when it has taken the one before, so the worker counts a period
* per completed call. */
static size_t psp_frames_consumed(void *data)
{
psp_audio_t* psp = (psp_audio_t*)data;
if (!psp)
return 0;
return retro_atomic_load_acquire_size(&psp->consumed);
}
/* Periods played as silence for want of audio. */
static size_t psp_underruns(void *data)
{
psp_audio_t* psp = (psp_audio_t*)data;
if (!psp)
return 0;
return retro_atomic_load_acquire_size(&psp->underruns);
}
audio_driver_t audio_psp = {
@@ -435,17 +515,13 @@ audio_driver_t audio_psp = {
psp_audio_set_nonblock_state,
psp_audio_free,
psp_audio_use_float,
#if defined(VITA)
"vita",
#elif defined(ORBIS)
"orbis",
#else
"psp",
#endif
NULL,
NULL,
psp_write_avail,
psp_buffer_size,
NULL, /* write_raw */
psp_wait_writable
psp_wait_writable,
psp_frames_consumed,
psp_underruns
};
+111 -8
View File
@@ -63,6 +63,24 @@ typedef struct
* lowers it at once. */
retro_atomic_size_t writable_cached;
retro_atomic_size_t sink_frames_cached;
/* Times the server ran out of audio for this stream, from its own
* underflow callback. One atomic add there, read by the frontend. */
retro_atomic_size_t underruns;
/* The sink's clock against the rate the stream was opened at,
* fitted from the timing info the server already hands over:
* read_index is what the sink has played, in bytes, and timestamp
* is when that was true. A fit over every sample rather than two
* points, because noise on a single anchor divides by the window
* and reads as drift.
*
* Accumulated in the latency-update callback, on the mainloop's
* thread, and published as one int in ppm. Nothing acts on it. */
unsigned frame_bytes;
uint64_t clk_anchor_pos;
int64_t clk_anchor_us;
int clk_have_anchor;
double clk_sx, clk_sy, clk_sxx, clk_sxy, clk_n;
retro_atomic_int_t clk_ppm; /* AUDIO_CLOCK_PPM_NONE until known */
} pa_t;
/* A note for the eventcount census: this driver stays off it, on
@@ -218,6 +236,64 @@ static void pulse_stream_request_cb(pa_stream *s, size_t len, void *data)
pa_threaded_mainloop_signal(pa->mainloop, 0);
}
/* One (position, time) pair from the server's timing info into the
* fit. read_index is the sink's play position in bytes; it can step
* back on a rewind or a flush, which restarts the fit rather than
* reading as an enormous negative drift. */
static void pulse_clock_sample(pa_t *pa, const pa_timing_info *ti)
{
uint64_t pos;
int64_t us;
double x, y, denom;
if (!pa->frame_bytes || ti->read_index < 0)
return;
pos = (uint64_t)ti->read_index / pa->frame_bytes;
us = (int64_t)ti->timestamp.tv_sec * 1000000
+ (int64_t)ti->timestamp.tv_usec;
if (!pa->clk_have_anchor || pos < pa->clk_anchor_pos
|| us <= pa->clk_anchor_us)
{
pa->clk_anchor_pos = pos;
pa->clk_anchor_us = us;
pa->clk_have_anchor = 1;
pa->clk_sx = pa->clk_sy = pa->clk_sxx = pa->clk_sxy = pa->clk_n = 0.0;
retro_atomic_store_release_int(&pa->clk_ppm, AUDIO_CLOCK_PPM_NONE);
return;
}
/* Seconds and frames from the anchor: a fit on the raw values
* loses its answer to cancellation. */
x = (double)(us - pa->clk_anchor_us) / 1000000.0;
y = (double)(pos - pa->clk_anchor_pos);
pa->clk_sx += x;
pa->clk_sy += y;
pa->clk_sxx += x * x;
pa->clk_sxy += x * y;
pa->clk_n += 1.0;
/* A second of window at least, as the interface says. */
if (pa->clk_n < 4.0 || x < 1.0)
return;
denom = pa->clk_n * pa->clk_sxx - pa->clk_sx * pa->clk_sx;
if (denom <= 0.0)
return;
{
double slope = (pa->clk_n * pa->clk_sxy - pa->clk_sx * pa->clk_sy)
/ denom;
double ppm = (slope / (double)pa->rate - 1.0) * 1000000.0;
if (ppm > -100000.0 && ppm < 100000.0)
{
retro_atomic_store_release_int(&pa->clk_ppm, (int)ppm);
}
}
}
static void pulse_stream_latency_update_cb(pa_stream *s, void *data)
{
pa_t *pa = (pa_t*)data;
@@ -226,22 +302,25 @@ static void pulse_stream_latency_update_cb(pa_stream *s, void *data)
* the server's queue; NULL until timing data has arrived. */
const pa_timing_info *ti = pa_stream_get_timing_info(s);
if (ti && pa->rate)
{
retro_atomic_store_release_size(&pa->sink_frames_cached,
(size_t)((uint64_t)ti->sink_usec * pa->rate / 1000000));
pulse_clock_sample(pa, ti);
}
pa_threaded_mainloop_signal(pa->mainloop, 0);
}
/* The server telling us this stream ran dry. Counted, not logged: it
* arrives on the mainloop's thread while audio is playing, which is
* the one place a log line costs what it is reporting on. */
static void pulse_underrun_update_cb(pa_stream *s, void *data)
{
#if 0
pa_t *pa = (pa_t*)data;
(void)s;
RARCH_LOG("[PulseAudio] Underrun (Buffer: %u, Writable size: %u).\n",
(unsigned)pa->buffer_size,
(unsigned)pa_stream_writable_size(pa->stream));
#endif
if (pa)
retro_atomic_fetch_add_size(&pa->underruns, 1);
}
static void pulse_buffer_attr_cb(pa_stream *s, void *data)
@@ -299,6 +378,8 @@ static void *pulse_init(const char *device, unsigned rate,
return NULL;
retro_atomic_size_init(&pa->writable_cached, 0);
retro_atomic_size_init(&pa->sink_frames_cached, 0);
retro_atomic_size_init(&pa->underruns, 0);
retro_atomic_int_init(&pa->clk_ppm, AUDIO_CLOCK_PPM_NONE);
memset(&spec, 0, sizeof(spec));
@@ -351,7 +432,8 @@ static void *pulse_init(const char *device, unsigned rate,
spec.channels = (uint8_t)audio_layout_channels(pa->layout);
spec.rate = rate;
pulse_channel_map(pa->layout, &map);
pa->rate = rate;
pa->rate = rate;
pa->frame_bytes = (unsigned)pa_frame_size(&spec);
pa->stream = pa_stream_new(pa->context, "audio", &spec, &map);
if (!pa->stream)
@@ -778,6 +860,25 @@ static void pulse_device_list_free(void *data, void *array_list_data)
string_list_free(s);
}
static size_t pulse_underruns(void *data)
{
pa_t *pa = (pa_t*)data;
return pa ? retro_atomic_load_acquire_size(&pa->underruns) : 0;
}
static bool pulse_device_clock_ppm(void *data, double *ppm)
{
pa_t *pa = (pa_t*)data;
int v;
if (!pa)
return false;
v = retro_atomic_load_acquire_int(&pa->clk_ppm);
if (v == AUDIO_CLOCK_PPM_NONE)
return false;
*ppm = (double)v;
return true;
}
audio_driver_t audio_pulse = {
pulse_init,
pulse_write,
@@ -795,6 +896,8 @@ audio_driver_t audio_pulse = {
NULL, /* write_raw */
pulse_wait_writable,
pulse_frames_consumed,
NULL, /* underruns */
pulse_layout
pulse_underruns,
pulse_layout,
NULL, /* frames_consumed_fallback */
pulse_device_clock_ppm
};
+4 -2
View File
@@ -143,7 +143,6 @@ static void ra_free(void *data)
* float PCM codec in the versions this driver has been built against;
* revisit only if one appears. */
static bool ra_use_float(void *data) { return false; }
static size_t ra_write_avail(void *data) { return 0; }
audio_driver_t audio_roar = {
ra_init,
@@ -157,7 +156,10 @@ audio_driver_t audio_roar = {
"roar",
NULL,
NULL,
ra_write_avail,
/* write_avail - the VS API reports neither the room in the stream
* nor what the server still holds. NULL disables rate control;
* a constant would instead feed it a fill that never changes. */
NULL,
NULL, /* buffer_size */
NULL /* write_raw */
};
+21 -4
View File
@@ -24,6 +24,7 @@
#include <retro_atomic.h>
#include "../audio_driver.h"
#include "../../verbosity.h"
#include "rsound.h"
typedef struct rsd
@@ -32,9 +33,9 @@ typedef struct rsd
/* Single producer (the core thread in rs_write), single consumer
* (librsound's worker in rsound_audio_cb): a lock-free retro_spsc
* ring, so neither side takes librsound's callback lock to touch
* it. retro_spsc rounds capacity up to a power of two; fifo_size
* is the size asked for and the producer never fills past it. */
* ring, so the handoff between them needs nothing of librsound's.
* retro_spsc rounds capacity up to a power of two; fifo_size is the
* size asked for and the producer never fills past it. */
retro_spsc_t ring;
bool ring_init;
/* Only the bounded waits a full ring puts the writer into; the ring
@@ -65,6 +66,10 @@ typedef struct rsd
* volatile orders nothing between threads and the re-check below
* has to see the store that came before the notify. */
retro_atomic_int_t has_error;
/* Callbacks librsound asked to fill and the ring came up short
* for; librsound zero-fills the shortfall, so what goes out is
* silence the device played for want of audio. */
retro_atomic_size_t underruns;
} rsd_t;
/* Room in the ring against the size asked for: the physical room less
@@ -83,6 +88,9 @@ static ssize_t rsound_audio_cb(void *data, size_t bytes, void *userdata)
size_t write_size = retro_spsc_read(&rsd->ring, data, bytes);
retro_eventcount_notify(&rsd->park);
if (write_size < bytes)
retro_atomic_fetch_add_size(&rsd->underruns, 1);
return write_size;
}
@@ -109,6 +117,7 @@ static void *rs_init(const char *device, unsigned rate, unsigned latency,
goto error;
retro_atomic_int_init(&rsd->has_error, 0);
retro_atomic_size_init(&rsd->underruns, 0);
/* Checked: on a backend that parks through a condition variable the
* init allocates, and a failure leaves an object whose commit_wait
* would hand scond_wait a NULL cond. */
@@ -365,6 +374,12 @@ static size_t rs_wait_writable(void *data, size_t len)
}
static bool rs_use_float(void *data) { return false; }
static size_t rs_underruns(void *data)
{
rsd_t *rsd = (rsd_t*)data;
return rsd ? retro_atomic_load_acquire_size(&rsd->underruns) : 0;
}
audio_driver_t audio_rsound = {
rs_init,
rs_write,
@@ -380,5 +395,7 @@ audio_driver_t audio_rsound = {
rs_write_avail,
rs_buffer_size,
NULL, /* write_raw */
rs_wait_writable
rs_wait_writable,
NULL, /* frames_consumed */
rs_underruns
};
Loaded 100 of 2112 files, more files were not shown because too many files have changed in this diff. Show more