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

bm1*_*563 5 webgl webgl2

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

我尝试使用 fenceSync 实现回调,类似于此答案。这应该在执行 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);

}());
Run Code Online (Sandbox Code Playgroud)
<!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>
Run Code Online (Sandbox Code Playgroud)

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

var fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0)
Run Code Online (Sandbox Code Playgroud)

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

gma*_*man 4

所以我认为这可能是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);
    
    Run Code Online (Sandbox Code Playgroud)

    pointsBuffer.calculateAndRetreive总是会立即返回并且output永远是undefined

  3. 这是主观的,但传递回调和稍后使用的参数看起来就像使用 JavaScript 的 C 程序员。JavaScript 具有闭包,因此可以说没有理由将参数传递给回调。回调本身始终可以“关闭”它需要的任何变量。就像我说的,虽然这是一个风格问题,所以请随意继续按照你现在的方式去做。我只是指出它对我来说很突出。

  4. 该代码传递了一个drawType calculateAndRetreive,但从未使用过。

  5. 作为未来的示例,这是一个最小的存储库。

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

更新

如果您希望代码正常工作,我建议您使用转换反馈对象。变换反馈对象就像顶点数组对象一样,除了输出而不是输入。gl.vertexAttribPointer一个顶点数组对象包含了所有的属性设置(用、 、等设置的设置gl.enableVertexAttribArray)。变换反馈对象包含所有不同的输出设置(使用gl.bindBufferBase和设置的设置gl.bindBufferRange

当前的问题来自规范中关于在绑定转换反馈时使用缓冲区的含糊语言。

您可以取消绑定它们,在您的情况下调用gl.bindBufferBase索引null0。或者您可以将它们存储在转换反馈对象中,然后取消绑定该对象。推荐使用transformfeedback对象的原因是因为它保存更多的状态。如果您绑定了 4 个边界,则可以通过解除绑定它们所绑定的 TransformFeedback 对象(1 次调用)来解除所有绑定,而将 null 与gl.bindBufferBase/绑定gl.bindBufferRange将是 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);

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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