g_display lags behind surfaceChanged until the render thread applies the
resize, and a recreate after a suboptimal present can run in between.
The check then swapped a correct extent for a frame. Android now sets the
draw size callback to the latest size from backbufferResize, and the
check uses that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A second Win32 window, added as a Game output like the SDL one. Keys
typed into it still go to the emulator; the mouse doesn't. It's for
testing multiple outputs (standing in for a TV over AirPlay) until real
multi-window support takes it over.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A swapchain pass closes its step label and opens one in presentCmd, but
only if it gets an image. With the main window minimized it doesn't, and
the end of the step closed a label that wasn't open, in presentCmd if an
extra swapchain's pass had moved recording there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
e32e558aab dropped it, assuming the stale extent came from our own resize
race. A Redmi 9A (PowerVR, Android 11) shows it doesn't: after a rotation,
vkGetPhysicalDeviceSurfaceCapabilitiesKHR returns the new transform with the
old extent, even when queried after surfaceChanged and ApplyResize have
given us the new size, and the wrong swapchain then stays until the next
rotation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The SDL game window gets its Vulkan surface the same way the main window
does, so it should work on Linux too. Windows and OpenGL aren't done.
EmuScreen asks the draw context for the output's size, which follows a
recreated Vulkan swapchain.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Swapchains other than the main one are VKRSwapchains, owned by the
render manager, each with its own surface, image views, framebuffers
and depth buffer, in the main swapchain's format so the backbuffer render
pass and pipelines work for all of them. A render step with no
framebuffer can name one.
They don't pace the frame: acquiring doesn't wait, and the pass is
skipped if there's no image. The frame's last submit waits on and
signals the semaphores of every swapchain acquired that frame, and each
is presented after the main one. When one goes out of date, it's
recreated at the start of the next frame.
The main swapchain's path is unchanged; frametests on Vulkan match.
VulkanContext::CreateSurface is the old per-platform surface code, now
also used for the extra ones.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Built in the same job, uploaded with the PPSSPP AppImage and attached to
releases. Its .zsync isn't shipped, since already-installed PPSSPP
AppImages update from any *<arch>.AppImage.zsync in the release. The
PPSSPP AppImage's own pattern now starts with PPSSPP- for the future.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Zuma is a pretty bad port from PC and has really weird aiming math,
clearly partially derived from the mouse aiming code.
- Each stick axis got its own dead zone (7864 of 32767), so the angle
bent towards the nearest axis, and at half deflection anything within
about 15 degrees of an axis was on it.
- The aim puts a cursor 450 pixels out along the stick and points the
frog at it, but only moves a cursor coordinate when that axis is
non-zero. With an axis in its dead zone, or the stick straight along
the other one, the frog pointed at wherever the cursor had last been:
full right after a diagonal aimed 27 degrees off.
- The axes are read as 127 minus the PSP's value, so a stick at rest
reads -1, which the old dead zone hid.
Two hooks: one gives the left stick a circular dead zone of the same
size, centred on 128, once the game has stored its floats; the other
moves a cursor coordinate that wasn't updated to the frog's own.
Reading the angle the game sets for 24 directions at full, 50% and 45%
deflection, it now matches the stick's to within 0.6 degrees, the
stick's 8-bit steps. Before, it was off by up to 27 degrees.
The aim still ignores the stick below about 43% of its travel: the
game's own minimum length on top of the dead zone.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
We queried VkPhysicalDevicePortabilitySubsetFeaturesKHR but never enabled
it at device creation, so to the validation layers we were using image
view swizzles (and sampler LOD bias) without having them. Enable whatever
the device offers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Through the Khronos loader (like the Vulkan SDK's, needed for the
validation layers), MoltenVK is only listed if the instance enables
VK_KHR_portability_enumeration. Without it, instance creation failed with
VK_ERROR_INCOMPATIBLE_DRIVER and we fell back to OpenGL. Loading MoltenVK
directly, as the app normally does, works as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 1080p cap was meant to save Wi-Fi bandwidth, but an Apple TV 4K
only offers 3840x2160 over AirPlay, so the stream was 4K anyway, just
upscaled.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
UIContext::BeginFrame sets the scissor, which on Vulkan needs a bound
render target, and sets it to the primary's size. In portrait, that cut
the TV's menu message off past the phone's width.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"In menus on Henrik's iPhone" if the platform gives us the name the user
gave the device, otherwise "In menus on your iPhone" (or Mac). iOS only
gives the name with an entitlement Apple has to grant, so for now it's
the model there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
An external display (like an Apple TV over AirPlay) becomes a Game
output, when the backend is Metal. The game is shown there full screen,
and the phone shows the touch controls with "Playing on TV" in place of
the game. The menus stay on the phone. With other backends, iOS keeps
mirroring.
Up to iOS 26 the external display scene comes from the Info.plist role.
iOS 27 ignores that, and only offers the scene to apps that register a
scene accessory for it, so we do that too. The registration is withdrawn
when the view controller shuts down, so it follows a backend change, and
a restart now goes to the app's scene rather than the first one.
An Apple TV 4K only offers 3840x2160 over AirPlay, so the stream stays
4K, but we render at 1920x1080 and let the layer scale it up.
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>
OutputParams describes the surface the game is presented to.
PrimaryOutputParams() builds it from g_display and the system's safe
area insets, as before. SetOutputOverride() lets the game go to another
output, like a TV, which will have its own size, rotation and insets.
No change for the primary output: frametests on Metal and Vulkan match
before and after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Not on tvOS, where the Metal backend hasn't been run yet. Existing
installs keep the backend saved in their ini.
The multi-swapchain plan now does Metal before Vulkan to match.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Its default called IsVREnabled() during static initialization, before VR is
set up, so it was always false in practice.
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>
SETTING now takes offsets with offsetof and passes them along with the
setting's type, instead of the block and member pointers, and the
ConfigSetting constructors are constexpr. So the tables are built at compile
time, and later per-setting metadata can be checked by the compiler too.
Defaults that weren't constants had to change:
- AutoFrameSkip called IsVREnabled() during static initialization, before VR
is set up. It's now a default callback, evaluated when the config is
loaded or reset, which can turn it on by default on VR devices.
- DisplayRefreshRate read g_Config's own initial value, which is 60.
Path defaults now share the string member of the default union.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The settings screen showed them in percent but they were stored as frame
rates out of 60, so the screen kept converted copies and wrote them back. Now
they're stored in percent under new ini keys (AltSpeedPercent,
AltSpeedPercent2, AnalogAltSpeedPercent), migrated from the old frame rate
keys like the audio volumes were, and converted to a frame rate where the
timing needs one.
Low percentages no longer round to a 0 fps limit, which meant unlimited.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Apply the display layout's scale and offset to the overlay itself instead
of always centering it, so moving/resizing the display in Display Layout
moves the overlay with the game inside it, and the portrait default
(screen high up) is respected.
- Rescanning overlays (done whenever the display layout screen recreates its
views, e.g. after closing the picker popup) no longer unloads the active
overlay unless its definition changed.
- Re-picking the current overlay no longer posts GPU config change
notifications, which caused an audio glitch with a game running behind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBg32EFPsTXjAFu7dnTcz
Draws a user image around or behind the PSP screen, to fill the black bars
on displays whose aspect ratio doesn't match the PSP's (#20325).
- Overlays are PNGs in PSP/OVERLAYS. A plain PNG there just works: it's
drawn in front of the game, and its transparent hole is found by walking
from the image center to each edge until a nearly opaque pixel. Existing
RetroArch overlay images can be dropped in as-is.
- A subfolder with an overlay.ini can define overlays with a Name, Layer
(Foreground/Background) and an explicit Hole=x,y,w,h.
- "Load overlay image..." in the display layout screen imports any PNG
(including Android content URIs) into the overlays folder and selects it.
- The overlay is fit to the screen (aspect preserved, centered). The hole is
mapped with the same scale, and the game is fit into it with the normal
stretch / integer scale / aspect ratio options. Offset and scale don't
apply while an overlay with a hole is active. This goes through
CalculateDisplayOutputRect, so touch mapping and post shaders agree.
- The selection is a per-orientation, per-game DisplayLayout setting.
- Disabled in VR, Cardboard, and with skip buffer effects (except software
rendering), since the presentation path doesn't run there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QGBg32EFPsTXjAFu7dnTcz
The session category was switched before the audio unit's input was
turned off, and iOS refuses ('!pri') to leave PlayAndRecord while the
input runs. So after a game used the microphone, the session stayed in
PlayAndRecord, ignoring the silent mode setting. Switch it while the
unit is stopped instead.
Also log the two setCategory failures separately, and the route's input.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The playback categories we use route to AirPlay on their own (checked
with screen mirroring to an Apple TV, with every combination of the
silent mode and mix-with-others settings). PlayAndRecord, used while a
game records from the microphone, needs AllowAirPlay for that.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
EXIT_APP is handled on the render thread, and calling exit() there ran
the destructor of the still joinable render thread std::thread, which
calls std::terminate. Shut the view controller down on the main thread
first, which joins the render thread, then exit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pulling down Control Center starts a touch on our view that UIKit
doesn't always end or cancel. The tracker kept that slot down forever,
and touch input stopped working until the app was restarted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- inet_pton's IPv6 half is rewritten as a plain parser of the groups before
and after "::". It no longer rejects a valid trailing "::" after seven
groups or accepts nine groups around a "::", writes bytes in network order
without assuming the host's, and only touches the output on success.
- The IPv4 half rejects empty octets ("1..2.3"), and an unsupported family
returns -1 like the POSIX one, instead of success.
- An empty label ("a..b", ".example.com") ended the encoded name early, so
the direct lookup asked for something else and cached it under the name
that was asked for.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- inet_pton could write outside its output for IPv6 strings with more than
eight colons or two "::". Only reachable through AF_INET today, but it's a
public function. Added a unit test.
- The direct DNS lookup took any datagram that arrived within its timeout as
the answer, and cached it until restart. The socket is now connected, so the
OS drops packets from anyone else, the query has a random ID, and replies
with a different ID, without the response bit, truncated or with an error
code are rejected.
- Packet fields are read and written with memcpy instead of through unaligned
pointers, which can fault on 32-bit ARM.
- The lookup cache has a lock. Only the emu thread uses it so far.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>