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