UIKit lays out at the new size from the main thread, often while a frame
is being drawn, and the swapchain was only resized after presenting. So
the first frame of a rotation was laid out for one size and drawn at the
other. The view controller now hands the new size to the render thread,
and NativeFrame resizes before drawing instead of after presenting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the GL view controller gone, PPSSPPViewControllerMetal was the only
subclass of PPSSPPBaseViewController. They're now one class,
PPSSPPViewController, in ios/ViewController.mm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also drops what they guarded: the pre-14 UIImagePickerController and
document picker fallbacks, the psychic paper entitlement workaround
(fixed in 13.5), and the CS_EXECSEG_ALLOW_UNSIGNED JIT probe, which
returned false on everything from 14.4 on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sideload build ran ios/macbundle.sh after building, which looked for
git-version.cpp in the source root instead of the build dir, so sideload
builds shipped with an empty version in Info.plist. Both now use
iosbundle.sh, and share the Xcode target settings and scheme generation
except for bundle ID and signing.
RESOURCE was listed three times in one set_target_properties, and only
the last one counted, so the launch storyboard never got compiled into
either bundle. Settings.bundle only made it into sideload builds, and is
deleted: its one toggle, for external displays, hasn't been read since
the external display code was removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app now always uses the Metal view controller with Vulkan (MoltenVK),
for both the App Store and sideload builds. The GL view controller is gone,
and Common/GPU/OpenGL (except GLFeatures, used by the shader generators) and
GPU/GLES are no longer compiled for it. PPSSPP_API_ANY_GL is now off for
all of iOS except the libretro core, which still uses GLES.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It has done nothing since 2017. iCade input came through iCadeReaderView,
a hidden UIKeyInput view that decoded the controller's keystrokes, and
f0ba66fcd6 ("add support for smart keyboard") commented out the code
that created it. Nothing has made one since, so the view controller's
iCadeEventDelegate methods and the ICadeTracker behind them were never
called.
iCade controllers present themselves to iOS as Bluetooth keyboards, so
their keys still arrive as hardware keyboard presses, which can be
mapped like any other keys. Modern controllers go through
GameController.framework anyway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view controller was the keyboard's text input, and UIKit hands first
responder back to it whenever something it presented goes away: a share
sheet, a photo picker, the alert after an app icon change. Each time,
up came the keyboard, and hideKeyboard calls were sprinkled around to
put it away again, one on a four second timer.
Text input now goes to a small view that only becomes first responder
when a text field gets focus. The view controller is first responder
otherwise, for hardware keyboard presses, but takes no text, so it can't
bring up the keyboard. The workaround hideKeyboard calls are gone, and
so is the endEditing on rotation, which dropped the keyboard if you
turned the phone while typing. Showing and hiding are synchronous now,
since every caller is on the main thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offers the file through the share sheet, which works out the type from
the extension. Also gives NativeInit a cache directory, Library/Caches,
which ShareFilesAsZip needs for its zips. That moves the SPIR-V and
download caches there from Documents/PSP/SYSTEM/CACHE, out of backups
and the Files app, which is where Apple wants caches anyway.
Co-Authored-By: Claude Opus 5.5 <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>
sceUsbMic input comes from the input side of the RemoteIO unit that
already does the output. The input callback only copies samples into a
lock-free ring buffer, sized so it never allocates, and the CPU thread
drains it into Microphone::addAudioData when sceUsbMic polls, the way
the SDL backend works. Reconfiguring the unit happens on the main thread.
Whether the game is recording is kept apart from whether the unit's
input is on, so recording resumes after an interruption or a trip to
the background, which shut the unit down. The unit also runs, silently,
when sound is off but the game is recording.
While recording, the session is PlayAndRecord, with Bluetooth through
A2DP so headphones keep full quality and the built-in mic is the input.
Permission is asked for on iOS 17+ through AVAudioApplication.
Also moves the microphone recorder homebrew used to test this from
Tools/psp-microphone to pspautotests, as demos/microphone, and updates
the submodule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The camera delivers frames in the sensor's landscape orientation however
the phone is held, so in portrait the picture was 90 degrees off. Rotate
the capture connection: on iOS 17+ with a rotation coordinator, which
keeps the picture level with the horizon, and before that to match the
interface orientation.
A portrait frame doesn't fit the landscape size the game asks for, so
scale frames to cover it and crop the rest, rather than stretching.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unused IAPManager startObserving (which would have registered the
observer twice), the obsolete UILaunchImageFile, debug logging of the
SceneDelegate class loading, and a commented-out JIT version check.
Also bring the file overview at the top of main.mm up to date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It was commented out over doubts about the threading. Setting the
pasteboard on the main thread settles those.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RESTART_APP's arguments, like --start-screen=touchscreentest, were only
logged. Worse, the block that got them captured param1 by reference, and
ran after the request's string was gone. Copy them, and parse them along
with the startup path when relaunching.
When Vulkan failed to initialize, the config switched to OpenGL, but
the Metal view controller stayed, so nothing rendered until the user
restarted the app by hand. Restart it automatically, unless OpenGL is
disabled, where config loading would pick Vulkan again and loop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame was drawn at its own size into a context of the size the game
asked for, so the game got one corner of the picture. Also drop the
preview layer, which was never shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The safe area insets were converted to pixels with UIScreen.scale, but
the display size with nativeScale. The two differ on the Plus models
and the minis, so the insets were off by up to 15% there. Both use the
window scene's nativeScale now.
Touch positions were truncated to integers before being multiplied by
the screen scale, putting them on a grid about 3 dp apart. Convert
straight from points to dp, by the view size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Request an orientation update with setNeedsUpdateOfSupportedInterfaceOrientations
on iOS 16+, where attemptRotationToDeviceOrientation is deprecated.
- Open the document picker with UTTypes on iOS 14+, and present it from
sharedViewController rather than through the deprecated keyWindow.
- List cameras with a discovery session instead of devicesWithMediaType.
- IS_IPAD() instead of UI_USER_INTERFACE_IDIOM().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sideloaded builds read hardware keyboard events from the private
UIEvent._gsEvent, at offsets hardcoded for iOS 7 and 9, through a
UIApplication subclass. The UIPress path the App Store build uses works
from iOS 13.4, so use it in all builds, and delete the hack along with
PPSSPPUIApplication and SmartKeyboardMap.
System_Vibrate asked the private UIDevice feedbackSupportLevel whether
to use the feedback generator, in App Store builds too, and sideloaded
builds fell back to the private AudioServicesPlaySystemSoundWithVibration.
The feedback generator just does nothing on devices without a Taptic
Engine, so always use it. That leaves the iPhone 6s and first SE
without virtual button vibration in sideloaded builds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The minimum is iOS 13 (15 for the App Store build), so the iOS 10
display link fallbacks, the iOS 13 orientation check, the weak import
of GCController and the respondsToSelector checks for APIs from iOS 8
to 13 can go.
The keyboard presses checked for iOS 13.0 but used UIPress.key outside
the check, which needs 13.4, so it would crash on 13.0-13.3 if it ever
runs there. It only runs in App Store builds now. Folded the two copies
into one function with the right check.
The deployment target was 13.0 in the toolchain's compile flags but
12.0 in CMAKE_OSX_DEPLOYMENT_TARGET and 11.0 in the Xcode attribute for
non-App Store builds. All three now come from the toolchain file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
System requests and System_Vibrate come from the emu thread. Camera,
GPS, share and keyboard requests called sharedViewController directly
from there, which the main thread can nil out during a restart, and the
share sheet, the location manager and the feedback generator were used
off the main thread. Dispatch them all to the main thread.
The share sheet also had no popover anchor, which iPad requires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both view controllers stopped the emu/render thread on every rotation
and started it again, tearing down all of graphics. Since d0e45b94d9
NativeFrame resizes the graphics context and posts GPU_DISPLAY_RESIZED
on iOS too, so a NativeResized() is all a size change needs, like
Android now does. GLKView resizes its drawable by itself, and the Vulkan
context recreates the swapchain, which MoltenVK sizes from the layer.
The resize happens in viewDidLayoutSubviews, which UIKit runs at the
final size as a rotation animation starts. Resizing when the rotation
completed instead left half the screen black while it animated. This
also covers window resizes, which matter more now that
UIRequiresFullScreen is deprecated (iPadOS 26).
NativeFrame sets the PSP_CoreParameter() size, so the main thread
doesn't have to.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iOS Vulkan render loop was a copy of Android's from before it moved
to RunGraphicsLoop, and still had the bugs Android has since fixed:
- When InitSurface failed, the thread deleted the graphics context
through its own copy of the pointer, leaving the view controller to
call ShutdownAPI on freed memory later.
- A thread that exited by itself was never joined, since the exit
request went by renderLoopRunning, so every later start bailed with
"Already running" and the screen stayed black.
- renderLoopRunning was only set once the thread got going, so an exit
request arriving before that skipped the join.
Also join the render thread (GL or Vulkan) in shutdown, which on app
termination runs without a willResignActive first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.4.0 and older don't apply dynamic blend constants. The stock 1.4.2 build
needs iOS 15. Non-App Store builds keep their targets and dylib.
Co-Authored-By: Claude Opus 5.5 <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>
- AudioEngine.mm/.h: not in the source list at all, only in the ARC
set_source_files_properties list, and written for manual retain/release
([super dealloc]), so it couldn't have compiled under ARC anyway. The actual
audio path is iOSCoreAudio.mm.
- DisplayManager.mm: empty file, referenced nowhere.
- iOSCoreAudioSetDisplayConnected: never called, so g_displayConnected was
always false and the external-display branch in UpdateSession unreachable.
Also dropped an unused local there.
- AppDelegate: the "screen" property was never assigned, so both view
controllers were setting their frame from [nil bounds], i.e. CGRectZero. The
window sizes the root view anyway. Also removed the stale launchPPSSPP:argv:
declaration (it lives on SceneDelegate now), a forward declaration of a
PPSSPPViewController protocol that doesn't exist, and an unreachable return.
- getView: declared on the base class and implemented identically in both
subclasses, called by nobody.
- bindDefaultFBO: moved the empty implementation to the base class so it isn't
missing there, and dropped the Metal no-op override.
- The unused delta/lastTimestamp computation in displayLinkFired.
Also fixed the viewWillAppear/viewWillDisappear log messages in the GL view
controller, which claimed to resume and pause the display link but don't.
The getView and bindDefaultFBO changes clear three -Wincomplete-implementation
warnings, and the AppDelegate.h one clears a fourth.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc
updatedTransactions posted success/failure unconditionally, but StoreKit also
delivers transactions we didn't initiate - unfinished ones at startup, for
instance - when no request is pending. Request IDs start at 10, so that just
produced "Unexpected request ID 0" errors. Guard it like the restore-finished
handler already does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc
NSStringEncodingConversionAllowLossy is a conversion option, not an encoding.
Its value is 1, which as an NSStringEncoding means NSASCIIStringEncoding, so any
URL with non-ASCII characters produced a nil string and a nil NSURL. Use UTF-8,
and bail out with a log instead of calling openURL with nil.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc
viewDidLoad deleted the graphics context on InitAPI failure but left the member
pointing at the freed object, and then kept going - runVulkanRenderLoop and
shutdown would both use it. Null it out instead; both already check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc
prefersStatusBarHidden was dead code - it computed an orientation and a
(commented out) user preference, then unconditionally returned false. So the
status bar was only ever hidden on iPhone in landscape, and only because iOS
does that on its own in compact height.
Now it honors bImmersiveMode from the DisplayLayoutConfig matching the current
orientation, so it also applies in portrait and on iPad. Adds the corresponding
checkbox to the iOS system settings, and updates the status bar on rotation and
when the setting is toggled.
Also fixes a missing break in the ROTATE_UPDATED case in System_Notify, and a
static/non-static mismatch on sceKernelLoadModuleBufferUsbWlan that broke the
build (the header intentionally exposes it for sceVshBridge).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc