Open Timbals opened 4 months ago
I've been playing around with various ideas on how to make the API safe. Here are some things I tried:
Initializing required fields in new
.
Doesn't allow checking that the correct session
is used.
Use an enum
and union
to represent the polymorphic structs.
This would require less validation because the enum can contain references to the high-level structs (e.g. xr::Space
instead of sys::Space
) and would allow validating the same-session requirement because the high-level structs contain that information.
Introduces run-time overhead and is awkward to use with slices (layers
in FrameStream::end
).
Use a const generic typestate pattern to ensure all required fields are initialized.
This feels over-engineered and doesn't allow checking that the correct session
is used.
No run-time overhead and mostly keeps the current API.
Change the relevant functions to unsafe
.
I'm currently leaning towards this being the best option. None of the "safe" variants seem to be worth the extra complexity/maintenance[^1]. Especially because the OpenXR API seems to allow a lot of local reasoning about validity (at least when compared to Vulkan)[^2].
But that's a question about the general direction of this crate. Should it be more like ash
and mark more things unsafe, or should it be more like vulkano
and start tracking a bunch of state?
@Ralith any thoughts?
[^1]: E.g. XR_META_recommended_layer_resolution
actually changes the validity requirement of composition layers because it allows NULL
swapchains to be passed to xrGetRecommendedLayerResolutionMETA
. I don't know how this could be integrated ergonomically.
[^2]: E.g. create_swapchain
requires face_count
to be 1
or 6
but we currently allow any u32
, additionally it requires that the corresponding graphic APIs limits are respected which would require implementing getting these limits for all graphics APIs
Thank you for this detailed investigation!
Initializing required fields in new.
I remain skeptical of adding arguments to new
. It seems difficult for users to predict which fields would be affected, and might lead to large argument lists. Inability to check some invariants is also unfortunate.
Use an enum and union to represent the polymorphic structs.
This is very interesting. I think this might provide the best ergonomics. Many openxrs
APIs already have similar marshaling to what this would require (e.g. anything involving strings). If anything, I think the existing "zero-cost" pattern is over-engineered and inconsistent with OpenXR and openxrs
conventions at large, so I am not concerned about overhead. It's only once a frame, after all.
As a minor nit, I'd also consider making each variant a unit tuple (enum CompositionLayer<'a, G> { Projection(CompositionLayerProjection<'a, G>), ... }
) and implementing From
. This might be a bit more convenient for downstream code to manipulate, e.g. perhaps when building up a layer through multiple functions.
Either way, generating this might take a bit of effort, but I think it's in the running for best ergonomics, which is my overriding concern (after soundness, obviously).
is awkward to use with slices
Sorry, why is that awkward? There's nothing wrong with a slice of an enum. Sure, we'd have to marshal into a separate allocation, but so what?
There's one other option to consider: a builder pattern. It might look something like:
impl<G> FrameStream {
fn end(&mut self) -> FrameBuilder<'_, G> { /* ... */ }
}
pub struct FrameBuilder<'a, G> {
session: &'a Session<G>,
layers: Vec<CompositionLayerRaw>,
}
impl<'a, G> FrameBuilder<'a, G> {
pub fn push<L: CompositionLayer>(&mut self, layer: L) -> Result<&mut Self> {
layer.validate(self.session)?;
self.layers.push(layer.into_raw());
Ok(self)
}
/// Calls `xrEndFrame` with the supplied composition layers
pub fn present(self) { /* ... */ }
}
/// Safety: `validate` must return `Ok` if and only if the result of `into_raw` can be safely passed to `xrEndFrame` for the supplied session
pub unsafe trait CompositionLayer {
/// Check whether `self` can be safely presented on `session`
fn validate<G>(&self, session: Session<G>) -> Result<()>;
fn into_raw(self) -> CompositionLayerRaw;
}
pub union CompositionLayerRaw { /* ... */ }
This is ergonomically similar to the enum case, but exposes a few more implementation details. One upside is that users can supply their own composition layers, but that's already possible by calling the raw xrEndFrame
function pointer yourself, so I don't know if it's much of an advantage. Probably not easier to generate either, so on balance I lean towards the enum approach, which reduces the API surface.
Change the relevant functions to unsafe.
This is strict improvement and I'd be happy to merge it.
But that's a question about the general direction of this crate. Should it be more like ash and mark more things unsafe, or should it be more like vulkano and start tracking a bunch of state?
The intent of this crate is to offer safe bindings, and it's largely successful in that so far. This is motivated by OpenXR's decision to prioritize safety itself, with most invariants being enforced dynamically by the implementation already, a sharp contrast to Vulkan where checks are almost never guaranteed. I'm willing to expose unsafe escape hatches when convenient, and to consider unsafe APIs if enforcing safety seems disproportionately expensive, but as you note, safety in OpenXR is generally easy to reason about, so I don't think it's likely to get out of hand the way vulkano
does.
Notes mostly for myself:
LayerFlags
for the composition layers should be explicitly initialized anyway)push_next
? (XrCompositionLayerAlphaBlendFB, XrCompositionLayerColorScaleBiasKHR, XrCompositionLayerDepthTestFB, XrCompositionLayerImageLayoutFB, XrCompositionLayerSecureContentFB, XrCompositionLayerSettingsFB)
next
, but the layers could reasonably want multiple things at the same time -> maybe implement the FrameBuilder
Haptic
enum because it clashes with the Haptic
marker struct, so the enum is named HapticData
samples_consumed: &mut u32
causes problems:
Copy
/Clone
because mutable references can't be copiedas_raw
can't be implemented because &self
gives a &&mut u32
which can't be de-referencedinto_raw
consuming self
. Works but requires always passing owned versions.from_raw
not possible to implement because going from just the handle to high-level struct is not possiblehow to
push_next
?
Extension structs could be represented with Option
fields in the enum scheme, perhaps?
The generator generated a bunch of builders for structs that aren't used. I modified the generator to not generate these. Only the builders for the composition layers and haptics are publicly exported. The other builders are only used internally.
Replaced builder structs with high-level enums/structs. Polymorphic structs got replaced with an enum. There's also a union for each enum that combines all the raw structs from the
sys
crate.I also marked
create_swapchain
as unsafe. Similar tocreate_session
, the interaction with the graphics API means the runtime isn't required to check all requirements.Fixes #163