Added curved window mode on visionOS 26 (#15298)

This commit is contained in:
Sam Lantinga
2026-05-12 16:48:06 -07:00
committed by GitHub
parent f30ec9940a
commit 5cf16e4522
22 changed files with 2328 additions and 99 deletions

View File

@@ -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 = "<group>"; };
1485C32F2BBA4A0C0063985B /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
3AFD09E92F9766BA00208BA9 /* SDL_CurvedUIShader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedUIShader.swift; sourceTree = "<group>"; };
5616CA49252BB2A5005D5928 /* SDL_url.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_url.c; sourceTree = "<group>"; };
5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_sysurl.h; sourceTree = "<group>"; };
5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDL_sysurl.m; sourceTree = "<group>"; };
@@ -970,7 +977,6 @@
F338A1192D1B37E4007CDFDF /* SDL_tray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_tray.c; sourceTree = "<group>"; };
F3395BA72D9A5971007246C8 /* SDL_hidapi_8bitdo.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_8bitdo.c; sourceTree = "<group>"; };
F3395BA72D9A5971007246C9 /* SDL_hidapi_flydigi.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_flydigi.c; sourceTree = "<group>"; };
F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_gamesir.c; sourceTree = "<group>"; };
F344003C2D4022E1003F26D7 /* INSTALL.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = INSTALL.md; sourceTree = "<group>"; };
F362B9152B3349E200D30B94 /* controller_list.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = controller_list.h; sourceTree = "<group>"; };
F362B9162B3349E200D30B94 /* SDL_gamepad_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gamepad_c.h; sourceTree = "<group>"; };
@@ -1026,6 +1032,13 @@
F3990E022A788303000D8759 /* SDL_hidapi_mac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_mac.h; sourceTree = "<group>"; };
F3990E032A788303000D8759 /* SDL_hidapi_ios.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_ios.h; sourceTree = "<group>"; };
F3A4909D2554D38500E92A8B /* SDL_hidapi_ps5.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_ps5.c; sourceTree = "<group>"; };
F3A837162F69C80100AD32B6 /* SDL_RealityKitHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_RealityKitHelper.swift; sourceTree = "<group>"; };
F3A8956D2F7D8AAA00B9E5C2 /* SDL_CurvedContentHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedContentHosting.swift; sourceTree = "<group>"; };
F3A8956E2F7D8AAA00B9E5C2 /* SDL_CurvedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_CurvedContentView.swift; sourceTree = "<group>"; };
F3A895762F7DC14400B9E5C2 /* SDL_UIKitBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDL_UIKitBridge.m; sourceTree = "<group>"; };
F3A895772F7DC14400B9E5C2 /* SDL_UIKitBridge-objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDL_UIKitBridge-objc.h"; sourceTree = "<group>"; };
F3A895782F7DC14400B9E5C2 /* SDL_UIKitBridge-swift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SDL_UIKitBridge-swift.h"; sourceTree = "<group>"; };
F3A8957C2F7DC15500B9E5C2 /* SDL_uikitviewcontroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDL_uikitviewcontroller.swift; sourceTree = "<group>"; };
F3A9AE922C8A13C100AAC390 /* SDL_gpu_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_gpu_util.h; sourceTree = "<group>"; };
F3A9AE932C8A13C100AAC390 /* SDL_render_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_render_gpu.c; sourceTree = "<group>"; };
F3A9AE942C8A13C100AAC390 /* SDL_shaders_gpu.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_shaders_gpu.c; sourceTree = "<group>"; };
@@ -1149,6 +1162,7 @@
F3FA5A1A2B59ACE000FEAD97 /* yuv_rgb_lsx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = yuv_rgb_lsx.c; sourceTree = "<group>"; };
F3FA5A1B2B59ACE000FEAD97 /* yuv_rgb_lsx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_lsx.h; sourceTree = "<group>"; };
F3FA5A1C2B59ACE000FEAD97 /* yuv_rgb_common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = yuv_rgb_common.h; sourceTree = "<group>"; };
F3FBB1092DDF93AB0000F9A0 /* SDL_hidapi_gamesir.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_gamesir.c; sourceTree = "<group>"; };
F3FD042C2C9B755700824C4C /* SDL_hidapi_nintendo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDL_hidapi_nintendo.h; sourceTree = "<group>"; };
F3FD042D2C9B755700824C4C /* SDL_hidapi_steam_hori.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_steam_hori.c; sourceTree = "<group>"; };
F59C710600D5CB5801000001 /* SDL.info */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = text; path = SDL.info; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -2365,17 +2387,6 @@
path = vulkan;
sourceTree = "<group>";
};
E4F2578B2C81903800FCEAFC /* xr */ = {
isa = PBXGroup;
children = (
E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */,
E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */,
E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */,
E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */,
);
path = xr;
sourceTree = "<group>";
};
E4F257872C81903800FCEAFC /* gpu */ = {
isa = PBXGroup;
children = (
@@ -2388,6 +2399,17 @@
path = gpu;
sourceTree = "<group>";
};
E4F2578B2C81903800FCEAFC /* xr */ = {
isa = PBXGroup;
children = (
E4F257882C81903800FCEAFC /* SDL_gpu_openxr.c */,
E4F257892C81903800FCEAFC /* SDL_openxrdyn.c */,
E4F2578A2C81903800FCEAFC /* SDL_gpu_openxr_c.h */,
E4F2578C2C81903800FCEAFC /* SDL_openxr_internal.h */,
);
path = xr;
sourceTree = "<group>";
};
F338A1142D1B3735007CDFDF /* tray */ = {
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;
};

View File

@@ -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 */

View File

@@ -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.

View File

@@ -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)

