Skip to content

Commit aaf2ced

Browse files
Add Tutorial30_HelloVisionOS
1 parent 7e86608 commit aaf2ced

25 files changed

Lines changed: 1834 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: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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/LauncherView.swift
23+
src/visionOS/PreviewView.swift
24+
)
25+
26+
set(VISIONOS_BRIDGE_SOURCE
27+
src/visionOS/VisionOSAppBridge.mm
28+
)
29+
30+
set(VISIONOS_BRIDGE_INCLUDE
31+
src/visionOS/VisionOSAppBridge.h
32+
)
33+
34+
set(SHADERS
35+
assets/cube.vsh
36+
assets/cube.psh
37+
assets/shader_constants.fxh
38+
)
39+
40+
set(INFO_PLIST
41+
${CMAKE_CURRENT_SOURCE_DIR}/src/visionOS/Info.plist
42+
)
43+
44+
set_source_files_properties(${SHADERS} PROPERTIES
45+
VS_TOOL_OVERRIDE "None"
46+
MACOSX_PACKAGE_LOCATION "Resources"
47+
)
48+
49+
find_library(QUARTZCORE_FRAMEWORK QuartzCore)
50+
find_library(UIKIT_FRAMEWORK UIKit)
51+
if(NOT QUARTZCORE_FRAMEWORK OR NOT UIKIT_FRAMEWORK)
52+
message(FATAL_ERROR "QuartzCore/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+
${QUARTZCORE_FRAMEWORK}
75+
${UIKIT_FRAMEWORK}
76+
)
77+
set_common_target_properties(Tutorial30_HelloVisionOS_Native)
78+
set_target_properties(Tutorial30_HelloVisionOS_Native PROPERTIES
79+
FOLDER "DiligentSamples/Tutorials/Tutorial30_HelloVisionOS"
80+
)
81+
82+
add_executable(Tutorial30_HelloVisionOS MACOSX_BUNDLE
83+
${VISIONOS_SWIFT_SOURCE}
84+
${SHADERS}
85+
)
86+
87+
set_target_properties(Tutorial30_HelloVisionOS PROPERTIES
88+
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.diligentengine.samples.Tutorial30-HelloVisionOS"
89+
MACOSX_BUNDLE_INFO_PLIST "${INFO_PLIST}"
90+
BUILD_RPATH "@executable_path"
91+
FOLDER "DiligentSamples/Tutorials"
92+
XCODE_ATTRIBUTE_SWIFT_VERSION "5.0"
93+
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER
94+
"${CMAKE_CURRENT_SOURCE_DIR}/src/visionOS/VisionOSAppBridge.h"
95+
XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "YES"
96+
)
97+
98+
target_link_libraries(Tutorial30_HelloVisionOS PRIVATE
99+
Tutorial30_HelloVisionOS_Native
100+
)
101+
102+
source_group("src" FILES ${TUTORIAL_SOURCE} ${TUTORIAL_INCLUDE})
103+
source_group("visionOS" FILES ${VISIONOS_SWIFT_SOURCE} ${VISIONOS_BRIDGE_SOURCE} ${VISIONOS_BRIDGE_INCLUDE})
104+
source_group("assets" FILES ${SHADERS})
105+
106+
target_sources(Tutorial30_HelloVisionOS PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/readme.md")
107+
set_source_files_properties(
108+
"${CMAKE_CURRENT_SOURCE_DIR}/readme.md" PROPERTIES HEADER_FILE_ONLY TRUE
109+
)
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: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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+
### Shared scene
140+
141+
`Tutorial30_Scene` is format-agnostic. The PSO is created lazily on the first `Render()` call,
142+
using the RTV and DSV formats of the targets the scene is rendered to:
143+
144+
```cpp
145+
void Tutorial30_Scene::Render(IDeviceContext* pContext,
146+
ITextureView* pRTV,
147+
ITextureView* pDSV,
148+
const float4x4& ViewProj)
149+
{
150+
EnsurePipelineState(pRTV->GetTexture()->GetDesc().Format,
151+
pDSV->GetTexture()->GetDesc().Format);
152+
...
153+
}
154+
```
155+
156+
This lets the same scene object serve both the preview swap chain and the CompositorServices
157+
drawables without hard-coding any format constants.
158+
159+
160+
### Conventions: right-handed, reverse-Z
161+
162+
Both renderers use **right-handed view and projection matrices**:
163+
164+
- Immersive uses `cp_drawable_compute_projection(..., right_up_back, ...)` and a plain
165+
`simd_inverse` of the anchor-relative view transform.
166+
- Preview builds RH matrices explicitly in `Tutorial30_Preview.cpp`.
167+
168+
Both also use **reverse-Z**: the depth target is cleared to `0`, the PSO uses
169+
`COMPARISON_FUNC_GREATER_EQUAL`, and the immersive drawable is configured with
170+
`cp_drawable_set_depth_range(FarZ, NearZ)`.
171+
172+
Because the RH convention flips NDC winding relative to the default D3D convention, the PSO sets
173+
`FrontCounterClockwise = True` with `CULL_MODE_BACK`. This matches the canonical VR pattern used
174+
by [Tutorial28_HelloOpenXR](../Tutorial28_HelloOpenXR/readme.md).
175+

0 commit comments

Comments
 (0)