Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save brycehutchings/7300e30fe645137df714b907c0f33032 to your computer and use it in GitHub Desktop.
Save brycehutchings/7300e30fe645137df714b907c0f33032 to your computer and use it in GitHub Desktop.
// 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