Files
FlaxEngine/Source/Engine/Platform/iOS/iOSPlatform.cpp
T
Andrei Gagua 0c354b85f3 Fix Vulkan swapchain wait for acquired backbuffer
Move backbuffer submit waiting until after swapchain image acquire, and track the submitted fence counter per backbuffer to avoid waiting on stale command buffer objects. This preserves multi-buffered presentation and fixes iOS/MoltenVK frame pacing drops caused by waiting on the wrong swapchain image.
2026-06-06 22:08:43 +03:00

712 lines
19 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#if PLATFORM_IOS
#include "iOSPlatform.h"
#include "iOSWindow.h"
#include "iOSFile.h"
#include "iOSFileSystem.h"
#include "iOSApp.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Delegate.h"
#include "Engine/Core/Utilities.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Platform/Apple/AppleUtils.h"
#include "Engine/Platform/StringUtils.h"
#include "Engine/Platform/MessageBox.h"
#include "Engine/Platform/Window.h"
#include "Engine/Platform/WindowsManager.h"
#include "Engine/Platform/BatteryInfo.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Input/Input.h"
#include "Engine/Input/InputDevice.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Content/Storage/ContentStorageManager.h"
#include <UIKit/UIKit.h>
#include <CoreFoundation/CoreFoundation.h>
#include <QuartzCore/CAMetalLayer.h>
#include <SystemConfiguration/SystemConfiguration.h>
#include <sys/utsname.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <os/proc.h>
class iOSTouchScreen : public InputDevice
{
private:
CriticalSection _locker;
public:
explicit iOSTouchScreen()
: InputDevice(SpawnParams(Guid::New(), TypeInitializer), TEXT("iOS Touch Screen"))
{
}
void ResetState() override
{
ScopeLock lock(_locker);
InputDevice::ResetState();
}
bool Update(EventQueue& queue) override
{
ScopeLock lock(_locker);
return InputDevice::Update(queue);
}
void OnTouch(EventType type, float x, float y, int32 pointerId)
{
ScopeLock lock(_locker);
Event& e = _queue.AddOne();
e.Type = type;
e.Target = nullptr;
e.TouchData.Position.X = x;
e.TouchData.Position.Y = y;
e.TouchData.PointerId = pointerId;
}
};
int32 Dpi = 96;
Guid DeviceId;
FlaxView* MainView = nullptr;
FlaxViewController* MainViewController = nullptr;
iOSWindow* MainWindow = nullptr;
iOSTouchScreen* TouchScreen;
bool HasFocus = true;
bool IsPaused = false;
struct MessagePipeline
{
CriticalSection Locker;
Array<Function<void()>> List;
void Add(const Function<void()>& func, bool wait)
{
Locker.Lock();
List.Add(func);
Locker.Unlock();
// TODO: use atomic counters for more optimized waiting
while (wait)
{
Platform::Sleep(1);
Locker.Lock();
wait = List.HasItems();
Locker.Unlock();
}
}
void Run()
{
Locker.Lock();
for (const auto& func : List)
{
func();
}
List.Clear();
Locker.Unlock();
}
};
MessagePipeline UIThreadPipeline;
MessagePipeline MainThreadPipeline;
#define PLATFORM_IOS_MAX_TOUCHES 8
@interface FlaxView()
{
UITouch* activeTouches[PLATFORM_IOS_MAX_TOUCHES];
}
@end
@implementation FlaxView
+ (Class)layerClass { return [CAMetalLayer class]; }
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
if (!MainWindow)
return;
float scale = [[UIScreen mainScreen] scale];
float width = (float)frame.size.width * scale;
float height = (float)frame.size.height * scale;
iOSPlatform::RunOnMainThread([width, height]() { MainWindow->CheckForResize(width, height); });
}
- (void)Init
{
self.multipleTouchEnabled = YES;
Platform::MemoryClear(activeTouches, sizeof(activeTouches));
}
- (void)OnTouchEvent:(NSSet*)touches ofType:(InputDevice::EventType)eventType
{
float scale = [[UIScreen mainScreen] scale];
for (UITouch* touch in touches)
{
// Get touch location
CGPoint location = [touch locationInView:self];
location.x *= scale;
location.y *= scale;
// Get touch index (use list of activly tracked touches)
int32 touchIndex = 0;
for (; touchIndex < PLATFORM_IOS_MAX_TOUCHES; touchIndex++)
{
if (activeTouches[touchIndex] == touch)
break;
}
if (touchIndex == PLATFORM_IOS_MAX_TOUCHES && eventType == InputDevice::EventType::TouchDown)
{
// Find free slot for a new touches
touchIndex = 0;
for (; touchIndex < PLATFORM_IOS_MAX_TOUCHES; touchIndex++)
{
if (activeTouches[touchIndex] == nil)
{
activeTouches[touchIndex] = touch;
break;
}
}
}
if (touchIndex == PLATFORM_IOS_MAX_TOUCHES)
continue;
// Send event to the input device
TouchScreen->OnTouch(eventType, location.x, location.y, touchIndex);
// Remove ended touches
if (eventType == InputDevice::EventType::TouchUp)
activeTouches[touchIndex] = nil;
}
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
[self OnTouchEvent:touches ofType:InputDevice::EventType::TouchDown];
}
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
[self OnTouchEvent:touches ofType:InputDevice::EventType::TouchMove];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event
{
[self OnTouchEvent:touches ofType:InputDevice::EventType::TouchUp];
}
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event
{
[self OnTouchEvent:touches ofType:InputDevice::EventType::TouchUp];
}
@end
@implementation FlaxViewController
- (BOOL)prefersHomeIndicatorAutoHidden
{
return YES;
}
- (BOOL)prefersStatusBarHidden
{
return YES;
}
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
return UIStatusBarAnimationSlide;
}
@end
@interface FlaxAppDelegate()
@property(strong, nonatomic) CADisplayLink* displayLink;
@end
@implementation FlaxAppDelegate
- (void)MainThreadMain:(NSDictionary*)launchOptions
{
// Run engine on a separate game thread
Engine::Main(TEXT(""));
}
- (void)UIThreadMain
{
// Invoke callbacks
UIThreadPipeline.Run();
}
- (void)scene:(UIScene*)scene willConnectToSession:(UISceneSession*)session options:(UISceneConnectionOptions*)connectionOptions
{
//LOG(Info, "[iOS] willConnectToSession");
// Create window
CGRect frame = [[UIScreen mainScreen] bounds];
self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene*)scene];
// Create view
self.view = [[FlaxView alloc] initWithFrame:frame];
[self.view resignFirstResponder];
[self.view setNeedsDisplay];
[self.view setHidden:NO];
[self.view setOpaque:YES];
[self.view Init];
self.view.backgroundColor = [UIColor clearColor];
MainView = self.view;
// Create view controller
self.viewController = [[FlaxViewController alloc] init];
[self.viewController setView:self.view];
[self.viewController setNeedsUpdateOfHomeIndicatorAutoHidden];
[self.viewController setNeedsStatusBarAppearanceUpdate];
MainViewController = self.viewController;
// Link controller and show window
self.window.rootViewController = MainViewController;
[self.window makeKeyAndVisible];
}
- (UISceneConfiguration*)application:(UIApplication*)application configurationForConnectingSceneSession:(UISceneSession*)connectingSceneSession options:(UISceneConnectionOptions*)options API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] configurationForConnectingSceneSession");
UISceneConfiguration* config = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
config.delegateClass = [FlaxAppDelegate class];
return config;
}
- (void)application:(UIApplication*)application didDiscardSceneSessions:(NSSet<UISceneSession*>*)sceneSessions API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] didDiscardSceneSessions");
}
- (void)sceneDidDisconnect:(UIScene*)scene API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] sceneDidDisconnect");
}
- (void)sceneDidBecomeActive:(UIScene*)scene API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] sceneDidBecomeActive");
// Focus app
HasFocus = true;
if (MainWindow)
{
Function<void()> func = []()
{
MainWindow->OnGotFocus();
};
iOSPlatform::RunOnMainThread(func);
}
}
- (void)sceneWillResignActive:(UIScene*)scene API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] sceneWillResignActive");
// Defocus app
HasFocus = false;
if (MainWindow)
{
Function<void()> func = []()
{
MainWindow->OnLostFocus();
};
iOSPlatform::RunOnMainThread(func);
}
}
- (void)sceneDidEnterBackground:(UIScene*)scene API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] sceneDidEnterBackground");
// Pause
IsPaused = true;
}
- (void)sceneWillEnterForeground:(UIScene*)scene API_AVAILABLE(ios(13.0), tvos(13.0), visionos(1.0))
{
LOG(Info, "[iOS] sceneDidEnterBackground");
// Resume
IsPaused = false;
}
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
//LOG(Info, "[iOS] didFinishLaunchingWithOptions");
// Create UI thread update callback
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIThreadMain)];
const int32 targetFrameRate = 60;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
if (@available(iOS 15.0, *))
self.displayLink.preferredFrameRateRange = CAFrameRateRangeMake(targetFrameRate, targetFrameRate, targetFrameRate);
else
#endif
self.displayLink.preferredFramesPerSecond = targetFrameRate;
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// Run engine on a separate main thread
NSThread* mainThread = [[NSThread alloc] initWithTarget:self selector:@selector(MainThreadMain:) object:launchOptions];
#if BUILD_DEBUG
const int32 mainThreadStackSize = 4 * 1024 * 1024; // 4 MB
#else
const int32 mainThreadStackSize = 2 * 1024 * 1024; // 2 MB
#endif
[mainThread setStackSize:mainThreadStackSize];
[mainThread start];
return YES;
}
- (void)applicationWillResignActive:(UIApplication*)application
{
LOG(Info, "[iOS] applicationWillResignActive");
// Defocus app
HasFocus = false;
if (MainWindow)
MainWindow->OnLostFocus();
}
- (void)applicationDidEnterBackground:(UIApplication*)application
{
LOG(Info, "[iOS] applicationDidEnterBackground");
// Pause
IsPaused = true;
}
- (void)applicationWillEnterForeground:(UIApplication*)application
{
LOG(Info, "[iOS] applicationWillEnterForeground");
// Resume
IsPaused = false;
}
- (void)applicationDidBecomeActive:(UIApplication*)application
{
LOG(Info, "[iOS] applicationDidBecomeActive");
// Focus app
HasFocus = true;
if (MainWindow)
MainWindow->OnGotFocus();
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication*)application
{
LOG(Warning, "[iOS] applicationDidReceiveMemoryWarning");
LOG(Warning, "os_proc_available_memory: {}", Utilities::BytesToText(os_proc_available_memory()));
}
@end
DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon)
{
NSString* title = (NSString*)AppleUtils::ToString(caption);
NSString* message = (NSString*)AppleUtils::ToString(text);
Function<void()> func = [&title, &message]()
{
UIAlertController* alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* button = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(id){ }];
[alert addAction:button];
[MainViewController presentViewController:alert animated:YES completion:nil];
};
iOSPlatform::RunOnUIThread(func, true);
return DialogResult::OK;
}
iOSWindow::iOSWindow(const CreateWindowSettings& settings)
: WindowBase(settings)
{
CGRect frame = [[UIScreen mainScreen] bounds];
float scale = [[UIScreen mainScreen] scale];
_clientSize = Float2((float)frame.size.width * scale, (float)frame.size.height * scale);
MainWindow = this;
}
iOSWindow::~iOSWindow()
{
MainWindow = nullptr;
}
void iOSWindow::CheckForResize(float width, float height)
{
const Float2 clientSize(width, height);
if (clientSize != _clientSize)
{
_clientSize = clientSize;
OnResize(width, height);
}
}
void* iOSWindow::GetNativePtr() const
{
return MainView;
}
void iOSWindow::Show()
{
if (!_visible)
{
InitSwapChain();
if (_showAfterFirstPaint)
{
if (RenderTask)
RenderTask->Enabled = true;
return;
}
// Show
_focused = true;
// Base
WindowBase::Show();
}
}
bool iOSWindow::IsClosed() const
{
return false;
}
bool iOSWindow::IsForegroundWindow() const
{
return IsFocused() && Platform::GetHasFocus();
}
void iOSWindow::BringToFront(bool force)
{
Focus();
}
void iOSWindow::SetIsFullscreen(bool isFullscreen)
{
}
// Fallback to file placed side-by-side with application
#define IOS_FALLBACK_PATH(path) (Globals::ProjectFolder / StringUtils::GetFileName(path))
iOSFile* iOSFile::Open(const StringView& path, FileMode mode, FileAccess access, FileShare share)
{
iOSFile* file;
if (mode == FileMode::OpenExisting && !AppleFileSystem::FileExists(path))
file = (iOSFile*)UnixFile::Open(IOS_FALLBACK_PATH(path), mode, access, share);
else
file = (iOSFile*)UnixFile::Open(path, mode, access, share);
return file;
}
bool iOSFileSystem::FileExists(const StringView& path)
{
return AppleFileSystem::FileExists(path) || AppleFileSystem::FileExists(IOS_FALLBACK_PATH(path));
}
uint64 iOSFileSystem::GetFileSize(const StringView& path)
{
if (AppleFileSystem::FileExists(path))
return AppleFileSystem::GetFileSize(path);
return AppleFileSystem::GetFileSize(IOS_FALLBACK_PATH(path));
}
bool iOSFileSystem::IsReadOnly(const StringView& path)
{
if (AppleFileSystem::FileExists(path))
return AppleFileSystem::IsReadOnly(path);
return AppleFileSystem::IsReadOnly(IOS_FALLBACK_PATH(path));
}
#undef IOS_FALLBACK_PATH
void iOSPlatform::RunOnUIThread(const Function<void()>& func, bool wait)
{
UIThreadPipeline.Add(func, wait);
}
void iOSPlatform::RunOnMainThread(const Function<void()>& func, bool wait)
{
MainThreadPipeline.Add(func, wait);
}
bool iOSPlatform::Init()
{
if (ApplePlatform::Init())
return true;
// Setup screen scaling
ScreenScale = [[UIScreen mainScreen] scale];
CustomDpiScale *= ScreenScale;
Dpi = Math::TruncToInt(163 * ScreenScale);
// Get device identifier
NSString* uuid = [UIDevice currentDevice].identifierForVendor.UUIDString;
String uuidStr = AppleUtils::ToString((CFStringRef)uuid);
Guid::Parse(uuidStr, DeviceId);
// Setup native platform input devices
// TODO: add Gamepad for vibrations usability
Input::CustomDevices.Add(TouchScreen = New<iOSTouchScreen>());
// Use more aggressive content buffers freeing to reduce peek memory
ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromMilliseconds(30);
return false;
}
void iOSPlatform::LogInfo()
{
ApplePlatform::LogInfo();
struct utsname systemInfo;
uname(&systemInfo);
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
LOG(Info, "{3}, iOS {0}.{1}.{2}", version.majorVersion, version.minorVersion, version.patchVersion, String(systemInfo.machine));
LOG(Info, "os_proc_available_memory: {}", Utilities::BytesToText(os_proc_available_memory()));
}
void iOSPlatform::Tick()
{
// Invoke callbacks
MainThreadPipeline.Run();
ApplePlatform::Tick();
}
BatteryInfo iOSPlatform::GetBatteryInfo()
{
BatteryInfo result;
UIDevice* uiDevice = [UIDevice currentDevice];
if (uiDevice)
{
uiDevice.batteryMonitoringEnabled = YES;
result.BatteryLifePercent = Math::Saturate([uiDevice batteryLevel]);
switch ([uiDevice batteryState])
{
case UIDeviceBatteryStateUnknown:
result.BatteryLifePercent = 1.0f;
break;
case UIDeviceBatteryStateUnplugged:
result.State = BatteryInfo::States::BatteryDischarging;
break;
case UIDeviceBatteryStateCharging:
result.State = BatteryInfo::States::BatteryCharging;
break;
case UIDeviceBatteryStateFull:
result.State = BatteryInfo::States::Connected;
break;
}
}
return result;
}
int32 iOSPlatform::GetDpi()
{
return Dpi;
}
NetworkConnectionType iOSPlatform::GetNetworkConnectionType()
{
struct sockaddr_in emptyAddr;
Platform::MemoryClear(&emptyAddr, sizeof(emptyAddr));
emptyAddr.sin_len = sizeof(emptyAddr);
emptyAddr.sin_family = AF_INET;
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)&emptyAddr);
SCNetworkReachabilityFlags reachabilityFlags;
bool connected = SCNetworkReachabilityGetFlags(reachability, &reachabilityFlags);
CFRelease(reachability);
if (connected)
{
if (reachabilityFlags == 0)
return NetworkConnectionType::AirplaneMode;
if ((reachabilityFlags & kSCNetworkReachabilityFlagsReachable) != 0 &&
(reachabilityFlags & kSCNetworkReachabilityFlagsConnectionRequired) == 0 &&
(reachabilityFlags & kSCNetworkReachabilityFlagsInterventionRequired) == 0)
{
if ((reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN) == 0)
return NetworkConnectionType::WiFi;
return NetworkConnectionType::Cell;
}
}
return NetworkConnectionType::None;
}
ScreenOrientationType iOSPlatform::GetScreenOrientationType()
{
UIInterfaceOrientation orientation = UIInterfaceOrientationUnknown;
Function<void()> func = [&orientation]()
{
orientation = [[[[[UIApplication sharedApplication] delegate] window] windowScene] interfaceOrientation];
};
iOSPlatform::RunOnUIThread(func, true);
switch (orientation)
{
case UIInterfaceOrientationPortrait: return ScreenOrientationType::Portrait;
case UIInterfaceOrientationPortraitUpsideDown: return ScreenOrientationType::PortraitUpsideDown;
case UIInterfaceOrientationLandscapeLeft: return ScreenOrientationType::LandscapeLeft;
case UIInterfaceOrientationLandscapeRight: return ScreenOrientationType::LandscapeRight;
case UIInterfaceOrientationUnknown:
default: return ScreenOrientationType::Unknown;
}
}
Guid iOSPlatform::GetUniqueDeviceId()
{
return DeviceId;
}
String iOSPlatform::GetComputerName()
{
return TEXT("iPhone");
}
bool iOSPlatform::GetHasFocus()
{
return HasFocus;
}
bool iOSPlatform::GetIsPaused()
{
return IsPaused;
}
Float2 iOSPlatform::GetDesktopSize()
{
CGRect frame = [[UIScreen mainScreen] bounds];
float scale = [[UIScreen mainScreen] scale];
return Float2((float)frame.size.width * scale, (float)frame.size.height * scale);
}
String iOSPlatform::GetMainDirectory()
{
String path = StringUtils::GetDirectoryName(GetExecutableFilePath());
if (path.EndsWith(TEXT("/Contents/iOS")))
{
// If running from executable in a package, go up to the Contents
path = StringUtils::GetDirectoryName(path);
}
return path;
}
Window* iOSPlatform::CreateWindow(const CreateWindowSettings& settings)
{
return New<iOSWindow>(settings);
}
#endif