|
| 1 | +--- |
| 2 | +title: Integrate Remote Rendering into a HoloLens Holographic App |
| 3 | +description: Explains how to integrate Remote Rendering into a plain HoloLens Holographic App as created with Visual Studio project wizard |
| 4 | +author: florianborn71 |
| 5 | +ms.author: flborn |
| 6 | +ms.date: 05/04/2020 |
| 7 | +ms.topic: tutorial |
| 8 | +--- |
| 9 | + |
| 10 | +# Tutorial: Integrate Remote Rendering into a HoloLens Holographic App |
| 11 | + |
| 12 | +In this tutorial you will learn: |
| 13 | + |
| 14 | +> [!div class="checklist"] |
| 15 | +> |
| 16 | +> * Using Visual Studio to create a Holographic App that can be deployed to HoloLens |
| 17 | +> * Add the necessary code snippets and project settings to combine local rendering with remotely rendered content |
| 18 | +
|
| 19 | +This tutorial focuses on adding the necessary bits to a native `Holographic App` sample to combine local rendering with Azure Remote Rendering. It leaves out the parts for proper error- and status display inside the app (for example through text panels), because adding UI elements in native C++ is beyond the scope of this sample. Building a dynamic text panel from scratch is cumbersome as you need to deal with writing text to native `DirectX11` resources via `DirectWrite` and then render these resources as 3d overlays in the scene using custom shaders. A good starting point is class `StatusDisplay` which is part of the |
| 20 | +[Remoting Player sample project on GitHub](https://github.com/microsoft/MixedReality-HolographicRemoting-Samples/tree/master/player/common/Content). In fact, the pre-canned version of this tutorial uses a local copy of that class. |
| 21 | + |
| 22 | +> [!TIP] |
| 23 | +> The [ARR samples repository](https://github.com/Azure/azure-remote-rendering) contains the outcome of this tutorial as a Visual Studio project that is ready to use. It is also enriched with proper error- and status reporting through UI class `StatusDisplay`. Inside the tutorial, all ARR specific additions are scoped by `#ifdef USE_REMOTE_RENDERING` / `#endif`, so it is easy to see what has been modified. |
| 24 | +
|
| 25 | +## Prerequisites |
| 26 | + |
| 27 | +For this tutorial you need: |
| 28 | + |
| 29 | +* Your account information (account ID, account key, subscription ID). If you don't have an account, [create an account](../../how-tos/create-an-account.md). |
| 30 | +* Windows SDK 10.0.18362.0 [(download)](https://developer.microsoft.com/windows/downloads/windows-10-sdk) |
| 31 | +* The latest version of Visual Studio 2019 [(download)](https://visualstudio.microsoft.com/vs/older-downloads/) |
| 32 | +* Windows Mixed Reality App Templates for Visual Studio -> download link (Visual Studio Marketplace) |
| 33 | + |
| 34 | +## Create a new Holographic App sample |
| 35 | + |
| 36 | +As a first step, we create a stock sample that is the basis for the Remote Rendering integration. Open Visual Studio and select "Create a new project" and search for "Holographic DirectX 11 App (Universal Windows) (C++/WinRT)" |
| 37 | + |
| 38 | + |
| 39 | + |
| 40 | +Type in a project name of your choice, choose the path and select the "Create" button. |
| 41 | +In the new project, switch the configuration to **"Release / ARM64"**. You should now be able to compile and deploy it to a connected HoloLens 2 device. If you run it on HoloLens, you should see a rotating cube in front of you. |
| 42 | + |
| 43 | +## Add Remote Rendering dependencies through NuGet |
| 44 | + |
| 45 | +First step into adding Remote Rendering capabilities is to add the client side dependencies. These are availble as a NuGet package. |
| 46 | +In the Solution Explorer, right-click on the project and select **"Manage NuGet Packages..."** from the context menu. |
| 47 | + |
| 48 | +In the prompted dialog, browse for the **"Azure Remote Rendering"** NuGet package: |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | +and add it to the project by selecting the package and then pressing the "Install" button. |
| 53 | + |
| 54 | +The NuGet package adds the Remote Rendering dependencies to the project. Specifically: |
| 55 | +* link against the client library (RemoteRenderingClient.lib), |
| 56 | +* set up the .dll dependencies, |
| 57 | +* set the correct path to the include directory. |
| 58 | + |
| 59 | +## Integrate Remote Rendering |
| 60 | + |
| 61 | +Now that the project is prepared, we can start with the code. A good entry point into the application is class `HolographicAppMain`(file HolographicAppMain.h/cpp) because it has all the necessary hooks for initialization, de-initialization, and rendering. |
| 62 | + |
| 63 | +### Includes |
| 64 | + |
| 65 | +We start by adding the necessary includes. Add the following include to file HolographicAppMain.h: |
| 66 | + |
| 67 | +```cpp |
| 68 | +#include <AzureRemoteRendering.h> |
| 69 | +``` |
| 70 | + |
| 71 | +...and this additional include to file HolographicAppMain.cpp: |
| 72 | + |
| 73 | +```cpp |
| 74 | +#include <AzureRemoteRendering.inl> |
| 75 | +``` |
| 76 | + |
| 77 | +For simplicity of code, we define the following namespace shortcut at the top of file HolographicAppMain.h, after the include: |
| 78 | + |
| 79 | +```cpp |
| 80 | +namespace RR = Microsoft::Azure::RemoteRendering; |
| 81 | +``` |
| 82 | +
|
| 83 | +This is useful so we don't have to write out the full namespace everywhere but still can recognize ARR specific data structures. Of course you can also use the `using namespace...` directive. |
| 84 | +
|
| 85 | +### Initialization |
| 86 | + |
| 87 | +We need to hold a few objects for the session etc. during the lifetime of the application. The lifetime coincides with the lifetime of the application's `HolographicAppMain` object, so we simply add our objects as members to class `HolographicAppMain`. The next step is adding the following class members in file HolographicAppMain.h: |
| 88 | +
|
| 89 | +```cpp |
| 90 | +class HolographicAppMain |
| 91 | +{ |
| 92 | + ... |
| 93 | + // members: |
| 94 | + RR::ApiHandle<RR::AzureFrontend> m_frontEnd; // the front end instance |
| 95 | + RR::ApiHandle<RR::AzureSession> m_session; // the current remote rendering session |
| 96 | + RR::ApiHandle<RR::RemoteManager> m_api; // the API instance, that is used to perform all the actions. This is just a shortcut to m_session->Actions() |
| 97 | + RR::ApiHandle<RR::GraphicsBindingWmrD3d11> m_graphicsBinding; // the graphics binding instance |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +A good place to do the actual implementation is the constructor of class `HolographicAppMain`. We have to do three types of initialization there: |
| 102 | +1. The one-time initialization of the Remote Rendering system |
| 103 | +1. Front end creation |
| 104 | +1. Session creation |
| 105 | + |
| 106 | +We do all of that sequentially in the constructor. However, in real use cases it might be appropriate to do this separately, for example linked to UI events. Add the following code to the beginning of the constructor body in file HolographicAppMain.cpp: |
| 107 | + |
| 108 | +```cpp |
| 109 | +HolographicAppMain::HolographicAppMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : |
| 110 | + m_deviceResources(deviceResources) |
| 111 | +{ |
| 112 | + // 1. one time initialization |
| 113 | + { |
| 114 | + RR::RemoteRenderingInitialization clientInit; |
| 115 | + memset(&clientInit, 0, sizeof(RR::RemoteRenderingInitialization)); |
| 116 | + clientInit.connectionType = RR::ConnectionType::General; |
| 117 | + clientInit.graphicsApi = RR::GraphicsApiType::WmrD3D11; |
| 118 | + clientInit.toolId = "<sample name goes here>"; // <put your sample name here> |
| 119 | + clientInit.unitsPerMeter = 1.0f; |
| 120 | + clientInit.forward = RR::Axis::Z_Neg; |
| 121 | + clientInit.right = RR::Axis::X; |
| 122 | + clientInit.up = RR::Axis::Y; |
| 123 | + RR::StartupRemoteRendering(clientInit); |
| 124 | + } |
| 125 | + |
| 126 | + // 2. Create front end |
| 127 | + { |
| 128 | + // fill out the following credentials: |
| 129 | + RR::AzureFrontendAccountInfo init; |
| 130 | + memset(&init, 0, sizeof(RR::AzureFrontendAccountInfo)); |
| 131 | + init.AccountId = "00000000-0000-0000-0000-000000000000"; |
| 132 | + init.AccountKey = "<account key>"; |
| 133 | + init.AccountDomain = "<account domain>"; |
| 134 | + |
| 135 | + m_frontEnd = RR::ApiHandle(RR::AzureFrontend(init)); |
| 136 | + } |
| 137 | + |
| 138 | + // 3. Open rendering session |
| 139 | + { |
| 140 | + auto openSession = m_frontEnd->OpenRenderingSession("<SessionId>"); |
| 141 | + if (!openSession) |
| 142 | + { |
| 143 | + return; |
| 144 | + } |
| 145 | + |
| 146 | + m_session = *openSession; |
| 147 | + m_api = m_session->Actions(); // cache the 'actions' object for convenience |
| 148 | + m_graphicsBinding = m_session->GetGraphicsBinding().as<RR::GraphicsBindingWmrD3d11>(); |
| 149 | + } |
| 150 | + |
| 151 | + { // listen to connection status changed callbacks |
| 152 | + m_session->ConnectionStatusChanged([this](auto status, auto error) |
| 153 | + { |
| 154 | + OnConnectionStatusChanged(status, error); |
| 155 | + }); |
| 156 | + } |
| 157 | + |
| 158 | + |
| 159 | + // rest of constructor code: |
| 160 | + ... |
| 161 | +} |
| 162 | +``` |
| 163 | +In the last addition above, we register to a callback that is triggered whenever the connection status on given session changes. We re-direct that call to our own function `OnConnectionStatusChanged`, which does not exist yet. We will declare and implement it as part of the state machine in the next paragraph. Also note that credentials are hard-coded in the sample and need to be filled out in place ([account ID, account key](../../../how-tos/create-an-account.md#retrieve-the-account-information) and [domain](../../../reference/regions.md)). |
| 164 | +
|
| 165 | +We do the de-initialization symmetrically and in reverse order at the end of the destructor body: |
| 166 | +
|
| 167 | +```cpp |
| 168 | +HolographicAppMain::~HolographicAppMain() |
| 169 | +{ |
| 170 | + // existing destructor code: |
| 171 | + ... |
| 172 | + |
| 173 | + // destroy session: |
| 174 | + if (m_session != nullptr) |
| 175 | + { |
| 176 | + m_session->DisconnectFromRuntime(); |
| 177 | + m_session = nullptr; |
| 178 | + } |
| 179 | +
|
| 180 | + // destroy front end: |
| 181 | + m_frontEnd = nullptr; |
| 182 | +
|
| 183 | + // one-time de-initialization: |
| 184 | + RR::ShutdownRemoteRendering(); |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +## State machine |
| 189 | + |
| 190 | +In Remote Rendering, key functions to create a session and to load a model are asynchronous functions. To reflect this, we need a simple state machine that essentially transitions through the following states automatically: |
| 191 | + |
| 192 | +*Initialization -> Session creation -> model loading (with progress)* |
| 193 | + |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | +Accordingly, as a next step, we add a bit of state machine handling to the class. We declare our own enum `ConnectionStatus` for the various states that our application can be in. It is very similar to `RR::ConnectionStatus`, but has an additional state for failed connection. |
| 198 | + |
| 199 | +Add the following members and functions to the class declaration: |
| 200 | + |
| 201 | +```cpp |
| 202 | +namespace HolographicApp |
| 203 | +{ |
| 204 | + // our applications's possible states: |
| 205 | + enum class ConnectionStatus |
| 206 | + { |
| 207 | + Disconnected, |
| 208 | + Connecting, |
| 209 | + Connected, |
| 210 | + ConnectionFailed, |
| 211 | + }; |
| 212 | + |
| 213 | + class HolographicAppMain |
| 214 | + { |
| 215 | + ... |
| 216 | + // member functions for state transition handling |
| 217 | + void OnConnectionStatusChanged(RR::ConnectionStatus status, RR::Result error); |
| 218 | + void SetNewState(ConnectionStatus state, RR::Result error); |
| 219 | + |
| 220 | + // members for state handling: |
| 221 | + ConnectionStatus m_currentStatus = ConnectionStatus::Disconnected; |
| 222 | + RR::Result m_connectionResult = RR::Result::Success; |
| 223 | + bool m_isConnected = false; |
| 224 | + |
| 225 | + } |
| 226 | +``` |
| 227 | +
|
| 228 | +### Per frame update |
| 229 | +
|
| 230 | +We have to perform some operations once per simulation tick. This includes ticking the client and custom handling of our state machine. |
| 231 | +Class `HolographicApp1Main` provides a good hook for this, so add the following code to the body of function `HolographicApp1Main::Update`: |
| 232 | +
|
| 233 | +```cpp |
| 234 | +// Updates the application state once per frame. |
| 235 | +HolographicFrame HolographicAppMain::Update() |
| 236 | +{ |
| 237 | + // tick Remote rendering: |
| 238 | + if (m_session != nullptr) |
| 239 | + { |
| 240 | + // tick the client to receive messages |
| 241 | + m_api->Update(); |
| 242 | +
|
| 243 | + if (m_isConnected && !m_modelLoadTriggered) |
| 244 | + { |
| 245 | + m_modelLoadTriggered = true; |
| 246 | + LoadModel(); |
| 247 | + } |
| 248 | + } |
| 249 | +
|
| 250 | + // rest of the body: |
| 251 | + ... |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +### Rendering |
| 256 | + |
| 257 | +The last thing to do is invoking the rendering. We have to do this in the exact right position within the rendering pipeline, after the clear. Insert the following snippet into the `UseHolographicCameraResources` lock inside function `HolographicAppMain::Render`: |
| 258 | + |
| 259 | +```cpp |
| 260 | + ... |
| 261 | + // existing clear function: |
| 262 | + context->ClearDepthStencilView(depthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0); |
| 263 | + |
| 264 | + // inject remote rendering: as soon as we are connected, start blitting the remote frame. |
| 265 | + // We do the blitting after the Clear, and before cube rendering |
| 266 | + if (m_isConnected) |
| 267 | + { |
| 268 | + m_graphicsBinding->BlitRemoteFrame(); |
| 269 | + } |
| 270 | + |
| 271 | + ... |
| 272 | +``` |
| 273 | +
|
| 274 | +## Run the sample |
| 275 | +
|
| 276 | +The sample should now be in a state to compile and run. Note that there is no error display integrated into this demo. So if something goes wrong (e.g. authentication failures), there is no visible feedback. Displaying the current state and loading progress is left to the user. The pre-canned version of this sample on GitHub on the other hand has status text. |
| 277 | +
|
| 278 | +
|
| 279 | +## Next steps |
| 280 | +
|
| 281 | +In this tutorial, you learned all the steps necessary to take a blank Unity project and get it working with Azure Remote Rendering. In the next tutorial, we will take a closer look at how to work with remote entities. |
| 282 | +
|
| 283 | +> [!div class="nextstepaction"] |
| 284 | +> [Tutorial: Working with remote entities in Unity](working-with-remote-entities.md) |
0 commit comments