回调转换反馈以防止 getBufferSubData 图形管道停顿

Callback on transform feedback to prevent getBufferSubData graphics pipeline stall

当我使用 getBufferSubData 从 Chrome 中的变换缓冲区读取顶点数据到 Float32Array 时,我收到警告“性能警告:读取使用缓冲区被读回而没有等待栅栏。这导致了图形管道停顿。”。我的理解是,一旦调用 getBufferSubData,GPU 就会尝试将顶点数据写回 CPU,这可能是在着色器完成之前。我想,如果我能避免这种情况,我可能会加快我的应用程序的速度,而且我认为最好的方法是使用回调。澄清一下,返回的数据是正确的;我希望加快我的应用程序并更好地了解正在发生的事情。

我尝试使用 fenceSync 实现回调,类似于 this answer。这应该在执行 getBufferSubData 之前检查 GPU 是否已完成执行当前命令(包括变换反馈)。这是我的代码。

(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>

这会给出警告“GL_INVALID_OPERATION:缓冲区绑定用于转换反馈。”并且返回数组中的每个值都是 0。导致问题的行似乎成为:

var fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)

,这似乎干扰了变换反馈。 checkSync 函数似乎工作正常。我的问题是 1)我哪里出错了? 2) 这是一种可以通过一些调整适用于我的用例的技术,还是我需要尝试完全不同的东西?

所以我认为这可能是 a bug in Chrome。您的代码在 Mac Chrome 上运行但在 Windows Chrome 上运行失败。

有一个错误,代码等待 CONDITION_SATISFIED 但状态也可能是 ALREADY_SIGNALED

一些注意事项:

  1. 我写这个答案时的代码调用了 getBufferSubData 两次。

    正确的做法是在栅栏通过之后调用它,而不是之前。该警告与在 AFAICT 之前调用它有关。

  2. 计时代码没有意义。

    底部的代码

    var t0 = performance.now();
    ...
    var output = pointsBuffer.calculateAndRetreive(...);
    var t1 = performance.now();
    console.log("GPU function took " + (t1 - t0) + " milliseconds.");
    console.log(output);
    

    pointsBuffer.calculateAndRetreive 总是 return 立即 output 将永远是 undefined

  3. 这是主观的,但传递一个回调和一个稍后要使用的参数看起来像 C 程序员使用 JavaScript。 JavaScript 有闭包,因此可以说永远没有理由传递要传递给回调的参数。回调本身总是可以 "close" 覆盖它需要的任何变量。就像我说的,尽管这是一个风格问题,所以请随意继续按照您正在做的方式去做。我只是指出它对我来说很突出。

  4. 代码将 drawType 传递给 calculateAndRetreive 但从未使用过。

  5. 作为未来的示例,这里有一个 minimal 回购。

'use strict';

/* global document, setTimeout */

const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2");

function compileShader(gl, shaderType, shaderSource) {
  const shader = gl.createShader(shaderType);
  gl.shaderSource(shader, shaderSource);
  gl.compileShader(shader);
  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }
  throw new Error(gl.getShaderInfoLog(shader));
}

function createProgram(gl, rawVertex, rawFragment, transformFeedbackAttribs) {
  const program = gl.createProgram();
  gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, rawVertex));
  gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, rawFragment));
  if (transformFeedbackAttribs) {
    gl.transformFeedbackVaryings(program, [transformFeedbackAttribs], gl.INTERLEAVED_ATTRIBS);
  }
  gl.linkProgram(program);
  const success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }
  throw new Error(gl.getProgramInfoLog(program));
}

const vertexShader = `#version 300 es
in float inputValue;
out float outputValue;
void main() {
  outputValue = inputValue * 2.0;
}`;

const fragmentShader = `#version 300 es
precision mediump float;
out vec4 dummy;
void main() {
  dummy = vec4(0.0);
}`;

const program = createProgram(gl, vertexShader, fragmentShader, ['outputValue']);
gl.useProgram(program);

const input = new Float32Array([11, 22, 33, 44]);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, input, gl.STATIC_DRAW);
const inputLoc = gl.getAttribLocation(program, 'inputValue');
gl.enableVertexAttribArray(inputLoc);
gl.vertexAttribPointer(inputLoc, 1, gl.FLOAT, false, 0, 0);


const transformBuffer = gl.createBuffer();
gl.enable(gl.RASTERIZER_DISCARD);

gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, transformBuffer);
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, input.length * 4, gl.STATIC_READ);

gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, transformBuffer);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, input.length);
gl.endTransformFeedback();

const fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
log('waiting...');

