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>
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>
"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>
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>
Same as Android: rotation rates go to NativeGyroscope, and SYSPROP_HAS_GYRO shows the "Use gyroscope" option.
Accelerometer data is now scaled from g to m/s^2, like Android's. Tilt normalizes it, but the gyro fusion only trusts the accelerometer when it reads about 9.81, so in g it would never correct drift.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SYSPROP_HAS_ACCELEROMETER was true for every phone and tablet, whether or
not it had the sensor. The activity now tells the native side what the
sensor manager found, which also answers the new SYSPROP_HAS_GYRO. VR
headsets still report neither. Every other platform returns false for the
gyro for now.
The "Use gyroscope" checkbox uses the new property instead of waiting for
gyro data to show up.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tilt used the raw accelerometer as the gravity vector, which jitters with
sensor noise and jumps when the device is shaken. Now, when gyro data is
coming in, a complementary filter integrates the gyro rates and only pulls
the result toward the accelerometer with a 0.5 s time constant, trusting
the accelerometer less the further it reads from 1g. The fused vector goes
through the same ProcessTilt path, so calibration and sensitivity work as
before. Without a gyro, nothing changes.
The fusion lives in TiltEventProcessor behind NativeGyroscope(), so other
sources (DualSense) can feed it too. Android registers TYPE_GYROSCOPE next
to the accelerometer. A "Use gyroscope" checkbox (on by default, per game)
appears in tilt setup once gyro data has been seen.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PPSSPP has access to the URI, so a frontend or adb can launch the game with it,
which isn't true of a plain path on modern Android.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Scoped storage no longer flips on while no activity is registered: whether
legacy storage was preserved is fixed per install, so it's asked once.
- Free space reads as unknown (-1), not 0 bytes, without an activity.
- Strings from Java (file listings and info, crash history, the camera list,
the input debug string) go through GetJavaString, so names with characters
outside the BMP aren't mangled, and a null string can't crash the crash
history.
- Requests (inputbox, browse_*) whose command can't be delivered are failed
instead of left waiting forever.
- A font that fails to load uses the default typeface (-1) instead of font 0,
and is retried later if there was no activity yet. Missing exception checks
added in the text renderer.
- Method IDs and the storage callbacks are looked up once in JNI_OnLoad.
- Unknown permissions are logged correctly.
Also removes what was left of Moga support, the unused backbuffer format and
a stray MessageBox macro, and tidies System_GetCameraDeviceList.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part of #21585
Android 17 blocks LAN traffic in both directions (TCP and UDP, including
broadcast and multicast) without ACCESS_LOCAL_NETWORK. Ask for it when
networking, the built-in ad hoc server, UPnP, share-on-startup or the
remote debugger gets turned on, when a game initializes ad hoc with
networking enabled, and at startup if a server is set to start then.
A denied ad hoc send fails with EPERM, which the games never see, so show
a message for it.
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>
With 270 degree pre-rotation, the rotated y runs along the physical
height, but was clamped to the physical width, so scissors on the left
of the screen moved. This was #15773, which got that pre-rotation
disabled. The 90 degree case had the bounds swapped too, but only too
loose there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetJavaString() returned Java's modified UTF-8, so input like emoji
reached C++ as invalid UTF-8. Commands to Java now get converted the other
way. Request IDs are made on several threads, and the permissions map was
written on the Java UI thread and read on the render thread. The frame
command debug log no longer prints params, which could hold a password.
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>
Since db8ff43, a leftover local shadowed lastImguiEnabled_ in runImDebugger,
so TEXT_GOTFOCUS was never sent and SDL never started text input for imgui.
Drive it from io.WantTextInput instead, so it's only on while editing.
The regular UI and the debugger now say independently whether they want text
input, and the platform only gets TEXT_LOSTFOCUS once neither does, so leaving
an imgui field doesn't break type-to-search on the screen underneath.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Oboe input callback used to call Microphone::addAudioData directly,
writing PSP memory from the audio thread. Now it only pushes samples
into a SampleRing, and pollRecording drains it on the CPU thread.
Starting and stopping still go through Java, which asks for permission.
Every host with a microphone polls now, so sceUsbMic drops what only the
push path needed: QueueBuf's recursive mutex, restarting the mic on a
device change, and the startMic parameter nobody read. Also removes the
unused System_AudioRecordingState and Microphone::isNeedInput.
The Java microphone_command branch didn't return true, so every command
logged "processCommand failed".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CPU-thread polling added for SDL was keyed off defined(SDL), which
headless and Core are built with too, hence the extra headless check.
Ask the host instead, with SYSPROP_MICROPHONE_NEEDS_POLLING, so other
hosts can hand their input over the same way.
startMic now sends the same startRecording command for every non-Win32
host, taking the rate from curSampleRate like the SDL path did. The
Android path read it from param, which leaked, and which is null when
coming from onMicDeviceChange.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offers a file through the system share sheet, for things like sending a
zipped savestate to a friend. Android for now (SYSPROP_SUPPORTS_SHARE_FILE):
content:// paths from a picked folder are shared as they are, and plain paths
through a FileProvider. Either way the receiving app can read just that file.
With no MIME type given, it's guessed from the extension.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Oboe uses AAudio on Android 8.1+ and falls back to OpenSL ES on older
devices. Output and microphone input both go through it. The mic now gets
resampled by Oboe from the device's rate to whatever the game asks for.
New: when a stream is disconnected (headphones unplugged, Bluetooth device
going away), it's reopened on the new default device instead of going
silent.
Oboe 1.11.0 is added as a submodule, built through ext/oboe-build for CMake
and as a module in Android.mk.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Instead of the PSP's web browser, a dialog shows the URL the game wants
and opens it in the host's browser on X, or backs out on O. Either way the
game sees the browser closed normally. Platforms that can't open a URL
(the new SYSPROP_CAN_LAUNCH_URL) only offer to back out. Only plain
printable-ASCII http(s) addresses are handed over, and on Linux without a
shell.
What the firmware does (sceUtility_Driver, 6.61, plus
utility/dialog/htmlviewer): the HtmlViewer has its own state apart from the
other dialogs, so they don't block each other, and its calls return
WRONG_TYPE until one has started. The request size picks the 2.00 to 3.00
layout, and InitStart allocates 3.5MB of user memory (4.5MB with options
bit 0x400 from 2.70 on), failing with 800200d9 without it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Update()/Entries()/ClickEntry()/Show()/SetProgressBar()/RemoveProgressBar()
locked mutex_ around entries_, but CancelById/ShowAchievementUnlocked/
ShowAchievementProgress/ShowChallengeIndicator/ShowLeaderboardTracker/
ClearAchievementStuff/SetClickCallback/SetFlags didn't. Core/WebServer.cpp
runs its own thread and calls SetClickCallback() while the main thread's
Update() concurrently erases/iterates the same vector every frame.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4QAoxV2KY7ek4PcZw3WvY