Skip to content

compositor: only release swapchain image in Submit if one was acquired - #431

Open
shakespear-dev wants to merge 1 commit into
Supreeeme:mainfrom
shakespear-dev:submit-release-guard
Open

shakespear-dev wants to merge 1 commit into
Supreeeme:mainfrom
shakespear-dev:submit-release-guard

Conversation

@shakespear-dev

Copy link
Copy Markdown

Fixes a panic in IVRCompositor::Submit that kills the game on startup:

panicked at openxr-0.21.1/src/swapchain.rs:107:9:
wait_image must be called before release_image
   ...
   6: <xrizer::compositor::Compositor as openvr::bindings::vr::IVRCompositor029_Interface>::Submit
   7: openvr::bindings::vr::ivrcompositor021::Submit

How I hit it

Serious Sam VR: The First Encounter (Steam 552450) under Proton, WiVRn, Quest 3, Linux, AMD GPU. The game shows "xrizer crashed" on every launch, before the menu. Same result with the v0.5 release and with the current nightly (0989a7f). In the log the panic always comes right after the first game texture arrives:

INFO  xrizer::compositor  Creating real backend for texture type Vulkan
INFO  xrizer::openxr_data Began OpenXR session.
INFO  xrizer::compositor  Received game texture, restarted session with new data
ERROR xrizer              panicked at ... wait_image must be called before release_image

The full log excerpt with the backtrace is at the bottom of this description.

This looks like the same crash as #364 (Serious Sam Fusion, also WiVRn): same panic text at the same point in the log. That issue was closed after a reinstall made it go away, without a code change.

Why it happens

FrameController::submit_impl releases the swapchain image as soon as both eyes have been submitted, whenever a swapchain exists. It does not check that an image was actually acquired. begin_frame, a few lines above, guards the very same release with self.image_acquired.

The unguarded release is reached when the first Submit arrives while no frame is begun:

  1. The first Submit finds no frame controller and calls initialize_real_session, which restarts the session for the game's texture.
  2. FrameController::new creates the swapchain from the texture info but acquires nothing (image_acquired: false).
  3. post_session_restart replays the previous frame state. For Waited it only calls maybe_wait_frame; begin_frame, the only place that acquires an image, does not run.
  4. The second eye is submitted, submit_impl calls release_image() on a swapchain with nothing acquired, and the openxr crate asserts.

In the usual implicit-timing flow the previous state is Begun, so begin_frame runs during the restart and acquires an image. That is why most games never see this.

Steps to reproduce

Without a headset, on main: apply only the test from this PR and run

cargo test --lib explicit_timing_submit_without_begun_frame

It sets explicit timing, calls WaitGetPoses, then submits both eyes without SubmitExplicitTimingData. It fails with the same message at the same place (openxr-0.21.1/src/swapchain.rs:107), right after "Received game texture, restarted session with new data".

With a headset: launch Serious Sam VR: TFE through xrizer; it panics before the menu.

One caveat: my logs from the game were at INFO level, so I have not confirmed that the game takes exactly this call sequence (explicit timing without SubmitExplicitTimingData). The test is a sequence that provably reaches the same unguarded release; the game's panic message, backtrace and position in the log match it.

The fix

Release only when an image was acquired, the same condition begin_frame uses:

if self.image_acquired
    && let Some(data) = self.swapchain_data.as_mut()
{
    trace!("releasing image");
    data.swapchain.release_image().unwrap();
}
self.image_acquired = false;

The frame that was submitted without being begun is dropped, as before: PostPresentHandoff skips it because the state is not Begun, and the next WaitGetPoses takes the existing "discard frame" path, where begin_frame acquires an image normally. The second half of the test checks that the following frame goes Begun -> Ended. Nothing changes for apps that begin their frames properly, since image_acquired is already true for them at this point.

With this change the game starts, reaches the menu and plays; I finished a level with it.

cargo test, cargo +nightly miri test, cargo fmt --check and cargo clippy --workspace --all-targets pass locally.

xrizer log from the crashing run (stock nightly, 0989a7f)
[2026-09-17T23:47:40.936 INFO  xrizer ThreadId(1)] Initializing XRizer version 0.5.0
[2026-09-17T23:47:40.936 INFO  xrizer::clientcore ThreadId(1)] Creating ClientCore version "IVRClientCore_003"
[2026-09-17T23:47:41.010 INFO  xrizer::openxr_data ThreadId(1)] Creating OpenXR session with graphics API openxr::graphics::vulkan::Vulkan
[2026-09-17T23:47:41.010 INFO  xrizer::openxr_data ThreadId(1)] New session created!
[2026-09-17T23:47:41.010 INFO  xrizer::openxr_data ThreadId(1)] OpenXR session state changed: READY
[2026-09-17T23:47:41.010 INFO  xrizer::openxr_data ThreadId(1)] Began OpenXR session.
[2026-09-17T23:47:43.150 INFO  xrizer::compositor ThreadId(1)] Creating real backend for texture type Vulkan
[2026-09-17T23:47:43.150 INFO  xrizer::openxr_data ThreadId(1)] OpenXR session state changed: SYNCHRONIZED
[2026-09-17T23:47:43.150 INFO  xrizer::openxr_data ThreadId(1)] OpenXR session state changed: STOPPING
[2026-09-17T23:47:43.150 INFO  xrizer::openxr_data ThreadId(1)] OpenXR session state changed: IDLE
[2026-09-17T23:47:43.150 INFO  xrizer::openxr_data ThreadId(1)] OpenXR session state changed: EXITING
[2026-09-17T23:47:43.151 INFO  xrizer::openxr_data ThreadId(1)] Creating OpenXR session with graphics API openxr::graphics::vulkan::Vulkan
[2026-09-17T23:47:43.151 INFO  xrizer::openxr_data ThreadId(1)] New session created!
[2026-09-17T23:47:43.151 INFO  xrizer::openxr_data ThreadId(1)] OpenXR session state changed: READY
[2026-09-17T23:47:43.151 INFO  xrizer::openxr_data ThreadId(1)] Began OpenXR session.
[2026-09-17T23:47:43.157 INFO  xrizer::compositor ThreadId(1)] Received game texture, restarted session with new data
[2026-09-17T23:47:43.158 ERROR xrizer ThreadId(1)] panicked at /cargo/registry/src/index.crates.io-1949cf8c6b5b557f/openxr-0.21.1/src/swapchain.rs:107:9:
wait_image must be called before release_image
[2026-09-17T23:47:43.158 ERROR xrizer ThreadId(1)] Backtrace: 
   0: xrizer::init_logging::{{closure}}::{{closure}}
   1: std::panicking::panic_with_hook
   2: std::panicking::panic_handler::{closure#0}
   3: std::sys::backtrace::__rust_end_short_backtrace::<std::panicking::panic_handler::{closure#0}, !>
   4: __rustc::rust_begin_unwind
   5: core::panicking::panic_fmt
   6: <xrizer::compositor::Compositor as openvr::bindings::vr::IVRCompositor029_Interface>::Submit
   7: openvr::bindings::vr::ivrcompositor021::Submit
   8: _Z20IVRCompositor_SubmitI33u_IVRCompositor_IVRCompositor_02145IVRCompositor_IVRCompositor_021_Submit_paramsEiPT_PT0_b.constprop.0.isra.0
   9: IVRCompositor_IVRCompositor_021_Submit
  10: __wine_unix_call_dispatcher
  11: server_init_process_done
  12: main
  13: __libc_start_call_main
  14: __libc_start_main@@GLIBC_2.34
  15: _start

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