From 8bfc151395f071d6045bcdd195bb7063ce53ac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:39:29 +0200 Subject: [PATCH 01/18] iOS: Hide the status bar when immersive mode is enabled 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 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- Core/HLE/sceKernelModule.cpp | 2 +- UI/GameSettingsScreen.cpp | 8 +++++++ assets/lang/en_US.ini | 1 + ios/ViewControllerCommon.h | 1 + ios/ViewControllerCommon.mm | 41 ++++++++++++++++++++++-------------- ios/main.mm | 8 +++++++ 6 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 4ef998c915..3b0212eb64 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2539,7 +2539,7 @@ static u32 sceKernelLoadModuleDNAS(const char *name, u32 flags) } // Pretty sure this is a badly brute-forced function name... -static SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr) +SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr) { if (flags != 0) { WARN_LOG_REPORT(Log::Loader, "sceKernelLoadModuleBufferUsbWlan: unsupported flags: %08x", flags); diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 1787301c2e..e0b978f4c2 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -1165,6 +1165,14 @@ void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) { switchMode->OnChoice.Add([](EventParams &e) { System_Notify(SystemNotification::APP_SWITCH_MODE_CHANGED); }); + + { + // Note: On iPhone, iOS hides the status bar in landscape no matter what this is set to. + DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(GetDeviceOrientation()); + systemSettings->Add(new CheckBox(&config.bImmersiveMode, sy->T("Hide status bar")))->OnClick.Add([](EventParams &e) { + System_Notify(SystemNotification::IMMERSIVE_MODE_CHANGE); + }); + } #endif #if PPSSPP_PLATFORM(ANDROID) diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 109d0d8a35..2f7d45b472 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -1424,6 +1424,7 @@ Default tab = Default tab Dynarec/JIT (recommended) = Dynarec/JIT (recommended) Final processed image = Final processed image Hide navigation bar = Hide navigation bar +Hide status bar = Hide status bar I/O timing method = I/O timing method JIT using IR = JIT using IR Loaded plugin: %1 = Loaded plugin: %1 diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index 5ba55cd34d..77427673a6 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -26,6 +26,7 @@ - (void)startVideo:(int)width height:(int)height; - (void)stopVideo; - (void)appSwitchModeChanged; +- (void)immersiveModeChanged; - (void)setupController:(GCController *)controller; // Forwarded from the AppDelegate diff --git a/ios/ViewControllerCommon.mm b/ios/ViewControllerCommon.mm index 4964db869a..855f17de51 100644 --- a/ios/ViewControllerCommon.mm +++ b/ios/ViewControllerCommon.mm @@ -481,26 +481,24 @@ extern float g_safeInsetBottom; #endif #pragma mark - Status Bar Control -// iOS calls this to determine whether to hide the status bar -- (BOOL)prefersStatusBarHidden { - UIInterfaceOrientation orientation; - +// The immersive mode setting is per-orientation, so we need to know which way we're facing. +// Can't just use g_display for this, since it lags behind during rotation. +// Note: Using viewIfLoaded, since this can get called before the view exists, and we don't want to force it into existence. +- (DeviceOrientation)currentDeviceOrientation { if (@available(iOS 13.0, *)) { - UIWindowScene *scene = self.view.window.windowScene; + UIWindowScene *scene = self.viewIfLoaded.window.windowScene; if (scene != nil) { - orientation = scene.interfaceOrientation; - } else { - orientation = UIApplication.sharedApplication.statusBarOrientation; + return UIInterfaceOrientationIsPortrait(scene.interfaceOrientation) ? DeviceOrientation::Portrait : DeviceOrientation::Landscape; } - } else { - orientation = UIApplication.sharedApplication.statusBarOrientation; } + CGSize size = self.viewIfLoaded.bounds.size; + return size.height > size.width ? DeviceOrientation::Portrait : DeviceOrientation::Landscape; +} - BOOL isLandscape = UIInterfaceOrientationIsLandscape(orientation); - - bool userWantsStatusBar = true; // g_Config.bShowStatusBar; - // return isLandscape || !userWantsStatusBar; - return false; +// iOS calls this to determine whether to hide the status bar. +// Note that on iPhone, iOS hides it in landscape regardless of what we return here. +- (BOOL)prefersStatusBarHidden { + return g_Config.GetDisplayLayoutConfig([self currentDeviceOrientation]).bImmersiveMode ? YES : NO; } // Optional: choose light/dark text for the status bar @@ -508,11 +506,22 @@ extern float g_safeInsetBottom; return UIStatusBarStyleLightContent; } -// This should also be called when the user preference changes. - (void)onOrientationChanged { [self setNeedsStatusBarAppearanceUpdate]; } +// Called from the C++ side when the user toggles the immersive mode setting. +- (void)immersiveModeChanged { + [self setNeedsStatusBarAppearanceUpdate]; +} + +- (void)viewWillTransitionToSize:(CGSize)size + withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + // The immersive mode setting is per-orientation, so the status bar may need to change along with the rotation. + [self setNeedsStatusBarAppearanceUpdate]; +} + - (void)updateResolutionWithView:(UIView *)view { // 1. Get the scale from the window scene (safest for Metal) CGFloat scale = 1.0; diff --git a/ios/main.mm b/ios/main.mm index 989e50f5d3..d1dc5f2795 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -432,6 +432,13 @@ void System_Notify(SystemNotification notification) { } }); break; + case SystemNotification::IMMERSIVE_MODE_CHANGE: + dispatch_async(dispatch_get_main_queue(), ^{ + if (sharedViewController) { + [sharedViewController immersiveModeChanged]; + } + }); + break; case SystemNotification::UI_STATE_CHANGED: dispatch_async(dispatch_get_main_queue(), ^{ if (sharedViewController) { @@ -452,6 +459,7 @@ void System_Notify(SystemNotification notification) { [UIViewController attemptRotationToDeviceOrientation]; } }); + break; default: break; } From 6aad668a38502f9aa7624a8d1936ced5f0e6ccd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:45:42 +0200 Subject: [PATCH 02/18] iOS: Fix always-true condition in the gsEvent key decoder "else if (GSEVENT_TYPE_KEYDOWN)" tests a constant, so every gsEvent that wasn't a key up - modifier events (type 12) in particular - was delivered to NativeKey as a key down. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/PPSSPPUIApplication.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/PPSSPPUIApplication.mm b/ios/PPSSPPUIApplication.mm index 759adce34d..decb39c617 100644 --- a/ios/PPSSPPUIApplication.mm +++ b/ios/PPSSPPUIApplication.mm @@ -81,7 +81,7 @@ key.keyCode = getSmartKeyboardMap((int)eventScanCode); key.deviceId = DEVICE_ID_KEYBOARD; NativeKey(key); - } else if (GSEVENT_TYPE_KEYDOWN) { + } else if (eventType == GSEVENT_TYPE_KEYDOWN) { struct KeyInput key; key.flags = KeyInputFlags::DOWN; key.keyCode = getSmartKeyboardMap((int)eventScanCode); From 89f7094513d2d0228d2f19fb1436f356466b4e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:46:29 +0200 Subject: [PATCH 03/18] iOS: Fix use-after-free when Vulkan init fails 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 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/ViewControllerMetal.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 62645f52d2..671916907d 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -206,7 +206,9 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met g_Config.iGPUBackend = (int)GPUBackend::OPENGL; SetGPUBackend(GPUBackend::OPENGL); delete graphicsContext; - // TODO: What to do here? + graphicsContext = nullptr; // The render loop and shutdown check for this. + // TODO: What to do here? We've switched the config over to GL, but we're still the Metal view controller, + // so we won't render anything until the app gets restarted. } [self updateResolutionWithView:self.view]; From bc2a5c7f3b6aee06d0009264b10b1ca50336dbd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:46:40 +0200 Subject: [PATCH 04/18] iOS: Fix per-frame leak in the camera capture path imageFromSampleBuffer released the input context but not the output one, leaking a full-size bitmap context for every captured camera frame. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/CameraHelper.mm | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/CameraHelper.mm b/ios/CameraHelper.mm index ffdb282365..8407b04df6 100644 --- a/ios/CameraHelper.mm +++ b/ios/CameraHelper.mm @@ -145,6 +145,7 @@ NSString *getSelectedCamera() { CGContextDrawImage(outContext, outRect, inImage); CGImageRelease(inImage); CGImageRef outImage = CGBitmapContextCreateImage(outContext); + CGContextRelease(outContext); CGColorSpaceRelease(colorSpace); CVPixelBufferUnlockBaseAddress(imageBuffer, 0); From 8de3d9b8a0a2c3924b94e67b0bb008a90f1a80ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:46:49 +0200 Subject: [PATCH 05/18] iOS: Don't fall off the end of checkPermission The switch covered every AVAuthorizationStatus known today, so an unhandled one would return garbage from a non-void function. Treat unknown statuses as denied. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/CameraHelper.mm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/CameraHelper.mm b/ios/CameraHelper.mm index 8407b04df6..5db050bf9a 100644 --- a/ios/CameraHelper.mm +++ b/ios/CameraHelper.mm @@ -53,6 +53,10 @@ NSString *getSelectedCamera() { case AVAuthorizationStatusAuthorized: { return 0; } + default: { + NSLog(@"unknown camera authorization status %ld, treating as denied", (long)status); + return 1; + } } } From 99624b7fd402ba777fda1cc2792a660c03a5ae18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:47:01 +0200 Subject: [PATCH 06/18] iOS: Fix string encoding used when launching URLs 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 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/main.mm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ios/main.mm b/ios/main.mm index d1dc5f2795..51c646b507 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -604,7 +604,11 @@ void System_AskForPermission(SystemPermission permission) {} void System_LaunchUrl(LaunchUrlType urlType, std::string_view url) { std::string strUrl(url); - NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithCString:strUrl.c_str() encoding:NSStringEncodingConversionAllowLossy]]; + NSURL *nsUrl = [NSURL URLWithString:[NSString stringWithUTF8String:strUrl.c_str()]]; + if (!nsUrl) { + ERROR_LOG(Log::System, "Failed to parse URL: %s", strUrl.c_str()); + return; + } dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] openURL:nsUrl options:@{} completionHandler:nil]; }); From 11a402df6f5dfa53b61578961da15d0b22e0c7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:47:09 +0200 Subject: [PATCH 07/18] iOS: Call [super init] in LocationHelper's initializer Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/LocationHelper.mm | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ios/LocationHelper.mm b/ios/LocationHelper.mm index 237a9e12fe..b8623d70b7 100644 --- a/ios/LocationHelper.mm +++ b/ios/LocationHelper.mm @@ -7,8 +7,11 @@ -(id) init { NSLog(@"LocationHelper::init"); - locationManager = [[CLLocationManager alloc] init]; - [locationManager setDelegate:self]; + self = [super init]; + if (self) { + locationManager = [[CLLocationManager alloc] init]; + [locationManager setDelegate:self]; + } return self; } From 6548a4f5cf78ac8f4226dc6557f34012a1a38bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:47:25 +0200 Subject: [PATCH 08/18] iOS: Move accelerometer queue setup inside the init null check Harmless today since messaging nil is a no-op, but it doesn't belong outside the "if (self)" block. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/ViewControllerCommon.mm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/ViewControllerCommon.mm b/ios/ViewControllerCommon.mm index 855f17de51..f32fdf7d64 100644 --- a/ios/ViewControllerCommon.mm +++ b/ios/ViewControllerCommon.mm @@ -76,11 +76,11 @@ static int GetPickerRequestId(id picker) { selector:@selector(onOrientationChanged) name:UIDeviceOrientationDidChangeNotification object:nil]; - } - self.accelerometerQueue = [[NSOperationQueue alloc] init]; - self.accelerometerQueue.name = @"AccelerometerQueue"; - self.accelerometerQueue.maxConcurrentOperationCount = 1; + self.accelerometerQueue = [[NSOperationQueue alloc] init]; + self.accelerometerQueue.name = @"AccelerometerQueue"; + self.accelerometerQueue.maxConcurrentOperationCount = 1; + } return self; } From 56affb3cb952666da4b5be140940901b2c1f876d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:47:51 +0200 Subject: [PATCH 09/18] iOS: Don't resolve request ID 0 from the payment queue 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 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/IAPManager.mm | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ios/IAPManager.mm b/ios/IAPManager.mm index e48a6025e0..a3b0e587bc 100644 --- a/ios/IAPManager.mm +++ b/ios/IAPManager.mm @@ -134,15 +134,20 @@ // Perform the unlock (updaing the variable and switching the icon). [self unlockGold]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - g_requestManager.PostSystemSuccess(_pendingRequestID, "", 0); - _pendingRequestID = 0; + // The queue also delivers transactions we didn't ask for, like unfinished ones at startup. + if (_pendingRequestID != 0) { + g_requestManager.PostSystemSuccess(_pendingRequestID, "", 0); + _pendingRequestID = 0; + } break; case SKPaymentTransactionStateFailed: NSLog(@"[IAPManager] Purchase failed (requestID: %d): %@", _pendingRequestID, transaction.error.localizedDescription); [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; // Optionally post failure callback here - g_requestManager.PostSystemFailure(_pendingRequestID); - _pendingRequestID = 0; + if (_pendingRequestID != 0) { + g_requestManager.PostSystemFailure(_pendingRequestID); + _pendingRequestID = 0; + } break; default: break; From d3012ea1db26fd25bcbf886d0a5a0be8853f4164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:48:00 +0200 Subject: [PATCH 10/18] iOS: Don't pass a non-literal format string to NSLog Benign as written since neither string contains a format specifier, but it's exactly the pattern -Wformat-security exists to catch. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- ios/IAPManager.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/IAPManager.mm b/ios/IAPManager.mm index a3b0e587bc..db8dc9f919 100644 --- a/ios/IAPManager.mm +++ b/ios/IAPManager.mm @@ -130,7 +130,7 @@ switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: case SKPaymentTransactionStateRestored: - NSLog(transaction.transactionState == SKPaymentTransactionStatePurchased ? @"IAP Purchase" : @"IAP Restore"); + NSLog(@"%@", transaction.transactionState == SKPaymentTransactionStatePurchased ? @"IAP Purchase" : @"IAP Restore"); // Perform the unlock (updaing the variable and switching the icon). [self unlockGold]; [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; From b6b8780cb9abba82d52cd89d96fd0a1982515b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 19:57:06 +0200 Subject: [PATCH 11/18] iOS: Remove some dead code - 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 Claude-Session: https://claude.ai/code/session_01JZk5y4Fzw811WJoNWZb8Sc --- CMakeLists.txt | 1 - ios/AppDelegate.h | 4 - ios/AppDelegate.mm | 2 - ios/AudioEngine.h | 13 ---- ios/AudioEngine.mm | 149 ------------------------------------ ios/DisplayManager.mm | 0 ios/ViewController.mm | 26 +------ ios/ViewControllerCommon.h | 1 - ios/ViewControllerCommon.mm | 3 + ios/ViewControllerMetal.mm | 11 --- ios/iOSCoreAudio.h | 6 +- ios/iOSCoreAudio.mm | 15 ---- 12 files changed, 7 insertions(+), 224 deletions(-) delete mode 100644 ios/AudioEngine.h delete mode 100644 ios/AudioEngine.mm delete mode 100644 ios/DisplayManager.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index 21442f5016..6e9973b96a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -976,7 +976,6 @@ elseif(IOS AND NOT LIBRETRO) ios/iCade/iCadeReaderView.m ios/main.mm ios/CameraHelper.mm - ios/AudioEngine.mm ios/LocationHelper.mm ios/Controls.mm Core/Util/DarwinFileSystemServices.mm diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index 4a08b20a37..651d2fe431 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -4,15 +4,11 @@ #include -@protocol PPSSPPViewController; - @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; -@property (strong, nonatomic) UIScreen *screen; @property (nonatomic, strong) NSDictionary *launchOptions; -- (BOOL)launchPPSSPP:(int)argc argv:(char**)argv; - (void)processFilePath:(NSString *)path; // Exports the game library to a caller app via a URL scheme callback. diff --git a/ios/AppDelegate.mm b/ios/AppDelegate.mm index beab1370e9..6af4f6e2af 100644 --- a/ios/AppDelegate.mm +++ b/ios/AppDelegate.mm @@ -248,8 +248,6 @@ static std::vector GatherGameLibrary() { default: return UIInterfaceOrientationMaskAll; } - - return UIInterfaceOrientationMaskAll; // or at least include Portrait } @end diff --git a/ios/AudioEngine.h b/ios/AudioEngine.h deleted file mode 100644 index 6613513b27..0000000000 --- a/ios/AudioEngine.h +++ /dev/null @@ -1,13 +0,0 @@ -// -// AudioEngine.h -// PPSSPP -// -// Created by rock88 on 15/03/2013. -// Copyright (c) 2013 Homebrew. All rights reserved. -// - -#import - -@interface AudioEngine : NSObject - -@end diff --git a/ios/AudioEngine.mm b/ios/AudioEngine.mm deleted file mode 100644 index 5efd7da3e0..0000000000 --- a/ios/AudioEngine.mm +++ /dev/null @@ -1,149 +0,0 @@ -// -// AudioEngine.mm -// PPSSPP -// -// Created by rock88 on 15/03/2013. -// Copyright (c) 2013 Homebrew. All rights reserved. -// - -#import "AudioEngine.h" -#import -#import -#import -#import - -#import - -static volatile BOOL done = 0; - -#define SAMPLE_SIZE 44100 -static short stream[SAMPLE_SIZE]; - -void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata); - -@interface AudioEngine () - -@property (nonatomic,assign) ALCdevice *alcDevice; -@property (nonatomic,assign) ALCcontext *alContext; -@property (nonatomic,assign) ALuint buffer; -@property (nonatomic,assign) ALuint source; - -@end - -@implementation AudioEngine -@synthesize alcDevice,alContext,buffer,source; - -- (id)init -{ - self = [super init]; - if (self) - { - [self audioInit]; - [self audioLoop]; - } - return self; -} - -- (void)dealloc -{ - [self audioShutdown]; - [super dealloc]; -} - -- (void)checkALError -{ - ALenum ErrCode; - std::string Err = "OpenAL error: "; - if ((ErrCode = alGetError()) != AL_NO_ERROR) - { - Err += (char *)alGetString(ErrCode); - printf("%s\n",Err.c_str()); - } -} - -- (void)audioInit -{ - done = 0; - alcDevice = alcOpenDevice(NULL); - - if (alcDevice) - { - NSLog(@"OpenAL device opened: %s",alcGetString(alcDevice, ALC_DEVICE_SPECIFIER)); - } - else - { - NSLog(@"WARNING: could not open OpenAL device"); - return; - } - - alContext = alcCreateContext(alcDevice, NULL); - - if (alContext) - { - alcMakeContextCurrent(alContext); - } - else - { - NSLog(@"ERROR: no OpenAL context"); - return; - } - - alGenSources(1, &source); - alGenBuffers(1, &buffer); -} - -- (void)audioShutdown -{ - done = 1; - alcMakeContextCurrent(NULL); - - if (alContext) - { - alcDestroyContext(alContext); - alContext = NULL; - } - - if (alcDevice) - { - alcCloseDevice(alcDevice); - alcDevice = NULL; - } -} - -- (bool)playing -{ - ALenum state; - alGetSourcei(source, AL_SOURCE_STATE, &state); - return (state == AL_PLAYING); -} - -- (void)audioLoop -{ - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){ - const int sampleRateHz = 44100; - while (!done) { - if (![self playing]) { - NativeMix(stream, SAMPLE_SIZE / 2, sampleRateHz, 0); - frames_ready = SAMPLE_SIZE / 2; - } else { - frames_ready = 0; - } - - if (frames_ready > 0) { - const size_t bytes_ready = frames_ready * sizeof(short) * 2; - alSourcei(source, AL_BUFFER, 0); - alBufferData(buffer, AL_FORMAT_STEREO16, stream, bytes_ready, sampleRateHz); - alSourcei(source, AL_BUFFER, buffer); - alSourcePlay(source); - - // TODO: Maybe this could get behind? - usleep((1000000 * frames_ready) / sampleRateHz); - } else { - usleep(100); - } - pthread_yield_np(); - } - }); -} - -@end diff --git a/ios/DisplayManager.mm b/ios/DisplayManager.mm deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 6383d9c08d..82cd2f2c87 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -5,7 +5,6 @@ // Modified by xSacha // Reworked by hrydgard -#import "AppDelegate.h" #import "ViewController.h" #import "iOSCoreAudio.h" @@ -56,7 +55,6 @@ PPSSPPBaseViewController *sharedViewController; @property (nonatomic, strong) EAGLContext *glContext; @property (nonatomic, strong) GLKView *glView; @property (nonatomic, strong) CADisplayLink *displayLink; -@property (nonatomic, assign) NSTimeInterval lastTimestamp; @property (nonatomic, strong) EAGLContext* context; @@ -134,10 +132,6 @@ PPSSPPBaseViewController *sharedViewController; } [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; - self.lastTimestamp = 0; - - UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen]; - self.view.frame = [screen bounds]; self.view.multipleTouchEnabled = YES; graphicsContext = new OpenGLGraphicsContext(); @@ -173,14 +167,12 @@ PPSSPPBaseViewController *sharedViewController; - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - // Resume display link unless explicitly paused - INFO_LOG(Log::G3D, "viewWillAppear - resuming display link"); + INFO_LOG(Log::G3D, "viewWillAppear"); } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; - // stop rendering while not visible - INFO_LOG(Log::G3D, "viewWillDisappear - pausing display link"); + INFO_LOG(Log::G3D, "viewWillDisappear"); } - (void)dealloc { @@ -205,16 +197,6 @@ PPSSPPBaseViewController *sharedViewController; } - (void)displayLinkFired:(CADisplayLink *)dl { - // compute delta time - NSTimeInterval timestamp = dl.timestamp; - NSTimeInterval delta = 0; - if (self.lastTimestamp > 0) { - delta = timestamp - self.lastTimestamp; - } else { - delta = dl.duration; // fallback - } - self.lastTimestamp = timestamp; - // Ensure context is current before drawing [EAGLContext setCurrentContext:self.glContext]; @@ -286,10 +268,6 @@ PPSSPPBaseViewController *sharedViewController; [(GLKView*)self.glView bindDrawable]; } -- (UIView *)getView { - return [self view]; -} - // Can't consolidate this yet. - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { diff --git a/ios/ViewControllerCommon.h b/ios/ViewControllerCommon.h index 77427673a6..c42e735271 100644 --- a/ios/ViewControllerCommon.h +++ b/ios/ViewControllerCommon.h @@ -20,7 +20,6 @@ - (void)shareText:(NSString *)text; - (void)shutdown; - (void)bindDefaultFBO; -- (UIView *)getView; - (void)startLocation; - (void)stopLocation; - (void)startVideo:(int)width height:(int)height; diff --git a/ios/ViewControllerCommon.mm b/ios/ViewControllerCommon.mm index f32fdf7d64..d5f34e7d3b 100644 --- a/ios/ViewControllerCommon.mm +++ b/ios/ViewControllerCommon.mm @@ -84,6 +84,9 @@ static int GetPickerRequestId(id picker) { return self; } +// Only means anything for the GL backend, which overrides this. +- (void)bindDefaultFBO {} + - (void)shutdown { self.gameController = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/ios/ViewControllerMetal.mm b/ios/ViewControllerMetal.mm index 671916907d..bf634c197f 100644 --- a/ios/ViewControllerMetal.mm +++ b/ios/ViewControllerMetal.mm @@ -1,4 +1,3 @@ -#import "AppDelegate.h" #import "ViewControllerMetal.h" #import "iOSCoreAudio.h" @@ -193,8 +192,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::System, "Metal viewDidLoad"); - UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen]; - self.view.frame = [screen bounds]; self.view.multipleTouchEnabled = YES; // self.view.insetsLayoutMarginsFromSafeArea = NO; // self.view.clipsToBounds = YES; @@ -220,10 +217,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::G3D, "Detected size: %dx%d", g_display.pixel_xres, g_display.pixel_yres); } -- (UIView *)getView { - return [self view]; -} - - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; INFO_LOG(Log::G3D, "viewWillAppear"); @@ -247,10 +240,6 @@ static void VulkanRenderLoop(GraphicsContext *graphicsContext, CAMetalLayer *met INFO_LOG(Log::G3D, "viewDidDisappear"); } -- (void)bindDefaultFBO { - // Do nothing -} - - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; diff --git a/ios/iOSCoreAudio.h b/ios/iOSCoreAudio.h index 944e0e81dc..62cfa6e186 100644 --- a/ios/iOSCoreAudio.h +++ b/ios/iOSCoreAudio.h @@ -21,7 +21,5 @@ void iOSCoreAudioInit(); void iOSCoreAudioShutdown(); -// Ignore mute switch when connected to external display. -// Also, obey other settings. -void iOSCoreAudioUpdateSession(); -void iOSCoreAudioSetDisplayConnected(bool connected); \ No newline at end of file +// Applies the audio settings (mix with others, respect silent mode) to the session. +void iOSCoreAudioUpdateSession(); \ No newline at end of file diff --git a/ios/iOSCoreAudio.mm b/ios/iOSCoreAudio.mm index e8f7619583..9d8332db7a 100644 --- a/ios/iOSCoreAudio.mm +++ b/ios/iOSCoreAudio.mm @@ -29,18 +29,9 @@ #define SAMPLE_RATE 44100 static AudioComponentInstance audioInstance = nil; -static bool g_displayConnected = false; void iOSCoreAudioUpdateSession() { NSError *error = nil; - if (g_displayConnected) { - INFO_LOG(Log::Audio, "Display connected, setting Playback mode"); - // Special handling when a display is connected. Always exclusive. - // Let's revisit this later. - [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; - return; - } - INFO_LOG(Log::Audio, "RespectSilentMode: %d MixWithOthers: %d", g_Config.bAudioRespectSilentMode, g_Config.bAudioMixWithOthers); // Hacky hack to force iOS to re-evaluate. @@ -48,7 +39,6 @@ void iOSCoreAudioUpdateSession() { [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAudioProcessing error:&error]; // Here, we apply the settings. - const bool mixWithOthers = g_Config.bAudioMixWithOthers; if (g_Config.bAudioMixWithOthers) { if (g_Config.bAudioRespectSilentMode) { [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error]; @@ -69,11 +59,6 @@ void iOSCoreAudioUpdateSession() { } } -void iOSCoreAudioSetDisplayConnected(bool connected) { - g_displayConnected = connected; - iOSCoreAudioUpdateSession(); -} - void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata); OSStatus iOSCoreAudioCallback(void *inRefCon, From e4b90c0471b9670a89d4ebd2b7c3085edbbb3d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 6 Aug 2026 23:17:24 +0200 Subject: [PATCH 12/18] Fix issue with chat button / DevMenu button becoming focusable This made gameplay a bit difficult when these buttons were enabled. --- Common/UI/Root.cpp | 8 +++++--- Common/UI/Root.h | 2 +- Common/UI/UIScreen.cpp | 2 +- Common/UI/UIScreen.h | 1 + UI/EmuScreen.cpp | 8 ++++++++ UI/EmuScreen.h | 2 ++ 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Common/UI/Root.cpp b/Common/UI/Root.cpp index dfb775eb95..564381e87c 100644 --- a/Common/UI/Root.cpp +++ b/Common/UI/Root.cpp @@ -124,6 +124,8 @@ void EnableFocusMovement(bool enable) { focusMoves.clear(); heldKeys.clear(); focusedView = nullptr; + } else { + enable = enable; } } @@ -361,7 +363,7 @@ restart: } } -DialogResult UpdateViewHierarchy(ViewGroup *root) { +DialogResult UpdateViewHierarchy(ViewGroup *root, bool canEnableFocusMovement) { ProcessHeldKeys(root); frameCount++; @@ -370,7 +372,7 @@ DialogResult UpdateViewHierarchy(ViewGroup *root) { return DR_NONE; } - if (focusMoves.size()) { + if (focusMoves.size() && canEnableFocusMovement) { EnableFocusMovement(true); if (!GetFocusedView()) { // Find a view to focus. @@ -397,8 +399,8 @@ DialogResult UpdateViewHierarchy(ViewGroup *root) { } } } - focusMoves.clear(); } + focusMoves.clear(); root->Update(); return DispatchEvents(); diff --git a/Common/UI/Root.h b/Common/UI/Root.h index 861941f6b7..e5d341503a 100644 --- a/Common/UI/Root.h +++ b/Common/UI/Root.h @@ -26,7 +26,7 @@ DialogResult DispatchEvents(); class ViewGroup; void LayoutViewHierarchy(const UIContext &dc, const UI::Margins &rootMargins, UI::ViewGroup *root, ViewLayoutMode layoutMode, bool immersiveMode); -DialogResult UpdateViewHierarchy(ViewGroup *root); +DialogResult UpdateViewHierarchy(ViewGroup *root, bool canEnableFocusMovement = true); enum class KeyEventResult { IGNORE_KEY, // Don't let it be processed. diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index ed723aba67..97d3b26d7c 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -104,7 +104,7 @@ void UIScreen::update() { DoRecreateViews(); if (root_) { - DialogResult result = UpdateViewHierarchy(root_); + DialogResult result = UpdateViewHierarchy(root_, AllowFocusMovement()); if (result != DR_NONE) { TriggerFinish(result); } diff --git a/Common/UI/UIScreen.h b/Common/UI/UIScreen.h index 7202455124..149b05068c 100644 --- a/Common/UI/UIScreen.h +++ b/Common/UI/UIScreen.h @@ -50,6 +50,7 @@ public: protected: virtual void CreateViews() = 0; + virtual bool AllowFocusMovement() const { return true; } Bounds GetLayoutBounds(UIContext &dc) const; diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index e1639f4d1f..004a858d3d 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1738,6 +1738,14 @@ ScreenRenderFlags EmuScreen::render(ScreenRenderMode mode) { return screenRenderFlags; } +bool EmuScreen::AllowFocusMovement() const { + if (chatMenu_ && chatMenu_->GetVisibility() == UI::V_VISIBLE) { + return true; + } + // We have just a game controller and possibly a chat and debug button. Don't allow focus movement. + return false; +} + ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { using namespace Draw; ScreenRenderFlags flags = ScreenRenderFlags::NONE; diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 68fb0b13eb..8ca0a385ac 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -77,6 +77,8 @@ protected: void SetPSPAnalog(int rotation, int stick, float x, float y) override; ViewLayoutMode LayoutMode() const override; + bool AllowFocusMovement() const override; + private: void CreateViews() override; ScreenRenderFlags RunEmulation(bool skipBufferEffects); From 510cfb421c976d11c227112c92b168499af7c9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 25 Jul 2026 12:35:11 +0200 Subject: [PATCH 13/18] Extend LoadExecForKernel with real VSH loadexec/exit syscalls Adds SceKernelLoadExecVSHParam (matching JPCSP's reference layout - its first four fields line up with the existing SceKernelLoadExecParam, which is why the plain sceKernelLoadExec already worked for sceKernelLoadExecVSHMs2) and fills in the rest of LoadExecForKernel's NIDs from JPCSP: real implementations for sceKernelExitVSHVSH/Kernel (mirrors sceKernelExitGame) and sceKernelLoadExecBufferVSHUsbWlan (loads an exec from an in-RAM buffer instead of a file - the VSH's "push a game over USB/WLAN" path), plus UNIMPL stubs for everything JPCSP itself only knows by NID. sceKernelLoadExecBufferVSHUsbWlan needed __KernelLoadExec split into a file-reading front end and a shared __KernelLoadExecFromPtr back end that both it and the new buffer-based path call into - a pure extract-method refactor of the single most heavily used boot path in the emulator. Verified no regression: same 11 passed / 9 pre-existing-failed split on pspautotests/tests/cpu/*, and loader/bss still passes, before and after this change. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU --- AGENTS.md | 5 +++ Core/HLE/sceKernel.cpp | 87 +++++++++++++++++++++++++++++++++--- Core/HLE/sceKernel.h | 22 +++++++++ Core/HLE/sceKernelModule.cpp | 36 +++++++++------ Core/HLE/sceKernelModule.h | 4 ++ 5 files changed, 135 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d16382c510..396781f54e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,6 +163,11 @@ small examples to copy from). A module is a `const HLEFunction []` table o build-tested here, so double check them by hand against how an existing neighboring file (e.g. `sceVaudio.cpp`) is listed in each. Note: New files in the unittest project have to be updated in the unittest part in android/jni/Android.mk. +## Adding HLE functions + +Always add new functions at the *end* of an array of const HLEFunction. We store the function index in savestates +(in resolved syscall opcodes), so inserting a new function in the middle of an existing array will break things. + ## WebSocket debugger PPSSPP has a JSON/WebSocket debugger and automation API (connect, read/write memory, search memory for values or byte diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 85df5f83ef..e36c32b415 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -968,15 +968,92 @@ void Register_LoadExecForUser() RegisterHLEModule("LoadExecForUser", ARRAY_SIZE(LoadExecForUser), LoadExecForUser); } -const HLEFunction LoadExecForKernel[] = -{ +// sceKernelExitVSHVSH and sceKernelExitVSHKernel (two NIDs each, across firmware versions) are +// the VSH's own way of tearing itself down - when called from a regular game/homebrew context +// (which can happen, since these live in a kernel-mode module games can still reach) they have +// the same observable effect as sceKernelExitGame. See SceKernelLoadExecVSHParam in sceKernel.h. +static int sceKernelExitVSH(u32 paramPtr) { + if (Memory::IsValidRange(paramPtr, sizeof(SceKernelLoadExecVSHParam))) { + auto param = PSPPointer::Create(paramPtr); + INFO_LOG(Log::sceKernel, "sceKernelExitVSH: size=%d, args=%08x, argp=%08x, flags=%08x", param->size, param->args, param->argp, param->flags); + } + + INFO_LOG(Log::sceKernel, "sceKernelExitVSH"); + __KernelSwitchOffThread("VSH exited"); + Core_Stop(); + + g_OSD.Show(OSDType::MESSAGE_INFO, "sceKernelExitVSH()", 0.0f, "kernelexit"); + return hleNoLog(0); +} + +// The VSH's own version of sceKernelLoadExec, used for pushing a game to run over USB/WLAN +// (e.g. from a PC) rather than loading it from a file already on the memory stick/UMD. +static int sceKernelLoadExecBufferVSHUsbWlan(int bufferSize, u32 bufferAddr, u32 paramPtr) { + if (!Memory::IsValidRange(bufferAddr, bufferSize)) { + return hleLogError(Log::sceKernel, SCE_KERNEL_ERROR_ILLEGAL_ADDR, "invalid buffer"); + } + + const u8 *data = Memory::GetPointerUnchecked(bufferAddr); + std::string error_string; + if (!__KernelLoadExecFromBuffer(data, (size_t)bufferSize, paramPtr, &error_string)) { + Core_UpdateState(CORE_RUNTIME_ERROR); + return hleLogError(Log::sceKernel, -1, "failed: %s", error_string.c_str()); + } + if (gpu) { + gpu->Reinitialize(); + } + return hleLogInfo(Log::sceKernel, 0); +} + +const HLEFunction LoadExecForKernel[] = { {0x4AC57943, &WrapI_I, "sceKernelRegisterExitCallback", 'i', "i", HLE_KERNEL_SYSCALL }, - {0XA3D5E142, nullptr, "sceKernelExitVSHVSH", '?', "" }, + {0XA3D5E142, &WrapI_U, "sceKernelExitVSHVSH", 'i', "x", HLE_KERNEL_SYSCALL }, {0X28D0D249, &WrapI_CU, "sceKernelLoadExecVSHMs2", 'i', "sx" }, - {0x6D302D3D, &WrapV_V, "sceKernelExitVSHKernel", 'v', "x", HLE_KERNEL_SYSCALL },// when called in game mode it will have the same effect that sceKernelExitGame + {0x6D302D3D, &WrapI_U, "sceKernelExitVSHKernel", 'i', "x", HLE_KERNEL_SYSCALL },// when called in game mode it will have the same effect that sceKernelExitGame {0x05572A5F, &WrapV_V, "sceKernelExitGame", 'v', "", HLE_KERNEL_SYSCALL }, + {0X08F7166C, &WrapI_U, "sceKernelExitVSHVSH", 'i', "x", HLE_KERNEL_SYSCALL }, + {0XD940C83C, &WrapI_CU, "sceKernelLoadExecVSHMs2", 'i', "sx" }, + {0XF9CFCF2F, &WrapI_CU, "sceKernelLoadExec_F9CFCF2F", 'i', "sx" }, + {0XD8320A28, &WrapI_CU, "sceKernelLoadExecVSHDisc", 'i', "sx" }, + {0XC3474C2A, &WrapI_U, "sceKernelExitVSHKernel", 'i', "x", HLE_KERNEL_SYSCALL }, + {0XBEF585EC, &WrapI_IUU, "sceKernelLoadExecBufferVSHUsbWlan", 'i', "ixx" }, + // Everything below here is only known by NID - even JPCSP, which is further along in VSH + // support, only knows them by name/NID (or not even that) and stubs them all out. + {0X11412288, nullptr, "sceKernelLoadExec_11412288", '?', "" }, + {0XA5ECA6E3, nullptr, "sceKernelLoadExec_11412288", '?', "" }, + {0X00745486, nullptr, "sceKernelLoadExecVSHMs4", '?', "" }, + {0X4FB44D27, nullptr, "sceKernelLoadExecVSHMs1", '?', "" }, + {0XCC6A47D2, nullptr, "sceKernelLoadExecVSHMs3", '?', "" }, + {0X7CABED9B, nullptr, "sceKernelLoadExecVSHMs5", '?', "" }, + {0X1B305B09, nullptr, "sceKernelLoadExecVSHDiscDebug", '?', "" }, + {0XD4B49C4B, nullptr, "sceKernelLoadExecVSHDiscUpdater", '?', "" }, + {0X2B8813AF, nullptr, "sceKernelLoadExecBufferVSHUsbWlanDebug", '?', "" }, + {0X1F08547A, nullptr, "sceKernelInvokeExitCallback", '?', "" }, + {0X1F88A490, nullptr, "sceKernelRegisterExitCallback", '?', "" }, + {0X24114598, nullptr, "sceKernelUnregisterExitCallback", '?', "" }, + {0XB57D0DEC, nullptr, "sceKernelCheckExitCallback", '?', "" }, + {0X032A7938, nullptr, "LoadExecForKernel_032A7938", '?', "" }, + {0X077BA314, nullptr, "LoadExecForKernel_077BA314", '?', "" }, + {0X16A68007, nullptr, "LoadExecForKernel_16A68007", '?', "" }, + {0X1B8AB02E, nullptr, "LoadExecForKernel_1B8AB02E", '?', "" }, + {0X40564748, nullptr, "LoadExecForKernel_40564748", '?', "" }, + {0X47A5A49C, nullptr, "LoadExecForKernel_47A5A49C", '?', "" }, + {0X7CAFE77F, nullptr, "LoadExecForKernel_7CAFE77F", '?', "" }, + {0X87C3589C, nullptr, "LoadExecForKernel_87C3589C", '?', "" }, + {0X8C4679D3, nullptr, "LoadExecForKernel_8C4679D3", '?', "" }, + {0X9BD32619, nullptr, "LoadExecForKernel_9BD32619", '?', "" }, + {0XA6658F10, nullptr, "LoadExecForKernel_A6658F10", '?', "" }, + {0XB343FDAB, nullptr, "LoadExecForKernel_B343FDAB", '?', "" }, + {0XBC26BEEF, nullptr, "LoadExecForKernel_BC26BEEF", '?', "" }, + {0XC11E6DF1, nullptr, "LoadExecForKernel_C11E6DF1", '?', "" }, + {0XC540E3B3, nullptr, "LoadExecForKernel_C540E3B3", '?', "" }, + {0XC7C83B1E, nullptr, "LoadExecForKernel_C7C83B1E", '?', "" }, + {0XDBD0CF1B, nullptr, "LoadExecForKernel_DBD0CF1B", '?', "" }, + {0XE1972A24, nullptr, "LoadExecForKernel_E1972A24", '?', "" }, + {0XE704ECC3, nullptr, "LoadExecForKernel_E704ECC3", '?', "" }, + {0XAE9EFC0D, nullptr, "LoadExecForKernel_AE9EFC0D", '?', "" }, }; - + void Register_LoadExecForKernel() { RegisterHLEModule("LoadExecForKernel", ARRAY_SIZE(LoadExecForKernel), LoadExecForKernel); diff --git a/Core/HLE/sceKernel.h b/Core/HLE/sceKernel.h index b1d79f9476..c1a16c3ecb 100644 --- a/Core/HLE/sceKernel.h +++ b/Core/HLE/sceKernel.h @@ -82,6 +82,28 @@ struct SceKernelLoadExecParam u32_le keyp; // Encryption key? Not yet used }; +// Used by the VSH's own LoadExec variants (sceKernelLoadExecVSHMs2 and friends, see +// LoadExecForKernel in sceKernel.cpp) instead of SceKernelLoadExecParam above. The first four +// fields share the same layout as SceKernelLoadExecParam, which is why the plain +// sceKernelLoadExec happens to work fine even when handed one of these. Fields past "flags" +// are only present if size >= 0x30 - see JPCSP's SceKernelLoadExecVSHParam for the reference +// this was ported from. +struct SceKernelLoadExecVSHParam +{ + SceSize_le size; // Size of the structure + SceSize_le args; // Size of the arg string + u32_le argp; // Pointer to the arg string + u32_le keyp; // Pointer to the encryption key string + SceSize_le vshmainArgsSize; // Size of the vshmain-specific arg buffer + u32_le vshmainArgs; // Pointer to the vshmain-specific arg buffer + u32_le configFilep; // Pointer to a config file path string + u32_le unknownString; + u32_le flags; + u32_le extArgs; + u32_le extArgp; + u32_le opt11; +}; + void __KernelInit(); void __KernelShutdown(); void __KernelDoState(PointerWrap &p); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 3b0212eb64..4ad422b684 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1776,7 +1776,10 @@ void __KernelLoadReset() { __KernelInit(); } -bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string) { +// Shared by __KernelLoadExec (loading from a file) and __KernelLoadExecFromBuffer (loading +// from a buffer already in RAM, as used by the VSH's USB/WLAN game-push feature) - everything +// past the point where we have the executable's bytes in hand. +bool __KernelLoadExecFromPtr(const u8 *data, size_t size, const char *filename, u32 paramPtr, std::string *error_string) { SceKernelLoadExecParam param{}; auto paramData = PSPPointer::Create(paramPtr); @@ -1801,18 +1804,7 @@ bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_str __KernelLoadReset(); - std::vector fileData; - if (pspFileSystem.ReadEntireFile(filename, fileData) < 0) { - ERROR_LOG(Log::Loader, "Failed to load executable %s - file doesn't exist", filename); - *error_string = StringFromFormat("Could not find executable %s", filename); - delete[] param_argp; - delete[] param_key; - __KernelShutdown(); - return false; - } - - size_t size = fileData.size(); - PSPModule *module = __KernelLoadModule(fileData.data(), size, 0, filename, error_string); + PSPModule *module = __KernelLoadModule((u8 *)data, size, 0, filename, error_string); if (!module || module->isFake) { if (module) { @@ -1872,6 +1864,22 @@ bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_str return true; } +bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string) { + std::vector fileData; + if (pspFileSystem.ReadEntireFile(filename, fileData) < 0) { + ERROR_LOG(Log::Loader, "Failed to load executable %s - file doesn't exist", filename); + *error_string = StringFromFormat("Could not find executable %s", filename); + __KernelShutdown(); + return false; + } + + return __KernelLoadExecFromPtr(fileData.data(), fileData.size(), filename, paramPtr, error_string); +} + +bool __KernelLoadExecFromBuffer(const u8 *data, size_t size, u32 paramPtr, std::string *error_string) { + return __KernelLoadExecFromPtr(data, size, "vshbuffer", paramPtr, error_string); +} + bool __KernelLoadGEDump(std::string_view base_filename, std::string *error_string) { __KernelLoadReset(); @@ -2538,7 +2546,7 @@ static u32 sceKernelLoadModuleDNAS(const char *name, u32 flags) return hleNoLog(0); } -// Pretty sure this is a badly brute-forced function name... +// Suspecting this is a badly brute-forced function name... Although, it's not entirely implausible. Buffer makes sense. SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr) { if (flags != 0) { diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 2a2d39dd20..0cdcf97f31 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -235,6 +235,10 @@ u32 __KernelGetModuleGP(SceUID module); bool KernelModuleIsKernelMode(SceUID module); bool __KernelLoadGEDump(std::string_view base_filename, std::string *error_string); bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string); +// Exposed so sceVshBridge can reuse it directly for vshKernelLoadModuleBufferVSH, matching JPCSP. +SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr); +bool __KernelLoadExecFromBuffer(const u8 *data, size_t size, u32 paramPtr, std::string *error_string); +// Exposed for HLE.cpp's "Unknown syscall" diagnostic - see the definition for details. bool KernelFindImportByStubAddr(u32 stubAddr, std::string *importModuleName, u32 *nid, std::string *importingModuleName); // Describes which loaded module (and section within it) an address falls in, e.g. "EBOOT.BIN.text+1234". // Returns an empty string if the address isn't inside any currently loaded module. From 2c73aead45e256cffc34e65cc9159ee2f6b09477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 3 Aug 2026 19:18:19 +0200 Subject: [PATCH 14/18] Minor code cleanup on pause screen --- Common/UI/Screen.cpp | 12 ++++++++++++ Common/UI/Screen.h | 1 + UI/PauseScreen.cpp | 35 +++++++++++++++++------------------ UI/PauseScreen.h | 2 ++ 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/Common/UI/Screen.cpp b/Common/UI/Screen.cpp index 3b0ca9ab43..c447483e4b 100644 --- a/Common/UI/Screen.cpp +++ b/Common/UI/Screen.cpp @@ -532,3 +532,15 @@ void ScreenManager::SetBackgroundOverlayScreens(Screen *backgroundScreen, Screen overlayScreen_ = overlayScreen; overlayScreen_->setScreenManager(this); } + +const char *DialogResultToString(DialogResult result) { + switch (result) { + case DR_NONE: return "DR_NONE"; + case DR_OK: return "DR_OK"; + case DR_CANCEL: return "DR_CANCEL"; + case DR_YES: return "DR_YES"; + case DR_NO: return "DR_NO"; + case DR_BACK: return "DR_BACK"; + default: return "(N/A)"; + } +} diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h index e726037f14..347f8ae456 100644 --- a/Common/UI/Screen.h +++ b/Common/UI/Screen.h @@ -36,6 +36,7 @@ enum DialogResult { DR_NO, DR_BACK, }; +const char *DialogResultToString(DialogResult result); class ScreenManager; class UIContext; diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 0310e3f941..32b2502e8a 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -389,9 +389,7 @@ GamePauseScreen::~GamePauseScreen() { void GamePauseScreen::OnVKey(VirtKey virtualKeyCode, bool down) { // Simple de-bounce using createdTime_, just to be safe. if (down && virtualKeyCode == VIRTKEY_PAUSE && time_now_d() > createdTime_ + 0.1) { - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_BACK; + FinishNextFrame(DR_BACK); } } @@ -410,9 +408,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, int slotNum = v->GetSlot(); auto doLoad = [this, slotNum]() { SaveState::LoadSlot(saveStatePrefix_, slotNum, &ShowMessageAfterSaveStateAction); - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_CANCEL; + FinishNextFrame(DR_CANCEL); }; if (g_Config.bConfirmLoadState) { screenManager()->push(new LoadStateConfirmScreen(saveStatePrefix_, slotNum, [doLoad](bool result) { @@ -783,16 +779,12 @@ void GamePauseScreen::ShowContextMenu(UI::View *menuButton, bool portrait) { screenManager()->push(new UI::MessagePopupScreen(di->T("Reset"), confirmMessage, di->T("Reset"), di->T("Cancel"), [this](bool result) { if (result) { System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_BACK; // resume + FinishNextFrame(DR_BACK); // resume } })); } else { System_PostUIMessage(UIMessage::REQUEST_GAME_RESET); - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrameResult_ = DR_BACK; // resume - finishNextFrame_ = true; + FinishNextFrame(DR_BACK); // resume } }); auto dev = GetI18NCategory(I18NCat::DEVELOPER); @@ -827,9 +819,7 @@ void GamePauseScreen::dialogFinished(const Screen *dialog, DialogResult dr) { std::string tag = dialog->tag(); if (tag == "ScreenshotView") { if (dr == DR_OK) { - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrame_ = true; - finishNextFrameResult_ = DR_BACK; + FinishNextFrame(DR_BACK); } else if (dr != DR_CANCEL && dr != DR_BACK) { // Just go back to the pause menu, but refresh the savestate thumbnails in case something changed. SaveState::Rescan(saveStatePrefix_); @@ -904,9 +894,7 @@ void GamePauseScreen::OnExit(UI::EventParams &e) { if (g_Config.bPauseMenuExitsEmulator) { System_ExitApp(); } else { - std::lock_guard lock(finishNextFrameMutex_); - finishNextFrameResult_ = DR_OK; // exit game - finishNextFrame_ = true; + FinishNextFrame(DR_OK); // exit game } } })); @@ -962,3 +950,14 @@ void GamePauseScreen::OnDeleteConfig(UI::EventParams &e) { } })); } + +// This is a bit of a hack that we should try to remove. +void GamePauseScreen::FinishNextFrame(DialogResult finishNextFrameResult) { + std::lock_guard lock(finishNextFrameMutex_); + if (!finishNextFrame_) { + finishNextFrameResult_ = finishNextFrameResult; + finishNextFrame_ = true; + } else { + WARN_LOG(Log::UI, "Duplicate call to FinishNextFrame - we were already finishing with result %d, now trying to finish with result %d", finishNextFrameResult_, finishNextFrameResult); + } +} diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 7852b5ed01..2053ebd12b 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -66,6 +66,8 @@ private: void AddExtraOptions(UI::ViewGroup *parent); + void FinishNextFrame(DialogResult finishNextFrameResult); + // hack std::mutex finishNextFrameMutex_; bool finishNextFrame_ = false; From 798ba7f79d73ff26ccdad33cae98f99e909bb851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 8 Mar 2026 23:40:16 +0100 Subject: [PATCH 15/18] Fix some bugs in Resolve.cpp --- Common/Net/Resolve.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Common/Net/Resolve.cpp b/Common/Net/Resolve.cpp index 445df830fe..64ffdd2893 100644 --- a/Common/Net/Resolve.cpp +++ b/Common/Net/Resolve.cpp @@ -591,6 +591,23 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t return false; } +#ifndef _WIN32 + // On non-Windows, we can't use select() if fd >= FD_SETSIZE + // For DNS, just set a socket timeout instead + struct timeval timeout; + timeout.tv_sec = 5; + timeout.tv_usec = 0; + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) { + WARN_LOG(Log::sceNet, "Failed to set socket timeout for DNS query"); + } +#else + // Windows version + DWORD timeout = 5000; // 5 seconds + if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)) < 0) { + WARN_LOG(Log::sceNet, "Failed to set socket timeout for DNS query"); + } +#endif + struct sockaddr_in server_addr{}; server_addr.sin_family = AF_INET; server_addr.sin_port = htons(DNS_PORT); @@ -616,7 +633,7 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t *((uint16_t *)(qinfo + 2)) = htons(DNS_QUERY_CLASS_IN); // Query class: IN // Send DNS query - size_t query_len = sizeof(DNSHeader) + (qinfo - buffer) + 4; + size_t query_len = (qinfo + 4) - buffer; if (sendto(sockfd, (const char *)buffer, (int)query_len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ERROR_LOG(Log::Net, "Failed to send DNS query"); closesocket(sockfd); @@ -627,7 +644,7 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t socklen_t server_len = sizeof(server_addr); size_t response_len; if ((response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len)) < 0) { - ERROR_LOG(Log::Net, "Failed to receive DNS response"); + ERROR_LOG(Log::sceNet, "Failed to receive DNS response (timeout or error)"); closesocket(sockfd); return false; } From 3504be26b084c120e5f42b4231939619497cb7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 9 Mar 2026 00:17:31 +0100 Subject: [PATCH 16/18] More DNS fixes by copilot, additionally fixed up by claude --- AGENTS.md | 18 +++++++++- Common/Net/Resolve.cpp | 80 +++++++++++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 396781f54e..0073595d9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,23 @@ Ignore the folder ai_instructions in the root directory, it's old stuff from con ## Build and Validation -To verify that things build on Linux/Mac, use ./b.sh --debug. For Windows, use the Visual Studio solution in the Windows subdirectory. +Check the current platform before picking a build method - don't assume Linux/Mac just because a +`build/` directory or Makefiles happen to be present (e.g. from a prior WSL/cross build); match the +method to the OS you're actually running on. + +To verify that things build on Linux/Mac, use ./b.sh --debug. + +On Windows, ALWAYS build via MSBuild against the Visual Studio solution (`Windows/PPSSPP.sln`) - +never use `./b.sh`, `make`, `cmake --build`, or any other POSIX build tooling on Windows, even if +such a build directory exists locally. If `msbuild` isn't on PATH, locate it with vswhere first: + +```powershell +$msbuild = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe +& $msbuild "Windows\PPSSPP.sln" /t:Common /p:Configuration=Debug /p:Platform=x64 /m /nologo /v:minimal +``` + +Swap `/t:Common` for the project you actually touched (e.g. `/t:Core`, `/t:UnitTest`), or omit +`/t:` to build the whole solution. In addition to the pspautotests runner (test.py), there is a separate binary with C++ unit tests in the /unittest subdirectory. After substantial changes (at the end of a chunk of work, not diff --git a/Common/Net/Resolve.cpp b/Common/Net/Resolve.cpp index 64ffdd2893..a395dc2fa5 100644 --- a/Common/Net/Resolve.cpp +++ b/Common/Net/Resolve.cpp @@ -467,9 +467,10 @@ struct DNSHeader { // Function to convert a domain name to DNS query format // http://www.tcpipguide.com/free/t_DNSNameNotationandMessageCompressionTechnique.htm -static void encode_domain_name(const char *domain, unsigned char *encoded) { +static bool encode_domain_name(const char *domain, unsigned char *encoded, size_t max_len) { const char *pos = domain; unsigned char *ptr = encoded; + const unsigned char *end = encoded + max_len; while (*pos) { const char *start = pos; @@ -477,21 +478,36 @@ static void encode_domain_name(const char *domain, unsigned char *encoded) { pos++; } - *ptr++ = (unsigned char)(pos - start); // length field - memcpy(ptr, start, pos - start); - ptr += pos - start; + size_t label_len = pos - start; + if (label_len > 63 || ptr + label_len + 1 >= end) { + return false; // Label too long or buffer overflow + } + + *ptr++ = (unsigned char)label_len; // length field + memcpy(ptr, start, label_len); + ptr += label_len; if (*pos == '.') { pos++; } } + if (ptr >= end) { + return false; + } *ptr = 0; // End of domain name + return true; } // Function to parse and print the DNS response static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint32_t *output) { + if (response_len < sizeof(DNSHeader)) { + ERROR_LOG(Log::sceNet, "DNS response too short"); + return false; + } + DNSHeader *dns = (DNSHeader *)buffer; unsigned char *ptr = buffer + sizeof(struct DNSHeader); + unsigned char *end = buffer + response_len; DEBUG_LOG(Log::Net, "DNS Response:"); DEBUG_LOG(Log::Net, "ID: 0x%x", ntohs(dns->id)); @@ -504,10 +520,19 @@ static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint3 // Skip over the question section const int q_count = ntohs(dns->q_count); for (int i = 0; i < q_count; i++) { - while (*ptr != 0) { - ptr += (*ptr) + 1; + while (ptr < end && *ptr != 0) { + int jump = *ptr; + ptr += jump + 1; + if (ptr >= end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (question section)"); + return false; + } } ptr += 5; // Null byte + QTYPE (2 bytes) + QCLASS (2 bytes) + if (ptr > end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (question section end)"); + return false; + } } *output = 0; @@ -518,13 +543,34 @@ static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint3 DEBUG_LOG(Log::Net, "Answer %d:\n", i + 1); // Skip the name (can be a pointer or a sequence) + if (ptr >= end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (answer %d name)", i); + return false; + } + if ((*ptr & 0xC0) == 0xC0) { + if (ptr + 2 > end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (answer %d name pointer)", i); + return false; + } ptr += 2; // Pointer (2 bytes) } else { - while (*ptr != 0) ptr += (*ptr) + 1; + while (ptr < end && *ptr != 0) { + int jump = *ptr; + ptr += jump + 1; + if (ptr >= end) { + ERROR_LOG(Log::sceNet, "DNS response malformed (answer %d name loop)", i); + return false; + } + } ptr++; } + if (ptr + 10 > end) { + ERROR_LOG(Log::sceNet, "DNS response too short for answer %d header", i); + return false; + } + // TODO: Use a struct or something. uint16_t type = ntohs(*((uint16_t *)ptr)); ptr += 2; @@ -540,6 +586,11 @@ static bool parse_dns_response(unsigned char *buffer, size_t response_len, uint3 DEBUG_LOG(Log::Net, " TTL: %u", ttl); DEBUG_LOG(Log::Net, " Data length: %d", (int)data_len); + if (ptr + data_len > end) { + ERROR_LOG(Log::sceNet, "DNS response data exceeds buffer"); + return false; + } + if (type == DNS_QUERY_TYPE_A && data_len == 4) { // IPv4 address char ip[INET_ADDRSTRLEN]; @@ -586,7 +637,7 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0); // Create UDP socket - if (sockfd < 0) { + if (sockfd == INVALID_SOCKET) { ERROR_LOG(Log::Net, "Socket creation for direct DNS failed"); return false; } @@ -626,7 +677,12 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t dns->q_count = htons(1); // One question unsigned char *qname = buffer + sizeof(DNSHeader); - encode_domain_name(domain, qname); + size_t qname_space = sizeof(buffer) - sizeof(DNSHeader) - 4; // Reserve 4 bytes for qtype and qclass + if (!encode_domain_name(domain, qname, qname_space)) { + ERROR_LOG(Log::sceNet, "Domain name too long or invalid: %s", domain); + closesocket(sockfd); + return false; + } unsigned char *qinfo = qname + strlen((const char *)qname) + 1; *((uint16_t *)qinfo) = htons(DNS_QUERY_TYPE_A); // Query type: A @@ -642,8 +698,8 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t // Receive DNS response socklen_t server_len = sizeof(server_addr); - size_t response_len; - if ((response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len)) < 0) { + int response_len = recvfrom(sockfd, (char *)buffer, sizeof(buffer), 0, (struct sockaddr *)&server_addr, &server_len); + if (response_len < 0) { ERROR_LOG(Log::sceNet, "Failed to receive DNS response (timeout or error)"); closesocket(sockfd); return false; @@ -653,7 +709,7 @@ bool DirectDNSLookupIPV4(const char *dns_server_ip, const char *domain, uint32_t closesocket(sockfd); // Done communicating, time to parse. - if (!parse_dns_response(buffer, response_len, ipv4_addr)) { + if (!parse_dns_response(buffer, (size_t)response_len, ipv4_addr)) { return false; } From da567f6a89c33ba5c1ceb2d623ab97e0814ea4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 5 Aug 2026 15:51:43 +0200 Subject: [PATCH 17/18] Discord cleanup --- UI/DiscordIntegration.cpp | 1 + ext/discord-rpc-build/CMakeLists.txt | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/UI/DiscordIntegration.cpp b/UI/DiscordIntegration.cpp index 8318e7a723..cc10a37212 100644 --- a/UI/DiscordIntegration.cpp +++ b/UI/DiscordIntegration.cpp @@ -146,6 +146,7 @@ void Discord::SetPresenceMenu() { discordPresence.startTimestamp = time(0); discordPresence.largeImageText = "PPSSPP is the best PlayStation Portable emulator around!"; discordPresence.largeImageKey = System_GetPropertyBool(SYSPROP_APP_GOLD) ? "icon_gold_png" : "icon_regular_png"; + Discord_UpdatePresence(&discordPresence); #endif } diff --git a/ext/discord-rpc-build/CMakeLists.txt b/ext/discord-rpc-build/CMakeLists.txt index 82252d745a..079c06496b 100644 --- a/ext/discord-rpc-build/CMakeLists.txt +++ b/ext/discord-rpc-build/CMakeLists.txt @@ -16,15 +16,12 @@ set(ALL_SOURCE_FILES ${SRC_DIR}/serialization.cpp ) -if(APPLE) - set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} - ${SRC_DIR}/discord_register_osx.m - ) -endif() - if(WIN32) set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/connection_win.cpp) set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/discord_register_win.cpp) +elseif(APPLE) + set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/connection_unix.cpp) + set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/discord_register_osx.m) else() set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/connection_unix.cpp) set(ALL_SOURCE_FILES ${ALL_SOURCE_FILES} ${SRC_DIR}/discord_register_linux.cpp) @@ -33,3 +30,11 @@ endif() add_library(discord-rpc STATIC ${ALL_SOURCE_FILES}) target_include_directories(discord-rpc PUBLIC ../discord-rpc/src ../discord-rpc/include ../rapidjson/include) + +if(UNIX) + target_link_libraries(discord-rpc PUBLIC pthread) +endif() + +if(APPLE) + target_link_libraries(discord-rpc PRIVATE "-framework AppKit") +endif() From 7700861d49d312cbd99dcecde1b85a10684d7e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 5 Mar 2026 13:40:07 +0100 Subject: [PATCH 18/18] More iOS cleanup Remove outdated code. --- ios/ViewController.mm | 15 --------------- ios/ViewControllerCommon.mm | 6 +++--- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 82cd2f2c87..d80334194a 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -47,17 +47,12 @@ PPSSPPBaseViewController *sharedViewController; @interface PPSSPPViewControllerGL () { GraphicsContext *graphicsContext; - - int imageRequestId; - NSString *imageFilename; } @property (nonatomic, strong) EAGLContext *glContext; @property (nonatomic, strong) GLKView *glView; @property (nonatomic, strong) CADisplayLink *displayLink; -@property (nonatomic, strong) EAGLContext* context; - @end @implementation PPSSPPViewControllerGL {} @@ -156,7 +151,6 @@ PPSSPPBaseViewController *sharedViewController; [self hideKeyboard]; - // Initialize the motion manager for accelerometer control. INFO_LOG(Log::G3D, "Done with viewDidLoad."); } @@ -247,15 +241,6 @@ PPSSPPBaseViewController *sharedViewController; _dbg_assert_(graphicsContext); - if (self.context) { - if ([EAGLContext currentContext] == self.context) { - [EAGLContext setCurrentContext:nil]; - } - self.context = nil; - } - - [[NSNotificationCenter defaultCenter] removeObserver:self]; - graphicsContext->ShutdownSurface(); graphicsContext->ShutdownAPI(); delete graphicsContext; diff --git a/ios/ViewControllerCommon.mm b/ios/ViewControllerCommon.mm index d5f34e7d3b..46e96cf891 100644 --- a/ios/ViewControllerCommon.mm +++ b/ios/ViewControllerCommon.mm @@ -142,7 +142,7 @@ static int GetPickerRequestId(id picker) { // This needs to be called really late during startup, unfortunately. #if PPSSPP_PLATFORM(IOS_APP_STORE) [IAPManager sharedIAPManager]; // Kick off the IAPManager early. - NSLog(@"Metal viewDidAppear. updating icon"); + NSLog(@"viewDidAppear. updating icon"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [[IAPManager sharedIAPManager] updateIcon:false]; [self hideKeyboard]; @@ -526,9 +526,9 @@ extern float g_safeInsetBottom; } - (void)updateResolutionWithView:(UIView *)view { - // 1. Get the scale from the window scene (safest for Metal) + // 1. Get the scale from the window scene CGFloat scale = 1.0; - if (view.window.windowScene) { + if (view.window && view.window.windowScene) { scale = view.window.windowScene.screen.nativeScale; } else { scale = [UITraitCollection currentTraitCollection].displayScale;