View File

@@ -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)

View File

@@ -35,6 +35,9 @@
#endif
#ifdef SDL_VIDEO_DRIVER_UIKIT
#import <UIKit/UIKit.h>
#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<MTLTexture> 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<MTLTexture> METAL_CopyToStagingTexture(SDL_Renderer *renderer, id<MTLTexture> texture, SDL_Rect *rect)
{
SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
MTLTextureDescriptor *desc;
id<MTLTexture> stagingtex;
id<MTLBlitCommandEncoder> blitcmd;
desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:texture.pixelFormat
width:rect->w
height:rect->h
mipmapped:NO];
if (desc == nil) {
SDL_OutOfMemory();
return nil;
}
stagingtex = [data.mtldevice newTextureWithDescriptor:desc];
if (stagingtex == nil) {
SDL_OutOfMemory();
return nil;
}
blitcmd = [data.mtlcmdbuffer blitCommandEncoder];
[blitcmd copyFromTexture:texture
sourceSlice:0
sourceLevel:0
sourceOrigin:MTLOriginMake(rect->x, rect->y, 0)
sourceSize:MTLSizeMake(rect->w, rect->h, 1)
toTexture:stagingtex
destinationSlice:0
destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blitcmd endEncoding];
rect->x = 0;
rect->y = 0;
return stagingtex;
}
#endif // SDL_PLATFORM_VISIONOS
static SDL_Surface *METAL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rect *rect)
{
@autoreleasepool {
SDL3METAL_RenderData *data = (__bridge SDL3METAL_RenderData *)renderer->internal;
id<MTLTexture> 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<MTLBlitCommandEncoder> blitcmd = [data.mtlcmdbuffer blitCommandEncoder];
[blitcmd generateMipmapsForTexture:data.mtlrealitykittexture];
[blitcmd endEncoding];
data.mtlrealitykittexture = nil;
}
else
#endif
{
SDL_assert(data.mtlbackbuffer != nil);
[data.mtlcmdbuffer presentDrawable:data.mtlbackbuffer];
}
}
[data.mtlcmdbuffer commit];
@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,7 @@
#include "SDL_uikitopengles.h"
#include "SDL_uikitvideo.h"
#include "SDL_uikitwindow.h"
#include "SDL_UIKitBridge-objc.h"
#import <Foundation/Foundation.h>
#import <GameController/GameController.h>
@@ -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

View File

@@ -1,24 +1,23 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
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);

View File

@@ -1,24 +1,23 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
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.
*

View File

@@ -1,22 +1,22 @@
/*
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
Simple DirectMedia Layer
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
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"

View File

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

View File

@@ -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());

View File

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

View File

@@ -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_

View File

@@ -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;
}
}