1use glow::HasContext;
2use parking_lot::Mutex;
3use std::sync::{atomic::AtomicU8, Arc};
4use wgt::AstcChannel;
5
6use crate::auxil::db;
7use crate::gles::ShaderClearProgram;
8
9const GL_UNMASKED_VENDOR_WEBGL: u32 = 0x9245;
12const GL_UNMASKED_RENDERER_WEBGL: u32 = 0x9246;
13
14impl super::Adapter {
15 fn parse_version(mut src: &str) -> Result<(u8, u8), crate::InstanceError> {
21 let webgl_sig = "WebGL ";
22 let is_webgl = src.starts_with(webgl_sig);
26 if is_webgl {
27 let pos = src.rfind(webgl_sig).unwrap_or(0);
28 src = &src[pos + webgl_sig.len()..];
29 } else {
30 let es_sig = " ES ";
31 match src.rfind(es_sig) {
32 Some(pos) => {
33 src = &src[pos + es_sig.len()..];
34 }
35 None => {
36 return Err(crate::InstanceError::new(format!(
37 "OpenGL version {src:?} does not contain 'ES'"
38 )));
39 }
40 }
41 };
42
43 let glsl_es_sig = "GLSL ES ";
44 let is_glsl = match src.find(glsl_es_sig) {
45 Some(pos) => {
46 src = &src[pos + glsl_es_sig.len()..];
47 true
48 }
49 None => false,
50 };
51
52 Self::parse_full_version(src).map(|(major, minor)| {
53 (
54 if is_webgl && !is_glsl {
56 major + 1
57 } else {
58 major
59 },
60 minor,
61 )
62 })
63 }
64
65 pub(super) fn parse_full_version(src: &str) -> Result<(u8, u8), crate::InstanceError> {
81 let (version, _vendor_info) = match src.find(' ') {
82 Some(i) => (&src[..i], src[i + 1..].to_string()),
83 None => (src, String::new()),
84 };
85
86 let mut it = version.split('.');
89 let major = it.next().and_then(|s| s.parse().ok());
90 let minor = it.next().and_then(|s| {
91 let trimmed = if s.starts_with('0') {
92 "0"
93 } else {
94 s.trim_end_matches('0')
95 };
96 trimmed.parse().ok()
97 });
98
99 match (major, minor) {
100 (Some(major), Some(minor)) => Ok((major, minor)),
101 _ => Err(crate::InstanceError::new(format!(
102 "unable to extract OpenGL version from {version:?}"
103 ))),
104 }
105 }
106
107 fn make_info(vendor_orig: String, renderer_orig: String, version: String) -> wgt::AdapterInfo {
108 let vendor = vendor_orig.to_lowercase();
109 let renderer = renderer_orig.to_lowercase();
110
111 let strings_that_imply_integrated = [
113 " xpress", "amd renoir",
115 "radeon hd 4200",
116 "radeon hd 4250",
117 "radeon hd 4290",
118 "radeon hd 4270",
119 "radeon hd 4225",
120 "radeon hd 3100",
121 "radeon hd 3200",
122 "radeon hd 3000",
123 "radeon hd 3300",
124 "radeon(tm) r4 graphics",
125 "radeon(tm) r5 graphics",
126 "radeon(tm) r6 graphics",
127 "radeon(tm) r7 graphics",
128 "radeon r7 graphics",
129 "nforce", "tegra", "shield", "igp",
133 "mali",
134 "intel",
135 "v3d",
136 "apple m", ];
138 let strings_that_imply_cpu = ["mesa offscreen", "swiftshader", "llvmpipe"];
139
140 let inferred_device_type = if vendor.contains("qualcomm")
142 || vendor.contains("intel")
143 || strings_that_imply_integrated
144 .iter()
145 .any(|&s| renderer.contains(s))
146 {
147 wgt::DeviceType::IntegratedGpu
148 } else if strings_that_imply_cpu.iter().any(|&s| renderer.contains(s)) {
149 wgt::DeviceType::Cpu
150 } else {
151 wgt::DeviceType::Other
157 };
158
159 let vendor_id = if vendor.contains("amd") {
161 db::amd::VENDOR
162 } else if vendor.contains("imgtec") {
163 db::imgtec::VENDOR
164 } else if vendor.contains("nvidia") {
165 db::nvidia::VENDOR
166 } else if vendor.contains("arm") {
167 db::arm::VENDOR
168 } else if vendor.contains("qualcomm") {
169 db::qualcomm::VENDOR
170 } else if vendor.contains("intel") {
171 db::intel::VENDOR
172 } else if vendor.contains("broadcom") {
173 db::broadcom::VENDOR
174 } else if vendor.contains("mesa") {
175 db::mesa::VENDOR
176 } else if vendor.contains("apple") {
177 db::apple::VENDOR
178 } else {
179 0
180 };
181
182 let driver;
183 let driver_info;
184 if version.starts_with("WebGL ") || version.starts_with("OpenGL ") {
185 let es_sig = " ES";
186 match version.find(es_sig) {
187 Some(pos) => {
188 driver = version[..pos + es_sig.len()].to_owned();
189 driver_info = version[pos + es_sig.len() + 1..].to_owned();
190 }
191 None => {
192 let pos = version.find(' ').unwrap();
193 driver = version[..pos].to_owned();
194 driver_info = version[pos + 1..].to_owned();
195 }
196 }
197 } else {
198 driver = "OpenGL".to_owned();
199 driver_info = version;
200 }
201
202 wgt::AdapterInfo {
203 name: renderer_orig,
204 vendor: vendor_id,
205 device: 0,
206 device_type: inferred_device_type,
207 driver,
208 driver_info,
209 backend: wgt::Backend::Gl,
210 }
211 }
212
213 pub(super) unsafe fn expose(
214 context: super::AdapterContext,
215 ) -> Option<crate::ExposedAdapter<super::Api>> {
216 let gl = context.lock();
217 let extensions = gl.supported_extensions();
218
219 let (vendor_const, renderer_const) = if extensions.contains("WEBGL_debug_renderer_info") {
220 #[cfg(Emscripten)]
223 if unsafe { super::emscripten::enable_extension("WEBGL_debug_renderer_info\0") } {
224 (GL_UNMASKED_VENDOR_WEBGL, GL_UNMASKED_RENDERER_WEBGL)
225 } else {
226 (glow::VENDOR, glow::RENDERER)
227 }
228 #[cfg(not(Emscripten))]
230 (GL_UNMASKED_VENDOR_WEBGL, GL_UNMASKED_RENDERER_WEBGL)
231 } else {
232 (glow::VENDOR, glow::RENDERER)
233 };
234
235 let vendor = unsafe { gl.get_parameter_string(vendor_const) };
236 let renderer = unsafe { gl.get_parameter_string(renderer_const) };
237 let version = unsafe { gl.get_parameter_string(glow::VERSION) };
238 log::debug!("Vendor: {}", vendor);
239 log::debug!("Renderer: {}", renderer);
240 log::debug!("Version: {}", version);
241
242 let full_ver = Self::parse_full_version(&version).ok();
243 let es_ver = full_ver.map_or_else(|| Self::parse_version(&version).ok(), |_| None);
244
245 if let Some(full_ver) = full_ver {
246 let core_profile = (full_ver >= (3, 2)).then(|| unsafe {
247 gl.get_parameter_i32(glow::CONTEXT_PROFILE_MASK)
248 & glow::CONTEXT_CORE_PROFILE_BIT as i32
249 != 0
250 });
251 log::trace!(
252 "Profile: {}",
253 core_profile
254 .map(|core_profile| if core_profile {
255 "Core"
256 } else {
257 "Compatibility"
258 })
259 .unwrap_or("Legacy")
260 );
261 }
262
263 if es_ver.is_none() && full_ver.is_none() {
264 log::warn!("Unable to parse OpenGL version");
265 return None;
266 }
267
268 if let Some(es_ver) = es_ver {
269 if es_ver < (3, 0) {
270 log::warn!(
271 "Returned GLES context is {}.{}, when 3.0+ was requested",
272 es_ver.0,
273 es_ver.1
274 );
275 return None;
276 }
277 }
278
279 if let Some(full_ver) = full_ver {
280 if full_ver < (3, 3) {
281 log::warn!(
282 "Returned GL context is {}.{}, when 3.3+ is needed",
283 full_ver.0,
284 full_ver.1
285 );
286 return None;
287 }
288 }
289
290 let shading_language_version = {
291 let sl_version = unsafe { gl.get_parameter_string(glow::SHADING_LANGUAGE_VERSION) };
292 log::debug!("SL version: {}", &sl_version);
293 if full_ver.is_some() {
294 let (sl_major, sl_minor) = Self::parse_full_version(&sl_version).ok()?;
295 let mut value = sl_major as u16 * 100 + sl_minor as u16 * 10;
296 if value > 450 {
298 value = 450;
299 }
300 naga::back::glsl::Version::Desktop(value)
301 } else {
302 let (sl_major, sl_minor) = Self::parse_version(&sl_version).ok()?;
303 let value = sl_major as u16 * 100 + sl_minor as u16 * 10;
304 naga::back::glsl::Version::Embedded {
305 version: value,
306 is_webgl: cfg!(any(webgl, Emscripten)),
307 }
308 }
309 };
310
311 log::debug!("Supported GL Extensions: {:#?}", extensions);
312
313 let supported = |(req_es_major, req_es_minor), (req_full_major, req_full_minor)| {
314 let es_supported = es_ver
315 .map(|es_ver| es_ver >= (req_es_major, req_es_minor))
316 .unwrap_or_default();
317
318 let full_supported = full_ver
319 .map(|full_ver| full_ver >= (req_full_major, req_full_minor))
320 .unwrap_or_default();
321
322 es_supported || full_supported
323 };
324
325 let supports_storage =
326 supported((3, 1), (4, 3)) || extensions.contains("GL_ARB_shader_storage_buffer_object");
327 let supports_compute =
328 supported((3, 1), (4, 3)) || extensions.contains("GL_ARB_compute_shader");
329 let supports_work_group_params = supports_compute;
330
331 let is_angle = renderer.contains("ANGLE");
333
334 let vertex_shader_storage_blocks = if supports_storage {
335 let value =
336 (unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_SHADER_STORAGE_BLOCKS) } as u32);
337
338 if value == 0 && extensions.contains("GL_ARB_shader_storage_buffer_object") {
339 let new = (unsafe { gl.get_parameter_i32(glow::MAX_COMPUTE_SHADER_STORAGE_BLOCKS) }
342 as u32);
343 log::warn!("Max vertex shader storage blocks is zero, but GL_ARB_shader_storage_buffer_object is specified. Assuming the compute value {new}");
344 new
345 } else {
346 value
347 }
348 } else {
349 0
350 };
351 let fragment_shader_storage_blocks = if supports_storage {
352 (unsafe { gl.get_parameter_i32(glow::MAX_FRAGMENT_SHADER_STORAGE_BLOCKS) } as u32)
353 } else {
354 0
355 };
356 let vertex_shader_storage_textures = if supports_storage {
357 (unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_IMAGE_UNIFORMS) } as u32)
358 } else {
359 0
360 };
361 let fragment_shader_storage_textures = if supports_storage {
362 (unsafe { gl.get_parameter_i32(glow::MAX_FRAGMENT_IMAGE_UNIFORMS) } as u32)
363 } else {
364 0
365 };
366 let max_storage_block_size = if supports_storage {
367 (unsafe { gl.get_parameter_i32(glow::MAX_SHADER_STORAGE_BLOCK_SIZE) } as u32)
368 } else {
369 0
370 };
371 let max_element_index = unsafe { gl.get_parameter_i32(glow::MAX_ELEMENT_INDEX) } as u32;
372
373 let vertex_ssbo_false_zero =
379 vertex_shader_storage_blocks == 0 && vertex_shader_storage_textures != 0;
380 if vertex_ssbo_false_zero {
381 log::warn!("Max vertex shader SSBO == 0 and SSTO != 0. Interpreting as false zero.");
383 }
384
385 let max_storage_buffers_per_shader_stage = if vertex_shader_storage_blocks == 0 {
386 fragment_shader_storage_blocks
387 } else {
388 vertex_shader_storage_blocks.min(fragment_shader_storage_blocks)
389 };
390 let max_storage_textures_per_shader_stage = if vertex_shader_storage_textures == 0 {
391 fragment_shader_storage_textures
392 } else {
393 vertex_shader_storage_textures.min(fragment_shader_storage_textures)
394 };
395
396 let mut downlevel_flags = wgt::DownlevelFlags::empty()
397 | wgt::DownlevelFlags::NON_POWER_OF_TWO_MIPMAPPED_TEXTURES
398 | wgt::DownlevelFlags::CUBE_ARRAY_TEXTURES
399 | wgt::DownlevelFlags::COMPARISON_SAMPLERS
400 | wgt::DownlevelFlags::VERTEX_AND_INSTANCE_INDEX_RESPECTS_RESPECTIVE_FIRST_VALUE_IN_INDIRECT_DRAW;
401 downlevel_flags.set(wgt::DownlevelFlags::COMPUTE_SHADERS, supports_compute);
402 downlevel_flags.set(
403 wgt::DownlevelFlags::FRAGMENT_WRITABLE_STORAGE,
404 max_storage_block_size != 0,
405 );
406 downlevel_flags.set(
407 wgt::DownlevelFlags::INDIRECT_EXECUTION,
408 supported((3, 1), (4, 3)) || extensions.contains("GL_ARB_multi_draw_indirect"),
409 );
410 downlevel_flags.set(wgt::DownlevelFlags::BASE_VERTEX, supported((3, 2), (3, 2)));
411 downlevel_flags.set(
412 wgt::DownlevelFlags::INDEPENDENT_BLEND,
413 supported((3, 2), (4, 0)) || extensions.contains("GL_EXT_draw_buffers_indexed"),
414 );
415 downlevel_flags.set(
416 wgt::DownlevelFlags::VERTEX_STORAGE,
417 max_storage_block_size != 0
418 && max_storage_buffers_per_shader_stage != 0
419 && (vertex_shader_storage_blocks != 0 || vertex_ssbo_false_zero),
420 );
421 downlevel_flags.set(wgt::DownlevelFlags::FRAGMENT_STORAGE, supports_storage);
422 if extensions.contains("EXT_texture_filter_anisotropic")
423 || extensions.contains("GL_EXT_texture_filter_anisotropic")
424 {
425 let max_aniso =
426 unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_MAX_ANISOTROPY_EXT) } as u32;
427 downlevel_flags.set(wgt::DownlevelFlags::ANISOTROPIC_FILTERING, max_aniso >= 16);
428 }
429 downlevel_flags.set(
430 wgt::DownlevelFlags::BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED,
431 !(cfg!(any(webgl, Emscripten)) || is_angle),
432 );
433 downlevel_flags.set(
435 wgt::DownlevelFlags::UNRESTRICTED_INDEX_BUFFER,
436 !cfg!(any(webgl, Emscripten)),
437 );
438 downlevel_flags.set(
439 wgt::DownlevelFlags::UNRESTRICTED_EXTERNAL_TEXTURE_COPIES,
440 !cfg!(any(webgl, Emscripten)),
441 );
442 downlevel_flags.set(
443 wgt::DownlevelFlags::FULL_DRAW_INDEX_UINT32,
444 max_element_index == u32::MAX,
445 );
446 downlevel_flags.set(
447 wgt::DownlevelFlags::MULTISAMPLED_SHADING,
448 supported((3, 2), (4, 0)) || extensions.contains("OES_sample_variables"),
449 );
450 let query_buffers = extensions.contains("GL_ARB_query_buffer_object")
451 || extensions.contains("GL_AMD_query_buffer_object");
452 if query_buffers {
453 downlevel_flags.set(wgt::DownlevelFlags::NONBLOCKING_QUERY_RESOLVE, true);
454 }
455
456 let mut features = wgt::Features::empty()
457 | wgt::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
458 | wgt::Features::CLEAR_TEXTURE
459 | wgt::Features::PUSH_CONSTANTS
460 | wgt::Features::DEPTH32FLOAT_STENCIL8;
461 features.set(
462 wgt::Features::ADDRESS_MODE_CLAMP_TO_BORDER | wgt::Features::ADDRESS_MODE_CLAMP_TO_ZERO,
463 extensions.contains("GL_EXT_texture_border_clamp")
464 || extensions.contains("GL_ARB_texture_border_clamp"),
465 );
466 features.set(
467 wgt::Features::DEPTH_CLIP_CONTROL,
468 extensions.contains("GL_EXT_depth_clamp") || extensions.contains("GL_ARB_depth_clamp"),
469 );
470 features.set(
471 wgt::Features::VERTEX_WRITABLE_STORAGE,
472 downlevel_flags.contains(wgt::DownlevelFlags::VERTEX_STORAGE)
473 && vertex_shader_storage_textures != 0,
474 );
475 features.set(
476 wgt::Features::MULTIVIEW,
477 extensions.contains("OVR_multiview2") || extensions.contains("GL_OVR_multiview2"),
478 );
479 features.set(
480 wgt::Features::DUAL_SOURCE_BLENDING,
481 extensions.contains("GL_EXT_blend_func_extended")
482 || extensions.contains("GL_ARB_blend_func_extended"),
483 );
484 features.set(
485 wgt::Features::SHADER_PRIMITIVE_INDEX,
486 supported((3, 2), (3, 2))
487 || extensions.contains("OES_geometry_shader")
488 || extensions.contains("GL_ARB_geometry_shader4"),
489 );
490 features.set(
491 wgt::Features::SHADER_EARLY_DEPTH_TEST,
492 supported((3, 1), (4, 2)) || extensions.contains("GL_ARB_shader_image_load_store"),
493 );
494 features.set(wgt::Features::SHADER_UNUSED_VERTEX_OUTPUT, true);
495 if extensions.contains("GL_ARB_timer_query") {
496 features.set(wgt::Features::TIMESTAMP_QUERY, true);
497 features.set(wgt::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS, true);
498 features.set(wgt::Features::TIMESTAMP_QUERY_INSIDE_PASSES, true);
499 }
500 let gl_bcn_exts = [
501 "GL_EXT_texture_compression_s3tc",
502 "GL_EXT_texture_compression_rgtc",
503 "GL_ARB_texture_compression_bptc",
504 ];
505 let gles_bcn_exts = [
506 "GL_EXT_texture_compression_s3tc_srgb",
507 "GL_EXT_texture_compression_rgtc",
508 "GL_EXT_texture_compression_bptc",
509 ];
510 let webgl_bcn_exts = [
511 "WEBGL_compressed_texture_s3tc",
512 "WEBGL_compressed_texture_s3tc_srgb",
513 "EXT_texture_compression_rgtc",
514 "EXT_texture_compression_bptc",
515 ];
516 let bcn_exts = if cfg!(any(webgl, Emscripten)) {
517 &webgl_bcn_exts[..]
518 } else if es_ver.is_some() {
519 &gles_bcn_exts[..]
520 } else {
521 &gl_bcn_exts[..]
522 };
523 features.set(
524 wgt::Features::TEXTURE_COMPRESSION_BC,
525 bcn_exts.iter().all(|&ext| extensions.contains(ext)),
526 );
527 let has_etc = if cfg!(any(webgl, Emscripten)) {
528 extensions.contains("WEBGL_compressed_texture_etc")
529 } else {
530 es_ver.is_some() || extensions.contains("GL_ARB_ES3_compatibility")
531 };
532 features.set(wgt::Features::TEXTURE_COMPRESSION_ETC2, has_etc);
533
534 if extensions.contains("WEBGL_compressed_texture_astc")
536 || extensions.contains("GL_OES_texture_compression_astc")
537 {
538 #[cfg(webgl)]
539 {
540 if context
541 .glow_context
542 .compressed_texture_astc_supports_ldr_profile()
543 {
544 features.insert(wgt::Features::TEXTURE_COMPRESSION_ASTC);
545 }
546 if context
547 .glow_context
548 .compressed_texture_astc_supports_hdr_profile()
549 {
550 features.insert(wgt::Features::TEXTURE_COMPRESSION_ASTC_HDR);
551 }
552 }
553
554 #[cfg(any(native, Emscripten))]
555 {
556 features.insert(wgt::Features::TEXTURE_COMPRESSION_ASTC);
557 features.insert(wgt::Features::TEXTURE_COMPRESSION_ASTC_HDR);
558 }
559 } else {
560 features.set(
561 wgt::Features::TEXTURE_COMPRESSION_ASTC,
562 extensions.contains("GL_KHR_texture_compression_astc_ldr"),
563 );
564 features.set(
565 wgt::Features::TEXTURE_COMPRESSION_ASTC_HDR,
566 extensions.contains("GL_KHR_texture_compression_astc_hdr"),
567 );
568 }
569
570 features.set(
571 wgt::Features::FLOAT32_FILTERABLE,
572 extensions.contains("GL_ARB_color_buffer_float")
573 || extensions.contains("GL_EXT_color_buffer_float")
574 || extensions.contains("OES_texture_float_linear"),
575 );
576
577 if es_ver.is_none() {
578 features |= wgt::Features::POLYGON_MODE_LINE | wgt::Features::POLYGON_MODE_POINT;
579 }
580
581 let mut private_caps = super::PrivateCapabilities::empty();
584 private_caps.set(
585 super::PrivateCapabilities::BUFFER_ALLOCATION,
586 extensions.contains("GL_EXT_buffer_storage")
587 || extensions.contains("GL_ARB_buffer_storage"),
588 );
589 private_caps.set(
590 super::PrivateCapabilities::SHADER_BINDING_LAYOUT,
591 supports_compute,
592 );
593 private_caps.set(
594 super::PrivateCapabilities::SHADER_TEXTURE_SHADOW_LOD,
595 extensions.contains("GL_EXT_texture_shadow_lod"),
596 );
597 private_caps.set(
598 super::PrivateCapabilities::MEMORY_BARRIERS,
599 supported((3, 1), (4, 2)),
600 );
601 private_caps.set(
602 super::PrivateCapabilities::VERTEX_BUFFER_LAYOUT,
603 supported((3, 1), (4, 3)) || extensions.contains("GL_ARB_vertex_attrib_binding"),
604 );
605 private_caps.set(
606 super::PrivateCapabilities::INDEX_BUFFER_ROLE_CHANGE,
607 !cfg!(any(webgl, Emscripten)),
608 );
609 private_caps.set(
610 super::PrivateCapabilities::GET_BUFFER_SUB_DATA,
611 cfg!(any(webgl, Emscripten)) || full_ver.is_some(),
612 );
613 let color_buffer_float = extensions.contains("GL_EXT_color_buffer_float")
614 || extensions.contains("GL_ARB_color_buffer_float")
615 || extensions.contains("EXT_color_buffer_float");
616 let color_buffer_half_float = extensions.contains("GL_EXT_color_buffer_half_float")
617 || extensions.contains("GL_ARB_half_float_pixel");
618 private_caps.set(
619 super::PrivateCapabilities::COLOR_BUFFER_HALF_FLOAT,
620 color_buffer_half_float || color_buffer_float,
621 );
622 private_caps.set(
623 super::PrivateCapabilities::COLOR_BUFFER_FLOAT,
624 color_buffer_float,
625 );
626 private_caps.set(super::PrivateCapabilities::QUERY_BUFFERS, query_buffers);
627 private_caps.set(super::PrivateCapabilities::QUERY_64BIT, full_ver.is_some());
628 private_caps.set(
629 super::PrivateCapabilities::TEXTURE_STORAGE,
630 supported((3, 0), (4, 2)),
631 );
632 private_caps.set(super::PrivateCapabilities::DEBUG_FNS, gl.supports_debug());
633 private_caps.set(
634 super::PrivateCapabilities::INVALIDATE_FRAMEBUFFER,
635 supported((3, 0), (4, 3)),
636 );
637 if let Some(full_ver) = full_ver {
638 let supported =
639 full_ver >= (4, 2) && extensions.contains("GL_ARB_shader_draw_parameters");
640 private_caps.set(
641 super::PrivateCapabilities::FULLY_FEATURED_INSTANCING,
642 supported,
643 );
644 features.set(wgt::Features::INDIRECT_FIRST_INSTANCE, supported);
651 }
652
653 let max_texture_size = unsafe { gl.get_parameter_i32(glow::MAX_TEXTURE_SIZE) } as u32;
654 let max_texture_3d_size = unsafe { gl.get_parameter_i32(glow::MAX_3D_TEXTURE_SIZE) } as u32;
655
656 let min_uniform_buffer_offset_alignment =
657 (unsafe { gl.get_parameter_i32(glow::UNIFORM_BUFFER_OFFSET_ALIGNMENT) } as u32);
658 let min_storage_buffer_offset_alignment = if supports_storage {
659 (unsafe { gl.get_parameter_i32(glow::SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT) } as u32)
660 } else {
661 256
662 };
663 let max_uniform_buffers_per_shader_stage =
664 unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_UNIFORM_BLOCKS) }
665 .min(unsafe { gl.get_parameter_i32(glow::MAX_FRAGMENT_UNIFORM_BLOCKS) })
666 as u32;
667
668 let max_compute_workgroups_per_dimension = if supports_work_group_params {
669 unsafe { gl.get_parameter_indexed_i32(glow::MAX_COMPUTE_WORK_GROUP_COUNT, 0) }
670 .min(unsafe { gl.get_parameter_indexed_i32(glow::MAX_COMPUTE_WORK_GROUP_COUNT, 1) })
671 .min(unsafe { gl.get_parameter_indexed_i32(glow::MAX_COMPUTE_WORK_GROUP_COUNT, 2) })
672 as u32
673 } else {
674 0
675 };
676
677 let max_color_attachments = unsafe {
678 gl.get_parameter_i32(glow::MAX_COLOR_ATTACHMENTS)
679 .min(gl.get_parameter_i32(glow::MAX_DRAW_BUFFERS))
680 .min(crate::MAX_COLOR_ATTACHMENTS as i32) as u32
681 };
682
683 let max_color_attachment_bytes_per_sample = 32;
685
686 let limits = wgt::Limits {
687 max_texture_dimension_1d: max_texture_size,
688 max_texture_dimension_2d: max_texture_size,
689 max_texture_dimension_3d: max_texture_3d_size,
690 max_texture_array_layers: unsafe {
691 gl.get_parameter_i32(glow::MAX_ARRAY_TEXTURE_LAYERS)
692 } as u32,
693 max_bind_groups: crate::MAX_BIND_GROUPS as u32,
694 max_bindings_per_bind_group: 65535,
695 max_dynamic_uniform_buffers_per_pipeline_layout: max_uniform_buffers_per_shader_stage,
696 max_dynamic_storage_buffers_per_pipeline_layout: max_storage_buffers_per_shader_stage,
697 max_sampled_textures_per_shader_stage: super::MAX_TEXTURE_SLOTS as u32,
698 max_samplers_per_shader_stage: super::MAX_SAMPLERS as u32,
699 max_storage_buffers_per_shader_stage,
700 max_storage_textures_per_shader_stage,
701 max_uniform_buffers_per_shader_stage,
702 max_uniform_buffer_binding_size: unsafe {
703 gl.get_parameter_i32(glow::MAX_UNIFORM_BLOCK_SIZE)
704 } as u32,
705 max_storage_buffer_binding_size: if supports_storage {
706 unsafe { gl.get_parameter_i32(glow::MAX_SHADER_STORAGE_BLOCK_SIZE) }
707 } else {
708 0
709 } as u32,
710 max_vertex_buffers: if private_caps
711 .contains(super::PrivateCapabilities::VERTEX_BUFFER_LAYOUT)
712 {
713 (unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_BINDINGS) } as u32)
714 } else {
715 16 }
717 .min(crate::MAX_VERTEX_BUFFERS as u32),
718 max_vertex_attributes: (unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIBS) }
719 as u32)
720 .min(super::MAX_VERTEX_ATTRIBUTES as u32),
721 max_vertex_buffer_array_stride: if private_caps
722 .contains(super::PrivateCapabilities::VERTEX_BUFFER_LAYOUT)
723 {
724 if let Some(full_ver) = full_ver {
725 if full_ver >= (4, 4) {
726 let value =
728 (unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_STRIDE) })
729 as u32;
730
731 if value == 0 {
732 log::warn!("Max vertex attribute stride is 0. Assuming it is 2048");
736 2048
737 } else {
738 value
739 }
740 } else {
741 log::warn!("Max vertex attribute stride unknown. Assuming it is 2048");
742 2048
743 }
744 } else {
745 (unsafe { gl.get_parameter_i32(glow::MAX_VERTEX_ATTRIB_STRIDE) }) as u32
746 }
747 } else {
748 !0
749 },
750 min_subgroup_size: 0,
751 max_subgroup_size: 0,
752 max_push_constant_size: super::MAX_PUSH_CONSTANTS as u32 * 4,
753 min_uniform_buffer_offset_alignment,
754 min_storage_buffer_offset_alignment,
755 max_inter_stage_shader_components: {
756 let max_varying_components =
760 unsafe { gl.get_parameter_i32(glow::MAX_VARYING_COMPONENTS) } as u32;
761 if max_varying_components == 0 {
762 60
764 } else {
765 max_varying_components
766 }
767 },
768 max_color_attachments,
769 max_color_attachment_bytes_per_sample,
770 max_compute_workgroup_storage_size: if supports_work_group_params {
771 (unsafe { gl.get_parameter_i32(glow::MAX_COMPUTE_SHARED_MEMORY_SIZE) } as u32)
772 } else {
773 0
774 },
775 max_compute_invocations_per_workgroup: if supports_work_group_params {
776 (unsafe { gl.get_parameter_i32(glow::MAX_COMPUTE_WORK_GROUP_INVOCATIONS) } as u32)
777 } else {
778 0
779 },
780 max_compute_workgroup_size_x: if supports_work_group_params {
781 (unsafe { gl.get_parameter_indexed_i32(glow::MAX_COMPUTE_WORK_GROUP_SIZE, 0) }
782 as u32)
783 } else {
784 0
785 },
786 max_compute_workgroup_size_y: if supports_work_group_params {
787 (unsafe { gl.get_parameter_indexed_i32(glow::MAX_COMPUTE_WORK_GROUP_SIZE, 1) }
788 as u32)
789 } else {
790 0
791 },
792 max_compute_workgroup_size_z: if supports_work_group_params {
793 (unsafe { gl.get_parameter_indexed_i32(glow::MAX_COMPUTE_WORK_GROUP_SIZE, 2) }
794 as u32)
795 } else {
796 0
797 },
798 max_compute_workgroups_per_dimension,
799 max_buffer_size: i32::MAX as u64,
800 max_non_sampler_bindings: std::u32::MAX,
801 };
802
803 let mut workarounds = super::Workarounds::empty();
804
805 workarounds.set(
806 super::Workarounds::EMULATE_BUFFER_MAP,
807 cfg!(any(webgl, Emscripten)),
808 );
809
810 let r = renderer.to_lowercase();
811 if context.is_owned()
814 && r.contains("mesa")
815 && r.contains("intel")
816 && r.split(&[' ', '(', ')'][..])
817 .any(|substr| substr.len() == 3 && substr.chars().nth(2) == Some('l'))
818 {
819 log::warn!(
820 "Detected skylake derivative running on mesa i915. Clears to srgb textures will \
821 use manual shader clears."
822 );
823 workarounds.set(super::Workarounds::MESA_I915_SRGB_SHADER_CLEAR, true);
824 }
825
826 let downlevel_defaults = wgt::DownlevelLimits {};
827 let max_samples = unsafe { gl.get_parameter_i32(glow::MAX_SAMPLES) };
828
829 #[cfg_attr(target_arch = "wasm32", allow(dropping_references))]
833 drop(gl);
834
835 Some(crate::ExposedAdapter {
836 adapter: super::Adapter {
837 shared: Arc::new(super::AdapterShared {
838 context,
839 private_caps,
840 workarounds,
841 features,
842 shading_language_version,
843 next_shader_id: Default::default(),
844 program_cache: Default::default(),
845 es: es_ver.is_some(),
846 max_msaa_samples: max_samples,
847 }),
848 },
849 info: Self::make_info(vendor, renderer, version),
850 features,
851 capabilities: crate::Capabilities {
852 limits,
853 downlevel: wgt::DownlevelCapabilities {
854 flags: downlevel_flags,
855 limits: downlevel_defaults,
856 shader_model: wgt::ShaderModel::Sm5,
857 },
858 alignments: crate::Alignments {
859 buffer_copy_offset: wgt::BufferSize::new(4).unwrap(),
860 buffer_copy_pitch: wgt::BufferSize::new(4).unwrap(),
861 },
862 },
863 })
864 }
865
866 unsafe fn compile_shader(
867 source: &str,
868 gl: &glow::Context,
869 shader_type: u32,
870 es: bool,
871 ) -> Option<glow::Shader> {
872 let source = if es {
873 format!("#version 300 es\nprecision lowp float;\n{source}")
874 } else {
875 let version = gl.version();
876 if version.major == 3 && version.minor == 0 {
877 format!("#version 130\n{source}")
879 } else {
880 format!("#version 140\n{source}")
882 }
883 };
884 let shader = unsafe { gl.create_shader(shader_type) }.expect("Could not create shader");
885 unsafe { gl.shader_source(shader, &source) };
886 unsafe { gl.compile_shader(shader) };
887
888 if !unsafe { gl.get_shader_compile_status(shader) } {
889 let msg = unsafe { gl.get_shader_info_log(shader) };
890 if !msg.is_empty() {
891 log::error!("\tShader compile error: {}", msg);
892 }
893 unsafe { gl.delete_shader(shader) };
894 None
895 } else {
896 Some(shader)
897 }
898 }
899
900 unsafe fn create_shader_clear_program(
901 gl: &glow::Context,
902 es: bool,
903 ) -> Option<ShaderClearProgram> {
904 let program = unsafe { gl.create_program() }.expect("Could not create shader program");
905 let vertex = unsafe {
906 Self::compile_shader(
907 include_str!("./shaders/clear.vert"),
908 gl,
909 glow::VERTEX_SHADER,
910 es,
911 )?
912 };
913 let fragment = unsafe {
914 Self::compile_shader(
915 include_str!("./shaders/clear.frag"),
916 gl,
917 glow::FRAGMENT_SHADER,
918 es,
919 )?
920 };
921 unsafe { gl.attach_shader(program, vertex) };
922 unsafe { gl.attach_shader(program, fragment) };
923 unsafe { gl.link_program(program) };
924
925 let linked_ok = unsafe { gl.get_program_link_status(program) };
926 let msg = unsafe { gl.get_program_info_log(program) };
927 if !msg.is_empty() {
928 log::warn!("Shader link error: {}", msg);
929 }
930 if !linked_ok {
931 return None;
932 }
933
934 let color_uniform_location = unsafe { gl.get_uniform_location(program, "color") }
935 .expect("Could not find color uniform in shader clear shader");
936 unsafe { gl.delete_shader(vertex) };
937 unsafe { gl.delete_shader(fragment) };
938
939 Some(ShaderClearProgram {
940 program,
941 color_uniform_location,
942 })
943 }
944}
945
946impl crate::Adapter for super::Adapter {
947 type A = super::Api;
948
949 unsafe fn open(
950 &self,
951 features: wgt::Features,
952 _limits: &wgt::Limits,
953 ) -> Result<crate::OpenDevice<super::Api>, crate::DeviceError> {
954 let gl = &self.shared.context.lock();
955 unsafe { gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1) };
956 unsafe { gl.pixel_store_i32(glow::PACK_ALIGNMENT, 1) };
957 let main_vao =
958 unsafe { gl.create_vertex_array() }.map_err(|_| crate::DeviceError::OutOfMemory)?;
959 unsafe { gl.bind_vertex_array(Some(main_vao)) };
960
961 let zero_buffer =
962 unsafe { gl.create_buffer() }.map_err(|_| crate::DeviceError::OutOfMemory)?;
963 unsafe { gl.bind_buffer(glow::COPY_READ_BUFFER, Some(zero_buffer)) };
964 let zeroes = vec![0u8; super::ZERO_BUFFER_SIZE];
965 unsafe { gl.buffer_data_u8_slice(glow::COPY_READ_BUFFER, &zeroes, glow::STATIC_DRAW) };
966
967 let shader_clear_program = if self
971 .shared
972 .workarounds
973 .contains(super::Workarounds::MESA_I915_SRGB_SHADER_CLEAR)
974 {
975 Some(unsafe {
976 Self::create_shader_clear_program(gl, self.shared.es)
977 .ok_or(crate::DeviceError::ResourceCreationFailed)?
978 })
979 } else {
980 None
982 };
983
984 Ok(crate::OpenDevice {
985 device: super::Device {
986 shared: Arc::clone(&self.shared),
987 main_vao,
988 #[cfg(all(native, feature = "renderdoc"))]
989 render_doc: Default::default(),
990 },
991 queue: super::Queue {
992 shared: Arc::clone(&self.shared),
993 features,
994 draw_fbo: unsafe { gl.create_framebuffer() }
995 .map_err(|_| crate::DeviceError::OutOfMemory)?,
996 copy_fbo: unsafe { gl.create_framebuffer() }
997 .map_err(|_| crate::DeviceError::OutOfMemory)?,
998 shader_clear_program,
999 zero_buffer,
1000 temp_query_results: Mutex::new(Vec::new()),
1001 draw_buffer_count: AtomicU8::new(1),
1002 current_index_buffer: Mutex::new(None),
1003 },
1004 })
1005 }
1006
1007 unsafe fn texture_format_capabilities(
1008 &self,
1009 format: wgt::TextureFormat,
1010 ) -> crate::TextureFormatCapabilities {
1011 use crate::TextureFormatCapabilities as Tfc;
1012 use wgt::TextureFormat as Tf;
1013
1014 let sample_count = {
1015 let max_samples = self.shared.max_msaa_samples;
1016 if max_samples >= 16 {
1017 Tfc::MULTISAMPLE_X2
1018 | Tfc::MULTISAMPLE_X4
1019 | Tfc::MULTISAMPLE_X8
1020 | Tfc::MULTISAMPLE_X16
1021 } else if max_samples >= 8 {
1022 Tfc::MULTISAMPLE_X2 | Tfc::MULTISAMPLE_X4 | Tfc::MULTISAMPLE_X8
1023 } else {
1024 Tfc::MULTISAMPLE_X2 | Tfc::MULTISAMPLE_X4
1029 }
1030 };
1031
1032 let empty = Tfc::empty();
1037 let base = Tfc::COPY_SRC | Tfc::COPY_DST;
1038 let unfilterable = base | Tfc::SAMPLED;
1039 let depth = base | Tfc::SAMPLED | sample_count | Tfc::DEPTH_STENCIL_ATTACHMENT;
1040 let filterable = unfilterable | Tfc::SAMPLED_LINEAR;
1041 let renderable =
1042 unfilterable | Tfc::COLOR_ATTACHMENT | sample_count | Tfc::MULTISAMPLE_RESOLVE;
1043 let filterable_renderable = filterable | renderable | Tfc::COLOR_ATTACHMENT_BLEND;
1044 let storage = base | Tfc::STORAGE | Tfc::STORAGE_READ_WRITE;
1045
1046 let feature_fn = |f, caps| {
1047 if self.shared.features.contains(f) {
1048 caps
1049 } else {
1050 empty
1051 }
1052 };
1053
1054 let bcn_features = feature_fn(wgt::Features::TEXTURE_COMPRESSION_BC, filterable);
1055 let etc2_features = feature_fn(wgt::Features::TEXTURE_COMPRESSION_ETC2, filterable);
1056 let astc_features = feature_fn(wgt::Features::TEXTURE_COMPRESSION_ASTC, filterable);
1057 let astc_hdr_features = feature_fn(wgt::Features::TEXTURE_COMPRESSION_ASTC_HDR, filterable);
1058
1059 let private_caps_fn = |f, caps| {
1060 if self.shared.private_caps.contains(f) {
1061 caps
1062 } else {
1063 empty
1064 }
1065 };
1066
1067 let half_float_renderable = private_caps_fn(
1068 super::PrivateCapabilities::COLOR_BUFFER_HALF_FLOAT,
1069 Tfc::COLOR_ATTACHMENT
1070 | Tfc::COLOR_ATTACHMENT_BLEND
1071 | sample_count
1072 | Tfc::MULTISAMPLE_RESOLVE,
1073 );
1074
1075 let float_renderable = private_caps_fn(
1076 super::PrivateCapabilities::COLOR_BUFFER_FLOAT,
1077 Tfc::COLOR_ATTACHMENT
1078 | Tfc::COLOR_ATTACHMENT_BLEND
1079 | sample_count
1080 | Tfc::MULTISAMPLE_RESOLVE,
1081 );
1082
1083 let texture_float_linear = feature_fn(wgt::Features::FLOAT32_FILTERABLE, filterable);
1084
1085 match format {
1086 Tf::R8Unorm => filterable_renderable,
1087 Tf::R8Snorm => filterable,
1088 Tf::R8Uint => renderable,
1089 Tf::R8Sint => renderable,
1090 Tf::R16Uint => renderable,
1091 Tf::R16Sint => renderable,
1092 Tf::R16Unorm => empty,
1093 Tf::R16Snorm => empty,
1094 Tf::R16Float => filterable | half_float_renderable,
1095 Tf::Rg8Unorm => filterable_renderable,
1096 Tf::Rg8Snorm => filterable,
1097 Tf::Rg8Uint => renderable,
1098 Tf::Rg8Sint => renderable,
1099 Tf::R32Uint => renderable | storage,
1100 Tf::R32Sint => renderable | storage,
1101 Tf::R32Float => unfilterable | storage | float_renderable | texture_float_linear,
1102 Tf::Rg16Uint => renderable,
1103 Tf::Rg16Sint => renderable,
1104 Tf::Rg16Unorm => empty,
1105 Tf::Rg16Snorm => empty,
1106 Tf::Rg16Float => filterable | half_float_renderable,
1107 Tf::Rgba8Unorm => filterable_renderable | storage,
1108 Tf::Rgba8UnormSrgb => filterable_renderable,
1109 Tf::Bgra8Unorm | Tf::Bgra8UnormSrgb => filterable_renderable,
1110 Tf::Rgba8Snorm => filterable | storage,
1111 Tf::Rgba8Uint => renderable | storage,
1112 Tf::Rgba8Sint => renderable | storage,
1113 Tf::Rgb10a2Uint => renderable,
1114 Tf::Rgb10a2Unorm => filterable_renderable,
1115 Tf::Rg11b10Float => filterable | float_renderable,
1116 Tf::Rg32Uint => renderable,
1117 Tf::Rg32Sint => renderable,
1118 Tf::Rg32Float => unfilterable | float_renderable | texture_float_linear,
1119 Tf::Rgba16Uint => renderable | storage,
1120 Tf::Rgba16Sint => renderable | storage,
1121 Tf::Rgba16Unorm => empty,
1122 Tf::Rgba16Snorm => empty,
1123 Tf::Rgba16Float => filterable | storage | half_float_renderable,
1124 Tf::Rgba32Uint => renderable | storage,
1125 Tf::Rgba32Sint => renderable | storage,
1126 Tf::Rgba32Float => unfilterable | storage | float_renderable | texture_float_linear,
1127 Tf::Stencil8
1128 | Tf::Depth16Unorm
1129 | Tf::Depth32Float
1130 | Tf::Depth32FloatStencil8
1131 | Tf::Depth24Plus
1132 | Tf::Depth24PlusStencil8 => depth,
1133 Tf::NV12 => empty,
1134 Tf::Rgb9e5Ufloat => filterable,
1135 Tf::Bc1RgbaUnorm
1136 | Tf::Bc1RgbaUnormSrgb
1137 | Tf::Bc2RgbaUnorm
1138 | Tf::Bc2RgbaUnormSrgb
1139 | Tf::Bc3RgbaUnorm
1140 | Tf::Bc3RgbaUnormSrgb
1141 | Tf::Bc4RUnorm
1142 | Tf::Bc4RSnorm
1143 | Tf::Bc5RgUnorm
1144 | Tf::Bc5RgSnorm
1145 | Tf::Bc6hRgbFloat
1146 | Tf::Bc6hRgbUfloat
1147 | Tf::Bc7RgbaUnorm
1148 | Tf::Bc7RgbaUnormSrgb => bcn_features,
1149 Tf::Etc2Rgb8Unorm
1150 | Tf::Etc2Rgb8UnormSrgb
1151 | Tf::Etc2Rgb8A1Unorm
1152 | Tf::Etc2Rgb8A1UnormSrgb
1153 | Tf::Etc2Rgba8Unorm
1154 | Tf::Etc2Rgba8UnormSrgb
1155 | Tf::EacR11Unorm
1156 | Tf::EacR11Snorm
1157 | Tf::EacRg11Unorm
1158 | Tf::EacRg11Snorm => etc2_features,
1159 Tf::Astc {
1160 block: _,
1161 channel: AstcChannel::Unorm | AstcChannel::UnormSrgb,
1162 } => astc_features,
1163 Tf::Astc {
1164 block: _,
1165 channel: AstcChannel::Hdr,
1166 } => astc_hdr_features,
1167 }
1168 }
1169
1170 unsafe fn surface_capabilities(
1171 &self,
1172 surface: &super::Surface,
1173 ) -> Option<crate::SurfaceCapabilities> {
1174 if surface.presentable {
1175 let mut formats = vec![
1176 wgt::TextureFormat::Rgba8Unorm,
1177 #[cfg(native)]
1178 wgt::TextureFormat::Bgra8Unorm,
1179 ];
1180 if surface.supports_srgb() {
1181 formats.extend([
1182 wgt::TextureFormat::Rgba8UnormSrgb,
1183 #[cfg(native)]
1184 wgt::TextureFormat::Bgra8UnormSrgb,
1185 ])
1186 }
1187 if self
1188 .shared
1189 .private_caps
1190 .contains(super::PrivateCapabilities::COLOR_BUFFER_HALF_FLOAT)
1191 {
1192 formats.push(wgt::TextureFormat::Rgba16Float)
1193 }
1194
1195 Some(crate::SurfaceCapabilities {
1196 formats,
1197 present_modes: if cfg!(windows) {
1198 vec![wgt::PresentMode::Fifo, wgt::PresentMode::Immediate]
1199 } else {
1200 vec![wgt::PresentMode::Fifo] },
1202 composite_alpha_modes: vec![wgt::CompositeAlphaMode::Opaque], maximum_frame_latency: 2..=2, current_extent: None,
1205 usage: crate::TextureUses::COLOR_TARGET,
1206 })
1207 } else {
1208 None
1209 }
1210 }
1211
1212 unsafe fn get_presentation_timestamp(&self) -> wgt::PresentationTimestamp {
1213 wgt::PresentationTimestamp::INVALID_TIMESTAMP
1214 }
1215}
1216
1217impl super::AdapterShared {
1218 pub(super) unsafe fn get_buffer_sub_data(
1219 &self,
1220 gl: &glow::Context,
1221 target: u32,
1222 offset: i32,
1223 dst_data: &mut [u8],
1224 ) {
1225 if self
1226 .private_caps
1227 .contains(super::PrivateCapabilities::GET_BUFFER_SUB_DATA)
1228 {
1229 unsafe { gl.get_buffer_sub_data(target, offset, dst_data) };
1230 } else {
1231 log::error!("Fake map");
1232 let length = dst_data.len();
1233 let buffer_mapping =
1234 unsafe { gl.map_buffer_range(target, offset, length as _, glow::MAP_READ_BIT) };
1235
1236 unsafe { std::ptr::copy_nonoverlapping(buffer_mapping, dst_data.as_mut_ptr(), length) };
1237
1238 unsafe { gl.unmap_buffer(target) };
1239 }
1240 }
1241}
1242
1243#[cfg(send_sync)]
1244unsafe impl Sync for super::Adapter {}
1245#[cfg(send_sync)]
1246unsafe impl Send for super::Adapter {}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::super::Adapter;
1251
1252 #[test]
1253 fn test_version_parse() {
1254 Adapter::parse_version("1").unwrap_err();
1255 Adapter::parse_version("1.").unwrap_err();
1256 Adapter::parse_version("1 h3l1o. W0rld").unwrap_err();
1257 Adapter::parse_version("1. h3l1o. W0rld").unwrap_err();
1258 Adapter::parse_version("1.2.3").unwrap_err();
1259
1260 assert_eq!(Adapter::parse_version("OpenGL ES 3.1").unwrap(), (3, 1));
1261 assert_eq!(
1262 Adapter::parse_version("OpenGL ES 2.0 Google Nexus").unwrap(),
1263 (2, 0)
1264 );
1265 assert_eq!(Adapter::parse_version("GLSL ES 1.1").unwrap(), (1, 1));
1266 assert_eq!(
1267 Adapter::parse_version("OpenGL ES GLSL ES 3.20").unwrap(),
1268 (3, 2)
1269 );
1270 assert_eq!(
1271 Adapter::parse_version("WebGL 2.0 (OpenGL ES 3.0 Chromium)").unwrap(),
1273 (3, 0)
1274 );
1275 assert_eq!(
1276 Adapter::parse_version("WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)").unwrap(),
1277 (3, 0)
1278 );
1279 }
1280}