Pārlūkot izejas kodu

Added curved window mode on visionOS 26 (#15298)

Sam Lantinga 2 nedēļas atpakaļ
vecāks
revīzija
5cf16e4522

+ 68 - 24
Xcode/SDL/SDL.xcodeproj/project.pbxproj

@@ -50,13 +50,14 @@
 		0000AEB9AE90228CA2D60000 /* SDL_asyncio.c in Sources */ = {isa = PBXBuildFile; fileRef = 00003928A612EC33D42C0000 /* SDL_asyncio.c */; };
 		0000AEB9AE90228CA2D60000 /* SDL_asyncio.c in Sources */ = {isa = PBXBuildFile; fileRef = 00003928A612EC33D42C0000 /* SDL_asyncio.c */; };
 		0000D5B526B85DE7AB1C0000 /* SDL_cocoapen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0000CCA310B73A7B59910000 /* SDL_cocoapen.m */; };
 		0000D5B526B85DE7AB1C0000 /* SDL_cocoapen.m in Sources */ = {isa = PBXBuildFile; fileRef = 0000CCA310B73A7B59910000 /* SDL_cocoapen.m */; };
 		007317A40858DECD00B2BC32 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0073179D0858DECD00B2BC32 /* Cocoa.framework */; platformFilters = (macos, ); };
 		007317A40858DECD00B2BC32 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0073179D0858DECD00B2BC32 /* Cocoa.framework */; platformFilters = (macos, ); };
-		007317A60858DECD00B2BC32 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0073179F0858DECD00B2BC32 /* IOKit.framework */; platformFilters = (ios, maccatalyst, macos, ); };
+		007317A60858DECD00B2BC32 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0073179F0858DECD00B2BC32 /* IOKit.framework */; platformFilters = (ios, maccatalyst, macos, xros, ); };
 		00CFA89D106B4BA100758660 /* ForceFeedback.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00CFA89C106B4BA100758660 /* ForceFeedback.framework */; platformFilters = (macos, ); };
 		00CFA89D106B4BA100758660 /* ForceFeedback.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00CFA89C106B4BA100758660 /* ForceFeedback.framework */; platformFilters = (macos, ); };
-		00D0D08410675DD9004B05EF /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00D0D08310675DD9004B05EF /* CoreFoundation.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Required, ); }; };
+		00D0D08410675DD9004B05EF /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00D0D08310675DD9004B05EF /* CoreFoundation.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Required, ); }; };
 		00D0D0D810675E46004B05EF /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007317C10858E15000B2BC32 /* Carbon.framework */; platformFilters = (macos, ); };
 		00D0D0D810675E46004B05EF /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007317C10858E15000B2BC32 /* Carbon.framework */; platformFilters = (macos, ); };
 		02D6A1C228A84B8F00A7F002 /* SDL_hidapi_sinput.c in Sources */ = {isa = PBXBuildFile; fileRef = 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */; };
 		02D6A1C228A84B8F00A7F002 /* SDL_hidapi_sinput.c in Sources */ = {isa = PBXBuildFile; fileRef = 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */; };
 		1485C3312BBA4AF30063985B /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */; platformFilters = (maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; };
 		1485C3312BBA4AF30063985B /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */; platformFilters = (maccatalyst, macos, ); settings = {ATTRIBUTES = (Weak, ); }; };
-		557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Weak, ); }; };
+		3AFD09EA2F9766BA00208BA9 /* SDL_CurvedUIShader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */; platformFilters = (xros, ); };
+		557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Weak, ); }; };
 		557D0CFB254586D7003913E3 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A75FDABD23E28B6200529352 /* GameController.framework */; settings = {ATTRIBUTES = (Required, ); }; };
 		557D0CFB254586D7003913E3 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A75FDABD23E28B6200529352 /* GameController.framework */; settings = {ATTRIBUTES = (Required, ); }; };
 		5616CA4C252BB2A6005D5928 /* SDL_url.c in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA49252BB2A5005D5928 /* SDL_url.c */; };
 		5616CA4C252BB2A6005D5928 /* SDL_url.c in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA49252BB2A5005D5928 /* SDL_url.c */; };
 		5616CA4D252BB2A6005D5928 /* SDL_sysurl.h in Headers */ = {isa = PBXBuildFile; fileRef = 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */; };
 		5616CA4D252BB2A6005D5928 /* SDL_sysurl.h in Headers */ = {isa = PBXBuildFile; fileRef = 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */; };
@@ -82,8 +83,8 @@
 		A1626A522617008D003F1973 /* SDL_triangle.h in Headers */ = {isa = PBXBuildFile; fileRef = A1626A512617008C003F1973 /* SDL_triangle.h */; };
 		A1626A522617008D003F1973 /* SDL_triangle.h in Headers */ = {isa = PBXBuildFile; fileRef = A1626A512617008C003F1973 /* SDL_triangle.h */; };
 		A1BB8B6327F6CF330057CFA8 /* SDL_list.c in Sources */ = {isa = PBXBuildFile; fileRef = A1BB8B6127F6CF320057CFA8 /* SDL_list.c */; };
 		A1BB8B6327F6CF330057CFA8 /* SDL_list.c in Sources */ = {isa = PBXBuildFile; fileRef = A1BB8B6127F6CF320057CFA8 /* SDL_list.c */; };
 		A1BB8B6C27F6CF330057CFA8 /* SDL_list.h in Headers */ = {isa = PBXBuildFile; fileRef = A1BB8B6227F6CF330057CFA8 /* SDL_list.h */; };
 		A1BB8B6C27F6CF330057CFA8 /* SDL_list.h in Headers */ = {isa = PBXBuildFile; fileRef = A1BB8B6227F6CF330057CFA8 /* SDL_list.h */; };
-		A7381E961D8B69D600B177DD /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7381E951D8B69D600B177DD /* CoreAudio.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Required, ); }; };
-		A7381E971D8B6A0300B177DD /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7381E931D8B69C300B177DD /* AudioToolbox.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); };
+		A7381E961D8B69D600B177DD /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7381E951D8B69D600B177DD /* CoreAudio.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Required, ); }; };
+		A7381E971D8B6A0300B177DD /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7381E931D8B69C300B177DD /* AudioToolbox.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); };
 		A75FDB5823E39E6100529352 /* hidapi.h in Headers */ = {isa = PBXBuildFile; fileRef = A75FDB5723E39E6100529352 /* hidapi.h */; };
 		A75FDB5823E39E6100529352 /* hidapi.h in Headers */ = {isa = PBXBuildFile; fileRef = A75FDB5723E39E6100529352 /* hidapi.h */; };
 		A75FDBC523EA380300529352 /* SDL_hidapi_rumble.h in Headers */ = {isa = PBXBuildFile; fileRef = A75FDBC323EA380300529352 /* SDL_hidapi_rumble.h */; };
 		A75FDBC523EA380300529352 /* SDL_hidapi_rumble.h in Headers */ = {isa = PBXBuildFile; fileRef = A75FDBC323EA380300529352 /* SDL_hidapi_rumble.h */; };
 		A75FDBCE23EA380300529352 /* SDL_hidapi_rumble.c in Sources */ = {isa = PBXBuildFile; fileRef = A75FDBC423EA380300529352 /* SDL_hidapi_rumble.c */; };
 		A75FDBCE23EA380300529352 /* SDL_hidapi_rumble.c in Sources */ = {isa = PBXBuildFile; fileRef = A75FDBC423EA380300529352 /* SDL_hidapi_rumble.c */; };
@@ -368,8 +369,6 @@
 		E4F257972C81903800FCEAFC /* SDL_sysgpu.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F257862C81903800FCEAFC /* SDL_sysgpu.h */; };
 		E4F257972C81903800FCEAFC /* SDL_sysgpu.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F257862C81903800FCEAFC /* SDL_sysgpu.h */; };
 		E4F257982C81903800FCEAFC /* SDL_gpu_openxr.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */; };
 		E4F257982C81903800FCEAFC /* SDL_gpu_openxr.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */; };
 		E4F257992C81903800FCEAFC /* SDL_openxrdyn.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */; };
 		E4F257992C81903800FCEAFC /* SDL_openxrdyn.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */; };
-		E4F2579A2C81903800FCEAFC /* SDL_gpu_openxr_c.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */; };
-		E4F2579B2C81903800FCEAFC /* SDL_openxr_internal.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */; };
 		E4F7981A2AD8D84800669F54 /* SDL_core_unsupported.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F798192AD8D84800669F54 /* SDL_core_unsupported.c */; };
 		E4F7981A2AD8D84800669F54 /* SDL_core_unsupported.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F798192AD8D84800669F54 /* SDL_core_unsupported.c */; };
 		E4F7981C2AD8D85500669F54 /* SDL_dynapi_unsupported.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F7981B2AD8D85500669F54 /* SDL_dynapi_unsupported.h */; };
 		E4F7981C2AD8D85500669F54 /* SDL_dynapi_unsupported.h in Headers */ = {isa = PBXBuildFile; fileRef = E4F7981B2AD8D85500669F54 /* SDL_dynapi_unsupported.h */; };
 		E4F7981E2AD8D86A00669F54 /* SDL_render_unsupported.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F7981D2AD8D86A00669F54 /* SDL_render_unsupported.c */; };
 		E4F7981E2AD8D86A00669F54 /* SDL_render_unsupported.c in Sources */ = {isa = PBXBuildFile; fileRef = E4F7981D2AD8D86A00669F54 /* SDL_render_unsupported.c */; };
@@ -434,6 +433,13 @@
 		F3990E062A788303000D8759 /* SDL_hidapi_ios.h in Headers */ = {isa = PBXBuildFile; fileRef = F3990E032A788303000D8759 /* SDL_hidapi_ios.h */; };
 		F3990E062A788303000D8759 /* SDL_hidapi_ios.h in Headers */ = {isa = PBXBuildFile; fileRef = F3990E032A788303000D8759 /* SDL_hidapi_ios.h */; };
 		F3990E072A78833C000D8759 /* hid.m in Sources */ = {isa = PBXBuildFile; fileRef = A75FDAA523E2792500529352 /* hid.m */; };
 		F3990E072A78833C000D8759 /* hid.m in Sources */ = {isa = PBXBuildFile; fileRef = A75FDAA523E2792500529352 /* hid.m */; };
 		F3A4909E2554D38600E92A8B /* SDL_hidapi_ps5.c in Sources */ = {isa = PBXBuildFile; fileRef = F3A4909D2554D38500E92A8B /* SDL_hidapi_ps5.c */; };
 		F3A4909E2554D38600E92A8B /* SDL_hidapi_ps5.c in Sources */ = {isa = PBXBuildFile; fileRef = F3A4909D2554D38500E92A8B /* SDL_hidapi_ps5.c */; };
+		F3A8371C2F69C80100AD32B6 /* SDL_RealityKitHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A837162F69C80100AD32B6 /* SDL_RealityKitHelper.swift */; platformFilters = (xros, ); };
+		F3A895712F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8956D2F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift */; platformFilters = (xros, ); };
+		F3A895722F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8956E2F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift */; platformFilters = (xros, ); };
+		F3A895792F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h in Headers */ = {isa = PBXBuildFile; fileRef = F3A895772F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h */; platformFilters = (xros, ); };
+		F3A8957A2F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h in Headers */ = {isa = PBXBuildFile; fileRef = F3A895782F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h */; platformFilters = (xros, ); };
+		F3A8957B2F7DC14400B9E5C2 /* SDL_UIKitBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = F3A895762F7DC14400B9E5C2 /* SDL_UIKitBridge.m */; platformFilters = (xros, ); };
+		F3A8957D2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8957C2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift */; platformFilters = (xros, ); };
 		F3A9AE982C8A13C100AAC390 /* SDL_gpu_util.h in Headers */ = {isa = PBXBuildFile; fileRef = F3A9AE922C8A13C100AAC390 /* SDL_gpu_util.h */; };
 		F3A9AE982C8A13C100AAC390 /* SDL_gpu_util.h in Headers */ = {isa = PBXBuildFile; fileRef = F3A9AE922C8A13C100AAC390 /* SDL_gpu_util.h */; };
 		F3A9AE992C8A13C100AAC390 /* SDL_render_gpu.c in Sources */ = {isa = PBXBuildFile; fileRef = F3A9AE932C8A13C100AAC390 /* SDL_render_gpu.c */; };
 		F3A9AE992C8A13C100AAC390 /* SDL_render_gpu.c in Sources */ = {isa = PBXBuildFile; fileRef = F3A9AE932C8A13C100AAC390 /* SDL_render_gpu.c */; };
 		F3A9AE9A2C8A13C100AAC390 /* SDL_shaders_gpu.c in Sources */ = {isa = PBXBuildFile; fileRef = F3A9AE942C8A13C100AAC390 /* SDL_shaders_gpu.c */; };
 		F3A9AE9A2C8A13C100AAC390 /* SDL_shaders_gpu.c in Sources */ = {isa = PBXBuildFile; fileRef = F3A9AE942C8A13C100AAC390 /* SDL_shaders_gpu.c */; };
@@ -559,7 +565,7 @@
 		F3FBB10A2DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c in Sources */ = {isa = PBXBuildFile; fileRef = F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */; };
 		F3FBB10A2DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c in Sources */ = {isa = PBXBuildFile; fileRef = F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */; };
 		F3FD042E2C9B755700824C4C /* SDL_hidapi_nintendo.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */; };
 		F3FD042E2C9B755700824C4C /* SDL_hidapi_nintendo.h in Headers */ = {isa = PBXBuildFile; fileRef = F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */; };
 		F3FD042F2C9B755700824C4C /* SDL_hidapi_steam_hori.c in Sources */ = {isa = PBXBuildFile; fileRef = F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */; };
 		F3FD042F2C9B755700824C4C /* SDL_hidapi_steam_hori.c in Sources */ = {isa = PBXBuildFile; fileRef = F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */; };
-		FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); settings = {ATTRIBUTES = (Required, ); }; };
+		FA73671D19A540EF004122E4 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA73671C19A540EF004122E4 /* CoreVideo.framework */; platformFilters = (ios, maccatalyst, macos, tvos, xros, ); settings = {ATTRIBUTES = (Required, ); }; };
 /* End PBXBuildFile section */
 /* End PBXBuildFile section */
 
 
 /* Begin PBXContainerItemProxy section */
 /* Begin PBXContainerItemProxy section */
@@ -617,6 +623,7 @@
 		00D0D08310675DD9004B05EF /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
 		00D0D08310675DD9004B05EF /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
 		02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = "<group>"; };
 		02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = "<group>"; };
 		1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
 		1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
+		3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedUIShader.swift; sourceTree = "<group>"; };
 		5616CA49252BB2A5005D5928 /* SDL_url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_url.c; sourceTree = "<group>"; };
 		5616CA49252BB2A5005D5928 /* SDL_url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_url.c; sourceTree = "<group>"; };
 		5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysurl.h; sourceTree = "<group>"; };
 		5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysurl.h; sourceTree = "<group>"; };
 		5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDL_sysurl.m; sourceTree = "<group>"; };
 		5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDL_sysurl.m; sourceTree = "<group>"; };
@@ -970,7 +977,6 @@
 		F338A1192D1B37E4007CDFDF /* SDL_tray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_tray.c; sourceTree = "<group>"; };
 		F338A1192D1B37E4007CDFDF /* SDL_tray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_tray.c; sourceTree = "<group>"; };
 		F3395BA72D9A5971007246C8 /* SDL_hidapi_8bitdo.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_8bitdo.c; sourceTree = "<group>"; };
 		F3395BA72D9A5971007246C8 /* SDL_hidapi_8bitdo.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_8bitdo.c; sourceTree = "<group>"; };
 		F3395BA72D9A5971007246C9 /* SDL_hidapi_flydigi.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_flydigi.c; sourceTree = "<group>"; };
 		F3395BA72D9A5971007246C9 /* SDL_hidapi_flydigi.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_flydigi.c; sourceTree = "<group>"; };
-		F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_gamesir.c; sourceTree = "<group>"; };
 		F344003C2D4022E1003F26D7 /* INSTALL.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = INSTALL.md; sourceTree = "<group>"; };
 		F344003C2D4022E1003F26D7 /* INSTALL.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = INSTALL.md; sourceTree = "<group>"; };
 		F362B9152B3349E200D30B94 /* controller_list.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = controller_list.h; sourceTree = "<group>"; };
 		F362B9152B3349E200D30B94 /* controller_list.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = controller_list.h; sourceTree = "<group>"; };
 		F362B9162B3349E200D30B94 /* SDL_gamepad_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gamepad_c.h; sourceTree = "<group>"; };
 		F362B9162B3349E200D30B94 /* SDL_gamepad_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gamepad_c.h; sourceTree = "<group>"; };
@@ -1026,6 +1032,13 @@
 		F3990E022A788303000D8759 /* SDL_hidapi_mac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_mac.h; sourceTree = "<group>"; };
 		F3990E022A788303000D8759 /* SDL_hidapi_mac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_mac.h; sourceTree = "<group>"; };
 		F3990E032A788303000D8759 /* SDL_hidapi_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_ios.h; sourceTree = "<group>"; };
 		F3990E032A788303000D8759 /* SDL_hidapi_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_ios.h; sourceTree = "<group>"; };
 		F3A4909D2554D38500E92A8B /* SDL_hidapi_ps5.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps5.c; sourceTree = "<group>"; };
 		F3A4909D2554D38500E92A8B /* SDL_hidapi_ps5.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps5.c; sourceTree = "<group>"; };
+		F3A837162F69C80100AD32B6 /* SDL_RealityKitHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_RealityKitHelper.swift; sourceTree = "<group>"; };
+		F3A8956D2F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedContentHosting.swift; sourceTree = "<group>"; };
+		F3A8956E2F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedContentView.swift; sourceTree = "<group>"; };
+		F3A895762F7DC14400B9E5C2 /* SDL_UIKitBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDL_UIKitBridge.m; sourceTree = "<group>"; };
+		F3A895772F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDL_UIKitBridge-objc.h"; sourceTree = "<group>"; };
+		F3A895782F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDL_UIKitBridge-swift.h"; sourceTree = "<group>"; };
+		F3A8957C2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_uikitviewcontroller.swift; sourceTree = "<group>"; };
 		F3A9AE922C8A13C100AAC390 /* SDL_gpu_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gpu_util.h; sourceTree = "<group>"; };
 		F3A9AE922C8A13C100AAC390 /* SDL_gpu_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gpu_util.h; sourceTree = "<group>"; };
 		F3A9AE932C8A13C100AAC390 /* SDL_render_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_render_gpu.c; sourceTree = "<group>"; };
 		F3A9AE932C8A13C100AAC390 /* SDL_render_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_render_gpu.c; sourceTree = "<group>"; };
 		F3A9AE942C8A13C100AAC390 /* SDL_shaders_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_shaders_gpu.c; sourceTree = "<group>"; };
 		F3A9AE942C8A13C100AAC390 /* SDL_shaders_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_shaders_gpu.c; sourceTree = "<group>"; };
@@ -1149,6 +1162,7 @@
 		F3FA5A1A2B59ACE000FEAD97 /* yuv_rgb_lsx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = yuv_rgb_lsx.c; sourceTree = "<group>"; };
 		F3FA5A1A2B59ACE000FEAD97 /* yuv_rgb_lsx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = yuv_rgb_lsx.c; sourceTree = "<group>"; };
 		F3FA5A1B2B59ACE000FEAD97 /* yuv_rgb_lsx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_lsx.h; sourceTree = "<group>"; };
 		F3FA5A1B2B59ACE000FEAD97 /* yuv_rgb_lsx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_lsx.h; sourceTree = "<group>"; };
 		F3FA5A1C2B59ACE000FEAD97 /* yuv_rgb_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_common.h; sourceTree = "<group>"; };
 		F3FA5A1C2B59ACE000FEAD97 /* yuv_rgb_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_common.h; sourceTree = "<group>"; };
+		F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_gamesir.c; sourceTree = "<group>"; };
 		F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_nintendo.h; sourceTree = "<group>"; };
 		F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_nintendo.h; sourceTree = "<group>"; };
 		F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_hori.c; sourceTree = "<group>"; };
 		F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_hori.c; sourceTree = "<group>"; };
 		F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = "<group>"; };
 		F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = "<group>"; };
@@ -1738,8 +1752,15 @@
 		A7D8A61823E2513D00DCD162 /* uikit */ = {
 		A7D8A61823E2513D00DCD162 /* uikit */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				F3A8956D2F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift */,
+				F3A8956E2F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift */,
+				3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */,
+				F3A837162F69C80100AD32B6 /* SDL_RealityKitHelper.swift */,
 				A7D8A62F23E2513D00DCD162 /* SDL_uikitappdelegate.h */,
 				A7D8A62F23E2513D00DCD162 /* SDL_uikitappdelegate.h */,
 				A7D8A61E23E2513D00DCD162 /* SDL_uikitappdelegate.m */,
 				A7D8A61E23E2513D00DCD162 /* SDL_uikitappdelegate.m */,
+				F3A895762F7DC14400B9E5C2 /* SDL_UIKitBridge.m */,
+				F3A895772F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h */,
+				F3A895782F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h */,
 				A7D8A62123E2513D00DCD162 /* SDL_uikitclipboard.h */,
 				A7D8A62123E2513D00DCD162 /* SDL_uikitclipboard.h */,
 				A7D8A62A23E2513D00DCD162 /* SDL_uikitclipboard.m */,
 				A7D8A62A23E2513D00DCD162 /* SDL_uikitclipboard.m */,
 				A7D8A62D23E2513D00DCD162 /* SDL_uikitevents.h */,
 				A7D8A62D23E2513D00DCD162 /* SDL_uikitevents.h */,
@@ -1754,18 +1775,19 @@
 				A7D8A62323E2513D00DCD162 /* SDL_uikitopengles.m */,
 				A7D8A62323E2513D00DCD162 /* SDL_uikitopengles.m */,
 				A7D8A62B23E2513D00DCD162 /* SDL_uikitopenglview.h */,
 				A7D8A62B23E2513D00DCD162 /* SDL_uikitopenglview.h */,
 				A7D8A62023E2513D00DCD162 /* SDL_uikitopenglview.m */,
 				A7D8A62023E2513D00DCD162 /* SDL_uikitopenglview.m */,
+				000063D3D80F97ADC7770000 /* SDL_uikitpen.h */,
+				000053D344416737F6050000 /* SDL_uikitpen.m */,
 				A7D8A62223E2513D00DCD162 /* SDL_uikitvideo.h */,
 				A7D8A62223E2513D00DCD162 /* SDL_uikitvideo.h */,
 				A7D8A63223E2513D00DCD162 /* SDL_uikitvideo.m */,
 				A7D8A63223E2513D00DCD162 /* SDL_uikitvideo.m */,
 				A7D8A61923E2513D00DCD162 /* SDL_uikitview.h */,
 				A7D8A61923E2513D00DCD162 /* SDL_uikitview.h */,
 				A7D8A62923E2513D00DCD162 /* SDL_uikitview.m */,
 				A7D8A62923E2513D00DCD162 /* SDL_uikitview.m */,
 				A7D8A62423E2513D00DCD162 /* SDL_uikitviewcontroller.h */,
 				A7D8A62423E2513D00DCD162 /* SDL_uikitviewcontroller.h */,
 				A7D8A63023E2513D00DCD162 /* SDL_uikitviewcontroller.m */,
 				A7D8A63023E2513D00DCD162 /* SDL_uikitviewcontroller.m */,
+				F3A8957C2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift */,
 				A7D8A63323E2513D00DCD162 /* SDL_uikitvulkan.h */,
 				A7D8A63323E2513D00DCD162 /* SDL_uikitvulkan.h */,
 				A7D8A62523E2513D00DCD162 /* SDL_uikitvulkan.m */,
 				A7D8A62523E2513D00DCD162 /* SDL_uikitvulkan.m */,
 				A7D8A62723E2513D00DCD162 /* SDL_uikitwindow.h */,
 				A7D8A62723E2513D00DCD162 /* SDL_uikitwindow.h */,
 				A7D8A61A23E2513D00DCD162 /* SDL_uikitwindow.m */,
 				A7D8A61A23E2513D00DCD162 /* SDL_uikitwindow.m */,
-				000063D3D80F97ADC7770000 /* SDL_uikitpen.h */,
-				000053D344416737F6050000 /* SDL_uikitpen.m */,
 			);
 			);
 			path = uikit;
 			path = uikit;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2365,17 +2387,6 @@
 			path = vulkan;
 			path = vulkan;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
