SDL: Implement visionOS support

From 690eae7d227cbd94664564353bdc7504c9b82166 Mon Sep 17 00:00:00 2001
From: Ravbug <[EMAIL REDACTED]>
Date: Sun, 23 Jul 2023 23:11:09 -0700
Subject: [PATCH] Implement visionOS support

---
 .github/workflows/visionos.yml            | 22 +++++++
 CMakeLists.txt                            | 34 ++++++-----
 cmake/sdlplatform.cmake                   |  3 +
 include/SDL3/SDL_platform_defines.h       |  3 +
 src/misc/ios/SDL_sysurl.m                 |  4 ++
 src/video/uikit/SDL_uikitappdelegate.m    | 37 +++++++++---
 src/video/uikit/SDL_uikitevents.m         |  4 +-
 src/video/uikit/SDL_uikitmessagebox.m     |  4 ++
 src/video/uikit/SDL_uikitmetalview.m      |  2 +
 src/video/uikit/SDL_uikitmodes.h          | 17 +++++-
 src/video/uikit/SDL_uikitmodes.m          | 73 +++++++++++++++++++++--
 src/video/uikit/SDL_uikitvideo.h          |  4 ++
 src/video/uikit/SDL_uikitvideo.m          | 13 +++-
 src/video/uikit/SDL_uikitviewcontroller.m |  8 ++-
 src/video/uikit/SDL_uikitwindow.m         | 30 ++++++++--
 15 files changed, 217 insertions(+), 41 deletions(-)
 create mode 100644 .github/workflows/visionos.yml

diff --git a/.github/workflows/visionos.yml b/.github/workflows/visionos.yml
new file mode 100644
index 000000000000..6eebf13ed357
--- /dev/null
+++ b/.github/workflows/visionos.yml
@@ -0,0 +1,22 @@
+name: Build (visionOS)
+
+# FIXME: CMake 3.28 is not yet available on the github runner
+# on: [push, pull_request]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  Build:
+    name: visionOS
+    runs-on: macos-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - name: Configure
+        run: |
+          cmake -B build -GXcode -DCMAKE_SYSTEM_NAME=visionOS
+      - name: Build
+        run: |
+          cmake --build build
diff --git a/CMakeLists.txt b/CMakeLists.txt
index eb9d1b71823a..e4971e577c0a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -131,7 +131,7 @@ endif()
 #  so we'll just use libusb when it's available. libusb does not support iOS,
 #  so we default to yes on iOS.
 #  TODO: Windows can support libusb, the hid.c file just depends on Unix APIs
-if((WINDOWS AND NOT WINDOWS_STORE) OR IOS OR TVOS OR ANDROID)
+if((WINDOWS AND NOT WINDOWS_STORE) OR IOS OR TVOS OR VISIONOS OR ANDROID)
   set(SDL_HIDAPI_LIBUSB_AVAILABLE FALSE)
 else()
   set(SDL_HIDAPI_LIBUSB_AVAILABLE TRUE)
@@ -303,8 +303,8 @@ set_option(SDL_DISKAUDIO           "Support the disk writer audio driver" ON)
 set_option(SDL_DUMMYAUDIO          "Support the dummy audio driver" ON)
 set_option(SDL_DUMMYVIDEO          "Use dummy video driver" ON)
 dep_option(SDL_IBUS                "Enable IBus support" ON ${UNIX_SYS} OFF)
-set_option(SDL_OPENGL              "Include OpenGL support" ON)
-set_option(SDL_OPENGLES            "Include OpenGL ES support" ON)
+dep_option(SDL_OPENGL              "Include OpenGL support" ON "NOT VISIONOS" OFF)
+dep_option(SDL_OPENGLES            "Include OpenGL ES support" ON "NOT VISIONOS" OFF) 
 set_option(SDL_PTHREADS            "Use POSIX threads for multi-threading" ${SDL_PTHREADS_DEFAULT})
 dep_option(SDL_PTHREADS_SEM        "Use pthread semaphores" ON "SDL_PTHREADS" OFF)
 dep_option(SDL_OSS                 "Support the OSS audio API" ON "UNIX_SYS OR RISCOS" OFF)
