use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpuProfile {
pub vendor: String,
pub renderer: String,
pub version: String,
pub shading_language_version: String,
pub unmasked_vendor: String,
pub unmasked_renderer: String,
pub extensions: Vec<String>,
pub params: Vec<(u32, serde_json::Value)>,
pub shader_precision: Vec<(u32, u32, [i32; 3])>,
#[serde(default)]
pub webgl1: Option<WebGL1Surface>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebGL1Surface {
pub version: String,
pub shading_language_version: String,
pub extensions: Vec<String>,
}
pub fn nvidia_rtx_3060_windows() -> GpuProfile {
GpuProfile {
vendor: "WebKit".into(),
renderer: "WebKit WebGL".into(),
version: "WebGL 1.0 (OpenGL ES 2.0 Chromium)".into(),
shading_language_version: "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)".into(),
unmasked_vendor: "Google Inc. (NVIDIA)".into(),
unmasked_renderer:
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)".into(),
extensions: vec![
"ANGLE_instanced_arrays".into(),
"EXT_blend_minmax".into(),
"EXT_clip_control".into(),
"EXT_color_buffer_half_float".into(),
"EXT_depth_clamp".into(),
"EXT_disjoint_timer_query".into(),
"EXT_float_blend".into(),
"EXT_frag_depth".into(),
"EXT_polygon_offset_clamp".into(),
"EXT_shader_texture_lod".into(),
"EXT_texture_compression_bptc".into(),
"EXT_texture_compression_rgtc".into(),
"EXT_texture_filter_anisotropic".into(),
"EXT_texture_mirror_clamp_to_edge".into(),
"EXT_sRGB".into(),
"KHR_parallel_shader_compile".into(),
"OES_element_index_uint".into(),
"OES_fbo_render_mipmap".into(),
"OES_standard_derivatives".into(),
"OES_texture_float".into(),
"OES_texture_float_linear".into(),
"OES_texture_half_float".into(),
"OES_texture_half_float_linear".into(),
"OES_vertex_array_object".into(),
"WEBGL_blend_func_extended".into(),
"WEBGL_color_buffer_float".into(),
"WEBGL_compressed_texture_s3tc".into(),
"WEBGL_compressed_texture_s3tc_srgb".into(),
"WEBGL_debug_renderer_info".into(),
"WEBGL_debug_shaders".into(),
"WEBGL_depth_texture".into(),
"WEBGL_draw_buffers".into(),
"WEBGL_lose_context".into(),
"WEBGL_multi_draw".into(),
"WEBGL_polygon_mode".into(),
],
params: common_params_desktop(),
shader_precision: standard_shader_precision(),
webgl1: None,
}
}
pub fn apple_m3_macos() -> GpuProfile {
apple_m3_family_profile("Apple M3")
}
pub fn apple_m3_pro_macos() -> GpuProfile {
apple_m3_family_profile("Apple M3 Pro")
}
pub fn apple_m3_max_macos() -> GpuProfile {
apple_m3_family_profile("Apple M3 Max")
}
fn apple_m3_family_profile(chip_name: &str) -> GpuProfile {
GpuProfile {
vendor: "WebKit".into(),
renderer: "WebKit WebGL".into(),
version: "WebGL 2.0 (OpenGL ES 3.0 Chromium)".into(),
shading_language_version: "WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)".into(),
unmasked_vendor: "Google Inc. (Apple)".into(),
unmasked_renderer: format!(
"ANGLE (Apple, ANGLE Metal Renderer: {chip_name}, Unspecified Version)"
),
extensions: vec![
"EXT_clip_control".into(),
"EXT_color_buffer_float".into(),
"EXT_color_buffer_half_float".into(),
"EXT_conservative_depth".into(),
"EXT_depth_clamp".into(),
"EXT_disjoint_timer_query_webgl2".into(),
"EXT_float_blend".into(),
"EXT_polygon_offset_clamp".into(),
"EXT_render_snorm".into(),
"EXT_texture_compression_bptc".into(),
"EXT_texture_compression_rgtc".into(),
"EXT_texture_filter_anisotropic".into(),
"EXT_texture_mirror_clamp_to_edge".into(),
"EXT_texture_norm16".into(),
"KHR_parallel_shader_compile".into(),
"NV_shader_noperspective_interpolation".into(),
"OES_draw_buffers_indexed".into(),
"OES_sample_variables".into(),
"OES_shader_multisample_interpolation".into(),
"OES_texture_float_linear".into(),
"WEBGL_blend_func_extended".into(),
"WEBGL_clip_cull_distance".into(),
"WEBGL_compressed_texture_astc".into(),
"WEBGL_compressed_texture_etc".into(),
"WEBGL_compressed_texture_etc1".into(),
"WEBGL_compressed_texture_pvrtc".into(),
"WEBGL_compressed_texture_s3tc".into(),
"WEBGL_compressed_texture_s3tc_srgb".into(),
"WEBGL_debug_renderer_info".into(),
"WEBGL_debug_shaders".into(),
"WEBGL_lose_context".into(),
"WEBGL_multi_draw".into(),
"WEBGL_polygon_mode".into(),
"WEBGL_provoking_vertex".into(),
"WEBGL_render_shared_exponent".into(),
"WEBGL_stencil_texturing".into(),
],
params: apple_m3_params(),
shader_precision: standard_shader_precision(),
webgl1: Some(apple_m3_webgl1_surface()),
}
}
fn apple_m3_webgl1_surface() -> WebGL1Surface {
WebGL1Surface {
version: "WebGL 1.0 (OpenGL ES 2.0 Chromium)".into(),
shading_language_version: "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)".into(),
extensions: vec![
"ANGLE_instanced_arrays".into(),
"EXT_blend_minmax".into(),
"EXT_clip_control".into(),
"EXT_color_buffer_half_float".into(),
"EXT_depth_clamp".into(),
"EXT_disjoint_timer_query".into(),
"EXT_float_blend".into(),
"EXT_frag_depth".into(),
"EXT_polygon_offset_clamp".into(),
"EXT_sRGB".into(),
"EXT_shader_texture_lod".into(),
"EXT_texture_compression_bptc".into(),
"EXT_texture_compression_rgtc".into(),
"EXT_texture_filter_anisotropic".into(),
"EXT_texture_mirror_clamp_to_edge".into(),
"KHR_parallel_shader_compile".into(),
"OES_element_index_uint".into(),
"OES_fbo_render_mipmap".into(),
"OES_standard_derivatives".into(),
"OES_texture_float".into(),
"OES_texture_float_linear".into(),
"OES_texture_half_float".into(),
"OES_texture_half_float_linear".into(),
"OES_vertex_array_object".into(),
"WEBGL_blend_func_extended".into(),
"WEBGL_color_buffer_float".into(),
"WEBGL_compressed_texture_astc".into(),
"WEBGL_compressed_texture_etc".into(),
"WEBGL_compressed_texture_etc1".into(),
"WEBGL_compressed_texture_pvrtc".into(),
"WEBGL_compressed_texture_s3tc".into(),
"WEBGL_compressed_texture_s3tc_srgb".into(),
"WEBGL_debug_renderer_info".into(),
"WEBGL_debug_shaders".into(),
"WEBGL_depth_texture".into(),
"WEBGL_draw_buffers".into(),
"WEBGL_lose_context".into(),
"WEBGL_multi_draw".into(),
"WEBGL_polygon_mode".into(),
],
}
}
fn apple_m3_params() -> Vec<(u32, serde_json::Value)> {
use serde_json::json;
let mut params = common_params_desktop();
for (pname, value) in params.iter_mut() {
match *pname {
0x0D3A => *value = json!([16384, 16384]), 0x846D => *value = json!([1.0, 511.0]), _ => {}
}
}
params
}
pub fn apple_m2_pro_macos() -> GpuProfile {
GpuProfile {
vendor: "WebKit".into(),
renderer: "WebKit WebGL".into(),
version: "WebGL 1.0 (OpenGL ES 2.0 Chromium)".into(),
shading_language_version: "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)".into(),
unmasked_vendor: "Google Inc. (Apple)".into(),
unmasked_renderer: "ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version)"
.into(),
extensions: vec![
"ANGLE_instanced_arrays".into(),
"EXT_blend_minmax".into(),
"EXT_clip_control".into(),
"EXT_color_buffer_half_float".into(),
"EXT_depth_clamp".into(),
"EXT_float_blend".into(),
"EXT_frag_depth".into(),
"EXT_polygon_offset_clamp".into(),
"EXT_shader_texture_lod".into(),
"EXT_texture_compression_bptc".into(),
"EXT_texture_compression_rgtc".into(),
"EXT_texture_filter_anisotropic".into(),
"EXT_texture_mirror_clamp_to_edge".into(),
"EXT_sRGB".into(),
"KHR_parallel_shader_compile".into(),
"OES_element_index_uint".into(),
"OES_fbo_render_mipmap".into(),
"OES_standard_derivatives".into(),
"OES_texture_float".into(),
"OES_texture_float_linear".into(),
"OES_texture_half_float".into(),
"OES_texture_half_float_linear".into(),
"OES_vertex_array_object".into(),
"WEBGL_blend_func_extended".into(),
"WEBGL_color_buffer_float".into(),
"WEBGL_compressed_texture_astc".into(),
"WEBGL_compressed_texture_etc".into(),
"WEBGL_compressed_texture_etc1".into(),
"WEBGL_compressed_texture_s3tc".into(),
"WEBGL_compressed_texture_s3tc_srgb".into(),
"WEBGL_debug_renderer_info".into(),
"WEBGL_debug_shaders".into(),
"WEBGL_depth_texture".into(),
"WEBGL_draw_buffers".into(),
"WEBGL_lose_context".into(),
"WEBGL_multi_draw".into(),
],
params: common_params_desktop(),
shader_precision: standard_shader_precision(),
webgl1: None,
}
}
pub fn intel_uhd_630_linux() -> GpuProfile {
GpuProfile {
vendor: "WebKit".into(),
renderer: "WebKit WebGL".into(),
version: "WebGL 1.0 (OpenGL ES 2.0 Chromium)".into(),
shading_language_version: "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)".into(),
unmasked_vendor: "Google Inc. (Intel)".into(),
unmasked_renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6)"
.into(),
extensions: vec![
"ANGLE_instanced_arrays".into(),
"EXT_blend_minmax".into(),
"EXT_clip_control".into(),
"EXT_color_buffer_half_float".into(),
"EXT_depth_clamp".into(),
"EXT_disjoint_timer_query".into(),
"EXT_float_blend".into(),
"EXT_frag_depth".into(),
"EXT_polygon_offset_clamp".into(),
"EXT_shader_texture_lod".into(),
"EXT_texture_compression_bptc".into(),
"EXT_texture_compression_rgtc".into(),
"EXT_texture_filter_anisotropic".into(),
"EXT_texture_mirror_clamp_to_edge".into(),
"EXT_sRGB".into(),
"KHR_parallel_shader_compile".into(),
"OES_element_index_uint".into(),
"OES_fbo_render_mipmap".into(),
"OES_standard_derivatives".into(),
"OES_texture_float".into(),
"OES_texture_float_linear".into(),
"OES_texture_half_float".into(),
"OES_texture_half_float_linear".into(),
"OES_vertex_array_object".into(),
"WEBGL_compressed_texture_s3tc".into(),
"WEBGL_compressed_texture_s3tc_srgb".into(),
"WEBGL_debug_renderer_info".into(),
"WEBGL_debug_shaders".into(),
"WEBGL_depth_texture".into(),
"WEBGL_draw_buffers".into(),
"WEBGL_lose_context".into(),
"WEBGL_multi_draw".into(),
],
params: common_params_desktop(),
shader_precision: standard_shader_precision(),
webgl1: None,
}
}
fn common_params_desktop() -> Vec<(u32, serde_json::Value)> {
use serde_json::json;
vec![
(0x0D33, json!(16384)), (0x851C, json!(16384)), (0x84E8, json!(16384)), (0x8073, json!(2048)), (0x8869, json!(16)), (0x8DFB, json!(1024)), (0x8DFD, json!(15)), (0x8DFC, json!(1024)), (0x8872, json!(16)), (0x8B4D, json!(16)), (0x8B4C, json!(32)), (0x846D, json!([1.0, 8190.0])), (0x846E, json!([1.0, 1.0])), (0x0D3A, json!([32767, 32767])), (0x0D56, json!(8)), (0x0D57, json!(8)), (0x80AA, json!(2)), (0x80A9, json!(4)), ]
}
fn standard_shader_precision() -> Vec<(u32, u32, [i32; 3])> {
let mut out = Vec::with_capacity(12);
for &shader_type in &[0x8B31u32, 0x8B30u32] {
out.push((shader_type, 0x8DF0, [127, 127, 23]));
out.push((shader_type, 0x8DF1, [127, 127, 23]));
out.push((shader_type, 0x8DF2, [127, 127, 23]));
out.push((shader_type, 0x8DF3, [15, 14, 0]));
out.push((shader_type, 0x8DF4, [31, 30, 0]));
out.push((shader_type, 0x8DF5, [31, 30, 0]));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apple_m3_webgl1_surface_is_spec_correct() {
let gpu = apple_m3_macos();
let w1 = gpu
.webgl1
.as_ref()
.expect("apple_m3 must have a webgl1 surface");
assert_eq!(w1.version, "WebGL 1.0 (OpenGL ES 2.0 Chromium)");
assert_eq!(
w1.shading_language_version,
"WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)"
);
for banned in [
"EXT_color_buffer_float",
"OES_draw_buffers_indexed",
"EXT_disjoint_timer_query_webgl2",
"WEBGL_clip_cull_distance",
"WEBGL_provoking_vertex",
] {
assert!(
!w1.extensions.iter().any(|e| e == banned),
"WebGL 1 surface must not expose WebGL-2-only ext {banned}"
);
}
for required in [
"OES_texture_float",
"ANGLE_instanced_arrays",
"WEBGL_depth_texture",
"EXT_disjoint_timer_query",
"WEBGL_color_buffer_float",
] {
assert!(
w1.extensions.iter().any(|e| e == required),
"WebGL 1 surface must expose core-promoted ext {required}"
);
}
assert_ne!(
w1.extensions, gpu.extensions,
"webgl1 must differ from webgl2"
);
assert_ne!(w1.version, gpu.version);
}
#[test]
fn nvidia_profile_has_many_extensions() {
let gpu = nvidia_rtx_3060_windows();
assert!(
gpu.extensions.len() >= 25,
"expected >=25 extensions, got {}",
gpu.extensions.len()
);
}
#[test]
fn apple_profile_has_astc_extension() {
let gpu = apple_m2_pro_macos();
assert!(
gpu.extensions
.iter()
.any(|e| e == "WEBGL_compressed_texture_astc"),
"Apple GPU missing WEBGL_compressed_texture_astc"
);
}
#[test]
fn intel_profile_lacks_astc() {
let gpu = intel_uhd_630_linux();
assert!(
!gpu.extensions
.iter()
.any(|e| e == "WEBGL_compressed_texture_astc"),
"Intel Linux shouldn't expose WEBGL_compressed_texture_astc"
);
}
#[test]
fn shader_precision_has_all_12_entries() {
let gpu = nvidia_rtx_3060_windows();
assert_eq!(gpu.shader_precision.len(), 12);
}
#[test]
fn shader_precision_int_differs_from_float() {
let gpu = nvidia_rtx_3060_windows();
let high_float = gpu
.shader_precision
.iter()
.find(|(_, p, _)| *p == 0x8DF2)
.unwrap()
.2;
let high_int = gpu
.shader_precision
.iter()
.find(|(_, p, _)| *p == 0x8DF5)
.unwrap()
.2;
assert_ne!(
high_float, high_int,
"HIGH_FLOAT and HIGH_INT must have different values"
);
assert_eq!(high_float, [127, 127, 23]);
assert_eq!(high_int, [31, 30, 0]);
}
#[test]
fn params_include_max_texture_size() {
let gpu = nvidia_rtx_3060_windows();
let max_tex = gpu.params.iter().find(|(k, _)| *k == 0x0D33);
assert!(max_tex.is_some(), "MAX_TEXTURE_SIZE missing");
}
#[test]
fn different_gpus_have_different_renderers() {
let nvidia = nvidia_rtx_3060_windows();
let apple = apple_m2_pro_macos();
let intel = intel_uhd_630_linux();
assert_ne!(nvidia.unmasked_renderer, apple.unmasked_renderer);
assert_ne!(apple.unmasked_renderer, intel.unmasked_renderer);
assert_ne!(nvidia.unmasked_renderer, intel.unmasked_renderer);
}
#[test]
fn apple_m3_matches_captured_chrome_147_fixture() {
let gpu = apple_m3_macos();
assert_eq!(gpu.vendor, "WebKit");
assert_eq!(gpu.renderer, "WebKit WebGL");
assert_eq!(gpu.version, "WebGL 2.0 (OpenGL ES 3.0 Chromium)");
assert_eq!(
gpu.shading_language_version,
"WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)"
);
assert_eq!(gpu.unmasked_vendor, "Google Inc. (Apple)");
assert_eq!(
gpu.unmasked_renderer,
"ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)"
);
assert_eq!(
gpu.extensions.len(),
36,
"Apple M3 WebGL 2 extension list must be exactly 36 (captured fixture). \
Add to or remove from the preset only with an updated capture."
);
for required in &[
"EXT_disjoint_timer_query_webgl2",
"WEBGL_clip_cull_distance",
"WEBGL_provoking_vertex",
] {
assert!(
gpu.extensions.iter().any(|e| e == required),
"missing WebGL 2 extension {required}"
);
}
let viewport_dims = gpu
.params
.iter()
.find(|(k, _)| *k == 0x0D3A)
.expect("MAX_VIEWPORT_DIMS present")
.1
.clone();
assert_eq!(
viewport_dims,
serde_json::json!([16384, 16384]),
"Apple M3 MAX_VIEWPORT_DIMS must be [16384, 16384], not the \
common_params_desktop [32767, 32767] default"
);
let point_size = gpu
.params
.iter()
.find(|(k, _)| *k == 0x846D)
.expect("ALIASED_POINT_SIZE_RANGE present")
.1
.clone();
assert_eq!(
point_size,
serde_json::json!([1.0, 511.0]),
"Apple M3 ALIASED_POINT_SIZE_RANGE must be [1, 511]"
);
let high_float = gpu
.shader_precision
.iter()
.find(|(s, p, _)| *s == 0x8B30 && *p == 0x8DF2)
.expect("FRAGMENT_SHADER HIGH_FLOAT present")
.2;
assert_eq!(high_float, [127, 127, 23]);
}
}