From d81619739ed73d4d5dfa3742019b3355cfac48b8 Mon Sep 17 00:00:00 2001 From: dsafdsaf132 Date: Wed, 10 Jun 2026 13:28:09 +0900 Subject: [PATCH 1/2] Add context extension controls --- README.md | 11 ++++ binding/egl_context_wrapper.h | 3 + binding/webgl_rendering_context.cc | 101 ++++++++++++++++++++++++++--- binding/webgl_rendering_context.h | 5 ++ src/binding.ts | 93 +++++++++++++++++++++++++- src/index.ts | 13 +++- test/webgl2-smoke.js | 58 ++++++++++++++++- 7 files changed, 271 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 97c6477..d9bd4ed 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,17 @@ const gl = nodeGles.createWebGLRenderingContext({ console.log(gl.getParameter(gl.VERSION)); ``` +### Context Options + +- `width` / `height`: drawing buffer size, default `1`. +- `majorVersion` / `minorVersion`: requested OpenGL ES version, default `3.0`. +- `webGLCompatibility`: requests ANGLE WebGL compatibility mode. The legacy + misspelled `webGLCompability` option is still accepted as an alias. +- `enabledExtensions`: optional WebGL extension allowlist. When set, only listed + supported extensions are exposed. +- `disabledExtensions`: optional WebGL extension blocklist. This takes + precedence over `enabledExtensions`. + ## Context Lifecycle For batch rendering, call `gl.destroy()` or `gl.dispose()` after the final diff --git a/binding/egl_context_wrapper.h b/binding/egl_context_wrapper.h index 018ae82..db9eedd 100644 --- a/binding/egl_context_wrapper.h +++ b/binding/egl_context_wrapper.h @@ -42,6 +42,9 @@ struct GLContextOptions { uint32_t client_minor_es_version = 0; uint32_t width = 1; uint32_t height = 1; + bool has_enabled_extensions_filter = false; + std::vector enabled_extensions; + std::vector disabled_extensions; }; // Provides lookup of EGL/GL extensions. diff --git a/binding/webgl_rendering_context.cc b/binding/webgl_rendering_context.cc index d520d5f..2ac4415 100644 --- a/binding/webgl_rendering_context.cc +++ b/binding/webgl_rendering_context.cc @@ -412,6 +412,44 @@ static napi_status GetStringParam(napi_env env, napi_value string_value, return napi_ok; } +static napi_status GetOptionalStringArrayParam(napi_env env, napi_value value, + const char *name, + std::vector *out) { + napi_valuetype value_type; + napi_status nstatus = napi_typeof(env, value, &value_type); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); + + if (value_type == napi_undefined || value_type == napi_null) { + return napi_ok; + } + + bool is_array = false; + nstatus = napi_is_array(env, value, &is_array); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); + if (!is_array) { + std::string message = std::string(name) + " must be an array"; + NAPI_THROW_ERROR(env, message.c_str()); + return napi_invalid_arg; + } + + uint32_t length = 0; + nstatus = napi_get_array_length(env, value, &length); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); + + for (uint32_t i = 0; i < length; ++i) { + napi_value item; + nstatus = napi_get_element(env, value, i, &item); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); + + std::string extension_name; + nstatus = GetStringParam(env, item, extension_name); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); + out->push_back(extension_name); + } + + return napi_ok; +} + static napi_status CreateInfoLogString(napi_env env, const char *log, GLsizei length, napi_value *result) { if (log == nullptr || length <= 0) { @@ -1976,7 +2014,10 @@ WebGLRenderingContext::WebGLRenderingContext(napi_env env, ref_(nullptr), eglContextWrapper_(nullptr), drawing_buffer_color_space_("srgb"), - unpack_color_space_("srgb") { + unpack_color_space_("srgb"), + has_enabled_extensions_filter_(opts.has_enabled_extensions_filter), + enabled_extensions_(opts.enabled_extensions), + disabled_extensions_(opts.disabled_extensions) { eglContextWrapper_ = EGLContextWrapper::Create(env, opts); if (!eglContextWrapper_) { bool exception_pending = false; @@ -3061,13 +3102,13 @@ napi_status WebGLRenderingContext::NewInstance(napi_env env, nstatus = napi_get_reference_value(env, constructor_ref_, &ctor_value); ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); - size_t argc = 5; - napi_value args[5]; + size_t argc = 7; + napi_value args[7]; napi_value js_this; nstatus = napi_get_cb_info(env, info, &argc, args, &js_this, nullptr); ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); ENSURE_ARGC_RETVAL(env, argc, 5, nstatus); - argc = std::min(argc, static_cast(5)); + argc = std::min(argc, static_cast(7)); nstatus = napi_new_instance(env, ctor_value, argc, args, instance); ENSURE_NAPI_OK_RETVAL(env, nstatus, nstatus); @@ -3082,8 +3123,8 @@ napi_value WebGLRenderingContext::InitInternal(napi_env env, ENSURE_CONSTRUCTOR_CALL_RETVAL(env, info, nullptr); - size_t argc = 5; - napi_value args[5]; + size_t argc = 7; + napi_value args[7]; napi_value js_this; nstatus = napi_get_cb_info(env, info, &argc, args, &js_this, nullptr); ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr); @@ -3105,6 +3146,23 @@ napi_value WebGLRenderingContext::InitInternal(napi_env env, nstatus = napi_get_value_bool(env, args[4], &opts.webgl_compatibility); ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr); + if (argc > 5) { + napi_valuetype value_type; + nstatus = napi_typeof(env, args[5], &value_type); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr); + opts.has_enabled_extensions_filter = + value_type != napi_undefined && value_type != napi_null; + nstatus = GetOptionalStringArrayParam(env, args[5], "enabledExtensions", + &opts.enabled_extensions); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr); + } + + if (argc > 6) { + nstatus = GetOptionalStringArrayParam(env, args[6], "disabledExtensions", + &opts.disabled_extensions); + ENSURE_NAPI_OK_RETVAL(env, nstatus, nullptr); + } + WebGLRenderingContext *context = new (std::nothrow) WebGLRenderingContext(env, opts); if (context == nullptr) { @@ -6172,6 +6230,16 @@ static bool ExtensionNameEqualsIgnoringCase(const std::string &input, return i == input.size() && extension[i] == '\0'; } +static bool ExtensionListContains(const std::vector &extensions, + const char *extension) { + for (const std::string &entry : extensions) { + if (ExtensionNameEqualsIgnoringCase(entry, extension)) { + return true; + } + } + return false; +} + static bool SupportsANGLEInstancedArrays(EGLContextWrapper *egl_ctx) { return (egl_ctx->glDrawArraysInstanced && egl_ctx->glDrawElementsInstanced && egl_ctx->glVertexAttribDivisor) || @@ -6329,6 +6397,21 @@ static void AddUniqueExtension(std::vector *extensions, } } +bool WebGLRenderingContext::IsExtensionAllowed( + const char *extension_name) const { + if (extension_name == nullptr || extension_name[0] == '\0') { + return false; + } + if (!enabled_extensions_.empty() && + !ExtensionListContains(enabled_extensions_, extension_name)) { + return false; + } + if (has_enabled_extensions_filter_ && enabled_extensions_.empty()) { + return false; + } + return !ExtensionListContains(disabled_extensions_, extension_name); +} + /* static */ napi_value WebGLRenderingContext::GetExtension(napi_env env, napi_callback_info info) { @@ -6356,7 +6439,8 @@ napi_value WebGLRenderingContext::GetExtension(napi_env env, EGLContextWrapper *egl_ctx = context->eglContextWrapper_; napi_value webgl_extension = nullptr; - if (extension != nullptr && extension->is_supported(egl_ctx)) { + if (extension != nullptr && context->IsExtensionAllowed(extension->name) && + extension->is_supported(egl_ctx)) { nstatus = extension->new_instance(env, js_this, egl_ctx, &webgl_extension); } else { nstatus = napi_get_null(env, &webgl_extension); @@ -7842,7 +7926,8 @@ napi_value WebGLRenderingContext::GetSupportedExtensions( std::vector supported_extensions; for (const WebGLExtensionDescriptor &extension : kKnownWebGLExtensions) { - if (extension.is_supported(context->eglContextWrapper_)) { + if (context->IsExtensionAllowed(extension.name) && + extension.is_supported(context->eglContextWrapper_)) { AddUniqueExtension(&supported_extensions, extension.name); } } diff --git a/binding/webgl_rendering_context.h b/binding/webgl_rendering_context.h index 56833ec..08a005f 100644 --- a/binding/webgl_rendering_context.h +++ b/binding/webgl_rendering_context.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "egl_context_wrapper.h" @@ -58,6 +59,7 @@ class WebGLRenderingContext { WebGLRenderingContext(napi_env env, GLContextOptions opts); ~WebGLRenderingContext(); void DestroyNativeResources(); + bool IsExtensionAllowed(const char* extension_name) const; static napi_value InitInternal(napi_env env, napi_callback_info info); static void Cleanup(napi_env env, void* native, void* hint); @@ -346,6 +348,9 @@ class WebGLRenderingContext { PixelStoreState pixel_store_state_; bool supports_webgl2_pixel_store_; std::deque pending_errors_; + bool has_enabled_extensions_filter_; + std::vector enabled_extensions_; + std::vector disabled_extensions_; std::atomic alloc_count_; }; diff --git a/src/binding.ts b/src/binding.ts index 83d9a0b..f1f8cf1 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -77,12 +77,67 @@ export type NodeGlesWEBGLDrawBuffers = { drawBuffersWEBGL(buffers: number[] | Uint32Array): void; }; +export type NodeGlesEXTBlendMinmax = { + readonly MAX_EXT: number; + readonly MIN_EXT: number; +}; + +export type NodeGlesEXTColorBufferFloat = {}; + +export type NodeGlesEXTColorBufferHalfFloat = {}; + +export type NodeGlesEXTFragDepth = {}; + export type NodeGlesEXTFloatBlend = {}; +export type NodeGlesEXTSRGB = { + readonly SRGB_EXT: number; + readonly SRGB_ALPHA_EXT: number; + readonly SRGB8_ALPHA8_EXT: number; + readonly FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT: number; +}; + +export type NodeGlesEXTShaderTextureLod = {}; + +export type NodeGlesEXTTextureFilterAnisotropic = { + readonly MAX_TEXTURE_MAX_ANISOTROPY_EXT: number; + readonly TEXTURE_MAX_ANISOTROPY_EXT: number; +}; + export type NodeGlesEXTTextureMirrorClampToEdge = { readonly MIRROR_CLAMP_TO_EDGE_EXT: number; }; +export type NodeGlesOESElementIndexUint = {}; + +export type NodeGlesOESStandardDerivatives = { + readonly FRAGMENT_SHADER_DERIVATIVE_HINT_OES: number; +}; + +export type NodeGlesOESTextureFloat = {}; + +export type NodeGlesOESTextureFloatLinear = {}; + +export type NodeGlesOESTextureHalfFloat = { + readonly HALF_FLOAT_OES: number; +}; + +export type NodeGlesOESTextureHalfFloatLinear = {}; + +export type NodeGlesWEBGLDebugRendererInfo = { + readonly UNMASKED_VENDOR_WEBGL: number; + readonly UNMASKED_RENDERER_WEBGL: number; +}; + +export type NodeGlesWEBGLDepthTexture = { + readonly UNSIGNED_INT_24_8_WEBGL: number; +}; + +export type NodeGlesWEBGLLoseContext = { + loseContext(): void; + restoreContext(): void; +}; + export type NodeGlesWebGL2RenderingContext = WebGL2RenderingContext & { drawingBufferColorSpace: "srgb" | "display-p3"; readonly drawingBufferFormat: number; @@ -189,14 +244,48 @@ export type NodeGlesWebGL2RenderingContext = WebGL2RenderingContext & { srcOffsetOrOffset?: number, srcLengthOverride?: number): void; getExtension(extensionName: "ANGLE_instanced_arrays"): NodeGlesANGLEInstancedArrays | null; + getExtension(extensionName: "EXT_blend_minmax"): + NodeGlesEXTBlendMinmax | null; + getExtension(extensionName: "EXT_color_buffer_float"): + NodeGlesEXTColorBufferFloat | null; + getExtension(extensionName: "WEBGL_color_buffer_float"): + NodeGlesEXTColorBufferFloat | null; + getExtension(extensionName: "EXT_color_buffer_half_float"): + NodeGlesEXTColorBufferHalfFloat | null; + getExtension(extensionName: "EXT_frag_depth"): + NodeGlesEXTFragDepth | null; getExtension(extensionName: "EXT_float_blend"): NodeGlesEXTFloatBlend | null; + getExtension(extensionName: "EXT_sRGB"): + NodeGlesEXTSRGB | null; + getExtension(extensionName: "EXT_shader_texture_lod"): + NodeGlesEXTShaderTextureLod | null; + getExtension(extensionName: "EXT_texture_filter_anisotropic"): + NodeGlesEXTTextureFilterAnisotropic | null; getExtension(extensionName: "EXT_texture_mirror_clamp_to_edge"): NodeGlesEXTTextureMirrorClampToEdge | null; + getExtension(extensionName: "OES_element_index_uint"): + NodeGlesOESElementIndexUint | null; + getExtension(extensionName: "OES_standard_derivatives"): + NodeGlesOESStandardDerivatives | null; + getExtension(extensionName: "OES_texture_float"): + NodeGlesOESTextureFloat | null; + getExtension(extensionName: "OES_texture_float_linear"): + NodeGlesOESTextureFloatLinear | null; + getExtension(extensionName: "OES_texture_half_float"): + NodeGlesOESTextureHalfFloat | null; + getExtension(extensionName: "OES_texture_half_float_linear"): + NodeGlesOESTextureHalfFloatLinear | null; getExtension(extensionName: "OES_vertex_array_object"): NodeGlesOESVertexArrayObject | null; + getExtension(extensionName: "WEBGL_debug_renderer_info"): + NodeGlesWEBGLDebugRendererInfo | null; + getExtension(extensionName: "WEBGL_depth_texture"): + NodeGlesWEBGLDepthTexture | null; getExtension(extensionName: "WEBGL_draw_buffers"): NodeGlesWEBGLDrawBuffers | null; + getExtension(extensionName: "WEBGL_lose_context"): + NodeGlesWEBGLLoseContext | null; }; export interface NodeJsGlBinding { @@ -205,6 +294,8 @@ export interface NodeJsGlBinding { height: number, client_major_es_version: number, client_minor_es_version: number, - webgl_compatbility: boolean + webgl_compatibility: boolean, + enabled_extensions?: string[], + disabled_extensions?: string[] ): WebGLRenderingContext | NodeGlesWebGL2RenderingContext; } diff --git a/src/index.ts b/src/index.ts index d931684..b804992 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,27 +21,36 @@ import {NodeJsGlBinding} from './binding'; const binding = bindings('nodejs_gl_binding') as NodeJsGlBinding; +type WebGLExtensionName = string; interface ContextArguments { width?: number, height?: number, + webGLCompatibility?: boolean, webGLCompability?: boolean, majorVersion?: number, minorVersion?: number, + enabledExtensions?: WebGLExtensionName[], + disabledExtensions?: WebGLExtensionName[], }; const createWebGLRenderingContext = function(args: ContextArguments = {}) { const width = args.width || 1; const height = args.height || 1; - const webGLCompability = args.webGLCompability || false; + const webGLCompatibility = args.webGLCompatibility === undefined ? + args.webGLCompability || false : args.webGLCompatibility; const majorVersion = args.majorVersion || 3; const minorVersion = args.minorVersion || 0; + const enabledExtensions = args.enabledExtensions; + const disabledExtensions = args.disabledExtensions; return binding.createWebGLRenderingContext( width, height, majorVersion, minorVersion, - webGLCompability, + webGLCompatibility, + enabledExtensions, + disabledExtensions, ); diff --git a/test/webgl2-smoke.js b/test/webgl2-smoke.js index 3fd7a26..6f78142 100644 --- a/test/webgl2-smoke.js +++ b/test/webgl2-smoke.js @@ -6,12 +6,12 @@ const path = require("path"); const nodeGles = require(".."); function createContext(options = {}) { - return nodeGles.createWebGLRenderingContext({ + return nodeGles.createWebGLRenderingContext(Object.assign({}, options, { width: options.width === undefined ? 16 : options.width, height: options.height === undefined ? 16 : options.height, majorVersion: options.majorVersion === undefined ? 3 : options.majorVersion, minorVersion: options.minorVersion === undefined ? 0 : options.minorVersion - }); + })); } function assertNoError(gl, label) { @@ -359,6 +359,59 @@ function testSupportedExtensionsReflectGetExtension(gl) { assertNoError(gl, "getExtension browser-compatible names"); } +function testExtensionCreationOptions() { + let gl = createContext({ + enabledExtensions: [] + }); + try { + const supported = gl.getSupportedExtensions(); + assert.deepStrictEqual(supported, []); + assert.strictEqual(gl.getExtension("WEBGL_lose_context"), null); + } finally { + gl.destroy(); + } + + gl = createContext({ + enabledExtensions: ["WEBGL_lose_context"] + }); + try { + const supported = gl.getSupportedExtensions(); + assert.deepStrictEqual(supported, ["WEBGL_lose_context"]); + assert.notStrictEqual(gl.getExtension("webgl_lose_context"), null); + assert.strictEqual(gl.getExtension("WEBGL_debug_renderer_info"), null); + } finally { + gl.destroy(); + } + + gl = createContext({ + disabledExtensions: ["WEBGL_lose_context"] + }); + try { + const supported = gl.getSupportedExtensions(); + assert(!supported.includes("WEBGL_lose_context")); + assert.strictEqual(gl.getExtension("WEBGL_lose_context"), null); + assert.notStrictEqual(gl.getExtension("WEBGL_debug_renderer_info"), null); + } finally { + gl.destroy(); + } + + gl = createContext({ + enabledExtensions: ["WEBGL_lose_context"], + disabledExtensions: ["webgl_lose_context"] + }); + try { + const supported = gl.getSupportedExtensions(); + assert(!supported.includes("WEBGL_lose_context")); + assert.strictEqual(gl.getExtension("WEBGL_lose_context"), null); + } finally { + gl.destroy(); + } + + assert.throws( + () => createContext({enabledExtensions: "WEBGL_lose_context"}), + /enabledExtensions must be an array/); +} + function testUnsupportedExtensionDoesNotWriteStderr() { const result = spawnSync(process.execPath, ["-e", ` const assert = require("assert"); @@ -2660,6 +2713,7 @@ const gl = createContext(); console.log(gl.getParameter(gl.VERSION)); testRequiredWebGL2Methods(gl); testSupportedExtensionsReflectGetExtension(gl); +testExtensionCreationOptions(); testUnsupportedExtensionDoesNotWriteStderr(); testInfoLogEmptyStrings(gl); testGetParameterSemantics(gl); From 1b40ed3af1d449e429bfbe52a8b4b3e6f0f851e8 Mon Sep 17 00:00:00 2001 From: dsafdsaf132 Date: Wed, 10 Jun 2026 13:46:18 +0900 Subject: [PATCH 2/2] Block extension aliases together --- binding/webgl_rendering_context.cc | 25 ++++++++++++++++++++++++- test/webgl2-smoke.js | 22 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/binding/webgl_rendering_context.cc b/binding/webgl_rendering_context.cc index 2ac4415..c500218 100644 --- a/binding/webgl_rendering_context.cc +++ b/binding/webgl_rendering_context.cc @@ -6386,6 +6386,28 @@ static const WebGLExtensionDescriptor *CanonicalWebGLExtension( return nullptr; } +static bool WebGLExtensionDescriptorsAlias( + const WebGLExtensionDescriptor *lhs, const WebGLExtensionDescriptor *rhs) { + return lhs != nullptr && rhs != nullptr && + lhs->is_supported == rhs->is_supported && + lhs->new_instance == rhs->new_instance; +} + +static bool ExtensionListContainsExtensionOrAlias( + const std::vector &extensions, const char *extension) { + const WebGLExtensionDescriptor *target = CanonicalWebGLExtension(extension); + for (const std::string &entry : extensions) { + if (ExtensionNameEqualsIgnoringCase(entry, extension)) { + return true; + } + const WebGLExtensionDescriptor *listed = CanonicalWebGLExtension(entry); + if (WebGLExtensionDescriptorsAlias(listed, target)) { + return true; + } + } + return false; +} + static void AddUniqueExtension(std::vector *extensions, const char *extension) { if (extension == nullptr || extension[0] == '\0') { @@ -6409,7 +6431,8 @@ bool WebGLRenderingContext::IsExtensionAllowed( if (has_enabled_extensions_filter_ && enabled_extensions_.empty()) { return false; } - return !ExtensionListContains(disabled_extensions_, extension_name); + return !ExtensionListContainsExtensionOrAlias(disabled_extensions_, + extension_name); } /* static */ diff --git a/test/webgl2-smoke.js b/test/webgl2-smoke.js index 6f78142..f98815b 100644 --- a/test/webgl2-smoke.js +++ b/test/webgl2-smoke.js @@ -360,6 +360,13 @@ function testSupportedExtensionsReflectGetExtension(gl) { } function testExtensionCreationOptions() { + const defaultGl = createContext(); + const defaultExtensions = defaultGl.getSupportedExtensions(); + const supportsColorBufferFloatAliases = + defaultExtensions.includes("EXT_color_buffer_float") && + defaultExtensions.includes("WEBGL_color_buffer_float"); + defaultGl.destroy(); + let gl = createContext({ enabledExtensions: [] }); @@ -410,6 +417,21 @@ function testExtensionCreationOptions() { assert.throws( () => createContext({enabledExtensions: "WEBGL_lose_context"}), /enabledExtensions must be an array/); + + if (supportsColorBufferFloatAliases) { + gl = createContext({ + disabledExtensions: ["EXT_color_buffer_float"] + }); + try { + const supported = gl.getSupportedExtensions(); + assert(!supported.includes("EXT_color_buffer_float")); + assert(!supported.includes("WEBGL_color_buffer_float")); + assert.strictEqual(gl.getExtension("EXT_color_buffer_float"), null); + assert.strictEqual(gl.getExtension("WEBGL_color_buffer_float"), null); + } finally { + gl.destroy(); + } + } } function testUnsupportedExtensionDoesNotWriteStderr() {