-		E4F2578B2C81903800FCEAFC /* xr */ = {
-			isa = PBXGroup;
-			children = (
-				E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */,
-				E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */,
-				E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */,
-				E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */,
-			);
-			path = xr;
-			sourceTree = "<group>";
-		};
 		E4F257872C81903800FCEAFC /* gpu */ = {
 		E4F257872C81903800FCEAFC /* gpu */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -2388,6 +2399,17 @@
 			path = gpu;
 			path = gpu;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		E4F2578B2C81903800FCEAFC /* xr */ = {
+			isa = PBXGroup;
+			children = (
+				E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */,
+				E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */,
+				E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */,
+				E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */,
+			);
+			path = xr;
+			sourceTree = "<group>";
+		};
 		F338A1142D1B3735007CDFDF /* tray */ = {
 		F338A1142D1B3735007CDFDF /* tray */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -2561,6 +2583,8 @@
 				F3EFA5ED2D5AB97300BCF22F /* SDL_stb_c.h in Headers */,
 				F3EFA5ED2D5AB97300BCF22F /* SDL_stb_c.h in Headers */,
 				F3EFA5EE2D5AB97300BCF22F /* stb_image.h in Headers */,
 				F3EFA5EE2D5AB97300BCF22F /* stb_image.h in Headers */,
 				F3EFA5EF2D5AB97300BCF22F /* SDL_surface_c.h in Headers */,
 				F3EFA5EF2D5AB97300BCF22F /* SDL_surface_c.h in Headers */,
+				F3A895792F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h in Headers */,
+				F3A8957A2F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h in Headers */,
 				A7D8AE8E23E2514100DCD162 /* SDL_cocoakeyboard.h in Headers */,
 				A7D8AE8E23E2514100DCD162 /* SDL_cocoakeyboard.h in Headers */,
 				A7D8AF0623E2514100DCD162 /* SDL_cocoamessagebox.h in Headers */,
 				A7D8AF0623E2514100DCD162 /* SDL_cocoamessagebox.h in Headers */,
 				A7D8AEB223E2514100DCD162 /* SDL_cocoametalview.h in Headers */,
 				A7D8AEB223E2514100DCD162 /* SDL_cocoametalview.h in Headers */,
@@ -2843,6 +2867,9 @@
 			attributes = {
 			attributes = {
 				LastUpgradeCheck = 1130;
 				LastUpgradeCheck = 1130;
 				TargetAttributes = {
 				TargetAttributes = {
+					BECDF5FE0761BA81005FE872 = {
+						LastSwiftMigration = 2630;
+					};
 					F3676F582A7885080091160D = {
 					F3676F582A7885080091160D = {
 						CreatedOnToolsVersion = 14.3.1;
 						CreatedOnToolsVersion = 14.3.1;
 					};
 					};
@@ -2931,6 +2958,7 @@
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
 				A7D8B9E323E2514400DCD162 /* SDL_drawline.c in Sources */,
 				A7D8B9E323E2514400DCD162 /* SDL_drawline.c in Sources */,
+				F3A8957B2F7DC14400B9E5C2 /* SDL_UIKitBridge.m in Sources */,
 				A7D8AE7C23E2514100DCD162 /* SDL_yuv.c in Sources */,
 				A7D8AE7C23E2514100DCD162 /* SDL_yuv.c in Sources */,
 				A7D8B62F23E2514300DCD162 /* SDL_sysfilesystem.m in Sources */,
 				A7D8B62F23E2514300DCD162 /* SDL_sysfilesystem.m in Sources */,
 				A7D8B41C23E2514300DCD162 /* SDL_systls.c in Sources */,
 				A7D8B41C23E2514300DCD162 /* SDL_systls.c in Sources */,
@@ -3059,6 +3087,7 @@
 				F3B439512C935C2400792030 /* SDL_dummyprocess.c in Sources */,
 				F3B439512C935C2400792030 /* SDL_dummyprocess.c in Sources */,
 				A7D8B76423E2514300DCD162 /* SDL_mixer.c in Sources */,
 				A7D8B76423E2514300DCD162 /* SDL_mixer.c in Sources */,
 				A7D8BB5723E2514500DCD162 /* SDL_events.c in Sources */,
 				A7D8BB5723E2514500DCD162 /* SDL_events.c in Sources */,
+				F3A8957D2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift in Sources */,
 				A7D8ADE623E2514100DCD162 /* SDL_blit_0.c in Sources */,
 				A7D8ADE623E2514100DCD162 /* SDL_blit_0.c in Sources */,
 				89E5801E2D03602200DAF6D3 /* SDL_hidapi_lg4ff.c in Sources */,
 				89E5801E2D03602200DAF6D3 /* SDL_hidapi_lg4ff.c in Sources */,
 				A7D8B8A823E2514400DCD162 /* SDL_diskaudio.c in Sources */,
 				A7D8B8A823E2514400DCD162 /* SDL_diskaudio.c in Sources */,
@@ -3086,6 +3115,7 @@
 				A7D8B56323E2514300DCD162 /* SDL_hidapi_gamecube.c in Sources */,
 				A7D8B56323E2514300DCD162 /* SDL_hidapi_gamecube.c in Sources */,
 				A7D8B4DC23E2514300DCD162 /* SDL_joystick.c in Sources */,
 				A7D8B4DC23E2514300DCD162 /* SDL_joystick.c in Sources */,
 				A7D8BA4923E2514400DCD162 /* SDL_render_gles2.c in Sources */,
 				A7D8BA4923E2514400DCD162 /* SDL_render_gles2.c in Sources */,
+				F3A8371C2F69C80100AD32B6 /* SDL_RealityKitHelper.swift in Sources */,
 				A7D8AC2D23E2514100DCD162 /* SDL_surface.c in Sources */,
 				A7D8AC2D23E2514100DCD162 /* SDL_surface.c in Sources */,
 				A7D8B54B23E2514300DCD162 /* SDL_hidapi_xboxone.c in Sources */,
 				A7D8B54B23E2514300DCD162 /* SDL_hidapi_xboxone.c in Sources */,
 				A7D8AD2323E2514100DCD162 /* SDL_blit_auto.c in Sources */,
 				A7D8AD2323E2514100DCD162 /* SDL_blit_auto.c in Sources */,
@@ -3141,6 +3171,8 @@
 				A7D8A94B23E2514000DCD162 /* SDL.c in Sources */,
 				A7D8A94B23E2514000DCD162 /* SDL.c in Sources */,
 				A7D8AEA023E2514100DCD162 /* SDL_cocoavulkan.m in Sources */,
 				A7D8AEA023E2514100DCD162 /* SDL_cocoavulkan.m in Sources */,
 				A7D8AB6123E2514100DCD162 /* SDL_offscreenwindow.c in Sources */,
 				A7D8AB6123E2514100DCD162 /* SDL_offscreenwindow.c in Sources */,
+				F3A895712F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift in Sources */,
+				F3A895722F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift in Sources */,
 				566E26D8246274CC00718109 /* SDL_locale.c in Sources */,
 				566E26D8246274CC00718109 /* SDL_locale.c in Sources */,
 				63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */,
 				63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */,
 				000040E76FDC6AE48CBF0000 /* SDL_hashtable.c in Sources */,
 				000040E76FDC6AE48CBF0000 /* SDL_hashtable.c in Sources */,
@@ -3153,6 +3185,7 @@
 				00002B20A48E055EB0350000 /* SDL_camera_coremedia.m in Sources */,
 				00002B20A48E055EB0350000 /* SDL_camera_coremedia.m in Sources */,
 				000080903BC03006F24E0000 /* SDL_filesystem.c in Sources */,
 				000080903BC03006F24E0000 /* SDL_filesystem.c in Sources */,
 				F3FBB1082DDF93AB0000F99F /* SDL_hidapi_flydigi.c in Sources */,
 				F3FBB1082DDF93AB0000F99F /* SDL_hidapi_flydigi.c in Sources */,
+				3AFD09EA2F9766BA00208BA9 /* SDL_CurvedUIShader.swift in Sources */,
 				F3FBB10A2DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c in Sources */,
 				F3FBB10A2DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c in Sources */,
 				0000481D255AF155B42C0000 /* SDL_sysfsops.c in Sources */,
 				0000481D255AF155B42C0000 /* SDL_sysfsops.c in Sources */,
 				0000494CC93F3E624D3C0000 /* SDL_systime.c in Sources */,
 				0000494CC93F3E624D3C0000 /* SDL_systime.c in Sources */,
@@ -3239,13 +3272,18 @@
 			isa = XCBuildConfiguration;
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = F3F7BE3B2CBD79D200C984AF /* config.xcconfig */;
 			baseConfigurationReference = F3F7BE3B2CBD79D200C984AF /* config.xcconfig */;
 			buildSettings = {
 			buildSettings = {
+				CLANG_ENABLE_MODULES = YES;
 				CLANG_LINK_OBJC_RUNTIME = NO;
 				CLANG_LINK_OBJC_RUNTIME = NO;
-				GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				DEFINES_MODULE = YES;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
 				EXPORTED_SYMBOLS_FILE = "$(SRCROOT)/../../src/dynapi/SDL_dynapi.exports";
 				EXPORTED_SYMBOLS_FILE = "$(SRCROOT)/../../src/dynapi/SDL_dynapi.exports";
+				GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION;
 				OTHER_LDFLAGS = "-liconv";
 				OTHER_LDFLAGS = "-liconv";
 				SUPPORTS_MACCATALYST = YES;
 				SUPPORTS_MACCATALYST = YES;
+				"SWIFT_OBJC_BRIDGING_HEADER[sdk=xr*]" = "../../src/video/uikit/SDL_UIKitBridge-swift.h";
+				SWIFT_VERSION = 6.0;
+				XROS_DEPLOYMENT_TARGET = 26.0;
 			};
 			};
 			name = Release;
 			name = Release;
 		};
 		};
@@ -3305,13 +3343,19 @@
 			isa = XCBuildConfiguration;
 			isa = XCBuildConfiguration;
 			baseConfigurationReference = F3F7BE3B2CBD79D200C984AF /* config.xcconfig */;
 			baseConfigurationReference = F3F7BE3B2CBD79D200C984AF /* config.xcconfig */;
 			buildSettings = {
 			buildSettings = {
+				CLANG_ENABLE_MODULES = YES;
 				CLANG_LINK_OBJC_RUNTIME = NO;
 				CLANG_LINK_OBJC_RUNTIME = NO;
-				GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				DEFINES_MODULE = YES;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
 				GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
 				EXPORTED_SYMBOLS_FILE = "$(SRCROOT)/../../src/dynapi/SDL_dynapi.exports";
 				EXPORTED_SYMBOLS_FILE = "$(SRCROOT)/../../src/dynapi/SDL_dynapi.exports";
+				GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION;
 				OTHER_LDFLAGS = "-liconv";
 				OTHER_LDFLAGS = "-liconv";
 				SUPPORTS_MACCATALYST = YES;
 				SUPPORTS_MACCATALYST = YES;
+				"SWIFT_OBJC_BRIDGING_HEADER[sdk=xr*]" = "../../src/video/uikit/SDL_UIKitBridge-swift.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 6.0;
+				XROS_DEPLOYMENT_TARGET = 26.0;
 			};
 			};
 			name = Debug;
 			name = Debug;
 		};
 		};

+ 2 - 1
include/SDL3/SDL_events.h

@@ -163,8 +163,9 @@ typedef enum SDL_EventType
                                              associated with the window. Otherwise, the handle has already been destroyed and all resources
                                              associated with the window. Otherwise, the handle has already been destroyed and all resources
                                              associated with it are invalid */
                                              associated with it are invalid */
     SDL_EVENT_WINDOW_HDR_STATE_CHANGED, /**< Window HDR properties have changed */
     SDL_EVENT_WINDOW_HDR_STATE_CHANGED, /**< Window HDR properties have changed */
+    SDL_EVENT_WINDOW_CURVATURE_CHANGED, /**< Window curvature has changed to data1 (on visionOS) */
     SDL_EVENT_WINDOW_FIRST = SDL_EVENT_WINDOW_SHOWN,
     SDL_EVENT_WINDOW_FIRST = SDL_EVENT_WINDOW_SHOWN,
-    SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_HDR_STATE_CHANGED,
+    SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_CURVATURE_CHANGED,
 
 
     /* Keyboard events */
     /* Keyboard events */
     SDL_EVENT_KEY_DOWN        = 0x300, /**< Key pressed */
     SDL_EVENT_KEY_DOWN        = 0x300, /**< Key pressed */

+ 11 - 0
include/SDL3/SDL_video.h

@@ -1384,6 +1384,11 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren
  *   popup windows and have the behaviors and guidelines outlined in
  *   popup windows and have the behaviors and guidelines outlined in
  *   SDL_CreatePopupWindow().
  *   SDL_CreatePopupWindow().
  *
  *
+ * These are additional supported properties with visionOS:
+ *
+ * - `SDL_PROP_WINDOW_CREATE_CURVATURE_FLOAT`: the curvature of the window on visionOS. Curved windows have square corners and additional controls for more immersive gaming.
+ * This can be -1 (disabled), which is the default, 0 (no curve), or set to a specific curvature radius in millimeters. A common value for a gaming monitor is 1000.
+ *
  * If this window is being created to be used with an SDL_Renderer, you should
  * If this window is being created to be used with an SDL_Renderer, you should
  * not add a graphics API specific property
  * not add a graphics API specific property
  * (`SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN`, etc), as SDL will handle that
  * (`SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN`, etc), as SDL will handle that
@@ -1446,6 +1451,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop
 #define SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER                   "SDL.window.create.x11.window"
 #define SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER                   "SDL.window.create.x11.window"
 #define SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_CANVAS_ID_STRING         "SDL.window.create.emscripten.canvas_id"
 #define SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_CANVAS_ID_STRING         "SDL.window.create.emscripten.canvas_id"
 #define SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING  "SDL.window.create.emscripten.keyboard_element"
 #define SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING  "SDL.window.create.emscripten.keyboard_element"
+#define SDL_PROP_WINDOW_CREATE_CURVATURE_FLOAT                     "SDL.window.create.curvature"
 
 
 /**
 /**
  * Get the numeric ID of a window.
  * Get the numeric ID of a window.
@@ -1624,6 +1630,10 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window)
  * - `SDL_PROP_WINDOW_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING`: the keyboard
  * - `SDL_PROP_WINDOW_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING`: the keyboard
  *   element that associates keyboard events to this window
  *   element that associates keyboard events to this window
  *
  *
+ * On visionOS:
+ *
+ * - `SDL_PROP_WINDOW_CURVATURE_FLOAT`: the curvature of the window in curved mode on visionOS. This value is updated dynamically when changed via the screen ornaments. This can be 0 (no curve), or a specific curvature radius in millimeters. A common value for a gaming monitor is 1000.
+ *
  * \param window the window to query.
  * \param window the window to query.
  * \returns a valid property ID on success or 0 on failure; call
  * \returns a valid property ID on success or 0 on failure; call
  *          SDL_GetError() for more information.
  *          SDL_GetError() for more information.
@@ -1673,6 +1683,7 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetWindowProperties(SDL_Window
 #define SDL_PROP_WINDOW_X11_WINDOW_NUMBER                           "SDL.window.x11.window"
 #define SDL_PROP_WINDOW_X11_WINDOW_NUMBER                           "SDL.window.x11.window"
 #define SDL_PROP_WINDOW_EMSCRIPTEN_CANVAS_ID_STRING                 "SDL.window.emscripten.canvas_id"
 #define SDL_PROP_WINDOW_EMSCRIPTEN_CANVAS_ID_STRING                 "SDL.window.emscripten.canvas_id"
 #define SDL_PROP_WINDOW_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING          "SDL.window.emscripten.keyboard_element"
 #define SDL_PROP_WINDOW_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING          "SDL.window.emscripten.keyboard_element"
+#define SDL_PROP_WINDOW_CURVATURE_FLOAT                             "SDL.window.curvature"
 
 
 /**
 /**
  * Get the window flags.
  * Get the window flags.

+ 1 - 0
src/events/SDL_events.c

@@ -565,6 +565,7 @@ int SDL_GetEventDescription(const SDL_Event *event, char *buf, int buflen)
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_LEAVE_FULLSCREEN);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_LEAVE_FULLSCREEN);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_DESTROYED);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_DESTROYED);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_HDR_STATE_CHANGED);
         SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_HDR_STATE_CHANGED);
+        SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_CURVATURE_CHANGED);
 #undef SDL_WINDOWEVENT_CASE
 #undef SDL_WINDOWEVENT_CASE
 
 
 #define PRINT_KEYDEV_EVENT(event) (void)SDL_snprintf(details, sizeof(details), " (timestamp=%" SDL_PRIu64 " which=%u)", event->kdevice.timestamp, (uint)event->kdevice.which)
 #define PRINT_KEYDEV_EVENT(event) (void)SDL_snprintf(details, sizeof(details), " (timestamp=%" SDL_PRIu64 " which=%u)", event->kdevice.timestamp, (uint)event->kdevice.which)

+ 5 - 0
src/render/SDL_render.c

@@ -177,7 +177,12 @@ bool SDL_AddSupportedTextureFormat(SDL_Renderer *renderer, SDL_PixelFormat forma
 
 
 void SDL_SetupRendererColorspace(SDL_Renderer *renderer, SDL_PropertiesID props)
 void SDL_SetupRendererColorspace(SDL_Renderer *renderer, SDL_PropertiesID props)
 {
 {
+#ifdef SDL_PLATFORM_VISIONOS
+    // The RealityKit texture always renders in linear colorspace
+    renderer->output_colorspace = SDL_COLORSPACE_SRGB_LINEAR;
+#else
     renderer->output_colorspace = (SDL_Colorspace)SDL_GetNumberProperty(props, SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB);
     renderer->output_colorspace = (SDL_Colorspace)SDL_GetNumberProperty(props, SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB);
+#endif
 }
 }
 
 
 bool SDL_RenderingLinearSpace(SDL_Renderer *renderer)
 bool SDL_RenderingLinearSpace(SDL_Renderer *renderer)

+ 108 - 13
src/render/metal/SDL_render_metal.m

@@ -35,6 +35,9 @@
 #endif
 #endif
 #ifdef SDL_VIDEO_DRIVER_UIKIT
 #ifdef SDL_VIDEO_DRIVER_UIKIT
 #import <UIKit/UIKit.h>
 #import <UIKit/UIKit.h>
+#ifdef SDL_PLATFORM_VISIONOS
+#import "../../video/uikit/SDL_UIKitBridge-objc.h"
+#endif
 #endif
 #endif
 
 
 // Regenerate these with build-metal-shaders.sh
 // Regenerate these with build-metal-shaders.sh
@@ -139,6 +142,9 @@ typedef struct METAL_ShaderPipelines
 @property(nonatomic, assign) METAL_ShaderPipelines *activepipelines;
 @property(nonatomic, assign) METAL_ShaderPipelines *activepipelines;
 @property(nonatomic, assign) METAL_ShaderPipelines *allpipelines;
 @property(nonatomic, assign) METAL_ShaderPipelines *allpipelines;
 @property(nonatomic, assign) int pipelinescount;
 @property(nonatomic, assign) int pipelinescount;
+#ifdef SDL_PLATFORM_VISIONOS
+@property(nonatomic, retain) id<MTLTexture> mtlrealitykittexture;
+#endif
 @end
 @end
 
 
 @implementation SDL3METAL_RenderData
 @implementation SDL3METAL_RenderData
@@ -453,16 +459,25 @@ static bool METAL_ActivateRenderCommandEncoder(SDL_Renderer *renderer, MTLLoadAc
             SDL3METAL_TextureData *texdata = (__bridge SDL3METAL_TextureData *)renderer->target->internal;
             SDL3METAL_TextureData *texdata = (__bridge SDL3METAL_TextureData *)renderer->target->internal;
             mtltexture = texdata.mtltexture;
             mtltexture = texdata.mtltexture;
         } else {
         } else {
-            if (data.mtlbackbuffer == nil) {
-                /* The backbuffer's contents aren't guaranteed to persist after
-                 * presenting, so we can leave it undefined when loading it. */
-                data.mtlbackbuffer = [data.mtllayer nextDrawable];
-                if (load == MTLLoadActionLoad) {
-                    load = MTLLoadActionDontCare;
+#ifdef SDL_PLATFORM_VISIONOS
+            if (renderer->window && SDL_UIKit_IsCurvedWindow(renderer->window)) {
+                data.mtlrealitykittexture = SDL_UIKit_GetCurvedDisplayTexture(renderer->window, [data.mtlcmdqueue commandBuffer], (int)data.mtllayer.drawableSize.width, (int)data.mtllayer.drawableSize.height, data.mtllayer.pixelFormat);
+                mtltexture = data.mtlrealitykittexture;
+            } else
+#endif
+            {
+                // Standard rendering path: use CAMetalLayer drawable
+                if (data.mtlbackbuffer == nil) {
+                    // The backbuffer's contents aren't guaranteed to persist after
+                    // presenting, so we can leave it undefined when loading it.
+                    data.mtlbackbuffer = [data.mtllayer nextDrawable];
+                    if (load == MTLLoadActionLoad) {
+                        load = MTLLoadActionDontCare;
+                    }
+                }
+                if (data.mtlbackbuffer != nil) {
+                    mtltexture = data.mtlbackbuffer.texture;
                 }
                 }
-            }
-            if (data.mtlbackbuffer != nil) {
-                mtltexture = data.mtlbackbuffer.texture;
             }
             }
         }
         }
 
 
@@ -1922,12 +1937,57 @@ static bool METAL_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd
     }
     }
 }
 }
 
 
+#ifdef SDL_PLATFORM_VISIONOS
+static id<MTLTexture> METAL_CopyToStagingTexture(SDL_Renderer *renderer, id<MTLTexture> texture, SDL_Rect *rect)
+{
+    SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
+    MTLTextureDescriptor *desc;
+    id<MTLTexture> stagingtex;
+    id<MTLBlitCommandEncoder> blitcmd;
+
+    desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:texture.pixelFormat
+                                                              width:rect->w
+                                                             height:rect->h
+                                                          mipmapped:NO];
+    if (desc == nil) {
+        SDL_OutOfMemory();
+        return nil;
+    }
+
+    stagingtex = [data.mtldevice newTextureWithDescriptor:desc];
+    if (stagingtex == nil) {
+        SDL_OutOfMemory();
+        return nil;
+    }
+
+    blitcmd = [data.mtlcmdbuffer blitCommandEncoder];
+
+    [blitcmd copyFromTexture:texture
+                 sourceSlice:0
+                 sourceLevel:0
+                sourceOrigin:MTLOriginMake(rect->x, rect->y, 0)
+                  sourceSize:MTLSizeMake(rect->w, rect->h, 1)
+                   toTexture:stagingtex
+            destinationSlice:0
+            destinationLevel:0
+           destinationOrigin:MTLOriginMake(0, 0, 0)];
+
+    [blitcmd endEncoding];
+
+    rect->x = 0;
+    rect->y = 0;
+
+    return stagingtex;
+}
+#endif // SDL_PLATFORM_VISIONOS
+
 static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rect *rect)
 static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rect *rect)
 {
 {
     @autoreleasepool {
     @autoreleasepool {
         SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
         SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
         id<MTLTexture> mtltexture;
         id<MTLTexture> mtltexture;
         MTLRegion mtlregion;
         MTLRegion mtlregion;
+        SDL_Rect read_rect = *rect;
         Uint32 format;
         Uint32 format;
         SDL_Surface *surface;
         SDL_Surface *surface;
 
 
@@ -1951,6 +2011,15 @@ static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rec
         }
         }
 #endif
 #endif
 
 
+#ifdef SDL_PLATFORM_VISIONOS
+        if (!renderer->target && data.mtlrealitykittexture) {
+            mtltexture = METAL_CopyToStagingTexture(renderer, mtltexture, &read_rect);
+            if (mtltexture == nil) {
+                return NULL;
+            }
+        }
+#endif
+
         /* Commit the current command buffer and wait until it's completed, to make
         /* Commit the current command buffer and wait until it's completed, to make
          * sure the GPU has finished rendering to it by the time we read it. */
          * sure the GPU has finished rendering to it by the time we read it. */
         [data.mtlcmdbuffer commit];
         [data.mtlcmdbuffer commit];
@@ -1958,7 +2027,7 @@ static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rec
         data.mtlcmdencoder = nil;
         data.mtlcmdencoder = nil;
         data.mtlcmdbuffer = nil;
         data.mtlcmdbuffer = nil;
 
 
-        mtlregion = MTLRegionMake2D(rect->x, rect->y, rect->w, rect->h);
+        mtlregion = MTLRegionMake2D(read_rect.x, read_rect.y, read_rect.w, read_rect.h);
 
 
         switch (mtltexture.pixelFormat) {
         switch (mtltexture.pixelFormat) {
         case MTLPixelFormatBGRA8Unorm:
         case MTLPixelFormatBGRA8Unorm:
@@ -1991,9 +2060,16 @@ static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rec
             SDL_SetError("Unknown framebuffer pixel format");
             SDL_SetError("Unknown framebuffer pixel format");
             return NULL;
             return NULL;
         }
         }
-        surface = SDL_CreateSurface(rect->w, rect->h, format);
+        surface = SDL_CreateSurface(read_rect.w, read_rect.h, format);
         if (surface) {
         if (surface) {
             [mtltexture getBytes:surface->pixels bytesPerRow:surface->pitch fromRegion:mtlregion mipmapLevel:0];
             [mtltexture getBytes:surface->pixels bytesPerRow:surface->pitch fromRegion:mtlregion mipmapLevel:0];
+            if (SDL_RenderingLinearSpace(renderer) &&
+                (!SDL_ISPIXELFORMAT_10BIT(format) && !SDL_ISPIXELFORMAT_FLOAT(format))) {
+                if (!SDL_ConvertPixelsAndColorspace(surface->w, surface->h, format, SDL_COLORSPACE_SRGB_LINEAR, 0, surface->pixels, surface->pitch, format, SDL_COLORSPACE_SRGB, 0, surface->pixels, surface->pitch)) {
+                    SDL_DestroySurface(surface);
+                    return NULL;
+                }
+            }
         }
         }
         return surface;
         return surface;
     }
     }
@@ -2022,8 +2098,22 @@ static bool METAL_RenderPresent(SDL_Renderer *renderer)
         // If we don't have a drawable to present, don't try to present it.
         // If we don't have a drawable to present, don't try to present it.
         //  But we'll still try to commit the command buffer in case it was already enqueued.
         //  But we'll still try to commit the command buffer in case it was already enqueued.
         if (ready) {
         if (ready) {
-            SDL_assert(data.mtlbackbuffer != nil);
-            [data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer];
+#ifdef SDL_PLATFORM_VISIONOS
+            if (data.mtlrealitykittexture) {
+                // Generate mipmaps
+                id<MTLBlitCommandEncoder> blitcmd = [data.mtlcmdbuffer blitCommandEncoder];
+
+                [blitcmd generateMipmapsForTexture:data.mtlrealitykittexture];
+                [blitcmd endEncoding];
+
+                data.mtlrealitykittexture = nil;
+            }
+            else
+#endif
+            {
+                SDL_assert(data.mtlbackbuffer != nil);
+                [data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer];
+            }
         }
         }
 
 
         [data.mtlcmdbuffer commit];
         [data.mtlcmdbuffer commit];
@@ -2057,6 +2147,11 @@ static void METAL_DestroyRenderer(SDL_Renderer *renderer)
                 [data.mtlcmdencoder endEncoding];
                 [data.mtlcmdencoder endEncoding];
             }
             }
 
 
+            if (data.mtlcmdbuffer != nil) {
+                [data.mtlcmdbuffer commit];
+                [data.mtlcmdbuffer waitUntilCompleted];
+            }
+
             DestroyAllPipelines(data.allpipelines, data.pipelinescount);
             DestroyAllPipelines(data.allpipelines, data.pipelinescount);
 
 
             /* Release the metal view instead of destroying it,
             /* Release the metal view instead of destroying it,

+ 410 - 0
src/video/uikit/SDL_CurvedContentHosting.swift

@@ -0,0 +1,410 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+import SwiftUI
+import RealityKit
+import Metal
+
+// Icons used by buttons below
+
+// Flat button
+/* SVG:
+ <svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M133.333 400H666.667" stroke="black" stroke-width="66.6667" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ */
+struct FlatButtonIcon : Shape {
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+        let width = rect.size.width
+        let height = rect.size.height
+        var strokePath = Path()
+        strokePath.move(to: CGPoint(x: 0.16667*width, y: 0.5*height))
+        strokePath.addLine(to: CGPoint(x: 0.83333*width, y: 0.5*height))
+        path.addPath(strokePath.strokedPath(StrokeStyle(lineWidth: 0.08333*width, lineCap: .round, lineJoin: .round, miterLimit: 4)))
+        return path
+    }
+}
+
+// Curved button
+/* SVG:
+ <svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M133 380C311 317.333 489 317.333 667 380" stroke="black" stroke-width="66.6667" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ */
+struct CurvedButtonIcon : Shape {
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+        let width = rect.size.width
+        let height = rect.size.height
+        var strokePath = Path()
+        strokePath.move(to: CGPoint(x: 0.16625*width, y: 0.475*height))
+        strokePath.addCurve(to: CGPoint(x: 0.83375*width, y: 0.475*height), control1: CGPoint(x: 0.38875*width, y: 0.39667*height), control2: CGPoint(x: 0.61125*width, y: 0.39667*height))
+        path.addPath(strokePath.strokedPath(StrokeStyle(lineWidth: 0.08333*width, lineCap: .round, lineJoin: .round, miterLimit: 4)))
+        return path
+    }
+}
+
+// Curviest button
+/* SVG:
+ <svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M133 370C310.667 230 488.333 230 666 370" stroke="black" stroke-width="66.6667" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ */
+struct CurviestButtonIcon : Shape {
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+        let width = rect.size.width
+        let height = rect.size.height
+        var strokePath = Path()
+        strokePath.move(to: CGPoint(x: 0.16625*width, y: 0.4625*height))
+        strokePath.addCurve(to: CGPoint(x: 0.8325*width, y: 0.4625*height), control1: CGPoint(x: 0.38833*width, y: 0.2875*height), control2: CGPoint(x: 0.61042*width, y: 0.2875*height))
+        path.addPath(strokePath.strokedPath(StrokeStyle(lineWidth: 0.08333*width, lineCap: .round, lineJoin: .round, miterLimit: 4)))
+        return path
+    }
+}
+
+/// UIHostingController subclass that hides the visionOS glass background.
+internal class SDL_ClearHostingController<Content: View>: UIHostingController<Content> {
+    override var preferredContainerBackgroundStyle: UIContainerBackgroundStyle {
+        return .hidden
+    }
+}
+
+/// ObjC-accessible wrapper that manages presenting SDL curved content
+/// via a UIHostingController
+@MainActor
+@objc(SDL_CurvedContentHosting)
+internal class SDL_CurvedContentHosting: NSObject {
+    private let settings = SDL_CurvedContentSettings()
+
+    private let helper = SDL_RealityKitHelper()
+
+    private var hostingController: SDL_ClearHostingController<SDL_CurvedContentView>?
+
+    @objc public override init() {
+        //NSLog("SDL_CurvedContentHosting init")
+        super.init()
+    }
+
+    /// Present the curved content view full-screen from the given view controller.
+    /// Uses two-phase presentation: first bootstraps the RealityView as a hidden
+    /// child VC, then presents modally (without animation) once content is ready.
+    /// Modal presentation is required on visionOS to get an independent depth budget
+    /// that doesn't clip curved mesh content extending forward from the window.
+    @objc public func present(from viewController: UIViewController) {
+        let contentView = SDL_CurvedContentView(helper: helper, settings: settings, onContentReady: { [weak self] in
+            guard let self, let hc = self.hostingController else { return }
+
+            hc.willMove(toParent: nil)
+            hc.view.removeFromSuperview()
+            hc.removeFromParent()
+            hc.view.layer.opacity = 1
+
+            //NSLog("SDL_CurvedContentHosting: RealityView content ready - presenting modally")
+            viewController.present(hc, animated: false) { [weak self] in
+                self?.updateOrnaments()
+            }
+        })
+
+        // Spin up an async task to present / dismiss ornaments when there are updates to the scene state.
+        let settings = self.settings
+        let sceneStateObservations = Observations { [weak settings] in
+            guard let settings else { return nil as (SDL_CurvedContentSettings.SceneState, SDL_CurvedContentSettings.InputType, Bool, Bool)? }
+            return (settings.sceneState, settings.inputType, settings.isSnapped, settings.settingsExpanded)
+        }
+        Task { [weak self] in
+            for await _ in sceneStateObservations {
+                guard let self else { return }
+                self.updateOrnaments()
+            }
+        }
+
+        let hc = SDL_ClearHostingController(rootView: contentView)
+        hc.modalPresentationStyle = .fullScreen
+        hc.view.backgroundColor = .clear
+        hostingController = hc
+
+        hc.view.layer.opacity = 0
+        viewController.addChild(hc)
+        hc.view.frame = viewController.view.bounds
+        viewController.view.addSubview(hc.view)
+        hc.didMove(toParent: viewController)
+
+        //NSLog("SDL_CurvedContentHosting: Bootstrapping RealityView as hidden child")
+    }
+
+    private func updateOrnaments() {
+        guard let hostingController else { return }
+        let settings = self.settings
+        let sceneState = settings.sceneState
+        UIView.animate(withDuration: 0.0) {
+            if sceneState == .interactive {
+                var sceneAnchor: UnitPoint
+                var contentAlignment: Alignment
+                if settings.isSnapped {
+                    if settings.settingsExpanded {
+                        sceneAnchor = .bottom
+                        contentAlignment = .center
+                    } else {
+                        sceneAnchor = .bottom
+                        contentAlignment = .top
+                    }
+                } else {
+                    if settings.settingsExpanded {
+                        sceneAnchor = .leading
+                        contentAlignment = .center
+                    } else {
+                        sceneAnchor = .leading
+                        contentAlignment = .trailing
+                    }
+                }
+                hostingController.ornaments = [
+                    UIHostingOrnament(sceneAnchor: sceneAnchor, contentAlignment: contentAlignment) {
+                        SDL_SettingsPanelView(settings: settings)
+                    }
+                ]
+            } else {
+                hostingController.ornaments = []
+            }
+        }
+    }
+
+    /// Get the display texture for this frame.
+    @objc public func getDisplayTexture(_ commandBuffer: MTLCommandBuffer, width: Int, height: Int, pixelFormat: MTLPixelFormat) -> MTLTexture? {
+        return helper.getDisplayTexture(commandBuffer, width: width, height: height, pixelFormat: pixelFormat)
+    }
+}
+
+// MARK: - Settings Panel
+
+@Observable
+internal class SDL_CurvedContentSettings {
+    /// State of the app user interface, determined by the content view's state.
+    enum SceneState {
+        /// A state which allows the user to configure the scene.  Ornaments should be visible.
+        case interactive
+
+        /// A state which hides all UI except for the game itself.  Ornaments should not be visible.
+        case cinematic
+    }
+
+    enum InputType {
+        case eyes
+        case pointer
+    }
+
+    var inputType: InputType = .eyes
+    var showHover: Bool = true
+    var isDimmed: Bool = false
+    var curvatureRadius: Float = SDL_VisionOS_GetCurvature()
+    var sceneState: SceneState = .interactive
+    var isSnapped: Bool = false
+    var settingsExpanded: Bool = false
+}
+
+struct SDL_SettingsPanelView: View {
+    let settings: SDL_CurvedContentSettings
+    @State private var curvatureSlider: Float = 0.0
+
+    static let minimumCurvatureRadius: Float = 800.0
+    static let maximumCurvatureRadius: Float = 4500.0
+
+    static let curvatureSteps: [Float] = [
+        0,
+        4000,
+        3000,
+        2300,
+        1800,
+        1500,
+        1000,
+        800
+    ]
+
+    static let curvatureStepsSliderValue: [Float] = curvatureSteps.map {
+        if $0 <= 0.01 {
+            return 0 // flat
+        }
+        return 1.0 - ($0 - minimumCurvatureRadius) / (maximumCurvatureRadius - minimumCurvatureRadius)
+    }
+
+    private var curvatureLabel: String {
+        if settings.curvatureRadius > 0 {
+            return "\(Int(settings.curvatureRadius))R"
+        } else {
+            return ""
+        }
+    }
+
+    var body: some View {
+        if settings.settingsExpanded {
+            expandedPanel
+        } else {
+            collapsedBar
+        }
+    }
+
+    // MARK: Collapsed
+
+    private var collapsedBar: some View {
+        Button(action: { withAnimation { settings.settingsExpanded = true } }) {
+            if settings.isSnapped {
+                HStack(spacing: 12) {
+                    Image(systemName: settings.showHover ? "eye" : "eye.slash")
+
+                    Image(systemName: settings.isDimmed ? "moon.fill" : "sun.max")
+                        .foregroundStyle(settings.isDimmed ? .primary : .secondary)
+
+                    Divider().frame(height: 8)
+
+                    if settings.curvatureRadius == 0 {
+                        FlatButtonIcon()
+                            .frame(width: 24, height: 24)
+                    } else if settings.curvatureRadius > 1000.0 {
+                        CurvedButtonIcon()
+                            .frame(width: 24, height: 24)
+                    } else {
+                        CurviestButtonIcon()
+                            .frame(width: 24, height: 24)
+                    }
+                }
+                .padding(.horizontal, 16)
+                .padding(.vertical, 16)
+            } else {
+                VStack(spacing: 12) {
+                    Image(systemName: settings.showHover ? "eye" : "eye.slash")
+
+                    Image(systemName: settings.isDimmed ? "moon.fill" : "sun.max")
+                        .foregroundStyle(settings.isDimmed ? .primary : .secondary)
+
+                    Divider().frame(height: 8)
+
+                    if settings.curvatureRadius == 0 {
+                        FlatButtonIcon()
+                            .frame(width: 24, height: 24)
+                    } else if settings.curvatureRadius > 1000.0 {
+                        CurvedButtonIcon()
+                            .frame(width: 24, height: 24)
+                    } else {
+                        CurviestButtonIcon()
+                            .frame(width: 24, height: 24)
+                    }
+                }
+                .padding(.horizontal, 16)
+                .padding(.vertical, 16)
+            }
+        }
+        .buttonStyle(.plain)
+        .glassBackgroundEffect()
+    }
+
+    // MARK: Expanded
+
+    private var expandedPanel: some View {
+        VStack(spacing: 16) {
+            // Input type and dim controls
+            @Bindable var settings = self.settings
+
+            Text("").font(.title).padding(8)
+
+            HStack() {
+                Spacer()
+                Image(systemName: "eye.slash")
+
+                Toggle(isOn: $settings.showHover) {
+                }
+                .labelsHidden()
+                .tint(.secondary)
+
+                Image(systemName: "eye")
+                Spacer()
+
+                Spacer()
+                Image(systemName: "sun.max")
+
+                Toggle(isOn: $settings.isDimmed) {
+                }
+                .labelsHidden()
+                .tint(.secondary)
+
+                Image(systemName: "moon.fill")
+                Spacer()
+            }
+
+            // Curvature slider
+            VStack(spacing: 4) {
+                Text("\(curvatureLabel)")
+                    .font(.caption)
+
+                HStack() {
+                    FlatButtonIcon()
+                        .frame(width: 24, height: 24)
+
+                    Slider(value: $curvatureSlider, in: 0...1) {
+                    } currentValueLabel: {
+                        Text("\(curvatureLabel)")
+                    } ticks: {
+                        SliderTickContentForEach(Self.curvatureStepsSliderValue, id: \.self) { value in
+                            SliderTick(value)
+                        }
+                    }
+                    .onAppear {
+                        let curvature = settings.curvatureRadius
+                        if curvature > 0 {
+                            curvatureSlider = 1.0 - (curvature - Self.minimumCurvatureRadius)
+                            / (Self.maximumCurvatureRadius - Self.minimumCurvatureRadius)
+                        } else {
+                            curvatureSlider = 0.0
+                        }
+                    }
+                    .onChange(of: curvatureSlider) {
+                        let clamped = max(0.0, min(1.0, curvatureSlider))
+                        if clamped == 0 {
+                            settings.curvatureRadius = 0
+                        } else {
+                            let radius = roundf(curvatureSlider * Self.minimumCurvatureRadius
+                                                + (1.0 - curvatureSlider) * Self.maximumCurvatureRadius)
+                            settings.curvatureRadius = radius
+                        }
+                        SDL_VisionOS_SendCurvatureChanged(settings.curvatureRadius)
+                    }
+
+                    CurviestButtonIcon()
+                        .frame(width: 24, height: 24)
+                }
+            }
+        }
+        .padding(20)
+        .frame(width: 340)
+        .overlay(alignment: .topLeading) {
+            // X button
+            Button(action: { withAnimation { settings.settingsExpanded = false } }) {
+                Image(systemName: "xmark")
+                    .font(.system(size: 15, weight: .bold, design: .rounded))
+                    .padding(8)
+                    .contentShape(Circle())
+            }
+            .buttonStyle(.bordered)
+            .buttonBorderShape(.circle)
+            .padding(20)
+        }
+        .glassBackgroundEffect()
+    }
+}

+ 350 - 0
src/video/uikit/SDL_CurvedContentView.swift

