UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

154 lines (153 loc) 5.99 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug, DebugHelper } from "../../../core/debug.js"; let id = 0; class WebgpuUploadStream { /** * @param {UploadStream} uploadStream - The upload stream. */ constructor(uploadStream) { /** * Available staging buffers ready for immediate use. * * @type {GPUBuffer[]} * @private */ __publicField(this, "availableStagingBuffers", []); /** * Staging buffers currently in use by the GPU. * * @type {GPUBuffer[]} * @private */ __publicField(this, "pendingStagingBuffers", []); __publicField(this, "_destroyed", false); /** * The device's _submitVersion at the time the last staging copy was recorded. * Used to detect whether the copy has been submitted before the next upload. * * @private */ __publicField(this, "_lastUploadSubmitVersion", -1); this.uploadStream = uploadStream; this.useSingleBuffer = uploadStream.useSingleBuffer; } /** * Handles device lost event. * TODO: Implement proper WebGPU device lost handling if needed. * * @protected */ _onDeviceLost() { } destroy() { this._destroyed = true; this.availableStagingBuffers.forEach((buffer) => buffer.destroy()); this.pendingStagingBuffers.forEach((buffer) => buffer.destroy()); } /** * Update staging buffers: recycle completed ones and remove undersized buffers. * * @param {number} minByteSize - Minimum size for buffers to keep. Smaller buffers are destroyed. */ update(minByteSize) { const pending = this.pendingStagingBuffers; for (let i = 0; i < pending.length; i++) { const buffer = pending[i]; buffer.mapAsync(GPUMapMode.WRITE).then(() => { if (!this._destroyed) { this.availableStagingBuffers.push(buffer); } else { buffer.destroy(); } }); } pending.length = 0; const available = this.availableStagingBuffers; for (let i = available.length - 1; i >= 0; i--) { if (available[i].size < minByteSize) { available[i].destroy(); available.splice(i, 1); } } } /** * Upload data to a storage buffer using staging buffers (optimized) or direct write (simple). * * @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload. * @param {import('../storage-buffer.js').StorageBuffer} target - The target storage buffer. * @param {number} offset - The element offset in the target. Byte offset must be a multiple of 4. * @param {number} size - The number of elements to upload. Byte size must be a multiple of 4. */ upload(data, target, offset, size) { if (this.useSingleBuffer) { this.uploadDirect(data, target, offset, size); } else { this.uploadStaging(data, target, offset, size); } } /** * Direct storage buffer write (simple, blocking). * * @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload. * @param {import('../storage-buffer.js').StorageBuffer} target - The target storage buffer. * @param {number} offset - The element offset in the target. * @param {number} size - The number of elements to upload. * @private */ uploadDirect(data, target, offset, size) { const byteOffset = offset * data.BYTES_PER_ELEMENT; const byteSize = size * data.BYTES_PER_ELEMENT; Debug.assert(byteOffset % 4 === 0, `WebGPU upload offset in bytes (${byteOffset}) must be a multiple of 4`); Debug.assert(byteSize % 4 === 0, `WebGPU upload size in bytes (${byteSize}) must be a multiple of 4`); target.write(byteOffset, data, 0, size); } /** * Staging buffer-based upload. * * @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload. * @param {import('../storage-buffer.js').StorageBuffer} target - The target storage buffer. * @param {number} offset - The element offset in the target. * @param {number} size - The number of elements to upload. * @private */ uploadStaging(data, target, offset, size) { const device = this.uploadStream.device; const byteOffset = offset * data.BYTES_PER_ELEMENT; const byteSize = size * data.BYTES_PER_ELEMENT; if (this.pendingStagingBuffers.length > 0) { Debug.assert( device.submitVersion !== this._lastUploadSubmitVersion, 'UploadStream: each instance can only upload once per submit. A previous staging buffer copy has not been submitted yet. This causes WebGPU "buffer used in submit while mapped" errors. Ensure the caller defers uploads to one per frame.' ); } this.update(byteSize); Debug.assert(byteOffset % 4 === 0, `WebGPU upload offset in bytes (${byteOffset}) must be a multiple of 4 for copyBufferToBuffer`); Debug.assert(byteSize % 4 === 0, `WebGPU upload size in bytes (${byteSize}) must be a multiple of 4 for copyBufferToBuffer`); const buffer = this.availableStagingBuffers.pop() ?? (() => { const newBuffer = this.uploadStream.device.wgpu.createBuffer({ size: byteSize, usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC, mappedAtCreation: true }); DebugHelper.setLabel(newBuffer, `UploadStream-Staging-${id++}`); return newBuffer; })(); const mappedRange = buffer.getMappedRange(); new Uint8Array(mappedRange).set(new Uint8Array(data.buffer, data.byteOffset, byteSize)); buffer.unmap(); device.getCommandEncoder().copyBufferToBuffer( buffer, 0, target.impl.buffer, byteOffset, byteSize ); this.pendingStagingBuffers.push(buffer); this._lastUploadSubmitVersion = device.submitVersion; } } export { WebgpuUploadStream };