Skip to content

Commit 9c1a465

Browse files
Add Tutorial30_HelloVisionOS
1 parent 7e86608 commit 9c1a465

20 files changed

Lines changed: 1715 additions & 6 deletions

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)

Tutorials/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,4 @@ if(PLATFORM_WIN32 AND DILIGENT_USE_OPENXR)
5656
add_subdirectory(Tutorial28_HelloOpenXR)
5757
endif()
5858
add_subdirectory(Tutorial29_OIT)
59+
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
cmake_minimum_required (VERSION 3.21)
2+
3+
project(Tutorial30_HelloVisionOS CXX Swift)
4+
5+
set(TUTORIAL_SOURCE
6+
src/Tutorial30_Immersive.mm
7+
src/Tutorial30_Preview.mm
8+
src/Tutorial30_Scene.cpp
9+
)
10+
11+
set(TUTORIAL_INCLUDE
12+
src/Tutorial30_Immersive.hpp
13+
src/Tutorial30_Preview.hpp
14+
src/Tutorial30_Scene.hpp
15+
)
16+
17+
set(VISIONOS_SWIFT_SOURCE
18+
src/visionOS/VisionOSApp.swift
19+
src/visionOS/ImmersiveState.swift
20+
src/visionOS/LauncherView.swift
21+
src/visionOS/PreviewView.swift
22+
)
23+
24+
set(VISIONOS_BRIDGE_SOURCE
25+
src/visionOS/VisionOSAppBridge.mm
26+
)
27+
28+
set(VISIONOS_BRIDGE_INCLUDE
29+
src/visionOS/VisionOSAppBridge.h
30+
)
31+
32+
set(SHADERS
33+
assets/cube.vsh
34+
assets/cube.psh
35+
assets/shader_constants.fxh
36+
)
37+
38+
set(INFO_PLIST
39+
${CMAKE_CURRENT_SOURCE_DIR}/src/visionOS/Info.plist
40+
)
41+
42+
set_source_files_properties(${SHADERS} PROPERTIES
43+
VS_TOOL_OVERRIDE "None"
44+
MACOSX_PACKAGE_LOCATION "Resources"
45+
)
46+
47+
find_library(METAL_FRAMEWORK Metal)
48+
find_library(QUARTZCORE_FRAMEWORK QuartzCore)
49+
find_library(COMPOSITORSERVICES_FWK CompositorServices)
50+
find_library(UIKIT_FRAMEWORK UIKit)
51+
if(NOT METAL_FRAMEWORK OR NOT QUARTZCORE_FRAMEWORK OR NOT COMPOSITORSERVICES_FWK OR NOT UIKIT_FRAMEWORK)
52+
message(FATAL_ERROR "Metal/QuartzCore/CompositorServices/UIKit frameworks not found")
53+
endif()
54+
55+
add_library(Tutorial30_HelloVisionOS_Native STATIC
56+
${TUTORIAL_SOURCE}
57+
${TUTORIAL_INCLUDE}
58+
${VISIONOS_BRIDGE_SOURCE}
59+
${VISIONOS_BRIDGE_INCLUDE}
60+
)
61+
62+
target_include_directories(Tutorial30_HelloVisionOS_Native
63+
PUBLIC src/visionOS # Swift bridging header path
64+
PRIVATE src
65+
)
66+
67+
target_link_libraries(Tutorial30_HelloVisionOS_Native
68+
PRIVATE
69+
Diligent-BuildSettings
70+
Diligent-Common
71+
Diligent-GraphicsTools
72+
Diligent-GraphicsAccessories
73+
Diligent-GraphicsEngineMetal-static
74+
${METAL_FRAMEWORK}
75+
${QUARTZCORE_FRAMEWORK}
76+
${COMPOSITORSERVICES_FWK}
77+
${UIKIT_FRAMEWORK}
78+
)
79+
set_common_target_properties(Tutorial30_HelloVisionOS_Native)
80+
set_target_properties(Tutorial30_HelloVisionOS_Native PROPERTIES
81+
FOLDER "DiligentSamples/Tutorials/Tutorial30_HelloVisionOS"
82+
)
83+
84+
add_executable(Tutorial30_HelloVisionOS MACOSX_BUNDLE
85+
${VISIONOS_SWIFT_SOURCE}
86+
${SHADERS}
87+
)
88+
89+
set_target_properties(Tutorial30_HelloVisionOS PROPERTIES
90+
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.diligentengine.samples.Tutorial30-HelloVisionOS"
91+
MACOSX_BUNDLE_INFO_PLIST "${INFO_PLIST}"
92+
BUILD_RPATH "@executable_path"
93+
FOLDER "DiligentSamples/Tutorials"
94+
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
95+
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER
96+
"${CMAKE_CURRENT_SOURCE_DIR}/src/visionOS/VisionOSAppBridge.h"
97+
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
98+
)
99+
100+
target_link_libraries(Tutorial30_HelloVisionOS PRIVATE
101+
Tutorial30_HelloVisionOS_Native
102+
)
103+
104+
source_group("src" FILES ${TUTORIAL_SOURCE} ${TUTORIAL_INCLUDE})
105+
source_group("visionOS" FILES ${VISIONOS_SWIFT_SOURCE} ${VISIONOS_BRIDGE_SOURCE} ${VISIONOS_BRIDGE_INCLUDE})
106+
source_group("assets" FILES ${SHADERS})
107+
108+
target_sources(Tutorial30_HelloVisionOS PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/readme.md")
109+
set_source_files_properties(
110+
"${CMAKE_CURRENT_SOURCE_DIR}/readme.md" PROPERTIES HEADER_FILE_ONLY TRUE
111+
)
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: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
8+
## Prerequisites
9+
10+
- Xcode 16+ with the visionOS SDK (`xrsimulator` / `xros`).
11+
- CMake 3.28+ (required for the `visionOS` system name).
12+
- Apple Vision Pro device or the visionOS simulator.
13+
14+
Only the Metal backend is supported on visionOS. This tutorial is self-contained and does not use
15+
`SampleBase` / `NativeAppBase`, because neither fits the SwiftUI + CompositorServices entry point
16+
that visionOS requires.
17+
18+
19+
## Building
20+
21+
Configure and build for the simulator:
22+
23+
```bash
24+
cmake -S . -B build/visionOS -G Xcode \
25+
-DCMAKE_SYSTEM_NAME=visionOS \
26+
-DCMAKE_OSX_SYSROOT=xrsimulator \
27+
-DCMAKE_BUILD_TYPE=Debug
28+
cmake --build build/visionOS --config Debug --target Tutorial30_HelloVisionOS \
29+
-- CODE_SIGN_IDENTITY="" CODE_SIGNING_ALLOWED=NO
30+
```
31+
32+
For a device build, use `-DCMAKE_OSX_SYSROOT=xros` and supply a valid code signing identity.
33+
34+
35+
## Application Structure
36+
37+
On visionOS the application entry point is a SwiftUI `App`. Because Swift can't call C++ directly,
38+
the tutorial is split into three layers:
39+
40+
- **Swift / SwiftUI** (`src/visionOS/`) — declares the launcher window and the `ImmersiveSpace`,
41+
owns a `UIView` with a `CAMetalLayer` for preview and hands layers off to the C++ side.
42+
- **Objective-C++ bridge** (`src/visionOS/VisionOSAppBridge.{h,mm}`) — owns the C++ renderer
43+
objects and exposes them to Swift.
44+
- **C++ renderers** (`src/Tutorial30_{Preview,Immersive,Scene}.{hpp,cpp,mm}`) — Diligent Engine
45+
code. `Tutorial30_Scene` is shared between the two renderers.
46+
47+
Each renderer creates its own `IRenderDevice` and `IDeviceContext`; the only thing they share is
48+
the `Tutorial30_Scene` C++ class.
49+
50+
51+
## Diligent Engine Integration
52+
53+
### Preview renderer
54+
55+
`Tutorial30_Preview` renders into a plain `CAMetalLayer`-backed `UIView`. This is the standard
56+
Metal swap chain path — identical to any iOS/macOS Diligent application — so it serves as a
57+
familiar starting point before the immersive renderer:
58+
59+
```cpp
60+
IEngineFactoryMtl* pFactoryMtl = GetEngineFactoryMtl();
61+
pFactoryMtl->CreateDeviceAndContextsMtl(EngineCI, &m_pDevice, &m_pImmediateContext);
62+
63+
SwapChainDesc SCDesc;
64+
SCDesc.ColorBufferFormat = TEX_FORMAT_BGRA8_UNORM_SRGB;
65+
SCDesc.DepthBufferFormat = TEX_FORMAT_D32_FLOAT;
66+
SCDesc.Width = WidthPx;
67+
SCDesc.Height = HeightPx;
68+
69+
NativeWindow Window{pCAMetalLayer};
70+
pFactoryMtl->CreateSwapChainMtl(m_pDevice, m_pImmediateContext, SCDesc, Window, &m_pSwapChain);
71+
```
72+
73+
Rendering is driven by a `CADisplayLink` on the main thread. The preview uses a simple orbit
74+
camera with right-handed view and projection matrices (see below).
75+
76+
77+
### Immersive renderer
78+
79+
`Tutorial30_Immersive` does **not** create a swap chain. CompositorServices owns the textures and
80+
hands them out per frame, so the renderer only needs a Diligent device and context:
81+
82+
```cpp
83+
IEngineFactoryMtl* pFactoryMtl = GetEngineFactoryMtl();
84+
pFactoryMtl->CreateDeviceAndContextsMtl(EngineCI, &m_pDevice, &m_pImmediateContext);
85+
86+
m_Scene = std::make_unique<Tutorial30_Scene>(m_pDevice);
87+
m_WorldTracker = CreateCompositorServicesWorldTracker();
88+
```
89+
90+
All of the CompositorServices frame pacing (timing query, world anchor update, pose prediction,
91+
drawable acquisition and present) is encapsulated in `CompositorServicesUtilities.h` in
92+
`Diligent-GraphicsTools`. The tutorial only provides two callbacks:
93+
94+
```cpp
95+
void Tutorial30_Immersive::RenderNewFrame()
96+
{
97+
auto UpdateCb = MakeCallback([this]() {
98+
m_Scene->Update();
99+
});
100+
101+
auto RenderDrawableCb = MakeCallback([this](cp_drawable_t Drawable) {
102+
RenderDrawable(Drawable);
103+
});
104+
105+
RenderCompositorServicesFrame(m_LayerRenderer, m_WorldTracker,
106+
UpdateCb, UpdateCb,
107+
RenderDrawableCb, RenderDrawableCb);
108+
}
109+
```
110+
111+
Each drawable contains one or two views (one per eye). For each view the renderer obtains the
112+
color and depth textures from helpers in `CompositorServicesUtilities.h`, wraps them into
113+
Diligent texture objects, computes view/projection matrices and submits the scene:
114+
115+
```cpp
116+
const float4x4 ViewProj =
117+
GetCompositorServicesViewMatrix(m_WorldTracker, Drawable, ViewIdx) *
118+
GetCompositorServicesProjectionMatrix(Drawable, ViewIdx, NearZ, FarZ);
119+
120+
m_Scene->Render(m_pImmediateContext, pRTV, pDSV, ViewProj);
121+
PresentCompositorServicesDrawable(m_pImmediateContext, Drawable);
122+
m_pImmediateContext->Flush();
123+
m_pImmediateContext->FinishFrame();
124+
m_pDevice->ReleaseStaleResources();
125+
```
126+
127+
The immersive render loop runs on a dedicated serial `dispatch_queue` with `USER_INTERACTIVE`
128+
QoS, so it never blocks the main thread.
129+
130+
131+
### Shared scene
132+
133+
`Tutorial30_Scene` is format-agnostic. The PSO is created lazily on the first `Render()` call,
134+
using the RTV and DSV formats of the targets the scene is rendered to:
135+
136+
```cpp
137+
void Tutorial30_Scene::Render(IDeviceContext* pContext,
138+
ITextureView* pRTV,
139+
ITextureView* pDSV,
140+
const float4x4& ViewProj)
141+
{
142+
EnsurePipelineState(pRTV->GetTexture()->GetDesc().Format,
143+
pDSV->GetTexture()->GetDesc().Format);
144+
...
145+
}
146+
```
147+
148+
This lets the same scene object serve both the preview swap chain and the CompositorServices
149+
drawables without hard-coding any format constants.
150+
151+
152+
### Conventions: right-handed, reverse-Z
153+
154+
Both renderers use **right-handed view and projection matrices**:
155+
156+
- Immersive uses `cp_drawable_compute_projection(..., right_up_back, ...)` and a plain
157+
`simd_inverse` of the anchor-relative view transform.
158+
- Preview builds RH matrices explicitly in `Tutorial30_Preview.mm`.
159+
160+
Both also use **reverse-Z**: the depth target is cleared to `0`, the PSO uses
161+
`COMPARISON_FUNC_GREATER_EQUAL`, and the immersive drawable is configured with
162+
`cp_drawable_set_depth_range(FarZ, NearZ)`.
163+
164+
Because the RH convention flips NDC winding relative to the default D3D convention, the PSO sets
165+
`FrontCounterClockwise = True` with `CULL_MODE_BACK`. This matches the canonical VR pattern used
166+
by [Tutorial28_HelloOpenXR](../Tutorial28_HelloOpenXR/readme.md).
167+
168+
169+
## Notes
170+
171+
- SwiftUI is the only supported entry point on visionOS; a UIKit-only app works in the simulator
172+
but is rejected by the real device.
173+
- Assets in `assets/` are copied into the app bundle `Resources` directory by CMake, and
174+
`AppleFileSystem::OpenFile` resolves them via `CFBundleCopyResourceURL`, so no `chdir()` at
175+
startup is required.
176+
- The `SIGTERM` / `SIGKILL` seen in the debugger on window close are normal simulator shutdown
177+
signals, not a crash. Add `process handle SIGTERM --stop false --pass true --notify true` to
178+
your `~/.lldbinit` if you want lldb to ignore them.

0 commit comments

Comments
 (0)