@@ -0,0 +1,350 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+import SwiftUI
+import RealityKit
+import GameController
+
+/// SwiftUI view that presents SDL content on a curved RealityKit mesh
+/// inside a UIHostingController
+internal struct SDL_CurvedContentView: View {
+    /// Helper object used to manage the mesh and texture of the curved UI.
+    let helper: SDL_RealityKitHelper
+
+    /// Settings object provided by the caller which determines the UI state.
+    let settings: SDL_CurvedContentSettings
+
+    /// Information about the window snap status
+    @Environment(\.surfaceSnappingInfo) private var snappedStatus
+
+    /// Closure which is called when the content is ready to present.
+    let onContentReady: @MainActor () -> Void
+
+    /// RealityKit entity which is created on appear, to be populated by the curved UI content.
+    @State private var curvedUIEntity: ModelEntity! = nil
+
+    /// Curved UI material which is created on appear.  Holds the compiled shader and material parameters.
+    @State private var curvedUIMaterial: CurvedUIMaterial! = nil
+
+    /// Converts SwiftUI points to meters (RealityKit coordinates)
+    ///
+    /// - Note: This conversion varies depending on the physical distance between the window and the user.
+    @PhysicalMetric(from: .meters) private var pointsPerMeter: Float = 1
+
+    /// Inverse of ``pointsPerMeter``.
+    var metersPerPoint: Float { 1.0 / pointsPerMeter }
+
+    /// The cursor color which should be passed to `curvedUIMaterial`
+    @State private var cursorColor: UIColor = .lightGray
+
+    /// The cursor color on interact (pinch/drag/click) which should be passed to `curvedUIMaterial`
+    @State private var cursorColorOnInteract: UIColor = .systemCyan
+
+    /// Whether to show the cursor overlay on the mesh surface.
+    private var showCursor: Bool {
+        return !mouseInputEnabled && settings.showHover
+    }
+
+    /// Whether mouse input is enabled.  When this is the case, the collision shape for indirect input should be disabled.
+    private var mouseInputEnabled: Bool {
+        return settings.inputType == .pointer
+    }
+
+    private var shouldPopulateCollisionShape: Bool {
+        return curvedUIEntity != nil && helper.collisionShape != nil && !mouseInputEnabled
+    }
+
+    /// Value use to animate the screen radius
+    @State private var animatedScreenRadius: Float = 1010
+
+    let SDL_EVENT_FINGER_DOWN: UInt32 = 0x700
+    let SDL_EVENT_FINGER_UP: UInt32 = 0x701
+    let SDL_EVENT_FINGER_MOTION: UInt32 = 0x702
+    let SDL_EVENT_FINGER_CANCELED: UInt32 = 0x703
+    private(set) static var last_fingerID: UInt64 = 0
+    private(set) static var fingers: [SpatialEventCollection.Event.ID: UInt64] = [:]
+
+    private func sendTouchEvent(event: SpatialEventCollection.Event, proxy: GeometryProxy3D) {
+        var fingerID: UInt64
+        var eventType: UInt32
+        if let value = Self.fingers[event.id] {
+            fingerID = value
+            if event.phase == SpatialEventCollection.Event.Phase.active {
+                eventType = SDL_EVENT_FINGER_MOTION
+            } else if event.phase == SpatialEventCollection.Event.Phase.ended {
+                eventType = SDL_EVENT_FINGER_UP
+                Self.fingers.removeValue(forKey: event.id)
+            } else {
+                eventType = SDL_EVENT_FINGER_CANCELED
+                Self.fingers.removeValue(forKey: event.id)
+            }
+        } else if event.phase == SpatialEventCollection.Event.Phase.active {
+            Self.last_fingerID += 1
+            fingerID = Self.last_fingerID
+            Self.fingers[event.id] = fingerID
+            eventType = SDL_EVENT_FINGER_DOWN
+        } else {
+            return
+        }
+
+        let loc = Point3D(x: event.location3D.x - proxy.size.width / 2,
+                          y: event.location3D.y - proxy.size.height / 2,
+                          z: event.location3D.z - proxy.size.depth / 2)
+        let meshPos = SIMD3<Float>(Float(loc.x) * metersPerPoint,
+                                   Float(loc.y) * metersPerPoint,
+                                   Float(loc.z) * metersPerPoint)
+        let uv = helper.meshGeometry.normalizedUV(fromMeshPosition: meshPos)
+
+        SDL_VisionOS_SendTouch(event.timestamp, fingerID, eventType, uv.x, uv.y)
+    }
+
+    var body: some View {
+        GeometryReader3D { proxy in
+            realityContent(proxy)
+                .glassBackgroundEffect(displayMode: .never)
+        }
+    }
+
+    private func realityContent(_ proxy: GeometryProxy3D) -> some View {
+        RealityView { content in
+            //NSLog("SDL_CurvedContentView: RealityView setup")
+
+            let frameInMeters: BoundingBox = content.convert(proxy.frame(in: .local), from: .local, to: .scene)
+            helper.updateMeshSize(width: frameInMeters.extents.x, height: frameInMeters.extents.y)
+
+            // Compile curved UI shader (may take a while)
+            let material = try! await CurvedUIMaterial()
+            self.curvedUIMaterial = material
+
+            // Create RealityKit Entity to host the curved UI content
+            let mesh = try! await MeshResource(from: helper.lowLevelMesh)
+            let entity = ModelEntity(mesh: mesh, materials: [material.shaderGraphMaterial])
+
+            // Add InputTargetComponent to the mesh to accept input.
+            entity.components.set(InputTargetComponent(allowedInputTypes: .all))
+
+            // Add HoverEffectComponent to visualize the gaze target
+            let shaderInputs = HoverEffectComponent.ShaderHoverEffectInputs.default
+            let hoverEffect = HoverEffectComponent.HoverEffect.shader(shaderInputs)
+            let hoverEffectComponent = HoverEffectComponent(hoverEffect)
+            entity.components.set(hoverEffectComponent)
+
+            // Increase the responsiveness of the hover effect
+            RenderRefreshSystem.registerSystem()
+            entity.components.set(RenderRefreshComponent(
+                componentToRefresh: hoverEffectComponent
+            ))
+
+            self.curvedUIEntity = entity
+            content.add(entity)
+
+            // Call the user-provided contentReady closure.
+            onContentReady()
+        } update: { content in
+            let frameInMeters: BoundingBox = content.convert(proxy.frame(in: .local), from: .local, to: .scene)
+            helper.updateMeshSize(width: frameInMeters.extents.x, height: frameInMeters.extents.y)
+
+            let frame = proxy.frame(in: .local)
+            SDL_VisionOS_SendSizeChanged(Int(frame.size.width), Int(frame.size.height))
+        }
+        .overlay {
+            if mouseInputEnabled {
+                // This enables mouse motion events, but blocks hover location
+                Color.white
+                    .opacity(0.001)
+                    .pointerStyle(.shape(Circle(), size: .zero))
+            }
+        }
+        .gesture(
+            SpatialEventGesture()
+                .onChanged { events in
+                    guard curvedUIMaterial != nil else { return }
+
+                    if !mouseInputEnabled {
+                        curvedUIMaterial.isInteracting = true
+
+                        for event in events {
+                            if event.kind != .pointer {
+                                sendTouchEvent(event: event, proxy: proxy)
+                            } else {
+                                settings.inputType = .pointer
+                                settings.sceneState = .cinematic
+                            }
+                        }
+                    }
+                }
+                .onEnded { events in
+                    guard curvedUIMaterial != nil else { return }
+
+                    if !mouseInputEnabled {
+                        for event in events {
+                            if event.kind != .pointer {
+                                sendTouchEvent(event: event, proxy: proxy)
+                            }
+                        }
+                    } else {
+                        for event in events {
+                            if event.kind != .pointer {
+                                settings.inputType = .eyes
+                                settings.sceneState = .interactive
+                            }
+                        }
+                    }
+
+                    curvedUIMaterial.isInteracting = false
+                }
+        )
+        .onChange(of: sceneActivationOrObject(showCursor), initial: true) {
+            curvedUIMaterial?.showCursor = showCursor
+        }
+        .onChange(of: sceneActivationOrObject(cursorColor), initial: true) {
+            curvedUIMaterial?.cursorColor = cursorColor
+        }
+        .onChange(of: sceneActivationOrObject(cursorColorOnInteract), initial: true) {
+            curvedUIMaterial?.cursorColorOnInteract = cursorColorOnInteract
+        }
+        .onChange(of: sceneActivationOrObject(helper.meshGeometry), initial: true) {
+            guard curvedUIMaterial != nil else { return }
+            let geometry = helper.meshGeometry
+            curvedUIMaterial.cursorSize = geometry.height * 0.01
+        }
+        .onChange(of: sceneActivationOrObject(helper.textureResource), initial: true) {
+            if let textureResource = helper.textureResource {
+                curvedUIMaterial?.gameTexture = textureResource
+            }
+        }
+        .onChange(of: sceneActivationOrObject(curvedUIMaterial), initial: true) {
+            // Update the materials array of the entity with the updated material parameters.
+            if let curvedUIMaterial, let curvedUIEntity {
+                curvedUIEntity.model!.materials = [curvedUIMaterial.shaderGraphMaterial]
+            }
+        }
+        .onChange(of: settings.inputType, initial: true) { oldInputType, inputType in
+            if inputType == .pointer {
+                SDL_VisionOS_SendPointerMode(true)
+            } else {
+                SDL_VisionOS_SendPointerMode(false)
+            }
+        }
+        .onChange(of: settings.curvatureRadius, initial: true) { oldRadius, curvatureRadius in
+            if oldRadius != curvatureRadius {
+                withAnimation(.smooth) {
+                    if curvatureRadius > 0 {
+                        animatedScreenRadius = curvatureRadius / 1000
+                    } else {
+                        animatedScreenRadius = AnimatedCurveRadiusModifier.assumedFlatThreshold + 0.01
+                    }
+                }
+            } else {
+                if curvatureRadius > 0 {
+                    animatedScreenRadius = curvatureRadius / 1000
+                } else {
+                    animatedScreenRadius = AnimatedCurveRadiusModifier.assumedFlatThreshold + 0.01
+                }
+            }
+        }
+        .modifier(AnimatedCurveRadiusModifier(helper: helper, curveRadius: animatedScreenRadius))
+        .onChange(of: sceneActivationOrObject(shouldPopulateCollisionShape ? helper.collisionShape : nil)) {
+            guard let curvedUIEntity else { return }
+            if let shape = helper.collisionShape, shouldPopulateCollisionShape {
+                curvedUIEntity.components.set(CollisionComponent(shapes: [shape]))
+            } else {
+                curvedUIEntity.components.set(CollisionComponent(shapes: []))
+            }
+        }
+        .onChange(of: snappedStatus) {
+            settings.isSnapped = snappedStatus.isSnapped
+            helper.updateSnappedStatus(snapped: snappedStatus.isSnapped)
+        }
+        .preferredSurroundingsEffect(settings.isDimmed ? .dark : nil)
+        .frame(depth: 0)
+        .ignoresSafeArea()
+        .persistentSystemOverlays(settings.sceneState == .cinematic ? .hidden : .automatic)
+        .handlesGameControllerEvents(matching: .gamepad)
+    }
+}
+
+// MARK: Animating the curve radius
+
+@Animatable
+private struct AnimatedCurveRadiusModifier: @MainActor ViewModifier {
+    /// Curvature radius beyond which we assume it is flat.
+    static let assumedFlatThreshold: Float = 30.0
+
+    /// Helper object to modify
+    let helper: SDL_RealityKitHelper
+
+    /// Curve radius > `assumedFlatThreshold` meters is assumed to be flat.
+    var curveRadius: Float
+
+    func body(content: Content) -> some View {
+        content.onChange(of: curveRadius, initial: true) {
+            if curveRadius > 10 {
+                helper.updateMeshCurvature(curvatureRadius: 0)
+            } else {
+                helper.updateMeshCurvature(curvatureRadius: curveRadius)
+            }
+        }
+    }
+}
+
+// MARK: Bridging SwiftUI and RealityKit
+
+private extension SDL_CurvedContentView {
+    private struct Box<T: Equatable>: Equatable {
+        var sceneActivation: Bool
+        var value: T
+    }
+
+    /// Convenience function which triggers an `onChange` event either when `object` changes, or when
+    /// ``curvedUIMaterial`` finishes compiling.
+    func sceneActivationOrObject<T: Equatable>(_ object: T) -> some Equatable {
+        return Box(sceneActivation: self.curvedUIMaterial != nil && self.curvedUIEntity != nil, value: object)
+    }
+}
+
+// MARK: Per-frame component refresh
+
+/// Attach this component to an entity to reset a RealityKit component every rendering frame.
+/// This can be used to disable system-default interpolation on any component that applies it.
+///
+/// Example — to reset a platform-specific component every frame:
+///     entity.components.set(RenderRefreshComponent(
+///         componentToRefresh: CustomComponent()
+///     ))
+private struct RenderRefreshComponent: TransientComponent {
+    var componentToRefresh: (any Component)?
+}
+
+private struct RenderRefreshSystem: System {
+    static let query = EntityQuery(where: .has(RenderRefreshComponent.self))
+    init(scene: RealityKit.Scene) {
+        RenderRefreshComponent.registerComponent()
+    }
+
+    func update(context: SceneUpdateContext) {
+        for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
+            guard let refresh = entity.components[RenderRefreshComponent.self],
+                  let component = refresh.componentToRefresh else { continue }
+            entity.components.remove(type(of: component))
+            entity.components.set(component)
+        }
+    }
+}

+ 544 - 0
src/video/uikit/SDL_CurvedUIShader.swift