@@ -348,7 +348,7 @@ dep_option(SDL_KMSDRM_SHARED       "Dynamically load KMS DRM support" ON "SDL_KM
 set_option(SDL_OFFSCREEN           "Use offscreen video driver" ON)
 option_string(SDL_BACKGROUNDING_SIGNAL "number to use for magic backgrounding signal or 'OFF'" OFF)
 option_string(SDL_FOREGROUNDING_SIGNAL "number to use for magic foregrounding signal or 'OFF'" OFF)
-set_option(SDL_HIDAPI              "Enable the HIDAPI subsystem" ON)
+dep_option(SDL_HIDAPI              "Enable the HIDAPI subsystem" ON "NOT VISIONOS" OFF)
 dep_option(SDL_HIDAPI_LIBUSB       "Use libusb for low level joystick drivers" ${SDL_HIDAPI_LIBUSB_DEFAULT} "SDL_HIDAPI;${SDL_HIDAPI_LIBUSB_AVAILABLE}" OFF)
 dep_option(SDL_HIDAPI_JOYSTICK     "Use HIDAPI for low level joystick drivers" ON SDL_HIDAPI OFF)
 dep_option(SDL_VIRTUAL_JOYSTICK    "Enable the virtual-joystick driver" ON SDL_HIDAPI OFF)
@@ -2031,7 +2031,7 @@ elseif(APPLE)
   endif()
 
   if(SDL_MISC)
-    if(IOS OR TVOS)
+    if(IOS OR TVOS OR VISIONOS)
       sdl_glob_sources("${SDL3_SOURCE_DIR}/src/misc/ios/*.m")
     else()
       sdl_glob_sources("${SDL3_SOURCE_DIR}/src/misc/macos/*.m")
@@ -2054,10 +2054,10 @@ elseif(APPLE)
 
   if(SDL_JOYSTICK)
     sdl_glob_sources("${SDL3_SOURCE_DIR}/src/joystick/apple/*.m")
-    if(IOS OR TVOS)
+    if(IOS OR TVOS OR VISIONOS)
       sdl_glob_sources("${SDL3_SOURCE_DIR}/src/joystick/steam/*.c")
       set(SDL_JOYSTICK_MFI 1)
-      if(IOS)
+      if(IOS OR VISIONOS)
         set(SDL_FRAMEWORK_COREMOTION 1)
       endif()
       set(SDL_FRAMEWORK_GAMECONTROLLER 1)
@@ -2089,15 +2089,17 @@ elseif(APPLE)
         set(SDL_FRAMEWORK_GAMECONTROLLER 1)
         set(SDL_FRAMEWORK_COREHAPTICS 1)
       endif()
-      set(SDL_JOYSTICK_IOKIT 1)
-      set(SDL_FRAMEWORK_IOKIT 1)
+      if(NOT VISIONOS)
+        set(SDL_JOYSTICK_IOKIT 1)
+        set(SDL_FRAMEWORK_IOKIT 1)
+      endif()
       set(SDL_FRAMEWORK_FF 1)
     endif()
     set(HAVE_SDL_JOYSTICK TRUE)
   endif()
 
   if(SDL_HAPTIC)
-    if (IOS OR TVOS)
+    if (IOS OR TVOS OR VISIONOS)
       sdl_glob_sources("${SDL3_SOURCE_DIR}/src/haptic/dummy/*.c")
       set(SDL_HAPTIC_DUMMY 1)
     else()
@@ -2110,7 +2112,7 @@ elseif(APPLE)
   endif()
 
   if(SDL_POWER)
-    if (IOS OR TVOS)
+    if (IOS OR TVOS OR VISIONOS)
       sdl_glob_sources("${SDL3_SOURCE_DIR}/src/power/uikit/*.m")
       set(SDL_POWER_UIKIT 1)
     else()
@@ -2139,7 +2141,7 @@ elseif(APPLE)
   endif()
 
   if(SDL_SENSOR)
-    if(IOS)
+    if(IOS OR VISIONOS)
       set(SDL_SENSOR_COREMOTION 1)
       set(HAVE_SDL_SENSORS TRUE)
       sdl_glob_sources("${SDL3_SOURCE_DIR}/src/sensor/coremotion/*.m")
@@ -2148,7 +2150,7 @@ elseif(APPLE)
 
   # iOS hack needed - http://code.google.com/p/ios-cmake/ ?
   if(SDL_VIDEO)
-    if (IOS OR TVOS)
+    if (IOS OR TVOS OR VISIONOS)
       set(SDL_VIDEO_DRIVER_UIKIT 1)
       set(SDL_FRAMEWORK_COREGRAPHICS 1)
       set(SDL_FRAMEWORK_QUARTZCORE 1)
@@ -2168,7 +2170,7 @@ elseif(APPLE)
     endif()
 
     if(SDL_OPENGLES)
-      if(IOS OR TVOS)
+      if(IOS OR TVOS OR VISIONOS)
         set(SDL_FRAMEWORK_OPENGLES 1)
         set(SDL_VIDEO_OPENGL_ES 1)
       else()
@@ -2253,7 +2255,7 @@ elseif(APPLE)
     endif()
   endif()
   if(SDL_FRAMEWORK_METAL)
-    if(IOS OR TVOS)
+    if(IOS OR TVOS OR VISIONOS)
       sdl_link_dependency(metal LINK_OPTIONS "-Wl,-framework,Metal")
     else()
       sdl_link_dependency(metal LINK_OPTIONS "-Wl,-weak_framework,Metal")
@@ -2263,7 +2265,7 @@ elseif(APPLE)
     sdl_link_dependency(opengles LINK_OPTIONS "-Wl,-framework,OpenGLES")
   endif()
   if(SDL_FRAMEWORK_QUARTZCORE)
-    if(IOS OR TVOS)
+    if(IOS OR TVOS OR VISIONOS)
       sdl_link_dependency(quartz_core LINK_OPTIONS "-Wl,-framework,QuartzCore")
     else()
       sdl_link_dependency(metal LINK_OPTIONS "-Wl,-weak_framework,QuartzCore")
diff --git a/cmake/sdlplatform.cmake b/cmake/sdlplatform.cmake
index ebb2077fba05..b402cc038d49 100644
--- a/cmake/sdlplatform.cmake
+++ b/cmake/sdlplatform.cmake
@@ -14,6 +14,9 @@ macro(SDL_DetectCMakePlatform)
       set(SDL_CMAKE_PLATFORM tvOS)
     elseif(CMAKE_SYSTEM_NAME MATCHES ".*iOS.*")
       set(SDL_CMAKE_PLATFORM iOS)
+    elseif (CMAKE_SYSTEM_NAME MATCHES "visionOS")
+      set(SDL_CMAKE_PLATFORM visionOS)
+      set(VISIONOS ON)       # CMAKE does not set this automatically yet 
     endif()
   elseif(CMAKE_SYSTEM_NAME MATCHES "Haiku.*")
     set(SDL_CMAKE_PLATFORM Haiku)
diff --git a/include/SDL3/SDL_platform_defines.h b/include/SDL3/SDL_platform_defines.h
index e84fd8d86772..565a09437980 100644
--- a/include/SDL3/SDL_platform_defines.h
+++ b/include/SDL3/SDL_platform_defines.h
@@ -94,6 +94,9 @@
 #ifndef TARGET_OS_SIMULATOR
 #define TARGET_OS_SIMULATOR 0
 #endif
+#ifndef TARGET_OS_XR
+#define TARGET_OS_XR 0
+#endif
 
 #if TARGET_OS_TV
 #undef __TVOS__
diff --git a/src/misc/ios/SDL_sysurl.m b/src/misc/ios/SDL_sysurl.m
index 8261e43cb8b8..7db7f48db0da 100644
--- a/src/misc/ios/SDL_sysurl.m
+++ b/src/misc/ios/SDL_sysurl.m
@@ -30,9 +30,13 @@ int SDL_SYS_OpenURL(const char *url)
 {
     @autoreleasepool {
 
+#if TARGET_OS_XR
+        return SDL_Unsupported();  // openURL is not suported on visionOS
+#else
         NSString *nsstr = [NSString stringWithUTF8String:url];
         NSURL *nsurl = [NSURL URLWithString:nsstr];
         return [[UIApplication sharedApplication] openURL:nsurl] ? 0 : -1;
+#endif
     }
 }
 
diff --git a/src/video/uikit/SDL_uikitappdelegate.m b/src/video/uikit/SDL_uikitappdelegate.m
index 4b93b8d2c03e..e9d6ca33793b 100644
--- a/src/video/uikit/SDL_uikitappdelegate.m
+++ b/src/video/uikit/SDL_uikitappdelegate.m
@@ -79,7 +79,7 @@ int SDL_RunApp(int argc, char* argv[], SDL_main_func mainFunction, void * reserv
     return exit_status;
 }
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
 /* Load a launch image using the old UILaunchImageFile-era naming rules. */
 static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh)
 {
@@ -142,8 +142,10 @@ - (void)viewDidLoad
     self.storyboardViewController.view.frame = self.view.bounds;
     [self.storyboardViewController didMoveToParentViewController:self];
 
+#if !TARGET_OS_XR
     UIApplication.sharedApplication.statusBarHidden = self.prefersStatusBarHidden;
     UIApplication.sharedApplication.statusBarStyle = self.preferredStatusBarStyle;
+#endif
 }
 
 - (BOOL)prefersStatusBarHidden
