playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
154 lines (153 loc) • 5.99 kB
JavaScript
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
};