@@ -0,0 +1,544 @@
+//
+//  SDL_CurvedUIShader.swift
+//  SDL3
+//
+//  Created by Adrian Biagioli on 4/21/26.
+//
+
+import Foundation
+import RealityKit
+
+/// A MaterialX curved UI shader USDA.  This is loaded on launch into a ShaderGraphMaterial.
+///
+/// You can inspect this shader yourself in Reality Composer Pro.
+/// To do this, copy this string and save it as a .usda file.
+/// Then, add it to a Reality Composer Pro object.
+private let curvedUIShaderUSDA = """
+#usda 1.0
+(
+    customLayerData = {
+        string creator = "Reality Composer Pro Version 2.0 (494.100.6)"
+    }
+    defaultPrim = "Root"
+    metersPerUnit = 1
+    upAxis = "Y"
+)
+
+def Xform "Root"
+{
+    def Material "CurvedUIMaterial"
+    {
+        reorder nameChildren = ["DefaultSurfaceShader", "UnlitSurface", "TextureCoordinates", "Position", "Image2D", "Group2", "Group4", "CursorPositionOnScreen", "SelectCursorColor", "SelectCursorOpacity", "GameTextureRGB", "NormalizedDistance", "Dot", "Group", "Dot_1", "DiscardCursorOutsideRange", "MixCursorOverGame", "HideCursorIfDisabled"]
+        color3f inputs:CursorColor = (0, 0.87658346, 1) (
+            colorSpace = "lin_srgb"
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-374.2671, 402.7502)
+                    int stackingOrderInSubgraph = 1955
+                }
+            }
+        )
+        color3f inputs:CursorColorOnInteract = (0.016926037, 0, 0.7703071) (
+            colorSpace = "lin_srgb"
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-408.82837, 336.09396)
+                    int stackingOrderInSubgraph = 2017
+                }
+            }
+        )
+        float inputs:CursorEdgeThreshold = 0.9 (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-706.12756, 582.3273)
+                    int stackingOrderInSubgraph = 1951
+                }
+            }
+        )
+        float inputs:CursorOpacityEdge = 0.7 (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-704.3221, 648.0528)
+                    int stackingOrderInSubgraph = 1953
+                }
+            }
+        )
+        float inputs:CursorOpacityInside = 0.4 (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-701.167, 710.96765)
+                    int stackingOrderInSubgraph = 1955
+                }
+            }
+        )
+        float inputs:CursorSize = 0.003 (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-1204.8192, 509.2949)
+                    int stackingOrderInSubgraph = 2015
+                }
+            }
+        )
+        asset inputs:GameTexture (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-1270.7656, -315.35458)
+                    int stackingOrderInSubgraph = 1834
+                }
+            }
+        )
+        bool inputs:IsInteracting = 0 (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-373.38513, 263.61777)
+                    int stackingOrderInSubgraph = 1955
+                }
+            }
+        )
+        bool inputs:ShowCursor = 1 (
+            customData = {
+                dictionary realitykit = {
+                    float2 positionInSubgraph = (-1721.0664, 367.89142)
+                    int stackingOrderInSubgraph = 2360
+                }
+            }
+        )
+        token outputs:mtlx:surface.connect = </Root/CurvedUIMaterial/UnlitSurface.outputs:out>
+        token outputs:realitykit:vertex
+        token outputs:surface.connect = </Root/CurvedUIMaterial/DefaultSurfaceShader.outputs:surface>
+        float2 ui:nodegraph:realitykit:subgraphOutputs:pos = (612.1894, 109.99387)
+        int ui:nodegraph:realitykit:subgraphOutputs:stackingOrder = 1993
+
+        def Shader "DefaultSurfaceShader" (
+            active = false
+        )
+        {
+            uniform token info:id = "UsdPreviewSurface"
+            color3f inputs:diffuseColor = (1, 1, 1)
+            float inputs:roughness = 0.75
+            token outputs:surface
+        }
+
+        def Shader "UnlitSurface"
+        {
+            uniform token info:id = "ND_realitykit_unlit_surfaceshader"
+            bool inputs:applyPostProcessToneMap = 0
+            color3f inputs:color.connect = </Root/CurvedUIMaterial/MixCursorOverGame.outputs:out>
+            bool inputs:hasPremultipliedAlpha
+            float inputs:opacity
+            float inputs:opacityThreshold
+            token outputs:out
+            float2 ui:nodegraph:node:pos = (368.7634, 58.4275)
+            int ui:nodegraph:node:stackingOrder = 1993
+        }
+
+        def Shader "TextureCoordinates"
+        {
+            uniform token info:id = "ND_texcoord_vector2"
+            float2 outputs:out
+            float2 ui:nodegraph:node:pos = (-1292.3005, -120.02362)
+            int ui:nodegraph:node:stackingOrder = 1834
+        }
+
+        def Shader "Position"
+        {
+            uniform token info:id = "ND_position_vector3"
+            string inputs:space = "world"
+            float3 outputs:out
+            float2 ui:nodegraph:node:pos = (-1205.6492, 445.2142)
+            int ui:nodegraph:node:stackingOrder = 2314
+        }
+
+        def Shader "Image2D"
+        {
+            uniform token info:id = "ND_RealityKitTexture2D_color4"
+            float inputs:bias
+            string inputs:border_color
+            float inputs:dynamic_min_lod_clamp
+            asset inputs:file.connect = </Root/CurvedUIMaterial.inputs:GameTexture>
+            bool inputs:no_flip_v = 1
+            int2 inputs:offset
+            float2 inputs:texcoord.connect = </Root/CurvedUIMaterial/TextureCoordinates.outputs:out>
+            string inputs:u_wrap_mode
+            string inputs:v_wrap_mode
+            color4f outputs:out
+            float2 ui:nodegraph:node:pos = (-1023.8389, -194.1174)
+            int ui:nodegraph:node:stackingOrder = 1834
+            string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:no_flip_v"]
+        }
+
+        def Scope "Group2" (
+            kind = "group"
+        )
+        {
+            string ui:group:annotation = "Apply final color to UnlitMaterial"
+            string ui:group:annotationDescription = ""
+            string[] ui:group:members = ["p:UnlitSurface", "o:_subgraphOutput"]
+        }
+
+        def Scope "Group4" (
+            kind = "group"
+        )
+        {
+            string ui:group:annotation = "Sample game texture"
+            string ui:group:annotationDescription = ""
+            string[] ui:group:members = ["i:inputs:GameTexture", "p:Image2D", "p:TextureCoordinates"]
+        }
+
+        def Shader "SelectCursorColor"
+        {
+            uniform token info:id = "ND_ifequal_color3B"
+            color3f inputs:in1.connect = </Root/CurvedUIMaterial.inputs:CursorColorOnInteract>
+            color3f inputs:in2.connect = </Root/CurvedUIMaterial.inputs:CursorColor>
+            bool inputs:value1.connect = </Root/CurvedUIMaterial.inputs:IsInteracting>
+            bool inputs:value2 = 1
+            color3f outputs:out
+            float2 ui:nodegraph:node:pos = (-175.6293, 330.2353)
+            int ui:nodegraph:node:stackingOrder = 1955
+        }
+
+        def Shader "SelectCursorOpacity"
+        {
+            uniform token info:id = "ND_ifgreater_float"
+            float inputs:in1.connect = </Root/CurvedUIMaterial.inputs:CursorOpacityEdge>
+            float inputs:in2.connect = </Root/CurvedUIMaterial.inputs:CursorOpacityInside>
+            float inputs:value1.connect = </Root/CurvedUIMaterial/Dot.outputs:out>
+            float inputs:value2.connect = </Root/CurvedUIMaterial.inputs:CursorEdgeThreshold>
+            float outputs:out
+            float2 ui:nodegraph:node:pos = (-463.96164, 578.08826)
+            int ui:nodegraph:node:stackingOrder = 1853
+        }
+
+        def Shader "GameTextureRGB"
+        {
+            uniform token info:id = "ND_swizzle_color4_color3"
+            string inputs:channels = "rgb"
+            color4f inputs:in.connect = </Root/CurvedUIMaterial/Image2D.outputs:out>
+            color3f outputs:out
+            float2 ui:nodegraph:node:pos = (-732.1035, -11.733684)
+            int ui:nodegraph:node:stackingOrder = 1834
+        }
+
+        def NodeGraph "NormalizedDistance"
+        {
+            float3 inputs:A (
+                customData = {
+                    dictionary realitykit = {
+                        float2 positionInSubgraph = (79.30469, 187.10547)
+                        int stackingOrderInSubgraph = 1406
+                    }
+                }
+            )
+            float3 inputs:A.connect = </Root/CurvedUIMaterial/HideCursorIfDisabled.outputs:out>
+            float3 inputs:B (
+                customData = {
+                    dictionary realitykit = {
+                        float2 positionInSubgraph = (79.234375, 270.22266)
+                        int stackingOrderInSubgraph = 1408
+                    }
+                }
+            )
+            float3 inputs:B.connect = </Root/CurvedUIMaterial/Position.outputs:out>
+            float inputs:Radius (
+                customData = {
+                    dictionary realitykit = {
+                        float2 positionInSubgraph = (306.85156, 333.83984)
+                        int stackingOrderInSubgraph = 1406
+                    }
+                }
+            )
+            float inputs:Radius.connect = </Root/CurvedUIMaterial.inputs:CursorSize>
+            float outputs:ZeroToOneDistance (
+                customData = {
+                    dictionary realitykit = {
+                        float2 positionInSubgraph = (444.625, 223)
+                        int stackingOrderInSubgraph = 1409
+                    }
+                }
+            )
+            float outputs:ZeroToOneDistance.connect = </Root/CurvedUIMaterial/NormalizedDistance/Remap.outputs:out>
+            float2 ui:nodegraph:node:pos = (-998.9227, 417.7417)
+            int ui:nodegraph:node:stackingOrder = 2010
+            string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["outputs:Clamp_out", "inputs:A"]
+            float2 ui:nodegraph:realitykit:subgraphOutputs:pos = (711.2656, 366.07812)
+            int ui:nodegraph:realitykit:subgraphOutputs:stackingOrder = 1409
+
+            def Shader "Remap"
+            {
+                uniform token info:id = "ND_remap_float"
+                float inputs:in.connect = </Root/CurvedUIMaterial/NormalizedDistance/MTLDistance.outputs:out>
+                float inputs:inhigh.connect = </Root/CurvedUIMaterial/NormalizedDistance.inputs:Radius>
+                float inputs:inlow = 0
+                float inputs:outhigh = 1
+                float inputs:outlow = 0
+                float outputs:out
+                float2 ui:nodegraph:node:pos = (503, 318.58984)
+                int ui:nodegraph:node:stackingOrder = 1407
+            }
+
+            def Shader "MTLDistance"
+            {
+                uniform token info:id = "ND_MTL_distance_vector3_float"
+                float3 inputs:x.connect = </Root/CurvedUIMaterial/NormalizedDistance.inputs:A>
+                float3 inputs:y.connect = </Root/CurvedUIMaterial/NormalizedDistance.inputs:B>
+                float outputs:out
+                float2 ui:nodegraph:node:pos = (304, 186.67969)
+                int ui:nodegraph:node:stackingOrder = 1402
+            }
+        }
+
+        def Shader "Dot"
+        {
+            uniform token info:id = "ND_dot_float"
+            float inputs:in.connect = </Root/CurvedUIMaterial/NormalizedDistance.outputs:ZeroToOneDistance>
+            float outputs:out
+            float2 ui:nodegraph:node:pos = (-626.7584, 475.93542)
+            int ui:nodegraph:node:stackingOrder = 1735
+        }
+
+        def Scope "Group" (
+            kind = "group"
+        )
+        {
+            string ui:group:annotation = "Select cursor color and opacity"
+            string ui:group:annotationDescription = "The color is selected depending if the user is interacting (click/tap/pinch/drag).  The opacity is selected via the distance between this fragment's position and the cursor position"
+            string[] ui:group:members = ["i:inputs:IsInteracting", "p:Dot_1", "p:DiscardCursorOutsideRange", "i:inputs:CursorColorOnInteract", "p:SelectCursorColor", "i:inputs:CursorColor", "p:Dot", "i:inputs:CursorOpacityEdge", "i:inputs:CursorOpacityInside", "p:SelectCursorOpacity", "i:inputs:CursorEdgeThreshold"]
+        }
+
+        def Shader "Dot_1"
+        {
+            uniform token info:id = "ND_dot_float"
+            float inputs:in.connect = </Root/CurvedUIMaterial/Dot.outputs:out>
+            float outputs:out
+            float2 ui:nodegraph:node:pos = (-370.1385, 475.2281)
+            int ui:nodegraph:node:stackingOrder = 1851
+        }
+
+        def Shader "DiscardCursorOutsideRange"
+        {
+            uniform token info:id = "ND_ifgreater_float"
+            float inputs:in1 = 0
+            float inputs:in2.connect = </Root/CurvedUIMaterial/SelectCursorOpacity.outputs:out>
+            float inputs:value1.connect = </Root/CurvedUIMaterial/Dot_1.outputs:out>
+            float inputs:value2 = 1
+            float outputs:out
+            float2 ui:nodegraph:node:pos = (-192.05971, 600.1504)
+            int ui:nodegraph:node:stackingOrder = 1966
+        }
+
+        def Shader "MixCursorOverGame"
+        {
+            uniform token info:id = "ND_mix_color3"
+            color3f inputs:bg.connect = </Root/CurvedUIMaterial/GameTextureRGB.outputs:out>
+            color3f inputs:fg.connect = </Root/CurvedUIMaterial/SelectCursorColor.outputs:out>
+            float inputs:mix.connect = </Root/CurvedUIMaterial/DiscardCursorOutsideRange.outputs:out>
+            color3f outputs:out
+            float2 ui:nodegraph:node:pos = (90.70218, -17.587646)
+            int ui:nodegraph:node:stackingOrder = 1973
+        }
+
+        def Shader "HideCursorIfDisabled"
+        {
+            uniform token info:id = "ND_ifequal_vector3B"
+            float3 inputs:in1.connect = </Root/CurvedUIMaterial/HoverState.outputs:position>
+            float3 inputs:in2 = (999999, 999999, 999999)
+            bool inputs:value1.connect = </Root/CurvedUIMaterial/And.outputs:out>
+            bool inputs:value2 = 1
+            bool inputs:value2.connect = None
+            float3 outputs:out
+            float2 ui:nodegraph:node:pos = (-1281.8472, 322.0585)
+            int ui:nodegraph:node:stackingOrder = 2361
+        }
+
+        def Shader "HoverState"
+        {
+            uniform token info:id = "ND_realitykit_hover_state"
+            float outputs:intensity
+            bool outputs:isActive
+            float3 outputs:position
+            float outputs:timeSinceHoverStart
+            float2 ui:nodegraph:node:pos = (-1730.769, 258.70575)
+            int ui:nodegraph:node:stackingOrder = 2360
+            string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["outputs:position"]
+        }
+
+        def Shader "And"
+        {
+            uniform token info:id = "ND_realitykit_logical_and"
+            bool inputs:in1.connect = </Root/CurvedUIMaterial/HoverState.outputs:isActive>
+            bool inputs:in2.connect = </Root/CurvedUIMaterial.inputs:ShowCursor>
+            bool outputs:out
+            float2 ui:nodegraph:node:pos = (-1571.7467, 334.56076)
+            int ui:nodegraph:node:stackingOrder = 2360
+        }
+    }
+}
+
+"""
+
+/// A wrapper object around a RealityKit `ShaderGraphMaterial`, but specific to the SDL curved UI shader.
+///
+/// This struct provides material parameters that pass through to the `ShaderGraphMaterial`.
+@MainActor
+struct CurvedUIMaterial: @MainActor Equatable {
+    /// A cached ShaderGraphMaterial, populated with a prototype ShaderGraphMaterial.
+    ///
+    /// On subsequent loads, the alread-loaded material is used directly.
+    @MainActor private static var cachedShaderGraph: ShaderGraphMaterial?
+    
+    /// The ShaderGraphMaterial which should be used to populate the curved UI Entity's `ModelComponent`.
+    ///
+    /// - Note: ShaderGraphMaterial is a value type (`struct`), so you must re-query this value after changing any parameters.
+    private(set) var shaderGraphMaterial: ShaderGraphMaterial
+    
+    /// Initializes the curved UI material.
+    ///
+    /// If the shader needs to compile (first launch), then it compiles before returning.
+    /// If the shader is already compiled, returns immediately.
+    @MainActor
+    init() async throws {
+        if let cachedShaderGraph = Self.cachedShaderGraph {
+            self.shaderGraphMaterial = cachedShaderGraph
+        } else {
+            let result = try await ShaderGraphMaterial(
+                named: "/Root/CurvedUIMaterial",
+                from: Data(curvedUIShaderUSDA.utf8)
+            )
+            Self.cachedShaderGraph = result
+            self.shaderGraphMaterial = result
+        }
+    }
+    
+    /// The texture containing SDL content.
+    var gameTexture: TextureResource! {
+        get { shaderGraphMaterial.getParameter(.gameTexture) }
+        set { try! shaderGraphMaterial.setParameter(.gameTexture, value: newValue) }
+    }
+    
+    /// Color of the cursor overlay when not actively interacting.
+    var cursorColor: UIColor! {
+        get { shaderGraphMaterial.getParameter(.cursorColor) }
+        set { try! shaderGraphMaterial.setParameter(.cursorColor, value: newValue) }
+    }
+    
+    /// Color of the cursor when interacting (click/tap/pinch/drag)
+    var cursorColorOnInteract: UIColor! {
+        get { shaderGraphMaterial.getParameter(.cursorColorOnInteract) }
+        set { try! shaderGraphMaterial.setParameter(.cursorColorOnInteract, value: newValue) }
+    }
+    
+    /// The size of the cursor in meters.
+    var cursorSize: Float! {
+        get { shaderGraphMaterial.getParameter(.cursorSize) }
+        set { try! shaderGraphMaterial.setParameter(.cursorSize, value: newValue) }
+    }
+    
+    /// Whether to show the cursor overlay on the mesh surface.
+    var showCursor: Bool! {
+        get { shaderGraphMaterial.getParameter(.showCursor) }
+        set { try! shaderGraphMaterial.setParameter(.showCursor, value: newValue) }
+    }
+
+    /// True if the user is actively interacting with the scene (e.g. click, tap, pinch, or drag).
+    var isInteracting: Bool! {
+        get { shaderGraphMaterial.getParameter(.isInteracting) }
+        set { try! shaderGraphMaterial.setParameter(.isInteracting, value: newValue) }
+    }
+    
+    static func == (lhs: CurvedUIMaterial, rhs: CurvedUIMaterial) -> Bool {
+        return lhs.gameTexture == rhs.gameTexture
+            && lhs.cursorColor == rhs.cursorColor
+            && lhs.cursorColorOnInteract == rhs.cursorColorOnInteract
+            && lhs.cursorSize == rhs.cursorSize
+            && lhs.showCursor == rhs.showCursor
+            && lhs.isInteracting == rhs.isInteracting
+    }
+}
+
+@MainActor
+private extension MaterialParameters.Handle {
+    static let gameTexture = ShaderGraphMaterial.parameterHandle(name: "GameTexture")
+    static let cursorColor = ShaderGraphMaterial.parameterHandle(name: "CursorColor")
+    static let cursorColorOnInteract = ShaderGraphMaterial.parameterHandle(name: "CursorColorOnInteract")
+    static let cursorSize = ShaderGraphMaterial.parameterHandle(name: "CursorSize")
+    static let showCursor = ShaderGraphMaterial.parameterHandle(name: "ShowCursor")
+    static let isInteracting = ShaderGraphMaterial.parameterHandle(name: "IsInteracting")
+}
+
+private extension ShaderGraphMaterial {
+    /// Convenience function to recover a typed shader parameter (without going through `MaterialParametres.Value` enum)
+    func getParameter<T>(_ handle: MaterialParameters.Handle, type: T.Type = T.self) -> T? {
+        guard let value = self.getParameter(handle: handle) else { return nil }
+        
+        switch (type.self, value) {
+        case (is MaterialParameters.Texture.Type, .texture(let v)): return (v as! T)
+        case (is TextureResource.Type, .texture(let v)): return (v.resource as! T)
+        case (is TextureResource.Type, .textureResource(let v)): return (v as! T)
+        case (is Float.Type, .float(let v)): return (v as! T)
+        case (is SIMD2<Float>.Type, .simd2Float(let v)): return (v as! T)
+        case (is SIMD3<Float>.Type, .simd3Float(let v)): return (v as! T)
+        case (is SIMD4<Float>.Type, .simd4Float(let v)): return (v as! T)
+        case (is UIColor.Type, .color(let v)): fallthrough
+        case (is CGColor.Type, .color(let v)):
+            // `is CGColor` works for both UIColor and CGColor
+            if type == CGColor.self {
+                return (v as! T)
+            } else if type == UIColor.self {
+                return (UIColor(cgColor: v) as! T)
+            } else {
+                preconditionFailure("Unknown Color type \(type)")
+            }
+        case (is float2x2.Type, .float2x2(let v)): return (v as! T)
+        case (is float3x3.Type, .float3x3(let v)): return (v as! T)
+        case (is float4x4.Type, .float4x4(let v)): return (v as! T)
+        case (is Bool.Type, .bool(let v)): return (v as! T)
+        case (is Int.Type, .int(let v)): return (Int(v) as! T)
+        case (is Int32.Type, .int(let v)): return (v as! T)
+        default:
+            preconditionFailure("Invalid type \(type) for handle with value \(value)")
+        }
+    }
+    
+    /// Convenience function to set a typed shader parameter (without going through `MaterialParametres.Value` enum)
+    mutating func setParameter<T>(_ handle: MaterialParameters.Handle, value: T!) throws {
+        guard let value else { preconditionFailure("can not clear a material parameter") }
+        switch type(of: value).self {
+        case is MaterialParameters.Texture.Type:
+            try self.setParameter(handle: handle, value: .texture(value as! MaterialParameters.Texture))
+        case is TextureResource.Type:
+            try self.setParameter(handle: handle, value: .textureResource(value as! TextureResource))
+        case is Float.Type:
+            try self.setParameter(handle: handle, value: .float(value as! Float))
+        case is SIMD2<Float>.Type:
+            try self.setParameter(handle: handle, value: .simd2Float(value as! SIMD2<Float>))
+        case is SIMD3<Float>.Type:
+            try self.setParameter(handle: handle, value: .simd3Float(value as! SIMD3<Float>))
+        case is SIMD4<Float>.Type:
+            try self.setParameter(handle: handle, value: .simd4Float(value as! SIMD4<Float>))
+        case is CGColor.Type: fallthrough
+        case is UIColor.Type:
+            // `is CGColor` works for both UIColor and CGColor
+            if T.self == UIColor.self {
+                try self.setParameter(handle: handle, value: .color(value as! UIColor))
+            } else if T.self == CGColor.self {
+                try self.setParameter(handle: handle, value: .color(value as! CGColor))
+            } else {
+                preconditionFailure("Unknown Color type \(type(of: value))")
+            }
+        case is float2x2.Type:
+            try self.setParameter(handle: handle, value: .float2x2(value as! float2x2))
+        case is float3x3.Type:
+            try self.setParameter(handle: handle, value: .float3x3(value as! float3x3))
+        case is float4x4.Type:
+            try self.setParameter(handle: handle, value: .float4x4(value as! float4x4))
+        case is Bool.Type:
+            try self.setParameter(handle: handle, value: .bool(value as! Bool))
+        case is Int.Type:
+            try self.setParameter(handle: handle, value: .int(Int32(value as! Int)))
+        case is Int32.Type:
+            try self.setParameter(handle: handle, value: .int(value as! Int32))
+        default:
+            preconditionFailure("Invalid type \(type(of: value))")
+        }
+    }
+}

+ 396 - 0
src/video/uikit/SDL_RealityKitHelper.swift