@@ -210,11 +212,18 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
         NSArray *launchimages = [bundle objectForInfoDictionaryKey:@"UILaunchImages"];
         NSString *imagename = nil;
         UIImage *image = nil;
-
+        
+#if TARGET_OS_XR
+        int screenw = SDL_XR_SCREENWIDTH;
+        int screenh = SDL_XR_SCREENHEIGHT;
+#else
         int screenw = (int)([UIScreen mainScreen].bounds.size.width + 0.5);
         int screenh = (int)([UIScreen mainScreen].bounds.size.height + 0.5);
+#endif
 
-#if !TARGET_OS_TV
+       
+
+#if !TARGET_OS_TV && !TARGET_OS_XR
         UIInterfaceOrientation curorient = [UIApplication sharedApplication].statusBarOrientation;
 
         /* We always want portrait-oriented size, to match UILaunchImageSize. */
@@ -244,7 +253,7 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
                     }
                 }
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
                 UIInterfaceOrientationMask orientmask = UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown;
                 NSString *orientstring = dict[@"UILaunchImageOrientation"];
 
@@ -273,7 +282,7 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
                 image = [UIImage imageNamed:imagename];
             }
         }
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
         else {
             imagename = [bundle objectForInfoDictionaryKey:@"UILaunchImageFile"];
 
@@ -288,10 +297,15 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB
 #endif
 
         if (image) {
-            UIImageView *view = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
+#if TARGET_OS_XR
+            CGRect viewFrame = CGRectMake(0, 0, screenw, screenh);
+#else
+            CGRect viewFrame = [UIScreen mainScreen].bounds;
+#endif
+            UIImageView *view = [[UIImageView alloc] initWithFrame:viewFrame];
             UIImageOrientation imageorient = UIImageOrientationUp;
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
             /* Bugs observed / workaround tested in iOS 8.3. */
             if (UIInterfaceOrientationIsLandscape(curorient)) {
                 if (image.size.width < image.size.height) {
@@ -420,7 +434,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
     NSString *screenname = nil;
 
     /* tvOS only uses a plain launch image. */
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
     screenname = [bundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"];
 
     if (screenname) {
@@ -443,7 +457,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
     }
 
     if (vc.view) {
-        launchWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+#if TARGET_OS_XR
+        CGRect viewFrame = CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT);
+#else
+        CGRect viewFrame = [UIScreen mainScreen].bounds;
+#endif
+        launchWindow = [[UIWindow alloc] initWithFrame:viewFrame];
 
         /* We don't want the launch window immediately hidden when a real SDL
          * window is shown - we fade it out ourselves when we're ready. */
diff --git a/src/video/uikit/SDL_uikitevents.m b/src/video/uikit/SDL_uikitevents.m
index 4bec9685a853..8913c3dab2f4 100644
--- a/src/video/uikit/SDL_uikitevents.m
+++ b/src/video/uikit/SDL_uikitevents.m
@@ -57,7 +57,7 @@ - (void)eventPumpChanged
         [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
         [notificationCenter addObserver:self selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil];
         [notificationCenter addObserver:self selector:@selector(applicationDidReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
         [notificationCenter addObserver:self
                                selector:@selector(applicationDidChangeStatusBarOrientation)
                                    name:UIApplicationDidChangeStatusBarOrientationNotification
@@ -99,7 +99,7 @@ - (void)applicationDidReceiveMemoryWarning
     SDL_OnApplicationDidReceiveMemoryWarning();
 }
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
 - (void)applicationDidChangeStatusBarOrientation
 {
     SDL_OnApplicationDidChangeStatusBarOrientation();
diff --git a/src/video/uikit/SDL_uikitmessagebox.m b/src/video/uikit/SDL_uikitmessagebox.m
index b0b612a164cf..e7d80ef7679d 100644
--- a/src/video/uikit/SDL_uikitmessagebox.m
+++ b/src/video/uikit/SDL_uikitmessagebox.m
@@ -98,7 +98,11 @@ static BOOL UIKit_ShowMessageBoxAlertController(const SDL_MessageBoxData *messag
     }
 
     if (window == nil || window.rootViewController == nil) {
+#if TARGET_OS_XR
+        alertwindow = [[UIWindow alloc] init];
+#else
         alertwindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+#endif
         alertwindow.rootViewController = [UIViewController new];
         alertwindow.windowLevel = UIWindowLevelAlert;
 
diff --git a/src/video/uikit/SDL_uikitmetalview.m b/src/video/uikit/SDL_uikitmetalview.m
index c82a4dcecc2b..c5317ebd900c 100644
--- a/src/video/uikit/SDL_uikitmetalview.m
+++ b/src/video/uikit/SDL_uikitmetalview.m
@@ -81,6 +81,7 @@ SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window)
         CGFloat scale = 1.0;
         SDL_uikitmetalview *metalview;
 
+#if !TARGET_OS_XR
         if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
             /* Set the scale to the natural scale factor of the screen - then
              * the backing dimensions of the Metal view will match the pixel
@@ -89,6 +90,7 @@ SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window)
              */
             scale = data.uiwindow.screen.nativeScale;
         }
+#endif
 
         metalview = [[SDL_uikitmetalview alloc] initWithFrame:data.uiwindow.bounds
                                                         scale:scale];
diff --git a/src/video/uikit/SDL_uikitmodes.h b/src/video/uikit/SDL_uikitmodes.h
index a744febb2757..b92f19011f46 100644
--- a/src/video/uikit/SDL_uikitmodes.h
+++ b/src/video/uikit/SDL_uikitmodes.h
@@ -27,26 +27,39 @@
 
 @interface SDL_UIKitDisplayData : NSObject
 
+#if !TARGET_OS_XR
 - (instancetype)initWithScreen:(UIScreen *)screen;
-
 @property(nonatomic, strong) UIScreen *uiscreen;
+#endif
 
 @end
 
 @interface SDL_UIKitDisplayModeData : NSObject
-
+#if !TARGET_OS_XR
 @property(nonatomic, strong) UIScreenMode *uiscreenmode;
+#endif
 
 @end
 
+#if !TARGET_OS_XR
 extern SDL_bool UIKit_IsDisplayLandscape(UIScreen *uiscreen);
+#endif
 
 extern int UIKit_InitModes(SDL_VideoDevice *_this);
+#if !TARGET_OS_XR
 extern int UIKit_AddDisplay(UIScreen *uiscreen, SDL_bool send_event);
 extern void UIKit_DelDisplay(UIScreen *uiscreen);
+#endif
 extern int UIKit_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display);
 extern int UIKit_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode);
 extern void UIKit_QuitModes(SDL_VideoDevice *_this);
 extern int UIKit_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect);
 
+// because visionOS does not have a screen
+// we create a fake 1080p display to maintain compatibility.
+#if TARGET_OS_XR
+#define SDL_XR_SCREENWIDTH 1920
+#define SDL_XR_SCREENHEIGHT 1080
+#endif
+
 #endif /* SDL_uikitmodes_h_ */
diff --git a/src/video/uikit/SDL_uikitmodes.m b/src/video/uikit/SDL_uikitmodes.m
index bc13c0df8df7..e397e97a68aa 100644
--- a/src/video/uikit/SDL_uikitmodes.m
+++ b/src/video/uikit/SDL_uikitmodes.m
@@ -30,6 +30,7 @@
 
 @implementation SDL_UIKitDisplayData
 
+#if !TARGET_OS_XR
 - (instancetype)initWithScreen:(UIScreen *)screen
 {
     if (self = [super init]) {
@@ -37,20 +38,23 @@ - (instancetype)initWithScreen:(UIScreen *)screen
     }
     return self;
 }
-
 @synthesize uiscreen;
+#endif
 
 @end
 
 @implementation SDL_UIKitDisplayModeData
 
+#if !TARGET_OS_XR
 @synthesize uiscreenmode;
+#endif
 
 @end
 
 @interface SDL_DisplayWatch : NSObject
 @end
 
+#if !TARGET_OS_XR
 @implementation SDL_DisplayWatch
 
 + (void)start
@@ -92,7 +96,9 @@ + (void)screenDisconnected:(NSNotification *)notification
 }
 
 @end
+#endif
 
+#if !TARGET_OS_XR
 static int UIKit_AllocateDisplayModeData(SDL_DisplayMode *mode,
                                          UIScreenMode *uiscreenmode)
 {
@@ -112,6 +118,7 @@ static int UIKit_AllocateDisplayModeData(SDL_DisplayMode *mode,
 
     return 0;
 }
+#endif
 
 static void UIKit_FreeDisplayModeData(SDL_DisplayMode *mode)
 {
@@ -121,6 +128,7 @@ static void UIKit_FreeDisplayModeData(SDL_DisplayMode *mode)
     }
 }
 
+#if !TARGET_OS_XR
 static float UIKit_GetDisplayModeRefreshRate(UIScreen *uiscreen)
 {
 #ifdef __IPHONE_10_3
@@ -235,7 +243,43 @@ int UIKit_AddDisplay(UIScreen *uiscreen, SDL_bool send_event)
     display.desktop_mode = mode;
 
     /* Allocate the display data */
+#if TARGET_OS_XR
+    SDL_UIKitDisplayData *data = [[SDL_UIKitDisplayData alloc] init];
+#else
     SDL_UIKitDisplayData *data = [[SDL_UIKitDisplayData alloc] initWithScreen:uiscreen];
+#endif
+    if (!data) {
+        UIKit_FreeDisplayModeData(&display.desktop_mode);
+        return SDL_OutOfMemory();
+    }
+
+    display.driverdata = (SDL_DisplayData *)CFBridgingRetain(data);
+    if (SDL_AddVideoDisplay(&display, send_event) == 0) {
+        return -1;
+    }
+    return 0;
+}
+#endif
+
+#if TARGET_OS_XR
+int UIKit_AddDisplay(SDL_bool send_event){
+    CGSize size = CGSizeMake(SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT);
+    SDL_VideoDisplay display;
+    SDL_DisplayMode mode;
+
+    SDL_zero(mode);
+    mode.w = (int)size.width;
+    mode.h = (int)size.height;
+    mode.pixel_density = 1;
+    mode.format = SDL_PIXELFORMAT_ABGR8888;
+    mode.refresh_rate = 60;
+    
+    display.natural_orientation = SDL_ORIENTATION_LANDSCAPE;
+
+    display.desktop_mode = mode;
+    
+    SDL_UIKitDisplayData *data = [[SDL_UIKitDisplayData alloc] init];
+    
     if (!data) {
         UIKit_FreeDisplayModeData(&display.desktop_mode);
         return SDL_OutOfMemory();
@@ -247,6 +291,9 @@ int UIKit_AddDisplay(UIScreen *uiscreen, SDL_bool send_event)
     }
     return 0;
 }
+#endif
+
+#if !TARGET_OS_XR
 
 void UIKit_DelDisplay(UIScreen *uiscreen)
 {
@@ -281,20 +328,27 @@ SDL_bool UIKit_IsDisplayLandscape(UIScreen *uiscreen)
         return (size.width > size.height);
     }
 }
-
+#endif
 int UIKit_InitModes(SDL_VideoDevice *_this)
 {
     @autoreleasepool {
+#if TARGET_OS_XR
+        UIKit_AddDisplay(SDL_FALSE);
+#else
         for (UIScreen *uiscreen in [UIScreen screens]) {
             if (UIKit_AddDisplay(uiscreen, SDL_FALSE) < 0) {
                 return -1;
             }
         }
-#if !TARGET_OS_TV
+#endif
+        
+#if !TARGET_OS_TV && !TARGET_OS_XR
         SDL_OnApplicationDidChangeStatusBarOrientation();
 #endif
 
+#if !TARGET_OS_XR
         [SDL_DisplayWatch start];
+#endif
     }
 
     return 0;
@@ -302,6 +356,7 @@ int UIKit_InitModes(SDL_VideoDevice *_this)
 
 int UIKit_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display)
 {
+#if !TARGET_OS_XR
     @autoreleasepool {
         SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->driverdata;
 
@@ -331,11 +386,13 @@ int UIKit_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display)
             UIKit_AddDisplayMode(display, w, h, data.uiscreen, uimode, addRotation);
         }
     }
+#endif
     return 0;
 }
 
 int UIKit_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode)
 {
+#if !TARGET_OS_XR
     @autoreleasepool {
         SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->driverdata;
 
@@ -359,7 +416,7 @@ int UIKit_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_
             }
         }
     }
-
+#endif
     return 0;
 }
 
@@ -367,7 +424,11 @@ int UIKit_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *displ
 {
     @autoreleasepool {
         SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->driverdata;
+#if TARGET_OS_XR
+        CGRect frame = CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT);
+#else
         CGRect frame = data.uiscreen.bounds;
+#endif
 
         /* the default function iterates displays to make a fake offset,
          as if all the displays were side-by-side, which is fine for iOS. */
@@ -386,7 +447,9 @@ int UIKit_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *displ
 
 void UIKit_QuitModes(SDL_VideoDevice *_this)
 {
+#if !TARGET_OS_XR
     [SDL_DisplayWatch stop];
+#endif
 
     /* Release Objective-C objects, so higher level doesn't free() them. */
     int i, j;
@@ -408,7 +471,7 @@ void UIKit_QuitModes(SDL_VideoDevice *_this)
     }
 }
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
 void SDL_OnApplicationDidChangeStatusBarOrientation(void)
 {
     BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation);
diff --git a/src/video/uikit/SDL_uikitvideo.h b/src/video/uikit/SDL_uikitvideo.h
index 60ee2a9f2ab1..f13822299743 100644
--- a/src/video/uikit/SDL_uikitvideo.h
+++ b/src/video/uikit/SDL_uikitvideo.h
@@ -33,7 +33,11 @@
 
 @end
 
