Summary
A developer attempting to implement a GPGPU-style pipeline in WebGL2 failed to pass a computed texture from a compute-pass (FrameBuffer) to a vertex shader in a subsequent render-pass. While the texture correctly held data readable via readPixels, the vertex shader consistently returned 0.0 when attempting to sample the texture using texelFetch. The failure stems from a fundamental misunderstanding of Texture Unit Binding and the lifecycle of GPU State Management.
Root Cause
The issue is not with the shader logic, but with the missing binding step between the two programs. Specifically:
- Texture Unit Isolation: In WebGL, a texture is not “passed” to a shader by name; it is bound to a specific Texture Unit (e.g.,
TEXTURE0), and the shader’ssampler2Duniform is told which unit to look at. - State Reset: When the developer switched from
programComputetoprogramDraw, they updated the FrameBuffer and Viewport, but they failed to bind the texture to a texture unit for the second program to consume. - Undefined Uniform Mapping: Without calling
gl.activeTextureandgl.bindTexturefor the second program, theu_texturesampler points to an empty or incorrect unit, resulting in default values (typically0.0) being returned bytexelFetch.
Why This Happens in Real Systems
In complex graphics pipelines, this is known as a State Leak or a State Omission.
- Pipeline Complexity: As the number of textures (diffuse, normal, shadow maps, LUTs) increases, keeping track of which texture is bound to which unit becomes non-trivial.
- Implicit vs. Explicit State: Developers often assume that because a texture was “active” in the previous draw call, it remains “active” for the next. However, WebGL is a State Machine; changing the
useProgramdoes not automatically re-bind textures to the new program’s expected units. - Driver Optimizations: Modern drivers may optimize away unused bindings, making bugs intermittent and difficult to debug if the state isn’t explicitly managed.
Real-World Impact
- Silent Failures: Unlike a syntax error, this produces valid but incorrect visual output. The application doesn’t crash; it simply renders “black” or “zeroed” geometry.
- Performance Degradation: Attempting to debug this by over-binding textures (binding everything every frame) leads to excessive CPU-to-GPU driver overhead.
- Heisenbugs: In larger engines, a different part of the code might accidentally bind a texture to
TEXTURE0, making the bug appear to “fix itself” temporarily, only to reappear later.
Example or Code
To fix the issue, the developer must explicitly activate a texture unit, bind the texture to that unit, and ensure the uniform points to that unit.
// ... after gl.useProgram(programDraw) ...
// 1. Identify the texture unit you want to use (e.g., Unit 0)
const textureUnit = gl.TEXTURE0;
gl.activeTexture(textureUnit);
// 2. Bind the texture that was written to in the first pass
gl.bindTexture(gl.TEXTURE_2D, targetTexture);
// 3. Tell the shader's sampler to look at Texture Unit 0
const uTextureLocation = gl.getUniformLocation(programDraw, "u_texture");
gl.uniform1i(uTextureLocation, 0); // 0 corresponds to gl.TEXTURE0
// 4. Now proceed to draw
gl.bindVertexArray(vao);
gl.drawArrays(gl.LINE_STRIP, 0, length);
How Senior Engineers Fix It
Senior engineers avoid manual state management by implementing Abstraction Layers or Render Graphs:
- Command Buffers/State Caching: Implementing a wrapper around WebGL calls that tracks the current state of the GPU. If a request to bind
TEXTURE0is made and it is already bound, the wrapper skips the driver call to save performance. - Material Systems: Instead of manual
gl.bindTexturecalls, engineers use Material Objects that encapsulate all necessary uniforms and texture bindings, ensuring the entire “state package” is applied atomically before a draw call. - Validation Layers: Using tools like Spector.js to inspect the command stream and verify that the texture bound to the unit matches the sampler index in the shader.
Why Juniors Miss It
- Mental Model Mismatch: Juniors often view WebGL as a functional API (Input $\to$ Function $\to$ Output) rather than a State Machine (Set State $\to$ Execute).
- The “Magic” Assumption: There is an assumption that the GPU “remembers” the context of the texture from the previous program.
- Focus on Shaders: Beginners spend 90% of their time debugging GLSL syntax and 10% on the JavaScript glue code, when in reality, most WebGL bugs live in the JavaScript-to-GPU communication layer.