When no screen on the stack shows anything on an output (we're in the
menus), the background screen draws "Menus are on your device" there.
UI drawn to an output gets the primary's DPI scale, over the whole output.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The ScreenManager keeps a list of secondary outputs, each with its own
swapchain, recreated after a lost device. Platform code adds, resizes and
removes them from any thread through Native_AddOutput() and friends, which
take effect at the start of the next frame.
Each frame, every output is drawn by the topmost screen that claims it
(Screen::renderOutput). EmuScreen claims Game outputs: it presents the
game there, laid out for that output, and leaves it out of the backbuffer,
where the touch controls and the menus stay. Not in non-buffered mode,
which draws straight into the backbuffer.
--game-window opens such an output on Mac, with Metal. It's a smoke test
for multiple swapchains, and a stand-in for an AirPlay display.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The backbuffer is now a Swapchain, from GetPrimarySwapchain(), and
callers bind, measure and read it back through that instead of nullptr.
nullptr still means it too. Backends with one swapchain map the primary
to their old backbuffer path.
On Metal, CreateSwapchain() adds more, each with its own layer, depth
buffer and drawable. Passes merge per swapchain, and Present() presents
every drawable acquired that frame. The backbuffer is just the first
of them. Nothing creates a second one yet.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Set by ViewGroup when a view is added, inserted or swapped in. Lets a view
find the scroll view it's in, for example.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The saved tab was selected before the Search tab was added, so the index
was out of range and no tab was created. Select it after adding Search, and
create the current tab anyway if the saved one no longer exists (Search is
hidden on small screens).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MeasureString was slow mostly because of wrapping: DrawStringRect and
MeasureStringRect re-ran the word wrapper every frame, and it measures
every prefix of every word. Wrapped strings are now cached. The caches
are unordered_maps with an xxhash of the key, and lookups reuse a key
buffer so a hit doesn't allocate. UIContext also skips switching fonts
when measuring in the current style. In a Mac debug build, 200 cached
measures go from 0.26 to 0.06 ms and 10 wrapped paragraphs from 2.1 to
0.01 ms per frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The volume key filter in NativeKey left unmapped volume keys to the OS, so
they never reached the dialog and couldn't be mapped. A new InputMode::AllKeys
lets a screen ask for them; the dialog consumes them so the volume doesn't
change while mapping.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NativeKey calls KeyEventToFocusMoves directly on the input thread, while
ProcessHeldKeys iterates heldKeys on the UI thread, which tripped an
iterator assert in debug builds. ProcessHeldKeys also dispatched key
events mid-iteration, where a handler calling EnableFocusMovement(false)
would clear the set under it. Now it collects due repeats under the lock
and dispatches them after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no type-to-search active, Backspace goes to the parent directory. Never
on a repeat, so holding it doesn't climb to the root. Not every platform
flags repeats, so the browser also tracks whether it's held.
Windows raw input now flags auto-repeated key downs as IS_REPEAT, like
Android and UWP, and forgets the held keys it releases on losing focus. The
UI drops OS repeats of a key it's already repeating itself, so held arrows
don't double up (they were dropped before too, a step later).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Refresh already focused a game it was asked to. Now it does the same for
directories, and keeps the request until a listing that was still loading
has arrived. UI::ScrollFocusedViewIntoView scrolls the focused view into
sight once it's been laid out, since SetFocus alone doesn't scroll.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Windows, a key released while Alt is held comes as WM_SYSKEYUP, which
raw input dropped, so after Alt+Up the UI kept the arrow held and the focus
kept moving. Handle it like WM_KEYUP. Alt+arrows also no longer move the
focus, since they're shortcuts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ScrollView dropped every mouse event without the left button, so views inside
one never saw hovers or right clicks. This kept nested scroll views from
tracking hover for the mouse wheel, and GameButton's right-click info only
worked once a left click had set its hover flag.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixes#17608
Text fields took Ctrl or Cmd for copy/paste/undo; now it's Cmd on Apple
platforms and Ctrl elsewhere, as is Ctrl+F (Cmd+F) for game search. Word
delete in the search filter is Option+Backspace on Apple.
The ImGui debugger never got Super, so with ImGui's Mac Cmd/Ctrl swap,
its paste and other Ctrl shortcuts could not be reached at all on Mac.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tvOS only does that with a Back press it gets to see, so the view controller
lets controller events through to UIKit while the top screen is top level
(the new TOP_LEVEL_CHANGED notification), and passes presses that aren't
keyboard keys on to super. Elsewhere Back stays ours.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the center-to-center scoring, which skipped a wide view between narrow
ones, since its center is far away even when its edge is right there. Now it
works like Android's FocusFinder: only views in the direction of the move are
candidates, ones in the origin's beam beat ones off to the side (going up or
down, unless those are in a nearer row), and otherwise the nearest wins, with
distance along the move weighted 13 times distance across it. Unlike Android,
the distance across is to the candidate's nearest edge rather than its center.
First/last and page moves keep their scoring.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It only replaced the top screen, but most callers switch from a dialog:
Play on the game screen, the store, the installers, the framedump
launcher. The screens under it stayed alive, so launching a game from the
game screen and exiting it left [MainScreen, MainScreen], one more each
time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
That set dialogFinished_ to the caller, which processFinishDialog() then
deleted while it was still in the stack, leaking the popup. Take the
pointer out first and loop. Nothing does this now (MemStickScreen and
DevMenuScreen work around it), but it's easy to do by accident.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ignoreInput_ doesn't stop a tap outside the box or Enter on the default
button, which both call TriggerFinish() again during the lead-out, firing
OnCompleted and the choice's OnChange a second time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetRequesterToken() only returns the views token inside CreateViews(),
but tabbed screens create every tab other than the first on demand, and
the persist restore after CreateViews() can create one too. So text
inputs and file choosers there (Networking tab nickname, search filter,
RemoteISO and achievement settings) registered their [this] callbacks
under the screen token, which a recreate doesn't forget. Open the native
input box, rotate, press OK, and the callback wrote through the deleted
view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Popups are transparent, so the screen under one still renders, and
render() ran DoRecreateViews(). A RecreateViews() on that screen (a
resize or rotation on DisplayLayoutScreen or TouchControlLayoutScreen,
MainScreen on RECENT_FILES_CHANGED from the recent-files thread, the
achievements login finishing) then deleted the choice that opened the
popup. Closing the popup wrote through it and called its handlers.
Leave the old views in place until the screen is back on top, where
update() recreates them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeIsAtTopLevel() made a virtual call on the top screen from the Java
UI thread, while the render thread could be deleting it. Two quick Back
presses could hit that. Cache the flag in ScreenManager::Update()
instead, like passInputToMapper_ (now atomic too).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WipeRequesterToken() only reset the screen's token, after CreateViews() had
already handed it to the new views, so nothing was ever forgotten - not even
in ~Screen(), which saw a token of -1. A text input dialog open across a
RecreateViews() (on the RetroAchievements screen, a slow login failing) would
then write through a deleted PopupTextInputChoice, possibly into the field
that took its place: #21708.
Requests made while creating views now get their own token, forgotten
before the views are deleted. Screen-level requests keep the screen's token.
RequestManager: ForgetRequestsWithToken() now locks, and also drops
responses that arrived but weren't processed yet. ProcessRequests() calls
the callbacks one at a time without holding the lock, so a callback that
closes a screen drops the rest of that screen's responses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Achievement badges and game icons don't change, so re-downloading them
daily was wasted. InsertIcon now takes a max age, and each entry saves
its expiry time. Leaderboard avatars get a day, everything else 30 days.
Bumps the cache version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Icons were kept in icon.cache indefinitely, so one that changes on the
server (a RetroAchievements user avatar, keyed by username) never
refreshed. The insertion timestamp was process-relative, so it couldn't
be compared across sessions. Store wall-clock time, and skip entries
older than a day when loading. Bumps the cache version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GetDimensions only answered once a texture had been created, so the first
MeasureNotice for a new icon (and after the texture was released for being
unused) measured without it, and the icon was then drawn over the text.
Read the size from the PNG header when the icon is inserted or loaded.
Also remove a width adjustment in MeasureNotice that was overwritten
before use.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Prune expired failed_ entries, which otherwise grew for the whole
session.
- When CreateTexture fails, wait 5 seconds before decoding and uploading
again, instead of retrying every frame.
- Bind a newly created texture. Callers draw right after BindIconTexture,
and got whatever was bound before for the first frame.
- Make DiskCacheEntry's padding explicit. 32-bit x86 Linux aligns double
to 4, so its icon.cache didn't match other builds. The layout is
unchanged elsewhere.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The key was resized before its length was checked, and the data length
wasn't checked at all, so a corrupt icon.cache could make a multi-GB
allocation and crash every launch. The cache is only saved at a clean
exit, so a bad file stayed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Any 200 response went into the cache, and one that didn't decode (a
captive portal's HTML, or a PNG over the 256x128 limit) was marked bad,
saved to icon.cache and never downloaded again. Remove it and mark it
failed, so MarkPending allows another download after the retry delay.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UWP compiled naett with the WinHTTP backend but never reported
SYSPROP_SUPPORTS_HTTPS, so achievement traffic and badge icons went over
plain http. When the media server redirected to https, icons failed.
A failed icon download also left the URL marked pending, so it was never
retried for the rest of the session. Mark it failed instead, and let
IconCache retry it after 30 seconds.
The set-completed badge also skipped RemoveHttpsIfNeeded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Choices that are just numbers have no translation, so every lookup got
logged as a missing translation - the Graphics settings screen spammed
the log once per frame as long as Frame Skipping was on.
Fixes#21455
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
Unlike the directional moves, this doesn't look at where anything ended up on
screen - it walks the hierarchy in the order views were added, flattening nested
groups in place. That's what makes it predictable in the layouts where "what's
to the right of this" has no good answer.
A view is a stop if it's focusable and enabled, the same test the directional
moves apply, so the two agree on what's reachable. Hidden subtrees are skipped
whole, which is what keeps a TabHolder's inactive tabs - V_GONE rather than
removed - out of the order without any special casing. Containers are gated on
visibility only, not enabled, matching Key/Touch/Axis: disabling a container
doesn't stop its children being interactive anywhere else either.
Ctrl+Tab stays with ChoiceStrip, which uses it to switch tabs.
focusMoves now holds FocusMove rather than raw keycodes, so the direction is
decided in one place while the modifiers are still around, and a held key
repeats in the direction it was originally pressed with - the synthesized repeat
has no modifiers of its own. That also retires the keycode switch in
UpdateViewHierarchy and IsScrollKey, which had no other callers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF