Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/framework/components/gsplat/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class GSplatComponent extends Component {
* @type {number[]|null}
* @private
*/
_lodDistances = [5, 10, 15, 20, 25];
_lodDistances = [5, 10, 15, 20, 25, 30, 35, 40];

/**
* @type {BoundingBox|null}
Expand Down
4 changes: 4 additions & 0 deletions src/platform/graphics/null/null-graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ class NullGraphicsDevice extends GraphicsDevice {
return new NullDrawCommands();
}

createUploadStreamImpl(uploadStream) {
return null;
}

draw(primitive, indexBuffer, numInstances, drawCommands, first = true, last = true) {
}

Expand Down
2 changes: 2 additions & 0 deletions src/platform/graphics/shader-chunks/frag/shared-wgsl.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export default /* glsl */`

#define WEBGPU

// convert clip space position into texture coordinates for sampling scene grab textures
fn getGrabScreenPos(clipPos: vec4<f32>) -> vec2<f32> {
var uv: vec2<f32> = (clipPos.xy / clipPos.w) * 0.5 + vec2<f32>(0.5);
Expand Down
24 changes: 22 additions & 2 deletions src/platform/graphics/storage-buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ class StorageBuffer {
* of {@link BUFFERUSAGE_READ}, {@link BUFFERUSAGE_WRITE}, {@link BUFFERUSAGE_COPY_SRC} and
* {@link BUFFERUSAGE_COPY_DST} flags. This parameter can be omitted if no special usage is
* required.
* @param {boolean} [addStorageUsage] - If true, automatically adds BUFFERUSAGE_STORAGE flag.
* Set to false for staging buffers that use BUFFERUSAGE_WRITE. Defaults to true.
*/
constructor(graphicsDevice, byteSize, bufferUsage = 0) {
constructor(graphicsDevice, byteSize, bufferUsage = 0, addStorageUsage = true) {
this.device = graphicsDevice;
this.byteSize = byteSize;
this.bufferUsage = bufferUsage;

this.impl = graphicsDevice.createBufferImpl(BUFFERUSAGE_STORAGE | bufferUsage);
const usage = addStorageUsage ? (BUFFERUSAGE_STORAGE | bufferUsage) : bufferUsage;
this.impl = graphicsDevice.createBufferImpl(usage);
this.impl.allocate(graphicsDevice, byteSize);
this.device.buffers.push(this);

Expand Down Expand Up @@ -106,6 +109,23 @@ class StorageBuffer {
clear(offset = 0, size = this.byteSize) {
this.impl.clear(this.device, offset, size);
}

/**
* Copy data from another storage buffer into this storage buffer.
*
* @param {StorageBuffer} srcBuffer - The source storage buffer to copy from.
* @param {number} [srcOffset] - The byte offset in the source buffer. Defaults to 0.
* @param {number} [dstOffset] - The byte offset in this buffer. Defaults to 0.
* @param {number} [size] - The byte size of data to copy. Defaults to the full size of the
* source buffer minus the source offset.
*/
copy(srcBuffer, srcOffset = 0, dstOffset = 0, size = srcBuffer.byteSize - srcOffset) {
Debug.assert(srcOffset + size <= srcBuffer.byteSize, 'Source copy range exceeds buffer size');
Debug.assert(dstOffset + size <= this.byteSize, 'Destination copy range exceeds buffer size');

const commandEncoder = this.device.getCommandEncoder();
commandEncoder.copyBufferToBuffer(srcBuffer.impl.buffer, srcOffset, this.impl.buffer, dstOffset, size);
}
}

export { StorageBuffer };
64 changes: 64 additions & 0 deletions src/platform/graphics/upload-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @import { GraphicsDevice } from './graphics-device.js'
* @import { StorageBuffer } from './storage-buffer.js'
* @import { Texture } from './texture.js'
*/

/**
* Manages non-blocking uploads of data to GPU resources (textures or storage buffers).
* Internally pools staging resources (PBOs on WebGL, staging buffers on WebGPU) to avoid blocking
* when the GPU is busy with previous uploads.
*
* Important: Create one UploadStream per target resource.
*
* @category Graphics
* @ignore
*/
class UploadStream {
/**
* Create a new UploadStream instance.
*
* @param {GraphicsDevice} device - The graphics device.
* @param {boolean} [useSingleBuffer] - If true, uses simple direct uploads (single texture on
* WebGL, direct write on WebGPU). If false (default), uses optimized multi-buffer strategy (PBOs
* with orphaning on WebGL, staging buffers on WebGPU) for potentially non-blocking uploads.
*/
constructor(device, useSingleBuffer = false) {
this.device = device;
this.useSingleBuffer = useSingleBuffer;

// Create platform-specific implementation
this.impl = device.createUploadStreamImpl(this);
}

/**
* Upload data to a texture (WebGL path) or storage buffer (WebGPU path).
* For WebGL textures, both offset and size must be multiples of the texture width (aligned to
* full rows).
* For WebGPU storage buffers, both offset and size byte values must be multiples of 4.
*
* @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload. Must contain at least
* `size` elements.
* @param {Texture|StorageBuffer} target - The target resource (texture for WebGL, storage
* buffer for WebGPU).
* @param {number} [offset] - The element offset in the target where upload starts. Defaults to 0.
* For WebGL textures, must be a multiple of texture width. For WebGPU, the byte offset must be
* a multiple of 4.
* @param {number} [size] - The number of elements to upload. Defaults to data.length.
* For WebGL textures, must be a multiple of texture width. For WebGPU, the byte size must be
* a multiple of 4.
*/
upload(data, target, offset = 0, size = data.length) {
this.impl?.upload(data, target, offset, size);
}

/**
* Destroy the upload stream and clean up all pooled resources.
*/
destroy() {
this.impl?.destroy();
this.impl = null;
}
}

export { UploadStream };
5 changes: 5 additions & 0 deletions src/platform/graphics/webgl/webgl-graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { WebglShader } from './webgl-shader.js';
import { WebglDrawCommands } from './webgl-draw-commands.js';
import { WebglTexture } from './webgl-texture.js';
import { WebglRenderTarget } from './webgl-render-target.js';
import { WebglUploadStream } from './webgl-upload-stream.js';
import { BlendState } from '../blend-state.js';
import { DepthState } from '../depth-state.js';
import { StencilParameters } from '../stencil-parameters.js';
Expand Down Expand Up @@ -685,6 +686,10 @@ class WebglGraphicsDevice extends GraphicsDevice {
return new WebglRenderTarget();
}

createUploadStreamImpl(uploadStream) {
return new WebglUploadStream(uploadStream);
}

// #if _DEBUG
pushMarker(name) {
if (platform.browser && window.spector) {
Expand Down
182 changes: 182 additions & 0 deletions src/platform/graphics/webgl/webgl-upload-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { Debug } from '../../../core/debug.js';

/**
* @import { UploadStream } from '../upload-stream.js'
* @import { Texture } from '../texture.js'
*/

/**
* WebGL implementation of UploadStream.
* Can use either simple direct texture uploads or optimized PBO strategy with orphaning.
*
* @ignore
*/
class WebglUploadStream {
/**
* Available PBOs ready for immediate use.
*
* @type {Array<{pbo: WebGLBuffer, size: number}>}
*/
availablePBOs = [];

/**
* PBOs currently in use by the GPU.
*
* @type {Array<{pbo: WebGLBuffer, size: number, sync: WebGLSync}>}
*/
pendingPBOs = [];

/**
* @param {UploadStream} uploadStream - The upload stream.
*/
constructor(uploadStream) {
this.uploadStream = uploadStream;
this.useSingleBuffer = uploadStream.useSingleBuffer;
}

destroy() {
// @ts-ignore - gl is available on WebglGraphicsDevice
const gl = this.uploadStream.device.gl;
this.availablePBOs.forEach(info => gl.deleteBuffer(info.pbo));
this.pendingPBOs.forEach((item) => {
if (item.sync) gl.deleteSync(item.sync);
gl.deleteBuffer(item.pbo);
});
}

/**
* Update PBOs: poll completed ones and remove undersized buffers.
*
* @param {number} minByteSize - Minimum size for buffers to keep. Smaller buffers are destroyed.
*/
update(minByteSize) {
// @ts-ignore - gl is available on WebglGraphicsDevice
const gl = this.uploadStream.device.gl;

// Poll pending PBOs
const pending = this.pendingPBOs;
for (let i = pending.length - 1; i >= 0; i--) {
const item = pending[i];

const result = gl.clientWaitSync(item.sync, 0, 0);
if (result === gl.CONDITION_SATISFIED || result === gl.ALREADY_SIGNALED) {
gl.deleteSync(item.sync);
this.availablePBOs.push({ pbo: item.pbo, size: item.size });
pending.splice(i, 1);
}
}

// Remove any available PBOs that are too small
const available = this.availablePBOs;
for (let i = available.length - 1; i >= 0; i--) {
if (available[i].size < minByteSize) {
gl.deleteBuffer(available[i].pbo);
available.splice(i, 1);
}
}
}

/**
* Upload data to a texture using PBOs (optimized) or direct upload (simple).
*
* @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload.
* @param {Texture} target - The target texture.
* @param {number} offset - The element offset in the target. Must be a multiple of texture width.
* @param {number} size - The number of elements to upload. Must be a multiple of texture width.
*/
upload(data, target, offset, size) {
if (this.useSingleBuffer) {
this.uploadDirect(data, target, offset, size);
} else {
this.uploadPBO(data, target, offset, size);
}
}

/**
* Direct texture upload (simple, blocking).
*
* @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload.
* @param {Texture} target - The target texture.
* @param {number} offset - The element offset in the target.
* @param {number} size - The number of elements to upload.
* @private
*/
uploadDirect(data, target, offset, size) {
Debug.assert(offset === 0, 'Direct texture upload with non-zero offset is not supported. Use PBO mode instead.');
Debug.assert(target._levels);

target._levels[0] = data;
target.upload();
}

/**
* PBO-based upload with orphaning (optimized, potentially non-blocking).
*
* @param {Uint8Array|Uint32Array|Float32Array} data - The data to upload.
* @param {import('../texture.js').Texture} target - The target texture.
* @param {number} offset - The element offset in the target.
* @param {number} size - The number of elements to upload.
* @private
*/
uploadPBO(data, target, offset, size) {
const device = this.uploadStream.device;
// @ts-ignore - gl is available on WebglGraphicsDevice
const gl = device.gl;

const width = target.width;
const byteSize = size * data.BYTES_PER_ELEMENT;

// Update PBOs
this.update(byteSize);

// WebGL requires offset and size aligned to full rows for texSubImage2D
Debug.assert(offset % width === 0, `Upload offset (${offset}) must be a multiple of texture width (${width}) for row alignment`);
Debug.assert(size % width === 0, `Upload size (${size}) must be a multiple of texture width (${width}) for row alignment`);

const startY = offset / width;
const height = size / width;

// Get or create a PBO (guaranteed to be large enough after update)
const pboInfo = this.availablePBOs.pop() ?? (() => {
const pbo = gl.createBuffer();
return { pbo, size: byteSize };
})();

// Orphan + bufferSubData pattern
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pboInfo.pbo);
gl.bufferData(gl.PIXEL_UNPACK_BUFFER, byteSize, gl.STREAM_DRAW);
gl.bufferSubData(gl.PIXEL_UNPACK_BUFFER, 0, new Uint8Array(data.buffer, data.byteOffset, byteSize));

// Unbind PBO before setTexture
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);

// Ensure texture is created and bound
// @ts-ignore - setTexture is available on WebglGraphicsDevice
device.setTexture(target, 0);

// Rebind PBO for texSubImage2D
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, pboInfo.pbo);

// Set pixel-store parameters (use device methods for cached state)
device.setUnpackFlipY(false);
device.setUnpackPremultiplyAlpha(false);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
gl.pixelStorei(gl.UNPACK_SKIP_ROWS, 0);
gl.pixelStorei(gl.UNPACK_SKIP_PIXELS, 0);

// Copy from PBO to texture (GPU-side)
const impl = target.impl;
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, startY, width, height, impl._glFormat, impl._glPixelType, 0);

gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);

// Track for recycling
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
this.pendingPBOs.push({ pbo: pboInfo.pbo, size: byteSize, sync });

gl.flush();
}
}

export { WebglUploadStream };
5 changes: 5 additions & 0 deletions src/platform/graphics/webgpu/webgpu-graphics-device.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { WebgpuCompute } from './webgpu-compute.js';
import { WebgpuBuffer } from './webgpu-buffer.js';
import { StorageBuffer } from '../storage-buffer.js';
import { WebgpuDrawCommands } from './webgpu-draw-commands.js';
import { WebgpuUploadStream } from './webgpu-upload-stream.js';

/**
* @import { RenderPass } from '../render-pass.js'
Expand Down Expand Up @@ -516,6 +517,10 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
return new WebgpuRenderTarget(renderTarget);
}

createUploadStreamImpl(uploadStream) {
return new WebgpuUploadStream(uploadStream);
}

createBindGroupFormatImpl(bindGroupFormat) {
return new WebgpuBindGroupFormat(bindGroupFormat);
}
Expand Down
Loading