Skip to main content

impeller_hal/
capabilities.rs

1//! What a device can actually do.
2//!
3//! Everything above the HAL branches on these values, never on backend
4//! identity. A Mali GPU without `EGL_ANDROID_native_fence_sync` and a desktop
5//! Vulkan driver without `VK_KHR_external_fence_fd` present the same problem,
6//! and code that asks "is this GLES?" instead of "can this export a fence?"
7//! gets both cases wrong.
8
9use crate::format::FormatModifierSet;
10
11/// Supported MSAA sample counts, as a bitmask of powers of two.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct SampleCounts(u32);
14
15impl SampleCounts {
16    pub const fn from_mask(mask: u32) -> Self {
17        Self(mask)
18    }
19
20    /// Whether `count` is supported. Non-power-of-two counts are never valid.
21    pub const fn supports(self, count: u32) -> bool {
22        count.is_power_of_two() && (self.0 & count) != 0
23    }
24
25    /// The highest supported count, or 1 when only single-sampled rendering
26    /// is available.
27    pub const fn max(self) -> u32 {
28        if self.0 == 0 {
29            1
30        } else {
31            // Highest set bit.
32            1 << (u32::BITS - 1 - self.0.leading_zeros())
33        }
34    }
35}
36
37/// How a device can move images across process or device boundaries.
38///
39/// Both halves matter independently. A render-only GPU paired with a separate
40/// display controller — the common ARM SoC topology — needs export on one
41/// device and import on the other.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub struct DmaBufSupport {
44    /// Can import a dma-buf as a texture.
45    pub import: bool,
46    /// Can export one of its own images as a dma-buf.
47    pub export: bool,
48    /// Can allocate and import with an explicit format modifier.
49    ///
50    /// Without this, the only safe shared layout is linear, and scanout gives
51    /// up whatever bandwidth a vendor tiled or compressed layout would have
52    /// saved.
53    pub modifiers: bool,
54}
55
56impl DmaBufSupport {
57    /// Whether this device can allocate its own scanout buffers.
58    ///
59    /// When false, the DRM presentation path must allocate through GBM and
60    /// import instead. Both paths are supported; this is what picks between
61    /// them.
62    pub const fn can_allocate_scanout(self) -> bool {
63        self.export && self.modifiers
64    }
65}
66
67/// Whether GPU completion can be handed to another component as a fence.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub struct SyncSupport {
70    /// Can export a `sync_file` fd from a fence, for use as `IN_FENCE_FD` on
71    /// an atomic commit.
72    pub export_sync_file: bool,
73    /// Can import a `sync_file` fd and wait on it on the GPU timeline.
74    pub import_sync_file: bool,
75}
76
77impl SyncSupport {
78    /// Whether the frame loop can stay fully explicit on the DRM path.
79    ///
80    /// When false, the DRM target must wait on the CPU before committing.
81    /// That is correct but slower, and on Tier-1 hardware it is a driver bug
82    /// to chase rather than a state to settle into, so callers are expected
83    /// to log it loudly.
84    pub const fn supports_explicit_scanout(self) -> bool {
85        self.export_sync_file
86    }
87}
88
89/// Everything the layers above the HAL are allowed to branch on.
90#[derive(Debug, Clone, Default)]
91pub struct Capabilities {
92    /// Maximum width or height of a 2D texture.
93    pub max_texture_size: u32,
94    /// Supported MSAA sample counts for render targets.
95    pub sample_counts: SampleCounts,
96    /// Cross-device image sharing.
97    pub dma_buf: DmaBufSupport,
98    /// Cross-component synchronization.
99    pub sync: SyncSupport,
100    /// Whether the separable blend modes are available.
101    ///
102    /// These need a hardware extension and cannot be emulated with blend
103    /// factors, so a device without it refuses [`BlendMode::is_advanced`] modes
104    /// rather than substituting the nearest expressible one. Callers check this
105    /// before using one; nothing branches on which backend is in play.
106    ///
107    /// [`BlendMode::is_advanced`]: crate::BlendMode::is_advanced
108    pub advanced_blend: bool,
109    /// Whether a floating-point color attachment can be rendered into.
110    ///
111    /// A color outside the sRGB primaries' triangle has a component outside
112    /// zero to one, and `Rgba16Float` is the only format here that can hold
113    /// one. Whether a device will let it be a target is a separate question
114    /// from whether it will sample one: half-float is filterable in core ES
115    /// 3.0 and renderable only with an extension, and plenty of shipping
116    /// drivers have the first and not the second. Vulkan answers per format
117    /// from its format properties.
118    ///
119    /// Distinct from [`Self::render_formats`], which is the scanout list keyed
120    /// by DRM fourcc and is empty on GLES. A format with no fourcc is
121    /// deliberately absent from that list, so it cannot answer this.
122    pub float_render_targets: bool,
123    /// Formats and layouts this device can render into and export.
124    ///
125    /// One half of format negotiation; the presentation target supplies the
126    /// other.
127    pub render_formats: Vec<FormatModifierSet>,
128    /// Human-readable device and driver identification, for report
129    /// fingerprints and bug reports.
130    pub device_name: String,
131    pub driver_name: String,
132    /// Whether rendering happens on the CPU rather than on a GPU.
133    ///
134    /// Not a performance hint. It marks the device properties that are
135    /// consequences of having no graphics hardware rather than defects: a CPU
136    /// rasterizer has no tiling to describe, so advertising only a linear
137    /// layout is the correct answer for it and a sign of a missing modifier
138    /// query on anything else. A test that cannot tell those apart has to
139    /// choose between failing on software and not checking hardware, and both
140    /// are worse than asking.
141    ///
142    /// Vulkan takes this from the device type, which is authoritative. GLES has
143    /// no equivalent query and it is recognized from the renderer string, which
144    /// is not; a software implementation this does not know the name of reports
145    /// false, so treat a true as reliable and a false as merely unremarkable.
146    pub software: bool,
147}
148
149impl Capabilities {
150    /// Whether this device can drive a KMS plane directly, by either
151    /// allocation strategy.
152    ///
153    /// Note this says nothing about sync: a device can be scanout-capable and
154    /// still lack fence export, in which case the path works with a CPU wait.
155    pub fn supports_scanout(&self) -> bool {
156        self.dma_buf.can_allocate_scanout() || self.dma_buf.import
157    }
158
159    /// Whether every blend mode a batch uses is available on this device.
160    ///
161    /// Lives here rather than in each backend so the two refuse the same batch
162    /// for the same reason: a Vulkan device without the advanced-blend
163    /// extension and a GLES context without it are the same problem, and a
164    /// check written twice is a check that eventually disagrees with itself.
165    /// Refusing is deliberate — the alternative is substituting the nearest
166    /// expressible mode, which produces a picture nobody can debug from.
167    pub fn check_blend_modes(&self, batch: &crate::Batch) -> crate::Result<()> {
168        if !self.advanced_blend && batch.draws().iter().any(|draw| draw.blend.is_advanced()) {
169            return Err(crate::Error::Unsupported(
170                "advanced blend modes; this device has no advanced-blend extension",
171            ));
172        }
173        Ok(())
174    }
175
176    /// Whether this device can create the texture a descriptor asks for.
177    ///
178    /// Here rather than in each backend for the reason
179    /// [`Self::check_blend_modes`] is: two backends refusing the same thing for
180    /// the same reason, in one place, rather than a check written twice that
181    /// eventually disagrees with itself. Without it the failure arrives as a
182    /// framebuffer-incomplete number on one backend and a driver error on the
183    /// other, neither of which names what was missing.
184    pub fn check_texture(&self, desc: &crate::TextureDescriptor) -> crate::Result<()> {
185        // Refused for every device rather than for some, because this is not a
186        // capability: the pipeline carries sRGB-encoded components, so a target
187        // that applies the transfer on write applies it twice and the picture
188        // comes back over a third too bright. See `PixelFormat::is_drawable`.
189        //
190        // Only a render target. Sampling one decodes, which is also not what an
191        // encoded pipeline wants, but a caller may have data this is right for
192        // and `Context::create_image` says which format a picture wants. Drawing
193        // into one has no reading under which it is correct.
194        if desc.usage.render_target && !desc.format.is_drawable() {
195            return Err(crate::Error::Unsupported(
196                "an sRGB render target; this pipeline already holds encoded \
197                 color and the format would encode it a second time",
198            ));
199        }
200        if desc.usage.render_target
201            && desc.format == crate::PixelFormat::Rgba16Float
202            && !self.float_render_targets
203        {
204            return Err(crate::Error::Unsupported(
205                "a floating-point render target; this device can sample one but not draw into it",
206            ));
207        }
208        Ok(())
209    }
210
211    /// Whether an extent fits within the device's texture limit.
212    pub fn can_allocate(&self, extent: crate::format::Extent2D) -> bool {
213        extent.width <= self.max_texture_size && extent.height <= self.max_texture_size
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::format::Extent2D;
221
222    #[test]
223    fn sample_counts_reject_non_powers_of_two() {
224        let counts = SampleCounts::from_mask(1 | 2 | 4);
225        assert!(counts.supports(1));
226        assert!(counts.supports(4));
227        assert!(!counts.supports(8));
228        // 3 has bits in common with the mask but is not a valid count.
229        assert!(!counts.supports(3));
230    }
231
232    #[test]
233    fn sample_counts_report_max_and_degrade_to_single_sampled() {
234        assert_eq!(SampleCounts::from_mask(1 | 2 | 4).max(), 4);
235        assert_eq!(SampleCounts::default().max(), 1);
236    }
237
238    #[test]
239    fn self_allocation_requires_both_export_and_modifiers() {
240        let full = DmaBufSupport {
241            import: true,
242            export: true,
243            modifiers: true,
244        };
245        assert!(full.can_allocate_scanout());
246
247        // Export without modifier support cannot negotiate a scanout-capable
248        // layout, so allocation has to go through GBM instead.
249        let no_modifiers = DmaBufSupport {
250            modifiers: false,
251            ..full
252        };
253        assert!(!no_modifiers.can_allocate_scanout());
254    }
255
256    #[test]
257    fn import_only_devices_still_support_scanout() {
258        // The GBM-allocated path: cannot allocate its own scanout buffers but
259        // can import ones GBM made.
260        let caps = Capabilities {
261            dma_buf: DmaBufSupport {
262                import: true,
263                export: false,
264                modifiers: false,
265            },
266            ..Default::default()
267        };
268        assert!(!caps.dma_buf.can_allocate_scanout());
269        assert!(caps.supports_scanout());
270    }
271
272    #[test]
273    fn scanout_capability_is_independent_of_fence_export() {
274        let caps = Capabilities {
275            dma_buf: DmaBufSupport {
276                import: true,
277                export: true,
278                modifiers: true,
279            },
280            sync: SyncSupport::default(),
281            ..Default::default()
282        };
283        // Scanout works; it just cannot stay explicit, which is the caller's
284        // cue to log the CPU-wait fallback rather than to disable the path.
285        assert!(caps.supports_scanout());
286        assert!(!caps.sync.supports_explicit_scanout());
287    }
288
289    #[test]
290    fn allocation_limit_is_checked_on_both_axes() {
291        let caps = Capabilities {
292            max_texture_size: 4096,
293            ..Default::default()
294        };
295        assert!(caps.can_allocate(Extent2D::new(4096, 4096)));
296        assert!(!caps.can_allocate(Extent2D::new(4097, 16)));
297        assert!(!caps.can_allocate(Extent2D::new(16, 4097)));
298    }
299
300    /// Sampling a half-float texture and drawing into one are separate
301    /// permissions, and a device may offer the first without the second.
302    #[test]
303    fn a_float_target_is_refused_where_only_sampling_one_is_offered() {
304        let mut caps = Capabilities {
305            float_render_targets: false,
306            ..Capabilities::default()
307        };
308        let extent = Extent2D::new(4, 4);
309
310        let target = crate::TextureDescriptor::offscreen(extent, crate::PixelFormat::Rgba16Float);
311        assert!(
312            caps.check_texture(&target).is_err(),
313            "a float attachment was allowed where the device offers none"
314        );
315
316        // Sampling one is a different request and is not refused, which is the
317        // whole reason the two are separate: a gradient ramp wants exactly this
318        // and would otherwise be unavailable on the same devices.
319        let sampled = crate::TextureDescriptor::sampled(extent, crate::PixelFormat::Rgba16Float);
320        assert!(caps.check_texture(&sampled).is_ok());
321
322        // And an eight-bit target is never the question.
323        let ordinary = crate::TextureDescriptor::offscreen(extent, crate::PixelFormat::Rgba8Unorm);
324        assert!(caps.check_texture(&ordinary).is_ok());
325
326        caps.float_render_targets = true;
327        assert!(caps.check_texture(&target).is_ok());
328    }
329}