All Oculus Quest developers MUST PASS the concept review prior to gaining publishing access to the Quest Store and additional resources. Submit a concept document for review as early in your Quest application development cycle as possible. For additional information and context, please see Submitting Your App to the Oculus Quest Store.
Multiple Android activities that live in the same address space can cooperatively use the VrApi. However, only one activity can be in VR mode at a time. This topic explains when an activity is expected to enter/leave VR mode.
An Android activity can only be in VR mode while the activity is in the resumed state. The following shows how VR mode fits into the Android activity lifecycle.
1. VrActivity::onCreate() <---------+ 2. VrActivity::onStart() <-------+ | 3. VrActivity::onResume() <---+ | | 4. vrapi_EnterVrMode() | | | 5. vrapi_LeaveVrMode() | | | 6. VrActivity::onPause() -----+ | | 7. VrActivity::onStop() ----------+ | 8. VrActivity::onDestroy() ---------+
An Android activity can only be in VR mode while there is a valid Android surface. The following shows how VR mode fits into the Android surface lifecycle.
1. VrActivity::surfaceCreated() <----+ 2. VrActivity::surfaceChanged() | 3. vrapi_EnterVrMode() | 4. vrapi_LeaveVrMode() | 5. VrActivity::surfaceDestroyed() ---+
Note that the lifecycle of a surface is not necessarily tightly coupled with the lifecycle of an activity. These two lifecycles may interweave in complex ways. Usually,
surfaceCreated() is called after
surfaceDestroyed() is called between
onDestroy(). However, this is not guaranteed; for instance, under certain circumstances
surfaceDestroyed() may be called after
onDestroy() or even before
An Android activity is only in the resumed state with a valid Android surface between
onResume(), whichever comes last, and
onPause(), whichever comes first. In other words, a VR application will typically enter VR mode from
onResume(), whichever comes last, and leave VR mode from
onPause(), whichever comes first.
This is a high-level overview of the rendering pipeline used by VrApi. For more information, see
EGLContextfor the application.
vrapi_GetSystemPropertyInt( &java, VRAPI_SYS_PROP_SUGGESTED_EYE_TEXTURE_WIDTH ).
ANativeWindowfrom Android Surface.
ovrTracking2. Insert fence using
The API is designed to work with an Android activity using a plain Android
SurfaceView, where the Activity lifecycle and the surface lifecycle are managed completely in native code by sending the lifecycle events (
surfaceChanged, and so on) to Android.
The API does not work with an Android Activity using a
GLSurfaceView class manages the window surface and
EGLSurface and the implementation of GLSurfaceView may unbind the
onPause() gets called. As such, there is no way to leave VR mode before the
EGLSurface disappears. Another problem with
GLSurfaceView is that it creates the
eglChooseConfig(). The Android
EGL code pushes in multi-sample flags in
eglChooseConfig() if the user has selected the “force 4x MSAA” option in settings. Using a multi-sampled front buffer is completely wasted for Asynchronous TimeWarp (ATW) rendering.
Alternately, an Android
NativeActivity can be used to avoid manually handling all the lifecycle events. However, it is important to select the
EGLConfig manually without using
eglChooseConfig() to make sure the front buffer is not multi-sampled.
vrapi_GetSystemProperty* functions can be called at any time from any thread. This allows an application to set up its renderer, possibly running on a separate thread, before entering VR mode.
On Android, an application cannot just allocate a new window/frontbuffer and render to it. Android allocates and manages the window/frontbuffer and (after the fact) notifies the application of the state of affairs through lifecycle events (
surfaceDestroyed). The application (or third-party engine) typically handles these events. Since the VrApi cannot just allocate a new window/frontbuffer, and the VrApi does not handle the lifecycle events, the VrApi somehow has to take over ownership of the Android surface from the application. To allow this, the application can explicitly pass the
vrapi_EnterVrMode(), where the
EGLSurface is the surface created from the
EGLContext is used to match the version and config for the context used by the background TimeWarp thread. This
EGLContext, and no other context, can be current on the
If, however, the application does not explicitly pass in these objects, then
vrapi_EnterVrMode()must be called from a thread with an OpenGL ES context current on the Android window surface. The context of the calling thread is then used to match the version and config for the context used by the background ATW thread. The ATW will also hijack the Android window surface from the context that is current on the calling thread. On return, the context from the calling thread will be current on an invisible buffer, because the ATW takes ownership of the Android window surface. Note that this requires the config used by the calling thread to have an
Before getting sensor input, the application also needs to know when the images that are going to be synthesized will be displayed, because the sensor input needs to be predicted ahead for that time. As it turns out, it is not trivial to get an accurate predicted display time. Therefore the calculation of this predicted display time is part of the VrApi. An accurate predicted display time can only really be calculated once the rendering loop is up and running and submitting frames regularly. In other words, before getting sensor input, the application needs an accurate predicted display time, which in return requires the renderer to be up and running. As such, it makes sense that sensor input is not available until
vrapi_EnterVrMode() has been called. However, once the application is in VR mode, it can call
vrapi_GetPredictedTracking() at any time from any thread.
The VrApi allows for one frame of overlap which is essential on tiled mobile GPUs. Because there is one frame of overlap, the eye images have typically not completed rendering by the time they are submitted to
vrapi_SubmitFrame(). To allow the TimeWarp to check whether the eye images have completed rendering, the application can explicitly pass in a sync object (
CompletionFence) for each eye image through
vrapi_SubmitFrame(), or in the case of
CompletionFence is specified for the whole frame via the
ovrSubmitFrameDescription structure. Note that these sync objects must be EGLSyncKHR because the VrApi still supports OpenGL ES 2.0.
If, however, the application does not explicitly pass in sync objects, then
vrapi_SubmitFrame()must be called from the thread with the OpenGL ES context that was used for rendering, which allows
vrapi_SubmitFrame() to add a sync object to the current context and check if rendering has completed.
Note that even if no OpenGL ES objects are explicitly passed through the VrApi,
vrapi_SubmitFrame() can still be called from different threads.
vrapi_EnterVrMode() needs to be called from a thread with an OpenGL ES context that is current on the Android window surface. This does not need to be the same context that is also used for rendering.
vrapi_SubmitFrame() needs to be called from the thread with the OpenGL ES context that was used to render the eye images. If this is a different context than the context used to enter VR mode, then this context never needs to be current on the Android window surface for stereoscopic rendering.
Eye images are passed to
vrapi_SubmitFrame() as “texture swap chains” (
ovrTextureSwapChain). These texture swap chains are allocated through
vrapi_CreateTextureSwapChain(). This is important to allow these textures to be allocated in special system memory. When using a static eye image, the texture swap chain does not need to be buffered and the chain only needs to hold a single texture. When the eye images are dynamically updated, the texture swap chain needs to be buffered. When the texture swap chain is passed to
vrapi_SubmitFrame(), the application also passes in the chain index to the most recently updated texture.
It is critical in VR that we never show the user a stale frame.
vrapi_SubmitFrame() controls the synthesis rate through an application-specified frame parameter,
SwapInterval. It also determines the point where the calling thread gets released, currently the halfway point of the predicted display refresh cycle.
vrapi_SubmitFrame() only returns when both these conditions are met:
The ATW thread consumes new eye images and updates the V-sync counter halfway through a display refresh cycle. This is the first time ATW can start updating the first eye, covering the first half of the display. As a result,
vrapi_SubmitFrame() returns and releases the calling thread at the halfway point of the display refresh cycle.
vrapi_SubmitFrame() returns, synthesis has a full display refresh cycle to generate new eye images up to the next midway point. At the next halfway point, the TimeWarp has half a display refresh cycle (up to V-sync) to update the first eye. The TimeWarp then effectively waits for V-sync and then has another half a display refresh cycle (up to the next-next halfway point) to update the second eye. ATW uses a high priority GPU context and will eat away cycles from synthesis, so synthesis does not have a full display refresh cycle worth of actual GPU cycles. However, ATW tends to be very fast, leaving most of the GPU time for synthesis.
Instead of just using the latest sensor sampling, synthesis uses predicted sensor input for the middle of the time period during which the new eye images will be displayed. This predicted time is calculated using
vrapi_GetPredictedDisplayTime(). The number of frames predicted ahead depends on the pipeline depth, the extra latency mode, and the minimum number of V-syncs in between eye image rendering. Less than half a display refresh cycle before each eye image will be displayed, ATW will get new predicted sensor input using the very latest sensor sampling. ATW then corrects the eye images using this new sensor input. In other words, ATW always corrects the eye images even if the predicted sensor input for synthesis is not perfect. However, the better the prediction for synthesis, the less black will be pulled in at the edges by ATW.
The application can improve the prediction by fetching the latest predicted sensor input right before rendering each eye, and passing a possibly different sensor state for each eye to
vrapi_SubmitFrame(). However, it is very important that both eyes use a sensor state that is predicted for the exact same display time, so both eyes can be displayed at the same time without causing intra-frame motion judder. While the predicted orientation can be updated for each eye, the position must remain the same for both eyes, or the position would seem to judder “backwards in time” if a frame is dropped.
Ideally the eye images are only displayed for the
SwapInterval display refresh cycles that are centered about the eye image predicted display time. In other words, a set of eye images is first displayed at Predicted Display Time - (SwapInterval / 2) display refresh cycles. The eye images should never be shown before this time because that can cause intra-frame motion judder. Ideally the eye images are also not shown after Predicted Display Time + (SwapInterval / 2) display refresh cycles, but this may happen if synthesis fails to produce new eye images in time.