Commit Graph
49604 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 da33eab776 Outputs: Defensive checks, and don't wait for a Metal drawable twice in a frame
- Metal: when a layer has no drawable to give (it can take a second to
  say so), don't ask again in the same frame for every pass to it.
- Swapchains other than the primary are created, resized and destroyed
  between frames (Metal asserts it), and destroyed with the last
  reference to them (ReleaseAssertLast, Metal and Vulkan).
- BindFramebufferSamplesAsTexture asserts it isn't given a swapchain,
  like BindFramebufferAsTexture.
- An offscreen Metal swapchain can't be resized, asserted.
- VulkanRenderManager asserts that no additional swapchain outlives it.
- Native_RemoveOutputAndWait asserts it isn't called on the thread that
  handles the requests, where it would always time out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:08:52 +02:00
Henrik RydgårdandClaude Opus 5.5 ba55ee7e5f Screenshots: Take the game image while the game is on a TV
An output screenshot reads back the device's own screen, which only has
the touch controls on it then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:07:10 +02:00
Henrik RydgårdandClaude Opus 5.5 080470d041 Display layout: No editing while the TV shows a layout for another orientation
With the game on a TV (landscape) and the device in portrait, the display
layout screen edited the portrait layout, which the TV doesn't use, so
nothing seemed to change. Its controls are now disabled in that case, and
work as before with the device held the same way as the TV.

The screen rotation hotkeys change the layout the game is shown with, the
TV's, instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:06:07 +02:00
Henrik RydgårdandClaude Opus 5.5 b642984755 Outputs: Decide on the TV mode by the running backend
iOS and Android checked the backend setting, which a change sets right
away while the restart prompt is still up. A TV connected then got an
output OpenGL can't draw to, and showed black instead of mirroring.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:04:15 +02:00
Henrik RydgårdandClaude Opus 5.5 ea26b3c7dd SDL: Quit when the main window is closed, with --game-window open too
SDL sends a quit only when the last window closes, so with the game
window open, closing the main one did nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:04:01 +02:00
Henrik RydgårdandClaude Opus 5.5 013460b2fe Outputs: No Cardboard VR on a TV
The design doc said Cardboard VR turns off on a Game output, but only the
config was checked, so with it on the TV got the split screen too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:03:43 +02:00
Henrik RydgårdandClaude Opus 5.5 771e40e297 Vulkan: Recreate an additional swapchain that stays suboptimal
Only out of date and a lost surface recreated one, so a TV whose mode
changed without a resize (MoltenVK reports SUBOPTIMAL when the layer's
size no longer matches) kept a stretched picture. Like the main
swapchain, it's now recreated after a few SUBOPTIMAL presents in a row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:03:16 +02:00
Henrik RydgårdandClaude Opus 5.5 22aa12d047 iOS: Keep the TV's layer until its output's removal has been handled
The add request holds the layer unretained, and the swapchain only takes
a reference once it's handled. If the TV disconnected (or the setting
changed) before that, the window and the layer were freed with the add
still queued. The view controller now keeps a reference for the requests,
released after Native_RemoveOutputAndWait, off the main thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:02:36 +02:00
Henrik RydgårdandClaude Opus 5.5 be4bdf3590 Android: Show the game on the TV again after the system dismisses it
Android dismisses a Presentation by itself on some display changes, like
a mode switch right after plugging in. We kept the dismissed one, so the
TV went back to mirroring until it was replugged. Its dismiss listener
now drops it and checks again, and so does a change to any display other
than the device's own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:01:03 +02:00
Henrik RydgårdandClaude Opus 5.5 5799bdee59 Outputs: Rebuild the post-shader chain when the game moves to another output
Post-shader framebuffers (for shaders at output resolution, and those
that keep the previous frame) are sized for the output they were built
for. Moving the game to a TV or back, or the TV changing size, left them
sized for the other one. SetOutputOverride now asks for a rebuild when
the output changes.

Also corrects the design doc: "auto" render resolution follows the
device's screen, not the TV.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 23:00:13 +02:00
Henrik RydgårdandClaude Opus 5.5 3e53d4bde1 Outputs: Don't let a removal skip the wait while the graphics come back
The outputs were only marked live after deviceRestored(), so a
Native_RemoveOutputAndWait() that came in while it was recreating their
swapchains returned at once. On Android the surface could then be freed
while a swapchain was being made on it (unplugging the TV as the app
returns to the foreground). Mark them live before the queue is
processed, so such a removal waits for the next frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:59:29 +02:00
Henrik RydgårdandClaude Opus 5.5 a0e0115e65 NativeApp: Say why output requests have their own queue
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:44:16 +02:00
Henrik RydgårdandClaude Opus 5.5 d8f9ffc49f Translations: External display setting, and the TV mode messages
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:31:07 +02:00
Henrik RydgårdandClaude Opus 5.5 04ae3fea5b External display setting: Show the game, or mirror the screen
Graphics > "External display" on iOS and Android. "Show the game" (the
default) is the TV mode, "Mirror the screen" leaves the TV to the system.
A change applies right away. It's disabled on backends that can't draw to
a second swapchain.

On Android 12 and up, a second screen that's part of the device (like a
dual-screen handheld's) isn't used, so the game doesn't move there by
itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:31:07 +02:00
Henrik RydgårdandClaude Opus 5.5 5fbd134a7d Android: Show the game on a second display, with the controls on the phone
A presentation display (HDMI or USB-C video out, some wireless displays,
or the simulated ones in Developer options) gets a Presentation with a
SurfaceView, whose surface becomes a Game output, when the backend is
Vulkan. Otherwise Android keeps mirroring.

The surface mustn't be touched after surfaceDestroyed returns, so the
output is removed with the new Native_RemoveOutputAndWait(), which
returns once the render thread has let go of it (or right away if the
graphics are down). The UI drawn on an output now leaves out the main
swapchain's pre-rotation, as the game already did.

"In menus on %1" gets the device name from Settings.Global.DEVICE_NAME.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:31:07 +02:00
Henrik Rydgård f6fa84329e Merge pull request #22736 from hrydgard/avc-spliced-stream-timing
AvcDecoder: Keep the output clock going when a new stream is spliced on
2026-10-22 22:14:54 +02:00
Henrik Rydgård 79cedbc13f Merge pull request #22734 from hrydgard/vulkan-stale-extent-workaround
Android Vulkan: Bring back the stale surface extent workaround
2026-10-22 21:52:20 +02:00
Henrik RydgårdandClaude Opus 5.5 56c8d067af pspautotests: avchold covers a stream spliced on without a flush
Updates the submodule for video/mp4/avchold's ref3b1hrdsplice, recorded
on a PSP, which fails without the AvcDecoder fix on this branch: the pictures after
the splice never come out.

avchold and decodeend only test anything with Sony's mpeg and mp4
modules running, which headless never does for homebrew unless asked,
and which CI doesn't have. docs/pspautotests.md now says how to run
them (--disable-hle=0x110 and a memory stick with a firmware dump).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:47:45 +02:00
Henrik RydgårdandClaude Opus 5.5 ddfd3bcfb9 CmdLine: Take integer parameters in hex too, and refuse trailing junk
They were read with sscanf("%d"), which stops at the first character it
can't use, so --disable-hle=0x110 was accepted as 0 and quietly ran
everything on our HLE. Now decimal or 0x hex, optionally signed, and the
whole value has to parse. Up to 0xFFFFFFFF is taken, for full masks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:47:39 +02:00
Henrik Rydgård 1796c78a07 Merge pull request #22735 from hrydgard/multi-swapchain-4
Multi-swapchain support for Vulkan (iOS TV mode, --game-window on Mac, Linux and Windows)
2026-10-22 21:46:32 +02:00
Henrik RydgårdandClaude Opus 5.5 6c8c71a125 AvcDecoder: Keep the output clock going when a new stream is spliced on
Burnout Legends feeds its intro movies to one decoder back to back, with
no flush in between. Each new stream opens with a buffering period whose
cpb_removal_delay is 0, which put our reconstructed clock 18 frames back,
so every picture after it was held until the clock caught up, and then
20 came out of one decode. sceMpegAvcDecodeYCbCr converts every picture
a decode reports into the caller's buffer list, which only has room for
one, so mpeg.prx went on through stack garbage and crashed as the title
screen came up.

