Skip to content

Integrate libmonado and use it to pass battery status#229

Open
jvyden wants to merge 6 commits intoSupreeeme:mainfrom
jvyden:libmonado
Open

Integrate libmonado and use it to pass battery status#229
jvyden wants to merge 6 commits intoSupreeeme:mainfrom
jvyden:libmonado

Conversation

@jvyden
Copy link

@jvyden jvyden commented Nov 13, 2025

Closes #26.

Summary

This PR integrates the libmonado library through the libmonado-rs crate, the same crate wlx-overlay-s and others use. I've integrated it as an optional (but enabled by default) feature, with the ability for it to be either entirely compiled out or be silently unavailable on startup if we can't find Monado.

As a demonstration of usage of the new library, I've also integrated the DeviceIsCharging_Bool, DeviceProvidesBatteryStatus_Bool, and DeviceBatteryPercentage_Float tracked properties to allow games to observe device battery status.

Testing

I've tested this on WiVRn and it seems to work well for headset battery. I do not have a headset that reports controller battery through OpenXR, so I had a friend test it on their Index and it also worked.

In both cases, I've tested in Resonite using the game's built-in scripting language to observe the values reported directly.

Additionally, on WiVRn you need to specify LIBMONADO_PATH before xrizer can connect to Monado from within Proton's container. This seems to work fine on Monado for a reason I'm not sure why, but I've made an issue to track a workaround in libmonado-rs: technobaboo/libmonado-rs#24

Notes

  • This is my first time integrating a feature like this into a Rust project, and I'm not sure if I'm initializing libmonado in the correct place. Guidance here and with code quality would be appreciated as I'm not familiar with Rust.
  • I'm not entirely certain that libmonado is thread-safe, but over the past 3-4 days I've had no issues in release builds with wrapping the library context in a Send/Sync.
  • Unsure how to integrate unit tests into this. Should I not care about this since the bulk of these features are in other projects?

Comment on lines +813 to +815
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::UnknownProperty;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::UnknownProperty;
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think what we actually want here is ValueNotProvidedByDevice?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did so in 820fc0e but lmk if you want still want UnknownProperty by default

Copy link
Contributor

@ImSapphire ImSapphire Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it probably doesn't matter in practice, but I don't think returning an error when the battery isn't available makes sense for this property, unsure what SteamVR does

Comment on lines +751 to +765
let Some(d) = self.mnd_get_device_battery(device_index) else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return 0.0;
};

if !d.present {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return 0.0;
}

return d.charge;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let Some(d) = self.mnd_get_device_battery(device_index) else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return 0.0;
};
if !d.present {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return 0.0;
}
return d.charge;
let battery = self.mnd_get_device_battery(device_index);
if let Some(charge) = battery.and_then(|b| b.present.then_some(b.charge)) {
return charge;
} else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return 0.0;
}

Comment on lines +807 to +814
let Some(d) = self.mnd_get_device_battery(device_index) else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
};

return d.present;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let Some(d) = self.mnd_get_device_battery(device_index) else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
};
return d.present;
if let Some(d) = self.mnd_get_device_battery(device_index) {
return d.present;
} else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
};

Comment on lines +818 to +832
let Some(d) = self.mnd_get_device_battery(device_index) else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
};

if !d.present {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
}

return d.charging;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let Some(d) = self.mnd_get_device_battery(device_index) else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
};
if !d.present {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
}
return d.charging;
let battery = self.mnd_get_device_battery(device_index);
if let Some(charging) = battery.and_then(|b| b.present.then_some(b.charging)) {
return charging;
} else {
if let Some(err) = unsafe { err.as_mut() } {
*err = vr::ETrackedPropertyError::ValueNotProvidedByDevice;
}
return false;
}

}

#[cfg(feature = "monado")]
pub fn mnd_get_device_battery(&self, device_index: u32) -> Option<BatteryStatus> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn mnd_get_device_battery(&self, device_index: u32) -> Option<BatteryStatus> {
fn mnd_get_device_battery(&self, device_index: u32) -> Option<BatteryStatus> {

Comment on lines +10 to +11
unsafe impl Send for SafeMonado {}
unsafe impl Sync for SafeMonado {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a comment explaining why it's done

@galister
Copy link
Contributor

galister commented Nov 20, 2025

It's important to know that libmonado uses a separate IPC session to communicate with Monado, essentially the same as what libopenxr_monado does.

There are 2 major issues with this:

  • Monado has a low-low IPC client limit of 8. Once the limit is hit, it's not possible to launch more XR client applications. I have already ran into issues with just tools like Motoc and Wlx, and have ended up bumping the limit on my local build to 16.
  • While libmonado's IPC session counts as a headless session, there's a shortcoming in Monado that makes it create a full compositor object even for headless sessions. Due to this, these IPC sessions may not be so lightweight as they seem. Edit: this only happens with MND_HEADLESS, not with libmonado.

There is an unpublished extension EXT_battery_state_display, currently offered only by SteamVR. I would personally consider waiting for that to be finalized, as that would allow us to have a less intrusive solution.

@ImSapphire
Copy link
Contributor

Monado has a low-low IPC client limit of 8. Once the limit is hit, it's not possible to launch more XR client applications. I have already ran into issues with just tools like Motoc and Wlx, and have ended up bumping the limit on my local build to 16.

Maybe we should have this bumped upstream?

While libmonado's IPC session counts as a headless session, there's a shortcoming in Monado that makes it create a full compositor object even for headless sessions. Due to this, these IPC sessions may not be so lightweight as they seem. Edit: this only happens with MND_HEADLESS, not with libmonado.

This should be fixed in https://gitlab.freedesktop.org/monado/monado/-/merge_requests/2606

@galister
Copy link
Contributor

Monado IPC client increase: https://gitlab.freedesktop.org/monado/monado/-/merge_requests/2644

@SpookySkeletons
Copy link
Contributor

Monado IPC client increase: https://gitlab.freedesktop.org/monado/monado/-/merge_requests/2644

Now merged.

@SpookySkeletons
Copy link
Contributor

Mind giving this a rebase?

@jvyden
Copy link
Author

jvyden commented Jan 15, 2026

Sure, sorry, forgot about this branch. I should have this up to date tomorrow.

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.

Add optional libmonado data handler as supplement

5 participants