From 3865df34cf74701e330ae9f876b43468e38e3fe4 Mon Sep 17 00:00:00 2001 From: Micah Date: Tue, 2 May 2023 20:08:44 -0500 Subject: [PATCH] functions for copying data directly on the GPU --- docs/classes/GPULayer.md | 24 ++++++++++ src/GPULayer.ts | 46 +++++++++++++++++++ src/index.ts | 2 + src/utils.ts | 93 ++++++++++++++++++++++++++++++++++++++- tests/mocha/GPULayer.js | 38 ++++++++++++++++ tests/mocha/utils.js | 95 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 297 insertions(+), 1 deletion(-) diff --git a/docs/classes/GPULayer.md b/docs/classes/GPULayer.md index b65dca3..60102e0 100644 --- a/docs/classes/GPULayer.md +++ b/docs/classes/GPULayer.md @@ -27,6 +27,7 @@ - [clear](GPULayer.md#clear) - [getValues](GPULayer.md#getvalues) - [getValuesAsync](GPULayer.md#getvaluesasync) +- [copyToWebGLBuffer](GPULayer.md#copytowebglbuffer) - [getImage](GPULayer.md#getimage) - [savePNG](GPULayer.md#savepng) - [attachToThreeTexture](GPULayer.md#attachtothreetexture) @@ -280,6 +281,29 @@ This only works for WebGL2 contexts, will fall back to getValues() if WebGL1 con ___ +### copyToWebGLBuffer + +▸ **copyToWebGLBuffer**(`dstBuffer`, `dstOffset?`, `srcX?`, `srcY?`, `srcWidth?`, `srcHeight?`): `void` + +Copies the contents of the layer to a WebGLBuffer. + +#### Parameters + +| Name | Type | Default value | Description | +| :------ | :------ | :------ | :------ | +| `dstBuffer` | `WebGLBuffer` | `undefined` | The WebGLBuffer to copy the contents of the layer to. | +| `dstOffset` | `number` | `0` | The offset in bytes to start copying to. | +| `srcX?` | `number` | `0` | The x coordinate of the source rectangle. | +| `srcY?` | `number` | `0` | The y coordinate of the source rectangle. | +| `srcWidth?` | `number` | `undefined` | The width of the source rectangle. | +| `srcHeight?` | `number` | `undefined` | The height of the source rectangle. | + +#### Returns + +`void` + +___ + ### getImage ▸ **getImage**(`params?`): `HTMLImageElement` diff --git a/src/GPULayer.ts b/src/GPULayer.ts index bc3c8ce..6aff99d 100644 --- a/src/GPULayer.ts +++ b/src/GPULayer.ts @@ -49,6 +49,7 @@ import { } from './constants'; import { readPixelsAsync, + readPixelsToWebGLBuffer, readyToRead, } from './utils'; import { disposeFramebuffers, bindFrameBuffer } from './framebuffers'; @@ -945,6 +946,51 @@ export class GPULayer { return this._getValuesPost(_valuesRaw, _glNumChannels, _internalType); } + /** + * Copies the contents of the layer to a WebGLBuffer. + * @param dstBuffer - The WebGLBuffer to copy the contents of the layer to. + * @param dstOffset - The offset in bytes to start copying to. + * @param [srcX=0] - The x coordinate of the source rectangle. + * @param [srcY=0] - The y coordinate of the source rectangle. + * @param [srcWidth=0] - The width of the source rectangle. + * @param [srcHeight=0] - The height of the source rectangle. + */ + + copyToWebGLBuffer( + dstBuffer: WebGLBuffer, + dstOffset: number = 0, + srcX = 0, + srcY = 0, + srcWidth? : number, + srcHeight?: number, + ) { + const { width: fullWidth, height: fullHeight, _composer } = this; + const width = srcWidth || fullWidth; + const height = srcHeight || fullHeight; + + const { gl, isWebGL2 } = _composer; + if (!isWebGL2) { + throw new Error('copyToBuffer() is only supported for WebGL2.'); + } + + const { _glFormat, _glType, _valuesRaw, _glNumChannels, _internalType } = this._getValuesSetup(); + readPixelsToWebGLBuffer( + gl as WebGL2RenderingContext, + dstBuffer, + srcX, + srcY, + width, + height, + _glFormat, + _glType, + 4, // to-do: support all the types by passing in the component byte size + // maybe this can come from `this._getValuesSetup()`? + _glNumChannels, + dstOffset + ) + } + + private _getCanvasWithImageData(multiplier?: number) { const values = this.getValues(); const { width, height, numComponents, type } = this; diff --git a/src/index.ts b/src/index.ts index 215cb60..dc32219 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,8 @@ const _testing = { uniformInternalTypeForValue: utils.uniformInternalTypeForValue, indexOfLayerInArray: utils.indexOfLayerInArray, readPixelsAsync: utils.readPixelsAsync, + readPixelsToWebGLBuffer: utils.readPixelsToWebGLBuffer, + readPixelsToMultipleWebGLBuffers: utils.readPixelsToMultipleWebGLBuffers, ...extensions, ...regex, ...checks, diff --git a/src/utils.ts b/src/utils.ts index d946977..b5f215b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -874,4 +874,95 @@ export async function readPixelsAsync( gl.deleteBuffer(buf); return dstBuffer; - } \ No newline at end of file + } + + /** + * Read pixels from a framebuffer to a destination WebGLBuffer at a given offset. + * @param gl - WebGL2 Rendering Context + * @param dstBuffer - An object to read data into. The array type must match the type of the type parameter. + * @param x - The first horizontal pixel that is read from the lower left corner of a rectangular block of pixels. + * @param y - The first vertical pixel that is read from the lower left corner of a rectangular block of pixels. + * @param w - The width of the rectangle. + * @param h - The height of the rectangle. + * @param format - The GLenum format of the pixel data. + * @param componentType - The GLenum data type of the pixel data. + * @param componentSizeBytes - The size of each component in bytes. + * @param srcOffset - The offset in bytes from the start of the buffer object where data will be read. + * @param dstOffset - The offset in bytes from the start of the buffer object where data will be written. + * @returns + */ + + export function readPixelsToWebGLBuffer( + gl: WebGL2RenderingContext, + dstBuffer: WebGLBuffer, + x: number, y: number, + w: number, h: number, + format: number, + componentType: number, + componentSizeBytes: number = 4, + numComponents: number = 4, + srcOffset: number = 0, + dstOffset: number = 0, + ) { + const pbo = gl.createBuffer()!; + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo); + gl.bufferData(gl.PIXEL_PACK_BUFFER, w * h * numComponents * componentSizeBytes, gl.STATIC_COPY); + gl.readPixels(x, y, w, h, format, componentType, 0); + + gl.bindBuffer(gl.COPY_WRITE_BUFFER, dstBuffer); + gl.copyBufferSubData(gl.PIXEL_PACK_BUFFER, gl.COPY_WRITE_BUFFER, srcOffset, dstOffset, w * h * numComponents * componentSizeBytes); + gl.bindBuffer(gl.COPY_WRITE_BUFFER, null); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + + gl.deleteBuffer(pbo); + + } + + type PixelTransfers = { + dstBuffer: WebGLBuffer, + srcOffset: number, + dstOffset: number, + length: number, + } + + /** + * Read pixels from a framebuffer to multiple destination buffers at given offsets. + * @param gl - WebGL2 Rendering Context + * @param transfers - An array of transfer configurations representing a set of transfers to buffers. + * @param x - The first horizontal pixel that is read from the lower left corner of a rectangular block of pixels. + * @param y - The first vertical pixel that is read from the lower left corner of a rectangular block of pixels. + * @param w - The width of the rectangle. + * @param h - The height of the rectangle. + * @param format - The GLenum format of the pixel data. + * @param componentType - The GLenum data type of the pixel data. + * @param componentSizeBytes - The size of each component in bytes. + * @param numComponents - The number of components per pixel. + * @returns + */ + + export function readPixelsToMultipleWebGLBuffers( + gl: WebGL2RenderingContext, + transfers: PixelTransfers[], + x: number, y: number, + w: number, h: number, + format: number, + componentType: number, + componentSizeBytes: number = 4, + numComponents: number = 4, + ) { + const pbo = gl.createBuffer()!; + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo); + gl.bufferData(gl.PIXEL_PACK_BUFFER, w * h * componentSizeBytes * numComponents, gl.STREAM_READ); + gl.readPixels(x, y, w, h, format, componentType, 0); + + transfers.forEach( + ({ dstBuffer, srcOffset, dstOffset, length }) => { + gl.bindBuffer(gl.COPY_WRITE_BUFFER, dstBuffer); + gl.copyBufferSubData(gl.PIXEL_PACK_BUFFER, gl.COPY_WRITE_BUFFER, srcOffset, dstOffset, length); + gl.bindBuffer(gl.COPY_WRITE_BUFFER, null); + } + ) + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null); + + gl.deleteBuffer(pbo); + } \ No newline at end of file diff --git a/tests/mocha/GPULayer.js b/tests/mocha/GPULayer.js index 159f4e2..7113091 100644 --- a/tests/mocha/GPULayer.js +++ b/tests/mocha/GPULayer.js @@ -730,5 +730,43 @@ // dispose() marks them for deletion, but they are garbage collected later. }); }); + describe('copy to GPU buffer', () => { + it('should copy values to a GPU buffer using `GPULayer.copyToWebGLBuffer`', async () => { + const composer = new GPUComposer({ canvas: document.createElement('canvas') }); + const { gl } = composer; + + const glBuffer = gl.createBuffer(); + + // simulate binding this buffer as a vertex attribute + gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([Math.random(), Math.random(), Math.random(), Math.random()]), gl.STATIC_DRAW); + + const layer1 = new GPULayer(composer, { + name: 'test', + type: FLOAT, + numComponents: 4, + dimensions: [1,1], + clearValue: 3, + }); + // overwrite it with the pixels from the layer + layer1.clear(); + + layer1.copyToWebGLBuffer(glBuffer); + + // read it back to an array + const array = new Float32Array(4); + gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); + gl.getBufferSubData(gl.ARRAY_BUFFER, 0, array); + + assert.equal(array[0], 3); + assert.equal(array[1], 3); + assert.equal(array[2], 3); + assert.equal(array[3], 3); + + layer1.dispose(); + composer.dispose(); + gl.deleteBuffer(glBuffer); + }); + }) }); } \ No newline at end of file diff --git a/tests/mocha/utils.js b/tests/mocha/utils.js index a3b84f4..2132a3b 100644 --- a/tests/mocha/utils.js +++ b/tests/mocha/utils.js @@ -58,6 +58,8 @@ uniformInternalTypeForValue, indexOfLayerInArray, readPixelsAsync, + readPixelsToWebGLBuffer, + readPixelsToMultipleWebGLBuffers, SAMPLER2D_FILTER, SAMPLER2D_WRAP_X, SAMPLER2D_WRAP_Y, @@ -742,5 +744,98 @@ void main() { composer.dispose(); }); }); + describe('read pixels to GPU buffers', () => { + it('should transfer pixels to a single buffer using `readPixelsToWebGLBuffer`', async () => { + const composer = new GPUComposer({ canvas: document.createElement('canvas') }); + const { gl } = composer; + + const glBuffer = gl.createBuffer(); + + // simulate binding this buffer as a vertex attribute + gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([Math.random(), Math.random(), Math.random(), Math.random()]), gl.STATIC_DRAW); + + const layer1 = new GPULayer(composer, { + name: 'test', + type: FLOAT, + numComponents: 4, + dimensions: [1,1], + clearValue: 3, + }); + // overwrite it with the pixels from the layer + layer1.clear(); + + readPixelsToWebGLBuffer(gl, glBuffer, 0, 0, 1, 1, gl.RGBA, gl.FLOAT); + + // read it back to an array + const array = new Float32Array(4); + gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); + gl.getBufferSubData(gl.ARRAY_BUFFER, 0, array); + + assert.equal(array[0], 3); + assert.equal(array[1], 3); + assert.equal(array[2], 3); + assert.equal(array[3], 3); + + layer1.dispose(); + composer.dispose(); + gl.deleteBuffer(glBuffer); + }); + + it('should transfer pixels to multiple buffers using `readPixelsToMultipleWebGLBuffers`', async () => { + const composer = new GPUComposer({ canvas: document.createElement('canvas') }); + const { gl } = composer; + + const singleComponentBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, singleComponentBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([Math.random()]), gl.STATIC_DRAW); + + const rangeTwoBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, rangeTwoBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([Math.random(), Math.random()]), gl.STATIC_DRAW); + + const layer1 = new GPULayer(composer, { + name: 'test', + type: FLOAT, + numComponents: 4, + dimensions: 1, + clearValue: 3, + }); + + layer1.clear(); + layer1.setFromArray([1,2,3,4]); + + // first let's make sure the values are what we expect + const array = new Float32Array(4); + await readPixelsAsync(gl, 0, 0, 1, 1, gl.RGBA, gl.FLOAT, array); + + assert.equal(array[1], 2); + + await readPixelsToMultipleWebGLBuffers(gl, [ + { dstBuffer: singleComponentBuffer, srcOffset: 3*4, dstOffset: 0, length: 4 }, + { dstBuffer: rangeTwoBuffer, srcOffset: 1*4, dstOffset: 0, length: 2*4} + ], 0, 0, 1, 1, gl.RGBA, gl.FLOAT, 4); + + // read the single value (4th component) back to an array and check it + const singleComponentArray = new Float32Array(1); + gl.bindBuffer(gl.ARRAY_BUFFER, singleComponentBuffer); + gl.getBufferSubData(gl.ARRAY_BUFFER, 0, singleComponentArray, 0, 1); + + assert.equal(singleComponentArray[0], 4); + + // read the range values (2nd and 3rd components) back to an array and check them + const rangeTwoArray = new Float32Array(2); + gl.bindBuffer(gl.ARRAY_BUFFER, rangeTwoBuffer); + gl.getBufferSubData(gl.ARRAY_BUFFER, 0, rangeTwoArray, 0); + + assert.equal(rangeTwoArray[0], 2); + assert.equal(rangeTwoArray[1], 3); + + layer1.dispose(); + composer.dispose(); + gl.deleteBuffer(singleComponentBuffer); + gl.deleteBuffer(rangeTwoBuffer); + }); + }); }); } \ No newline at end of file