Skip to content

Commit 17a22c4

Browse files
Add Tutorial30_HelloVisionOS
1 parent 7e86608 commit 17a22c4

26 files changed

Lines changed: 1923 additions & 9 deletions

BuildTools/FormatValidation/validate_format_linux.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ source ../../../DiligentCore/BuildTools/FormatValidation/validate_format_linux_i
55
validate_format ../../SampleBase ../../Tutorials ../../Samples \
66
--exclude ../../SampleBase/src/UWP \
77
--exclude ../../SampleBase/src/Win32/resources \
8-
--exclude ../../Samples/Asteroids
8+
--exclude ../../Samples/Asteroids \
9+
--exclude ../../Tutorials/Tutorial30_HelloVisionOS/src/visionOS

BuildTools/FormatValidation/validate_format_mac.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ python3 ../../../DiligentCore/BuildTools/FormatValidation/clang-format-validate.
44
-r ../../SampleBase ../../Tutorials ../../Samples \
55
--exclude ../../SampleBase/src/UWP \
66
--exclude ../../SampleBase/src/Win32/resources \
7-
--exclude ../../Samples/Asteroids
7+
--exclude ../../Samples/Asteroids \
8+
--exclude ../../Tutorials/Tutorial30_HelloVisionOS/src/visionOS

BuildTools/FormatValidation/validate_format_win.bat

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ python ../../../DiligentCore/BuildTools/FormatValidation/clang-format-validate.p
44
--exclude ../../SampleBase/src/UWP ^
55
--exclude ../../SampleBase/src/Win32/resources ^
66
--exclude ../../Samples/Asteroids ^
7-
--exclude ../../Tutorials/Tutorial03_Texturing-CSharp
7+
--exclude ../../Tutorials/Tutorial03_Texturing-CSharp ^
8+
--exclude ../../Tutorials/Tutorial30_HelloVisionOS/src/visionOS

CMakeLists.txt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,17 @@ endfunction()
190190

191191
add_subdirectory(ThirdParty)
192192

193-
if(TARGET Diligent-NativeAppBase AND TARGET Diligent-TextureLoader AND TARGET Diligent-Imgui)
194-
add_subdirectory(SampleBase)
195-
endif()
193+
if(PLATFORM_VISIONOS)
194+
add_subdirectory(Tutorials/Tutorial30_HelloVisionOS)
195+
else()
196+
if(TARGET Diligent-NativeAppBase AND TARGET Diligent-TextureLoader AND TARGET Diligent-Imgui)
197+
add_subdirectory(SampleBase)
198+
endif()
196199

197-
if(NOT ${DILIGENT_BUILD_SAMPLE_BASE_ONLY} AND TARGET Diligent-SampleBase)
198-
add_subdirectory(Samples)
199-
add_subdirectory(Tutorials)
200+
if(NOT ${DILIGENT_BUILD_SAMPLE_BASE_ONLY} AND TARGET Diligent-SampleBase)
201+
add_subdirectory(Samples)
202+
add_subdirectory(Tutorials)
203+
endif()
200204
endif()
201205