@@ -0,0 +1,396 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+import RealityKit
+import SwiftUI
+import Metal
+import MetalKit
+import simd
+
+/// Custom vertex format for the curved plane mesh.
+/// Matches the layout described to LowLevelMesh via vertexAttributes/vertexLayouts.
+private struct CurvedPlaneVertex {
+    var position: SIMD3<Float> = .zero
+    var normal: SIMD3<Float> = .zero
+    var uv: SIMD2<Float> = .zero
+
+    static var vertexAttributes: [LowLevelMesh.Attribute] {
+        [
+            .init(semantic: .position, format: .float3, offset: MemoryLayout<Self>.offset(of: \.position)!),
+            .init(semantic: .normal, format: .float3, offset: MemoryLayout<Self>.offset(of: \.normal)!),
+            .init(semantic: .uv0, format: .float2, offset: MemoryLayout<Self>.offset(of: \.uv)!)
+        ]
+    }
+
+    static var vertexLayouts: [LowLevelMesh.Layout] {
+        [.init(bufferIndex: 0, bufferStride: MemoryLayout<Self>.stride)]
+    }
+
+    static func descriptor(vertexCount: Int, indexCount: Int) -> LowLevelMesh.Descriptor {
+        var desc = LowLevelMesh.Descriptor()
+        desc.vertexAttributes = vertexAttributes
+        desc.vertexLayouts = vertexLayouts
+        desc.vertexCapacity = vertexCount
+        desc.indexCapacity = indexCount
+        desc.indexType = .uint32
+        return desc
+    }
+}
+
+/// Provides RealityKit functionality
+///
+/// Key responsibilities:
+/// - Generate curved mesh geometry procedurally using LowLevelMesh for fast updates
+/// - Update textures using LowLevelTexture for efficient Metal → RealityKit transfer
+/// - Asynchronously cooks a physics collision mesh of the curved UI to be used as an input target
+@MainActor
+@Observable
+internal class SDL_RealityKitHelper {
+    /// A collision shape which should be assigned to the same entity as ``lowLevelMesh``, for input targeting.
+    private(set) var collisionShape: ShapeResource? = nil
+    
+    /// The TextureResource object which should be assigned to an entity in the scene.
+    private(set) var textureResource: TextureResource? = nil
+    
+    /// The LowLevelMesh object which should be assigned to an entity in the scene, positioned at the origin.
+    ///
+    /// This mesh is auomatically updated when you change ``meshGeometry`` via ``updateMeshGeometry()``.
+    /// LowLevelMesh is a class (reference type) so you can add it to your Entity's `MeshResource` once at init time.
+    let lowLevelMesh: LowLevelMesh
+
+    /// Topology characteristics of the generated mesh.  This is fixed at initialization time.
+    let meshTopology: CurvedMeshTopology
+
+    /// The current generated mesh geometry.  Update this with ``updateMeshGeometry()``
+    private(set) var meshGeometry: CurvedMeshGeometry = CurvedMeshGeometry(width: 1, height: 1)
+    
+    /// An async task responsible for managing physics mesh cooking.
+    ///
+    /// This guarantees that at most one cooking operation is active at a time.
+    /// Cooking generally takes > 1 frame, so it's important that there is not an explosion of redundant work
+    /// if there is a burst of resize activity.
+    private var physicsCookingTask: Task<Void, Never>?
+    
+    /// ``collisionShape`` is up to date with this `CurvedMeshGeometry`.
+    private var lastCookedGeometry: CurvedMeshGeometry?
+
+    /// LowLevelTexture that backs ``textureResource``.
+    private var lowLevelTexture: LowLevelTexture?
+    
+    struct CurvedMeshTopology: Sendable, Equatable {
+        /// Number of horizontal segments to use to generate the mesh grid
+        var segmentsX: Int = 32
+        
+        /// Number of vertical segments to use to generate the mesh grid
+        var segmentsY: Int = 32
+        
+        /// Total number of vertices required to generate a mesh with this topology
+        var vertexCount: Int { (segmentsX + 1) * (segmentsY + 1) }
+        
+        /// Total size of the index buffer when generating a mesh with this topology
+        var indexCount: Int { segmentsX * segmentsY * 6 }
+    }
+    
+    struct CurvedMeshGeometry: Sendable, Equatable {
+        /// Width of the mesh in meters.
+        var width: Float
+
+        /// Height of the mesh in meters.
+        var height: Float
+
+        /// Radius of the mesh curvature in meters, or `nil` for a flat mesh.
+        var curvatureRadius: Float = 0
+        
+        /// The bounding box of the mesh
+        var bounds: BoundingBox = BoundingBox()
+    
+        /// Current snapped status
+        var snapped: Bool = false
+
+        /// Converts a 3D position on the mesh surface (in meters, relative to mesh center)
+        /// to normalized texture coordinates (0..1, 0..1).
+        func normalizedUV(fromMeshPosition position: SIMD3<Float>) -> SIMD2<Float> {
+            if curvatureRadius > 0 {
+                let halfWidth = bounds.extents.x / 2
+                
+                let theta = asinf(halfWidth / curvatureRadius)
+                let angle = asinf(position.x / curvatureRadius)
+                
+                let u = (angle / theta + 1) / 2
+                let v = (position.y / height) + 0.5
+                return SIMD2(u, v)
+            } else {
+                let u = (position.x / width) + 0.5
+                let v = (position.y / height) + 0.5
+                return SIMD2(u, v)
+            }
+        }
+    }
+
+    init(meshTopology: CurvedMeshTopology = CurvedMeshTopology(),
+         meshGeometry: CurvedMeshGeometry = CurvedMeshGeometry(width: 1, height: 1)) {
+        self.meshTopology = meshTopology
+        self.meshGeometry = CurvedMeshGeometry(width: -1, height: -1)
+        
+        let lowLevelMesh = try! meshTopology.generateMesh()
+
+        self.lowLevelMesh = lowLevelMesh
+        
+        updateMeshGeometry(meshGeometry)
+    }
+    
+    // MARK: - Mesh Generation (LowLevelMesh)
+    
+    func updateSnappedStatus(snapped: Bool) {
+        var geometry = self.meshGeometry
+        geometry.snapped = snapped
+        updateMeshGeometry(geometry)
+    }
+    
+    func updateMeshSize(width: Float, height: Float) {
+        var geometry = self.meshGeometry
+        geometry.width = width
+        geometry.height = height
+        updateMeshGeometry(geometry)
+    }
+    
+    func updateMeshCurvature(curvatureRadius: Float) {
+        var geometry = self.meshGeometry
+        geometry.curvatureRadius = curvatureRadius
+        updateMeshGeometry(geometry)
+    }
+
+    /// Writes vertex position/normal/uv data into the LowLevelMesh buffer.
+    /// This is the fast path — called on every size or curvature change without
+    /// recreating MeshResource or Entity.
+    func updateMeshGeometry(_ meshGeometry: CurvedMeshGeometry) {
+        if meshGeometry == self.meshGeometry {
+            return // nothing to do
+        }
+        
+        let width = meshGeometry.width
+        let height = meshGeometry.height
+        let curvatureRadius = meshGeometry.curvatureRadius
+        
+        let segmentsX = meshTopology.segmentsX
+        let segmentsY = meshTopology.segmentsY
+        let indexCount = meshTopology.indexCount
+        
+        var boundsMin = SIMD3(repeating: Float.infinity)
+        var boundsMax = SIMD3(repeating: -Float.infinity)
+        
+        lowLevelMesh.withUnsafeMutableBytes(bufferIndex: 0) { rawBytes in
+            let vertices = rawBytes.bindMemory(to: CurvedPlaneVertex.self)
+
+            if curvatureRadius > 0 {
+
+                // Apply cylindrical curve: Z varies with X to create wrap-around
+                var curve_positions: [SIMD3<Float>] = []
+                var curve_normals: [SIMD3<Float>] = []
+                let r = curvatureRadius
+                let arc_length = width / r
+                for x in 0...segmentsX {
+                    let u = Float(x) / Float(segmentsX)
+                    let angle = (u - 0.5) * arc_length
+                    let vec: SIMD3<Float> = simd_normalize([sin(angle), 0.0, cos(angle)])
+                    let pos: SIMD3<Float> = [vec.x, vec.y, 1.0 - vec.z] * r
+                    curve_positions.append(pos)
+
+                    // Normal points toward viewer for convex curve
+                    curve_normals.append(-vec)
+                }
+                let offsetZ = meshGeometry.snapped ? 0 : -curve_positions[0].z
+                
+                for y in 0...segmentsY {
+                    let v = Float(y) / Float(segmentsY) * 2 - 1
+                    let posY = v * height / 2
+
+                    for x in 0...segmentsX {
+                        let u = Float(x) / Float(segmentsX) * 2 - 1
+                        
+                        let position = curve_positions[x] + SIMD3<Float>(0, posY, offsetZ)
+                        let normal = curve_normals[x]
+
+                        let idx = y * (segmentsX + 1) + x
+                        vertices[idx].position = position
+                        vertices[idx].normal = normal
+                        vertices[idx].uv = SIMD2<Float>((u + 1) / 2, (v + 1) / 2)
+                        
+                        boundsMin = min(boundsMin, position)
+                        boundsMax = max(boundsMax, position)
+                    }
+                }
+            } else {
+                // Flat plane — same grid, z=0
+                for y in 0...segmentsY {
+                    let v = Float(y) / Float(segmentsY)
+                    let posY = (v - 0.5) * height
+
+                    for x in 0...segmentsX {
+                        let u = Float(x) / Float(segmentsX)
+                        let posX = (u - 0.5) * width
+
+                        let idx = y * (segmentsX + 1) + x
+                        let position = SIMD3<Float>(posX, posY, 0)
+                        vertices[idx].position = position
+                        vertices[idx].normal = SIMD3<Float>(0, 0, -1)
+                        vertices[idx].uv = SIMD2<Float>(u, v)
+                        
+                        boundsMin = min(boundsMin, position)
+                        boundsMax = max(boundsMax, position)
+                    }
+                }
+            }
+        }
+
+        let bounds = BoundingBox(min: boundsMin, max: boundsMax)
+        lowLevelMesh.parts.replaceAll([
+            LowLevelMesh.Part(indexCount: indexCount, topology: .triangle, bounds: bounds)
+        ])
+        
+        self.meshGeometry = meshGeometry
+        self.meshGeometry.bounds = bounds
+        invalidatePhysicsMesh()
+    }
+
+    // MARK: - Physics Mesh Cooking
+
+    /// Schedules an async physics mesh cook. If a cook is already in progress,
+    /// it will automatically re-cook when done if the geometry has changed.
+    private func invalidatePhysicsMesh() {
+        guard physicsCookingTask == nil else { return }
+        physicsCookingTask = Task {
+            defer { physicsCookingTask = nil }
+            // Loop until the cooked physics mesh matches the current geometry.
+            // Each iteration cooks against whatever the MeshResource currently reflects.
+            while lastCookedGeometry != meshGeometry {
+                let geometryAtStart = meshGeometry
+                do {
+                    let meshResource = try await MeshResource(from: lowLevelMesh)
+                    let shape = try await ShapeResource.generateStaticMesh(from: meshResource)
+                    collisionShape = shape
+                    lastCookedGeometry = geometryAtStart
+                } catch {
+                    NSLog("SDL_RealityKitHelper: Failed to generate physics mesh: %@", error.localizedDescription)
+                    break
+                }
+            }
+        }
+    }
+
+    // MARK: - Texture Updates (LowLevelTexture Pipeline)
+
+    /// Creates or recreates the LowLevelTexture for the given dimensions
+    private func ensureLowLevelTexture(width: Int, height: Int, pixelFormat: MTLPixelFormat) {
+        // Check if we need to recreate (size or format changed)
+        if let lowLevelTexture,
+           lowLevelTexture.descriptor.width == width,
+           lowLevelTexture.descriptor.height == height,
+           lowLevelTexture.descriptor.pixelFormat == pixelFormat
+        {
+            return
+        }
+
+        //NSLog("SDL_RealityKitHelper: Creating LowLevelTexture %dx%d", width, height)
+
+        do {
+            // Create LowLevelTexture descriptor using Metal pixel format directly
+            var descriptor = LowLevelTexture.Descriptor()
+            descriptor.textureType = .type2D
+            descriptor.pixelFormat = pixelFormat
+            descriptor.width = width
+            descriptor.height = height
+            descriptor.depth = 1
+            let size = max(width, height)
+            if (size > 32) {
+                descriptor.mipmapLevelCount = Int(floor(log2(Float(size)))) - 5
+            } else {
+                descriptor.mipmapLevelCount = 0
+            }
+            descriptor.textureUsage = [.shaderRead, .renderTarget]
+
+            // Create the LowLevelTexture
+            lowLevelTexture = try LowLevelTexture(descriptor: descriptor)
+
+            // Create TextureResource from LowLevelTexture (this is reusable)
+            textureResource = try TextureResource(from: lowLevelTexture!)
+
+            //NSLog("SDL_RealityKitHelper: LowLevelTexture created successfully")
+        } catch {
+            NSLog("SDL_RealityKitHelper: ERROR - Failed to create LowLevelTexture: %@", error.localizedDescription)
+            lowLevelTexture = nil
+            textureResource = nil
+        }
+    }
+
+    @objc public func getDisplayTexture(_ commandBuffer: MTLCommandBuffer, width: Int, height: Int, pixelFormat: MTLPixelFormat) -> MTLTexture? {
+        // Ensure LowLevelTexture exists with correct dimensions
+        ensureLowLevelTexture(
+            width: width,
+            height: height,
+            pixelFormat: pixelFormat
+        )
+
+        guard let llt = lowLevelTexture else {
+            NSLog("SDL_RealityKitHelper: ERROR - No LowLevelTexture available")
+            return nil
+        }
+
+        // Get the writable texture from LowLevelTexture
+        return llt.replace(using: commandBuffer)
+    }
+}
+
+extension SDL_RealityKitHelper.CurvedMeshTopology {
+    @MainActor
+    func generateMesh() throws -> LowLevelMesh {
+        //NSLog("SDL_RealityKitHelper: Creating LowLevelMesh (%dx%d grid, %d vertices, %d indices)",
+        //      segmentsX, segmentsY, vertexCount, indexCount)
+
+        // Create LowLevelMesh with our custom vertex format
+        let desc = CurvedPlaneVertex.descriptor(vertexCount: vertexCount, indexCount: indexCount)
+        let mesh = try LowLevelMesh(descriptor: desc)
+
+        // Write index buffer once — topology never changes for a fixed grid
+        mesh.withUnsafeMutableIndices { rawIndices in
+            let indices = rawIndices.bindMemory(to: UInt32.self)
+            var idx = 0
+            for y in 0..<segmentsY {
+                for x in 0..<segmentsX {
+                    let i0 = UInt32(y * (segmentsX + 1) + x)
+                    let i1 = i0 + 1
+                    let i2 = i0 + UInt32(segmentsX + 1)
+                    let i3 = i2 + 1
+
+                    // Two triangles per quad (counter-clockwise winding)
+                    indices[idx]     = i0
+                    indices[idx + 1] = i1
+                    indices[idx + 2] = i2
+                    indices[idx + 3] = i1
+                    indices[idx + 4] = i3
+                    indices[idx + 5] = i2
+                    idx += 6
+                }
+            }
+        }
+        
+        return mesh
+    }
+}

+ 50 - 0
src/video/uikit/SDL_UIKitBridge-objc.h

@@ -0,0 +1,50 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#ifndef SDL_uikitvisionosscene_h_
+#define SDL_uikitvisionosscene_h_
+
+#import <UIKit/UIKit.h>
+#import <Metal/Metal.h>
+
+/**
+ * Return true if the curved content pointer mode is enabled
+ */
+bool SDL_VisionOS_PointerModeEnabled();
+
+/**
+ * Check if any window is using curved content mode (UIHostingController-based).
+ */
+bool SDL_UIKit_HasCurvedWindow();
+
+/**
+ * Check if a window is using curved content mode (UIHostingController-based).
+ *
+ * @param window The SDL window to check.
+ * @return true if the window is in curved mode, false otherwise.
+ */
+bool SDL_UIKit_IsCurvedWindow(SDL_Window *window);
+
+/**
+ * Get the curved content display texture.
+ */
+id<MTLTexture> SDL_UIKit_GetCurvedDisplayTexture(SDL_Window *window, id<MTLCommandBuffer> commandBuffer, int width, int height, MTLPixelFormat pixelFormat);
+
+#endif /* SDL_uikitvisionosscene_h_ */

+ 39 - 0
src/video/uikit/SDL_UIKitBridge-swift.h

@@ -0,0 +1,39 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#import "SDL_uikitviewcontroller.h"
+
+// Called from Swift scene delegates when window size changes
+void SDL_VisionOS_SendSizeChanged(long width, long height);
+
+// Called from Swift scene delegates to get the initial curvature
+float SDL_VisionOS_GetCurvature();
+
+// Called from Swift scene delegates when window curvature changes
+void SDL_VisionOS_SendCurvatureChanged(float curvature);
+
+// Called from Swift scene delegates when pointer mode changes
+void SDL_VisionOS_SendPointerMode(bool enabled);
+
+// Called from Swift scene delegates when visionOS delivers a touch event
+void SDL_VisionOS_SendTouch(NSTimeInterval timestamp, SDL_FingerID fingerID, Uint32 eventType, float x, float y);
+
+// Called from Swift to register the RealityKit hosting object with the SDL window
+void SDL_VisionOS_SetWindowRealityKitHosting(id hosting);

+ 187 - 0
src/video/uikit/SDL_UIKitBridge.m