Such a buffering period now continues from the last access unit instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 21:02:36 +02:00
Henrik RydgårdandClaude Opus 5.5 c542d186bd docs: Multi-swapchain status: Linux and Windows, no OpenGL
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:55:52 +02:00
Henrik RydgårdandClaude Opus 5.5 f8e7efc8b0 Android Vulkan: Check the surface extent against the latest window size
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>
2026-10-22 20:49:08 +02:00
Henrik RydgårdandClaude Opus 5.5 5009724d91 Windows: --game-window, on Vulkan
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>
2026-10-22 20:41:35 +02:00
Henrik RydgårdandClaude Opus 5.5 82e6ac86f0 Vulkan: Balance the debug label of a swapchain pass that has no image
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>
2026-10-22 20:41:35 +02:00
Henrik RydgårdandClaude Opus 5.5 ea5e39c440 Android Vulkan: Bring back the stale surface extent workaround
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>
2026-10-22 20:29:21 +02:00
Henrik RydgårdandClaude Opus 5.5 76d0d37941 Outputs: Vulkan too, for --game-window and the iOS TV mode
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>
2026-10-22 20:11:36 +02:00
Henrik RydgårdandClaude Opus 5.5 8e8cf5dbb4 Vulkan: Multiple swapchains
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>
2026-10-22 20:11:36 +02:00
Henrik Rydgård 47e67cb122 Merge pull request #22728 from hrydgard/multi-swapchain-3
Multi-swapchain (multi-window rendering) support, for Metal
2026-10-22 20:09:32 +02:00
Henrik Rydgård ba26373cb1 Merge pull request #22731 from hrydgard/zuma-analog
Zuma: Correct aiming algorithm to behave better with analog sticks
2026-10-22 20:03:04 +02:00
Henrik Rydgård 8485e8ebc0 Merge pull request #22733 from hrydgard/appimage-headless
AppImage: Also package PPSSPPHeadless, as an AppImage of its own
2026-10-22 20:02:45 +02:00
Henrik RydgårdandClaude Opus 5.5 ba54d88d16 AppImage: Also package PPSSPPHeadless, as an AppImage of its own
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>
2026-10-22 19:40:03 +02:00
Henrik RydgårdandClaude Opus 5.5 b3c44cba8c Zuma: Correct aiming algorithm to behave better with analog sticks
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>
2026-10-22 19:31:54 +02:00
Henrik Rydgård ac8976fc71 Merge pull request #22732 from hrydgard/moltenvk-portability
Vulkan: MoltenVK portability fixes (loader on Mac, portability subset features)
2026-10-22 19:13:22 +02:00
Henrik RydgårdandClaude Opus 5.5 b46e386f12 Vulkan: Enable the portability subset features MoltenVK has
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>
2026-10-22 18:46:43 +02:00
Henrik RydgårdandClaude Opus 5.5 db6cc41b9d Vulkan: Ask for portability drivers on Mac
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>
2026-10-22 18:46:43 +02:00
Henrik RydgårdandClaude Opus 5.5 d39df0b45c iOS: Render the TV output at the display's full resolution
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 3b1432d851 Outputs: Begin the UI frame after binding, and scissor to the output
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 014f6dceea Outputs: Name the device in the menu message
"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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 3ca76c674c Outputs: Say the menus are on the device, instead of showing black
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 923611b445 Display layout: Disable move/resize while the game is on another output
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 7bd4b13baa iOS: Show the game on an AirPlay TV, with the controls on the phone
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 d491fc90cf Secondary outputs, and --game-window on Mac to show the game in a second window
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 56c76ace35 thin3d: Swapchain objects, and multiple swapchains on Metal
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 26e302aa86 docs: Screens draw to secondary outputs through renderOutput()
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 4922859675 PresentationCommon: Take the output's size, insets and rotation as parameters
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 a2d18d21cd iOS: Make Metal the default graphics backend
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>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 7da7e71aea docs: Design for multiple swapchains and an iOS TV mode
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik Rydgård 679d804c26 Merge pull request #22730 from hrydgard/config-constexpr-parentptr
Config: Alternative speeds in percent, constexpr setting tables, view parent pointers
2026-10-22 18:41:48 +02:00
Henrik RydgårdandClaude Opus 5.5 44b5dc1b95 Config: Default AutoFrameSkip to false
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>
2026-10-22 18:00:17 +02:00