+#if TARGET_OS_XR
+CGRect UIKit_ComputeViewFrame(SDL_Window *window);
+#else
 CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen);
+#endif
 
 #endif /* __OBJC__ */
 
diff --git a/src/video/uikit/SDL_uikitvideo.m b/src/video/uikit/SDL_uikitvideo.m
index 51bef011f55e..0b07afb053af 100644
--- a/src/video/uikit/SDL_uikitvideo.m
+++ b/src/video/uikit/SDL_uikitvideo.m
@@ -52,7 +52,9 @@ @implementation SDL_UIKitVideoData
 static void UIKit_DeleteDevice(SDL_VideoDevice *device)
 {
     @autoreleasepool {
-        CFRelease(device->driverdata);
+        if (device->driverdata){
+            CFRelease(device->driverdata);
+        }
         SDL_free(device);
     }
 }
@@ -183,6 +185,7 @@ SDL_bool UIKit_IsSystemVersionAtLeast(double version)
 
 SDL_SystemTheme UIKit_GetSystemTheme(void)
 {
+#if !TARGET_OS_XR
     if (@available(iOS 12.0, tvOS 10.0, *)) {
         switch ([UIScreen mainScreen].traitCollection.userInterfaceStyle) {
         case UIUserInterfaceStyleDark:
@@ -193,9 +196,15 @@ SDL_SystemTheme UIKit_GetSystemTheme(void)
             break;
         }
     }
+#endif
     return SDL_SYSTEM_THEME_UNKNOWN;
 }
 
