Skip to content

[GLEmulation] Re-read client-side vertex arrays at draw time - #27974

Open
TheTomBritton wants to merge 3 commits into
emscripten-core:mainfrom
TheTomBritton:gl-client-arrays-draw-time
Open

TheTomBritton wants to merge 3 commits into
emscripten-core:mainfrom
TheTomBritton:gl-client-arrays-draw-time

Conversation

@TheTomBritton

Copy link
Copy Markdown

Fixes #27945.

Real GL reads client-side vertex arrays when the draw call executes, so an application may keep the same arrays bound via gl*Pointer and refill them in place between draws. GLImmediate cached the restrided copy of planar client arrays and skipped the re-copy unless a gl*Pointer/client-state call had set modifiedClientAttributes since the last draw — so every such draw re-drew the first draw's vertex data.

Two changes, both needed:

  1. Drop the modifiedClientAttributes early-out from prepareClientAttributes (and the now-unread flag everywhere), as proposed by @juj in the issue, so client arrays are examined at every draw.
  2. Stop overwriting attr.pointer with the restrided staging location in the planar slow path. With the early-out gone, this overwrite made the next prepareClientAttributes re-read the staging buffer's interleaved output using the original planar strides — reading its own previous output with the wrong layout — instead of the application's arrays. Nothing needs the overwritten value: rendering works from attr.offset/stride/vertexPointer, the interleaved fast path needs the real pointer to compute offsets, and glGetPointerv now answers with the pointer the application actually set.

Verification that (1) alone is not sufficient and (1)+(2) fixes the real-world app this was found in, with captures: #27945 (comment)

Test: browser.test_gl_client_arrays_refill draws planar client arrays, refills them in place without touching any GL state, draws again, and asserts via glReadPixels that each draw showed its own data. It fails before this change (second draw re-draws the first draw's triangle; the asserted pixel stays at the clear color) and passes after it.

Perf note: planar client arrays are now re-copied on every draw. That is slower, but they were already the documented very-slow path (GL_ASSERTIONS warns about it), correctness wins per the issue discussion, and interleaved arrays remain the fast path.

🤖 Generated with Claude Code

Real GL reads client-side vertex arrays when the draw call executes, so an
application may keep the same arrays bound via gl*Pointer and refill them
in place between draws. GLImmediate cached the restrided copy of planar
client arrays and skipped the re-copy unless a gl*Pointer or client-state
call had set modifiedClientAttributes since the last draw, so every such
draw re-drew the first draw's vertex data.

Two changes, both needed:

- Drop the modifiedClientAttributes early-out (and the now-unread flag)
  from prepareClientAttributes, so the client arrays are examined on every
  draw, per the discussion in emscripten-core#27945.

- Stop overwriting attr.pointer with the restrided staging location in the
  planar slow path. With the early-out gone this overwrite made the next
  prepareClientAttributes re-read the staging buffer's interleaved output
  using the original planar strides (reading its own output, with the
  wrong layout) instead of the application's arrays. Nothing needs the
  overwritten value: rendering works from attr.offset / stride /
  vertexPointer, the interleaved fast path needs the real pointer to
  compute offsets, and glGetPointerv now answers with the pointer the
  application actually set.

Planar client arrays are re-copied on every draw now, which is slower, but
they were already the documented very-slow path and correctness wins;
interleaved arrays remain the fast path.

Adds a browser test that draws planar client arrays, refills them in place
without touching any GL state, draws again, and asserts via readPixels
that each draw showed its own data. The test fails before this change and
passes after it, and the fix was also verified on the real-world app the
bug was found in (see emscripten-core#27945 for captures).

Fixes emscripten-core#27945

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/lib/libglemu.js Outdated
Comment on lines +2969 to +2972
// Leave attr.pointer at the client's own array: client-side vertex
// arrays are read at draw time, so the next draw call must re-read
// (and re-restride) the array's contents from the application's
// memory, not from this staging copy of the previous draw's data.

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.

Suggested change
// Leave attr.pointer at the client's own array: client-side vertex
// arrays are read at draw time, so the next draw call must re-read
// (and re-restride) the array's contents from the application's
// memory, not from this staging copy of the previous draw's data.
// n.b. The above restrided data cannot be cached, since the user might change the
// vertex data between draws, and there is no way to detect that. So later draws must
// re-do the above work.

Comment thread ChangeLog.md Outdated
Comment on lines +23 to +28
- `LEGACY_GL_EMULATION` now re-reads client-side vertex arrays at every draw
call, as real GL does. Previously, planar (non-interleaved) client arrays
that an application refilled in place between draws kept being drawn with
the first draw's data. Planar client arrays are consequently re-copied on
every draw, which can be slower; they were already the documented slow path,
and interleaved arrays remain fast. (#27945)

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.

Suggested change
- `LEGACY_GL_EMULATION` now re-reads client-side vertex arrays at every draw
call, as real GL does. Previously, planar (non-interleaved) client arrays
that an application refilled in place between draws kept being drawn with
the first draw's data. Planar client arrays are consequently re-copied on
every draw, which can be slower; they were already the documented slow path,
and interleaved arrays remain fast. (#27945)
- Removed broken caching of client-side vertex arrays with `LEGACY_GL_EMULATION`,
since there is no sound way to detect if the cached data might have gotten
modified by the user between draws. (#27945)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

LGTM, a couple of comment suggestions to avoid LLMism creep.

This change can have a performance impact to FFP GL code, but given that the caching logic was not sound, we cannot unfortunately keep that.

@TheTomBritton

Copy link
Copy Markdown
Author

Applied both suggestions verbatim in 2b0589f — thanks for the review!

@juj
juj enabled auto-merge (squash) October 14, 2026 17:59
Comment thread src/lib/libglemu.js Outdated

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.

Is this comment not out of date (since it refers to defining attr.pointer)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, that clause was out of date now that the loop no longer assigns attr.pointer. Dropped it in ced9b58.

#include <assert.h>
#include <stdio.h>

static void read_pixel(int x, int y, GLubyte rgba[4]) {

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.

minor nit: Drop the static here. See #27873. I'm trying to keep the tests consistent in this regard.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in ced9b58.

@juj
juj disabled auto-merge October 14, 2026 18:20
The restride loop no longer defines attr.pointer, so its comment no longer
says it does. The test helper drops `static`, matching emscripten-core#27873.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UR3bCLyLBQiGtevGJx2ARt
@sbc100 sbc100 changed the title GLEmulation: re-read client-side vertex arrays at draw time [GLEmulation] Re-read client-side vertex arrays at draw time Oct 23, 2026
@sbc100

sbc100 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

lgtm.. will land once the tests are passing.

@sbc100

sbc100 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

I think a rebase (or merge) will fix of the current failures.

This branch has not been deployed

No deployments
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.

LEGACY_GL_EMULATION: planar client-side vertex arrays are not re-read on later glDrawArrays — stale vertex data is drawn

3 participants