202206
if(PLATFORM_ANDROID)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
cmake_minimum_required (VERSION 3.28)
2+
3+
project(Tutorial30_HelloVisionOS CXX Swift)
4+
5+
set(TUTORIAL_SOURCE
6+
src/Tutorial30_Immersive.cpp
7+
src/Tutorial30_Preview.cpp
8+
src/Tutorial30_RenderEngine.cpp
9+
src/Tutorial30_Scene.cpp
10+
)
11+
12+
set(TUTORIAL_INCLUDE
13+
src/Tutorial30_Immersive.hpp
14+
src/Tutorial30_Preview.hpp
15+
src/Tutorial30_RenderEngine.hpp
16+
src/Tutorial30_Scene.hpp
17+
)
18+
19+
set(VISIONOS_SWIFT_SOURCE
20+
src/visionOS/VisionOSApp.swift
21+
src/visionOS/ImmersiveState.swift
22+
src/visionOS/ImmersiveView.swift
23+
src/visionOS/LauncherView.swift
24+
src/visionOS/PreviewView.swift
25+
)
26+
27+
set(VISIONOS_BRIDGE_SOURCE
28+
src/visionOS/VisionOSAppBridge.mm
29+
)
30+
31+
set(VISIONOS_BRIDGE_INCLUDE
32+
src/visionOS/VisionOSAppBridge.h
33+
)
34+
35+
set(SHADERS
36+
assets/cube.vsh
37+
assets/cube.psh
38+
assets/shader_constants.fxh
39+
)
40+
41+
set(INFO_PLIST
42+
${CMAKE_CURRENT_SOURCE_DIR}/src/visionOS/Info.plist
43+
)
44+
45+
set_source_files_properties(${SHADERS} PROPERTIES
46+
VS_TOOL_OVERRIDE "None"
47+
MACOSX_PACKAGE_LOCATION "Resources"
48+
)
49+
50+
find_library(QUARTZCORE_FRAMEWORK QuartzCore)
51+
find_library(UIKIT_FRAMEWORK UIKit)
52+
if(NOT QUARTZCORE_FRAMEWORK OR NOT UIKIT_FRAMEWORK)
53+
message(FATAL_ERROR "QuartzCore/UIKit frameworks not found")
54+
endif()
55+
56+
add_library(Tutorial30_HelloVisionOS_Native STATIC
57+
${TUTORIAL_SOURCE}
58+
${TUTORIAL_INCLUDE}
59+
${VISIONOS_BRIDGE_SOURCE}
60+
${VISIONOS_BRIDGE_INCLUDE}
61+
)
62+
63+
target_include_directories(Tutorial30_HelloVisionOS_Native
64+
PUBLIC src/visionOS # Swift bridging header path
65+
PRIVATE src
66+
)
67+
68+
target_link_libraries(Tutorial30_HelloVisionOS_Native
69+
PRIVATE
70+
Diligent-BuildSettings
71+
Diligent-Common
72+
Diligent-GraphicsTools
73+
Diligent-GraphicsAccessories
74+
Diligent-GraphicsEngineMetal-static
75+
${QUARTZCORE_FRAMEWORK}
76+
${UIKIT_FRAMEWORK}
77+
)
78+
set_common_target_properties(Tutorial30_HelloVisionOS_Native)
79+
set_target_properties(Tutorial30_HelloVisionOS_Native PROPERTIES
80+
FOLDER "DiligentSamples/Tutorials/Tutorial30_HelloVisionOS"
81+
)
82+
83+
add_executable(Tutorial30_HelloVisionOS MACOSX_BUNDLE
84+
${VISIONOS_SWIFT_SOURCE}
85+
${SHADERS}
86+
)
87+
88+
set_target_properties(Tutorial30_HelloVisionOS PROPERTIES
89+
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.diligentengine.samples.Tutorial30-HelloVisionOS"
90+
MACOSX_BUNDLE_INFO_PLIST "${INFO_PLIST}"
91+
BUILD_RPATH "@executable_path"
92+
FOLDER "DiligentSamples/Tutorials"
93+
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
94+
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER
95+
"${CMAKE_CURRENT_SOURCE_DIR}/src/visionOS/VisionOSAppBridge.h"
96+
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
97+
)
98+
99+
target_link_libraries(Tutorial30_HelloVisionOS PRIVATE
100+
Tutorial30_HelloVisionOS_Native
101+
)
102+
103+
source_group("src" FILES ${TUTORIAL_SOURCE} ${TUTORIAL_INCLUDE})
104+
source_group("visionOS" FILES ${VISIONOS_SWIFT_SOURCE} ${VISIONOS_BRIDGE_SOURCE} ${VISIONOS_BRIDGE_INCLUDE})
105+
source_group("assets" FILES ${SHADERS})
106+
107+
target_sources(Tutorial30_HelloVisionOS PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/readme.md")
108+
set_source_files_properties(
109+
"${CMAKE_CURRENT_SOURCE_DIR}/readme.md" PROPERTIES HEADER_FILE_ONLY TRUE
110+
)
54.5 KB
Loading
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#include "shader_constants.fxh"
2+
3+
struct PSOutput
4+
{
5+
float4 Color : SV_TARGET;
6+
};
7+
8+
void main(in PSInput PSIn,
9+
out PSOutput PSOut)
10+
{
11+
float4 Color = PSIn.Color;
12+
float3 LightDir = normalize(float3(-0.5, -0.8, -0.2));
13+
Color *= max(dot(-LightDir, PSIn.Normal), 0.0) * 0.5 + 0.2;
14+
PSOut.Color = Color;
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#include "shader_constants.fxh"
2+
3+
cbuffer Constants
4+
{
5+
float4x4 g_WorldViewProj;
6+
float4x4 g_NormalTransform;
7+
float4 g_Color;
8+
};
9+
10+
struct VSInput
11+
{
12+
float3 Pos : ATTRIB0;
13+
float3 Normal : ATTRIB1;
14+
};
15+
16+
void main(in VSInput VSIn,
17+
out PSInput PSIn)
18+
{
19+
PSIn.Pos = mul(float4(VSIn.Pos, 1.0), g_WorldViewProj);
20+
PSIn.Normal = mul(float4(VSIn.Normal, 0.0), g_NormalTransform);
21+
PSIn.Color = g_Color;
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
struct PSInput
2+
{
3+
float4 Pos : SV_POSITION;
4+
float3 Normal : NORMAL;
5+
float4 Color : COLOR;
6+
};
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Tutorial30 - Hello visionOS
2+
3+
This tutorial demonstrates how to run Diligent Engine on Apple visionOS. A single app hosts two
4+
renderers that share the same scene: a 2D **Preview** window and a fully immersive space driven by
5+
Apple **CompositorServices**.
6+
7+
![](Screenshot.jpg)
8+
9+
## Prerequisites
10+
11+
- Xcode 16+ with the visionOS SDK (`xrsimulator` / `xros`).
12+
- CMake 3.28+ (required for the `visionOS` system name).
13+
- Apple Vision Pro device or the visionOS simulator.
14+
15+
Only the Metal backend is supported on visionOS. This tutorial is self-contained and does not use
16+
`SampleBase` / `NativeAppBase`, because neither fits the SwiftUI + CompositorServices entry point
17+
that visionOS requires.
18+
19+
20+
## Building
21+
22+
Configure and build for the simulator:
23+
24+
```bash
25+
cmake -S . -B build/visionOS -G Xcode \
26+
-DCMAKE_SYSTEM_NAME=visionOS \
27+
-DCMAKE_OSX_SYSROOT=xrsimulator \
28+
-DCMAKE_BUILD_TYPE=Debug
29+
cmake --build build/visionOS --config Debug --target Tutorial30_HelloVisionOS \
30+
-- CODE_SIGN_IDENTITY="" CODE_SIGNING_ALLOWED=NO
31+
```
32+
33+
For a device build, use `-DCMAKE_OSX_SYSROOT=xros` and supply a valid code signing identity.
34+
35+
36+
## Application Structure
37+
38+
On visionOS the application entry point is a SwiftUI `App`. Because Swift can't call C++ directly,
39+
the tutorial is split into three layers:
40+
41+
- **Swift / SwiftUI** (`src/visionOS/`) — declares the launcher window and the `ImmersiveSpace`,
42+
owns a `UIView` with a `CAMetalLayer` for preview and hands layers off to the C++ side.
43+
- **Objective-C++ bridge** (`src/visionOS/VisionOSAppBridge.{h,mm}`) — owns the C++ preview /
44+
immersive driver objects and exposes them to Swift.
45+
- **C++ renderers** (`src/Tutorial30_{RenderEngine,Preview,Immersive,Scene}.{hpp,cpp}`) —
46+
Diligent Engine code. `Tutorial30_RenderEngine` owns the process-wide Metal device and
47+
immediate context; `Tutorial30_Scene` is shared by the preview and immersive paths.
48+
49+
The preview is paused while an immersive space is opening or active, so the shared immediate
50+
context and scene are not rendered from both SwiftUI paths at the same time. As an additional
51+
safeguard, the Objective-C++ bridge submits all preview and immersive rendering work to one shared
52+
serial render queue.
53+
54+
55+
## Diligent Engine Integration
56+
57+
### Preview renderer
58+
59+
`Tutorial30_Preview` renders into a plain `CAMetalLayer`-backed `UIView`. This is the standard
60+
Metal swap chain path — identical to any iOS/macOS Diligent application — so it serves as a
61+
familiar starting point before the immersive renderer:
62+
63+
```cpp
64+
Tutorial30_RenderEngine& Engine = Tutorial30_RenderEngine::Get();
65+
IEngineFactoryMtl* pFactoryMtl = static_cast<IEngineFactoryMtl*>(Engine.GetEngineFactory());
66+
67+
SwapChainDesc SCDesc;
68+
SCDesc.ColorBufferFormat = TEX_FORMAT_BGRA8_UNORM_SRGB;
69+
SCDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT;
70+
SCDesc.Width = WidthPx;
71+
SCDesc.Height = HeightPx;
72+
73+
NativeWindow Window{pCAMetalLayer};
74+
pFactoryMtl->CreateSwapChainMtl(Engine.GetDevice(), Engine.GetImmediateContext(),
75+
SCDesc, Window, &m_pSwapChain);
76+
```
77+
78+
Rendering is scheduled by a `CADisplayLink` on the main thread and executed on the shared serial
79+
render queue. The preview uses a simple orbit camera with right-handed view and projection matrices
80+
(see below).
81+
82+
83+
### Immersive renderer
84+
85+
`Tutorial30_Immersive` does **not** create a swap chain. CompositorServices owns the textures and
86+
hands them out per frame, so the renderer only attaches a `CompositorServicesSession` to the shared
87+
Diligent device and context:
88+
89+
```cpp
90+
Tutorial30_RenderEngine& Engine = Tutorial30_RenderEngine::Get();
91+
92+
m_Session = std::make_unique<CompositorServicesSession>(LayerRenderer,
93+
Engine.GetDevice(),
94+
Engine.GetImmediateContext());
95+
```
96+
97+
All of the CompositorServices frame pacing (timing query, world anchor update, pose prediction,
98+
drawable acquisition and present) is encapsulated in `CompositorServicesSession` in
99+
`Diligent-GraphicsTools`. The tutorial only provides two callbacks:
100+
101+
```cpp
102+
void Tutorial30_Immersive::RenderFrame()
103+
{
104+
Tutorial30_Scene& Scene = Tutorial30_RenderEngine::Get().GetScene();
105+
106+
m_Session->RenderFrame(
107+
[&Scene] { Scene.Update(); },
108+
[this](void* pDrawable) { RenderDrawable(pDrawable); });
109+
}
110+
```
111+
112+
Each drawable contains one or two views (one per eye). For each view the renderer obtains the
113+
color and depth textures from `CompositorServicesSession`, computes view/projection matrices
114+
and submits the scene:
115+
116+
```cpp
117+
RefCntAutoPtr<ITexture> pColor = m_Session->GetColorSwapchainImage(pDrawable, ViewIdx);
118+
RefCntAutoPtr<ITexture> pDepth = m_Session->GetDepthSwapchainImage(pDrawable, ViewIdx);
119+
120+
const float4x4 ViewProj =
121+
m_Session->GetViewMatrix(pDrawable, ViewIdx) *
122+
m_Session->GetProjectionMatrix(pDrawable, ViewIdx, NearZ, FarZ);
123+
124+
Scene.Render(pImmediateContext,
125+
pColor->GetDefaultView(TEXTURE_VIEW_RENDER_TARGET),
126+
pDepth->GetDefaultView(TEXTURE_VIEW_DEPTH_STENCIL),
127+
ViewProj);
128+
129+
m_Session->PresentDrawable(pDrawable);
130+
pImmediateContext->Flush();
131+
pImmediateContext->FinishFrame();
132+
pDevice->ReleaseStaleResources();
133+
```
134+
135+
The immersive render loop runs on the shared serial render queue with `USER_INTERACTIVE` QoS, so it
136+
never blocks the main thread and cannot overlap the preview renderer.
137+
138+
139+
### Conventions: right-handed, reverse-Z
140+
141+
Both renderers use **right-handed view and projection matrices**:
142+
143+
- Immersive uses `cp_drawable_compute_projection(..., right_up_back, ...)` and a plain
144+
`simd_inverse` of the anchor-relative view transform.
145+
- Preview builds RH matrices explicitly in `Tutorial30_Preview.cpp`.
146+
147+
Both also use **reverse-Z**: the depth target is cleared to `0`, the PSO uses
148+
`COMPARISON_FUNC_GREATER_EQUAL`, and the immersive drawable is configured with
149+
`cp_drawable_set_depth_range(FarZ, NearZ)`.
150+
151+
Because the RH convention flips NDC winding relative to the default D3D convention, the PSO sets
152+
`FrontCounterClockwise = True` with `CULL_MODE_BACK`. This matches the canonical VR pattern used
153+
by [Tutorial28_HelloOpenXR](../Tutorial28_HelloOpenXR/readme.md).
154+

0 commit comments

Comments
 (0)