Skip to content

Idle the render thread while the OpenXR session is stopped - #13

Open
jfrolund wants to merge 1 commit into
QuestCraftPlusPlus:OpenXR-1.21.10from
jfrolund:fix/xr-idle-guard
Open

Idle the render thread while the OpenXR session is stopped#13
jfrolund wants to merge 1 commit into
QuestCraftPlusPlus:OpenXR-1.21.10from
jfrolund:fix/xr-idle-guard

Conversation

@jfrolund

Copy link
Copy Markdown

Problem

MCOpenXR already tracks session state in isActive — set false on
XR_SESSION_STATE_STOPPING and _IDLE, true on _READY/_VISIBLE/_FOCUSED
but the render path never reads it. poll() checks only initialized.

So when the session stops (headset taken off, app backgrounded) xrWaitFrame returns
immediately with XR_ERROR_SESSION_NOT_RUNNING instead of pacing us to the display
rate. Nothing bails out, so xrBeginFrame, xrLocateViews and five xrLocateSpace
calls all run and all fail — and with nothing left to block on, the render thread
free-runs:

[07:40:26] [Render thread/ERROR]: xrLocateSpace for gripSpace[0] errored: XR_ERROR_TIME_INVALID
[07:40:26] [Render thread/ERROR]: xrEndFrame for  errored: XR_ERROR_SESSION_NOT_RUNNING

Impact, measured on a Quest 2

Roughly 1400 error lines per second, sustained for as long as the headset was left
alone — latestlog.txt reached 118 MB in one session, with 332,382 error lines.

The allocation churn from formatting those messages drives the heap to its ceiling, and
the game is eventually killed with reason=3 (LOW_MEMORY) — typically hours later while
unattended. It also made the headset's own auto-sleep unusable: sleeping stops the
session, which just drove this path harder, so the game appeared to "crash on sleep".

Change

  • Return early from updatePose() when the session is not running, sleeping briefly so
    the thread idles instead of spinning.
  • Skip frame submission in OpenXRStereoRenderer#endFrame for the same reason — no
    frame was begun, so releasing images and submitting a layer can only fail.
  • Rate limit logError() per call site and result, reporting a suppressed count, so any
    other failing call cannot flood the log the same way.

The guard goes inside updatePose() rather than poll() deliberately: poll() uses
paired Profiler.popPush/pop, and returning early there unbalances the profiler stack.

Result

Same test after the fix: zero error lines, and 263 bytes of log growth across a
40 minute sleep. The game has since run for over a day with the headset's auto-sleep
re-enabled and no LOW_MEMORY kills.

Notes

Built and verified against OpenXR-1.21.10. The on-device validation ran on the
OpenXR-1.21.5 equivalent of these changes (that being the version the current
QuestCraft release ships), with the updatePose guard and the logError throttle in
the deployed build; the endFrame guard is included here as the matching counterpart.
The same edits apply to the 1.21.5 and 1.21.8 branches if you want them backported.

🤖 Generated with Claude Code

MCOpenXR already tracks session state in isActive -- set false on
XR_SESSION_STATE_STOPPING and _IDLE, true on _READY/_VISIBLE/_FOCUSED --
but the render path never reads it. poll() checks only initialized.

So when the session stops (headset taken off, app backgrounded)
xrWaitFrame returns immediately with XR_ERROR_SESSION_NOT_RUNNING
instead of pacing us to the display rate. Nothing bails out, so
xrBeginFrame, xrLocateViews and five xrLocateSpace calls all run and all
fail, and with nothing left to block on the render thread free-runs.

Measured on a Quest 2: roughly 1400 error lines a second, sustained for
as long as the headset was left alone, growing latestlog.txt to 118MB in
a single session. The allocation churn from formatting those messages
drives the heap to its ceiling, and the game is eventually killed with
reason=3 (LOW_MEMORY) -- typically hours later while unattended. It also
made the headset's own auto-sleep unusable, since sleeping just drove
the same path harder.

Return early from updatePose() when the session is not running, sleeping
briefly so the thread idles instead of spinning, and skip frame
submission in OpenXRStereoRenderer#endFrame for the same reason. Rate
limit logError() per call site and result as well, so any other failing
call cannot flood the log the same way.

The guard goes inside updatePose() rather than poll() because poll()
uses paired Profiler.popPush/pop, and returning early there unbalances
the profiler stack.

After the fix the same test produced zero error lines and 263 bytes of
log growth across a 40 minute sleep, and the game has since run for over
a day with auto-sleep enabled and no LOW_MEMORY kills.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant