p5
Version:
[](https://www.npmjs.com/package/p5)
4,230 lines • 149 kB
JavaScript
import { W as WEBGPU, T as TRIANGLE_STRIP, L as LIGHTEST, D as DARKEST, S as SUBTRACT, R as REPLACE, E as EXCLUSION, a as SCREEN, M as MULTIPLY, b as REMOVE, A as ADD, B as BLEND, c as TRIANGLES, U as UNSIGNED_BYTE, F as FLOAT, H as HALF_FLOAT, d as UNSIGNED_INT, e as MIRROR, f as REPEAT, C as CLAMP, g as LINEAR, N as NEAREST } from '../constants-DwbuOBz3.js';
import { getStrokeDefs } from '../webgl/enums.js';
import { DataType } from '../strands/ir_types.js';
import { colorVertexShader, colorFragmentShader } from './shaders/color.js';
import { lineVertexShader, lineFragmentShader } from './shaders/line.js';
import { materialVertexShader, materialFragmentShader } from './shaders/material.js';
import { fontVertexShader, fontFragmentShader } from './shaders/font.js';
import { blitVertexShader, blitFragmentShader } from './shaders/blit.js';
import { wgslBackend } from './strands_wgslBackend.js';
import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base.js';
import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight.js';
import { baseComputeShader } from './shaders/compute.js';
import './shaders/functions/noise3DWGSL.js';
import './shaders/functions/randomWGSL.js';
import './shaders/functions/randomVertWGSL.js';
import './shaders/functions/randomComputeWGSL.js';
import '../strands/ir_dag.js';
import '../strands/strands_FES.js';
import '../ir_builders-CMXkjMoV.js';
import '../strands/ir_cfg.js';
import '../strands/strands_builtins.js';
/**
* @module 3D
* @submodule p5.strands
* @for p5
*/
const FRAME_STATE = {
PENDING: 0,
UNPROMOTED: 1,
PROMOTED: 2
};
function rendererWebGPU(p5, fn) {
const { lineDefs } = getStrokeDefs((n, v, t) => `const ${n}: ${t} = ${v};\n`);
// RendererWebGPU depends on these other classes being set up prior,
// as it is optimized for being in a standalone build, not core
const {
Renderer3D,
Shader,
Texture,
MipmapTexture,
Image,
Camera,
RGBA,
} = p5;
class StorageBuffer {
constructor(buffer, size, renderer, schema = null) {
this._isStorageBuffer = true;
this.buffer = buffer;
this.size = size;
this._renderer = renderer;
this._schema = schema;
}
/**
* Updates the data in the buffer with new values. The new data must be in
* the same format as the data originally passed to
* <a href="#/p5/createStorage">`createStorage()`</a>.
*
* ```js example
* let particles;
* let computeShader;
* let displayShader;
* let instance;
* const numParticles = 100;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
* particles = createStorage(makeParticles(width / 2, height / 2));
* computeShader = buildComputeShader(simulate);
* displayShader = buildMaterialShader(display);
* instance = buildGeometry(drawParticle);
* describe('100 orange particles shooting outward.');
* }
*
* function makeParticles(x, y) {
* let data = [];
* for (let i = 0; i < numParticles; i++) {
* let angle = (i / numParticles) * TWO_PI;
* let speed = random(0.5, 2);
* data.push({
* position: createVector(x, y),
* velocity: createVector(cos(angle) * speed, sin(angle) * speed),
* });
* }
* return data;
* }
*
* function drawParticle() {
* sphere(2);
* }
*
* function simulate() {
* let data = uniformStorage(particles);
* let idx = index.x;
* data[idx].position = data[idx].position + data[idx].velocity;
* }
*
* function display() {
* let data = uniformStorage(particles);
* worldInputs.begin();
* let pos = data[instanceID()].position;
* worldInputs.position.xy += pos - [width / 2, height / 2];
* worldInputs.end();
* }
*
* function draw() {
* background(30);
* if (frameCount % 60 === 0) {
* particles.update(makeParticles(random(width), random(height)));
* }
* compute(computeShader, numParticles);
* noStroke();
* fill(255, 200, 50);
* shader(displayShader);
* model(instance, numParticles);
* }
* ```
*
* @method update
* @for p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
* @param {Number[]|Float32Array|Object[]} data The new data to write into the buffer.
*/
update(data) {
const device = this._renderer.device;
if (this._schema !== null) {
// Buffer was created with a struct array
if (
!Array.isArray(data) ||
data.length === 0 ||
typeof data[0] !== 'object' ||
Array.isArray(data[0])
) {
throw new Error(
'update() expects an array of objects matching the original struct format'
);
}
const newSchema = this._renderer._inferStructSchema(data[0]);
if (newSchema.structBody !== this._schema.structBody) {
throw new Error(
`update() data structure doesn't match the original.\n` +
` Expected: ${this._schema.structBody}\n` +
` Got: ${newSchema.structBody}`
);
}
const packed = this._renderer._packStructArray(data, this._schema);
if (packed.byteLength > this.size) {
throw new Error(
`update() data (${packed.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
);
}
device.queue.writeBuffer(this.buffer, 0, packed);
} else {
// Buffer was created with a float array
let floatData;
if (data instanceof Float32Array) {
floatData = data;
} else if (Array.isArray(data)) {
floatData = new Float32Array(data);
} else {
throw new Error(
'update() expects a Float32Array or array of numbers for this buffer'
);
}
if (floatData.byteLength > this.size) {
throw new Error(
`update() data (${floatData.byteLength} bytes) exceeds buffer size (${this.size} bytes)`
);
}
device.queue.writeBuffer(this.buffer, 0, floatData);
}
}
/**
* Reads data from a storage buffer back into JavaScript.
*
* Copies data from the GPU to the CPU using a temporary buffer,
* so it must be awaited. Returns a `Float32Array` for number
* buffers, or an array of plain objects for struct buffers.
*
* Note: This is a GPU -> CPU read, so calling it often (like every frame)
* can be slow.
*
* ```js example
* let data;
* let computeShader;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
*
* data = createStorage(new Float32Array([1, 2, 3, 4]));
* computeShader = buildComputeShader(doubleValues);
* compute(computeShader, 4);
*
* let result = await data.read();
* // result is Float32Array [2, 4, 6, 8]
* for (let i = 0; i < result.length; i++) {
* print(result[i]);
* }
* describe('Prints the values 2, 4, 6, 8 to the console.');
* }
*
* function doubleValues() {
* let d = uniformStorage(data);
* let idx = index.x;
* d[idx] = d[idx] * 2;
* }
* ```
*
* @method read
* @for p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
* @returns {Promise<Float32Array|Object[]>}
*/
async read() {
const device = this._renderer.device;
this._renderer.flushDraw();
const stagingBuffer = device.createBuffer({
size: this.size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const commandEncoder = device.createCommandEncoder();
commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
const mappedRange = stagingBuffer.getMappedRange(0, this.size);
// Copy before unmapping because mapped memory becomes invalid after unmap
const rawCopy = new Float32Array(mappedRange.byteLength / 4);
rawCopy.set(new Float32Array(mappedRange));
stagingBuffer.unmap();
stagingBuffer.destroy();
if (this._schema !== null) {
return this._renderer._unpackStructArray(rawCopy, this._schema);
}
return rawCopy;
}
/**
* Updates a single element in the buffer at a given index. Use this
* when only a small number of elements need to change. If you need to
* replace all the data at once, use
* <a href="#/p5.StorageBuffer/update">`update()`</a> instead.
*
* ```js
* let buf;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
*
* // Float buffer: update one value by index
* buf = createStorage(new Float32Array([1, 2, 3, 4]));
* buf.set(2, 9.5); // only index 2 changes → [1, 2, 9.5, 4]
*
* let result = await buf.read();
* print(result[2]); // 9.5
* describe('Prints 9.5 to the console.');
* }
* ```
*
* ```js
* let particles;
* const numParticles = 100;
*
* async function setup() {
* await createCanvas(100, 100, WEBGPU);
* particles = createStorage(makeParticles());
*
* // Replace particle 42 without touching the others
* particles.set(42, {
* position: createVector(0, 0),
* velocity: createVector(1, 0),
* });
*
* // Read back to confirm the update
* let result = await particles.read();
* print(result[42].position.x, result[42].position.y); // 0, 0
* describe('Prints the position of particle 42 after updating it.');
* }
*
* function makeParticles() {
* let data = [];
* for (let i = 0; i < numParticles; i++) {
* data.push({
* position: createVector(random(width), random(height)),
* velocity: createVector(random(-1, 1), random(-1, 1)),
* });
* }
* return data;
* }
* ```
*
* @method set
* @for p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
* @param {Number} index The zero-based index of the element to update.
* @param {Number|Object} value The new value. Pass a number for float
* buffers, or a plain object matching the original struct layout for
* struct buffers.
*/
set(index, value) {
const device = this._renderer.device;
if (this._schema !== null) {
// buffer was created with an array of structs
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(
'set() expects a plain object matching the original struct format for this buffer'
);
}
const { stride } = this._schema;
const byteOffset = index * stride;
if (byteOffset + stride > this.size) {
throw new Error(
`set() index ${index} is out of bounds for this buffer ` +
`(buffer holds ${Math.floor(this.size / stride)} elements)`
);
}
// pack just this one element using the same logic as update()
const packed = this._renderer._packStructArray([value], this._schema);
// use packed.buffer (ArrayBuffer) so the size arg is always in bytes
device.queue.writeBuffer(this.buffer, byteOffset, packed.buffer, 0, stride);
} else {
// buffer was created with a float array
if (typeof value !== 'number') {
throw new Error(
'set() expects a number for this float buffer'
);
}
const byteOffset = index * 4;
if (byteOffset + 4 > this.size) {
throw new Error(
`set() index ${index} is out of bounds for this buffer ` +
`(buffer holds ${Math.floor(this.size / 4)} floats)`
);
}
device.queue.writeBuffer(this.buffer, byteOffset, new Float32Array([value]));
}
}
}
/**
* A block of data that shaders can read from, and compute shaders can also
* write to. This is only available in WebGPU mode.
*
* Note: <a href="#/p5/createStorage">`createStorage()`</a> is the recommended
* way to create an instance of this class.
*
* @class p5.StorageBuffer
* @beta
* @webgpu
* @webgpuOnly
*/
p5.StorageBuffer = StorageBuffer;
class RendererWebGPU extends Renderer3D {
constructor(pInst, w, h, isMainCanvas, elt) {
super(pInst, w, h, isMainCanvas, elt);
// Used to group draws into one big render pass
this.activeRenderPass = null;
this.activeRenderPassEncoder = null;
this.activeShaderOptions = null;
this.activeShader = null;
this.samplers = new Map();
// Some uniforms update every frame, like model matrices and sometimes colors.
// The fastest way to handle these is to use mapped memory. We'll batch those
// into bigger buffers with dynamic offsets, separate from the usual system
// where bind groups have their own little buffers that get cached when they
// are unchanged
this.uniformBufferAlignment = 256;
this.activeUniformBuffers = [];
this.currentUniformBuffer = undefined;
this.uniformBufferPool = [];
this.resettingUniformBuffers = [];
this.dynamicEntryOffsets = new Uint32Array(64);
// Cache for current frame's canvas texture view
this.currentCanvasColorTexture = null;
this.currentCanvasColorTextureView = null;
// Single reusable staging buffer for pixel reading
this.pixelReadBuffer = null;
this.pixelReadBufferSize = 0;
this.strandsBackend = wgslBackend;
// Registry to track all shaders for uniform data pooling
this._shadersWithPools = [];
// Registry to track geometries with buffer pools
this._geometriesWithPools = [];
// Flag to track if any draws have happened that need queue submission
this._hasPendingDraws = false;
this._pendingCommandEncoders = [];
// Queue of callbacks to run after next submit (mainly for safe texture deletion)
this._postSubmitCallbacks = [];
// Retired buffers to destroy at end of frame
this._retiredBuffers = [];
// Storage buffers for compute shaders
this._storageBuffers = new Set();
// 2D canvas for pixel reading fallback
this._pixelReadCanvas = null;
this._pixelReadCtx = null;
this.mainFramebuffer = null;
this._frameState = FRAME_STATE.PENDING;
this.finalCamera = new Camera(this);
this.finalCamera._computeCameraDefaultSettings();
this.finalCamera._setDefaultCamera();
this.depthFormat = 'depth24plus-stencil8';
this.depthTexture = null;
this.depthTextureView = null;
}
async setupContext() {
this._setAttributeDefaults(this._pInst);
await this._initContext();
}
_setAttributeDefaults(pInst) {
const defaults = {
forceFallbackAdapter: false,
powerPreference: 'high-performance',
};
if (pInst._webgpuAttributes === null) {
pInst._webgpuAttributes = defaults;
} else {
pInst._webgpuAttributes = Object.assign(defaults, pInst._webgpuAttributes);
}
return;
}
async _initContext() {
this.adapter = await navigator.gpu?.requestAdapter(this._webgpuAttributes);
this.device = await this.adapter?.requestDevice({
// Todo: check support
requiredFeatures: ['depth32float-stencil8']
});
if (!this.device) {
throw new Error('Your browser does not support WebGPU.');
}
this.queue = this.device.queue;
this.drawingContext = this.canvas.getContext('webgpu');
this.presentationFormat = navigator.gpu.getPreferredCanvasFormat();
this.drawingContext.configure({
device: this.device,
format: this.presentationFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
alphaMode: 'premultiplied',
});
// TODO disablable stencil
this.mainFramebuffer = this.createFramebuffer({ _useCanvasFormat: true });
this._updateSize();
this._update();
this.flushDraw();
}
async _setAttributes(key, value) {
if (typeof this._pInst._webgpuAttributes === "undefined") {
console.log(
"You are trying to use setAttributes on a p5.Graphics object " +
"that does not use a WebGPU renderer."
);
return;
}
let unchanged = true;
if (typeof value !== "undefined") {
//first time modifying the attributes
if (this._pInst._webgpuAttributes === null) {
this._pInst._webgpuAttributes = {};
}
if (this._pInst._webgpuAttributes[key] !== value) {
//changing value of previously altered attribute
this._pInst._webgpuAttributes[key] = value;
unchanged = false;
}
//setting all attributes with some change
} else if (key instanceof Object) {
if (this._pInst._webgpuAttributes !== key) {
this._pInst._webgpuAttributes = key;
unchanged = false;
}
}
//@todo_FES
if (!this.isP3D || unchanged) {
return;
}
if (!this._pInst._setupDone) {
if (this.geometryBufferCache.numCached() > 0) {
p5._friendlyError(
"Sorry, Could not set the attributes, you need to call setAttributes() " +
"before calling the other drawing methods in setup()"
);
return;
}
}
await this._resetContext(null, null, RendererWebGPU);
if (this.states.curCamera) {
this.states.curCamera._renderer = this._renderer;
}
}
_updateSize() {
if (!this.device || !this.depthFormat) return;
if (this.depthTexture && this.depthTexture.destroy) {
this.flushDraw();
const textureToDestroy = this.depthTexture;
this._postSubmitCallbacks.push(() => textureToDestroy.destroy());
this.depthTextureView = null;
}
this.depthTexture = this.device.createTexture({
size: {
width: Math.ceil(this.width * this._pixelDensity),
height: Math.ceil(this.height * this._pixelDensity),
depthOrArrayLayers: 1,
},
format: this.depthFormat,
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
this.depthTextureView = this.depthTexture.createView();
// Clear the main canvas after resize
this.clear();
}
_getCanvasColorTextureView() {
const canvasTexture = this.drawingContext.getCurrentTexture();
// If texture changed (new frame), update cache
if (this.currentCanvasColorTexture !== canvasTexture) {
this.currentCanvasColorTexture = canvasTexture;
this.currentCanvasColorTextureView = canvasTexture.createView();
}
return this.currentCanvasColorTextureView;
}
_beginActiveRenderPass() {
if (this.activeRenderPass) return;
// Use framebuffer texture if active, otherwise use canvas texture
const activeFramebuffer = this.activeFramebuffer();
const colorAttachment = {
view: activeFramebuffer
? (activeFramebuffer.aaColorTexture
? activeFramebuffer.aaColorTextureView
: activeFramebuffer.colorTextureView)
: this._getCanvasColorTextureView(),
loadOp: "load",
storeOp: "store",
// If using multisampled texture, resolve to non-multisampled texture
resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
? activeFramebuffer.colorTextureView
: undefined,
};
// Use framebuffer depth texture if active, otherwise use canvas depth texture
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
const renderPassDescriptor = {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthTextureView
? {
view: depthTextureView,
depthLoadOp: "load",
depthStoreOp: "store",
depthClearValue: 1.0,
stencilLoadOp: "load",
stencilStoreOp: "store",
depthReadOnly: false,
stencilReadOnly: false,
}
: undefined,
};
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
this.activeRenderPassEncoder = commandEncoder;
this.activeRenderPass = passEncoder;
}
_finishActiveRenderPass() {
if (!this.activeRenderPass) return;
const commandEncoder = this.activeRenderPassEncoder;
const passEncoder = this.activeRenderPass;
passEncoder.end();
// Store the command encoder for later submission
this._pendingCommandEncoders.push(commandEncoder.finish());
this.activeRenderPassEncoder = null;
this.activeRenderPass = null;
this.activeShader = null;
this.activeShaderOptions = null;
}
clear(...args) {
if (!this.device || !this.drawingContext) return;
const _r = args[0] || 0;
const _g = args[1] || 0;
const _b = args[2] || 0;
const _a = args[3] || 0;
// If PENDING and no custom framebuffer, clear means stay UNPROMOTED.
// However, if we are still in setup (frameCount == 0), we must promote
// so that mainFramebuffer gets the cleared content. This ensures that if
// draw() later promotes without a copy, it starts from the correct state
// rather than a stale mainFramebuffer.
// Note: a mid-draw-loop transition from UNPROMOTED back to PROMOTED
// (i.e. calling background() some frames but not others) will still
// lose intermediate UNPROMOTED frame content.
if (this._frameState !== FRAME_STATE.PROMOTED && !this.activeFramebuffer()) {
if (this._pInst.frameCount > 0) {
this._frameState = FRAME_STATE.UNPROMOTED;
} else {
this._promoteToFramebufferWithoutCopy();
// clear() then targets mainFramebuffer via activeFramebuffer()
}
}
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
// Use framebuffer texture if active, otherwise use canvas texture
const activeFramebuffer = this.activeFramebuffer();
const colorAttachment = {
view: activeFramebuffer
? (activeFramebuffer.aaColorTexture
? activeFramebuffer.aaColorTextureView
: activeFramebuffer.colorTextureView)
: this._getCanvasColorTextureView(),
clearValue: { r: _r * _a, g: _g * _a, b: _b * _a, a: _a },
loadOp: 'clear',
storeOp: 'store',
// If using multisampled texture, resolve to non-multisampled texture
resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture
? activeFramebuffer.colorTextureView
: undefined,
};
// Use framebuffer depth texture if active, otherwise use canvas depth texture
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
const depthAttachment = depthTextureView
? {
view: depthTextureView,
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilLoadOp: 'load',
stencilStoreOp: 'store',
}
: undefined;
const renderPassDescriptor = {
colorAttachments: [colorAttachment],
...(depthAttachment ? { depthStencilAttachment: depthAttachment } : {}),
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
/**
* Resets all depth information so that nothing previously drawn will
* occlude anything subsequently drawn.
*/
clearDepth(depth = 1) {
if (!this.device || !this.depthTextureView) return;
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
// Use framebuffer texture if active, otherwise use canvas texture
const activeFramebuffer = this.activeFramebuffer();
// Use framebuffer depth texture if active, otherwise use canvas depth texture
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (!depthTextureView) {
// No depth buffer to clear
return;
}
const depthAttachment = {
view: depthTextureView,
depthClearValue: depth,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilLoadOp: 'load',
stencilStoreOp: 'store',
};
const renderPassDescriptor = {
colorAttachments: [], // No color attachments, we're only clearing depth
depthStencilAttachment: depthAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_prepareBuffer(renderBuffer, geometry, shader) {
const attr = shader.attributes[renderBuffer.attr];
if (!attr) return;
const { src, dst, size, map } = renderBuffer;
const device = this.device;
const buffers = this._getOrMakeCachedBuffers(geometry);
let srcData = geometry[src];
if (!srcData || srcData.length === 0) {
if (renderBuffer.default) {
srcData = geometry[src] = renderBuffer.default(geometry);
srcData.isDefault = true;
} else {
return;
}
}
// Check if we already have a buffer for this data
let existingBuffer = buffers[dst];
const needsNewBuffer = !existingBuffer;
// Only create new buffer and write data if buffer doesn't exist or data is dirty
if (needsNewBuffer || geometry.dirtyFlags[src] !== false) {
const raw = map ? map(srcData) : srcData;
const typed = this._normalizeBufferData(raw, Float32Array);
// Get pooled buffer (may reuse existing or create new)
const pooledBufferInfo = this._getVertexBufferFromPool(geometry, dst, typed.byteLength);
// Create a copy of the data to avoid conflicts when geometry arrays are reset
const dataCopy = new typed.constructor(typed);
pooledBufferInfo.dataCopy = dataCopy;
// Write the data to the pooled buffer
device.queue.writeBuffer(pooledBufferInfo.buffer, 0, dataCopy);
// Update the buffers cache to use the pooled buffer
buffers[dst] = pooledBufferInfo.buffer;
geometry.dirtyFlags[src] = false;
}
shader.enableAttrib(attr, size);
}
_disableRemainingAttributes(shader) {}
_enableAttrib(attr) {
// TODO: is this necessary?
const loc = attr.location;
if (!this.registerEnabled.has(loc)) {
// TODO
this.registerEnabled.add(loc);
}
}
_ensureGeometryBuffers(buffers, indices, indexType) {
if (!indices) return;
const device = this.device;
const buffer = device.createBuffer({
size: Math.ceil((indices.length * indexType.BYTES_PER_ELEMENT) / 4) * 4,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
// Write index data to buffer
const mapping = new indexType(buffer.getMappedRange());
mapping.set(indices);
buffer.unmap();
buffers.indexBuffer = buffer;
buffers.indexBufferType = indexType === Uint32Array ? 'uint32' : 'uint16';
}
_freeBuffers(buffers) {
const destroyIfExists = (buf) => {
if (buf && buf.destroy) {
buf.destroy();
}
};
destroyIfExists(buffers.indexBuffer);
const freeDefs = (defs) => {
for (const def of defs) {
destroyIfExists(buffers[def.dst]);
buffers[def.dst] = null;
}
};
freeDefs(this.buffers.stroke);
freeDefs(this.buffers.fill);
freeDefs(this.buffers.user);
}
_getValidSampleCount(requestedCount) {
// WebGPU supports sample counts of 1, 4 (and sometimes 8)
if (requestedCount <= 1) return 1;
if (requestedCount <= 4) return 4;
return 4; // Cap at 4 for broader compatibility
}
_shaderOptions({ mode, compute, workgroupSize }) {
if (compute) return { compute: true, workgroupSize };
const activeFramebuffer = this.activeFramebuffer();
const format = activeFramebuffer ?
this._getWebGPUColorFormat(activeFramebuffer) :
this.presentationFormat;
const requestedSampleCount = activeFramebuffer ?
(activeFramebuffer.antialias ? activeFramebuffer.antialiasSamples : 1) :
1; // No MSAA needed when blitting already-antialiased textures to canvas
const sampleCount = this._getValidSampleCount(requestedSampleCount);
const depthFormat = activeFramebuffer
? (activeFramebuffer.useDepth ? this._getWebGPUDepthFormat(activeFramebuffer) : undefined)
: this.depthFormat;
const drawTarget = this.drawTarget();
const clipping = this._clipping;
const clipApplied = drawTarget._isClipApplied;
return {
topology: mode === TRIANGLE_STRIP ? 'triangle-strip' : 'triangle-list',
blendMode: this.states.curBlendMode,
sampleCount,
format,
depthFormat,
clipping,
clipApplied,
}
}
_shaderOptionsDifferent(newOptions) {
if (!this.activeShaderOptions) return true;
for (const key in this.activeShaderOptions) {
if (this.activeShaderOptions[key] !== newOptions[key]) return true;
}
return false;
}
_initShader(shader) {
const device = this.device;
if (shader.shaderType === 'compute') {
// Compute shader initialization
shader.computeModule = device.createShaderModule({ code: shader.computeSrc() });
shader._computePipelineCache = null;
shader._workgroupSize = null;
// Create compute pipeline (deferred until first compute() call)
shader.getPipeline = ({ workgroupSize }) => {
if (!shader._computePipelineCache) {
shader._computePipelineCache = device.createComputePipeline({
layout: shader._pipelineLayout,
compute: {
module: shader.computeModule,
entryPoint: 'main'
}
});
shader._workgroupSize = workgroupSize;
}
return shader._computePipelineCache;
};
return;
}
// Render shader initialization
shader.vertModule = device.createShaderModule({ code: shader.vertSrc() });
shader.fragModule = device.createShaderModule({ code: shader.fragSrc() });
shader._pipelineCache = new Map();
shader.getPipeline = ({ topology, blendMode, sampleCount, format, depthFormat, clipping, clipApplied }) => {
const key = `${topology}_${blendMode}_${sampleCount}_${format}_${depthFormat}_${clipping}_${clipApplied}`;
if (!shader._pipelineCache.has(key)) {
const pipeline = device.createRenderPipeline({
layout: shader._pipelineLayout,
vertex: {
module: shader.vertModule,
entryPoint: 'main',
buffers: this._getVertexLayout(shader),
},
fragment: {
module: shader.fragModule,
entryPoint: 'main',
targets: [{
format,
blend: this._getBlendState(blendMode),
}],
},
primitive: { topology },
multisample: { count: sampleCount },
...(depthFormat ? {
depthStencil: {
format: depthFormat,
depthWriteEnabled: !clipping,
depthCompare: 'less-equal',
stencilFront: {
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
failOp: 'keep',
depthFailOp: 'keep',
passOp: clipping ? 'replace' : 'keep',
},
stencilBack: {
compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'),
failOp: 'keep',
depthFailOp: 'keep',
passOp: clipping ? 'replace' : 'keep',
},
stencilReadMask: 0xFF,
stencilWriteMask: clipping ? 0xFF : 0x00,
},
} : {}),
});
shader._pipelineCache.set(key, pipeline);
}
return shader._pipelineCache.get(key);
};
}
_finalizeShader(shader) {
// Per-group buffer pools. We will pull from these when we draw multiple
// times using the shader in a render pass. These are per group instead of
// global so that we can reuse the last used buffer when uniform values
// don't change.
shader._uniformBufferGroups = [];
shader.buffersDirty = new Set();
for (const group of shader._uniformGroups) {
// Calculate the size needed for this group's uniforms
const groupUniforms = Object.values(group.uniforms);
const rawSize = Math.max(
0,
...groupUniforms.map(u => u.offsetEnd)
);
const alignedSize = Math.ceil(rawSize / 16) * 16;
shader._uniformBufferGroups.push({
group: group.group,
binding: group.binding,
cacheKey: group.group * 1000 + group.binding,
varName: group.varName,
structType: group.structType,
uniforms: groupUniforms,
size: alignedSize,
bufferPool: [],
nextBufferPool: [],
dynamic: groupUniforms.some(u => u.name.startsWith('uModel')),
buffersInUse: new Set(),
currentBuffer: null, // For caching
});
}
// Register this shader in our registry for pool cleanup
this._shadersWithPools.push(shader);
const bindGroupLayouts = new Map(); // group index -> bindGroupLayout
const groupEntries = new Map(); // group index -> array of entries
// Add all uniform group bindings to group 0
const structEntries = new Map();
for (const bufferGroup of shader._uniformBufferGroups) {
const entries = structEntries.get(bufferGroup.group) || [];
entries.push({
bufferGroup,
binding: bufferGroup.binding,
visibility: shader.shaderType === 'compute'
? GPUShaderStage.COMPUTE
: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic },
});
structEntries.set(bufferGroup.group, entries);
}
for (const [group, entries] of structEntries.entries()) {
entries.sort((a, b) => a.binding - b.binding);
groupEntries.set(group, entries);
}
// Add the variable amount of samplers and texture bindings that can come after
for (const sampler of shader.samplers) {
const group = sampler.group;
const entries = groupEntries.get(group) || [];
if (!['sampler', 'texture_2d<f32>'].includes(sampler.type)) {
throw new Error(`Unsupported texture type: ${sampler.type}`);
}
entries.push({
binding: sampler.binding,
visibility: sampler.visibility,
sampler: sampler.type === 'sampler'
? { type: 'filtering' }
: undefined,
texture: sampler.type === 'texture_2d<f32>'
? { sampleType: 'float', viewDimension: '2d' }
: undefined,
uniform: sampler,
});
entries.sort((a, b) => a.binding - b.binding);
groupEntries.set(group, entries);
}
// Add storage buffer bindings
for (const storage of shader._storageBuffers || []) {
const group = storage.group;
const entries = groupEntries.get(group) || [];
entries.push({
binding: storage.binding,
visibility: storage.visibility,
buffer: {
type: storage.accessMode === 'read' ? 'read-only-storage' : 'storage'
},
storage: storage,
});
entries.sort((a, b) => a.binding - b.binding);
groupEntries.set(group, entries);
}
// Create layouts and bind groups
const groupEntriesArr = [];
for (const [group, entries] of groupEntries) {
const layout = this.device.createBindGroupLayout({ entries });
bindGroupLayouts.set(group, layout);
groupEntriesArr.push([group, entries]);
}
shader._groupEntries = groupEntriesArr;
shader._bindGroupLayouts = [...bindGroupLayouts.values()];
// Reuse bind groups if they don't change
shader._cachedBindGroup = {};
// Remember which dynamic buffer we last used, so that we can
// possibly cache bind groups if unchanged
shader._lastDynamicBuffer = {};
shader._pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: shader._bindGroupLayouts,
});
shader._compiled = true;
}
_getBlendState(mode) {
switch (mode) {
case BLEND:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
};
case ADD:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one'
}
};
case REMOVE:
return {
color: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
operation: 'add',
srcFactor: 'zero',
dstFactor: 'one-minus-src-alpha'
}
};
case MULTIPLY:
return {
color: {
operation: 'add',
srcFactor: 'dst-color',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
operation: 'add',
srcFactor: 'dst-alpha',
dstFactor: 'one-minus-src-alpha'
}
};
case SCREEN:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-color'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
};
case EXCLUSION:
return {
color: {
operation: 'add',
srcFactor: 'one-minus-dst-color',
dstFactor: 'one-minus-src-color'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one'
}
};
case REPLACE:
return {
color: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'zero'
}
};
case SUBTRACT:
return {
color: {
operation: 'reverse-subtract',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'add',
srcFactor: 'one',
dstFactor: 'one-minus-src-alpha'
}
};
case DARKEST:
return {
color: {
operation: 'min',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'min',
srcFactor: 'one',
dstFactor: 'one'
}
};
case LIGHTEST:
return {
color: {
operation: 'max',
srcFactor: 'one',
dstFactor: 'one'
},
alpha: {
operation: 'max',
srcFactor: 'one',
dstFactor: 'one'
}
};
default:
console.warn(`Unsupported blend mode: ${mode}`);
return undefined;
}
}
_applyColorBlend() {}
_getVertexLayout(shader) {
const layouts = [];
for (const attrName in shader.attributes) {
const attr = shader.attributes[attrName];
if (!attr || attr.location === -1) continue;
// Get the vertex buffer info associated with this attribute
const renderBuffer =
this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
this.buffers.user.find(buf => buf.attr === attrName);
if (!renderBuffer) continue;
const { size } = renderBuffer;
// Convert from the number of floats (e.g. 3) to a recognized WebGPU
// format (e.g. "float32x3")
const format = this._getFormatFromSize(size);
layouts.push({
arrayStride: size * 4,
stepMode: 'vertex',
attributes: [
{
shaderLocation: attr.location,
offset: 0,
format,
},
],
});
}
return layouts;
}
_getVertexBuffers(shader) {
if (!shader._vertexBuffers) {
const buffers = [];
for (const attrName in shader.attributes) {
const attr = shader.attributes[attrName];
if (!attr || attr.location === -1) continue;
// Get the vertex buffer info associated with this attribute
const renderBuffer =
this.buffers[shader.shaderType].find(buf => buf.attr === attrName) ||
this.buffers.user.find(buf => buf.attr === attrName);
if (!renderBuffer) continue;
buffers.push(renderBuffer);
}
shader._vertexBuffers = buffers;
}
return shader._vertexBuffers;
}
_getFormatFromSize(size) {
switch (size) {
case 1: return 'float32';
case 2: return 'float32x2';
case 3: return 'float32x3';
case 4: return 'float32x4';
default: throw new Error(`Unsupported attribute size: ${size}`);
}
}
_useShader(shader, options) {}
_updateViewport() {
this._origViewport = {
width: this.width,
height: this.height,
};
this._viewport = [0, 0, this.width, this.height];
}
_createPixelsArray() {
this.pixels = new Uint8Array(
this.width * this.pixelDensity() * this.height * this.pixelDensity() * 4
);
}
supportsTriangleFan() {
return false;
}
viewport() {}
zClipRange() {
return [0, 1];
}
defaultNearScale() {
return 0.01;
}
defaultFarScale() {
return 100;
}
_resetBuffersBeforeDraw() {
this._finishActiveRenderPass();
// Set state to PENDING - we'll decide on first draw
if (this._pInst.frameCount > 0) {
this._frameState = FRAME_STATE.PENDING;
}
// Clear depth buffer but DON'T start any render pass yet
const activeFramebuffer = this.activeFramebuffer();
const commandEncoder = this.device.createCommandEncoder();
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (depthTextureView) {
const depthAttachment = {
view: depthTextureView,
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
stencilLoadOp: 'load',
stencilStoreOp: 'store',
};
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: depthAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
}
/**
* Promotes the current frame to use mainFramebuffer.
* Copies current canvas content to mainFramebuffer, then switches to rendering there.
* @private
*/
_promoteToFramebuffer() {
// Already promoted this frame
if (this._frameState === FRAME_STATE.PROMOTED) {
return;
}
// Already drawing to a custom framebuffer, no promotion needed
if (this.activeFramebuffer()) {
return;
}
// Flush any pending draws to canvas first
this.flushDraw();
// Mark as promoted
this._frameState = FRAME_STATE.PROMOTED;
// Get current canvas texture
const canvasTexture = this.drawingContext.getCurrentTexture();
// Ensure mainFramebuffer matches canvas size
if (this.mainFramebuffer.width !== this.width ||
this.mainFramebuffer.height !== this.height) {
this.mainFramebuffer.resize(this.width, this.height);
}
// Copy canvas textures to mainFramebuffer
const commandEncoder = this.device.createCommandEncoder();
// Copy color texture
commandEncoder.copyTextureToTexture(
{
texture: canvasTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
texture: this.mainFramebuffer.colorTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
width: Math.ceil(this.width * this._pixelDensity),
height: Math.ceil(this.height * this._pixelDensity),
depthOrArrayLayers: 1,
}
);
// Copy depth texture
commandEncoder.copyTextureToTexture(
{
texture: this.depthTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
texture: this.mainFramebuffer.depthTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
},
{
width: Math.ceil(this.width * this._pixelDensity),
height: Math.ceil(this.height * this._pixelDensity),
depthOrArrayLayers: 1,
}
);
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
// We want to make sure the transformation state is the same
// once we're drawing to the framebuffer, because normally
// those are reset.
const savedModelMatrix = this.states.uModelMatrix.copy();
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
this.states.uModelMatrix.reset();
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
this.mainFramebuffer.begin();
this.states.uModelMatrix.set(savedModelMatrix);
}
_promoteToFramebufferWithoutCopy() {
// Already promoted this frame
if (this._frameState === FRAME_STATE.PROMOTED) {
return;
}
// Ensure mainFramebuffer matches canvas size
if (this.mainFramebuffer.width !== this.width ||
this.mainFramebuffer.height !== this.height) {
this.mainFramebuffer.resize(this.width, this.height);
}
// Mark as promoted WITHOUT copying canvas content
this._frameState = FRAME_STATE.PROMOTED;
// Flush any pending draws first
this.flushDraw();
// Preserve transformation state
const savedModelMatrix = this.states.uModelMatrix.copy();
this.states.uModelMatrix.set(this.states.uModelMatrix.copy());
this.states.uModelMatrix.reset();
this.mainFramebuffer.defaultCamera.set(this.states.curCamera);
// Begin rendering to mainFramebuffer
this.mainFramebuffer.begin();
this.states.uModelMatrix.set(savedModelMatrix);
}
//////////////////////////////////////////////
// Geometry buffer pool management
//////////////////////////////////////////////
_initializeGeometryBufferPools(geometry) {
if (geometry._vertexBufferPools) {
return; // Already initialized
}
geometry._vertexBufferPools = {}; // Keyed by buffer type (dst)
geometry._vertexBuffersInUse = {}; // Keyed by buffer type (dst)
geometry._vertexBuffersToReturn = {}; // Keyed by buffer type (dst)
// Register this geometry for pool cleanup
this._geometriesWithPools.push(geometry);
}
_getVertexBufferFromPool(geometry, dst, size) {
// Initialize pools if needed
this._initializeGeometryBufferPools(geometry);
// Get or create pool for this buffer type
if (!geometry._vertexBufferPools[dst]) {
geometry._vertexBufferPools[dst] = [];
}
if (!geometry._vertexBuffersInUse[dst]) {
geometry._vertexBuffersInUse[dst] = [];
}
if (!geometry._vertexBuffersToReturn[dst]) {
geometry._vertexBuffersToReturn[dst] = [];
}
// Try to get a buffer from the pool
const pool = geometry._vertexBufferPools[dst];
if (pool.length > 0) {
const bufferInfo = pool.pop();
// Check if buffer is large enough
if (bufferInfo.buffer.size >= size) {
geometry._vertexBuffersInUse[dst].push(bufferInfo);
return bufferInfo;
} else {
// Buffer too small, don't destroy immediately as it may still be in use
// Add to retirement array
this._retiredBuffers.push(bufferInfo.buffer);
}
}
// No suitable buffer available, create a new one
const newBuffer = this.device.createBuffer({
size,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const bufferInfo = {
buffer: newBuffer,
size,
// Create a copy of the data array to avoid conflicts when geometry is reset
dataCopy: null
};
geometry._vertexBuffersInUse[dst].push(bufferInfo);
return bufferInfo;
}
_returnVertexBuffersToPool() {
// Return buffers marked for return back to their pools for all registered geometries
for (const geometry of this._geometriesWithPools) {
if (geometry._vertexBuffersToReturn) {
for (const [dst, buffersToReturn] of Object.entries(geometry._vertexBuffersToReturn)) {
if (buffersToReturn.length > 0) {
// Move all buffers from ToReturn back to pool
const pool = geometry._vertexBufferPools[dst] || [];
while (buffersToReturn.length > 0) {
const bufferInfo = buffersToReturn.pop();
// Clear the data copy reference to prevent memory leaks
bufferInfo.dataCopy = null;
pool.push(bufferInfo);
}
geometry._vertexBufferPools[dst] = pool;
}
}
}
}
}
// Called when geometry is reset - mark its buffers for return
onReset(geometry) {
this._markGeometryBuffersForReturn(geometry);
}
// Mark geometry buffers for return when geometry is reset/freed
_markGeometryBuffersForReturn(geometry) {
if (geometry._vertexBuffersInUse && geometry._vertexBuffersToReturn) {
for (const [dst, buffersInUse] of Object.entries(geometry._vertexBuffersInUse)) {
if (buffersInUse.length > 0) {
// Move all buffers from InUse to ToReturn
const buffersToReturn = geometry._vertexBuffersToReturn[dst] || [];
while (buffersInUse.length > 0) {
const bufferInfo = buffersInUse.pop();
buffersToReturn.push(bufferInfo);
}
geometry._vertexBuffersToReturn[dst] = buffersToReturn;
}
}
}
}
//////////////////////////////////////////////
// Uniform buffer pool management
//////////////////////////////////////////////
_getUniformBufferFromPool(bufferGroup) {
// Try to get a buffer from the pool
if (bufferGroup.bufferPool.length > 0) {
const bufferInfo = bufferGroup.bufferPool.pop();
bufferGroup.buffersInUse.add(bufferInfo);
return bufferInfo;
}
// No buffers available, create a new one
const newBuffer = this.device.createBuffer({
size: bufferGroup.size,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const newData = new Float32Array(bufferGroup.size / 4);
const newDataView = new DataView(newData.buffer);
const bufferInfo = {
buffer: newBuffer,
data: newData,
dataView: newDataView
};
bufferGroup.buffersInUse.add(bufferInfo);
return bufferInfo;
}
_getDynamicUniformBufferFromPool(bufferGroup) {
//
let buffer;
if (
this.currentUniformBuffer &&
this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size
) {
// We can fit this next block of uniforms into the current active memory chunk
buffer = this.currentUniformBuffer;
} else if (this.uniformBufferPool.length > 0) {
buffer = this.uniformBufferPool.pop();
this.activeUniformBuffers.push(buffer);
} else {
// Kinda arbitrary. Each dynamic offset has to be in groups of 256, but then
// we can choose how many things we want to be able to fit into a block.
// There's some overhead to each block so if we're drawing a lot of stuff,
// bigger is better. But it's also a lot of wasted memory if we AREN'T drawing
// a lot of stuff. So.... right now it's 40. Feel free to update this if
// a better balance can be achieved.
const size = 256 * 40;
buffer = {
dynamic: true,
lastOffset: 0,
offset: 0,
size,
buffer: this.device.createBuffer({
size,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
}),
uniformBuffer: this.device.createBuffer({
size,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}),
};
buffer.data = new Float32Array(buffer.buffer.getMappedRange());
buffer.dataView = new DataView(buffer.data.buffer);
this.activeUniformBuffers.push(buffer);
}
this.currentUniformBuffer = buffer;
return buffer;
}
_returnUniformBuffersToPool() {
// Return all used buffers back to their pools for all registered shaders
for (const shader of this._shadersWithPools) {
this._returnShaderBuffersToPool(shader);
}
}
_returnShaderBuffersToPool(shader) {
if (shader._uniformBufferGroups) {
for (const bufferGroup of shader._uniformBufferGroups) {
while (bufferGroup.nextBufferPool.length > 0) {
bufferGroup.bufferPool.push(bufferGroup.nextBufferPool.pop());
}
for (const bufferInfo of bufferGroup.buffersInUse.keys()) {
if (bufferInfo !== bufferGroup.currentBuffer) {
bufferGroup.nextBufferPool.push(bufferInfo);
}
}
bufferGroup.buffersInUse.clear();
if (bufferGroup.currentBuffer) {
bufferGroup.buffersInUse.add(bufferGroup.currentBuffer);
}
}
}
}
flushDraw() {
this._finishActiveRenderPass();
// Only submit if we actually had any draws
if (this._hasPendingDraws) {
// Create a copy of pending command encoders
const commandsToSubmit = this._pendingCommandEncoders;
this._pendingCommandEncoders = [];
this._hasPendingDraws = false;
if (this.activeUniformBuffers.length > 0) {
const encoder = this.device.createCommandEncoder();
for (const bufferInfo of this.activeUniformBuffers) {
bufferInfo.buffer.unmap();
encoder.copyBufferToBuffer(
bufferInfo.buffer,
bufferInfo.uniformBuffer,
);
}
commandsToSubmit.unshift(encoder.finish());
}
// Submit the commands
this.queue.submit(commandsToSubmit);
for (const buf of this.activeUniformBuffers) {
// buf.buffer = this.device.createBuffer({
// size: buf.size,
// usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
// mappedAtCreation: true,
// });
buf.offset = 0;
buf.lastOffset = 0;
// this.resettingUniformBuffers.push(
buf.buffer.mapAsync(GPUMapMode.WRITE).then(() => {
buf.data = new Float32Array(buf.buffer.getMappedRange());
buf.dataView = new DataView(buf.data.buffer);
this.uniformBufferPool.push(buf);
return buf;
});
// )
}
this.activeUniformBuffers = [];
this.currentUniformBuffer = undefined;
// Execute post-submit callbacks after GPU work completes
if (this._postSubmitCallbacks.length > 0) {
const callbacks = this._postSubmitCallbacks;
this._postSubmitCallbacks = [];
this.device.queue.onSubmittedWorkDone().then(() => {
for (const callback of callbacks) {
callback();
}
});
}
// Reset canvas texture cache for next frame
this.currentCanvasColorTexture = null;
this.currentCanvasColorTextureView = null;
}
}
_ensurePixelReadCanvas(width, height) {
// Create canvas if it doesn't exist
if (!this._pixelReadCanvas) {
this._pixelReadCanvas = document.createElement('canvas');
this._pixelReadCtx = this._pixelReadCanvas.getContext('2d');
}
// Resize canvas if dimensions changed
if (this._pixelReadCanvas.width !== width || this._pixelReadCanvas.height !== height) {
this._pixelReadCanvas.width = width;
this._pixelReadCanvas.height = height;
}
return { canvas: this._pixelReadCanvas, ctx: this._pixelReadCtx };
}
resize(w, h) {
super.resize(w, h);
this._hasPendingDraws = true;
this.flushDraw();
}
async finishDraw() {
this.flushDraw();
const states = [];
// Only blit if we promoted to framebuffer this frame
if (this._frameState === FRAME_STATE.PROMOTED) {
while (this.activeFramebuffers.length > 0) {
const fbo = this.activeFramebuffers.pop();
states.unshift({ fbo, diff: { ...this.states } });
}
this.flushDraw();
this._pInst.push();
this.states.setValue('enableLighting', false);
this.states.setValue('activeImageLight', null);
this._pInst.setCamera(this.finalCamera);
this._pInst.shader(this._getBlitShader());
this._pInst.resetMatrix();
this._pInst.imageMode(this._pInst.CENTER);
this._pInst.image(this.mainFramebuffer, 0, 0);
this._pInst.pop();
this.flushDraw();
}
// Return all uniform buffers to their pools
this._returnUniformBuffersToPool();
// Mark all geometry buffers for return after frame is complete
for (const geometry of this._geometriesWithPools) {
this._markGeometryBuffersForReturn(geometry);
}
// this.uniformBufferPool.push(...(await Promise.all(this.resettingUniformBuffers)));
this.resettingUniformBuffers = [];
// Return all vertex buffers to their pools
this._returnVertexBuffersToPool();
// Destroy all retired buffers
const retired = this._retiredBuffers;
this._postSubmitCallbacks.push(() => {
for (const buffer of retired) {
if (buffer && buffer.destroy) {
buffer.destroy();
}
}
});
this._retiredBuffers = [];
if (this._frameState === FRAME_STATE.PROMOTED) {
for (const { fbo, diff } of states) {
if (fbo !== this.mainFramebuffer || this._frameState !== FRAME_STATE.PROMOTED) {
fbo.begin();
}
for (const key in diff) {
this.states.setValue(key, diff[key]);
}
}
}
}
//////////////////////////////////////////////
// Rendering
//////////////////////////////////////////////
_drawBuffers(geometry, { mode = TRIANGLES, count = 1 }) {
const buffers = this.geometryBufferCache.getCached(geometry);
if (!buffers) return;
// If PENDING and no custom framebuffer, regular draw means PROMOTE
if (this._frameState === FRAME_STATE.PENDING && !this.activeFramebuffer()) {
this._promoteToFramebufferWithoutCopy();
}
this._beginActiveRenderPass();
const passEncoder = this.activeRenderPass;
const currentShader = this._curShader;
this.setupShaderBindGroups(currentShader, passEncoder, { mode, buffers });
// Bind vertex buffers
for (const buffer of currentShader._vertexBuffers || this._getVertexBuffers(currentShader)) {
const location = currentShader.attributes[buffer.attr].location;
const gpuBuffer = buffers[buffer.dst];
passEncoder.setVertexBuffer(location, gpuBuffer, 0);
}
if (currentShader.shaderType === "fill") {
// Bind index buffer and issue draw
if (buffers.indexBuffer) {
const indexFormat = buffers.indexFormat || "uint16";
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
} else {
passEncoder.draw(geometry.vertices.length, count, 0, 0);
}
} else if (currentShader.shaderType === "text") {
if (!buffers.indexBuffer) {
throw new Error("Text geometry must have an index buffer");
}
const indexFormat = buffers.indexFormat || "uint16";
passEncoder.setIndexBuffer(buffers.indexBuffer, indexFormat);
passEncoder.drawIndexed(geometry.faces.length * 3, count, 0, 0, 0);
}
if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") {
passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0);
}
// Mark that we have pending draws that need submission
this._hasPendingDraws = true;
}
setupShaderBindGroups(currentShader, passEncoder, shaderOptionsParams) {
const shaderOptions = this._shaderOptions(shaderOptionsParams);
if (
shaderOptions.compute ||
this.activeShader !== currentShader ||
this._shaderOptionsDifferent(shaderOptions)
) {
passEncoder.setPipeline(currentShader.getPipeline(shaderOptions));
}
if (!shaderOptions.compute) {
this.activeShader = currentShader;
this.activeShaderOptions = shaderOptions;
// Set stencil reference value for clipping
const drawTarget = this.drawTarget();
if (drawTarget._isClipApplied && !this._clipping) {
// When using the clip mask, test against reference value 0 (background)
// WebGL uses NOTEQUAL with ref 0, so fragments pass where stencil != 0
// In WebGPU with 'not-equal', we need ref 0 to pass where stencil != 0
passEncoder.setStencilReference(0);
} else if (this._clipping) {
// When writing to the clip mask, write reference value 1
passEncoder.setStencilReference(1);
}
}
for (const bufferGroup of currentShader._uniformBufferGroups) {
if (bufferGroup.dynamic) {
// Bind uniforms into a part of a big dynamic memory block because
// the group changes often
const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup);
if (currentShader._lastDynamicBuffer[bufferGroup.cacheKey] !== uniformBufferInfo) {
currentShader._cachedBindGroup[bufferGroup.group] = undefined;
currentShader._lastDynamicBuffer[bufferGroup.cacheKey] = uniformBufferInfo;
}
this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo);
uniformBufferInfo.lastOffset = uniformBufferInfo.offset;
uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment;
// Make a shallow copy so that we keep track of the last offset for this uniform
bufferGroup.currentDynamicBuffer = uniformBufferInfo;
bufferGroup.lastOffset = uniformBufferInfo.lastOffset;
} else {
// Bind uniforms to a binding-specific buffer, which may be cached for performance
let bufferInfo;
const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup);
if (!dataChanged && bufferGroup.currentBuffer) {
// Reuse the cached buffer - no need to pack or write
bufferInfo = bufferGroup.currentBuffer;
bufferGroup.buffersInUse.add(bufferInfo);
} else {
// Data changed - get a new buffer and write to it
bufferInfo = this._getUniformBufferFromPool(bufferGroup);
this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo);
this.device.queue.writeBuffer(
bufferInfo.buffer,
0,
bufferInfo.data.buffer,
bufferInfo.data.byteOffset,
bufferInfo.data.byteLength
);
currentShader.buffersDirty.delete(bufferGroup.group * 1000 + bufferGroup.binding);
currentShader._cachedBindGroup[bufferGroup.group] = undefined;
// Cache this buffer and data for next frame
bufferGroup.currentBuffer = bufferInfo;
}
}
}
for (const sampler of currentShader.samplers) {
const key = sampler.group * 1000 + sampler.binding;
if (currentShader.buffersDirty.has(key)) {
currentShader._cachedBindGroup[sampler.group] = undefined;
currentShader.buffersDirty.delete(key);
}
}
for (const storage of currentShader._storageBuffers || []) {
const key = storage.group * 1000 + storage.binding;
if (currentShader.buffersDirty.has(key)) {
currentShader._cachedBindGroup[storage.group] = undefined;
currentShader.buffersDirty.delete(key);
}
}
// Bind sampler/texture uniforms and uniform buffers
for (const iter of currentShader._groupEntries) {
const group = iter[0];
const entries = iter[1];
let dynamicOffsetIdx = 0;
const bgEntries = [];
let bindGroup = currentShader._cachedBindGroup[group];
for (const entry of entries) {
const bufferGroup = entry.bufferGroup;
// Check if this is a uniform buffer binding
const uniformBufferInfo =
bufferGroup?.currentBuffer || bufferGroup?.currentDynamicBuffer;
if (uniformBufferInfo) {
if (bufferGroup.dynamic) {
this.dynamicEntryOffsets[dynamicOffsetIdx++] = bufferGroup.lastOffset;
}
if (!bindGroup) {
bgEntries.push({
binding: entry.binding,
resource: bufferGroup.dynamic
? {
buffer: uniformBufferInfo.uniformBuffer,
offset: 0,
size: Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment,
}
: { buffer: uniformBufferInfo.buffer },
});
}
} else if (entry.storage && !bindGroup) {
// Storage buffer binding
const uniform = currentShader.uniforms[entry.storage.name];
if (!uniform || !uniform._cachedData || !uniform._cachedData._isStorageBuffer) {
throw new Error(
`Storage buffer "${entry.storage.name}" not set. ` +
`Use shader.setUniform("${entry.storage.name}", storageBuffer)`
);
}
bgEntries.push({
binding: entry.binding,
resource: { buffer: uniform._cachedData.buffer },
});
} else if (!bindGroup) {
bgEntries.push({
binding: entry.binding,
resource: entry.uniform.type === 'sampler'
? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler()
: (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view,
});
}
}
const layout = currentShader._bindGroupLayouts[group];
if (!bindGroup) {
bindGroup = this.device.createBindGroup({
layout,
entries: bgEntries,
});
}
currentShader._cachedBindGroup[group] = bindGroup;
if (dynamicOffsetIdx === 0) {
passEncoder.setBindGroup(
group,
bindGroup,
);
} else {
passEncoder.setBindGroup(
group,
bindGroup,
this.dynamicEntryOffsets,
0,
dynamicOffsetIdx
);
}
}
return passEncoder;
}
//////////////////////////////////////////////
// SHADER
//////////////////////////////////////////////
// Writes a single field's value into a Float32Array+DataView at (baseOffset + field.offset).
//
// Field interface (shared by uniform fields from _parseStruct and struct storage schema fields):
// baseType: string - 'f32', 'i32', 'u32', etc.
// size: number - byte size of the field
// offset: number - byte offset of the field within its struct
// packInPlace: bool - true for mat3, written with manual column padding
//
// value: number or number[] - the data to write
_packField(field, value, floatView, dataView, baseOffset) {
if (value === undefined) return;
// Duck typing instead of instanceof to avoid importing a separate
// copy of the Color/Vector classes
if (value?.isVector) {
value = value.values.length !== value.dimensions ? value.values.slice(0, value.dimensions) : value.values;
} else if (value?.isColor) {
value = value._getRGBA([1, 1, 1, 1]);
}
const byteOffset = baseOffset + field.offset;
if (field.baseType === 'u32') {
if (field.size === 4) {
dataView.setUint32(byteOffset, value, true);
} else {
for (let i = 0; i < value.length; i++) {
dataView.setUint32(byteOffset + i * 4, value[i], true);
}
}
} else if (field.baseType === 'i32') {
if (field.size === 4) {
dataView.setInt32(byteOffset, value, true);
} else {
for (let i = 0; i < value.length; i++) {
dataView.setInt32(byteOffset + i * 4, value[i], true);
}
}
} else if (field.packInPlace) {
// In-place packing for mat3: write directly to buffer with padding
const base = byteOffset / 4;
floatView[base + 0] = value[0]; floatView[base + 1] = value[1]; floatView[base + 2] = value[2];
floatView[base + 4] = value[3]; floatView[base + 5] = value[4]; floatView[base + 6] = value[5];
floatView[base + 8] = value[6]; floatView[base + 9] = value[7]; floatView[base + 10] = value[8];
} else if (field.size === 4) {
floatView.set([value], byteOffset / 4);
} else {
floatView.set(value, byteOffset / 4);
}
}
_packUniformGroup(shader, groupUniforms, bufferInfo) {
// Pack a single group's uniforms into a buffer
const data = bufferInfo.data;
const dataView = bufferInfo.dataView;
const offset = bufferInfo.offset || 0;
for (const uniform of groupUniforms) {
const fullUniform = shader.uniforms[uniform.name];
if (!fullUniform || fullUniform.isSampler) continue;
this._packField(fullUniform, fullUniform._mappedData, data, dataView, offset);
}
}
_hasGroupDataChanged(shader, bufferGroup) {
// First time
if (!bufferGroup.currentBuffer) return true;
return shader.buffersDirty.has(bufferGroup.group * 1000 + bufferGroup.binding);
}
_parseStruct(shaderSource, structName) {
const structMatch = shaderSource.match(
new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`)
);
if (!structMatch) {
throw new Error(`Can't find a struct definition for ${structName}`);
}
const structBody = structMatch[1];
const elements = {};
let match;
let index = 0;
let offset = 0;
const elementRegex =
/(?:@location\((\d+)\)\s+)?(\w+):\s*([^\n]+?),?\n/g;
const baseAlignAndSize = (type) => {
if (['f32', 'i32', 'u32', 'bool'].includes(type)) {
return { align: 4, size: 4, items: 1, baseType: type };
}
if (/^vec[2-4](<f32>|f)$/.test(type)) {
const n = parseInt(type.match(/^vec([2-4])/)[1]);
const size = 4 * n;
const align = n === 2 ? 8 : 16;
return { align, size, items: n, baseType: 'f32' };
}
if (/^vec[2-4]<(i32|u32)>$/.test(type)) {
const n = parseInt(type.match(/^vec([2-4])/)[1]);
const match = type.match(/^vec[2-4]<(i32|u32)>$/);
const baseType = match[1]; // 'i32' or 'u32'
const size = 4 * n;
const align = n === 2 ? 8 : 16;
return { align, size, items: n, baseType };
}
if (/^mat[2-4](?:x[2-4])?(<f32>|f)$/.test(type)) {
if (type[4] === 'x' && type[3] !== type[5]) {
throw new Error('Non-square matrices not implemented yet');
}
const dim = parseInt(type[3]);
const align = dim === 2 ? 8 : 16;
// Each column must be aligned
const size = Math.ceil(dim * 4 / align) * align * dim;
// For mat3, use in-place packing to avoid array allocation
const pack = dim === 3
? (data) => [
...data.slice(0, 3),
...data.slice(3, 6),
...data.slice(6, 9),
]
: undefined;
const packInPlace = dim === 3;
return { align, size, pack, packInPlace, items: dim * dim, baseType: 'f32' };
}
if (/^array<.+>$/.test(type)) {
const [, subtype, rawLength] = type.match(/^array<(.+),\s*(\d+)>/);
const length = parseInt(rawLength);
const {
align: elemAlign,
size: elemSize,
items: elemItems,
pack: elemPack = (data) => [...data],
baseType: elemBaseType
} = baseAlignAndSize(subtype);
const stride = Math.ceil(elemSize / elemAlign) * elemAlign;
const pack = (data) => {
const result = [];
for (let i = 0; i < data.length; i += elemItems) {
const elemData = elemPack(data.slice(i, elemItems));
result.push(...elemData);
for (let j = 0; j < stride / 4 - elemData.length; j++) {
result.push(0);
}
}
return result;
};
return {
align: elemAlign,
size: stride * length,
items: elemItems * length,
pack,
baseType: elemBaseType
};
}
throw new Error(`Unknown type in WGSL struct: ${type}`);
};
while ((match = elementRegex.exec(structBody)) !== null) {
const [_, location, name, type] = match;
const { size, align, pack, packInPlace, baseType } = baseAlignAndSize(type);
offset = Math.ceil(offset / align) * align;
const offsetEnd = offset + size;
elements[name] = {
name,
location: location ? parseInt(location) : undefined,
index,
type,
size,
offset,
offsetEnd,
pack,
packInPlace,
baseType
};
index++;
offset = offsetEnd;
}
return elements;
}
_mapUniformData(uniform, data) {
if (uniform.pack) {
return uniform.pack(data);
}
return data;
}
_getShaderAttributes(shader) {
const mainMatch = /fn main\(.+:\s*([^\s\)]+)/.exec(shader._vertSrc);
if (!mainMatch) throw new Error("Can't find `fn main` in vertex shader source");
const inputType = mainMatch[1];
return this._parseStruct(shader.vertSrc(), inputType);
}
getUniformMetadata(shader) {
// Parse all uniform struct bindings in group 0.
// TODO: support non-sampler uniforms being in other groups
// Each binding represents a logical group of uniforms, since they get
// updated or cached all at once.
const uniformGroups = [];
const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<uniform>\s+(\w+)\s*:\s*(\w+);/g;
let match;
const src = shader.shaderType === 'compute' ? shader.computeSrc() : shader.vertSrc();
while ((match = uniformVarRegex.exec(src)) !== null) {
const [_, groupNum, binding, varName, structType] = match;
const bindingIndex = parseInt(binding);
const uniforms = this._parseStruct(src, structType);
uniformGroups.push({
group: parseInt(groupNum),
binding: bindingIndex,
varName,
structType,
uniforms
});
}
if (uniformGroups.length === 0 && shader.shaderType !== 'compute') {
throw new Error('Expected at least one uniform struct bound to @group(0)');
}
// While we're also keeping track of the groups, the API we expose
// to users of p5 is just a flat list of uniforms (which can be the
// individual struct items in the group.)
const allUniforms = {};
for (const group of uniformGroups) {
for (const [uniformName, uniformData] of Object.entries(group.uniforms)) {
allUniforms[uniformName] = {
...uniformData,
group: group.group,
binding: group.binding,
varName: group.varName
};
}
}
// Store uniform groups for buffer pooling
shader._uniformGroups = uniformGroups;
// Extract samplers from group bindings
const samplers = {};
// TODO: support other texture types
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler);/g;
// Extract storage buffers
const storageBuffers = {};
const storageRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var<storage,\s*(read|read_write)>\s+(\w+)\s*:\s*array<\w+>/g;
// Track which bindings are taken by the struct properties we've parsed
// (the rest should be textures/samplers)
const structUniformBindings = {};
for (const g of uniformGroups) {
structUniformBindings[g.group + ',' + g.binding] = true;
}
for (const [src, visibility] of [
[shader.vertSrc(), GPUShaderStage.VERTEX],
[shader.fragSrc(), GPUShaderStage.FRAGMENT],
[shader.computeSrc ? shader.computeSrc() : null, GPUShaderStage.COMPUTE]
]) {
if (!src) continue; // Skip if shader stage doesn't exist
let match;
while ((match = samplerRegex.exec(src)) !== null) {
const [_, group, binding, name, type] = match;
const groupIndex = parseInt(group);
const bindingIndex = parseInt(binding);
// Skip struct uniform bindings which we've already parsed
if (structUniformBindings[groupIndex + ',' + bindingIndex]) continue;
const key = `${groupIndex},${bindingIndex}`;
samplers[key] = {
visibility: (samplers[key]?.visibility || 0) | visibility,
group: groupIndex,
binding: bindingIndex,
name,
type,
isSampler: true,
noData: type === 'sampler',
};
}
for (const sampler of Object.values(samplers)) {
if (sampler.type.startsWith('texture')) {
const samplerName = sampler.name + '_sampler';
const samplerNode = Object
.values(samplers)
.find((s) => s.name === samplerName);
if (!samplerNode) {
throw new Error(
`Every shader texture needs an accompanying sampler. Could not find sampler ${samplerName} for texture ${sampler.name}`
);
}
samplerNode.textureSource = sampler;
}
}
// Parse storage buffers
while ((match = storageRegex.exec(src)) !== null) {
const [_, group, binding, accessMode, name] = match;
const groupIndex = parseInt(group);
const bindingIndex = parseInt(binding);
const key = `${groupIndex},${bindingIndex}`;
const existing = storageBuffers[key];
// If any stage uses read_write, the bind group layout must use read_write
const finalAccessMode = (existing?.accessMode === 'read_write' || accessMode === 'read_write')
? 'read_write'
: accessMode;
storageBuffers[key] = {
visibility: (existing?.visibility || 0) | visibility,
group: groupIndex,
binding: bindingIndex,
name,
accessMode: finalAccessMode, // 'read' or 'read_write'
isStorage: true,
type: 'storage'
};
}
}
// Store storage buffers on shader for later use
shader._storageBuffers = Object.values(storageBuffers);
return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers), ...Object.values(storageBuffers)];
}
getNextBindingIndex({ vert, frag, compute }, group = 0) {
// Get the highest binding index in the specified group and return the next available
const bindingRegex = /@group\((\d+)\)\s*@binding\((\d+)\)/g;
let maxBindingIndex = -1;
const sources = [];
if (vert) sources.push([vert, GPUShaderStage.VERTEX]);
if (frag) sources.push([frag, GPUShaderStage.FRAGMENT]);
if (compute) sources.push([compute, GPUShaderStage.COMPUTE]);
for (const [src, visibility] of sources) {
let match;
while ((match = bindingRegex.exec(src)) !== null) {
const [_, groupIndex, bindingIndex] = match;
if (parseInt(groupIndex) === group) {
maxBindingIndex = Math.max(maxBindingIndex, parseInt(bindingIndex));
}
}
}
return maxBindingIndex + 1;
}
updateUniformValue(shader, uniform, data) {
if (uniform.isSampler) {
uniform.texture =
data instanceof Texture ? data : this.getTexture(data);
} else if (!data?._isStorageBuffer) {
uniform._mappedData = this._mapUniformData(uniform, uniform._cachedData);
}
shader.buffersDirty.add(uniform.group * 1000 + uniform.binding);
}
_updateTexture(uniform, tex) {
tex.update();
}
bindTexture(tex) {}
unbindTexture(tex) {}
_unbindFramebufferTexture(uniform) {}
createTexture({ width, height, format = 'rgba8unorm', usage }) {
const gpuTexture = this.device.createTexture({
size: [width, height],
format,
usage: usage || (
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT
),
});
return { gpuTexture, view: gpuTexture.createView() };
}
uploadTextureFromSource({ gpuTexture }, source) {
this.queue.copyExternalImageToTexture(
{ source },
{ texture: gpuTexture },
[source.width, source.height]
);
// Force submission to ensure texture upload completes before usage
this._hasPendingDraws = true;
this.flushDraw();
}
uploadTextureFromData({ gpuTexture }, data, width, height) {
this.queue.writeTexture(
{ texture: gpuTexture },
data,
{ bytesPerRow: width * 4, rowsPerImage: height },
{ width, height, depthOrArrayLayers: 1 }
);
// Force submission to ensure texture upload completes before usage
this._hasPendingDraws = true;
this.flushDraw();
}
setTextureParams(_texture) {}
getSampler(texture) {
const key = `${texture.minFilter}_${texture.magFilter}_${texture.wrapS}_${texture.wrapT}`;
if (this.samplers.has(key)) {
return this.samplers.get(key);
}
const constantMapping = {
[NEAREST]: 'nearest',
[LINEAR]: 'linear',
[CLAMP]: 'clamp-to-edge',
[REPEAT]: 'repeat',
[MIRROR]: 'mirror-repeat'
};
const sampler = this.device.createSampler({
magFilter: constantMapping[texture.magFilter],
minFilter: constantMapping[texture.minFilter],
addressModeU: constantMapping[texture.wrapS],
addressModeV: constantMapping[texture.wrapT],
});
this.samplers.set(key, sampler);
return sampler;
}
bindTextureToShader(_texture, _sampler, _uniformName, _unit) {}
deleteTexture({ gpuTexture }) {
this._postSubmitCallbacks.push(() => gpuTexture.destroy());
}
_getLightShader() {
if (!this._defaultLightShader) {
this._defaultLightShader = new Shader(
this,
materialVertexShader,
materialFragmentShader,
{
vertex: {
"void beforeVertex": "() {}",
"Vertex getObjectInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getWorldInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getCameraInputs": "(inputs: Vertex) { return inputs; }",
"void afterVertex": "() {}",
},
fragment: {
"void beforeFragment": "() {}",
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
"vec4f combineColors": `(components: ColorComponents) {
var rgb = vec3<f32>(0.0);
rgb += components.diffuse * components.baseColor;
rgb += components.ambient * components.ambientColor;
rgb += components.specular * components.specularColor;
rgb += components.emissive;
return vec4<f32>(rgb, components.opacity);
}`,
"vec4f getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
"void afterFragment": "() {}",
},
}
);
}
return this._defaultLightShader;
}
_getColorShader() {
if (!this._defaultColorShader) {
this._defaultColorShader = new Shader(
this,
colorVertexShader,
colorFragmentShader,
{
vertex: {
"void beforeVertex": "() {}",
"Vertex getObjectInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getWorldInputs": "(inputs: Vertex) { return inputs; }",
"Vertex getCameraInputs": "(inputs: Vertex) { return inputs; }",
"void afterVertex": "() {}",
},
fragment: {
"void beforeFragment": "() {}",
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
"void afterFragment": "() {}",
},
}
);
}
return this._defaultColorShader;
}
_getLineShader() {
if (!this._defaultLineShader) {
this._defaultLineShader = new Shader(
this,
lineDefs + lineVertexShader,
lineDefs + lineFragmentShader,
{
vertex: {
"void beforeVertex": "() {}",
"StrokeVertex getObjectInputs": "(inputs: StrokeVertex) { return inputs; }",
"StrokeVertex getWorldInputs": "(inputs: StrokeVertex) { return inputs; }",
"StrokeVertex getCameraInputs": "(inputs: StrokeVertex) { return inputs; }",
"void afterVertex": "() {}",
},
fragment: {
"void beforeFragment": "() {}",
"Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }",
"vec4<f32> getFinalColor": "(color: vec4<f32>, texCoord: vec2<f32>) { return color; }",
"bool shouldDiscard": "(outside: bool) { return outside; };",
"void afterFragment": "() {}",
},
}
);
}
return this._defaultLineShader;
}
_getFontShader() {
if (!this._defaultFontShader) {
this._defaultFontShader = new Shader(
this,
fontVertexShader,
fontFragmentShader
);
}
return this._defaultFontShader;
}
_getBlitShader() {
if (!this._defaultBlitShader) {
this._defaultBlitShader = new Shader(
this,
blitVertexShader,
blitFragmentShader
);
}
return this._defaultBlitShader;
}
//////////////////////////////////////////////
// Setting
//////////////////////////////////////////////
_adjustDimensions(width, height) {
// TODO: find max texture size
return { adjustedWidth: width, adjustedHeight: height };
}
_applyClip() {
const commandEncoder = this.device.createCommandEncoder();
const activeFramebuffer = this.activeFramebuffer();
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (!depthTextureView) {
return;
}
const depthStencilAttachment = {
view: depthTextureView,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
stencilClearValue: 0,
depthReadOnly: true,
stencilReadOnly: false,
};
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: depthStencilAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_unapplyClip() {
// In WebGPU, clip unapplication is handled through pipeline state rather than direct commands
// The stencil test configuration is set in the render pipeline based on _clipping and _clipInvert flags
// This is already handled in the _shaderOptions() method and pipeline creation
}
_clearClipBuffer() {
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
const activeFramebuffer = this.activeFramebuffer();
const depthTextureView = activeFramebuffer
? (activeFramebuffer.aaDepthTexture
? activeFramebuffer.aaDepthTextureView
: activeFramebuffer.depthTextureView)
: this.depthTextureView;
if (!depthTextureView) {
return;
}
const depthStencilAttachment = {
view: depthTextureView,
stencilLoadOp: 'clear',
stencilStoreOp: 'store',
stencilClearValue: 1,
depthReadOnly: true,
stencilReadOnly: false,
};
const renderPassDescriptor = {
colorAttachments: [],
depthStencilAttachment: depthStencilAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_applyStencilTestIfClipping() {
// This is done via pipeline state in WebGL so this is a no-op
}
//////////////////////////////////////////////
// Shader hooks
//////////////////////////////////////////////
uniformNameFromHookKey(key) {
return key.slice(0, key.indexOf(':'));
}
populateHooks(shader, src, shaderType) {
if (!src.includes('fn main')) return src;
// Apply some p5-specific preprocessing. WGSL doesn't have preprocessor
// statements, but some of our shaders might need it, so we add a lightweight
// way to add code if a hook is augmented. e.g.:
// struct Uniforms {
// // @p5 ifdef Vertex getWorldInputs
// uModelMatrix: mat4f,
// uViewMatrix: mat4f,
// // @p5 endif
// // @p5 ifndef Vertex getWorldInputs
// uModelViewMatrix: mat4f,
// // @p5 endif
// }
src = src.replace(
/\/\/ @p5 (ifdef|ifndef) (\w+)\s+(\w+)\n((?:(?!\/\/ @p5)(?:.|\n))*)\/\/ @p5 endif/g,
(_, condition, hookType, hookName, body) => {
const target = condition === 'ifdef';
if (
(
!!shader.hooks.modified.vertex[`${hookType} ${hookName}`] ||
!!shader.hooks.modified.fragment[`${hookType} ${hookName}`]
) === target
) {
return body;
} else {
return '';
}
}
);
let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/);
const getBuiltinParamName = (mainSrc, builtinName) => {
const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc);
return match ? match[1] : null;
};
const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => {
const existingName = getBuiltinParamName(mainSrc, builtinName);
if (existingName) {
return { mainSrc, argName: existingName };
}
const hasParams = /\(\s*\S/.test(mainSrc);
const injectedMain = mainSrc.replace(
/\)\s*(->|\{)/,
`${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1`
);
return { mainSrc: injectedMain, argName: fallbackName };
};
const getMainStructParameter = (mainSrc) => {
const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc);
if (!match) return null;
return { inputName: match[1], structName: match[2] };
};
const getStructBuiltinFieldName = (structName, builtinName) => {
const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain);
if (!structMatch) return null;
const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]);
return fieldMatch ? fieldMatch[1] : null;
};
const appendHookParams = (params, additionalParams) => {
if (additionalParams.length === 0) return params;
const hasParams = !/^\(\s*\)$/.test(params);
return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`;
};
let hookExtraParams = [];
let hookExtraArgs = [];
if (shaderType === 'vertex') {
const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32');
main = ensuredInstance.mainSrc;
const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32');
main = ensuredVertex.mainSrc;
hookExtraParams = ['instanceID: u32', '_p5VertexId: u32'];
hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName];
} else if (shaderType === 'fragment') {
const directPositionArg = getBuiltinParamName(main, 'position');
let fragmentPositionArg = directPositionArg;
if (!fragmentPositionArg) {
const mainStructParam = getMainStructParameter(main);
if (mainStructParam) {
const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position');
if (positionField) {
fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`;
}
}
}
if (!fragmentPositionArg) {
const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4<f32>');
main = ensuredPosition.mainSrc;
fragmentPositionArg = ensuredPosition.argName;
}
hookExtraParams = ['_p5FragPos: vec4<f32>'];
hookExtraArgs = [fragmentPositionArg];
} else if (shaderType === 'compute') {
const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3<u32>');
main = ensuredGlobalId.mainSrc;
hookExtraParams = ['_p5GlobalId: vec3<u32>'];
hookExtraArgs = [ensuredGlobalId.argName];
}
// Inject hook uniforms as a separate struct at a new binding
let hookUniformFields = '';
for (const key in shader.hooks.uniforms) {
// Skip textures, they don't get added to structs
if (key.endsWith(': sampler2D')) continue;
// WGSL format: "name: type"
hookUniformFields += ` ${key},\n`;
}
if (hookUniformFields) {
// Find the next available binding in group 0
// Use the source we're currently building (preMain) which has texture bindings. We can't call `fragSrc()`
// or `vertSrc()` because we may be in one of those calls already, and might infinite loop
const nextBinding = this.getNextBindingIndex({
vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc,
frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc,
compute: shaderType === 'compute' ? preMain + (shader.hooks.compute?.declarations ?? '') + shader.hooks.declarations : shader._computeSrc,
}, 0);
// Create HookUniforms struct and binding
const hookUniformsDecl = `
// Hook Uniforms (from .modify())
struct HookUniforms {
${hookUniformFields}}
@group(0) @binding(${nextBinding}) var<uniform> hooks: HookUniforms;
`;
// Insert before the first @group binding, or at the end if there are none
const replaced = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`);
if (replaced === preMain) {
// No @group bindings found in base shader, append to preMain
preMain = preMain + '\n' + hookUniformsDecl;
} else {
preMain = replaced;
}
}
// Handle varying variables by injecting them into VertexOutput and FragmentInput structs
if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) {
// Generate struct members for varying variables
let nextLocationIndex = this._getNextAvailableLocation(preMain, shaderType);
let varyingMembers = '';
for (const varyingVar of shader.hooks.varyingVariables) {
// varyingVar is a string like "varName: vec3<f32>"
const member = `@location(${nextLocationIndex++}) ${varyingVar},`;
varyingMembers += member + '\n';
}
if (shaderType === 'vertex') {
// Inject into VertexOutput struct
preMain = preMain.replace(
/struct\s+VertexOutput\s+\{([^}]*)\}/,
(match, body) => `struct VertexOutput {${body}\n${varyingMembers}}`
);
} else if (shaderType === 'fragment') {
// Inject into FragmentInput struct
preMain = preMain.replace(
/struct\s+FragmentInput\s+\{([^}]*)\}/,
(match, body) => `struct FragmentInput {${body}\n${varyingMembers}}`
);
}
}
// Add file-global varying variable declarations
if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) {
let varyingDeclarations = '';
for (const varyingVar of shader.hooks.varyingVariables) {
// varyingVar is a string like "varName: vec3<f32>"
const [varName, varType] = varyingVar.split(':').map(s => s.trim());
varyingDeclarations += `var<private> ${varName}: ${varType};\n`;
}
// Add declarations before the main function
preMain += varyingDeclarations;
if (shaderType === 'vertex') {
// In vertex shader, copy varying variables to output struct before return
let copyStatements = '';
for (const varyingVar of shader.hooks.varyingVariables) {
const [varName] = varyingVar.split(':').map(s => s.trim());
copyStatements += ` OUTPUT_VAR.${varName} = ${varName};\n`;
}
// Find the output variable name from the return statement and replace OUTPUT_VAR
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
if (returnMatch) {
const outputVarName = returnMatch[1];
copyStatements = copyStatements.replace(/OUTPUT_VAR/g, outputVarName);
// Insert before the return statement
postMain = postMain.replace(/(return\s+\w+\s*;)/g, `${copyStatements} $1`);
}
} else if (shaderType === 'fragment') {
// In fragment shader, initialize varying variables from input struct at start of main
let initStatements = '';
for (const varyingVar of shader.hooks.varyingVariables) {
const [varName] = varyingVar.split(':').map(s => s.trim());
initStatements += ` ${varName} = INPUT_VAR.${varName};\n`;
}
const mainStructParam = getMainStructParameter(main);
if (mainStructParam) {
const inputVarName = mainStructParam.inputName;
initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName);
// Insert after the main function parameter but before any other code (anchored to start)
postMain = initStatements + postMain;
}
}
}
// Handle instanceID varying for fragment access
if (shader.hooks.instanceIDVarying) {
const { name, declaration, source, interpolation } = shader.hooks.instanceIDVarying;
const nextLocIndex = this._getNextAvailableLocation(preMain, shaderType);
const interpAttr = interpolation ? ` @interpolate(${interpolation})` : '';
const [varName, varType] = declaration.split(':').map(s => s.trim());
const structMember = `@location(${nextLocIndex})${interpAttr} ${declaration},`;
if (shaderType === 'vertex') {
// Inject into VertexOutput struct
preMain = preMain.replace(
/struct\s+VertexOutput\s+\{([^}]*)\}/,
(match, body) => `struct VertexOutput {${body}\n${structMember}}`
);
// Add private global
preMain += `var<private> ${declaration};\n`;
// Assign from built-in instanceID at start of main()
postMain = `\n ${varName} = ${source};\n` + postMain;
// Copy to output struct before return
const returnMatch = postMain.match(/return\s+(\w+)\s*;/);
if (returnMatch) {
const outputVarName = returnMatch[1];
postMain = postMain.replace(
/(return\s+\w+\s*;)/g,
`${outputVarName}.${varName} = ${varName};\n $1`
);
}
} else if (shaderType === 'fragment') {
// Inject into FragmentInput struct
preMain = preMain.replace(
/struct\s+FragmentInput\s+\{([^}]*)\}/,
(match, body) => `struct FragmentInput {${body}\n${structMember}}`
);
// Add private global
preMain += `var<private> ${declaration};\n`;
// Initialize from input struct at start of main()
const mainStructParam = getMainStructParameter(main);
if (mainStructParam) {
const inputVarName = mainStructParam.inputName;
postMain = `\n ${varName} = ${inputVarName}.${varName};\n` + postMain;
}
}
}
let hooks = '';
let defines = '';
if (shader.hooks.declarations) {
hooks += shader.hooks.declarations + '\n';
}
if (shader.hooks[shaderType] && shader.hooks[shaderType].declarations) {
hooks += shader.hooks[shaderType].declarations + '\n';
}
for (const hookDef in shader.hooks.helpers) {
const [hookType, hookName] = hookDef.split(' ');
const [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks.helpers[hookDef]);
if (hookType === 'void') {
hooks += `fn ${hookName}${params}${body}\n`;
} else {
hooks += `fn ${hookName}${params} -> ${hookType}${body}\n`;
}
}
for (const hookDef in shader.hooks[shaderType]) {
if (hookDef === 'declarations') continue;
const [hookType, hookName] = hookDef.split(' ');
// Add a const so that if the shader wants to
// optimize away the extra function calls in main, it can do so
defines += `const AUGMENTED_HOOK_${hookName} = ${
shader.hooks.modified[shaderType][hookDef] ? 'true' : 'false'
};\n`;
let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]);
params = appendHookParams(params, hookExtraParams);
if (hookType === 'void') {
hooks += `fn HOOK_${hookName}${params}${body}\n`;
} else {
hooks += `fn HOOK_${hookName}${params} -> ${hookType}${body}\n`;
}
}
// Pass stage-specific builtins from main to each hook call.
// Collect ALL HOOK_ calls (including nested ones) then insert
// extra args from right to left so position shifts don't
// invalidate earlier insertion points.
if (hookExtraArgs.length > 0) {
const addHookArgs = (src) => {
const insertions = [];
let searchIdx = 0;
let m;
while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) {
const openParen = searchIdx + m.index + m[0].length - 1;
let pos = openParen + 1;
let nesting = 1;
let hasParams = false;
while (pos < src.length && nesting > 0) {
if (src[pos] === '(') nesting++;
else if (src[pos] === ')') {
nesting--;
if (nesting === 0) break;
} else if (/\S/.test(src[pos])) {
hasParams = true;
}
pos++;
}
insertions.push({ pos, hasParams });
searchIdx = openParen + 1;
}
insertions.sort((a, b) => b.pos - a.pos);
let result = src;
for (const { pos, hasParams } of insertions) {
const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', ');
result = result.slice(0, pos) + insertion + result.slice(pos);
}
return result;
};
preMain = addHookArgs(preMain);
postMain = addHookArgs(postMain);
}
return preMain + '\n' + defines + hooks + main + postMain;
}
_getNextAvailableLocation(shaderSource, shaderType) {
// Parse existing struct to find the highest @location number
let maxLocation = -1;
const structName = shaderType === 'vertex' ? 'VertexOutput' : 'FragmentInput';
// Find the struct definition
const structMatch = shaderSource.match(new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's'));
if (structMatch) {
const structBody = structMatch[1];
// Find all @location(N) declarations
const locationMatches = structBody.matchAll(/@location\((\d+)\)/g);
for (const match of locationMatches) {
const locationNum = parseInt(match[1]);
if (locationNum > maxLocation) {
maxLocation = locationNum;
}
}
}
return maxLocation + 1;
}
getShaderHookTypes(shader, hookName) {
// Create mapping from WGSL types to DataType entries
const wgslToDataType = {
'f32': DataType.float1,
'vec2<f32>': DataType.float2,
'vec3<f32>': DataType.float3,
'vec4<f32>': DataType.float4,
'vec2f': DataType.float2,
'vec3f': DataType.float3,
'vec4f': DataType.float4,
'i32': DataType.int1,
'vec2<i32>': DataType.int2,
'vec3<i32>': DataType.int3,
'vec4<i32>': DataType.int4,
'bool': DataType.bool1,
'vec2<bool>': DataType.bool2,
'vec3<bool>': DataType.bool3,
'vec4<bool>': DataType.bool4,
'mat2x2<f32>': DataType.mat2,
'mat3x3<f32>': DataType.mat3,
'mat4x4<f32>': DataType.mat4,
'texture_2d<f32>': DataType.sampler2D
};
let fullSrc = shader._vertSrc;
let body = shader.hooks.vertex[hookName];
if (!body) {
body = shader.hooks.fragment[hookName];
fullSrc = shader._fragSrc;
}
if (!body) {
body = shader.hooks.compute[hookName];
fullSrc = shader._computeSrc;
}
if (!body) {
throw new Error(`Can't find hook ${hookName}!`);
}
const nameParts = hookName.split(/\s+/g);
const functionName = nameParts.pop();
const returnType = nameParts.pop();
const returnQualifiers = [...nameParts];
const parameterMatch = /\(([^\)]*)\)/.exec(body);
if (!parameterMatch) {
throw new Error(`Couldn't find function parameters in hook body:\n${body}`);
}
const structProperties = structName => {
// WGSL struct parsing: struct StructName { field1: Type, field2: Type }
const structDefMatch = new RegExp(`struct\\s+${structName}\\s*{([^}]*)}`).exec(fullSrc);
if (!structDefMatch) return undefined;
const properties = [];
// Parse WGSL struct fields (e.g., "texCoord: vec2<f32>,")
for (const fieldSrc of structDefMatch[1].split(',')) {
const trimmed = fieldSrc.trim();
if (!trimmed) continue;
// Remove location decorations and parse field
// Format: [@location(N)] fieldName: Type
const fieldMatch = /(?:@location\([^)]*\)\s*)?(\w+)\s*:\s*([^,\s]+)/.exec(trimmed);
if (!fieldMatch) continue;
const name = fieldMatch[1];
let typeName = fieldMatch[2];
const dataType = wgslToDataType[typeName] || null;
const typeProperties = structProperties(typeName);
properties.push({
name,
type: {
typeName: typeName, // Keep native WGSL type name
qualifiers: [],
properties: typeProperties,
dataType: dataType
}
});
}
return properties;
};
const parameters = parameterMatch[1].split(',').map(paramString => {
// WGSL function parameters: name: type or name: binding<type>
const trimmed = paramString.trim();
if (!trimmed) return null;
const parts = trimmed.split(':').map(s => s.trim());
if (parts.length !== 2) return null;
const name = parts[0];
let typeName = parts[1];
// Handle texture bindings like "texture_2d<f32>" -> sampler2D DataType
if (typeName.includes('texture_2d')) {
typeName = 'texture_2d<f32>';
}
const dataType = wgslToDataType[typeName] || null;
const properties = structProperties(typeName);
return {
name,
type: {
typeName: typeName, // Keep native WGSL type name
qualifiers: [],
properties,
dataType: dataType
}
};
}).filter(Boolean);
// Convert WGSL return type to DataType
const returnDataType = wgslToDataType[returnType] || null;
return {
name: functionName,
returnType: {
typeName: returnType, // Keep native WGSL type name
qualifiers: returnQualifiers,
properties: structProperties(returnType),
dataType: returnDataType
},
parameters
};
}
//////////////////////////////////////////////
// Buffer management for pixel reading
//////////////////////////////////////////////
_ensurePixelReadBuffer(requiredSize) {
// Create or resize staging buffer if needed
if (!this.pixelReadBuffer || this.pixelReadBufferSize < requiredSize) {
// Clean up old buffer
if (this.pixelReadBuffer) {
this.flushDraw();
this.pixelReadBuffer.destroy();
}
// Create new buffer with padding to avoid frequent recreations
// Scale by 2 to ensure integer size and reasonable headroom
const bufferSize = Math.max(requiredSize, this.pixelReadBufferSize * 2);
this.pixelReadBuffer = this.device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
this.pixelReadBufferSize = bufferSize;
}
return this.pixelReadBuffer;
}
_alignBytesPerRow(bytesPerRow) {
// WebGPU requires bytesPerRow to be a multiple of 256 bytes for texture-to-buffer copies
return Math.ceil(bytesPerRow / 256) * 256;
}
//////////////////////////////////////////////
// Framebuffer methods
//////////////////////////////////////////////
defaultFramebufferAlpha() {
return true
}
defaultFramebufferAntialias() {
return this._pInst._webgpuAttributes?.antialias !== false;
}
supportsFramebufferAntialias() {
return true;
}
createFramebufferResources(framebuffer) {
}
validateFramebufferFormats(framebuffer) {
if (![
UNSIGNED_BYTE,
FLOAT,
HALF_FLOAT
].includes(framebuffer.format)) {
console.warn(
'Unknown Framebuffer format. ' +
'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' +
'Defaulting to UNSIGNED_BYTE.'
);
framebuffer.format = UNSIGNED_BYTE;
}
if (framebuffer.useDepth && ![
UNSIGNED_INT,
FLOAT
].includes(framebuffer.depthFormat)) {
console.warn(
'Unknown Framebuffer depth format. ' +
'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.'
);
framebuffer.depthFormat = FLOAT;
}
}
recreateFramebufferTextures(framebuffer) {
this.flushDraw();
// Clean up existing textures
if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) {
const tex = framebuffer.colorTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.colorTextureView = null;
}
if (framebuffer.aaColorTexture && framebuffer.aaColorTexture.destroy) {
const tex = framebuffer.aaColorTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.aaColorTextureView = null;
}
if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) {
const tex = framebuffer.depthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.depthTextureView = null;
}
if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) {
const tex = framebuffer.aaDepthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
framebuffer.aaDepthTextureView = null;
}
const baseDescriptor = {
size: {
width: framebuffer.width * framebuffer.density,
height: framebuffer.height * framebuffer.density,
depthOrArrayLayers: 1,
},
format: this._getWebGPUColorFormat(framebuffer),
};
// Create non-multisampled texture for texture binding (always needed)
const colorTextureDescriptor = {
...baseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_SRC |
(framebuffer._useCanvasFormat ? GPUTextureUsage.COPY_DST : 0),
sampleCount: 1,
};
framebuffer.colorTexture = this.device.createTexture(colorTextureDescriptor);
framebuffer.colorTextureView = framebuffer.colorTexture.createView();
// Create multisampled texture for rendering if antialiasing is enabled
if (framebuffer.antialias) {
const aaColorTextureDescriptor = {
...baseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount: this._getValidSampleCount(framebuffer.antialiasSamples),
};
framebuffer.aaColorTexture = this.device.createTexture(aaColorTextureDescriptor);
framebuffer.aaColorTextureView = framebuffer.aaColorTexture.createView();
}
if (framebuffer.useDepth) {
const depthBaseDescriptor = {
size: {
width: framebuffer.width * framebuffer.density,
height: framebuffer.height * framebuffer.density,
depthOrArrayLayers: 1,
},
format: this._getWebGPUDepthFormat(framebuffer),
};
// Create non-multisampled depth texture for texture binding (always needed)
const depthTextureDescriptor = {
...depthBaseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.TEXTURE_BINDING |
(framebuffer._useCanvasFormat ? GPUTextureUsage.COPY_DST : 0),
sampleCount: 1,
};
framebuffer.depthTexture = this.device.createTexture(depthTextureDescriptor);
framebuffer.depthTextureView = framebuffer.depthTexture.createView();
// Create multisampled depth texture for rendering if antialiasing is enabled
if (framebuffer.antialias) {
const aaDepthTextureDescriptor = {
...depthBaseDescriptor,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
sampleCount: this._getValidSampleCount(framebuffer.antialiasSamples),
};
framebuffer.aaDepthTexture = this.device.createTexture(aaDepthTextureDescriptor);
framebuffer.aaDepthTextureView = framebuffer.aaDepthTexture.createView();
}
}
// Clear the framebuffer textures after creation
this._clearFramebufferTextures(framebuffer);
}
_clearFramebufferTextures(framebuffer) {
this._finishActiveRenderPass();
const commandEncoder = this.device.createCommandEncoder();
// Clear the color texture (and multisampled texture if it exists)
const colorAttachment = {
view: framebuffer.aaColorTexture
? framebuffer.aaColorTextureView
: framebuffer.colorTextureView,
loadOp: "clear",
storeOp: "store",
clearValue: { r: 0, g: 0, b: 0, a: 0 },
resolveTarget: framebuffer.aaColorTexture
? framebuffer.colorTextureView
: undefined,
};
// Clear the depth texture if it exists
const depthTexture = framebuffer.aaDepthTexture || framebuffer.depthTexture;
const depthStencilAttachment = depthTexture ? {
view: framebuffer.aaDepthTexture
? framebuffer.aaDepthTextureView
: framebuffer.depthTextureView,
depthLoadOp: "clear",
depthStoreOp: "store",
depthClearValue: 1.0,
stencilLoadOp: "clear",
stencilStoreOp: "store",
depthReadOnly: false,
stencilReadOnly: false,
} : undefined;
const renderPassDescriptor = {
colorAttachments: [colorAttachment],
depthStencilAttachment: depthStencilAttachment,
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.end();
this._pendingCommandEncoders.push(commandEncoder.finish());
this._hasPendingDraws = true;
}
_getFramebufferColorTextureView(framebuffer) {
if (framebuffer.colorTexture) {
return framebuffer.colorTextureView;
}
return null;
}
createFramebufferTextureHandle(framebufferTexture) {
const src = framebufferTexture;
let renderer = this;
return {
get view() {
return renderer._getFramebufferColorTextureView(src.framebuffer);
},
get gpuTexture() {
return src.framebuffer.colorTexture;
}
};
}
// Maps a plain JS value to the WGSL type string that represents it in a struct.
_jsValueToWgslType(value) {
if (typeof value === 'number') return 'f32';
// Duck typing instead of instanceof to avoid importing a separate
// copy of the Color/Vector classes
if (value?.isVector) {
if (value.dimensions === 2) return 'vec2f';
if (value.dimensions === 3) return 'vec3f';
if (value.dimensions === 4) return 'vec4f';
throw new Error(`Unsupported vector dimension ${value.dimensions} for struct storage field`);
}
if (value?.isColor) {
return 'vec4f';
}
if (Array.isArray(value)) {
if (value.length === 2) return 'vec2f';
if (value.length === 3) return 'vec3f';
if (value.length === 4) return 'vec4f';
throw new Error(`Unsupported array length ${value.length} for struct storage field`);
}
throw new Error(`Unsupported value type ${typeof value} for struct storage field`);
}
// Infers a struct schema from the first element of a struct array.
//
// Returns { fields, stride, structBody } where:
// fields: field has the _packField interface (baseType, size, offset, packInPlace) plus:
// name: string - JS property name
// dim: number - float component count, used when creating StrandsNodes
// structBody: everything inside the { ... } of a WGSL struct definition
// stride: how many bytes are reserved for this struct in the buffer
_inferStructSchema(firstElement) {
const entries = Object.entries(firstElement);
if (!p5.disableFriendlyErrors) {
for (const [name, value] of entries) {
if (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
// Duck typing instead of instanceof to avoid importing a separate
// copy of the Color/Vector classes
!value?.isVector &&
!value?.isColor
) {
p5._friendlyError(
`The "${name}" property in your storage data contains a nested object. ` +
`Make sure you only use properties with numbers, arrays of numbers, or p5.Vector.`,
'createStorage'
);
}
}
}
const fieldLines = entries.map(([name, value]) =>
` ${name}: ${this._jsValueToWgslType(value)},`
).join('\n');
const structBody = `{\n${fieldLines}\n}`;
const elements = this._parseStruct(`struct _Tmp ${structBody}`, '_Tmp');
let maxEnd = 0;
let maxAlign = 1;
const fields = entries.map(([name, value]) => {
const el = elements[name];
maxEnd = Math.max(maxEnd, el.offsetEnd);
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
maxAlign = Math.max(maxAlign, align);
// Track original JS type for reconstruction during readback
const kind = value?.isVector ? 'vector'
: value?.isColor ? 'color'
: undefined;
return {
name,
baseType: el.baseType,
size: el.size,
offset: el.offset,
packInPlace: el.packInPlace ?? false,
dim: el.size / 4,
kind,
};
});
const stride = Math.ceil(maxEnd / maxAlign) * maxAlign;
return { fields, stride, structBody };
}
// Packs an array of plain objects into a Float32Array using the given struct schema.
// Reuses _packField so layout rules match uniform packing exactly.
_packStructArray(data, schema) {
const { fields, stride } = schema;
const totalBytes = Math.max(data.length * stride, 16);
const alignedBytes = Math.ceil(totalBytes / 16) * 16;
const buffer = new ArrayBuffer(alignedBytes);
const floatView = new Float32Array(buffer);
const dataView = new DataView(buffer);
for (let i = 0; i < data.length; i++) {
const item = data[i];
const baseOffset = i * stride;
for (const field of fields) {
this._packField(field, item[field.name], floatView, dataView, baseOffset);
}
}
return floatView;
}
// Inverse of _packStructArray reads packed buffer back into plain JS objects
// using the same schema layout - fields, stride and offsets
_unpackStructArray(floatView, schema) {
const { fields, stride } = schema;
const dataView = new DataView(floatView.buffer);
const count = Math.floor(floatView.byteLength / stride);
const result = [];
for (let i = 0; i < count; i++) {
const item = {};
const baseOffset = i * stride;
for (const field of fields) {
const byteOffset = baseOffset + field.offset;
const n = field.size / 4;
if (field.baseType === 'u32') {
if (n === 1) {
item[field.name] = dataView.getUint32(byteOffset, true);
} else {
item[field.name] = Array.from({ length: n }, (_, j) =>
dataView.getUint32(byteOffset + j * 4, true)
);
}
} else if (field.baseType === 'i32') {
if (n === 1) {
item[field.name] = dataView.getInt32(byteOffset, true);
} else {
item[field.name] = Array.from({ length: n }, (_, j) =>
dataView.getInt32(byteOffset + j * 4, true)
);
}
} else {
const idx = byteOffset / 4;
if (n === 1) {
item[field.name] = floatView[idx];
} else {
const values = Array.from(floatView.slice(idx, idx + n));
if (field.kind === 'vector') {
item[field.name] = this._pInst.createVector(...values);
} else if (field.kind === 'color') {
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
// Scale back to the current colorMode range
const maxes = this.states.colorMaxes[this.states.colorMode];
item[field.name] = this._pInst.color(
values[0] * maxes[0], values[1] * maxes[1],
values[2] * maxes[2], values[3] * maxes[3]
);
} else {
item[field.name] = values;
}
}
}
}
result.push(item);
}
return result;
}
createStorage(dataOrCount) {
const device = this.device;
// Struct array: an array of plain objects
if (Array.isArray(dataOrCount) && dataOrCount.length > 0 &&
typeof dataOrCount[0] === 'object' && !Array.isArray(dataOrCount[0])) {
if (!p5.disableFriendlyErrors && dataOrCount.length > 1) {
const firstKeys = Object.keys(dataOrCount[0]);
let warned = false;
for (let i = 1; i < dataOrCount.length; i++) {
const el = dataOrCount[i];
const elKeys = Object.keys(el);
const sameKeys = firstKeys.length === elKeys.length &&
firstKeys.every((k, j) => k === elKeys[j]);
if (!sameKeys) {
p5._friendlyError(
`Element ${i} has different fields than element 0. ` +
`All elements should have the same properties.`,
'createStorage'
);
break;
}
for (const key of firstKeys) {
const firstType = this._jsValueToWgslType(dataOrCount[0][key]);
const elType = this._jsValueToWgslType(el[key]);
if (firstType !== elType) {
p5._friendlyError(
`The "${key}" property of element ${i} has type ${elType} ` +
`but element 0 has type ${firstType}. Proporties should have the same type across all elements.`,
'createStorage'
);
warned = true;
break;
}
}
if (warned) break;
}
}
const schema = this._inferStructSchema(dataOrCount[0]);
const packed = this._packStructArray(dataOrCount, schema);
const size = packed.byteLength;
const buffer = device.createBuffer({
size,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Float32Array(buffer.getMappedRange()).set(packed);
buffer.unmap();
const storageBuffer = new StorageBuffer(buffer, size, this, schema);
this._storageBuffers.add(storageBuffer);
return storageBuffer;
}
// Determine buffer size and initial data
let size, initialData;
if (typeof dataOrCount === 'number') {
// createStorage(count) - zero-initialized
size = dataOrCount * 4; // floats are 4 bytes
initialData = new Float32Array(dataOrCount);
} else {
// createStorage(array) - from data
if (dataOrCount instanceof Float32Array) {
initialData = dataOrCount;
} else if (Array.isArray(dataOrCount)) {
initialData = new Float32Array(dataOrCount);
} else {
throw new Error('createStorage expects a number or array/Float32Array');
}
size = initialData.byteLength;
}
// Align to 16 bytes (WGSL storage buffer alignment requirement)
size = Math.ceil(size / 16) * 16;
// Create storage buffer with STORAGE | COPY_DST | COPY_SRC usage
const buffer = device.createBuffer({
size,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
mappedAtCreation: initialData.length > 0
});
// Write initial data if provided
if (initialData.length > 0) {
const mapping = new Float32Array(buffer.getMappedRange());
mapping.set(initialData);
buffer.unmap();
}
const storageBuffer = new StorageBuffer(buffer, size, this);
// Track for cleanup
this._storageBuffers.add(storageBuffer);
return storageBuffer;
}
_getWebGPUColorFormat(framebuffer) {
if (framebuffer.format === FLOAT) {
return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float';
} else if (framebuffer.format === HALF_FLOAT) {
return framebuffer.channels === RGBA ? 'rgba16float' : 'rgba16float';
} else {
// Framebuffer with _useCanvasFormat should match canvas presentation format
if (framebuffer._useCanvasFormat) {
return this.presentationFormat;
}
// Other framebuffers use standard RGBA format
return framebuffer.channels === RGBA ? 'rgba8unorm' : 'rgba8unorm';
}
}
_getWebGPUDepthFormat(framebuffer) {
if (framebuffer._useCanvasFormat) {
return this.depthFormat;
}
if (framebuffer.useStencil) {
return framebuffer.depthFormat === FLOAT ? 'depth32float-stencil8' : 'depth24plus-stencil8';
} else {
return framebuffer.depthFormat === FLOAT ? 'depth32float' : 'depth24plus';
}
}
_deleteFramebufferTexture(texture) {
this.flushDraw();
const handle = texture.rawTexture();
if (handle.texture && handle.texture.destroy) {
const tex = handle.texture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
this.textures.delete(texture);
}
deleteFramebufferTextures(framebuffer) {
this._deleteFramebufferTexture(framebuffer.color);
if (framebuffer.depth) this._deleteFramebufferTexture(framebuffer.depth);
}
deleteFramebufferResources(framebuffer) {
this.flushDraw();
if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) {
const tex = framebuffer.colorTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) {
const tex = framebuffer.depthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) {
const tex = framebuffer.aaDepthTexture;
this._postSubmitCallbacks.push(() => tex.destroy());
}
}
getFramebufferToBind(framebuffer) {
}
updateFramebufferTexture(framebuffer, property) {
// No-op for WebGPU since antialiasing is handled at pipeline level
}
bindFramebuffer(framebuffer) {}
framebufferYScale() {
return 1;
}
async readFramebufferPixels(framebuffer) {
this.flushDraw();
// await this.finishDraw();
// Ensure all pending GPU work is complete before reading pixels
// await this.queue.onSubmittedWorkDone();
const width = framebuffer.width * framebuffer.density;
const height = framebuffer.height * framebuffer.density;
const bytesPerPixel = 4;
const unalignedBytesPerRow = width * bytesPerPixel;
const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow);
const bufferSize = alignedBytesPerRow * height;
// const stagingBuffer = this._ensurePixelReadBuffer(bufferSize);
const stagingBuffer = this.device.createBuffer({
size: bufferSize,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyTextureToBuffer(
{
texture: framebuffer.colorTexture,
origin: { x: 0, y: 0, z: 0 },
mipLevel: 0,
aspect: 'all'
},
{ buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow, rowsPerImage: height },
{ width, height, depthOrArrayLayers: 1 }
);
this.device.queue.submit([commandEncoder.finish()]);
// Wait for the copy operation to complete
// await this.queue.onSubmittedWorkDone();
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize);
const mappedRange = stagingBuffer.getMappedRange(0, bufferSize);
// If alignment was needed, extract the actual pixel data
let result;
if (alignedBytesPerRow === unalignedBytesPerRow) {
result = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel));
stagingBuffer.unmap();
} else {
// Need to extract pixel data from aligned buffer
result = new Uint8Array(width * height * bytesPerPixel);
const mappedData = new Uint8Array(mappedRange);
for (let y = 0; y < height; y++) {
const srcOffset = y * alignedBytesPerRow;
const dstOffset = y * unalignedBytesPerRow;
result.set(mappedData.subarray(srcOffset, srcOffset + unalignedBytesPerRow), dstOffset);
}
stagingBuffer.unmap();
}
this._ensurePixelsAreRGBA(framebuffer, result);
return result;
}
async readFramebufferPixel(framebuffer, x, y) {
this.flushDraw();
// await this.finishDraw();
// Ensure all pending GPU work is complete before reading pixels
// await this.queue.onSubmittedWorkDone();
const bytesPerPixel = 4;
const alignedBytesPerRow = this._alignBytesPerRow(bytesPerPixel);
const bufferSize = alignedBytesPerRow;
const stagingBuffer = this._ensurePixelReadBuffer(bufferSize);
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyTextureToBuffer(
{
texture: framebuffer.colorTexture,
origin: { x, y, z: 0 }
},
{ buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow },
{ width: 1, height: 1, depthOrArrayLayers: 1 }
);
this.device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize);
const mappedRange = stagingBuffer.getMappedRange(0, bufferSize);
const pixelData = new Uint8Array(mappedRange);
const result = [pixelData[0], pixelData[1], pixelData[2], pixelData[3]];
this._ensurePixelsAreRGBA(framebuffer, result);
stagingBuffer.unmap();
return result;
}
async readFramebufferRegion(framebuffer, x, y, w, h) {
this.flushDraw();
// await this.finishDraw();
// const wasActive = this.activeFramebuffer() === framebuffer;
// if (wasActive) {
// framebuffer.end();
// this.flushDraw()
// }
// Ensure all pending GPU work is complete before reading pixels
// await this.queue.onSubmittedWorkDone();
const width = w * framebuffer.density;
const height = h * framebuffer.density;
const bytesPerPixel = 4;
const unalignedBytesPerRow = width * bytesPerPixel;
const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow);
const bufferSize = alignedBytesPerRow * height;
const stagingBuffer = this._ensurePixelReadBuffer(bufferSize);
const commandEncoder = this.device.createCommandEncoder();
commandEncoder.copyTextureToBuffer(
{
texture: framebuffer.colorTexture,
mipLevel: 0,
origin: { x: x * framebuffer.density, y: y * framebuffer.density, z: 0 }
},
{ buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow },
{ width, height, depthOrArrayLayers: 1 }
);
this.device.queue.submit([commandEncoder.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize);
const mappedRange = stagingBuffer.getMappedRange(0, bufferSize);
let pixelData;
if (alignedBytesPerRow === unalignedBytesPerRow) {
pixelData = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel));
} else {
// Need to extract pixel data from aligned buffer
pixelData = new Uint8Array(width * height * bytesPerPixel);
const mappedData = new Uint8Array(mappedRange);
for (let y = 0; y < height; y++) {
const srcOffset = y * alignedBytesPerRow;
const dstOffset = y * unalignedBytesPerRow;
pixelData.set(mappedData.subarray(srcOffset, srcOffset + unalignedBytesPerRow), dstOffset);
}
}
this._ensurePixelsAreRGBA(framebuffer, pixelData);
// WebGPU doesn't need vertical flipping unlike WebGL
const region = new Image(width, height);
region.imageData = region.canvas.getContext('2d').createImageData(width, height);
region.imageData.data.set(pixelData);
region.pixels = region.imageData.data;
region.updatePixels();
if (framebuffer.density !== 1) {
region.pixelDensity(framebuffer.density);
}
stagingBuffer.unmap();
// if (wasActive) framebuffer.begin();
return region;
}
updateFramebufferPixels(framebuffer) {
const width = framebuffer.width * framebuffer.density;
const height = framebuffer.height * framebuffer.density;
const bytesPerPixel = 4;
const expectedLength = width * height * bytesPerPixel;
if (!framebuffer.pixels || framebuffer.pixels.length !== expectedLength) {
throw new Error(
'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().'
);
}
this.device.queue.writeTexture(
{ texture: framebuffer.colorTexture },
framebuffer.pixels,
{
bytesPerRow: width * bytesPerPixel,
rowsPerImage: height
},
{ width, height, depthOrArrayLayers: 1 }
);
}
//////////////////////////////////////////////
// Main canvas pixel methods
//////////////////////////////////////////////
_ensurePixelsAreRGBA(framebuffer, result) {
// Convert BGRA to RGBA if reading from canvas-format framebuffer on BGRA systems
if (framebuffer._useCanvasFormat && this.presentationFormat === 'bgra8unorm') {
this._convertBGRtoRGB(result);
}
}
_convertBGRtoRGB(pixelData) {
for (let i = 0; i < pixelData.length; i += 4) {
const temp = pixelData[i];
pixelData[i] = pixelData[i + 2];
pixelData[i + 2] = temp;
}
}
async loadPixels() {
this._promoteToFramebuffer();
await this.mainFramebuffer.loadPixels();
this.pixels = this.mainFramebuffer.pixels.slice();
}
async get(x, y, w, h) {
this._promoteToFramebuffer();
return this.mainFramebuffer.get(x, y, w, h);
}
filter(...args) {
// If no custom framebuffer is active, promote to mainFramebuffer
if (!this.activeFramebuffer()) {
this._promoteToFramebuffer();
}
return super.filter(...args);
}
baseFilterShader() {
if (!this._baseFilterShader) {
this._baseFilterShader = new Shader(
this,
baseFilterVertexShader,
baseFilterFragmentShader,
{
vertex: {},
fragment: {
"vec4<f32> getColor": `(inputs: FilterInputs, canvasContent: texture_2d<f32>, canvasContent_sampler: sampler) -> vec4<f32> {
return textureSample(tex, tex_sampler, inputs.texCoord);
}`,
},
hookAliases: {
'getColor': ['filterColor'],
},
}
);
}
return this._baseFilterShader;
}
baseComputeShader() {
if (!this._baseComputeShader) {
this._baseComputeShader = new Shader(
this,
baseComputeShader,
{
compute: {
'void iteration': '(index: vec3<i32>) {}',
},
}
);
}
return this._baseComputeShader;
}
/*
* WebGPU-specific implementation of imageLight shader creation
*/
_createImageLightShader(type) {
if (type === 'diffused') {
return this._pInst.createShader(
imageLightVertexShader,
imageLightDiffusedFragmentShader
);
} else if (type === 'specular') {
return this._pInst.createShader(
imageLightVertexShader,
imageLightSpecularFragmentShader
);
}
throw new Error(`Unknown imageLight shader type: ${type}`);
}
/*
* WebGPU-specific implementation of mipmap texture creation
*/
_createMipmapTexture(levels) {
return new MipmapTexture(this, levels, {});
}
/*
* Prepare WebGPU texture to accumulate mip levels directly
*/
_prepareMipmapData(size, mipLevels) {
// Create WebGPU texture with mipmaps upfront
const textureDescriptor = {
size: {
width: size,
height: size,
depthOrArrayLayers: 1,
},
mipLevelCount: mipLevels,
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
};
const gpuTexture = this.device.createTexture(textureDescriptor);
return {
gpuTexture,
size,
mipLevels,
format: 'rgba8unorm'
};
}
/*
* Copy framebuffer content directly to WebGPU texture mip level
*/
_accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) {
this.flushDraw();
// Copy from framebuffer texture to the mip level
const commandEncoder = this.device.createCommandEncoder();
// Get the underlying WebGPU texture from the framebuffer
const sourceTexture = framebuffer.color.rawTexture().texture;
commandEncoder.copyTextureToTexture(
{
texture: sourceTexture,
origin: { x: 0, y: 0, z: 0 },
},
{
texture: mipmapData.gpuTexture,
mipLevel: mipLevel,
origin: { x: 0, y: 0, z: 0 },
},
{
width: width,
height: height,
depthOrArrayLayers: 1,
}
);
this.device.queue.submit([commandEncoder.finish()]);
}
/*
* Create final MipmapTexture from WebGPU texture
*/
_finalizeMipmapTexture(mipmapData) {
// Create a MipmapTexture that wraps the pre-built WebGPU texture
return new MipmapTexture(this, mipmapData, {});
}
createMipmapTextureHandle({ gpuTexture, format, dataType, width, height }) {
// WebGPU always uses pre-built GPU textures for mipmaps
return {
texture: gpuTexture,
view: gpuTexture.createView(),
glFormat: format || 'rgba8unorm',
glDataType: dataType || 'uint8'
};
}
compute(shader, x, y = 1, z = 1) {
if (shader.shaderType !== 'compute') {
throw new Error('compute() can only be called with a compute shader');
}
this._finishActiveRenderPass();
// Ensure shader is initialized and finalized
if (!shader._compiled) {
shader.init();
}
// Set default uniforms
shader.setDefaultUniforms();
shader.setUniform('uTotalCount', [x, y, z]);
// Calculate optimal workgroup size (8x8x1 = 64 threads per workgroup)
const WORKGROUP_SIZE_X = 8;
const WORKGROUP_SIZE_Y = 8;
const WORKGROUP_SIZE_Z = 1;
// auto spreading: if any dimension is too large or for performance optimization,
// spread total iteration count across dimensions
const totalIterations = x * y * z;
const MAX_THREADS_PER_DIM = 65535 * 8;
let px = x;
let py = y;
let pz = z;
// we spread if we exceed GPU limits OR if it involves a large 1D dispatch
const exceedsLimits = x > MAX_THREADS_PER_DIM || y > MAX_THREADS_PER_DIM || z > MAX_THREADS_PER_DIM;
const isLarge1D = totalIterations > 1024 && y === 1 && z === 1;
if (exceedsLimits || isLarge1D) {
// Always use 2D square spreading (√N × √N).
// Benchmarks showed 2D square equals or outperforms 3D cube at every
// scale tested, with simpler index reconstruction in the shader.
px = Math.ceil(Math.sqrt(totalIterations));
py = Math.ceil(totalIterations / px);
pz = 1;
}
shader.setUniform('uPhysicalCount', [px, py, pz]);
const workgroupCountX = Math.ceil(px / WORKGROUP_SIZE_X);
const workgroupCountY = Math.ceil(py / WORKGROUP_SIZE_Y);
const workgroupCountZ = Math.ceil(pz / WORKGROUP_SIZE_Z);
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
this.setupShaderBindGroups(shader, passEncoder, {
compute: true,
workgroupSize: [WORKGROUP_SIZE_X, WORKGROUP_SIZE_Y, WORKGROUP_SIZE_Z],
});
// Dispatch compute workgroups
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
}
}
p5.RendererWebGPU = RendererWebGPU;
p5.renderers[WEBGPU] = p5.RendererWebGPU;
// TODO: move this and the duplicate in the WebGL renderer to another file
fn.setAttributes = async function (key, value) {
return this._renderer._setAttributes(key, value);
};
}
if (typeof p5 !== "undefined") {
rendererWebGPU(p5, p5.prototype);
}
export { rendererWebGPU as default };