Last active
March 22, 2023 12:56
-
-
Save brycehutchings/7300e30fe645137df714b907c0f33032 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This example shows how a pipelined OpenXR renderer should work to minimize latency | |
// and avoid common mistakes around timestamps and poses. | |
// This struct stores state that needs to persist from game thread to render thread. | |
// With a pipelined renderer there will be two frames active most of the time so that | |
// the renderer and game logic can run in parallel. | |
struct MyFrameState | |
{ | |
XrTime displayTime; | |
array<XrView> views; | |
// This struct may need other state that needs to be associated with a frame. | |
// This likely includes the VIEW space location which can act as a camera pose. | |
}; | |
XrSession session = ...; | |
XrSpace sceneSpace = ...; // Some space that you are locating things in, often LOCAL or STAGE reference space. | |
MyFrameState gameFrame = null; | |
MyFrameState renderFrame = null; | |
Event renderFrameSignal; | |
Game Thread Loop | |
{ | |
XrFrameState frameState{XR_TYPE_FRAME_STATE}; | |
xrWaitFrame(session, nullptr, &frameState); | |
// Only on the second frame and later will the previous game frame be available. | |
if (gameFrame != null) | |
{ | |
// The render thread is notified it is time to render the previous gameFrame. | |
// | |
// It is safe to set renderFrame here because xrWaitFrame will block until the previous frame | |
// has called xrBeginFrame. A warning though: if xrBeginFrame is never called, xrWaitFrame may | |
// block indefinitely, so you must ensure that every call to xrWaitFrame has a corresponding | |
// call to xrBeginFrame! | |
renderFrame = move(gameFrame); | |
renderFrameSignal.notify(); | |
} | |
gameFrame = new GameFrame(); | |
// Store the time that this frame will be displayed, one frame ahead of the current frame that xrWaitFrame is for. | |
gameFrame.displayTime = frameState.predictedDisplayTime + frameState.predictedDisplayPeriod; | |
// Get an initial extrapolated view positions for the frame. This uses the computed time from above. | |
XrViewLocateInfo viewLocateInfo{XR_TYPE_VIEW_LOCATE_INFO}; | |
viewLocateInfo.viewConfigurationType = ...; | |
viewLocateInfo.displayTime = gameFrame.displayTime; | |
viewLocateInfo.space = sceneSpace; | |
uint32_t viewCount; // We assume gameFrame.views is already the correct length. | |
xrLocateViews(session, &viewLocateInfo, gameFrame.views.size(), &viewCount, gameFrame.views.data()); | |
// Omitted: Update actions and action spaces, poll events, etc. | |
// Game logic usually needs to know where you are looking, so the above xrLocateViews call acts as an early best guess. | |
RunGameLogic(gameFrame); | |
} | |
Render Thread Loop | |
{ | |
// Wait for signal that renderFrame is available. | |
renderFrameSignal.wait(); | |
// Move renderFrame data to a local viarable so that the game thread does not need to wait to store new data. | |
// This allows the game to use xrWaitFrame as a synchronization mechanism which will block until xrBeginFrame | |
// is called for the previous frame. | |
MyFrameState renderFrame = move(renderFrame); | |
// xrBeginFrame indicates the start of GPU work. CPU work should be kept to a minimum after this point. | |
// xrBeginFrame is also used to indicate when the next xrWaitFrame call can return. xrWaitFrame will | |
// block until xrBeginFrame has been called for the previous frame. | |
xrBeginFrame(session); | |
// Locate the views again using the same timestamp (sometimes called a late pose update). | |
// This will give the most accurate prediction possible. | |
// Other dynamic spaces like action spaces (e.g. controllers) could get a late update here as well but | |
// this is omitted to keep things simple. This may not be desirable if physics is applied to action spaces. | |
XrViewLocateInfo viewLocateInfo{XR_TYPE_VIEW_LOCATE_INFO}; | |
viewLocateInfo.viewConfigurationType = ...; | |
viewLocateInfo.displayTime = renderFrame.displayTime; | |
viewLocateInfo.space = sceneSpace; | |
uint32_t viewCount; // renderFrame.views is already the correct size, so ignore the output size. | |
xrLocateViews(session, &viewLocateInfo, renderFrame.views.size(), &viewCount, renderFrame.views.data()); | |
// Render using the late update poses. | |
RenderGameForEachView(renderFrame); | |
// Every projection view must specify the exact pose and FOV used to render. In this simple case this is | |
// the late pose update done a few lines earler. | |
array<XrCompositionLayerProjectionView> projectionViews(renderFrame.views.size()); | |
for (int viewIndex = 0; viewIndex < renderFrame.views.size(); viewIndex++) | |
{ | |
projectionViews[viewIndex].pose = renderFrame.views[viewIndex].pose; | |
projectionViews[viewIndex].fov = renderFrame.views[viewIndex].fov; | |
} | |
// Create the projection layer using the projection view data that was just set up. | |
XrCompositionLayerProjection projectionLayer{XR_TYPE_COMPOSITION_LAYER_PROJECTION}; | |
projectionLayer.space = sceneSpace; | |
projectionLayer.views = projectionViews.data(); | |
projectionLayer.viewCount = projectionViews.size(); | |
// Submit the frame using the timestamp. Note how the same computed timestamp is used for the entire frame | |
// on both the game thread and render thread. | |
XrFrameEndInfo frameEndInfo{XR_TYPE_FRAME_END_INFO}; | |
frameEndInfo.displayTime = renderFrame.displayTime; | |
frameEndInfo.environmentBlendMode = ...; | |
frameEndInfo.layerCount = ...; | |
frameEndInfo.layers = ...; // Includes projectionLayer and anything else desired. | |
xrEndFrame(session, &frameEndInfo); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment