Skip to content

webgl: populate uniformLocsById on first glUniform#26846

Merged
juj merged 3 commits into
emscripten-core:mainfrom
vittorioromeo:bugfix_uniform_locs_lazy_init
May 6, 2026
Merged

webgl: populate uniformLocsById on first glUniform#26846
juj merged 3 commits into
emscripten-core:mainfrom
vittorioromeo:bugfix_uniform_locs_lazy_init

Conversation

@vittorioromeo

@vittorioromeo vittorioromeo commented May 4, 2026

Copy link
Copy Markdown
Contributor

PR created with AI assistance.

$webglGetUniformLocation reads program.uniformLocsById to resolve a uniform location integer to a WebGLUniformLocation. uniformLocsById is reset to 0 by glLinkProgram and lazily allocated by
$webglPrepareUniformLocationsBeforeFirstUse, but that prepare call only ran on the glGetUniformLocation() path. As a result, calling glUniform*() on a freshly linked program before any glGetUniformLocation() — which is the intended usage with -sGL_EXPLICIT_UNIFORM_LOCATION and explicit layout(location=N) — silently no-opped: $webglGetUniformLocation read uniformLocsById[location] off the integer 0, returned undefined, and GLctx.uniform*() dropped the call with no error.

Fix: call $webglPrepareUniformLocationsBeforeFirstUse(p) at the top of $webglGetUniformLocation, inside the GL_TRACK_ERRORS guard. The prepare call is idempotent (guards on !uniformLocsById), so the steady-state overhead is a single property read.

Adds browser.test_webgl_uniform_before_get_location with two parameterizations (default and -sGL_ASSERTIONS).

Fixes: #26672

$webglGetUniformLocation reads program.uniformLocsById to resolve a uniform
location integer to a WebGLUniformLocation. uniformLocsById is reset to 0
by glLinkProgram and lazily allocated by
$webglPrepareUniformLocationsBeforeFirstUse, but that prepare call only ran
on the glGetUniformLocation() path. As a result, calling glUniform*() on a
freshly linked program before any glGetUniformLocation() — which is the
intended usage with -sGL_EXPLICIT_UNIFORM_LOCATION and explicit
layout(location=N) — silently no-opped: $webglGetUniformLocation read
uniformLocsById[location] off the integer 0, returned undefined, and
GLctx.uniform*() dropped the call with no error.

Fix: call $webglPrepareUniformLocationsBeforeFirstUse(p) at the top of
$webglGetUniformLocation, inside the GL_TRACK_ERRORS guard. The prepare
call is idempotent (guards on !uniformLocsById), so the steady-state
overhead is a single property read.

Adds browser.test_webgl_uniform_before_get_location with two
parameterizations (default and -sGL_ASSERTIONS).
@sbc100 sbc100 requested a review from juj May 4, 2026 17:00
Comment thread src/lib/libwebgl.js
// `glGetUniformLocation()` silently no-ops: `glLinkProgram` resets
// `uniformLocsById` to 0 and only `$webglPrepareUniformLocationsBeforeFirstUse`
// refills it. The call below is idempotent (guards on `!uniformLocsById`).
webglPrepareUniformLocationsBeforeFirstUse(p);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you gate this block with

#if GL_EXPLICIT_UNIFORM_LOCATION
   // ...
   webglPrepareUniformLocationsBeforeFirstUse(p);
#endif

so that WebGL programs that do not use the explicit uniform location extension, won't need to take this micro-perf-hit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will do.

emscripten_webgl_make_context_current(ctx);

const char* vs = "#version 300 es\n"
"layout(location = 0) uniform vec3 u_val;\n"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe here we might want to test layout(location = 6) or something else arbitrary, since default 0 could theoretically pass by the virtue of "the only uniform gets assigned an ID 0" kind of a behavior?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will do.

@juj

juj commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Thanks. Looks solid, one comment on the perf/code size aspect of the feature.

@vittorioromeo

Copy link
Copy Markdown
Contributor Author

@juj: addressed both comments, thanks for the review.

@juj juj left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks!

@juj juj enabled auto-merge (squash) May 5, 2026 12:43
@vittorioromeo

Copy link
Copy Markdown
Contributor Author

@juj: any idea why the CI is failing? The tests pass when I try them locally via runner.py

@juj

juj commented May 5, 2026

Copy link
Copy Markdown
Collaborator

@juj: any idea why the CI is failing? The tests pass when I try them locally via runner.py

It is the _assertions variant of the test that is failing. If you locally run

test/runner browser.test_webgl_uniform_before_get_location_assertions

you should see the failure.

This error is a real issue with the ASSERTIONS logic. The assertion code is too early with respect to the lazily initialized uniforms code, when GL_EXPLICIT_UNIFORM_LOCATION is being used.

To fix, it's probably simplest to add the following diff:

diff --git a/src/lib/libwebgl.js b/src/lib/libwebgl.js
index 549450fc92..a127f37d27 100644
--- a/src/lib/libwebgl.js
+++ b/src/lib/libwebgl.js
@@ -603,7 +603,9 @@ for (/**@suppress{duplicate}*/var i = 0; i <= {{{ GL_POOL_TEMP_BUFFERS_SIZE }}};

 #if GL_ASSERTIONS
     validateGLObjectID: (objectHandleArray, objectID, callerFunctionName, objectReadableType) => {
-      if (objectID != 0) {
+      // objectHandleArray may be uninitialized when GL uniforms are lazily initialized, and glUniform* is called
+      // for the first time before uniforms have been populated. So ignore this validation if the handle array is not present.
+      if (objectID != 0 && objectHandleArray) {
         if (objectHandleArray[objectID] === null) {
           err(`${callerFunctionName} called with an already deleted ${objectReadableType} ID ${objectID}!`);
         } else if (!(objectID in objectHandleArray)) {

because it looks like refactoring the assertions here so that all the GL.validateGLObjectID(GLctx.currentProgram.uniformLocsById, location, ...); checks occur after the lazy init has been done, can get quite hairy, since there are multiple conditional paths depending on the WebGL entry point type.

auto-merge was automatically disabled May 5, 2026 22:42

Head branch was pushed to by a user without write access

@vittorioromeo

Copy link
Copy Markdown
Contributor Author

@juj: I see, my apologies. I assumed that./test/runner.py browser.test_webgl_uniform_before_get_location would run both variants of the parametrized test automatically.

I applied your suggested diff to the PR.

@juj juj merged commit 793198b into emscripten-core:main May 6, 2026
27 checks passed
@juj

juj commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Great, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

glUniform* silently fails with -sGL_EXPLICIT_UNIFORM_LOCATION=1

2 participants