WebGL2 Compute shader via Transform Feedback Pixel Buffer Object

Github: https://github.com/RenaudRohlinger/webgl-compute-pbo

This example showcases a technique similar to compute shading using a transform feedback program and a Pixel Buffer Object (PBO) to simulate compute shader effects in environments where they are not available.
Every second, a 'compute' operation processes vertex data through transform feedback. The result stored in an array buffer on the GPU, is then transferred to a texture using a PBO (gl.PIXEL_UNPACK_BUFFER) and then used as input in the next frame. This setup keeps the processed data entirely on the GPU for enhanced performance.

Shaders can access the updated data in the texture through a texture uniform, allowing for efficient data manipulation and comparison using texelFetch. This approach minimizes CPU-GPU data transfer, optimizing performance for GPU-based computations like dynamic sorting or simulations.

The compute operation in this example incrementally updates vertex data values, transferring them to a texture using a GPU-to-GPU Pixel Buffer Object (PBO) transfer. This method avoids CPU involvement, improving performance. For debugging, the texture's contents are read back to the CPU using gl.readPixels.


Pixel Buffer Object Data:

Normalized [0 - 10] [black - red] values of the texture are displayed


Code:

<!-- Path: ./index.js -->
<script>
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl2');


// Updated vertex shader for transform feedback
const vsTransform = /* glsl */ `#version 300 es
uniform highp sampler2D u_data;
out vec4 o_data;

void main() {
    vec4 i_data = texelFetch(u_data, ivec2(gl_VertexID, 0), 0);
    o_data = mod(i_data + 1., 10.);
}`;

// Fragment shader for transform feedback (not used but required)
const fsTransform = `#version 300 es
precision highp float;
void main() {
}`;

// Create shaders and programs
const transformProgram = createProgram(gl, vsTransform, fsTransform, [
  'o_data',
]); // Add the varyings parameter
const drawProgram = createProgram(gl, vsDraw, fsDraw);

const size = 2;
const dataArray = new Float32Array(size * 4);
dataArray
  .map((_, i) => i)
  .forEach((v, i) => {
    dataArray[i] = v;
  });

const width = size;
const height = 1;

canvas.width = width * 4;
canvas.height = height;
canvas.style['image-rendering'] = 'pixelated';
canvas.style.display = 'block';
canvas.style.width = `${window.innerWidth - 14}px`;
canvas.style.height = `${height * 20}px`;

const datas = new Float32Array(dataArray);

// Setup transform feedback
const tf = gl.createTransformFeedback();

// Create a framebuffer
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

const dataTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, dataTexture);
gl.texImage2D(
  gl.TEXTURE_2D,
  0,
  gl.RGBA32F,
  width,
  height,
  0,
  gl.RGBA,
  gl.FLOAT,
  datas
);

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

const dataBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, dataBuffer);
gl.bufferData(gl.ARRAY_BUFFER, datas, gl.STATIC_DRAW);

gl.bindBuffer(gl.ARRAY_BUFFER, null);

// Attach the texture to the framebuffer
gl.framebufferTexture2D(
  gl.FRAMEBUFFER,
  gl.COLOR_ATTACHMENT0,
  gl.TEXTURE_2D,
  dataTexture,
  0
);

gl.bindFramebuffer(gl.FRAMEBUFFER, null);

const dataLocation = gl.getAttribLocation(transformProgram, 'a_data');

const drawDataLocation = gl.getAttribLocation(drawProgram, 'a_data');

function tick() {
  // Use the transform program
  gl.useProgram(transformProgram);

  // gl.bindBuffer(gl.ARRAY_BUFFER, null);

  // Set the write buffer as the transform feedback buffer
  gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, dataBuffer);

  // Perform transform feedback
  gl.enable(gl.RASTERIZER_DISCARD);
  gl.beginTransformFeedback(gl.POINTS);
  gl.drawArrays(gl.POINTS, 0, width); // Adjust the count as needed
  gl.endTransformFeedback();
  gl.disable(gl.RASTERIZER_DISCARD);

  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

  // Bind PBO and transfer data to texture
  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, dataBuffer);
  gl.bindTexture(gl.TEXTURE_2D, dataTexture);
  gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.FLOAT, 0);
  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);

  debug();
}

setInterval(tick, 1000);

</script>



Author: Renaud Rohlinger @onirenaud