From 83ab7fa4bf71eba8239fb624540b4efda13cae32 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 13 Sep 2026 18:04:57 +0800 Subject: [PATCH] fix(webgl): expose matrix uniforms and drawing buffer sizes --- README.md | 9 +- .../src/context_bootstrap/canvas.rs | 18 +-- .../context_bootstrap/canvas/backing_store.rs | 28 ++-- .../src/context_bootstrap/canvas/webgl.rs | 145 +++++++++++++++++- .../context_bootstrap/media_file_template.rs | 38 +++-- moli-renderer-v8/src/native_bridge/element.rs | 2 +- .../src/native_bridge/element/canvas.rs | 10 ++ .../src/script_vm/tests/canvas_webgl.rs | 14 ++ .../src/worker/thread/tests/postmessage.rs | 20 +++ .../fixtures/webgl-drawing-capabilities.js | 105 +++++++++++++ 10 files changed, 345 insertions(+), 44 deletions(-) create mode 100644 moli-renderer-v8/tests/fixtures/webgl-drawing-capabilities.js diff --git a/README.md b/README.md index b14a3a7967..0dd8c17d00 100644 --- a/README.md +++ b/README.md @@ -359,9 +359,12 @@ Its current intentional boundaries include: - Media capability queries use a Chromium-style software compatibility profile; advertised support and smoothness do not imply implemented media playback or hardware acceleration. -- WebGL renderer queries use generic compatibility labels (`WebKit` / `WebKit - WebGL`), including enabled debug-renderer queries. These labels do not identify - a physical GPU or an implemented graphics driver. +- WebGL queries use a declared Windows/ANGLE compatibility profile, with + `WebKit` / `WebKit WebGL` as masked labels. Debug identity, limits and shader + precision do not identify a physical GPU or an implemented graphics driver. + Drawing-buffer dimensions reflect the native canvas size; matrix-uniform + capability shims validate arguments but do not execute shaders or upload + uniforms to a GPU. Drawing and pixel readback remain incomplete. - `--layout` supports software screenshots and raster-backed CDP PDF generation, but not every Chrome screenshot or print mode is implemented. diff --git a/moli-renderer-v8/src/context_bootstrap/canvas.rs b/moli-renderer-v8/src/context_bootstrap/canvas.rs index 5c4d5ea977..61f8b77964 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas.rs @@ -213,15 +213,15 @@ pub(crate) use webgl::{ webgl_check_framebuffer_status_callback, webgl_create_buffer_callback, webgl_create_framebuffer_callback, webgl_create_program_callback, webgl_create_renderbuffer_callback, webgl_create_shader_callback, - webgl_get_attrib_location_callback, webgl_get_context_attributes_callback, - webgl_get_error_callback, webgl_get_extension_callback, webgl_get_parameter_callback, - webgl_get_shader_info_log_callback, webgl_get_shader_precision_format_callback, - webgl_get_supported_extensions_callback, webgl_is_context_lost_callback, - webgl_lose_context_noop_callback, webgl_noop_callback, webgl_uniform_location_callback, - webgl_viewport_callback, webgl2_color_space_getter_callback, - webgl2_color_space_setter_callback, webgl2_get_extension_callback, - webgl2_get_internalformat_parameter_callback, webgl2_get_parameter_callback, - webgl2_get_supported_extensions_callback, + webgl_drawing_buffer_size_callback, webgl_get_attrib_location_callback, + webgl_get_context_attributes_callback, webgl_get_error_callback, webgl_get_extension_callback, + webgl_get_parameter_callback, webgl_get_shader_info_log_callback, + webgl_get_shader_precision_format_callback, webgl_get_supported_extensions_callback, + webgl_is_context_lost_callback, webgl_lose_context_noop_callback, webgl_noop_callback, + webgl_uniform_location_callback, webgl_uniform_matrix_callback, webgl_viewport_callback, + webgl2_color_space_getter_callback, webgl2_color_space_setter_callback, + webgl2_get_extension_callback, webgl2_get_internalformat_parameter_callback, + webgl2_get_parameter_callback, webgl2_get_supported_extensions_callback, }; pub(super) fn install_canvas_template_bindings<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs b/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs index fa3eb1a1cf..67bba594f3 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/backing_store.rs @@ -1,7 +1,6 @@ use super::super::image_data::new_uint8_clamped_array_from_bytes; use super::{OFFSCREEN_CANVAS_HEIGHT_SLOT, OFFSCREEN_CANVAS_WIDTH_SLOT}; use crate::util::{get_private_object, get_private_value, set_private_value}; -use crate::webidl; use crate::{ document_runtime::DomHandle, native_bridge::{JsContextHost, node_runtime_and_handle_from_object_or_detached}, @@ -221,24 +220,21 @@ pub(super) fn canvas_like_dimensions<'s>( scope: &mut v8::PinScope<'s, '_>, canvas: v8::Local<'s, v8::Object>, ) -> Option<(u32, u32)> { - let width = canvas_like_dimension(scope, canvas, OFFSCREEN_CANVAS_WIDTH_SLOT, "width")?; - let height = canvas_like_dimension(scope, canvas, OFFSCREEN_CANVAS_HEIGHT_SLOT, "height")?; + // Backing dimensions are native state, not JS-visible property lookups. + // A page's own width/height getter must not resize a context or execute + // while its drawingBufferWidth/Height (or pixels) are being queried. + if html_canvas_identity(scope, canvas).is_some() { + return Some(crate::native_bridge::element::html_canvas_dimensions( + scope, canvas, + )); + } + let width = + get_private_value(scope, canvas, OFFSCREEN_CANVAS_WIDTH_SLOT)?.uint32_value(scope)?; + let height = + get_private_value(scope, canvas, OFFSCREEN_CANVAS_HEIGHT_SLOT)?.uint32_value(scope)?; Some((width, height)) } -fn canvas_like_dimension<'s>( - scope: &mut v8::PinScope<'s, '_>, - canvas: v8::Local<'s, v8::Object>, - slot: &str, - public_name: &'static str, -) -> Option { - let value = get_private_value(scope, canvas, slot) - .and_then(|value| value.number_value(scope)) - .or_else(|| webidl::optional_number_property(scope, canvas, public_name)) - .unwrap_or(0.0); - Some(value.max(0.0).trunc() as u32) -} - fn write_bytes_to_view<'s>( scope: &mut v8::PinScope<'s, '_>, view: v8::Local<'s, v8::Uint8ClampedArray>, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs b/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs index 6b4c40cb93..59e8b8004f 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/webgl.rs @@ -14,8 +14,16 @@ const WEBGL_EXTENSIONS_SLOT: &str = "__moliWebGlExtensions"; const WEBGL_VIEWPORT: u32 = 0x0BA2; const WEBGL_INVALID_ENUM: u32 = 0x0500; const WEBGL_INVALID_VALUE: u32 = 0x0501; +const WEBGL_INVALID_OPERATION: u32 = 0x0502; const WEBGL_MAX_VIEWPORT_DIMS: [i32; 2] = [32767, 32767]; +const WEBGL_MAX_TEXTURE_SIZE: u32 = 16384; const WEBGL_MAX_TEXTURE_IMAGE_UNITS: i32 = 16; +const WEBGL_UNIFORM_CONTEXT_SLOT: &str = "__moliWebGlUniformContext"; +const WEBGL_MATRIX_METHODS: &[(&str, usize)] = &[ + ("uniformMatrix2fv", 4), + ("uniformMatrix3fv", 9), + ("uniformMatrix4fv", 16), +]; const WEBGL_MASKED_VENDOR: &str = "WebKit"; const WEBGL_MASKED_RENDERER: &str = "WebKit WebGL"; // A declared Windows/ANGLE compatibility identity, not physical GPU discovery. @@ -144,7 +152,10 @@ struct WebGlShaderHandleDeclaration {} interface = web_api_interfaces::WebGLUniformLocation, fallback_to_string_tag = "WebGLUniformLocation" )] -struct WebGlUniformLocationHandleDeclaration {} +struct WebGlUniformLocationHandleDeclaration<'s> { + #[webapi(slot = WEBGL_UNIFORM_CONTEXT_SLOT)] + context: v8::Local<'s, v8::Object>, +} #[derive(WebApiObject)] #[webapi( @@ -495,7 +506,9 @@ fn webgl_resource_limit<'s>( // ANGLE renderer11_utils.cpp: feature level 11 resource caps, shared by // WebGL1/2. Querying these compatibility limits does not allocate resources. Some(match pname { - 0x0D33 | 0x84E8 | 0x851C => v8::Integer::new(scope, 16_384).into(), + 0x0D33 | 0x84E8 | 0x851C => { + v8::Integer::new_from_unsigned(scope, WEBGL_MAX_TEXTURE_SIZE).into() + } 0x0D3A => webgl_int32_array(scope, &WEBGL_MAX_VIEWPORT_DIMS), 0x846D => webgl_float32_array(scope, &[1.0, 1024.0]), 0x8872 | 0x8B4C => v8::Integer::new(scope, WEBGL_MAX_TEXTURE_IMAGE_UNITS).into(), @@ -647,17 +660,137 @@ pub(crate) fn webgl_create_shader_callback( rv.set(value.into()); } -pub(crate) fn webgl_uniform_location_callback( - scope: &mut v8::PinScope<'_, '_>, - _args: v8::FunctionCallbackArguments<'_>, +pub(crate) fn webgl_uniform_location_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let value = WebGlUniformLocationHandleDeclaration::new() + if webgl_extensions(scope, args.this()).is_none() { + return; + } + let value = WebGlUniformLocationHandleDeclaration::new(args.this()) .bind(scope) .expect("WebGLUniformLocation handle declaration should bind"); rv.set(value.into()); } +pub(crate) fn webgl_drawing_buffer_size_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + if webgl_extensions(scope, args.this()).is_none() { + return; + } + let Some(axis) = callback_data_item(scope, &args, &[0usize, 1], "WebGL drawing buffer axis") + else { + return; + }; + let dimensions = super::backing_store::canvas_owner_from_context(scope, args.this()) + .and_then(|canvas| super::backing_store::canvas_like_dimensions(scope, canvas)); + let Some((width, height)) = dimensions else { + rv.set_uint32(0); + return; + }; + // A zero canvas dimension still has a one-pixel drawing buffer in Blink. + // This is the compatibility buffer extent, not a physical GPU allocation. + rv.set_uint32([width, height][axis].clamp(1, WEBGL_MAX_TEXTURE_SIZE)); +} + +pub(crate) fn webgl_uniform_matrix_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + if webgl_extensions(scope, args.this()).is_none() { + return; + } + let Some((method, elements)) = + callback_data_item(scope, &args, WEBGL_MATRIX_METHODS, "WebGL matrix uniform") + else { + return; + }; + if args.length() < 3 { + throw_type_error(scope, &format!("{method}: 3 arguments required")); + return; + } + let location = args.get(0); + if !location.is_null_or_undefined() + && !v8::Local::::try_from(location).is_ok_and(|object| { + web_api_interfaces::WebGLUniformLocation::is_instance(scope, object) + }) + { + throw_type_error( + scope, + &format!("{method}: location is not a WebGLUniformLocation"), + ); + return; + } + let transpose = args.get(1).boolean_value(scope); + // Float32List's typed-array arm precedes sequence conversion: neither a + // custom iterator nor indexed accessors may run on a Float32Array. + let length = if let Ok(array) = v8::Local::::try_from(args.get(2)) { + array.length() + } else { + match webidl::argument::>( + scope, + &args, + 2, + webidl::Context::argument(method, 3), + ) { + Ok(values) => values.0.len(), + Err(error) => { + webidl::throw_error(scope, &error); + return; + } + } + }; + let webgl2 = web_api_interfaces::WebGL2RenderingContext::is_instance(scope, args.this()); + let mut range = [0u32; 2]; + if webgl2 { + for (i, value) in range.iter_mut().enumerate() { + if let Err(error) = webidl::argument::( + scope, + &args, + i as i32 + 3, + webidl::Context::argument(method, i + 4), + ) + .map(|parsed| *value = parsed.0) + { + webidl::throw_error(scope, &error); + return; + } + } + } + if length == 0 { + record_webgl_error(scope, args.this(), WEBGL_INVALID_VALUE); + return; + } + if location.is_null_or_undefined() { + return; + } + let location = v8::Local::::try_from(location).expect("validated uniform location"); + if !get_private_value(scope, location, WEBGL_UNIFORM_CONTEXT_SLOT) + .is_some_and(|context| context.strict_equals(args.this().into())) + { + record_webgl_error(scope, args.this(), WEBGL_INVALID_OPERATION); + return; + } + let [offset, requested] = range.map(|value| value as usize); + let available = length.saturating_sub(offset); + let count = if requested == 0 { available } else { requested }; + if (transpose && !webgl2) + || offset >= length + || count > available + || count < elements + || !count.is_multiple_of(elements) + { + record_webgl_error(scope, args.this(), WEBGL_INVALID_VALUE); + } + // Accept the validated capability call. Shader/program execution and GPU + // uniform uploads remain outside this shim, like the existing draw methods. +} + pub(crate) fn webgl_get_attrib_location_callback( scope: &mut v8::PinScope<'_, '_>, _args: v8::FunctionCallbackArguments<'_>, diff --git a/moli-renderer-v8/src/context_bootstrap/media_file_template.rs b/moli-renderer-v8/src/context_bootstrap/media_file_template.rs index cb469bb7e7..1de5cda40c 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_file_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_file_template.rs @@ -36,15 +36,15 @@ use super::canvas::{ webgl_boolean_callback, webgl_check_framebuffer_status_callback, webgl_create_buffer_callback, webgl_create_framebuffer_callback, webgl_create_program_callback, webgl_create_renderbuffer_callback, webgl_create_shader_callback, - webgl_get_attrib_location_callback, webgl_get_context_attributes_callback, - webgl_get_error_callback, webgl_get_extension_callback, webgl_get_parameter_callback, - webgl_get_shader_info_log_callback, webgl_get_shader_precision_format_callback, - webgl_get_supported_extensions_callback, webgl_is_context_lost_callback, - webgl_lose_context_noop_callback, webgl_noop_callback, webgl_uniform_location_callback, - webgl_viewport_callback, webgl2_color_space_getter_callback, - webgl2_color_space_setter_callback, webgl2_get_extension_callback, - webgl2_get_internalformat_parameter_callback, webgl2_get_parameter_callback, - webgl2_get_supported_extensions_callback, + webgl_drawing_buffer_size_callback, webgl_get_attrib_location_callback, + webgl_get_context_attributes_callback, webgl_get_error_callback, webgl_get_extension_callback, + webgl_get_parameter_callback, webgl_get_shader_info_log_callback, + webgl_get_shader_precision_format_callback, webgl_get_supported_extensions_callback, + webgl_is_context_lost_callback, webgl_lose_context_noop_callback, webgl_noop_callback, + webgl_uniform_location_callback, webgl_uniform_matrix_callback, webgl_viewport_callback, + webgl2_color_space_getter_callback, webgl2_color_space_setter_callback, + webgl2_get_extension_callback, webgl2_get_internalformat_parameter_callback, + webgl2_get_parameter_callback, webgl2_get_supported_extensions_callback, }; use super::file_api::{ file_list_item_callback, file_reader_abort_callback, file_reader_add_event_listener_callback, @@ -423,6 +423,14 @@ struct CanvasGradientTemplateMethodsDeclaration { #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::WebGLRenderingContext, enumerable)] struct WebGlRenderingContextTemplateMethodsDeclaration { + #[webapi(accessor_property = "drawingBufferWidth", getter = webgl_drawing_buffer_size_callback, + data = crate::util::callback_data_index_value(scope, 0))] + drawing_buffer_width: (), + + #[webapi(accessor_property = "drawingBufferHeight", getter = webgl_drawing_buffer_size_callback, + data = crate::util::callback_data_index_value(scope, 1))] + drawing_buffer_height: (), + #[webapi(method = "viewport", length = 4, callback = webgl_viewport_callback)] viewport: (), @@ -511,6 +519,18 @@ struct WebGlRenderingContextTemplateMethodsDeclaration { #[webapi(method = "uniform2fv", length = 2, callback = webgl_noop_callback)] uniform2fv: (), + #[webapi(method = "uniformMatrix2fv", length = 3, callback = webgl_uniform_matrix_callback, + data = crate::util::callback_data_index_value(scope, 0))] + uniform_matrix2fv: (), + + #[webapi(method = "uniformMatrix3fv", length = 3, callback = webgl_uniform_matrix_callback, + data = crate::util::callback_data_index_value(scope, 1))] + uniform_matrix3fv: (), + + #[webapi(method = "uniformMatrix4fv", length = 3, callback = webgl_uniform_matrix_callback, + data = crate::util::callback_data_index_value(scope, 2))] + uniform_matrix4fv: (), + #[webapi(method = "drawArrays", length = 3, callback = webgl_noop_callback)] draw_arrays: (), diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index dc5b6490ec..82d6341218 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -224,7 +224,7 @@ pub(super) use attributes::{ }; pub(crate) use canvas::{ canvas_get_context_callback, canvas_to_data_url_callback, - canvas_transfer_control_to_offscreen_callback, + canvas_transfer_control_to_offscreen_callback, html_canvas_dimensions, }; pub(crate) use canvas::{ html_canvas_height_getter_callback, html_canvas_height_setter_callback, diff --git a/moli-renderer-v8/src/native_bridge/element/canvas.rs b/moli-renderer-v8/src/native_bridge/element/canvas.rs index 2a9d754084..f3ecaed292 100644 --- a/moli-renderer-v8/src/native_bridge/element/canvas.rs +++ b/moli-renderer-v8/src/native_bridge/element/canvas.rs @@ -112,6 +112,16 @@ fn set_canvas_dimension_attribute<'s>( true } +pub(crate) fn html_canvas_dimensions<'s>( + scope: &mut v8::PinScope<'s, '_>, + canvas: v8::Local<'s, v8::Object>, +) -> (u32, u32) { + ( + canvas_dimension_value(scope, canvas, "width", 300), + canvas_dimension_value(scope, canvas, "height", 150), + ) +} + fn canvas_dimension_value<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs index 01c863d491..78a115a2c9 100644 --- a/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs +++ b/moli-renderer-v8/src/script_vm/tests/canvas_webgl.rs @@ -1,5 +1,19 @@ use super::*; +#[test] +fn webgl_drawing_capabilities_preserve_native_sizes_and_matrix_arguments() { + let mut vm = new_storage_test_vm("https://webgl-drawing-capabilities.test/"); + let result = vm + .eval(include_str!( + "../../../tests/fixtures/webgl-drawing-capabilities.js" + )) + .expect("WebGL drawing capability contract should hold"); + assert_eq!( + result, + r#"["html:webgl","offscreen:webgl","html:webgl2","offscreen:webgl2"]"# + ); +} + #[test] fn webgl_compatibility_profile_uses_d3d11_identity_and_precision() { let mut vm = new_storage_test_vm("https://webgl-compatibility-profile.test/"); diff --git a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs index 259de7c9ed..45b483558f 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -2133,6 +2133,26 @@ async fn worker_media_capabilities_matches_the_software_chromium_profile() { assert_eq!(expect_post_json(msg), "[3,3,3,0,3,3,3,3]"); } +#[tokio::test] +async fn worker_webgl_drawing_capabilities_match_the_window_contract() { + ensure_v8(); + let mut handle = spawn_worker( + format!( + "try {{ postMessage(JSON.parse({})); }} catch (error) {{ postMessage(String(error)); }} close();", + include_str!("../../../../tests/fixtures/webgl-drawing-capabilities.js") + ), + "https://webgl-drawing-capabilities.test/worker.js".into(), + ); + let message = timeout(TIMEOUT, handle.recv()) + .await + .expect("worker WebGL drawing capability task must settle") + .expect("worker must return the drawing capability result"); + assert_eq!( + expect_post_json(message), + r#"["offscreen:webgl","offscreen:webgl2"]"# + ); +} + #[tokio::test] async fn worker_webgl_compatibility_profile_matches_the_window_profile() { ensure_v8(); diff --git a/moli-renderer-v8/tests/fixtures/webgl-drawing-capabilities.js b/moli-renderer-v8/tests/fixtures/webgl-drawing-capabilities.js new file mode 100644 index 0000000000..40ec37aa46 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/webgl-drawing-capabilities.js @@ -0,0 +1,105 @@ +(() => { + const results = []; + const check = (value, message) => { if (!value) throw new Error(message); }; + const throws = (callback, message) => { + let name; try { callback(); } catch (error) { name = error.name; } + check(name === 'TypeError', message); + }; + const make = offscreen => offscreen ? new OffscreenCanvas(300, 150) : document.createElement('canvas'); + for (const kind of ['webgl', 'webgl2']) { + for (const offscreen of typeof document === 'object' ? [false, true] : [true]) { + const label = `${offscreen ? 'offscreen' : 'html'}:${kind}`; + const canvas = make(offscreen), gl = canvas.getContext(kind); + check(!!gl, `${label}: context available`); + const proto = Object.getPrototypeOf(gl); + for (const field of ['drawingBufferWidth', 'drawingBufferHeight']) { + const d = Object.getOwnPropertyDescriptor(proto, field); + check(d && typeof d.get === 'function' && d.set === undefined && d.enumerable && d.configurable, + `${label}: native readonly ${field}`); + throws(() => d.get.call({}), `${label}: ${field} receiver`); + throws(() => d.get.call(Object.create(proto)), `${label}: ${field} forged receiver`); + } + check(gl.drawingBufferWidth === 300 && gl.drawingBufferHeight === 150, `${label}: default extent`); + for (const [width, height] of [[319, 173], [0, 173], [0, 0], [23, 17]]) { + canvas.width = width; canvas.height = height; + check(gl.drawingBufferWidth === Math.max(1, width) && gl.drawingBufferHeight === Math.max(1, height), + `${label}: resize extent`); + } + Object.defineProperty(canvas, 'width', {configurable: true, get(){ throw new Error('page width getter'); }}); + Object.defineProperty(canvas, 'height', {configurable: true, get(){ throw new Error('page height getter'); }}); + Object.defineProperty(gl, 'canvas', {configurable: true, value: {width: 999, height: 999}}); + check(gl.drawingBufferWidth === 23 && gl.drawingBufferHeight === 17, `${label}: extent uses native owner and size`); + delete canvas.width; delete canvas.height; delete gl.canvas; + // A minimal real shader pair keeps uniform locations valid on Chromium. + const program = gl.createProgram(); + for (const [type, source] of [ + [gl.VERTEX_SHADER, 'attribute vec4 p; uniform mat2 m2; uniform mat3 m3; uniform mat4 m4; void main(){ gl_Position=m4*vec4(m3*vec3(m2*p.xy,p.z),p.w); }'], + [gl.FRAGMENT_SHADER, 'precision mediump float; void main(){gl_FragColor=vec4(1.0);}'], + ]) { + const shader = gl.createShader(type); + gl.shaderSource(shader, source); gl.compileShader(shader); + check(gl.getShaderParameter(shader, gl.COMPILE_STATUS), `${label}: shader compile`); + gl.attachShader(program, shader); + } + gl.linkProgram(program); gl.useProgram(program); + check(gl.getProgramParameter(program, gl.LINK_STATUS), `${label}: program linked`); + const other = make(offscreen).getContext(kind); + for (const size of [2, 3, 4]) { + const name = `uniformMatrix${size}fv`, method = gl[name]; + const d = Object.getOwnPropertyDescriptor(proto, name), count = size * size; + check(d?.value === method && method.name === name && method.length === 3 && d.enumerable && d.configurable && d.writable, + `${label}: matrix method descriptor`); + const location = gl.getUniformLocation(program, `m${size}`); + check(location !== null, `${label}: live matrix location`); + other[name](location, false, new Float32Array(count)); + check(other.getError() === 0x0502, `${label}: location belongs to its context (INVALID_OPERATION)`); + throws(() => method.call({}, null, false, []), `${label}: matrix receiver`); + throws(() => method.call(Object.create(proto), null, false, []), `${label}: forged matrix receiver`); + throws(() => method.call(gl), `${label}: required arguments`); + let iterated = 0; + const values = {[Symbol.iterator](){iterated++; return Array(count).fill(0)[Symbol.iterator]();}}; + throws(() => method.call(gl, {}, false, values), `${label}: invalid location`); + check(iterated === 0, `${label}: validate location before data conversion`); + check(method.call(gl, location, false, values) === undefined && gl.getError() === gl.NO_ERROR && iterated === 1, + `${label}: iterable values`); + const typed = new Float32Array(count); + Object.defineProperty(typed, Symbol.iterator, {get(){throw new Error('typed-array iterator must not be consulted');}}); + method.call(gl, location, false, typed); + check(gl.getError() === gl.NO_ERROR, `${label}: typed-array union branch`); + method.call(gl, location, false, new Float64Array(count)); + check(gl.getError() === gl.NO_ERROR, `${label}: other typed array uses iterable branch`); + throws(() => method.call(gl, location, false, null), `${label}: data cannot be null`); + throws(() => method.call(gl, location, false, [1n]), `${label}: numeric element conversion`); + const sentinel = new Error('iterator failure'); + let caught; + try {method.call(gl, location, false, {[Symbol.iterator](){throw sentinel;}});} + catch (error) {caught = error;} + check(caught === sentinel, `${label}: preserve iterator exception`); + method.call(gl, location, false, []); + check(gl.getError() === gl.INVALID_VALUE, `${label}: empty matrix error`); + method.call(gl, null, false, new Float32Array(0)); + check(gl.getError() === gl.INVALID_VALUE, `${label}: empty data precedes null-location no-op`); + method.call(gl, null, true, [1]); + check(gl.getError() === gl.NO_ERROR, `${label}: converted nonempty null-location call ignored`); + method.call(gl, location, false, Array(count - 1).fill(0)); + check(gl.getError() === gl.INVALID_VALUE, `${label}: incomplete matrix`); + method.call(gl, location, true, Array(count).fill(0)); + check(gl.getError() === (kind === 'webgl2' ? gl.NO_ERROR : gl.INVALID_VALUE), `${label}: transpose version policy`); + method.call(gl, location, false, Array(count + 1).fill(0), 1); + check(gl.getError() === (kind === 'webgl2' ? gl.NO_ERROR : gl.INVALID_VALUE), `${label}: srcOffset overload`); + method.call(gl, location, false, Array(count + 2).fill(0), 1, count); + check(gl.getError() === (kind === 'webgl2' ? gl.NO_ERROR : gl.INVALID_VALUE), `${label}: srcLength overload`); + method.call(gl, location, false, Array(count).fill(0), count); + check(gl.getError() === (kind === 'webgl2' ? gl.INVALID_VALUE : gl.NO_ERROR), `${label}: offset past end`); + const offset = {valueOf(){throw new TypeError('offset conversion');}}; + if (kind === 'webgl2') throws(() => method.call(gl, null, false, typed, offset), `${label}: offset WebIDL`); + else method.call(gl, null, false, typed, offset); + check(gl.getError() === gl.NO_ERROR, `${label}: WebIDL exceptions are not GL errors`); + } + gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + check(Array.from(gl.getParameter(gl.VIEWPORT)).join(',') === '0,0,23,17', `${label}: native extents feed viewport`); + results.push(label); + } + } + return JSON.stringify(results); +})()