Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

functions for copying data to buffers directly on the GPU #13

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/classes/GPULayer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down
46 changes: 46 additions & 0 deletions src/GPULayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
} from './constants';
import {
readPixelsAsync,
readPixelsToWebGLBuffer,
readyToRead,
} from './utils';
import { disposeFramebuffers, bindFrameBuffer } from './framebuffers';
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const _testing = {
uniformInternalTypeForValue: utils.uniformInternalTypeForValue,
indexOfLayerInArray: utils.indexOfLayerInArray,
readPixelsAsync: utils.readPixelsAsync,
readPixelsToWebGLBuffer: utils.readPixelsToWebGLBuffer,
readPixelsToMultipleWebGLBuffers: utils.readPixelsToMultipleWebGLBuffers,
...extensions,
...regex,
...checks,
Expand Down
93 changes: 92 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -874,4 +874,95 @@ export async function readPixelsAsync(

gl.deleteBuffer(buf);
return dstBuffer;
}
}

/**
* 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);
}
38 changes: 38 additions & 0 deletions tests/mocha/GPULayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
})
});
}
95 changes: 95 additions & 0 deletions tests/mocha/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
uniformInternalTypeForValue,
indexOfLayerInArray,
readPixelsAsync,
readPixelsToWebGLBuffer,
readPixelsToMultipleWebGLBuffers,
SAMPLER2D_FILTER,
SAMPLER2D_WRAP_X,
SAMPLER2D_WRAP_Y,
Expand Down Expand Up @@ -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);
});
});
});
}