+#if TARGET_OS_XR
+CGRect UIKit_ComputeViewFrame(SDL_Window *window){
+    return CGRectMake(window->x, window->y, window->w, window->h);
+}
+#else
 CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen)
 {
     SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->driverdata;
@@ -234,6 +243,8 @@ CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen)
     return frame;
 }
 
+#endif
+
 void UIKit_ForceUpdateHomeIndicator(void)
 {
 #if !TARGET_OS_TV
diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
index 4d0b96a54aa1..dfebb08628b2 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.m
+++ b/src/video/uikit/SDL_uikitviewcontroller.m
@@ -162,7 +162,9 @@ - (void)startAnimation
 {
     displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doLoop:)];
 
-#ifdef __IPHONE_10_3
+#if TARGET_OS_XR
+    displayLink.preferredFramesPerSecond = 90 / animationInterval;      //TODO: Get frame max frame rate on visionOS
+#elif defined(__IPHONE_10_3)
     SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->driverdata;
 
     if ([displayLink respondsToSelector:@selector(preferredFramesPerSecond)] && data != nil && data.uiwindow != nil && [data.uiwindow.screen respondsToSelector:@selector(maximumFramesPerSecond)]) {
@@ -511,7 +513,11 @@ - (void)updateKeyboard
 {
     CGAffineTransform t = self.view.transform;
     CGPoint offset = CGPointMake(0.0, 0.0);
+#if TARGET_OS_XR
+    CGRect frame = UIKit_ComputeViewFrame(window);
+#else
     CGRect frame = UIKit_ComputeViewFrame(window, self.view.window.screen);
+#endif
 
     if (self.keyboardHeight) {
         int rectbottom = self.textInputRect.y + self.textInputRect.h;
diff --git a/src/video/uikit/SDL_uikitwindow.m b/src/video/uikit/SDL_uikitwindow.m
index 13aa86462315..ed8c6dcd1722 100644
--- a/src/video/uikit/SDL_uikitwindow.m
+++ b/src/video/uikit/SDL_uikitwindow.m
@@ -65,6 +65,7 @@ @implementation SDL_uikitwindow
 
 - (void)layoutSubviews
 {
+#if !TARGET_OS_XR
     /* Workaround to fix window orientation issues in iOS 8. */
     /* As of July 1 2019, I haven't been able to reproduce any orientation
      * issues with this disabled on iOS 12. The issue this is meant to fix might
@@ -74,6 +75,7 @@ - (void)layoutSubviews
     if (!UIKit_IsSystemVersionAtLeast(9.0)) {
         self.frame = self.screen.bounds;
     }
+#endif
     [super layoutSubviews];
 }
 
@@ -84,8 +86,13 @@ static int SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow
     SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
     SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->driverdata;
     SDL_uikitview *view;
-
+   
+#if TARGET_OS_XR
+    CGRect frame = UIKit_ComputeViewFrame(window);
+#else
     CGRect frame = UIKit_ComputeViewFrame(window, displaydata.uiscreen);
+#endif
+    
     int width = (int)frame.size.width;
     int height = (int)frame.size.height;
 
@@ -98,13 +105,15 @@ static int SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow
 
     data.uiwindow = uiwindow;
 
+#if !TARGET_OS_XR
     if (displaydata.uiscreen != [UIScreen mainScreen]) {
         window->flags &= ~SDL_WINDOW_RESIZABLE;   /* window is NEVER resizable */
         window->flags &= ~SDL_WINDOW_INPUT_FOCUS; /* never has input focus */
         window->flags |= SDL_WINDOW_BORDERLESS;   /* never has a status bar. */
     }
+#endif
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
     if (displaydata.uiscreen == [UIScreen mainScreen]) {
         /* SDL_CreateWindow sets the window w&h to the display's bounds if the
          * fullscreen flag is set. But the display bounds orientation might not
@@ -166,7 +175,7 @@ int UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window)
         /* If monitor has a resolution of 0x0 (hasn't been explicitly set by the
          * user, so it's in standby), try to force the display to a resolution
          * that most closely matches the desired window size. */
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
         const CGSize origsize = data.uiscreen.currentMode.size;
         if ((origsize.width == 0.0f) && (origsize.height == 0.0f)) {
             const SDL_DisplayMode *bestmode;
@@ -197,12 +206,18 @@ int UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window)
 
         /* ignore the size user requested, and make a fullscreen window */
         /* !!! FIXME: can we have a smaller view? */
+#if TARGET_OS_XR
+        UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:CGRectMake(window->x, window->y, window->w, window->h)];
+#else
         UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds];
+#endif
 
         /* put the window on an external display if appropriate. */
+#if !TARGET_OS_XR
         if (data.uiscreen != [UIScreen mainScreen]) {
             [uiwindow setScreen:data.uiscreen];
         }
+#endif
 
         if (SetupWindowData(_this, window, uiwindow, SDL_TRUE) < 0) {
             return -1;
@@ -229,7 +244,10 @@ void UIKit_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
         /* Make this window the current mouse focus for touch input */
         SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
         SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->driverdata;
-        if (displaydata.uiscreen == [UIScreen mainScreen]) {
+#if !TARGET_OS_XR
+        if (displaydata.uiscreen == [UIScreen mainScreen])
+#endif
+        {
             SDL_SetMouseFocus(window);
             SDL_SetKeyboardFocus(window);
         }
@@ -258,7 +276,7 @@ static void UIKit_UpdateWindowBorder(SDL_VideoDevice *_this, SDL_Window *window)
     SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->driverdata;
     SDL_uikitviewcontroller *viewcontroller = data.viewcontroller;
 
-#if !TARGET_OS_TV
+#if !TARGET_OS_TV && !TARGET_OS_XR
     if (data.uiwindow.screen == [UIScreen mainScreen]) {
         if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) {
             [UIApplication sharedApplication].statusBarHidden = YES;
@@ -354,9 +372,11 @@ void UIKit_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int
         CGSize size = view.bounds.size;
         CGFloat scale = 1.0;
 
+#if !TARGET_OS_XR
         if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
             scale = windata.uiwindow.screen.nativeScale;
         }
+#endif
 
         /* Integer truncation of fractional values matches SDL_uikitmetalview and
          * SDL_uikitopenglview. */

(Patch may be truncated, please check the link at the top of this post.)