API reference

FramePhaseController Class

Modifiers: final
Phase-locked loop (PLL) controller that re-times the application tick against the OpenXR frame grid.
Background: the application tick (driven historically by Android Choreographer, i.e. the panel vsync clock) and the render frame (paced by xrWaitFrame, the compositor clock) are two independent ~72-90Hz clocks with no phase relationship. They beat against each other, so commit() routinely lands after the render thread has already started reading the committed state (read-during-write on the single-buffered renderable proxy) and the consumed input is ~a full frame stale.
This controller replaces the free-running Choreographer re-arm with a Handler-driven loop timed to the OpenXR predicted-display grid:
  • In locked / pre-sync mode it schedules the next tick via Handler.postDelayed against an absolute timeline so the realized lead converges to the target with low variance, using slew-rate-limited corrections, with phase-preserving catch-up if a callback runs long or is delayed.
  • In locked / post-sync midpoint mode it schedules against xrWaitFrame return timing so commit completion targets the midpoint between consecutive returns.
  • In fallback mode (bootstrap, paused, or non-XR) it reproduces the historical Choreographer behavior, including the drop-rejection that guards against 0 / sub-rate callbacks.
All scheduling, wall-clock reads, and tracing live here; the control algorithm and its state live in FramePhasePll, which is pure and unit-tested. All methods must be called from the main thread.

Signature

class FramePhaseController(callbacks: FramePhaseController.Callbacks)

Constructors

FramePhaseController ( callbacks )
Signature
constructor(callbacks: FramePhaseController.Callbacks)
Parameters
callbacks: FramePhaseController.Callbacks  Driver hooks for timing inputs, lifecycle guards, and the tick itself.

Properties

damping : Float
[Get][Set]
Proportional gain / damping: fraction of the phase error corrected per frame.
Signature
var damping: Float
enabled : Boolean
[Get][Set]
Whether phase-locking is enabled for this controller (the app's intent). Defaults to FramePhaseController.Companion.enabledByDefault. The loop additionally only phase-locks when the backend supports it (GraphicsHSR) and when predicted-display timing is available; otherwise it auto-falls-back to the historical free-running Choreographer schedule. So phase-sync is off on non-HSR backends even when this is true.
Signature
var enabled: Boolean
lockedScheduleMode : FramePhasePll.LockedScheduleMode
[Get][Set]
Locked-mode scheduling policy used after valid OpenXR timing is available.
Signature
var lockedScheduleMode: FramePhasePll.LockedScheduleMode
maxSlewUs : Long
[Get][Set]
Maximum schedule correction applied in a single frame, in microseconds (slew-rate limit).
Signature
var maxSlewUs: Long
targetLeadUs : Long
[Get][Set]
Target lead (us) from commit end to the next predicted-display grid line. Tune via captures.
Signature
var targetLeadUs: Long

Methods

onFrame ( frameTimeNanos )
Per-frame entry point. Called by the driver's doFrame (fallback mode, Choreographer) and by the internal Handler runnable (locked mode). The FramePhaseController.onFrame is the Choreographer vsync timestamp in fallback mode and System.nanoTime in locked mode; both are CLOCK_MONOTONIC.
Signature
fun onFrame(frameTimeNanos: Long)
Parameters
frameTimeNanos: Long
start ()
Starts (or restarts) the frame loop. No-op if already running.
Signature
fun start()
stop ()
Stops the frame loop and clears any pending scheduled frame.
Signature
fun stop()

Companion Object

Properties

enabledByDefault : Boolean
[Get][Set]
Process-wide default for FramePhaseController.enabled, read when a FramePhaseController is constructed. Set this before activity/service creation to globally opt the phase scheduler in or out (e.g. a kill-switch). Defaults to true (on where supported).
Signature
var enabledByDefault: Boolean
lockedScheduleModeByDefault : FramePhasePll.LockedScheduleMode
[Get][Set]
Process-wide default for locked scheduling. WAITFRAME_MIDPOINT targets commit completion midway between consecutive xrWaitFrame return times.
Signature
var lockedScheduleModeByDefault: FramePhasePll.LockedScheduleMode

Inner Interface

Callbacks Interface

Driver-supplied hooks. The controller owns scheduling; the driver owns lifecycle + the tick.

Signature

interface Callbacks

Methods

commit ()
The commit itself (spatial.tick(isResumed)). The controller measures wall-clock duration around this call to derive the realized lead and self-tune.
Signature
abstract fun commit()
getConfirmedFrameRate ()
Confirmed display refresh rate in Hz (e.g. scene.getConfirmedFrameRate()).
Signature
abstract fun getConfirmedFrameRate(): Float
Returns
Float
getFrameTiming ()
Latest OpenXR predicted-display timing snapshot (e.g. spatial.getFrameTiming()).
Signature
abstract fun getFrameTiming(): FrameTiming
Returns
FrameTiming
isPhaseSyncSupported ()
Whether the active graphics backend supports phase-locking (GraphicsHSR). When false, the controller never locks and runs the historical Choreographer schedule regardless of FramePhaseController.enabled — i.e. phase-sync defaults off on non-HSR backends. Queried at FramePhaseController.start.
Signature
abstract fun isPhaseSyncSupported(): Boolean
Returns
Boolean
onSceneTick ()
Per-frame application work, run only on frames that tick. The driver is responsible for any isResumed gating (matching the historical if (isResumed) onSceneTick()).
Signature
abstract fun onSceneTick()
postChoreographerFrame ()
Posts a Choreographer frame callback (the driver itself) so the next fallback-mode frame arrives via the driver's doFrame, which must delegate to FramePhaseController.onFrame. Used only in fallback mode; locked mode is driven by an internal Handler.
Signature
abstract fun postChoreographerFrame()
removeChoreographerFrame ()
Removes any pending Choreographer frame callback posted via FramePhaseController.Callbacks.postChoreographerFrame.
Signature
abstract fun removeChoreographerFrame()
shouldContinueFrameLoop ()
Top-of-frame lifecycle guard. When false, the controller neither ticks nor re-arms and the loop stops (until FramePhaseController.start is called again). Mirrors the drivers' isFinishing || isDestroyed / cancelScheduledFrames checks.
Signature
abstract fun shouldContinueFrameLoop(): Boolean
Returns
Boolean
shouldRearmFrameLoop ()
Re-arm guard evaluated after the tick. When false the loop stops (until FramePhaseController.start is called again). Mirrors VrActivity's post-pause OpenXR settle window; drivers without a settle window return the same value as FramePhaseController.Callbacks.shouldContinueFrameLoop.
Signature
abstract fun shouldRearmFrameLoop(): Boolean
Returns
Boolean