When I read vertex data to a Float32Array from a transform buffer in Chrome using getBufferSubData, I get the warning "performance warning: READ-usage buffer was read back without waiting on a fence. This caused a graphics pipeline stall.". My understanding is that the GPU is trying to write vertex data back to the CPU as soon as getBufferSubData is called, which may be before the shaders have finished. I figured that if I can prevent this I may be be able to speed up my application, and I thought the best way to do this would be with a callback. To clarify, the data returned is correct; I'm looking to speed up my application and better understand what's going on.
I have tried to implement a callback using fenceSync, similar to this answer. This should check whether the GPU has finished executing the current commands (including the transform feedback), before executing getBufferSubData. Here is my code.
(function () {
'use strict';
const createRandomF32Array = (arrSize) => {
return Float32Array.from({length: arrSize}, () => Math.floor(Math.random() * 1000));
};
const createGlContext = () => {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2");
canvas.id = 'webgl_canvas';
document.body.appendChild(canvas);
if (gl === null) {
alert("Unable to initialize WebGL. Your browser or machine may not support it.");
return;
}
return gl;
};
// creates a single set of linked shaders containing a vertex and a fragment shader
class shaderProgram {
constructor(gl, rawVertex, rawFragment, transformFeedbackAttribs=false) {
this.gl = gl;
const compiledVertex = this.compileShader(gl.VERTEX_SHADER, rawVertex);
const compiledFragment = this.compileShader(gl.FRAGMENT_SHADER, rawFragment);
this.program = this.createProgram(compiledVertex, compiledFragment, transformFeedbackAttribs);
this.attributeLocations = {};
this.uniformLocations = {};
}
// run on init
compileShader(shaderType, shaderSource) {
const gl = this.gl;
var shader = gl.createShader(shaderType);
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (success) {
return shader;
}
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
// run on init
createProgram = (rawVertex, rawFragment, transformFeedbackAttribs) => {
const gl = this.gl;
var program = gl.createProgram();
gl.attachShader(program, rawVertex);
gl.attachShader(program, rawFragment);
if (!(transformFeedbackAttribs === false)) {
gl.transformFeedbackVaryings(program, [transformFeedbackAttribs], gl.INTERLEAVED_ATTRIBS);
}
gl.linkProgram(program);
var success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
logAttributeLocations = (attributeName) => {
const gl = this.gl;
const attributeLocation = gl.getAttribLocation(this.program, attributeName);
if (!(attributeName in this.attributeLocations)) {
this.attributeLocations[attributeName] = attributeLocation;
}
return attributeLocation;
}
logUniformLocations = (uniformName) => {
const gl = this.gl;
const uniformLocation = gl.getUniformLocation(this.program, uniformName);
if (!(uniformName in this.uniformLocations)) {
this.uniformLocations[uniformName] = uniformLocation;
}
return uniformLocation;
}
activate = () => {
const gl = this.gl;
gl.useProgram(this.program);
}
deactivate = () => {
const gl = this.gl;
gl.useProgram(0);
}
}
// the aim of this class is to build a buffer to be sent to the gpu
class renderObject {
constructor(gl) {
this.gl = gl;
this.vao = this.gl.createVertexArray();
this.buffers = {};
}
addDataToShaderAttribute = (dataset, dataDimension, attributeLocation) => {
const gl = this.gl;
var attributeVboNumber = this.addDataToBuffer(dataset);
gl.bindVertexArray(this.vao);
gl.enableVertexAttribArray(attributeLocation);
gl.vertexAttribPointer(attributeLocation, dataDimension, gl.FLOAT, false, 0, 0);
return attributeVboNumber;
}
prepareDataForShaderUniform = (dataset) => {
const gl = this.gl;
var uniformVboNumber = this.addDataToBuffer(dataset);
return uniformVboNumber;
}
addDataToBuffer = (dataset) => {
const gl = this.gl;
var vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, dataset, gl.STATIC_DRAW);
var bufferNumber = Object.keys(this.buffers).length;
this.buffers[bufferNumber] = vertexBuffer;
return bufferNumber;
}
draw = (drawType, offset, dataLength) => {
const gl = this.gl;
gl.drawArrays(drawType, offset, dataLength);
}
calculateAndRetreive = (drawType, offset, dataLength) => {
const gl = this.gl;
var transformBuffer = gl.createBuffer();
var emptyDataArray = new Float32Array(dataLength);
gl.enable(gl.RASTERIZER_DISCARD);
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, transformBuffer);
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, emptyDataArray, gl.STATIC_READ);
var bufferNumber = Object.keys(this.buffers).length;
this.buffers[bufferNumber] = transformBuffer;
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, transformBuffer);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, offset, dataLength);
gl.endTransformFeedback();
var arrBuffer = emptyDataArray;
gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, arrBuffer);
this.callbackOnSync(this.returnBufferData, emptyDataArray);
}
callbackOnSync = (callback, param) => {
const gl = this.gl;
var fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
setTimeout(checkSync);
function checkSync() {
console.log(fence);
const status = gl.clientWaitSync(fence, 0, 0);
console.log(status);
if (status == gl.CONDITION_SATISFIED) {
gl.deleteSync(fence);
return callback(param);
} else {
return(setTimeout(checkSync));
}
}
}
returnBufferData = (arrBuffer) => {
const gl = this.gl;
gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, arrBuffer);
console.log(arrBuffer);
return arrBuffer;
}
}
var testVertex = "#version 300 es\r\n\r\nin float a_position;\r\nout float o_position;\r\n\r\nvoid main() {\r\n o_position = float(a_position + 5.0);\r\n}";
var testFragment = "#version 300 es\r\nprecision mediump float;\r\n\r\nout vec4 o_FragColor;\r\n\r\nvoid main() {\r\n o_FragColor = vec4(0.0);\r\n}";
const gl = createGlContext();
var positions = createRandomF32Array(1000);
var t0 = performance.now();
var testShader = new shaderProgram(gl, testVertex, testFragment, "o_position");
var aPositionAttribute = testShader.logAttributeLocations("a_position");
var uResolutionUniform = testShader.logUniformLocations("u_resolution");
var pointsBuffer = new renderObject(gl);
var dataBuffer = pointsBuffer.addDataToShaderAttribute(positions, 1, aPositionAttribute);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
testShader.activate();
var output = pointsBuffer.calculateAndRetreive(gl.TRIANGLES, 0, positions.length, testShader);
var t1 = performance.now();
console.log("GPU function took " + (t1 - t0) + " milliseconds.");
console.log(output);
}());
<!DOCTYPE html>
<html lang="en">
<meta charset="utf-8">
<head>
<title>Rollup Example</title>
</head>
<body>
</body>
<script src="../build/bundle.min.js"></script>
</html>
This gives the warning "GL_INVALID_OPERATION: Buffer is bound for transform feedback." and every value in the returned array is 0. The line causing the issue seems to be:
var fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
, which seems to be interfering with the Transform Feedback. The checkSync function seems to work fine. My questions are 1) Where am I going wrong with this? 2) Is this a technique that could work for my use case with some tweaking, or do I need to try something different entirely?