// luma.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors import { CommandBuffer, Texture } from '@luma.gl/core'; import { GL } from '@luma.gl/constants'; import { WEBGLTexture } from "./webgl-texture.js"; import { getWebGLTextureParameters } from "../converters/texture-formats.js"; function cast(value) { return value; } export class WEBGLCommandBuffer extends CommandBuffer { device; commands = []; constructor(device) { super(device, {}); this.device = device; } submitCommands(commands = this.commands) { for (const command of commands) { switch (command.name) { case 'copy-buffer-to-buffer': _copyBufferToBuffer(this.device, command.options); break; case 'copy-buffer-to-texture': _copyBufferToTexture(this.device, command.options); break; case 'copy-texture-to-buffer': _copyTextureToBuffer(this.device, command.options); break; case 'copy-texture-to-texture': _copyTextureToTexture(this.device, command.options); break; } } } } function _copyBufferToBuffer(device, options) { const source = cast(options.source); const destination = cast(options.destination); // {In WebGL2 we can p}erform the copy on the GPU // Use GL.COPY_READ_BUFFER+GL.COPY_WRITE_BUFFER avoid disturbing other targets and locking type device.gl.bindBuffer(36662, source.handle); device.gl.bindBuffer(36663, destination.handle); device.gl.copyBufferSubData(36662, 36663, options.sourceOffset ?? 0, options.destinationOffset ?? 0, options.size); device.gl.bindBuffer(36662, null); device.gl.bindBuffer(36663, null); } /** * Copies data from a Buffer object into a Texture object * NOTE: doesn't wait for copy to be complete */ function _copyBufferToTexture(device, options) { throw new Error('Not implemented'); } /** * Copies data from a Texture object into a Buffer object. * NOTE: doesn't wait for copy to be complete */ function _copyTextureToBuffer(device, options) { const { /** Texture to copy to/from. */ source, /** Mip-map level of the texture to copy to/from. (Default 0) */ mipLevel = 0, /** Defines which aspects of the texture to copy to/from. */ aspect = 'all', /** Width to copy */ width = options.source.width, /** Height to copy */ height = options.source.height, depthOrArrayLayers = 0, /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to/from. */ origin = [0, 0], /** Destination buffer */ destination, /** Offset, in bytes, from the beginning of the buffer to the start of the image data (default 0) */ byteOffset = 0, /** * The stride, in bytes, between the beginning of each block row and the subsequent block row. * Required if there are multiple block rows (i.e. the copy height or depth is more than one block). */ bytesPerRow, /** * Number of block rows per single image of the texture. * rowsPerImage × bytesPerRow is the stride, in bytes, between the beginning of each image of data and the subsequent image. * Required if there are multiple images (i.e. the copy depth is more than one). */ rowsPerImage } = options; // TODO - Not possible to read just stencil or depth part in WebGL? if (aspect !== 'all') { throw new Error('not supported'); } // TODO - mipLevels are set when attaching texture to framebuffer if (mipLevel !== 0 || depthOrArrayLayers !== 0 || bytesPerRow || rowsPerImage) { throw new Error('not implemented'); } // Asynchronous read (PIXEL_PACK_BUFFER) is WebGL2 only feature const { framebuffer, destroyFramebuffer } = getFramebuffer(source); let prevHandle; try { const webglBuffer = destination; const sourceWidth = width || framebuffer.width; const sourceHeight = height || framebuffer.height; const sourceParams = getWebGLTextureParameters(framebuffer.texture.props.format); const sourceFormat = sourceParams.dataFormat; const sourceType = sourceParams.type; // if (!target) { // // Create new buffer with enough size // const components = glFormatToComponents(sourceFormat); // const byteCount = glTypeToBytes(sourceType); // const byteLength = byteOffset + sourceWidth * sourceHeight * components * byteCount; // target = device.createBuffer({byteLength}); // } device.gl.bindBuffer(35051, webglBuffer.handle); // @ts-expect-error native bindFramebuffer is overridden by our state tracker prevHandle = device.gl.bindFramebuffer(36160, framebuffer.handle); device.gl.readPixels(origin[0], origin[1], sourceWidth, sourceHeight, sourceFormat, sourceType, byteOffset); } finally { device.gl.bindBuffer(35051, null); // prevHandle may be unassigned if the try block failed before binding if (prevHandle !== undefined) { device.gl.bindFramebuffer(36160, prevHandle); } if (destroyFramebuffer) { framebuffer.destroy(); } } } /** * Copies data from a Framebuffer or a Texture object into a Buffer object. * NOTE: doesn't wait for copy to be complete, it programs GPU to perform a DMA transfer. export function readPixelsToBuffer( source: Framebuffer | Texture, options?: { sourceX?: number; sourceY?: number; sourceFormat?: number; target?: Buffer; // A new Buffer object is created when not provided. targetByteOffset?: number; // byte offset in buffer object // following parameters are auto deduced if not provided sourceWidth?: number; sourceHeight?: number; sourceType?: number; } ): Buffer */ /** * Copy a rectangle from a Framebuffer or Texture object into a texture (at an offset) */ // eslint-disable-next-line complexity, max-statements function _copyTextureToTexture(device, options) { const { /** Texture to copy to/from. */ source, /** Mip-map level of the texture to copy to (Default 0) */ destinationMipLevel = 0, /** Defines which aspects of the texture to copy to/from. */ // aspect = 'all', /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy from. */ origin = [0, 0], /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to. */ destinationOrigin = [0, 0], /** Texture to copy to/from. */ destination /** Mip-map level of the texture to copy to/from. (Default 0) */ // destinationMipLevel = options.mipLevel, /** Defines the origin of the copy - the minimum corner of the texture sub-region to copy to/from. */ // destinationOrigin = [0, 0], /** Defines which aspects of the texture to copy to/from. */ // destinationAspect = options.aspect, } = options; let { width = options.destination.width, height = options.destination.height // depthOrArrayLayers = 0 } = options; const { framebuffer, destroyFramebuffer } = getFramebuffer(source); const [sourceX, sourceY] = origin; const [destinationX, destinationY, destinationZ] = destinationOrigin; // @ts-expect-error native bindFramebuffer is overridden by our state tracker const prevHandle = device.gl.bindFramebuffer(36160, framebuffer.handle); // TODO - support gl.readBuffer (WebGL2 only) // const prevBuffer = gl.readBuffer(attachment); let texture = null; let textureTarget; if (destination instanceof WEBGLTexture) { texture = destination; width = Number.isFinite(width) ? width : texture.width; height = Number.isFinite(height) ? height : texture.height; texture.bind(0); textureTarget = texture.target; } else { throw new Error('invalid destination'); } switch (textureTarget) { case 3553: case 34067: device.gl.copyTexSubImage2D(textureTarget, destinationMipLevel, destinationX, destinationY, sourceX, sourceY, width, height); break; case 35866: case 32879: device.gl.copyTexSubImage3D(textureTarget, destinationMipLevel, destinationX, destinationY, destinationZ, sourceX, sourceY, width, height); break; default: } if (texture) { texture.unbind(); } device.gl.bindFramebuffer(36160, prevHandle); if (destroyFramebuffer) { framebuffer.destroy(); } } // Returns number of components in a specific readPixels WebGL format export function glFormatToComponents(format) { switch (format) { case 6406: case 33326: case 6403: return 1; case 33328: case 33319: return 2; case 6407: case 34837: return 3; case 6408: case 34836: return 4; // TODO: Add support for additional WebGL2 formats default: throw new Error('GLFormat'); } } // Return byte count for given readPixels WebGL type export function glTypeToBytes(type) { switch (type) { case 5121: return 1; case 33635: case 32819: case 32820: return 2; case 5126: return 4; // TODO: Add support for additional WebGL2 types default: throw new Error('GLType'); } } // Helper methods function getFramebuffer(source) { if (source instanceof Texture) { const { width, height, id } = source; const framebuffer = source.device.createFramebuffer({ id: `framebuffer-for-${id}`, width, height, colorAttachments: [source] }); return { framebuffer, destroyFramebuffer: true }; } return { framebuffer: source, destroyFramebuffer: false }; }