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}