diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index 3704aa73ee..33664dcc30 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -50,13 +50,14 @@ 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 */; }; 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, ); }; - 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, ); }; 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, ); }; }; - 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, ); }; }; 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 */; }; @@ -82,8 +83,8 @@ 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 */; }; 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 */; }; 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 */; }; @@ -368,8 +369,6 @@ 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 */; }; 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 */; }; 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 */; }; @@ -434,6 +433,13 @@ 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 */; }; 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 */; }; 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 */; }; @@ -559,7 +565,7 @@ 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 */; }; 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 */ /* 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; }; 02D6A1C128A84B8F00A7F001 /* SDL_hidapi_sinput.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_sinput.c; sourceTree = ""; }; 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 = ""; }; 5616CA49252BB2A5005D5928 /* SDL_url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_url.c; sourceTree = ""; }; 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysurl.h; sourceTree = ""; }; 5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDL_sysurl.m; sourceTree = ""; }; @@ -970,7 +977,6 @@ F338A1192D1B37E4007CDFDF /* SDL_tray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_tray.c; sourceTree = ""; }; F3395BA72D9A5971007246C8 /* SDL_hidapi_8bitdo.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_8bitdo.c; sourceTree = ""; }; F3395BA72D9A5971007246C9 /* SDL_hidapi_flydigi.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_flydigi.c; sourceTree = ""; }; - F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_gamesir.c; sourceTree = ""; }; F344003C2D4022E1003F26D7 /* INSTALL.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = INSTALL.md; sourceTree = ""; }; F362B9152B3349E200D30B94 /* controller_list.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = controller_list.h; sourceTree = ""; }; F362B9162B3349E200D30B94 /* SDL_gamepad_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gamepad_c.h; sourceTree = ""; }; @@ -1026,6 +1032,13 @@ F3990E022A788303000D8759 /* SDL_hidapi_mac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_mac.h; sourceTree = ""; }; F3990E032A788303000D8759 /* SDL_hidapi_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_ios.h; sourceTree = ""; }; F3A4909D2554D38500E92A8B /* SDL_hidapi_ps5.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps5.c; sourceTree = ""; }; + F3A837162F69C80100AD32B6 /* SDL_RealityKitHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_RealityKitHelper.swift; sourceTree = ""; }; + F3A8956D2F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedContentHosting.swift; sourceTree = ""; }; + F3A8956E2F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedContentView.swift; sourceTree = ""; }; + F3A895762F7DC14400B9E5C2 /* SDL_UIKitBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDL_UIKitBridge.m; sourceTree = ""; }; + F3A895772F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDL_UIKitBridge-objc.h"; sourceTree = ""; }; + F3A895782F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDL_UIKitBridge-swift.h"; sourceTree = ""; }; + F3A8957C2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_uikitviewcontroller.swift; sourceTree = ""; }; F3A9AE922C8A13C100AAC390 /* SDL_gpu_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gpu_util.h; sourceTree = ""; }; F3A9AE932C8A13C100AAC390 /* SDL_render_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_render_gpu.c; sourceTree = ""; }; F3A9AE942C8A13C100AAC390 /* SDL_shaders_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_shaders_gpu.c; sourceTree = ""; }; @@ -1149,6 +1162,7 @@ F3FA5A1A2B59ACE000FEAD97 /* yuv_rgb_lsx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = yuv_rgb_lsx.c; sourceTree = ""; }; F3FA5A1B2B59ACE000FEAD97 /* yuv_rgb_lsx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_lsx.h; sourceTree = ""; }; F3FA5A1C2B59ACE000FEAD97 /* yuv_rgb_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_common.h; sourceTree = ""; }; + F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_gamesir.c; sourceTree = ""; }; F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_nintendo.h; sourceTree = ""; }; F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_hori.c; sourceTree = ""; }; F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = ""; }; @@ -1738,8 +1752,15 @@ A7D8A61823E2513D00DCD162 /* uikit */ = { isa = PBXGroup; children = ( + F3A8956D2F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift */, + F3A8956E2F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift */, + 3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */, + F3A837162F69C80100AD32B6 /* SDL_RealityKitHelper.swift */, A7D8A62F23E2513D00DCD162 /* SDL_uikitappdelegate.h */, A7D8A61E23E2513D00DCD162 /* SDL_uikitappdelegate.m */, + F3A895762F7DC14400B9E5C2 /* SDL_UIKitBridge.m */, + F3A895772F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h */, + F3A895782F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h */, A7D8A62123E2513D00DCD162 /* SDL_uikitclipboard.h */, A7D8A62A23E2513D00DCD162 /* SDL_uikitclipboard.m */, A7D8A62D23E2513D00DCD162 /* SDL_uikitevents.h */, @@ -1754,18 +1775,19 @@ A7D8A62323E2513D00DCD162 /* SDL_uikitopengles.m */, A7D8A62B23E2513D00DCD162 /* SDL_uikitopenglview.h */, A7D8A62023E2513D00DCD162 /* SDL_uikitopenglview.m */, + 000063D3D80F97ADC7770000 /* SDL_uikitpen.h */, + 000053D344416737F6050000 /* SDL_uikitpen.m */, A7D8A62223E2513D00DCD162 /* SDL_uikitvideo.h */, A7D8A63223E2513D00DCD162 /* SDL_uikitvideo.m */, A7D8A61923E2513D00DCD162 /* SDL_uikitview.h */, A7D8A62923E2513D00DCD162 /* SDL_uikitview.m */, A7D8A62423E2513D00DCD162 /* SDL_uikitviewcontroller.h */, A7D8A63023E2513D00DCD162 /* SDL_uikitviewcontroller.m */, + F3A8957C2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift */, A7D8A63323E2513D00DCD162 /* SDL_uikitvulkan.h */, A7D8A62523E2513D00DCD162 /* SDL_uikitvulkan.m */, A7D8A62723E2513D00DCD162 /* SDL_uikitwindow.h */, A7D8A61A23E2513D00DCD162 /* SDL_uikitwindow.m */, - 000063D3D80F97ADC7770000 /* SDL_uikitpen.h */, - 000053D344416737F6050000 /* SDL_uikitpen.m */, ); path = uikit; sourceTree = ""; @@ -2365,17 +2387,6 @@ path = vulkan; sourceTree = ""; }; - 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 = ""; - }; E4F257872C81903800FCEAFC /* gpu */ = { isa = PBXGroup; children = ( @@ -2388,6 +2399,17 @@ path = gpu; sourceTree = ""; }; + 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 = ""; + }; F338A1142D1B3735007CDFDF /* tray */ = { isa = PBXGroup; children = ( @@ -2561,6 +2583,8 @@ F3EFA5ED2D5AB97300BCF22F /* SDL_stb_c.h in Headers */, F3EFA5EE2D5AB97300BCF22F /* stb_image.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 */, A7D8AF0623E2514100DCD162 /* SDL_cocoamessagebox.h in Headers */, A7D8AEB223E2514100DCD162 /* SDL_cocoametalview.h in Headers */, @@ -2843,6 +2867,9 @@ attributes = { LastUpgradeCheck = 1130; TargetAttributes = { + BECDF5FE0761BA81005FE872 = { + LastSwiftMigration = 2630; + }; F3676F582A7885080091160D = { CreatedOnToolsVersion = 14.3.1; }; @@ -2931,6 +2958,7 @@ buildActionMask = 2147483647; files = ( A7D8B9E323E2514400DCD162 /* SDL_drawline.c in Sources */, + F3A8957B2F7DC14400B9E5C2 /* SDL_UIKitBridge.m in Sources */, A7D8AE7C23E2514100DCD162 /* SDL_yuv.c in Sources */, A7D8B62F23E2514300DCD162 /* SDL_sysfilesystem.m in Sources */, A7D8B41C23E2514300DCD162 /* SDL_systls.c in Sources */, @@ -3059,6 +3087,7 @@ F3B439512C935C2400792030 /* SDL_dummyprocess.c in Sources */, A7D8B76423E2514300DCD162 /* SDL_mixer.c in Sources */, A7D8BB5723E2514500DCD162 /* SDL_events.c in Sources */, + F3A8957D2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift in Sources */, A7D8ADE623E2514100DCD162 /* SDL_blit_0.c in Sources */, 89E5801E2D03602200DAF6D3 /* SDL_hidapi_lg4ff.c in Sources */, A7D8B8A823E2514400DCD162 /* SDL_diskaudio.c in Sources */, @@ -3086,6 +3115,7 @@ A7D8B56323E2514300DCD162 /* SDL_hidapi_gamecube.c in Sources */, A7D8B4DC23E2514300DCD162 /* SDL_joystick.c in Sources */, A7D8BA4923E2514400DCD162 /* SDL_render_gles2.c in Sources */, + F3A8371C2F69C80100AD32B6 /* SDL_RealityKitHelper.swift in Sources */, A7D8AC2D23E2514100DCD162 /* SDL_surface.c in Sources */, A7D8B54B23E2514300DCD162 /* SDL_hidapi_xboxone.c in Sources */, A7D8AD2323E2514100DCD162 /* SDL_blit_auto.c in Sources */, @@ -3141,6 +3171,8 @@ A7D8A94B23E2514000DCD162 /* SDL.c in Sources */, A7D8AEA023E2514100DCD162 /* SDL_cocoavulkan.m 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 */, 63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */, 000040E76FDC6AE48CBF0000 /* SDL_hashtable.c in Sources */, @@ -3153,6 +3185,7 @@ 00002B20A48E055EB0350000 /* SDL_camera_coremedia.m in Sources */, 000080903BC03006F24E0000 /* SDL_filesystem.c in Sources */, F3FBB1082DDF93AB0000F99F /* SDL_hidapi_flydigi.c in Sources */, + 3AFD09EA2F9766BA00208BA9 /* SDL_CurvedUIShader.swift in Sources */, F3FBB10A2DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c in Sources */, 0000481D255AF155B42C0000 /* SDL_sysfsops.c in Sources */, 0000494CC93F3E624D3C0000 /* SDL_systime.c in Sources */, @@ -3239,13 +3272,18 @@ isa = XCBuildConfiguration; baseConfigurationReference = F3F7BE3B2CBD79D200C984AF /* config.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_LINK_OBJC_RUNTIME = NO; - GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; EXPORTED_SYMBOLS_FILE = "$(SRCROOT)/../../src/dynapi/SDL_dynapi.exports"; + GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION; OTHER_LDFLAGS = "-liconv"; 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; }; @@ -3305,13 +3343,19 @@ isa = XCBuildConfiguration; baseConfigurationReference = F3F7BE3B2CBD79D200C984AF /* config.xcconfig */; buildSettings = { + CLANG_ENABLE_MODULES = YES; CLANG_LINK_OBJC_RUNTIME = NO; - GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; EXPORTED_SYMBOLS_FILE = "$(SRCROOT)/../../src/dynapi/SDL_dynapi.exports"; + GCC_PREPROCESSOR_DEFINITIONS = GLES_SILENCE_DEPRECATION; OTHER_LDFLAGS = "-liconv"; 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; }; diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index fa923b0d1e..5b60e3df9b 100644 --- a/include/SDL3/SDL_events.h +++ b/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 it are invalid */ 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_LAST = SDL_EVENT_WINDOW_HDR_STATE_CHANGED, + SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_CURVATURE_CHANGED, /* Keyboard events */ SDL_EVENT_KEY_DOWN = 0x300, /**< Key pressed */ diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index 6117eceaf9..57fa31f001 100644 --- a/include/SDL3/SDL_video.h +++ b/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 * 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 * not add a graphics API specific property * (`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_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_CURVATURE_FLOAT "SDL.window.create.curvature" /** * 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 * 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. * \returns a valid property ID on success or 0 on failure; call * 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_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_CURVATURE_FLOAT "SDL.window.curvature" /** * Get the window flags. diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index c909a2a583..315a8b6d90 100644 --- a/src/events/SDL_events.c +++ b/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_DESTROYED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_HDR_STATE_CHANGED); + SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_CURVATURE_CHANGED); #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) diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c index 5f80fae6d6..9894d44678 100644 --- a/src/render/SDL_render.c +++ b/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) { +#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); +#endif } bool SDL_RenderingLinearSpace(SDL_Renderer *renderer) diff --git a/src/render/metal/SDL_render_metal.m b/src/render/metal/SDL_render_metal.m index d5187b6113..66b1331c07 100644 --- a/src/render/metal/SDL_render_metal.m +++ b/src/render/metal/SDL_render_metal.m @@ -35,6 +35,9 @@ #endif #ifdef SDL_VIDEO_DRIVER_UIKIT #import +#ifdef SDL_PLATFORM_VISIONOS +#import "../../video/uikit/SDL_UIKitBridge-objc.h" +#endif #endif // 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 *allpipelines; @property(nonatomic, assign) int pipelinescount; +#ifdef SDL_PLATFORM_VISIONOS +@property(nonatomic, retain) id mtlrealitykittexture; +#endif @end @implementation SDL3METAL_RenderData @@ -453,16 +459,25 @@ static bool METAL_ActivateRenderCommandEncoder(SDL_Renderer *renderer, MTLLoadAc SDL3METAL_TextureData *texdata = (__bridge SDL3METAL_TextureData *)renderer->target->internal; mtltexture = texdata.mtltexture; } 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 METAL_CopyToStagingTexture(SDL_Renderer *renderer, id texture, SDL_Rect *rect) +{ + SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal; + MTLTextureDescriptor *desc; + id stagingtex; + id 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) { @autoreleasepool { SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal; id mtltexture; MTLRegion mtlregion; + SDL_Rect read_rect = *rect; Uint32 format; SDL_Surface *surface; @@ -1951,6 +2011,15 @@ static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rec } #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 * sure the GPU has finished rendering to it by the time we read it. */ [data.mtlcmdbuffer commit]; @@ -1958,7 +2027,7 @@ static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rec data.mtlcmdencoder = 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) { case MTLPixelFormatBGRA8Unorm: @@ -1991,9 +2060,16 @@ static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rec SDL_SetError("Unknown framebuffer pixel format"); return NULL; } - surface = SDL_CreateSurface(rect->w, rect->h, format); + surface = SDL_CreateSurface(read_rect.w, read_rect.h, format); if (surface) { [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; } @@ -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. // But we'll still try to commit the command buffer in case it was already enqueued. if (ready) { - SDL_assert(data.mtlbackbuffer != nil); - [data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer]; +#ifdef SDL_PLATFORM_VISIONOS + if (data.mtlrealitykittexture) { + // Generate mipmaps + id 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]; @@ -2057,6 +2147,11 @@ static void METAL_DestroyRenderer(SDL_Renderer *renderer) [data.mtlcmdencoder endEncoding]; } + if (data.mtlcmdbuffer != nil) { + [data.mtlcmdbuffer commit]; + [data.mtlcmdbuffer waitUntilCompleted]; + } + DestroyAllPipelines(data.allpipelines, data.pipelinescount); /* Release the metal view instead of destroying it, diff --git a/src/video/uikit/SDL_CurvedContentHosting.swift b/src/video/uikit/SDL_CurvedContentHosting.swift new file mode 100644 index 0000000000..d88b43680e --- /dev/null +++ b/src/video/uikit/SDL_CurvedContentHosting.swift @@ -0,0 +1,410 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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: + + + + */ +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: + + + + */ +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: + + + + */ +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: UIHostingController { + 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? + + @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() + } +} diff --git a/src/video/uikit/SDL_CurvedContentView.swift b/src/video/uikit/SDL_CurvedContentView.swift new file mode 100644 index 0000000000..36317e71db --- /dev/null +++ b/src/video/uikit/SDL_CurvedContentView.swift @@ -0,0 +1,350 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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(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: 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(_ 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) + } + } +} diff --git a/src/video/uikit/SDL_CurvedUIShader.swift b/src/video/uikit/SDL_CurvedUIShader.swift new file mode 100644 index 0000000000..a25ebbae66 --- /dev/null +++ b/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 = + token outputs:realitykit:vertex + token outputs:surface.connect = + 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 = + 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 = + bool inputs:no_flip_v = 1 + int2 inputs:offset + float2 inputs:texcoord.connect = + 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 = + color3f inputs:in2.connect = + bool inputs:value1.connect = + 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 = + float inputs:in2.connect = + float inputs:value1.connect = + float inputs:value2.connect = + 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 = + 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 = + float3 inputs:B ( + customData = { + dictionary realitykit = { + float2 positionInSubgraph = (79.234375, 270.22266) + int stackingOrderInSubgraph = 1408 + } + } + ) + float3 inputs:B.connect = + float inputs:Radius ( + customData = { + dictionary realitykit = { + float2 positionInSubgraph = (306.85156, 333.83984) + int stackingOrderInSubgraph = 1406 + } + } + ) + float inputs:Radius.connect = + float outputs:ZeroToOneDistance ( + customData = { + dictionary realitykit = { + float2 positionInSubgraph = (444.625, 223) + int stackingOrderInSubgraph = 1409 + } + } + ) + float outputs:ZeroToOneDistance.connect = + 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 = + float inputs:inhigh.connect = + 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 = + float3 inputs:y.connect = + 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 = + 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 = + 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 = + float inputs:value1.connect = + 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 = + color3f inputs:fg.connect = + float inputs:mix.connect = + 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 = + float3 inputs:in2 = (999999, 999999, 999999) + bool inputs:value1.connect = + 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 = + bool inputs:in2.connect = + 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(_ 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.Type, .simd2Float(let v)): return (v as! T) + case (is SIMD3.Type, .simd3Float(let v)): return (v as! T) + case (is SIMD4.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(_ 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.Type: + try self.setParameter(handle: handle, value: .simd2Float(value as! SIMD2)) + case is SIMD3.Type: + try self.setParameter(handle: handle, value: .simd3Float(value as! SIMD3)) + case is SIMD4.Type: + try self.setParameter(handle: handle, value: .simd4Float(value as! SIMD4)) + 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))") + } + } +} diff --git a/src/video/uikit/SDL_RealityKitHelper.swift b/src/video/uikit/SDL_RealityKitHelper.swift new file mode 100644 index 0000000000..424ed68bea --- /dev/null +++ b/src/video/uikit/SDL_RealityKitHelper.swift @@ -0,0 +1,396 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 = .zero + var normal: SIMD3 = .zero + var uv: SIMD2 = .zero + + static var vertexAttributes: [LowLevelMesh.Attribute] { + [ + .init(semantic: .position, format: .float3, offset: MemoryLayout.offset(of: \.position)!), + .init(semantic: .normal, format: .float3, offset: MemoryLayout.offset(of: \.normal)!), + .init(semantic: .uv0, format: .float2, offset: MemoryLayout.offset(of: \.uv)!) + ] + } + + static var vertexLayouts: [LowLevelMesh.Layout] { + [.init(bufferIndex: 0, bufferStride: MemoryLayout.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? + + /// ``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) -> SIMD2 { + 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] = [] + var curve_normals: [SIMD3] = [] + 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 = simd_normalize([sin(angle), 0.0, cos(angle)]) + let pos: SIMD3 = [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(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((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(posX, posY, 0) + vertices[idx].position = position + vertices[idx].normal = SIMD3(0, 0, -1) + vertices[idx].uv = SIMD2(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.. + + 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 +#import + +/** + * 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 SDL_UIKit_GetCurvedDisplayTexture(SDL_Window *window, id commandBuffer, int width, int height, MTLPixelFormat pixelFormat); + +#endif /* SDL_uikitvisionosscene_h_ */ diff --git a/src/video/uikit/SDL_UIKitBridge-swift.h b/src/video/uikit/SDL_UIKitBridge-swift.h new file mode 100644 index 0000000000..e63dc6c42b --- /dev/null +++ b/src/video/uikit/SDL_UIKitBridge-swift.h @@ -0,0 +1,39 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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); diff --git a/src/video/uikit/SDL_UIKitBridge.m b/src/video/uikit/SDL_UIKitBridge.m new file mode 100644 index 0000000000..b5c552f51a --- /dev/null +++ b/src/video/uikit/SDL_UIKitBridge.m @@ -0,0 +1,187 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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 SDL_UIKit_GetCurvedDisplayTexture(SDL_Window *window, id 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 texture = temp; + return texture; +} + +#endif /* SDL_PLATFORM_VISIONOS */ diff --git a/src/video/uikit/SDL_uikitevents.m b/src/video/uikit/SDL_uikitevents.m index 4f9de24af2..a2722f0c01 100644 --- a/src/video/uikit/SDL_uikitevents.m +++ b/src/video/uikit/SDL_uikitevents.m @@ -29,6 +29,7 @@ #include "SDL_uikitopengles.h" #include "SDL_uikitvideo.h" #include "SDL_uikitwindow.h" +#include "SDL_UIKitBridge-objc.h" #import #import @@ -308,6 +309,12 @@ static bool SetGCMouseRelativeMode(bool enabled) static void OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button, BOOL pressed) { 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); } @@ -318,19 +325,19 @@ static void OnGCMouseConnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14 SDL_AddMouse(mouseID, NULL); 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) { - OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed); + OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, 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; for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) { btn.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { - OnGCMouseButtonChanged(mouseID, auxiliary_button, pressed); + OnGCMouseButtonChanged(mouseID, auxiliary_button, pressed); }; ++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) { 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); } }; mouse.mouseInput.scroll.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { Uint64 timestamp = SDL_GetTicksNS(); - + /* 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 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. */ +#ifdef SDL_PLATFORM_VISIONOS + float vertical = -yValue; + float horizontal = xValue; +#else float vertical = -xValue; float horizontal = yValue; +#endif if (mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) { // Since these are raw values, we need to flip them ourselves diff --git a/src/video/uikit/SDL_uikitmetalview.h b/src/video/uikit/SDL_uikitmetalview.h index 5a2523e626..4890731c08 100644 --- a/src/video/uikit/SDL_uikitmetalview.h +++ b/src/video/uikit/SDL_uikitmetalview.h @@ -1,24 +1,23 @@ /* - Simple DirectMedia Layer - Copyright (C) 1997-2026 Sam Lantinga + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga - 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. + 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. - */ + 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. * @@ -43,6 +42,8 @@ - (instancetype)initWithFrame:(CGRect)frame scale:(CGFloat)scale; +- (void)updateDrawableSize; + @end SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window); diff --git a/src/video/uikit/SDL_uikitmetalview.m b/src/video/uikit/SDL_uikitmetalview.m index 596b311165..2e3d438553 100644 --- a/src/video/uikit/SDL_uikitmetalview.m +++ b/src/video/uikit/SDL_uikitmetalview.m @@ -1,24 +1,23 @@ /* - Simple DirectMedia Layer - Copyright (C) 1997-2026 Sam Lantinga + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga - 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. + 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. - */ + 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. * diff --git a/src/video/uikit/SDL_uikitview.m b/src/video/uikit/SDL_uikitview.m index d05a14a1c3..615636f594 100644 --- a/src/video/uikit/SDL_uikitview.m +++ b/src/video/uikit/SDL_uikitview.m @@ -1,22 +1,22 @@ /* - Simple DirectMedia Layer - Copyright (C) 1997-2026 Sam Lantinga + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga - 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. + 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: + 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. + 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" diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h index a758de7ff5..1c1e415047 100644 --- a/src/video/uikit/SDL_uikitviewcontroller.h +++ b/src/video/uikit/SDL_uikitviewcontroller.h @@ -59,6 +59,10 @@ - (void)loadView; - (void)viewDidLayoutSubviews; +#ifdef SDL_PLATFORM_VISIONOS +- (void)initializeVisionOSCurvedUI; +#endif + #ifndef SDL_PLATFORM_TVOS - (NSUInteger)supportedInterfaceOrientations; - (BOOL)prefersStatusBarHidden; diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m index e9b48c2e01..dc776741d9 100644 --- a/src/video/uikit/SDL_uikitviewcontroller.m +++ b/src/video/uikit/SDL_uikitviewcontroller.m @@ -33,6 +33,10 @@ #include "SDL_uikitwindow.h" #include "SDL_uikitopengles.h" +#ifdef SDL_PLATFORM_VISIONOS +#import "SDL3/SDL3-Swift.h" +#endif + #ifdef SDL_PLATFORM_TVOS 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; } @@ -141,6 +154,19 @@ static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char #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 { SDL_SetSystemTheme(UIKit_GetSystemTheme()); diff --git a/src/video/uikit/SDL_uikitviewcontroller.swift b/src/video/uikit/SDL_uikitviewcontroller.swift new file mode 100644 index 0000000000..a47519ed28 --- /dev/null +++ b/src/video/uikit/SDL_uikitviewcontroller.swift @@ -0,0 +1,33 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + 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) + } + } +} diff --git a/src/video/uikit/SDL_uikitwindow.h b/src/video/uikit/SDL_uikitwindow.h index 79b969c051..b9077ad36d 100644 --- a/src/video/uikit/SDL_uikitwindow.h +++ b/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. @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 #endif // SDL_uikitwindow_h_ diff --git a/src/video/uikit/SDL_uikitwindow.m b/src/video/uikit/SDL_uikitwindow.m index 3f1f1b464d..3c3b4fd926 100644 --- a/src/video/uikit/SDL_uikitwindow.m +++ b/src/video/uikit/SDL_uikitwindow.m @@ -53,7 +53,7 @@ @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_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal; @@ -106,6 +106,19 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow #endif window->w = width; 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 * orientation changes. This will trigger resize events, if appropriate. */ @@ -119,10 +132,6 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow * hierarchy. */ [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; } @@ -228,7 +237,7 @@ bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti } #endif - if (!SetupWindowData(_this, window, uiwindow, true)) { + if (!SetupWindowData(_this, window, uiwindow, create_props, true)) { return false; } }