cranpose-render-wgpu 0.1.14

WGPU renderer backend for Cranpose
Documentation
use crate::effect_renderer::CompositeSampleMode;

pub(crate) const MOTION_STABLE_SURFACE_SCALE_MULTIPLIER: f32 = 9.0;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum SurfaceRequirement {
    ExplicitOffscreen,
    RenderEffect,
    Backdrop,
    GroupOpacity,
    BlendMode,
    ShapeClip,
    ImmediateShadow,
    TextMaterialMask,
    MotionStableCapture,
    NonTranslationTransform,
    MixedDirectContent,
    PixelStableComposite,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub(crate) struct SurfaceRequirementSet {
    bits: u16,
}

impl SurfaceRequirementSet {
    const EXPLICIT_OFFSCREEN: u16 = 1 << 0;
    const RENDER_EFFECT: u16 = 1 << 1;
    const BACKDROP: u16 = 1 << 2;
    const GROUP_OPACITY: u16 = 1 << 3;
    const BLEND_MODE: u16 = 1 << 4;
    const SHAPE_CLIP: u16 = 1 << 5;
    const IMMEDIATE_SHADOW: u16 = 1 << 6;
    const TEXT_MATERIAL_MASK: u16 = 1 << 7;
    const MOTION_STABLE_CAPTURE: u16 = 1 << 8;
    const NON_TRANSLATION_TRANSFORM: u16 = 1 << 9;
    const MIXED_DIRECT_CONTENT: u16 = 1 << 10;
    const PIXEL_STABLE_COMPOSITE: u16 = 1 << 11;

    pub(crate) fn insert(&mut self, requirement: SurfaceRequirement) {
        self.bits |= Self::bit(requirement);
    }

    pub(crate) fn with(mut self, requirement: SurfaceRequirement) -> Self {
        self.insert(requirement);
        self
    }

    pub(crate) fn contains(self, requirement: SurfaceRequirement) -> bool {
        (self.bits & Self::bit(requirement)) != 0
    }

    pub(crate) fn has_isolating_requirement(self) -> bool {
        (self.bits
            & !(Self::MIXED_DIRECT_CONTENT | Self::IMMEDIATE_SHADOW | Self::PIXEL_STABLE_COMPOSITE))
            != 0
    }

    pub(crate) fn has_renderer_forced_surface(self) -> bool {
        self.contains(SurfaceRequirement::TextMaterialMask)
            || self.contains(SurfaceRequirement::NonTranslationTransform)
    }

    pub(crate) fn composite_requires_resampling(self) -> bool {
        self.contains(SurfaceRequirement::NonTranslationTransform)
            || !self.composite_preserves_raster_content()
    }

    pub(crate) fn composite_preserves_raster_content(self) -> bool {
        self.contains(SurfaceRequirement::MotionStableCapture)
            || self.contains(SurfaceRequirement::PixelStableComposite)
            || self.contains(SurfaceRequirement::ExplicitOffscreen)
            || self.contains(SurfaceRequirement::RenderEffect)
            || self.contains(SurfaceRequirement::Backdrop)
            || self.contains(SurfaceRequirement::GroupOpacity)
            || self.contains(SurfaceRequirement::BlendMode)
            || self.contains(SurfaceRequirement::ShapeClip)
    }

    #[cfg(test)]
    pub(crate) fn labels(self) -> impl Iterator<Item = &'static str> {
        const ORDERED: &[(SurfaceRequirement, &str)] = &[
            (SurfaceRequirement::ExplicitOffscreen, "explicit_offscreen"),
            (SurfaceRequirement::RenderEffect, "render_effect"),
            (SurfaceRequirement::Backdrop, "backdrop"),
            (SurfaceRequirement::GroupOpacity, "group_opacity"),
            (SurfaceRequirement::BlendMode, "blend_mode"),
            (SurfaceRequirement::ShapeClip, "shape_clip"),
            (SurfaceRequirement::ImmediateShadow, "immediate_shadow"),
            (SurfaceRequirement::TextMaterialMask, "text_material_mask"),
            (
                SurfaceRequirement::MotionStableCapture,
                "motion_stable_capture",
            ),
            (
                SurfaceRequirement::NonTranslationTransform,
                "non_translation_transform",
            ),
            (
                SurfaceRequirement::MixedDirectContent,
                "mixed_direct_content",
            ),
            (
                SurfaceRequirement::PixelStableComposite,
                "pixel_stable_composite",
            ),
        ];

        ORDERED
            .iter()
            .filter(move |(requirement, _)| self.contains(*requirement))
            .map(|(_, label)| *label)
    }

    #[cfg(test)]
    pub(crate) fn display(self) -> String {
        let mut joined = String::new();
        for (index, label) in self.labels().enumerate() {
            if index > 0 {
                joined.push('+');
            }
            joined.push_str(label);
        }
        if joined.is_empty() {
            joined.push_str("none");
        }
        joined
    }

    pub(crate) fn composite_sample_mode(self) -> CompositeSampleMode {
        if self.composite_requires_resampling() {
            CompositeSampleMode::Linear
        } else {
            CompositeSampleMode::Box4
        }
    }

    pub(crate) fn target_scale(self, root_scale: f32) -> f32 {
        if self.contains(SurfaceRequirement::MotionStableCapture) {
            root_scale * MOTION_STABLE_SURFACE_SCALE_MULTIPLIER
        } else {
            root_scale
        }
    }

    fn bit(requirement: SurfaceRequirement) -> u16 {
        match requirement {
            SurfaceRequirement::ExplicitOffscreen => Self::EXPLICIT_OFFSCREEN,
            SurfaceRequirement::RenderEffect => Self::RENDER_EFFECT,
            SurfaceRequirement::Backdrop => Self::BACKDROP,
            SurfaceRequirement::GroupOpacity => Self::GROUP_OPACITY,
            SurfaceRequirement::BlendMode => Self::BLEND_MODE,
            SurfaceRequirement::ShapeClip => Self::SHAPE_CLIP,
            SurfaceRequirement::ImmediateShadow => Self::IMMEDIATE_SHADOW,
            SurfaceRequirement::TextMaterialMask => Self::TEXT_MATERIAL_MASK,
            SurfaceRequirement::MotionStableCapture => Self::MOTION_STABLE_CAPTURE,
            SurfaceRequirement::NonTranslationTransform => Self::NON_TRANSLATION_TRANSFORM,
            SurfaceRequirement::MixedDirectContent => Self::MIXED_DIRECT_CONTENT,
            SurfaceRequirement::PixelStableComposite => Self::PIXEL_STABLE_COMPOSITE,
        }
    }
}

impl FromIterator<SurfaceRequirement> for SurfaceRequirementSet {
    fn from_iter<T: IntoIterator<Item = SurfaceRequirement>>(iter: T) -> Self {
        let mut requirements = Self::default();
        for requirement in iter {
            requirements.insert(requirement);
        }
        requirements
    }
}

#[cfg(test)]
mod tests {
    use crate::effect_renderer::CompositeSampleMode;

    use super::{SurfaceRequirement, SurfaceRequirementSet};

    #[test]
    fn labels_and_display_follow_requirement_order() {
        let requirements = SurfaceRequirementSet::default()
            .with(SurfaceRequirement::ImmediateShadow)
            .with(SurfaceRequirement::MixedDirectContent);

        assert_eq!(
            requirements.labels().collect::<Vec<_>>(),
            vec!["immediate_shadow", "mixed_direct_content"]
        );
        assert_eq!(
            requirements.display(),
            "immediate_shadow+mixed_direct_content"
        );
    }

    #[test]
    fn display_reports_none_for_empty_set() {
        assert_eq!(SurfaceRequirementSet::default().display(), "none");
    }

    #[test]
    fn immediate_shadow_is_ordered_draw_work_not_layer_isolation() {
        let requirements =
            SurfaceRequirementSet::default().with(SurfaceRequirement::ImmediateShadow);

        assert!(!requirements.has_isolating_requirement());
        assert!(!requirements.has_renderer_forced_surface());
    }

    #[test]
    fn pixel_stable_composite_uses_box4_without_forcing_surface() {
        let requirements =
            SurfaceRequirementSet::default().with(SurfaceRequirement::PixelStableComposite);

        assert!(!requirements.has_isolating_requirement());
        assert!(!requirements.has_renderer_forced_surface());
        assert_eq!(
            requirements.composite_sample_mode(),
            CompositeSampleMode::Box4
        );
        assert_eq!(requirements.target_scale(3.0), 3.0);
    }

    #[test]
    fn translation_only_layer_surfaces_composite_without_resampling() {
        for requirement in [
            SurfaceRequirement::ExplicitOffscreen,
            SurfaceRequirement::RenderEffect,
            SurfaceRequirement::Backdrop,
            SurfaceRequirement::GroupOpacity,
            SurfaceRequirement::BlendMode,
            SurfaceRequirement::MotionStableCapture,
        ] {
            let requirements = SurfaceRequirementSet::default().with(requirement);

            assert_eq!(
                requirements.composite_sample_mode(),
                CompositeSampleMode::Box4,
                "{requirement:?} should preserve pixels when the layer transform is pure translation"
            );
            assert!(!requirements.composite_requires_resampling());
        }
    }

    #[test]
    fn text_material_mask_intermediate_uses_linear_sampling() {
        let requirements =
            SurfaceRequirementSet::default().with(SurfaceRequirement::TextMaterialMask);

        assert_eq!(
            requirements.composite_sample_mode(),
            CompositeSampleMode::Linear
        );
        assert!(requirements.composite_requires_resampling());
    }

    #[test]
    fn motion_stable_text_material_mask_uses_box4_sampling() {
        let requirements = SurfaceRequirementSet::default()
            .with(SurfaceRequirement::TextMaterialMask)
            .with(SurfaceRequirement::MotionStableCapture);

        assert_eq!(
            requirements.composite_sample_mode(),
            CompositeSampleMode::Box4
        );
        assert!(!requirements.composite_requires_resampling());
    }

    #[test]
    fn non_translation_transform_uses_linear_sampling() {
        let requirements = SurfaceRequirementSet::default()
            .with(SurfaceRequirement::ExplicitOffscreen)
            .with(SurfaceRequirement::NonTranslationTransform);

        assert_eq!(
            requirements.composite_sample_mode(),
            CompositeSampleMode::Linear
        );
        assert!(requirements.composite_requires_resampling());
    }
}