@@ -0,0 +1,187 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_PLATFORM_VISIONOS
+
+#include "SDL_UIKitBridge-objc.h"
+#include "SDL_UIKitBridge-swift.h"
+#include "SDL_uikitevents.h"
+#include "SDL_uikitwindow.h"
+#include "SDL_uikitmetalview.h"
+#include "../../events/SDL_events_c.h"
+
+
+// Called from Swift scene delegates when window size changes
+void SDL_VisionOS_SendSizeChanged(long width, long height)
+{
+    SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
+    if (window) {
+        SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
+        CGRect bounds = CGRectMake(0, 0, width, height);
+
+        // Update the UIWindow
+        data.uiwindow.frame = bounds;
+
+        // Update the view
+        UIView *view = data.viewcontroller.view;
+        view.bounds = bounds;
+
+        // Update the metal layer
+        if ([view isKindOfClass:[SDL_uikitmetalview class]]) {
+            SDL_uikitmetalview *metalview = (SDL_uikitmetalview *)view;
+
+            [metalview updateDrawableSize];
+        }
+
+        // Send the resize event
+        SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, (int)width, (int)height);
+    }
+}
+
+// Called from Swift scene delegates to get the initial curvature
+float SDL_VisionOS_GetCurvature()
+{
+    SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
+    if (window) {
+        SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
+        return data.curvature;
+    }
+    return 0.0f;
+}
+
+// Called from Swift scene delegates when window curvature changes
+void SDL_VisionOS_SendCurvatureChanged(float curvature)
+{
+    SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
+    if (window) {
+        SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
+        if (curvature != data.curvature) {
+            data.curvature = curvature;
+            SDL_SetFloatProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_CURVATURE_FLOAT, curvature);
+            SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_CURVATURE_CHANGED, (int)curvature, 0);
+        }
+    }
+}
+
+static bool SDL_pointer_mode;
+
+void SDL_VisionOS_SendPointerMode(bool enabled)
+{
+    SDL_pointer_mode = enabled;
+}
+
+bool SDL_VisionOS_PointerModeEnabled()
+{
+    return SDL_pointer_mode;
+}
+
+// Called from Swift scene delegates when visionOS delivers a touch event
+void SDL_VisionOS_SendTouch(NSTimeInterval timestamp, SDL_FingerID fingerID, Uint32 eventType, float x, float y)
+{
+    const SDL_TouchID directTouchId = 1;
+    SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
+    if (!window) {
+        return;
+    }
+
+    float pressure;
+    if (eventType == SDL_EVENT_FINGER_DOWN || eventType == SDL_EVENT_FINGER_MOTION) {
+        pressure = 1.0f;
+    } else {
+        pressure = 0.0f;
+    }
+    if (eventType == SDL_EVENT_FINGER_MOTION) {
+        SDL_SendTouchMotion(UIKit_GetEventTimestamp(timestamp), directTouchId, fingerID, window, x, y, pressure);
+    } else {
+        SDL_SendTouch(UIKit_GetEventTimestamp(timestamp), directTouchId, fingerID, window, (SDL_EventType)eventType, x, y, pressure);
+    }
+}
+
+// MARK: - RealityKit Content Hosting
+
+// Called from Swift to register the RealityKit hosting object with the SDL window.
+void SDL_VisionOS_SetWindowRealityKitHosting(id hosting)
+{
+    SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
+    if (!window) {
+        SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "VISIONOS: No focused window for RealityKit hosting");
+        return;
+    }
+
+    SDL_UIKitWindowData *windowData = (__bridge SDL_UIKitWindowData *)window->internal;
+    windowData.curvedContentHosting = hosting;
+
+    // Updating curvedContentHosting updates the view controller so that the "container background" is hidden.
+    // On visionOS, this gets rid of the default glass background effect (not wanted for our content).
+    [windowData.viewcontroller setNeedsUpdateOfPreferredContainerBackgroundStyle];
+
+    //SDL_Log("VISIONOS: RealityKit hosting registered");
+}
+
+bool SDL_UIKit_HasCurvedWindow()
+{
+    SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
+    if (window) {
+        return SDL_UIKit_IsCurvedWindow(window);
+    }
+    return false;
+}
+
+bool SDL_UIKit_IsCurvedWindow(SDL_Window *window)
+{
+    SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
+    return data && data.curvedContentHosting;
+}
+
+id<MTLTexture> SDL_UIKit_GetCurvedDisplayTexture(SDL_Window *window, id<MTLCommandBuffer> commandBuffer, int width, int height, MTLPixelFormat pixelFormat)
+{
+    SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
+    if (!data || !data.curvedContentHosting) {
+        return nil;
+    }
+
+    id hosting = data.curvedContentHosting;
+    SEL getTextureSelector = NSSelectorFromString(@"getDisplayTexture:width:height:pixelFormat:");
+    if (![hosting respondsToSelector:getTextureSelector]) {
+        return nil;
+    }
+
+    NSMethodSignature *signature = [hosting methodSignatureForSelector:getTextureSelector];
+    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+    [invocation setSelector:getTextureSelector];
+    [invocation setTarget:hosting];
+
+    long arg_width = width;
+    long arg_height = height;
+    [invocation setArgument:&commandBuffer atIndex:2];
+    [invocation setArgument:&arg_width atIndex:3];
+    [invocation setArgument:&arg_height atIndex:4];
+    [invocation setArgument:&pixelFormat atIndex:5];
+    [invocation invoke];
+
+    __unsafe_unretained id temp = nil;
+    [invocation getReturnValue:&temp];
+    id<MTLTexture> texture = temp;
+    return texture;
+}
+
+#endif /* SDL_PLATFORM_VISIONOS */

+ 24 - 6
src/video/uikit/SDL_uikitevents.m

@@ -29,6 +29,7 @@
 #include "SDL_uikitopengles.h"
 #include "SDL_uikitopengles.h"
 #include "SDL_uikitvideo.h"
 #include "SDL_uikitvideo.h"
 #include "SDL_uikitwindow.h"
 #include "SDL_uikitwindow.h"
+#include "SDL_UIKitBridge-objc.h"
 
 
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
 #import <GameController/GameController.h>
 #import <GameController/GameController.h>
@@ -308,6 +309,12 @@ static bool SetGCMouseRelativeMode(bool enabled)
 static void OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button, BOOL pressed)
 static void OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button, BOOL pressed)
 {
 {
     Uint64 timestamp = SDL_GetTicksNS();
     Uint64 timestamp = SDL_GetTicksNS();
+
+#ifdef SDL_PLATFORM_VISIONOS
+    if (!SDL_VisionOS_PointerModeEnabled() && SDL_UIKit_HasCurvedWindow()) {
+        return;
+    }
+#endif
     SDL_SendMouseButton(timestamp, SDL_GetMouseFocus(), mouseID, button, pressed);
     SDL_SendMouseButton(timestamp, SDL_GetMouseFocus(), mouseID, button, pressed);
 }
 }
 
 
@@ -318,19 +325,19 @@ static void OnGCMouseConnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14
     SDL_AddMouse(mouseID, NULL);
     SDL_AddMouse(mouseID, NULL);
 
 
     mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
     mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
-      OnGCMouseButtonChanged(mouseID, SDL_BUTTON_LEFT, pressed);
+        OnGCMouseButtonChanged(mouseID, SDL_BUTTON_LEFT, pressed);
     };
     };
     mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
     mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
-      OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed);
+        OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed);
     };
     };
     mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
     mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
-      OnGCMouseButtonChanged(mouseID, SDL_BUTTON_RIGHT, pressed);
+        OnGCMouseButtonChanged(mouseID, SDL_BUTTON_RIGHT, pressed);
     };
     };
 
 
     int auxiliary_button = SDL_BUTTON_X1;
     int auxiliary_button = SDL_BUTTON_X1;
     for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) {
     for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) {
         btn.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
         btn.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
-          OnGCMouseButtonChanged(mouseID, auxiliary_button, pressed);
+            OnGCMouseButtonChanged(mouseID, auxiliary_button, pressed);
         };
         };
         ++auxiliary_button;
         ++auxiliary_button;
     }
     }
@@ -338,21 +345,32 @@ static void OnGCMouseConnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14
     mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput *mouseInput, float deltaX, float deltaY) {
     mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput *mouseInput, float deltaX, float deltaY) {
         Uint64 timestamp = SDL_GetTicksNS();
         Uint64 timestamp = SDL_GetTicksNS();
 
 
-        if (SDL_GCMouseRelativeMode()) {
+        bool send_motion = SDL_GCMouseRelativeMode();
+#ifdef SDL_PLATFORM_VISIONOS
+        if (!send_motion && SDL_VisionOS_PointerModeEnabled()) {
+            send_motion = true;
+        }
+#endif
+        if (send_motion) {
             SDL_SendMouseMotion(timestamp, SDL_GetMouseFocus(), mouseID, true, deltaX, -deltaY);
             SDL_SendMouseMotion(timestamp, SDL_GetMouseFocus(), mouseID, true, deltaX, -deltaY);
         }
         }
     };
     };
 
 
     mouse.mouseInput.scroll.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
     mouse.mouseInput.scroll.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
         Uint64 timestamp = SDL_GetTicksNS();
         Uint64 timestamp = SDL_GetTicksNS();
-
+        
         /* Raw scroll values come in here, vertical values in the first axis, horizontal values in the second axis.
         /* Raw scroll values come in here, vertical values in the first axis, horizontal values in the second axis.
          * The vertical values are negative moving the mouse wheel up and positive moving it down.
          * The vertical values are negative moving the mouse wheel up and positive moving it down.
          * The horizontal values are negative moving the mouse wheel left and positive moving it right.
          * The horizontal values are negative moving the mouse wheel left and positive moving it right.
          * The vertical values are inverted compared to SDL, and the horizontal values are as expected.
          * The vertical values are inverted compared to SDL, and the horizontal values are as expected.
          */
          */
+#ifdef SDL_PLATFORM_VISIONOS
+        float vertical = -yValue;
+        float horizontal = xValue;
+#else
         float vertical = -xValue;
         float vertical = -xValue;
         float horizontal = yValue;
         float horizontal = yValue;
+#endif
 
 
         if (mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) {
         if (mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) {
             // Since these are raw values, we need to flip them ourselves
             // Since these are raw values, we need to flip them ourselves

+ 21 - 20
src/video/uikit/SDL_uikitmetalview.h

@@ -1,24 +1,23 @@
 /*
 /*
- Simple DirectMedia Layer
- Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
-
- This software is provided 'as-is', without any express or implied
- warranty.  In no event will the authors be held liable for any damages
- arising from the use of this software.
-
- Permission is granted to anyone to use this software for any purpose,
- including commercial applications, and to alter it and redistribute it
- freely, subject to the following restrictions:
-
- 1. The origin of this software must not be misrepresented; you must not
- claim that you wrote the original software. If you use this software
- in a product, an acknowledgment in the product documentation would be
- appreciated but is not required.
- 2. Altered source versions must be plainly marked as such, and must not be
- misrepresented as being the original software.
- 3. This notice may not be removed or altered from any source distribution.
- */
-
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
 /*
 /*
  * @author Mark Callow, www.edgewise-consulting.com.
  * @author Mark Callow, www.edgewise-consulting.com.
  *
  *
@@ -43,6 +42,8 @@
 - (instancetype)initWithFrame:(CGRect)frame
 - (instancetype)initWithFrame:(CGRect)frame
                         scale:(CGFloat)scale;
                         scale:(CGFloat)scale;
 
 
+- (void)updateDrawableSize;
+
 @end
 @end
 
 
 SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window);
 SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window);

+ 19 - 20
src/video/uikit/SDL_uikitmetalview.m

@@ -1,24 +1,23 @@
 /*
 /*
- Simple DirectMedia Layer
- Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
-
- This software is provided 'as-is', without any express or implied
- warranty.  In no event will the authors be held liable for any damages
- arising from the use of this software.
-
- Permission is granted to anyone to use this software for any purpose,
- including commercial applications, and to alter it and redistribute it
- freely, subject to the following restrictions:
-
- 1. The origin of this software must not be misrepresented; you must not
- claim that you wrote the original software. If you use this software
- in a product, an acknowledgment in the product documentation would be
- appreciated but is not required.
- 2. Altered source versions must be plainly marked as such, and must not be
- misrepresented as being the original software.
- 3. This notice may not be removed or altered from any source distribution.
- */
-
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
 /*
 /*
  * @author Mark Callow, www.edgewise-consulting.com.
  * @author Mark Callow, www.edgewise-consulting.com.
  *
  *

+ 18 - 18
src/video/uikit/SDL_uikitview.m

@@ -1,22 +1,22 @@
 /*
 /*
- Simple DirectMedia Layer
- Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
-
- This software is provided 'as-is', without any express or implied
- warranty.  In no event will the authors be held liable for any damages
- arising from the use of this software.
-
- Permission is granted to anyone to use this software for any purpose,
- including commercial applications, and to alter it and redistribute it
- freely, subject to the following restrictions:
-
- 1. The origin of this software must not be misrepresented; you must not
-    claim that you wrote the original software. If you use this software
-    in a product, an acknowledgment in the product documentation would be
-    appreciated but is not required.
- 2. Altered source versions must be plainly marked as such, and must not be
-    misrepresented as being the original software.
- 3. This notice may not be removed or altered from any source distribution.
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
 */
 */
 #include "SDL_internal.h"
 #include "SDL_internal.h"
 
 

+ 4 - 0
src/video/uikit/SDL_uikitviewcontroller.h

@@ -59,6 +59,10 @@
 - (void)loadView;
 - (void)loadView;
 - (void)viewDidLayoutSubviews;
 - (void)viewDidLayoutSubviews;
 
 
+#ifdef SDL_PLATFORM_VISIONOS
+- (void)initializeVisionOSCurvedUI;
+#endif
+
 #ifndef SDL_PLATFORM_TVOS
 #ifndef SDL_PLATFORM_TVOS
 - (NSUInteger)supportedInterfaceOrientations;
 - (NSUInteger)supportedInterfaceOrientations;
 - (BOOL)prefersStatusBarHidden;
 - (BOOL)prefersStatusBarHidden;

+ 26 - 0
src/video/uikit/SDL_uikitviewcontroller.m

@@ -33,6 +33,10 @@
 #include "SDL_uikitwindow.h"
 #include "SDL_uikitwindow.h"
 #include "SDL_uikitopengles.h"
 #include "SDL_uikitopengles.h"
 
 
+#ifdef SDL_PLATFORM_VISIONOS
+#import "SDL3/SDL3-Swift.h"
+#endif
+
 #ifdef SDL_PLATFORM_TVOS
 #ifdef SDL_PLATFORM_TVOS
 static void SDLCALL SDL_AppleTVControllerUIHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 static void SDLCALL SDL_AppleTVControllerUIHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 {
 {
@@ -119,6 +123,15 @@ static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char
             }
             }
         }
         }
     }
     }
+
+#ifdef SDL_PLATFORM_VISIONOS
+    if (@available(visionOS 26.0, *)) {
+        SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)self.window->internal;
+        if (data.curvature >= 0.0f) {
+            [self initializeVisionOSCurvedUI];
+        }
+    }
+#endif
     return self;
     return self;
 }
 }
 
 
@@ -141,6 +154,19 @@ static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char
 #endif
 #endif
 }
 }
 
 
+#ifdef SDL_PLATFORM_VISIONOS
+- (UIContainerBackgroundStyle)preferredContainerBackgroundStyle
+{
+    if (self.window) {
+        SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)self.window->internal;
+        if (data && data.curvedContentHosting) {
+            return UIContainerBackgroundStyleHidden;
+        }
+    }
+    return UIContainerBackgroundStyleAutomatic;
+}
+#endif
+
 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
 {
 {
     SDL_SetSystemTheme(UIKit_GetSystemTheme());
     SDL_SetSystemTheme(UIKit_GetSystemTheme());

+ 33 - 0
src/video/uikit/SDL_uikitviewcontroller.swift

@@ -0,0 +1,33 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+import SwiftUI
+
+extension SDL_uikitviewcontroller {
+    @available(visionOS 26.0, *)
+    @objc func initializeVisionOSCurvedUI() {
+        Task {
+            let hosting = SDL_CurvedContentHosting()
+            hosting.present(from: self)
+            SDL_VisionOS_SetWindowRealityKitHosting(hosting)
+        }
+    }
+}

+ 6 - 0
src/video/uikit/SDL_uikitwindow.h

@@ -52,6 +52,12 @@ extern NSUInteger UIKit_GetSupportedOrientations(SDL_Window *window);
 // Array of SDL_uikitviews owned by this window.
 // Array of SDL_uikitviews owned by this window.
 @property(nonatomic, copy) NSMutableArray *views;
 @property(nonatomic, copy) NSMutableArray *views;
 
 
+#ifdef SDL_PLATFORM_VISIONOS
+// Hosting controller for curved content mode (UIHostingController-based)
+@property(nonatomic, strong) id curvedContentHosting;
+@property(nonatomic, assign) CGFloat curvature;
+#endif
+
 @end
 @end
 
 
 #endif // SDL_uikitwindow_h_
 #endif // SDL_uikitwindow_h_

+ 15 - 6
src/video/uikit/SDL_uikitwindow.m

@@ -53,7 +53,7 @@
 
 
 @end
 @end
 
 
-static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow *uiwindow, bool created)
+static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow *uiwindow, SDL_PropertiesID create_props, bool created)
 {
 {
     SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
     SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
     SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal;
     SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal;
@@ -106,6 +106,19 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow
 #endif
 #endif
     window->w = width;
     window->w = width;
     window->h = height;
     window->h = height;
+    
+    SDL_PropertiesID props = SDL_GetWindowProperties(window);
+    SDL_SetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, (__bridge void *)data.uiwindow);
+    SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG);
+
+#ifdef SDL_PLATFORM_VISIONOS
+    float curvature = SDL_GetFloatProperty(create_props, SDL_PROP_WINDOW_CREATE_CURVATURE_FLOAT, -1.0f);
+    if (curvature > 0.0f && curvature <= 1.0f) {
+        curvature = 0.0f;
+    }
+    data.curvature = curvature;
+    SDL_SetFloatProperty(props, SDL_PROP_WINDOW_CURVATURE_FLOAT, curvature);
+#endif
 
 
     /* The View Controller will handle rotating the view when the device
     /* The View Controller will handle rotating the view when the device
      * orientation changes. This will trigger resize events, if appropriate. */
      * orientation changes. This will trigger resize events, if appropriate. */
@@ -119,10 +132,6 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow
      * hierarchy. */
      * hierarchy. */
     [view setSDLWindow:window];
     [view setSDLWindow:window];
 
 
-    SDL_PropertiesID props = SDL_GetWindowProperties(window);
-    SDL_SetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, (__bridge void *)data.uiwindow);
-    SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG);
-
     return true;
     return true;
 }
 }
 
 
@@ -228,7 +237,7 @@ bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti
         }
         }
 #endif
 #endif
 
 
-        if (!SetupWindowData(_this, window, uiwindow, true)) {
+        if (!SetupWindowData(_this, window, uiwindow, create_props, true)) {
             return false;
             return false;
         }
         }
     }
     }