Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_shader/src/shader_cache.rs
9296 views
1
use crate::shader::*;
2
use alloc::sync::Arc;
3
use bevy_asset::AssetId;
4
use bevy_platform::collections::{hash_map::EntryRef, HashMap, HashSet};
5
use core::hash::Hash;
6
use naga::valid::Capabilities;
7
use thiserror::Error;
8
use tracing::debug;
9
use wgpu_types::{DownlevelFlags, Features};
10
11
/// Fully composed source code of a shader module, with all shader defs applied.
12
///
13
/// This is roughly equivalent to [`wgpu::ShaderSource`](https://docs.rs/wgpu/latest/wgpu/enum.ShaderSource.html),
14
/// but with less variants and more concrete types instead of [`Cow`](alloc::borrow::Cow).
15
///
16
/// This source will be parsed and validated by the renderer.
17
///
18
/// Any necessary shader translation (e.g. from WGSL to SPIR-V or vice versa)
19
/// must be done internally by the renderer.
20
#[cfg_attr(
21
not(feature = "decoupled_naga"),
22
expect(
23
clippy::large_enum_variant,
24
reason = "naga modules are the most common use, and are large"
25
)
26
)]
27
#[derive(Clone, Debug)]
28
pub enum ShaderCacheSource<'a> {
29
/// SPIR-V module represented as a slice of words.
30
SpirV(&'a [u8]),
31
/// WGSL module as a string slice.
32
Wgsl(String),
33
/// Naga module.
34
#[cfg(not(feature = "decoupled_naga"))]
35
Naga(naga::Module),
36
}
37
38
/// An id of a pipeline, typically in the [`PipelineCache`](https://docs.rs/bevy/latest/bevy/render/render_resource/struct.PipelineCache.html)
39
/// Typically corresponds to a unique combination of [`Shader`] and [`ShaderDefVal`]s.
40
pub type CachedPipelineId = usize;
41
42
struct ShaderData<ShaderModule> {
43
pipelines: HashSet<CachedPipelineId>,
44
processed_shaders: HashMap<Box<[ShaderDefVal]>, Arc<ShaderModule>>,
45
resolved_imports: HashMap<ShaderImport, AssetId<Shader>>,
46
dependents: HashSet<AssetId<Shader>>,
47
}
48
49
impl<T> Default for ShaderData<T> {
50
fn default() -> Self {
51
Self {
52
pipelines: Default::default(),
53
processed_shaders: Default::default(),
54
resolved_imports: Default::default(),
55
dependents: Default::default(),
56
}
57
}
58
}
59
60
/// A cache for shaders and shader imports, with asset state-tracking for
61
/// waiting to load shaders until all imports are resolved.
62
///
63
/// Note that the `RenderDevice` generic parameter is a means by which
64
/// to avoid a cyclic dependency with `bevy_render`, while also permitting
65
/// alternative rendering implementations. The actual processing of the
66
/// shader source into a usable compiled module is left to the renderer.
67
pub struct ShaderCache<ShaderModule, RenderDevice> {
68
device: RenderDevice,
69
data: HashMap<AssetId<Shader>, ShaderData<ShaderModule>>,
70
load_module: fn(
71
&RenderDevice,
72
ShaderCacheSource,
73
&ValidateShader,
74
) -> Result<ShaderModule, ShaderCacheError>,
75
#[cfg(feature = "shader_format_wesl")]
76
module_path_to_asset_id: HashMap<wesl::syntax::ModulePath, AssetId<Shader>>,
77
shaders: HashMap<AssetId<Shader>, Shader>,
78
import_path_shaders: HashMap<ShaderImport, AssetId<Shader>>,
79
waiting_on_import: HashMap<ShaderImport, Vec<AssetId<Shader>>>,
80
// The naga composer is only public for providing error messages and should not be touched.
81
#[doc(hidden)]
82
pub composer: naga_oil::compose::Composer,
83
}
84
85
/// A compile time shader value definition to be inlined into the shader source.
86
/// Variant tuples contain the name of the definition, and the value.
87
#[expect(missing_docs, reason = "Enum variants are self-explanatory")]
88
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, Debug, Hash)]
89
pub enum ShaderDefVal {
90
Bool(String, bool),
91
Int(String, i32),
92
UInt(String, u32),
93
}
94
95
impl From<&str> for ShaderDefVal {
96
fn from(key: &str) -> Self {
97
ShaderDefVal::Bool(key.to_string(), true)
98
}
99
}
100
101
impl From<String> for ShaderDefVal {
102
fn from(key: String) -> Self {
103
ShaderDefVal::Bool(key, true)
104
}
105
}
106
107
impl ShaderDefVal {
108
/// Returns the value of the define as a string.
109
pub fn value_as_string(&self) -> String {
110
match self {
111
ShaderDefVal::Bool(_, def) => def.to_string(),
112
ShaderDefVal::Int(_, def) => def.to_string(),
113
ShaderDefVal::UInt(_, def) => def.to_string(),
114
}
115
}
116
}
117
118
impl<ShaderModule, RenderDevice> ShaderCache<ShaderModule, RenderDevice> {
119
/// Creates a new [`ShaderCache`] with the given features and shader
120
/// module loading function. `load_module` is responsible for actually
121
/// compiling shader source into a module usable by the render device.
122
pub fn new(
123
device: RenderDevice,
124
features: Features,
125
downlevel: DownlevelFlags,
126
load_module: fn(
127
&RenderDevice,
128
ShaderCacheSource,
129
&ValidateShader,
130
) -> Result<ShaderModule, ShaderCacheError>,
131
) -> Self {
132
let capabilities = get_capabilities(features, downlevel);
133
#[cfg(debug_assertions)]
134
let composer = naga_oil::compose::Composer::default();
135
#[cfg(not(debug_assertions))]
136
let composer = naga_oil::compose::Composer::non_validating();
137
138
let composer = composer.with_capabilities(capabilities);
139
140
Self {
141
device,
142
composer,
143
load_module,
144
data: Default::default(),
145
#[cfg(feature = "shader_format_wesl")]
146
module_path_to_asset_id: Default::default(),
147
shaders: Default::default(),
148
import_path_shaders: Default::default(),
149
waiting_on_import: Default::default(),
150
}
151
}
152
153
fn add_import_to_composer(
154
composer: &mut naga_oil::compose::Composer,
155
import_path_shaders: &HashMap<ShaderImport, AssetId<Shader>>,
156
shaders: &HashMap<AssetId<Shader>, Shader>,
157
import: &ShaderImport,
158
) -> Result<(), ShaderCacheError> {
159
// Early out if we've already imported this module
160
if composer.contains_module(&import.module_name()) {
161
return Ok(());
162
}
163
164
// Check if the import is available (this handles the recursive import case)
165
let shader = import_path_shaders
166
.get(import)
167
.and_then(|handle| shaders.get(handle))
168
.ok_or(ShaderCacheError::ShaderImportNotYetAvailable)?;
169
170
// Recurse down to ensure all import dependencies are met
171
for import in &shader.imports {
172
Self::add_import_to_composer(composer, import_path_shaders, shaders, import)?;
173
}
174
175
composer
176
.add_composable_module(shader.into())
177
.map_err(Box::new)?;
178
// if we fail to add a module the composer will tell us what is missing
179
180
Ok(())
181
}
182
183
/// Attempts to retrieve or create a compiled shader module for the given
184
/// shader id and shader definitions.
185
///
186
/// The provided `pipeline` is tracked so it may later be reported "dirty"
187
/// when a shader is removed or replaced.
188
///
189
/// Note that the cache is keyed by `id` and `shader_defs`, meaning providing
190
/// the same `shader_defs` in a different order, or with redundancies, will
191
/// not result in cache hits, and thus require re-composing the module and
192
/// calling `load_module` again.
193
pub fn get(
194
&mut self,
195
pipeline: CachedPipelineId,
196
id: AssetId<Shader>,
197
shader_defs: &[ShaderDefVal],
198
) -> Result<Arc<ShaderModule>, ShaderCacheError> {
199
let shader = self
200
.shaders
201
.get(&id)
202
.ok_or(ShaderCacheError::ShaderNotLoaded(id))?;
203
204
let data = self.data.entry(id).or_default();
205
let n_asset_imports = shader
206
.imports
207
.iter()
208
.filter(|import| matches!(import, ShaderImport::AssetPath(_)))
209
.count();
210
let n_resolved_asset_imports = data
211
.resolved_imports
212
.keys()
213
.filter(|import| matches!(import, ShaderImport::AssetPath(_)))
214
.count();
215
if n_asset_imports != n_resolved_asset_imports {
216
return Err(ShaderCacheError::ShaderImportNotYetAvailable);
217
}
218
219
data.pipelines.insert(pipeline);
220
221
let module = match data.processed_shaders.entry_ref(shader_defs) {
222
EntryRef::Occupied(entry) => entry.into_mut(),
223
EntryRef::Vacant(entry) => {
224
debug!(
225
"processing shader {}, with shader defs {:?}",
226
id, shader_defs
227
);
228
let shader_source = match &shader.source {
229
Source::SpirV(data) => ShaderCacheSource::SpirV(data.as_ref()),
230
#[cfg(feature = "shader_format_wesl")]
231
Source::Wesl(_) => {
232
if let ShaderImport::AssetPath(path) = &shader.import_path {
233
let shader_resolver =
234
ShaderResolver::new(&self.module_path_to_asset_id, &self.shaders);
235
let module_path = wesl::syntax::ModulePath::from_path(path);
236
let mut compiler_options = wesl::CompileOptions {
237
imports: true,
238
condcomp: true,
239
lower: true,
240
..Default::default()
241
};
242
243
for shader_def in shader_defs {
244
match shader_def {
245
ShaderDefVal::Bool(key, value) => {
246
compiler_options.features.flags.insert(key.clone(), (*value).into());
247
}
248
_ => debug!(
249
"ShaderDefVal::Int and ShaderDefVal::UInt are not supported in wesl",
250
),
251
}
252
}
253
254
let compiled = wesl::compile(
255
&module_path,
256
&shader_resolver,
257
&wesl::EscapeMangler,
258
&compiler_options,
259
)
260
.unwrap();
261
262
ShaderCacheSource::Wgsl(compiled.to_string())
263
} else {
264
panic!("Wesl shaders must be imported from a file");
265
}
266
}
267
_ => {
268
for import in shader.imports.iter() {
269
Self::add_import_to_composer(
270
&mut self.composer,
271
&self.import_path_shaders,
272
&self.shaders,
273
import,
274
)?;
275
}
276
277
let shader_defs = shader_defs
278
.iter()
279
.chain(shader.shader_defs.iter())
280
.map(|def| match def.clone() {
281
ShaderDefVal::Bool(k, v) => {
282
(k, naga_oil::compose::ShaderDefValue::Bool(v))
283
}
284
ShaderDefVal::Int(k, v) => {
285
(k, naga_oil::compose::ShaderDefValue::Int(v))
286
}
287
ShaderDefVal::UInt(k, v) => {
288
(k, naga_oil::compose::ShaderDefValue::UInt(v))
289
}
290
})
291
.collect::<std::collections::HashMap<_, _>>();
292
293
let naga = self
294
.composer
295
.make_naga_module(naga_oil::compose::NagaModuleDescriptor {
296
shader_defs,
297
..shader.into()
298
})
299
.map_err(Box::new)?;
300
301
#[cfg(not(feature = "decoupled_naga"))]
302
{
303
ShaderCacheSource::Naga(naga)
304
}
305
306
#[cfg(feature = "decoupled_naga")]
307
{
308
let mut validator = naga::valid::Validator::new(
309
naga::valid::ValidationFlags::all(),
310
self.composer.capabilities,
311
);
312
let module_info = validator.validate(&naga).unwrap();
313
let wgsl = naga::back::wgsl::write_string(
314
&naga,
315
&module_info,
316
naga::back::wgsl::WriterFlags::empty(),
317
)
318
.unwrap();
319
ShaderCacheSource::Wgsl(wgsl)
320
}
321
}
322
};
323
324
let shader_module =
325
(self.load_module)(&self.device, shader_source, &shader.validate_shader)?;
326
327
entry.insert(Arc::new(shader_module))
328
}
329
};
330
331
Ok(module.clone())
332
}
333
334
fn clear(&mut self, id: AssetId<Shader>) -> Vec<CachedPipelineId> {
335
let mut shaders_to_clear = vec![id];
336
let mut pipelines_to_queue = Vec::new();
337
while let Some(handle) = shaders_to_clear.pop() {
338
if let Some(data) = self.data.get_mut(&handle) {
339
data.processed_shaders.clear();
340
pipelines_to_queue.extend(data.pipelines.iter().copied());
341
shaders_to_clear.extend(data.dependents.iter().copied());
342
343
if let Some(Shader { import_path, .. }) = self.shaders.get(&handle) {
344
self.composer
345
.remove_composable_module(&import_path.module_name());
346
}
347
}
348
}
349
350
pipelines_to_queue
351
}
352
353
/// Inserts and possibly replaces a shader at the given asset id.
354
///
355
/// Returns a vec of which cached pipelines depended on it
356
/// (directly or indirectly via a shader import) and thus must be recompiled.
357
pub fn set_shader(&mut self, id: AssetId<Shader>, shader: Shader) -> Vec<CachedPipelineId> {
358
let pipelines_to_queue = self.clear(id);
359
let path = &shader.import_path;
360
self.import_path_shaders.insert(path.clone(), id);
361
if let Some(waiting_shaders) = self.waiting_on_import.get_mut(path) {
362
for waiting_shader in waiting_shaders.drain(..) {
363
// resolve waiting shader import
364
let data = self.data.entry(waiting_shader).or_default();
365
data.resolved_imports.insert(path.clone(), id);
366
// add waiting shader as dependent of this shader
367
let data = self.data.entry(id).or_default();
368
data.dependents.insert(waiting_shader);
369
}
370
}
371
372
for import in shader.imports.iter() {
373
if let Some(import_id) = self.import_path_shaders.get(import).copied() {
374
// resolve import because it is currently available
375
let data = self.data.entry(id).or_default();
376
data.resolved_imports.insert(import.clone(), import_id);
377
// add this shader as a dependent of the import
378
let data = self.data.entry(import_id).or_default();
379
data.dependents.insert(id);
380
} else {
381
let waiting = self.waiting_on_import.entry(import.clone()).or_default();
382
waiting.push(id);
383
}
384
}
385
386
#[cfg(feature = "shader_format_wesl")]
387
if let Source::Wesl(_) = shader.source
388
&& let ShaderImport::AssetPath(path) = &shader.import_path
389
{
390
self.module_path_to_asset_id
391
.insert(wesl::syntax::ModulePath::from_path(path), id);
392
}
393
self.shaders.insert(id, shader);
394
pipelines_to_queue
395
}
396
397
/// Removes the shader with the given asset id.
398
///
399
/// Returns a vec of which cached pipelines depended on it
400
/// (directly or indirectly via a shader import) and thus must be recompiled.
401
pub fn remove(&mut self, id: AssetId<Shader>) -> Vec<CachedPipelineId> {
402
let pipelines_to_queue = self.clear(id);
403
if let Some(shader) = self.shaders.remove(&id) {
404
self.import_path_shaders.remove(&shader.import_path);
405
}
406
407
pipelines_to_queue
408
}
409
}
410
411
/// A Wesl import resolver. Maps module paths to actual Wesl shader source.
412
#[cfg(feature = "shader_format_wesl")]
413
pub struct ShaderResolver<'a> {
414
module_path_to_asset_id: &'a HashMap<wesl::syntax::ModulePath, AssetId<Shader>>,
415
shaders: &'a HashMap<AssetId<Shader>, Shader>,
416
}
417
418
#[cfg(feature = "shader_format_wesl")]
419
impl<'a> ShaderResolver<'a> {
420
/// Creates a shader resolver with the given map of module paths to shader asset ids,
421
/// and map of shader asset ids to shader source. This resolver is not meant to be
422
/// long living.
423
pub fn new(
424
module_path_to_asset_id: &'a HashMap<wesl::syntax::ModulePath, AssetId<Shader>>,
425
shaders: &'a HashMap<AssetId<Shader>, Shader>,
426
) -> Self {
427
Self {
428
module_path_to_asset_id,
429
shaders,
430
}
431
}
432
}
433
434
#[cfg(feature = "shader_format_wesl")]
435
impl<'a> wesl::Resolver for ShaderResolver<'a> {
436
fn resolve_source(
437
&self,
438
module_path: &wesl::syntax::ModulePath,
439
) -> Result<alloc::borrow::Cow<'_, str>, wesl::ResolveError> {
440
let asset_id = self
441
.module_path_to_asset_id
442
.get(module_path)
443
.ok_or_else(|| {
444
wesl::ResolveError::ModuleNotFound(
445
module_path.clone(),
446
"Invalid asset id".to_string(),
447
)
448
})?;
449
450
let shader = self.shaders.get(asset_id).unwrap();
451
Ok(alloc::borrow::Cow::Borrowed(shader.source.as_str()))
452
}
453
}
454
455
/// Type of error returned by a `PipelineCache` when the creation of a GPU pipeline object failed.
456
#[expect(missing_docs, reason = "Enum variants are self-explanatory")]
457
#[derive(Error, Debug)]
458
pub enum ShaderCacheError {
459
#[error(
460
"Pipeline could not be compiled because the following shader could not be loaded: {0:?}"
461
)]
462
ShaderNotLoaded(AssetId<Shader>),
463
#[error(transparent)]
464
ProcessShaderError(#[from] Box<naga_oil::compose::ComposerError>),
465
#[error("Shader import not yet available.")]
466
ShaderImportNotYetAvailable,
467
#[error("Could not create shader module: {0}")]
468
CreateShaderModule(String),
469
}
470
471
// TODO: This needs to be kept up to date with the capabilities in the `create_validator` function in wgpu-core
472
// https://github.com/gfx-rs/wgpu/blob/trunk/wgpu-core/src/device/mod.rs#L449
473
// We can't use the `wgpu-core` function to detect the device's capabilities because `wgpu-core` isn't included in WebGPU builds.
474
/// Get the device's capabilities for use in `naga_oil`.
475
fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabilities {
476
let mut capabilities = Capabilities::empty();
477
capabilities.set(
478
Capabilities::IMMEDIATES,
479
features.contains(Features::IMMEDIATES),
480
);
481
capabilities.set(
482
Capabilities::FLOAT64,
483
features.contains(Features::SHADER_F64),
484
);
485
capabilities.set(
486
Capabilities::SHADER_FLOAT16,
487
features.contains(Features::SHADER_F16),
488
);
489
capabilities.set(
490
Capabilities::SHADER_FLOAT16_IN_FLOAT32,
491
downlevel.contains(DownlevelFlags::SHADER_F16_IN_F32),
492
);
493
capabilities.set(
494
Capabilities::PRIMITIVE_INDEX,
495
features.contains(Features::SHADER_PRIMITIVE_INDEX),
496
);
497
capabilities.set(
498
Capabilities::TEXTURE_AND_SAMPLER_BINDING_ARRAY,
499
features.contains(Features::TEXTURE_BINDING_ARRAY),
500
);
501
capabilities.set(
502
Capabilities::BUFFER_BINDING_ARRAY,
503
features.contains(Features::BUFFER_BINDING_ARRAY),
504
);
505
capabilities.set(
506
Capabilities::STORAGE_TEXTURE_BINDING_ARRAY,
507
features.contains(Features::TEXTURE_BINDING_ARRAY)
508
&& features.contains(Features::STORAGE_RESOURCE_BINDING_ARRAY),
509
);
510
capabilities.set(
511
Capabilities::STORAGE_BUFFER_BINDING_ARRAY,
512
features.contains(Features::BUFFER_BINDING_ARRAY)
513
&& features.contains(Features::STORAGE_RESOURCE_BINDING_ARRAY),
514
);
515
capabilities.set(
516
Capabilities::TEXTURE_AND_SAMPLER_BINDING_ARRAY_NON_UNIFORM_INDEXING,
517
features.contains(Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING),
518
);
519
capabilities.set(
520
Capabilities::BUFFER_BINDING_ARRAY_NON_UNIFORM_INDEXING,
521
features.contains(Features::UNIFORM_BUFFER_BINDING_ARRAYS),
522
);
523
capabilities.set(
524
Capabilities::STORAGE_TEXTURE_BINDING_ARRAY_NON_UNIFORM_INDEXING,
525
features.contains(Features::STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING),
526
);
527
capabilities.set(
528
Capabilities::STORAGE_BUFFER_BINDING_ARRAY_NON_UNIFORM_INDEXING,
529
features.contains(Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING),
530
);
531
capabilities.set(
532
Capabilities::STORAGE_TEXTURE_16BIT_NORM_FORMATS,
533
features.contains(Features::TEXTURE_FORMAT_16BIT_NORM),
534
);
535
capabilities.set(
536
Capabilities::MULTIVIEW,
537
features.contains(Features::MULTIVIEW),
538
);
539
capabilities.set(
540
Capabilities::EARLY_DEPTH_TEST,
541
features.contains(Features::SHADER_EARLY_DEPTH_TEST),
542
);
543
capabilities.set(
544
Capabilities::SHADER_INT64,
545
features.contains(Features::SHADER_INT64),
546
);
547
capabilities.set(
548
Capabilities::SHADER_INT64_ATOMIC_MIN_MAX,
549
features.intersects(
550
Features::SHADER_INT64_ATOMIC_MIN_MAX | Features::SHADER_INT64_ATOMIC_ALL_OPS,
551
),
552
);
553
capabilities.set(
554
Capabilities::SHADER_INT64_ATOMIC_ALL_OPS,
555
features.contains(Features::SHADER_INT64_ATOMIC_ALL_OPS),
556
);
557
capabilities.set(
558
Capabilities::TEXTURE_ATOMIC,
559
features.contains(Features::TEXTURE_ATOMIC),
560
);
561
capabilities.set(
562
Capabilities::TEXTURE_INT64_ATOMIC,
563
features.contains(Features::TEXTURE_INT64_ATOMIC),
564
);
565
capabilities.set(
566
Capabilities::SHADER_FLOAT32_ATOMIC,
567
features.contains(Features::SHADER_FLOAT32_ATOMIC),
568
);
569
capabilities.set(
570
Capabilities::MULTISAMPLED_SHADING,
571
downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING),
572
);
573
capabilities.set(
574
Capabilities::DUAL_SOURCE_BLENDING,
575
features.contains(Features::DUAL_SOURCE_BLENDING),
576
);
577
capabilities.set(
578
Capabilities::CLIP_DISTANCE,
579
features.contains(Features::CLIP_DISTANCES),
580
);
581
capabilities.set(
582
Capabilities::CUBE_ARRAY_TEXTURES,
583
downlevel.contains(DownlevelFlags::CUBE_ARRAY_TEXTURES),
584
);
585
capabilities.set(
586
Capabilities::SUBGROUP,
587
features.intersects(Features::SUBGROUP | Features::SUBGROUP_VERTEX),
588
);
589
capabilities.set(
590
Capabilities::SUBGROUP_BARRIER,
591
features.intersects(Features::SUBGROUP_BARRIER),
592
);
593
capabilities.set(
594
Capabilities::RAY_QUERY,
595
features.intersects(Features::EXPERIMENTAL_RAY_QUERY),
596
);
597
capabilities.set(
598
Capabilities::SUBGROUP_VERTEX_STAGE,
599
features.contains(Features::SUBGROUP_VERTEX),
600
);
601
capabilities.set(
602
Capabilities::RAY_HIT_VERTEX_POSITION,
603
features.intersects(Features::EXPERIMENTAL_RAY_HIT_VERTEX_RETURN),
604
);
605
capabilities.set(
606
Capabilities::TEXTURE_EXTERNAL,
607
features.intersects(Features::EXTERNAL_TEXTURE),
608
);
609
capabilities.set(
610
Capabilities::SHADER_BARYCENTRICS,
611
features.intersects(Features::SHADER_BARYCENTRICS),
612
);
613
capabilities.set(
614
Capabilities::MESH_SHADER,
615
features.intersects(Features::EXPERIMENTAL_MESH_SHADER),
616
);
617
capabilities.set(
618
Capabilities::MESH_SHADER_POINT_TOPOLOGY,
619
features.intersects(Features::EXPERIMENTAL_MESH_SHADER_POINTS),
620
);
621
622
capabilities
623
}
624
625