fix(webgl): align query types, identity strings and GL errors

This commit is contained in:
ldm0
2026-09-28 11:27:11 +08:00
parent 1deef16c16
commit f46eece994
4 changed files with 218 additions and 42 deletions
@@ -12,6 +12,7 @@ const WEBGL_VIEWPORT_SLOT: &str = "__moliWebGlViewport";
const WEBGL_ERROR_SLOT: &str = "__moliWebGlError";
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_MAX_VIEWPORT_DIMS: [i32; 2] = [8192, 8192];
const WEBGL2_DRAWING_BUFFER_COLOR_SPACE_SLOT: &str = "__moliWebGl2DrawingBufferColorSpace";
@@ -48,6 +49,15 @@ struct WebGlGetParameterArgs {
pname: u32,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "WebGLRenderingContext.getShaderPrecisionFormat")]
struct WebGlGetShaderPrecisionFormatArgs {
#[webidl(required)]
shader_type: u32,
#[webidl(required)]
precision_type: u32,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "WebGLRenderingContext.viewport")]
struct WebGlViewportArgs {
@@ -67,7 +77,7 @@ struct WebGlContextStateDeclaration<'s> {
#[webapi(slot = WEBGL_VIEWPORT_SLOT)]
viewport: v8::Local<'s, v8::Array>,
#[webapi(slot = WEBGL_ERROR_SLOT)]
error: u32,
errors: v8::Local<'s, v8::Set>,
#[webapi(slot = WEBGL_EXTENSIONS_SLOT)]
extensions: v8::Local<'s, v8::Map>,
}
@@ -151,6 +161,7 @@ struct WebGl2ContextObjectDeclaration {
pub(crate) const WEBGL_CONSTANTS: &[(&str, u32)] = &[
("VIEWPORT", WEBGL_VIEWPORT),
("NO_ERROR", 0),
("INVALID_ENUM", WEBGL_INVALID_ENUM),
("INVALID_VALUE", WEBGL_INVALID_VALUE),
("DEPTH_TEST", 0x0B71),
("LEQUAL", 0x0203),
@@ -193,6 +204,7 @@ pub(crate) const WEBGL_CONSTANTS: &[(&str, u32)] = &[
("LOW_INT", 0x8DF3),
("MEDIUM_INT", 0x8DF4),
("HIGH_INT", 0x8DF5),
("COMPRESSED_TEXTURE_FORMATS", 0x86A3),
];
pub(crate) const WEBGL2_CONSTANTS: &[(&str, u32)] = &[
@@ -227,7 +239,6 @@ pub(crate) const WEBGL2_CONSTANTS: &[(&str, u32)] = &[
("MAX_SERVER_WAIT_TIMEOUT", 0x9111),
("MAX_ELEMENT_INDEX", 0x8D6B),
("MAX_CLIENT_WAIT_TIMEOUT_WEBGL", 0x9247),
("COMPRESSED_TEXTURE_FORMATS", 0x86A3),
("RENDERBUFFER", 0x8D41),
("SAMPLES", 0x80A9),
("FRAMEBUFFER", 0x8D40),
@@ -364,15 +375,17 @@ pub(crate) fn webgl_get_parameter_callback<'s>(
webgl2_get_parameter_callback(scope, args, rv);
return;
}
let Some(extensions) = webgl_extensions(scope, args.this()) else {
return;
};
let Some(parsed) = webidl::parse_args::<WebGlGetParameterArgs>(scope, &args) else {
rv.set_undefined();
return;
};
match parsed.pname {
WEBGL_VIEWPORT => return_webgl_viewport(scope, args.this(), &mut rv),
0x846D | 0x846E => rv.set(
webgl_array_value(scope, &[1, 1]).unwrap_or_else(|| v8::Array::new(scope, 0).into()),
),
0x846D | 0x846E => rv.set(webgl_float32_array(scope, &[1.0, 1.0])),
0x86A3 => rv.set(webgl_uint32_array(scope, &[])),
0x0D3A => rv.set(webgl_int32_array(scope, &WEBGL_MAX_VIEWPORT_DIMS)),
0x0D52..=0x0D55 => rv.set(v8::Integer::new(scope, 8).into()),
0x0D56 => rv.set(v8::Integer::new(scope, 24).into()),
@@ -382,11 +395,19 @@ pub(crate) fn webgl_get_parameter_callback<'s>(
0x8872 | 0x8B4C => rv.set(v8::Integer::new(scope, 8).into()),
0x8B4D => rv.set(v8::Integer::new(scope, 16).into()),
0x8DFB..=0x8DFD => rv.set(v8::Integer::new(scope, 128).into()),
0x1F00 | 0x9245 => rv.set(v8::String::empty(scope).into()),
0x1F01 | 0x9246 => rv.set(v8::String::empty(scope).into()),
0x1F02 => rv.set(v8::String::new(scope, "WebGL 1.0").unwrap().into()),
0x8B8C => rv.set(v8::String::new(scope, "WebGL GLSL ES 1.0").unwrap().into()),
_ => rv.set(v8::null(scope).into()),
// These masked API strings match the Chromium service surface, not a
// claim about the physical GPU. Unmasked hardware identity stays empty.
0x1F00 => rv.set(v8str(scope, "WebKit").into()),
0x1F01 => rv.set(v8str(scope, "WebKit WebGL").into()),
0x9245 | 0x9246 if webgl_debug_renderer_info_enabled(scope, extensions) => {
rv.set(v8::String::empty(scope).into())
}
0x1F02 => rv.set(v8str(scope, "WebGL 1.0 (OpenGL ES 2.0 Chromium)").into()),
0x8B8C => rv.set(v8str(scope, "WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)").into()),
_ => {
record_webgl_error(scope, args.this(), WEBGL_INVALID_ENUM);
rv.set_null();
}
}
}
@@ -395,6 +416,9 @@ pub(crate) fn webgl2_get_parameter_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(extensions) = webgl_extensions(scope, args.this()) else {
return;
};
let Some(parsed) = webidl::parse_args::<WebGl2GetParameterArgs>(scope, &args) else {
rv.set_undefined();
return;
@@ -425,7 +449,9 @@ pub(crate) fn webgl2_get_parameter_callback<'s>(
0x8A34 => rv.set(v8::Integer::new(scope, 256).into()),
0x1F00 => rv.set(v8str(scope, "WebKit").into()),
0x1F01 => rv.set(v8str(scope, "WebKit WebGL").into()),
0x9245 | 0x9246 => rv.set(v8::String::empty(scope).into()),
0x9245 | 0x9246 if webgl_debug_renderer_info_enabled(scope, extensions) => {
rv.set(v8::String::empty(scope).into())
}
0x1F02 => rv.set(v8str(scope, "WebGL 2.0 (OpenGL ES 3.0 Chromium)").into()),
0x8B8C => {
rv.set(v8str(scope, "WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)").into())
@@ -433,17 +459,28 @@ pub(crate) fn webgl2_get_parameter_callback<'s>(
0x86A3 => rv.set(webgl_uint32_array(scope, &[])),
0x0D33 | 0x84E8 | 0x851C => rv.set(v8::Integer::new(scope, 8192).into()),
0x0D3A => rv.set(webgl_int32_array(scope, &WEBGL_MAX_VIEWPORT_DIMS)),
0x846D | 0x846E => rv.set(webgl_int32_array(scope, &[1, 1])),
0x846D | 0x846E => rv.set(webgl_float32_array(scope, &[1.0, 1.0])),
0x0D52..=0x0D55 => rv.set(v8::Integer::new(scope, 8).into()),
0x0D56 => rv.set(v8::Integer::new(scope, 24).into()),
0x0D57 => rv.set(v8::Integer::new(scope, 0).into()),
0x8869 => rv.set(v8::Integer::new(scope, 16).into()),
0x8872 | 0x8B4C => rv.set(v8::Integer::new(scope, 16).into()),
0x8B4D => rv.set(v8::Integer::new(scope, 64).into()),
_ => rv.set(v8::null(scope).into()),
_ => {
record_webgl_error(scope, args.this(), WEBGL_INVALID_ENUM);
rv.set_null();
}
}
}
fn webgl_debug_renderer_info_enabled(
scope: &mut v8::PinScope<'_, '_>,
extensions: v8::Local<'_, v8::Map>,
) -> bool {
let key = v8str(scope, WebGlExtension::DebugRendererInfo.into()).into();
extensions.has(scope, key) == Some(true)
}
pub(crate) fn webgl2_get_internalformat_parameter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -486,6 +523,21 @@ fn webgl_int32_array<'s>(
.into()
}
fn webgl_float32_array<'s>(
scope: &mut v8::PinScope<'s, '_>,
values: &[f32],
) -> v8::Local<'s, v8::Value> {
let mut bytes = Vec::with_capacity(std::mem::size_of_val(values));
for value in values {
bytes.extend_from_slice(&value.to_ne_bytes());
}
let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared();
let buffer = v8::ArrayBuffer::with_backing_store(scope, &backing_store);
v8::Float32Array::new(scope, buffer, 0, values.len())
.expect("WebGL Float32Array construction should succeed")
.into()
}
fn webgl_uint32_array<'s>(
scope: &mut v8::PinScope<'s, '_>,
values: &[u32],
@@ -588,17 +640,35 @@ pub(crate) fn webgl_get_error_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(error) = get_private_value(scope, args.this(), WEBGL_ERROR_SLOT) else {
let Some(errors) = get_private_value(scope, args.this(), WEBGL_ERROR_SLOT)
.and_then(|value| v8::Local::<v8::Set>::try_from(value).ok())
else {
throw_type_error(scope, "Illegal invocation");
return;
};
if errors.size() == 0 {
rv.set_uint32(0);
return;
}
let error = errors
.as_array(scope)
.get_index(scope, 0)
.expect("pending WebGL errors have an own first entry");
errors.delete(scope, error);
rv.set(error);
set_private_value(
scope,
args.this(),
WEBGL_ERROR_SLOT,
v8::Integer::new(scope, 0).into(),
);
}
fn record_webgl_error<'s>(
scope: &mut v8::PinScope<'s, '_>,
context: v8::Local<'s, v8::Object>,
error: u32,
) {
let errors = get_private_value(scope, context, WEBGL_ERROR_SLOT)
.and_then(|value| v8::Local::<v8::Set>::try_from(value).ok())
.expect("validated WebGL receiver has error state");
// Like Blink's synthetic error list, preserve distinct pending errors but
// coalesce repeated occurrences until getError consumes them.
errors.add(scope, v8::Integer::new_from_unsigned(scope, error).into());
}
pub(crate) fn webgl_viewport_callback<'s>(
@@ -616,12 +686,7 @@ pub(crate) fn webgl_viewport_callback<'s>(
if parsed.width < 0 || parsed.height < 0 {
// GL errors preserve the previous viewport and remain pending until
// getError consumes them. WebIDL conversion failures throw instead.
set_private_value(
scope,
args.this(),
WEBGL_ERROR_SLOT,
v8::Integer::new_from_unsigned(scope, WEBGL_INVALID_VALUE).into(),
);
record_webgl_error(scope, args.this(), WEBGL_INVALID_VALUE);
return;
}
set_webgl_viewport(
@@ -675,7 +740,8 @@ pub(super) fn init_webgl_context_object<'s>(
let values = [0, 0, 300, 150].map(|value| v8::Integer::new(scope, value).into());
let viewport = v8::Array::new_with_elements(scope, &values);
let extensions = v8::Map::new(scope);
WebGlContextStateDeclaration::new(viewport, 0, extensions)
let errors = v8::Set::new(scope);
WebGlContextStateDeclaration::new(viewport, errors, extensions)
.initialize(scope, context)
.expect("WebGL context state should initialize");
}
@@ -778,12 +844,36 @@ pub(crate) fn webgl_is_context_lost_callback(
rv.set(v8::Boolean::new(scope, false).into());
}
pub(crate) fn webgl_get_shader_precision_format_callback(
scope: &mut v8::PinScope<'_, '_>,
_args: v8::FunctionCallbackArguments<'_>,
pub(crate) fn webgl_get_shader_precision_format_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let value = WebGlShaderPrecisionFormat::default()
if webgl_extensions(scope, args.this()).is_none() {
return;
}
let Some(parsed) = webidl::parse_args::<WebGlGetShaderPrecisionFormatArgs>(scope, &args) else {
return;
};
if !matches!(parsed.shader_type, 0x8B30 | 0x8B31) {
record_webgl_error(scope, args.this(), WEBGL_INVALID_ENUM);
rv.set_null();
return;
}
// A stable software-GL precision profile. Integer precision is always 0;
// the floating-point mantissa width is not meaningful for integer formats.
let (range_min, range_max, precision) = match parsed.precision_type {
0x8DF0 | 0x8DF1 => (15, 15, 10),
0x8DF2 => (127, 127, 23),
0x8DF3 | 0x8DF4 => (15, 14, 0),
0x8DF5 => (31, 30, 0),
_ => {
record_webgl_error(scope, args.this(), WEBGL_INVALID_ENUM);
rv.set_null();
return;
}
};
let value = WebGlShaderPrecisionFormat::new(precision, range_min, range_max)
.bind(scope)
.expect("WebGL shader precision format declaration should bind");
rv.set(value.into());
@@ -804,16 +894,6 @@ struct WebGlShaderPrecisionFormat {
range_max: i32,
}
impl Default for WebGlShaderPrecisionFormat {
fn default() -> Self {
Self {
precision: 23,
range_min: 127,
range_max: 127,
}
}
}
pub(crate) fn webgl_lose_context_noop_callback(
_scope: &mut v8::PinScope<'_, '_>,
_args: v8::FunctionCallbackArguments<'_>,
@@ -1,5 +1,17 @@
use super::*;
#[test]
fn webgl_basic_queries_preserve_types_extension_gates_and_errors() {
let mut vm = new_storage_test_vm("https://webgl-query-contract.test/");
let result = vm
.eval(include_str!("../../../tests/fixtures/webgl-queries.js"))
.expect("WebGL queries should preserve their types and error contracts");
assert_eq!(
result,
r#"["html:webgl","offscreen:webgl","html:webgl2","offscreen:webgl2"]"#
);
}
#[test]
fn webgl_extension_registry_and_context_identity_are_consistent() {
let mut vm = new_storage_test_vm("https://webgl-extensions.test/");
@@ -461,7 +473,7 @@ fn webgl_fingerprint_pipeline_methods_are_available() {
assert_eq!(
result,
r#"{"methods":["function","function","function","function","function"],"handles":["object","object","object","object"],"attrib":0,"constants":[34962,35044,35633,35632,5],"descriptor":[34962,true,false,false],"ownerCanvas":{"same":true,"ownEnumerable":false,"dataUrl":"string","hashTail":"SuQmCC"},"parameters":{"line":[1,1],"viewport":[8192,8192],"redBits":8,"vendor":"","version":"WebGL 1.0","missing":null},"precision":{"precision":23,"rangeMin":127,"rangeMax":127},"shaderStatus":true,"programStatus":true,"shaderLog":"","error":0}"#
r#"{"methods":["function","function","function","function","function"],"handles":["object","object","object","object"],"attrib":0,"constants":[34962,35044,35633,35632,5],"descriptor":[34962,true,false,false],"ownerCanvas":{"same":true,"ownEnumerable":false,"dataUrl":"string","hashTail":"SuQmCC"},"parameters":{"line":[1,1],"viewport":[8192,8192],"redBits":8,"vendor":"WebKit","version":"WebGL 1.0 (OpenGL ES 2.0 Chromium)","missing":null},"precision":{"precision":23,"rangeMin":127,"rangeMax":127},"shaderStatus":true,"programStatus":true,"shaderLog":"","error":1280}"#
);
}
@@ -2082,6 +2082,26 @@ async fn worker_offscreen_canvas_exposes_webgl_identity_consistently() {
);
}
#[tokio::test]
async fn worker_webgl_queries_match_the_window_contract() {
ensure_v8();
let mut handle = spawn_worker(
format!(
"postMessage(JSON.parse({})); close();",
include_str!("../../../../tests/fixtures/webgl-queries.js")
),
"test://worker_webgl_queries".into(),
);
let msg = timeout(TIMEOUT, handle.recv())
.await
.expect("timed out")
.expect("channel closed");
assert_eq!(
expect_post_json(msg),
r#"["offscreen:webgl","offscreen:webgl2"]"#
);
}
#[tokio::test]
async fn worker_webgl_extensions_use_intrinsic_objects_and_context_local_identity() {
ensure_v8();
+64
View File
@@ -0,0 +1,64 @@
// No GPU-specific identity or numeric limit is assumed by this contract probe.
(() => {
const results = [];
for (const kind of ['webgl', 'webgl2']) {
for (const offscreen of typeof document === 'undefined' ? [true] : [false, true]) {
const label = `${offscreen ? 'offscreen' : 'html'}:${kind}`;
const check = (condition, message) => {
if (!condition) throw new Error(`${label}: ${message}`);
};
const create = () => (offscreen ? new OffscreenCanvas(1, 1) : document.createElement('canvas')).getContext(kind);
const gl = create();
check(gl.getParameter(gl.VENDOR) === 'WebKit', 'masked vendor');
check(gl.getParameter(gl.RENDERER) === 'WebKit WebGL', 'masked renderer');
check(gl.getParameter(gl.VERSION).startsWith(kind === 'webgl' ? 'WebGL 1.0 (' : 'WebGL 2.0 ('), 'version format');
check(gl.getParameter(gl.SHADING_LANGUAGE_VERSION).startsWith('WebGL GLSL ES '), 'shading language version');
gl.getSupportedExtensions();
check(gl.getParameter(37445) === null, 'enumerating extensions does not enable debug queries');
check(gl.getError() === gl.INVALID_ENUM, 'disabled debug query signals an error');
const debug = gl.getExtension('webgl_debug_renderer_info');
for (const pname of [debug.UNMASKED_VENDOR_WEBGL, debug.UNMASKED_RENDERER_WEBGL]) {
check(typeof gl.getParameter(pname) === 'string', 'enabled debug query returns a string');
}
const other = create();
check(other.getParameter(37446) === null && other.getError() === other.INVALID_ENUM, 'activation is context-local');
for (const pname of [gl.ALIASED_LINE_WIDTH_RANGE, gl.ALIASED_POINT_SIZE_RANGE]) {
const range = gl.getParameter(pname);
check(range instanceof Float32Array && range.length === 2, 'range query is Float32Array');
const first = range[0];
range[0] = NaN;
check(gl.getParameter(pname)[0] === first, 'range query returns a copy');
}
check(gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS) instanceof Uint32Array, 'compressed formats query type');
check(gl.getParameter(gl.MAX_VIEWPORT_DIMS) instanceof Int32Array, 'viewport limits query type');
for (const shader of [gl.VERTEX_SHADER, gl.FRAGMENT_SHADER]) {
for (const name of ['LOW_INT', 'MEDIUM_INT', 'HIGH_INT']) {
const precision = gl.getShaderPrecisionFormat(shader, gl[name]);
check(precision.precision === 0 && precision.rangeMin > 0 && precision.rangeMax > 0, 'integer precision');
}
check(gl.getShaderPrecisionFormat(shader, gl.HIGH_FLOAT).precision > 0, 'floating-point precision');
}
check(gl.getError() === gl.NO_ERROR, 'valid queries leave no error');
for (const call of [() => gl.getParameter(), () => gl.getParameter.call({}, gl.VENDOR),
() => gl.getShaderPrecisionFormat(gl.VERTEX_SHADER),
() => gl.getShaderPrecisionFormat.call({}, gl.VERTEX_SHADER, gl.HIGH_FLOAT)]) {
let threw = false;
try { call(); } catch (error) { threw = error instanceof TypeError; }
check(threw, 'query checks receiver and required arguments');
}
check(gl.getError() === gl.NO_ERROR, 'WebIDL exceptions do not create GL errors');
check(gl.getParameter(0xffffffff) === null, 'unknown enum returns null');
check(gl.getParameter(0xffffffff) === null, 'repeat invalid query returns null');
gl.viewport(0, 0, -1, 1);
check(gl.getError() === gl.INVALID_ENUM, 'first pending error');
check(gl.getError() === gl.INVALID_VALUE, 'distinct error is preserved');
check(gl.getError() === gl.NO_ERROR, 'repeated pending errors coalesce');
check(gl.getShaderPrecisionFormat(0, gl.HIGH_FLOAT) === null, 'invalid shader enum');
check(gl.getError() === gl.INVALID_ENUM, 'invalid shader enum error');
check(gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, 0) === null, 'invalid precision enum');
check(gl.getError() === gl.INVALID_ENUM, 'invalid precision enum error');
results.push(label);
}
}
return JSON.stringify(results);
})()