setTimeout(waitForResult);
function waitForResult() {
  const status = gl.clientWaitSync(fence, 0, 0);
  if (status === gl.CONDITION_SATISFIED || status === gl.ALREADY_SIGNALED) {
    gl.deleteSync(fence);
    const output = new Float32Array(input.length);
    gl.getBufferSubData(gl.TRANSFORM_FEEDBACK_BUFFER, 0, output);
    log(output);
  } else {
    setTimeout(waitForResult);
  }
}

function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}

更新

如果您想让代码正常工作,我建议您使用 transformfeedback 对象。 transformfeedback 对象就像一个顶点数组对象,除了输出而不是输入。顶点数组对象包含所有属性设置(使用 gl.vertexAttribPointergl.enableVertexAttribArray 等设置的设置)。 transformfeedback 对象包含所有不同的输出设置(使用 gl.bindBufferBasegl.bindBufferRange 设置的设置)

当前的问题来自 the spec 中有关在绑定到转换反馈时使用缓冲区的模糊语言。

您可以取消绑定它们,在您的情况下,在索引 0 上使用 null 调用 gl.bindBufferBase。或者您可以将它们存储在 transformfeedback 对象中,然后取消绑定该对象。推荐使用 transformfeedback 对象的原因是因为它拥有更多状态。如果你有 4 个边界绑定,你可以通过解除绑定它们绑定到的 transformfeedback 对象来全部解除绑定(1 次调用),其中与 gl.bindBufferBase/gl.bindBufferRange 绑定 null 它将是 4 次调用。

'use strict';

/* global document, setTimeout */

const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2");

function compileShader(gl, shaderType, shaderSource) {
  const shader = gl.createShader(shaderType);
  gl.shaderSource(shader, shaderSource);
  gl.compileShader(shader);
  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }
  throw new Error(gl.getShaderInfoLog(shader));
}

function createProgram(gl, rawVertex, rawFragment, transformFeedbackAttribs) {
  const program = gl.createProgram();
  gl.attachShader(program, compileShader(gl, gl.VERTEX_SHADER, rawVertex));
  gl.attachShader(program, compileShader(gl, gl.FRAGMENT_SHADER, rawFragment));
  if (transformFeedbackAttribs) {
    gl.transformFeedbackVaryings(program, [transformFeedbackAttribs], gl.INTERLEAVED_ATTRIBS);
  }
  gl.linkProgram(program);
  const success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }
  throw new Error(gl.getProgramInfoLog(program));
}

const vertexShader = `#version 300 es
in float inputValue;
out float outputValue;
void main() {
  outputValue = inputValue * 2.0;
}`;

const fragmentShader = `#version 300 es
precision mediump float;
out vec4 dummy;
void main() {
  dummy = vec4(0.0);
}`;

const program = createProgram(gl, vertexShader, fragmentShader, ['outputValue']);
gl.useProgram(program);

const input = new Float32Array([11, 22, 33, 44]);
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, input, gl.STATIC_DRAW);
const inputLoc = gl.getAttribLocation(program, 'inputValue');
gl.enableVertexAttribArray(inputLoc);
gl.vertexAttribPointer(inputLoc, 1, gl.FLOAT, false, 0, 0);


const transformBuffer = gl.createBuffer();
gl.enable(gl.RASTERIZER_DISCARD);

const tf = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);

gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, transformBuffer);
gl.bufferData(gl.TRANSFORM_FEEDBACK_BUFFER, input.length * 4, gl.STATIC_READ);
gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);

gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, transformBuffer);
gl.beginTransformFeedback(gl.POINTS);
gl.drawArrays(gl.POINTS, 0, input.length);
gl.endTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);

const fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();
log('waiting...');

setTimeout(waitForResult);
function waitForResult() {
  const status = gl.clientWaitSync(fence, 0, 0);
  if (status === gl.CONDITION_SATISFIED || status === gl.ALREADY_SIGNALED) {
    gl.deleteSync(fence);
    const output = new Float32Array(input.length);
    gl.bindBuffer(gl.ARRAY_BUFFER, transformBuffer);
    gl.getBufferSubData(gl.ARRAY_BUFFER, 0, output);
    log(output);
  } else {
    setTimeout(waitForResult);
  }
}

function log(...args) {
  const elem = document.createElement('pre');
  elem.textContent = args.join(' ');
  document.body.appendChild(elem);
}

请注意,就像有一个默认的顶点数组对象一样,最初绑定并通过调用重新绑定的对象 gl.bindVertexArray(null),所以有一个默认的 transformfeedback 对象。

您可能会发现 this 有助于查看各种对象及其状态