From 2fa995afde7af07f652427256a0147381e313ca2 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 23 May 2025 16:01:49 -0400 Subject: [PATCH 01/98] Factor out a base 3D renderer --- src/core/p5.Renderer3D.js | 1370 ++++++++++++++++++++++++++++++ src/webgl/p5.RendererGL.js | 1380 +------------------------------ src/webgpu/p5.RendererWebGPU.js | 27 + 3 files changed, 1432 insertions(+), 1345 deletions(-) create mode 100644 src/core/p5.Renderer3D.js create mode 100644 src/webgpu/p5.RendererWebGPU.js diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js new file mode 100644 index 0000000000..b9e275c977 --- /dev/null +++ b/src/core/p5.Renderer3D.js @@ -0,0 +1,1370 @@ +import * as constants from "../core/constants"; +import { Renderer } from './p5.Renderer'; +import GeometryBuilder from "../webgl/GeometryBuilder"; +import { Matrix } from "../math/p5.Matrix"; +import { Camera } from "../webgl/p5.Camera"; +import { Vector } from "../math/p5.Vector"; +import { ShapeBuilder } from "../webgl/ShapeBuilder"; +import { GeometryBufferCache } from "../webgl/GeometryBufferCache"; +import { filterParamDefaults } from "../image/const"; +import { PrimitiveToVerticesConverter } from "../shape/custom_shapes"; +import { Color } from "../color/p5.Color"; +import { Element } from "../dom/p5.Element"; +import { Framebuffer } from "../webgl/p5.Framebuffer"; + +export const STROKE_CAP_ENUM = {}; +export const STROKE_JOIN_ENUM = {}; +export let lineDefs = ""; +const defineStrokeCapEnum = function (key, val) { + lineDefs += `#define STROKE_CAP_${key} ${val}\n`; + STROKE_CAP_ENUM[constants[key]] = val; +}; +const defineStrokeJoinEnum = function (key, val) { + lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; + STROKE_JOIN_ENUM[constants[key]] = val; +}; + +// Define constants in line shaders for each type of cap/join, and also record +// the values in JS objects +defineStrokeCapEnum("ROUND", 0); +defineStrokeCapEnum("PROJECT", 1); +defineStrokeCapEnum("SQUARE", 2); +defineStrokeJoinEnum("ROUND", 0); +defineStrokeJoinEnum("MITER", 1); +defineStrokeJoinEnum("BEVEL", 2); + +export class Renderer3D extends Renderer { + constructor(pInst, w, h, isMainCanvas, elt) { + super(pInst, w, h, isMainCanvas); + + // Create new canvas + this.canvas = this.elt = elt || document.createElement("canvas"); + this.setupContext(); + + if (this._isMainCanvas) { + // for pixel method sharing with pimage + this._pInst._curElement = this; + this._pInst.canvas = this.canvas; + } else { + // hide if offscreen buffer by default + this.canvas.style.display = "none"; + } + this.elt.id = "defaultCanvas0"; + this.elt.classList.add("p5Canvas"); + + // Set and return p5.Element + this.wrappedElt = new Element(this.elt, this._pInst); + + // Extend renderer with methods of p5.Element with getters + for (const p of Object.getOwnPropertyNames(Element.prototype)) { + if (p !== 'constructor' && p[0] !== '_') { + Object.defineProperty(this, p, { + get() { + return this.wrappedElt[p]; + } + }) + } + } + + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + + this.width = w; + this.height = h; + + // Set canvas size + this.elt.width = w * this._pixelDensity; + this.elt.height = h * this._pixelDensity; + this.elt.style.width = `${w}px`; + this.elt.style.height = `${h}px`; + this._origViewport = { + width: this.GL.drawingBufferWidth, + height: this.GL.drawingBufferHeight, + }; + this.viewport(this._origViewport.width, this._origViewport.height); + + // Attach canvas element to DOM + if (this._pInst._userNode) { + // user input node case + this._pInst._userNode.appendChild(this.elt); + } else { + //create main element + if (document.getElementsByTagName("main").length === 0) { + let m = document.createElement("main"); + document.body.appendChild(m); + } + //append canvas to main + document.getElementsByTagName("main")[0].appendChild(this.elt); + } + + this.isP3D = true; //lets us know we're in 3d mode + + // When constructing a new Geometry, this will represent the builder + this.geometryBuilder = undefined; + + // Push/pop state + this.states.uModelMatrix = new Matrix(4); + this.states.uViewMatrix = new Matrix(4); + this.states.uPMatrix = new Matrix(4); + + this.states.curCamera = new Camera(this); + this.states.uPMatrix.set(this.states.curCamera.projMatrix); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + + this.states.enableLighting = false; + this.states.ambientLightColors = []; + this.states.specularColors = [1, 1, 1]; + this.states.directionalLightDirections = []; + this.states.directionalLightDiffuseColors = []; + this.states.directionalLightSpecularColors = []; + this.states.pointLightPositions = []; + this.states.pointLightDiffuseColors = []; + this.states.pointLightSpecularColors = []; + this.states.spotLightPositions = []; + this.states.spotLightDirections = []; + this.states.spotLightDiffuseColors = []; + this.states.spotLightSpecularColors = []; + this.states.spotLightAngle = []; + this.states.spotLightConc = []; + this.states.activeImageLight = null; + + this.states.curFillColor = [1, 1, 1, 1]; + this.states.curAmbientColor = [1, 1, 1, 1]; + this.states.curSpecularColor = [0, 0, 0, 0]; + this.states.curEmissiveColor = [0, 0, 0, 0]; + this.states.curStrokeColor = [0, 0, 0, 1]; + + this.states.curBlendMode = constants.BLEND; + + this.states._hasSetAmbient = false; + this.states._useSpecularMaterial = false; + this.states._useEmissiveMaterial = false; + this.states._useNormalMaterial = false; + this.states._useShininess = 1; + this.states._useMetalness = 0; + + this.states.tint = [255, 255, 255, 255]; + + this.states.constantAttenuation = 1; + this.states.linearAttenuation = 0; + this.states.quadraticAttenuation = 0; + + this.states._currentNormal = new Vector(0, 0, 1); + + this.states.drawMode = constants.FILL; + + this.states._tex = null; + this.states.textureMode = constants.IMAGE; + this.states.textureWrapX = constants.CLAMP; + this.states.textureWrapY = constants.CLAMP; + + // erasing + this._isErasing = false; + + // simple lines + this._simpleLines = false; + + // clipping + this._clipDepths = []; + this._isClipApplied = false; + this._stencilTestOn = false; + + this.mixedAmbientLight = []; + this.mixedSpecularColor = []; + + // p5.framebuffer for this are calculated in getDiffusedTexture function + this.diffusedTextures = new Map(); + // p5.framebuffer for this are calculated in getSpecularTexture function + this.specularTextures = new Map(); + + this.preEraseBlend = undefined; + this._cachedBlendMode = undefined; + this._cachedFillStyle = [1, 1, 1, 1]; + this._cachedStrokeStyle = [0, 0, 0, 1]; + this._isBlending = false; + + this._useLineColor = false; + this._useVertexColor = false; + + this.registerEnabled = new Set(); + + // Camera + this.states.curCamera._computeCameraDefaultSettings(); + this.states.curCamera._setDefaultCamera(); + + // FilterCamera + this.filterCamera = new Camera(this); + this.filterCamera._computeCameraDefaultSettings(); + this.filterCamera._setDefaultCamera(); + // Information about the previous frame's touch object + // for executing orbitControl() + this.prevTouches = []; + // Velocity variable for use with orbitControl() + this.zoomVelocity = 0; + this.rotateVelocity = new Vector(0, 0); + this.moveVelocity = new Vector(0, 0); + // Flags for recording the state of zooming, rotation and moving + this.executeZoom = false; + this.executeRotateAndMove = false; + + this._drawingFilter = false; + this._drawingImage = false; + + this.specularShader = undefined; + this.sphereMapping = undefined; + this.diffusedShader = undefined; + this._baseFilterShader = undefined; + this._defaultLightShader = undefined; + this._defaultImmediateModeShader = undefined; + this._defaultNormalShader = undefined; + this._defaultColorShader = undefined; + this._defaultPointShader = undefined; + + this.states.userFillShader = undefined; + this.states.userStrokeShader = undefined; + this.states.userPointShader = undefined; + this.states.userImageShader = undefined; + + this.states.curveDetail = 1 / 4; + + // Used by beginShape/endShape functions to construct a p5.Geometry + this.shapeBuilder = new ShapeBuilder(this); + + this.geometryBufferCache = new GeometryBufferCache(this); + + this.curStrokeCap = constants.ROUND; + this.curStrokeJoin = constants.ROUND; + + // map of texture sources to textures created in this gl context via this.getTexture(src) + this.textures = new Map(); + + // set of framebuffers in use + this.framebuffers = new Set(); + // stack of active framebuffers + this.activeFramebuffers = []; + + // for post processing step + this.states.filterShader = undefined; + this.filterLayer = undefined; + this.filterLayerTemp = undefined; + this.defaultFilterShaders = {}; + + this.fontInfos = {}; + + this._curShader = undefined; + this.drawShapeCount = 1; + + this.scratchMat3 = new Matrix(3); + } + + remove() { + this.wrappedElt.remove(); + this.wrappedElt = null; + this.canvas = null; + this.elt = null; + } + + ////////////////////////////////////////////// + // Geometry Building + ////////////////////////////////////////////// + + /** + * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added + * to the geometry and then returned when + * endGeometry() is called. One can also use + * buildGeometry() to pass a function that + * draws shapes. + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them upfront with `beginGeometry()` and `endGeometry()` and then + * drawing that will run faster than repeatedly drawing the individual pieces. + * @private + */ + beginGeometry() { + if (this.geometryBuilder) { + throw new Error( + "It looks like `beginGeometry()` is being called while another p5.Geometry is already being build." + ); + } + this.geometryBuilder = new GeometryBuilder(this); + this.geometryBuilder.prevFillColor = this.states.fillColor; + this.fill(new Color([-1, -1, -1, -1])); + } + + /** + * Finishes creating a new p5.Geometry that was + * started using beginGeometry(). One can also + * use buildGeometry() to pass a function that + * draws shapes. + * @private + * + * @returns {p5.Geometry} The model that was built. + */ + endGeometry() { + if (!this.geometryBuilder) { + throw new Error( + "Make sure you call beginGeometry() before endGeometry()!" + ); + } + const geometry = this.geometryBuilder.finish(); + this.fill(this.geometryBuilder.prevFillColor); + this.geometryBuilder = undefined; + return geometry; + } + + /** + * Creates a new p5.Geometry that contains all + * the shapes drawn in a provided callback function. The returned combined shape + * can then be drawn all at once using model(). + * + * If you need to draw complex shapes every frame which don't change over time, + * combining them with `buildGeometry()` once and then drawing that will run + * faster than repeatedly drawing the individual pieces. + * + * One can also draw shapes directly between + * beginGeometry() and + * endGeometry() instead of using a callback + * function. + * @param {Function} callback A function that draws shapes. + * @returns {p5.Geometry} The model that was built from the callback function. + */ + buildGeometry(callback) { + this.beginGeometry(); + callback(); + return this.endGeometry(); + } + + ////////////////////////////////////////////// + // Shape drawing + ////////////////////////////////////////////// + + beginShape(...args) { + super.beginShape(...args); + // TODO remove when shape refactor is complete + // this.shapeBuilder.beginShape(...args); + } + + curveDetail(d) { + if (d === undefined) { + return this.states.curveDetail; + } else { + this.states.setValue("curveDetail", d); + } + } + + drawShape(shape) { + const visitor = new PrimitiveToVerticesConverter({ + curveDetail: this.states.curveDetail, + }); + shape.accept(visitor); + this.shapeBuilder.constructFromContours(shape, visitor.contours); + + if (this.geometryBuilder) { + this.geometryBuilder.addImmediate( + this.shapeBuilder.geometry, + this.shapeBuilder.shapeMode + ); + } else if (this.states.fillColor || this.states.strokeColor) { + if (this.shapeBuilder.shapeMode === constants.POINTS) { + this._drawPoints( + this.shapeBuilder.geometry.vertices, + this.buffers.point + ); + } else { + this._drawGeometry(this.shapeBuilder.geometry, { + mode: this.shapeBuilder.shapeMode, + count: this.drawShapeCount, + }); + } + } + this.drawShapeCount = 1; + } + + endShape(mode, count) { + this.drawShapeCount = count; + super.endShape(mode, count); + } + + vertexProperty(...args) { + this.currentShape.vertexProperty(...args); + } + + normal(xorv, y, z) { + if (xorv instanceof Vector) { + this.states.setValue("_currentNormal", xorv); + } else { + this.states.setValue("_currentNormal", new Vector(xorv, y, z)); + } + this.updateShapeVertexProperties(); + } + + model(model, count = 1) { + if (model.vertices.length > 0) { + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(model); + } else { + if (!this.geometryInHash(model.gid)) { + model._edgesToVertices(); + this._getOrMakeCachedBuffers(model); + } + + this._drawGeometry(model, { count }); + } + } + } + + ////////////////////////////////////////////// + // Rendering + ////////////////////////////////////////////// + + _drawGeometryScaled(model, scaleX, scaleY, scaleZ) { + let originalModelMatrix = this.states.uModelMatrix; + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + try { + this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); + + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(model); + } else { + this._drawGeometry(model); + } + } finally { + this.states.setValue("uModelMatrix", originalModelMatrix); + } + } + + _update() { + // reset model view and apply initial camera transform + // (containing only look at info; no projection). + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + this.states.uModelMatrix.reset(); + this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone()); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + + // reset light data for new frame. + + this.states.setValue("ambientLightColors", []); + this.states.setValue("specularColors", [1, 1, 1]); + + this.states.setValue("directionalLightDirections", []); + this.states.setValue("directionalLightDiffuseColors", []); + this.states.setValue("directionalLightSpecularColors", []); + + this.states.setValue("pointLightPositions", []); + this.states.setValue("pointLightDiffuseColors", []); + this.states.setValue("pointLightSpecularColors", []); + + this.states.setValue("spotLightPositions", []); + this.states.setValue("spotLightDirections", []); + this.states.setValue("spotLightDiffuseColors", []); + this.states.setValue("spotLightSpecularColors", []); + this.states.setValue("spotLightAngle", []); + this.states.setValue("spotLightConc", []); + + this.states.setValue("enableLighting", false); + + //reset tint value for new frame + this.states.setValue("tint", [255, 255, 255, 255]); + + //Clear depth every frame + this._resetBuffersBeforeDraw() + } + + background(...args) { + const _col = this._pInst.color(...args); + this.clear(..._col._getRGBA()); + } + + ////////////////////////////////////////////// + // Positioning + ////////////////////////////////////////////// + + get uModelMatrix() { + return this.states.uModelMatrix; + } + + get uViewMatrix() { + return this.states.uViewMatrix; + } + + get uPMatrix() { + return this.states.uPMatrix; + } + + get uMVMatrix() { + const m = this.uModelMatrix.copy(); + m.mult(this.uViewMatrix); + return m; + } + + /** + * Get a matrix from world-space to screen-space + */ + getWorldToScreenMatrix() { + const modelMatrix = this.states.uModelMatrix; + const viewMatrix = this.states.uViewMatrix; + const projectionMatrix = this.states.uPMatrix; + const projectedToScreenMatrix = new Matrix(4); + projectedToScreenMatrix.scale(this.width, this.height, 1); + projectedToScreenMatrix.translate([0.5, 0.5, 0.5]); + projectedToScreenMatrix.scale(0.5, -0.5, 0.5); + + const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); + const modelViewProjectionMatrix = modelViewMatrix.mult(projectionMatrix); + const worldToScreenMatrix = modelViewProjectionMatrix.mult(projectedToScreenMatrix); + return worldToScreenMatrix; + } + + ////////////////////////////////////////////// + // COLOR + ////////////////////////////////////////////// + /** + * Basic fill material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @chainable + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * noStroke(); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube spinning + */ + fill(...args) { + super.fill(...args); + //see material.js for more info on color blending in webgl + // const color = fn.color.apply(this._pInst, arguments); + const color = this.states.fillColor; + this.states.setValue("curFillColor", color._array); + this.states.setValue("drawMode", constants.FILL); + this.states.setValue("_useNormalMaterial", false); + this.states.setValue("_tex", null); + } + + /** + * Basic stroke material for geometry with a given color + * @param {Number|Number[]|String|p5.Color} v1 gray value, + * red or hue value (depending on the current color mode), + * or color Array, or CSS color string + * @param {Number} [v2] green or saturation value + * @param {Number} [v3] blue or brightness value + * @param {Number} [a] opacity + * @example + *
+ * + * function setup() { + * createCanvas(200, 200, WEBGL); + * } + * + * function draw() { + * background(0); + * stroke(240, 150, 150); + * fill(100, 100, 240); + * rotateX(frameCount * 0.01); + * rotateY(frameCount * 0.01); + * box(75, 75, 75); + * } + * + *
+ * + * @alt + * black canvas with purple cube with pink outline spinning + */ + stroke(...args) { + super.stroke(...args); + // const color = fn.color.apply(this._pInst, arguments); + this.states.setValue("curStrokeColor", this.states.strokeColor._array); + } + + getCommonVertexProperties() { + return { + ...super.getCommonVertexProperties(), + stroke: this.states.strokeColor, + fill: this.states.fillColor, + normal: this.states._currentNormal, + }; + } + + getSupportedIndividualVertexProperties() { + return { + textureCoordinates: true, + }; + } + + strokeCap(cap) { + this.curStrokeCap = cap; + } + + strokeJoin(join) { + this.curStrokeJoin = join; + } + getFilterLayer() { + if (!this.filterLayer) { + this.filterLayer = new Framebuffer(this); + } + return this.filterLayer; + } + getFilterLayerTemp() { + if (!this.filterLayerTemp) { + this.filterLayerTemp = new Framebuffer(this); + } + return this.filterLayerTemp; + } + matchSize(fboToMatch, target) { + if ( + fboToMatch.width !== target.width || + fboToMatch.height !== target.height + ) { + fboToMatch.resize(target.width, target.height); + } + + if (fboToMatch.pixelDensity() !== target.pixelDensity()) { + fboToMatch.pixelDensity(target.pixelDensity()); + } + } + filter(...args) { + let fbo = this.getFilterLayer(); + + // use internal shader for filter constants BLUR, INVERT, etc + let filterParameter = undefined; + let operation = undefined; + if (typeof args[0] === "string") { + operation = args[0]; + let useDefaultParam = + operation in filterParamDefaults && args[1] === undefined; + filterParameter = useDefaultParam + ? filterParamDefaults[operation] + : args[1]; + + // Create and store shader for constants once on initial filter call. + // Need to store multiple in case user calls different filters, + // eg. filter(BLUR) then filter(GRAY) + if (!(operation in this.defaultFilterShaders)) { + this.defaultFilterShaders[operation] = this._makeFilterShader(fbo.renderer, operation); + } + this.states.setValue( + "filterShader", + this.defaultFilterShaders[operation] + ); + } + // use custom user-supplied shader + else { + this.states.setValue("filterShader", args[0]); + } + + // Setting the target to the framebuffer when applying a filter to a framebuffer. + + const target = this.activeFramebuffer() || this; + + // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. + this.matchSize(fbo, target); + + fbo.draw(() => this.clear()); // prevent undesirable feedback effects accumulating secretly. + + let texelSize = [ + 1 / (target.width * target.pixelDensity()), + 1 / (target.height * target.pixelDensity()), + ]; + + // apply blur shader with multiple passes. + if (operation === constants.BLUR) { + // Treating 'tmp' as a framebuffer. + const tmp = this.getFilterLayerTemp(); + // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. + this.matchSize(tmp, target); + // setup + this.push(); + this.states.setValue("strokeColor", null); + this.blendMode(constants.BLEND); + + // draw main to temp buffer + this.shader(this.states.filterShader); + this.states.filterShader.setUniform("texelSize", texelSize); + this.states.filterShader.setUniform("canvasSize", [ + target.width, + target.height, + ]); + this.states.filterShader.setUniform( + "radius", + Math.max(1, filterParameter) + ); + + // Horiz pass: draw `target` to `tmp` + tmp.draw(() => { + this.states.filterShader.setUniform("direction", [1, 0]); + this.states.filterShader.setUniform("tex0", target); + this.clear(); + this.shader(this.states.filterShader); + this.noLights(); + this.plane(target.width, target.height); + }); + + // Vert pass: draw `tmp` to `fbo` + fbo.draw(() => { + this.states.filterShader.setUniform("direction", [0, 1]); + this.states.filterShader.setUniform("tex0", tmp); + this.clear(); + this.shader(this.states.filterShader); + this.noLights(); + this.plane(target.width, target.height); + }); + + this.pop(); + } + // every other non-blur shader uses single pass + else { + fbo.draw(() => { + this.states.setValue("strokeColor", null); + this.blendMode(constants.BLEND); + this.shader(this.states.filterShader); + this.states.filterShader.setUniform("tex0", target); + this.states.filterShader.setUniform("texelSize", texelSize); + this.states.filterShader.setUniform("canvasSize", [ + target.width, + target.height, + ]); + // filterParameter uniform only used for POSTERIZE, and THRESHOLD + // but shouldn't hurt to always set + this.states.filterShader.setUniform("filterParameter", filterParameter); + this.noLights(); + this.plane(target.width, target.height); + }); + } + // draw fbo contents onto main renderer. + this.push(); + this.states.setValue("strokeColor", null); + this.clear(); + this.push(); + this.states.setValue("imageMode", constants.CORNER); + this.blendMode(constants.BLEND); + target.filterCamera._resize(); + this.setCamera(target.filterCamera); + this.resetMatrix(); + this._drawingFilter = true; + this.image( + fbo, + 0, + 0, + this.width, + this.height, + -target.width / 2, + -target.height / 2, + target.width, + target.height + ); + this._drawingFilter = false; + this.clearDepth(); + this.pop(); + this.pop(); + } + + // Pass this off to the host instance so that we can treat a renderer and a + // framebuffer the same in filter() + + pixelDensity(newDensity) { + if (newDensity) { + return this._pInst.pixelDensity(newDensity); + } + return this._pInst.pixelDensity(); + } + + blendMode(mode) { + if ( + mode === constants.DARKEST || + mode === constants.LIGHTEST || + mode === constants.ADD || + mode === constants.BLEND || + mode === constants.SUBTRACT || + mode === constants.SCREEN || + mode === constants.EXCLUSION || + mode === constants.REPLACE || + mode === constants.MULTIPLY || + mode === constants.REMOVE + ) + this.states.setValue("curBlendMode", mode); + else if ( + mode === constants.BURN || + mode === constants.OVERLAY || + mode === constants.HARD_LIGHT || + mode === constants.SOFT_LIGHT || + mode === constants.DODGE + ) { + console.warn( + "BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode." + ); + } + } + + erase(opacityFill, opacityStroke) { + if (!this._isErasing) { + this.preEraseBlend = this.states.curBlendMode; + this._isErasing = true; + this.blendMode(constants.REMOVE); + this._cachedFillStyle = this.states.curFillColor.slice(); + this.states.setValue("curFillColor", [1, 1, 1, opacityFill / 255]); + this._cachedStrokeStyle = this.states.curStrokeColor.slice(); + this.states.setValue("curStrokeColor", [1, 1, 1, opacityStroke / 255]); + } + } + + noErase() { + if (this._isErasing) { + // Restore colors + this.states.setValue("curFillColor", this._cachedFillStyle.slice()); + this.states.setValue("curStrokeColor", this._cachedStrokeStyle.slice()); + // Restore blend mode + this.states.setValue("curBlendMode", this.preEraseBlend); + this.blendMode(this.preEraseBlend); + // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode + this._isErasing = false; + this._applyBlendMode(); + } + } + + drawTarget() { + return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; + } + + beginClip(options = {}) { + super.beginClip(options); + + this.drawTarget()._isClipApplied = true; + + this._applyClip(); + + this.push(); + this.resetShader(); + if (this.states.fillColor) this.fill(0, 0); + if (this.states.strokeColor) this.stroke(0, 0); + } + + endClip() { + this.pop(); + + this._unapplyClip(); + + // Mark the depth at which the clip has been applied so that we can clear it + // when we pop past this depth + this._clipDepths.push(this._pushPopDepth); + + super.endClip(); + } + + _clearClip() { + this._clearClipBuffer(); + if (this._clipDepths.length > 0) { + this._clipDepths.pop(); + } + this.drawTarget()._isClipApplied = false; + } + + /** + * @private + * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings + * of the renderer's canvas. It will be created if it does not yet exist, and + * reused if it does. + */ + _getTempFramebuffer() { + if (!this._tempFramebuffer) { + this._tempFramebuffer = new Framebuffer(this, { + format: constants.UNSIGNED_BYTE, + useDepth: this._pInst._glAttributes.depth, + depthFormat: constants.UNSIGNED_INT, + antialias: this._pInst._glAttributes.antialias, + }); + } + return this._tempFramebuffer; + } + + ////////////////////////////////////////////// + // HASH | for geometry + ////////////////////////////////////////////// + + geometryInHash(gid) { + return this.geometryBufferCache.isCached(gid); + } + + /** + * [resize description] + * @private + * @param {Number} w [description] + * @param {Number} h [description] + */ + resize(w, h) { + super.resize(w, h); + + // save canvas properties + const props = {}; + for (const key in this.drawingContext) { + const val = this.drawingContext[key]; + if (typeof val !== "object" && typeof val !== "function") { + props[key] = val; + } + } + + const dimensions = this._adjustDimensions(w, h); + w = dimensions.adjustedWidth; + h = dimensions.adjustedHeight; + + this.width = w; + this.height = h; + + this.canvas.width = w * this._pixelDensity; + this.canvas.height = h * this._pixelDensity; + this.canvas.style.width = `${w}px`; + this.canvas.style.height = `${h}px`; + this._updateViewport(); + + this.states.curCamera._resize(); + + //resize pixels buffer + if (typeof this.pixels !== "undefined") { + this._createPixelsArray(); + } + + for (const framebuffer of this.framebuffers) { + // Notify framebuffers of the resize so that any auto-sized framebuffers + // can also update their size + framebuffer._canvasSizeChanged(); + } + + // reset canvas properties + for (const savedKey in props) { + try { + this.drawingContext[savedKey] = props[savedKey]; + } catch (err) { + // ignore read-only property errors + } + } + } + + applyMatrix(a, b, c, d, e, f) { + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + if (arguments.length === 16) { + // this.states.uModelMatrix.apply(arguments); + Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); + } else { + this.states.uModelMatrix.apply([ + a, + b, + 0, + 0, + c, + d, + 0, + 0, + 0, + 0, + 1, + 0, + e, + f, + 0, + 1, + ]); + } + } + + /** + * [translate description] + * @private + * @param {Number} x [description] + * @param {Number} y [description] + * @param {Number} z [description] + * @chainable + * @todo implement handle for components or vector as args + */ + translate(x, y, z) { + if (x instanceof Vector) { + z = x.z; + y = x.y; + x = x.x; + } + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + this.states.uModelMatrix.translate([x, y, z]); + return this; + } + + /** + * Scales the Model View Matrix by a vector + * @private + * @param {Number | p5.Vector | Array} x [description] + * @param {Number} [y] y-axis scalar + * @param {Number} [z] z-axis scalar + * @chainable + */ + scale(x, y, z) { + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + this.states.uModelMatrix.scale(x, y, z); + return this; + } + + rotate(rad, axis) { + if (typeof axis === "undefined") { + return this.rotateZ(rad); + } + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + Matrix.prototype.rotate4x4.apply(this.states.uModelMatrix, arguments); + return this; + } + + rotateX(rad) { + this.rotate(rad, 1, 0, 0); + return this; + } + + rotateY(rad) { + this.rotate(rad, 0, 1, 0); + return this; + } + + rotateZ(rad) { + this.rotate(rad, 0, 0, 1); + return this; + } + + pop(...args) { + if ( + this._clipDepths.length > 0 && + this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] + ) { + this._clearClip(); + if (!this._userEnabledStencil) { + this._internalDisable.call(this.GL, this.GL.STENCIL_TEST); + } + + // Reset saved state + // this._userEnabledStencil = this._savedStencilTestState; + } + super.pop(...args); + this._applyStencilTestIfClipping(); + } + + resetMatrix() { + this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); + this.states.uModelMatrix.reset(); + this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone()); + this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); + return this; + } + + ////////////////////////////////////////////// + // SHADER + ////////////////////////////////////////////// + + _getStrokeShader() { + // select the stroke shader to use + const stroke = this.states.userStrokeShader; + if (stroke) { + return stroke; + } + return this._getLineShader(); + } + + /* + * This method will handle both image shaders and + * fill shaders, returning the appropriate shader + * depending on the current context (image or shape). + */ + _getFillShader() { + // If drawing an image, check for user-defined image shader and filters + if (this._drawingImage) { + // Use user-defined image shader if available and no filter is applied + if (this.states.userImageShader && !this._drawingFilter) { + return this.states.userImageShader; + } else { + return this._getLightShader(); // Fallback to light shader + } + } + // If user has defined a fill shader, return that + else if (this.states.userFillShader) { + return this.states.userFillShader; + } + // Use normal shader if normal material is active + else if (this.states._useNormalMaterial) { + return this._getNormalShader(); + } + // Use light shader if lighting or textures are enabled + else if (this.states.enableLighting || this.states._tex) { + return this._getLightShader(); + } + // Default to color shader if no other conditions are met + return this._getColorShader(); + } + + _getPointShader() { + // select the point shader to use + const point = this.states.userPointShader; + if (!point || !point.isPointShader()) { + return this._getPointShader(); + } + return point; + } + + baseMaterialShader() { + return this._getLightShader(); + } + + baseNormalShader() { + return this._getNormalShader(); + } + + baseColorShader() { + return this._getColorShader(); + } + + /** + * TODO(dave): un-private this when there is a way to actually override the + * shader used for points + * + * Get the shader used when drawing points with `point()`. + * + * You can call `pointShader().modify()` + * and change any of the following hooks: + * - `void beforeVertex`: Called at the start of the vertex shader. + * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. + * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. + * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. + * - `void afterVertex`: Called at the end of the vertex shader. + * - `void beforeFragment`: Called at the start of the fragment shader. + * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. + * - `vec4 getFinalColor`: Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. + * - `void afterFragment`: Called at the end of the fragment shader. + * + * Call `pointShader().inspectHooks()` to see all the possible hooks and + * their default implementations. + * + * @returns {p5.Shader} The `point()` shader + * @private() + */ + pointShader() { + return this._getPointShader(); + } + + baseStrokeShader() { + return this._getLineShader(); + } + + /** + * @private + * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if + * the main canvas is the current draw target. + */ + activeFramebuffer() { + return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; + } + + createFramebuffer(options) { + return new Framebuffer(this, options); + } + + _setGlobalUniforms(shader) { + const modelMatrix = this.states.uModelMatrix; + const viewMatrix = this.states.uViewMatrix; + const projectionMatrix = this.states.uPMatrix; + const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); + + shader.setUniform( + "uPerspective", + this.states.curCamera.useLinePerspective ? 1 : 0 + ); + shader.setUniform("uViewMatrix", viewMatrix.mat4); + shader.setUniform("uProjectionMatrix", projectionMatrix.mat4); + shader.setUniform("uModelMatrix", modelMatrix.mat4); + shader.setUniform("uModelViewMatrix", modelViewMatrix.mat4); + if (shader.uniforms.uModelViewProjectionMatrix) { + const modelViewProjectionMatrix = modelViewMatrix.copy(); + modelViewProjectionMatrix.mult(projectionMatrix); + shader.setUniform( + "uModelViewProjectionMatrix", + modelViewProjectionMatrix.mat4 + ); + } + if (shader.uniforms.uNormalMatrix) { + this.scratchMat3.inverseTranspose4x4(modelViewMatrix); + shader.setUniform("uNormalMatrix", this.scratchMat3.mat3); + } + if (shader.uniforms.uModelNormalMatrix) { + this.scratchMat3.inverseTranspose4x4(this.states.uModelMatrix); + shader.setUniform("uModelNormalMatrix", this.scratchMat3.mat3); + } + if (shader.uniforms.uCameraNormalMatrix) { + this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); + shader.setUniform("uCameraNormalMatrix", this.scratchMat3.mat3); + } + if (shader.uniforms.uCameraRotation) { + this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); + shader.setUniform("uCameraRotation", this.scratchMat3.mat3); + } + shader.setUniform("uViewport", this._viewport); + } + + _setStrokeUniforms(strokeShader) { + // set the uniform values + strokeShader.setUniform("uSimpleLines", this._simpleLines); + strokeShader.setUniform("uUseLineColor", this._useLineColor); + strokeShader.setUniform("uMaterialColor", this.states.curStrokeColor); + strokeShader.setUniform("uStrokeWeight", this.states.strokeWeight); + strokeShader.setUniform("uStrokeCap", STROKE_CAP_ENUM[this.curStrokeCap]); + strokeShader.setUniform( + "uStrokeJoin", + STROKE_JOIN_ENUM[this.curStrokeJoin] + ); + } + + _setFillUniforms(fillShader) { + this.mixedSpecularColor = [...this.states.curSpecularColor]; + const empty = this._getEmptyTexture(); + + if (this.states._useMetalness > 0) { + this.mixedSpecularColor = this.mixedSpecularColor.map( + (mixedSpecularColor, index) => + this.states.curFillColor[index] * this.states._useMetalness + + mixedSpecularColor * (1 - this.states._useMetalness) + ); + } + + // TODO: optimize + fillShader.setUniform("uUseVertexColor", this._useVertexColor); + fillShader.setUniform("uMaterialColor", this.states.curFillColor); + fillShader.setUniform("isTexture", !!this.states._tex); + // We need to explicitly set uSampler back to an empty texture here. + // In general, we record the last set texture so we can re-apply it + // the next time a shader is used. However, the texture() function + // works differently and is global p5 state. If the p5 state has + // been cleared, we also need to clear the value in uSampler to match. + fillShader.setUniform("uSampler", this.states._tex || empty); + fillShader.setUniform("uTint", this.states.tint); + + fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); + fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); + fillShader.setUniform("uSpecularMatColor", this.mixedSpecularColor); + fillShader.setUniform("uEmissiveMatColor", this.states.curEmissiveColor); + fillShader.setUniform("uSpecular", this.states._useSpecularMaterial); + fillShader.setUniform("uEmissive", this.states._useEmissiveMaterial); + fillShader.setUniform("uShininess", this.states._useShininess); + fillShader.setUniform("uMetallic", this.states._useMetalness); + + this._setImageLightUniforms(fillShader); + + fillShader.setUniform("uUseLighting", this.states.enableLighting); + + const pointLightCount = this.states.pointLightDiffuseColors.length / 3; + fillShader.setUniform("uPointLightCount", pointLightCount); + fillShader.setUniform( + "uPointLightLocation", + this.states.pointLightPositions + ); + fillShader.setUniform( + "uPointLightDiffuseColors", + this.states.pointLightDiffuseColors + ); + fillShader.setUniform( + "uPointLightSpecularColors", + this.states.pointLightSpecularColors + ); + + const directionalLightCount = + this.states.directionalLightDiffuseColors.length / 3; + fillShader.setUniform("uDirectionalLightCount", directionalLightCount); + fillShader.setUniform( + "uLightingDirection", + this.states.directionalLightDirections + ); + fillShader.setUniform( + "uDirectionalDiffuseColors", + this.states.directionalLightDiffuseColors + ); + fillShader.setUniform( + "uDirectionalSpecularColors", + this.states.directionalLightSpecularColors + ); + + // TODO: sum these here... + const ambientLightCount = this.states.ambientLightColors.length / 3; + this.mixedAmbientLight = [...this.states.ambientLightColors]; + + if (this.states._useMetalness > 0) { + this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors) => { + let mixing = ambientColors - this.states._useMetalness; + return Math.max(0, mixing); + }); + } + fillShader.setUniform("uAmbientLightCount", ambientLightCount); + fillShader.setUniform("uAmbientColor", this.mixedAmbientLight); + + const spotLightCount = this.states.spotLightDiffuseColors.length / 3; + fillShader.setUniform("uSpotLightCount", spotLightCount); + fillShader.setUniform("uSpotLightAngle", this.states.spotLightAngle); + fillShader.setUniform("uSpotLightConc", this.states.spotLightConc); + fillShader.setUniform( + "uSpotLightDiffuseColors", + this.states.spotLightDiffuseColors + ); + fillShader.setUniform( + "uSpotLightSpecularColors", + this.states.spotLightSpecularColors + ); + fillShader.setUniform("uSpotLightLocation", this.states.spotLightPositions); + fillShader.setUniform( + "uSpotLightDirection", + this.states.spotLightDirections + ); + + fillShader.setUniform( + "uConstantAttenuation", + this.states.constantAttenuation + ); + fillShader.setUniform("uLinearAttenuation", this.states.linearAttenuation); + fillShader.setUniform( + "uQuadraticAttenuation", + this.states.quadraticAttenuation + ); + } + + // getting called from _setFillUniforms + _setImageLightUniforms(shader) { + //set uniform values + shader.setUniform("uUseImageLight", this.states.activeImageLight != null); + // true + if (this.states.activeImageLight) { + // this.states.activeImageLight has image as a key + // look up the texture from the diffusedTexture map + let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); + shader.setUniform("environmentMapDiffused", diffusedLight); + let specularLight = this.getSpecularTexture(this.states.activeImageLight); + + shader.setUniform("environmentMapSpecular", specularLight); + } + } + + _setPointUniforms(pointShader) { + // set the uniform values + pointShader.setUniform("uMaterialColor", this.states.curStrokeColor); + // @todo is there an instance where this isn't stroke weight? + // should be they be same var? + pointShader.setUniform( + "uPointSize", + this.states.strokeWeight * this._pixelDensity + ); + } +} diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 5e46d2d106..7614dc33af 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1,9 +1,5 @@ import * as constants from "../core/constants"; -import GeometryBuilder from "./GeometryBuilder"; -import { Renderer } from "../core/p5.Renderer"; -import { Matrix } from "../math/p5.Matrix"; -import { Camera } from "./p5.Camera"; -import { Vector } from "../math/p5.Vector"; +import { Renderer3D, lineDefs } from "../core/p5.Renderer3D"; import { RenderBuffer } from "./p5.RenderBuffer"; import { DataArray } from "./p5.DataArray"; import { Shader } from "./p5.Shader"; @@ -12,9 +8,6 @@ import { Texture, MipmapTexture } from "./p5.Texture"; import { Framebuffer } from "./p5.Framebuffer"; import { Graphics } from "../core/p5.Graphics"; import { Element } from "../dom/p5.Element"; -import { ShapeBuilder } from "./ShapeBuilder"; -import { GeometryBufferCache } from "./GeometryBufferCache"; -import { filterParamDefaults } from "../image/const"; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -47,29 +40,6 @@ import filterOpaqueFrag from "./shaders/filters/opaque.frag"; import filterInvertFrag from "./shaders/filters/invert.frag"; import filterThresholdFrag from "./shaders/filters/threshold.frag"; import filterShaderVert from "./shaders/filters/default.vert"; -import { PrimitiveToVerticesConverter } from "../shape/custom_shapes"; -import { Color } from "../color/p5.Color"; - -const STROKE_CAP_ENUM = {}; -const STROKE_JOIN_ENUM = {}; -let lineDefs = ""; -const defineStrokeCapEnum = function (key, val) { - lineDefs += `#define STROKE_CAP_${key} ${val}\n`; - STROKE_CAP_ENUM[constants[key]] = val; -}; -const defineStrokeJoinEnum = function (key, val) { - lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; - STROKE_JOIN_ENUM[constants[key]] = val; -}; - -// Define constants in line shaders for each type of cap/join, and also record -// the values in JS objects -defineStrokeCapEnum("ROUND", 0); -defineStrokeCapEnum("PROJECT", 1); -defineStrokeCapEnum("SQUARE", 2); -defineStrokeJoinEnum("ROUND", 0); -defineStrokeJoinEnum("MITER", 1); -defineStrokeJoinEnum("BEVEL", 2); const defaultShaders = { normalVert, @@ -116,212 +86,15 @@ const filterShaderFrags = { * @todo extend class to include public method for offscreen * rendering (FBO). */ -class RendererGL extends Renderer { - constructor(pInst, w, h, isMainCanvas, elt, attr) { - super(pInst, w, h, isMainCanvas); - - // Create new canvas - this.canvas = this.elt = elt || document.createElement("canvas"); - this._setAttributeDefaults(pInst); - this._initContext(); - // This redundant property is useful in reminding you that you are - // interacting with WebGLRenderingContext, still worth considering future removal - this.GL = this.drawingContext; - - if (this._isMainCanvas) { - // for pixel method sharing with pimage - this._pInst._curElement = this; - this._pInst.canvas = this.canvas; - } else { - // hide if offscreen buffer by default - this.canvas.style.display = "none"; - } - this.elt.id = "defaultCanvas0"; - this.elt.classList.add("p5Canvas"); - - // Set and return p5.Element - this.wrappedElt = new Element(this.elt, this._pInst); - - // Extend renderer with methods of p5.Element with getters - for (const p of Object.getOwnPropertyNames(Element.prototype)) { - if (p !== 'constructor' && p[0] !== '_') { - Object.defineProperty(this, p, { - get() { - return this.wrappedElt[p]; - } - }) - } - } - - const dimensions = this._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - - this.width = w; - this.height = h; - - // Set canvas size - this.elt.width = w * this._pixelDensity; - this.elt.height = h * this._pixelDensity; - this.elt.style.width = `${w}px`; - this.elt.style.height = `${h}px`; - this._origViewport = { - width: this.GL.drawingBufferWidth, - height: this.GL.drawingBufferHeight, - }; - this.viewport(this._origViewport.width, this._origViewport.height); - - // Attach canvas element to DOM - if (this._pInst._userNode) { - // user input node case - this._pInst._userNode.appendChild(this.elt); - } else { - //create main element - if (document.getElementsByTagName("main").length === 0) { - let m = document.createElement("main"); - document.body.appendChild(m); - } - //append canvas to main - document.getElementsByTagName("main")[0].appendChild(this.elt); - } +class RendererGL extends Renderer3D { + constructor(pInst, w, h, isMainCanvas, elt) { + super(pInst, w, h, isMainCanvas, elt); - this.isP3D = true; //lets us know we're in 3d mode - - // When constructing a new Geometry, this will represent the builder - this.geometryBuilder = undefined; - - // Push/pop state - this.states.uModelMatrix = new Matrix(4); - this.states.uViewMatrix = new Matrix(4); - this.states.uPMatrix = new Matrix(4); - - this.states.curCamera = new Camera(this); - this.states.uPMatrix.set(this.states.curCamera.projMatrix); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); - - this.states.enableLighting = false; - this.states.ambientLightColors = []; - this.states.specularColors = [1, 1, 1]; - this.states.directionalLightDirections = []; - this.states.directionalLightDiffuseColors = []; - this.states.directionalLightSpecularColors = []; - this.states.pointLightPositions = []; - this.states.pointLightDiffuseColors = []; - this.states.pointLightSpecularColors = []; - this.states.spotLightPositions = []; - this.states.spotLightDirections = []; - this.states.spotLightDiffuseColors = []; - this.states.spotLightSpecularColors = []; - this.states.spotLightAngle = []; - this.states.spotLightConc = []; - this.states.activeImageLight = null; - - this.states.curFillColor = [1, 1, 1, 1]; - this.states.curAmbientColor = [1, 1, 1, 1]; - this.states.curSpecularColor = [0, 0, 0, 0]; - this.states.curEmissiveColor = [0, 0, 0, 0]; - this.states.curStrokeColor = [0, 0, 0, 1]; - - this.states.curBlendMode = constants.BLEND; - - this.states._hasSetAmbient = false; - this.states._useSpecularMaterial = false; - this.states._useEmissiveMaterial = false; - this.states._useNormalMaterial = false; - this.states._useShininess = 1; - this.states._useMetalness = 0; - - this.states.tint = [255, 255, 255, 255]; - - this.states.constantAttenuation = 1; - this.states.linearAttenuation = 0; - this.states.quadraticAttenuation = 0; - - this.states._currentNormal = new Vector(0, 0, 1); - - this.states.drawMode = constants.FILL; - - this.states._tex = null; - this.states.textureMode = constants.IMAGE; - this.states.textureWrapX = constants.CLAMP; - this.states.textureWrapY = constants.CLAMP; - - // erasing - this._isErasing = false; - - // simple lines - this._simpleLines = false; - - // clipping - this._clipDepths = []; - this._isClipApplied = false; - this._stencilTestOn = false; - - this.mixedAmbientLight = []; - this.mixedSpecularColor = []; - - // p5.framebuffer for this are calculated in getDiffusedTexture function - this.diffusedTextures = new Map(); - // p5.framebuffer for this are calculated in getSpecularTexture function - this.specularTextures = new Map(); - - this.preEraseBlend = undefined; - this._cachedBlendMode = undefined; - this._cachedFillStyle = [1, 1, 1, 1]; - this._cachedStrokeStyle = [0, 0, 0, 1]; if (this.webglVersion === constants.WEBGL2) { this.blendExt = this.GL; } else { this.blendExt = this.GL.getExtension("EXT_blend_minmax"); } - this._isBlending = false; - - this._useLineColor = false; - this._useVertexColor = false; - - this.registerEnabled = new Set(); - - // Camera - this.states.curCamera._computeCameraDefaultSettings(); - this.states.curCamera._setDefaultCamera(); - - // FilterCamera - this.filterCamera = new Camera(this); - this.filterCamera._computeCameraDefaultSettings(); - this.filterCamera._setDefaultCamera(); - // Information about the previous frame's touch object - // for executing orbitControl() - this.prevTouches = []; - // Velocity variable for use with orbitControl() - this.zoomVelocity = 0; - this.rotateVelocity = new Vector(0, 0); - this.moveVelocity = new Vector(0, 0); - // Flags for recording the state of zooming, rotation and moving - this.executeZoom = false; - this.executeRotateAndMove = false; - - this._drawingFilter = false; - this._drawingImage = false; - - this.specularShader = undefined; - this.sphereMapping = undefined; - this.diffusedShader = undefined; - this._baseFilterShader = undefined; - this._defaultLightShader = undefined; - this._defaultImmediateModeShader = undefined; - this._defaultNormalShader = undefined; - this._defaultColorShader = undefined; - this._defaultPointShader = undefined; - - this.states.userFillShader = undefined; - this.states.userStrokeShader = undefined; - this.states.userPointShader = undefined; - this.states.userImageShader = undefined; - - this.states.curveDetail = 1 / 4; - - // Used by beginShape/endShape functions to construct a p5.Geometry - this.shapeBuilder = new ShapeBuilder(this); this.buffers = { fill: [ @@ -407,32 +180,6 @@ class RendererGL extends Renderer { user: [], }; - this.geometryBufferCache = new GeometryBufferCache(this); - - this.curStrokeCap = constants.ROUND; - this.curStrokeJoin = constants.ROUND; - - // map of texture sources to textures created in this gl context via this.getTexture(src) - this.textures = new Map(); - - // set of framebuffers in use - this.framebuffers = new Set(); - // stack of active framebuffers - this.activeFramebuffers = []; - - // for post processing step - this.states.filterShader = undefined; - this.filterLayer = undefined; - this.filterLayerTemp = undefined; - this.defaultFilterShaders = {}; - - this.fontInfos = {}; - - this._curShader = undefined; - this.drawShapeCount = 1; - - this.scratchMat3 = new Matrix(3); - this._userEnabledStencil = false; // Store original methods for internal use this._internalEnable = this.drawingContext.enable; @@ -457,160 +204,12 @@ class RendererGL extends Renderer { }; } - remove() { - this.wrappedElt.remove(); - this.wrappedElt = null; - this.canvas = null; - this.elt = null; - } - - ////////////////////////////////////////////// - // Geometry Building - ////////////////////////////////////////////// - - /** - * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added - * to the geometry and then returned when - * endGeometry() is called. One can also use - * buildGeometry() to pass a function that - * draws shapes. - * - * If you need to draw complex shapes every frame which don't change over time, - * combining them upfront with `beginGeometry()` and `endGeometry()` and then - * drawing that will run faster than repeatedly drawing the individual pieces. - * @private - */ - beginGeometry() { - if (this.geometryBuilder) { - throw new Error( - "It looks like `beginGeometry()` is being called while another p5.Geometry is already being build." - ); - } - this.geometryBuilder = new GeometryBuilder(this); - this.geometryBuilder.prevFillColor = this.states.fillColor; - this.fill(new Color([-1, -1, -1, -1])); - } - - /** - * Finishes creating a new p5.Geometry that was - * started using beginGeometry(). One can also - * use buildGeometry() to pass a function that - * draws shapes. - * @private - * - * @returns {p5.Geometry} The model that was built. - */ - endGeometry() { - if (!this.geometryBuilder) { - throw new Error( - "Make sure you call beginGeometry() before endGeometry()!" - ); - } - const geometry = this.geometryBuilder.finish(); - this.fill(this.geometryBuilder.prevFillColor); - this.geometryBuilder = undefined; - return geometry; - } - - /** - * Creates a new p5.Geometry that contains all - * the shapes drawn in a provided callback function. The returned combined shape - * can then be drawn all at once using model(). - * - * If you need to draw complex shapes every frame which don't change over time, - * combining them with `buildGeometry()` once and then drawing that will run - * faster than repeatedly drawing the individual pieces. - * - * One can also draw shapes directly between - * beginGeometry() and - * endGeometry() instead of using a callback - * function. - * @param {Function} callback A function that draws shapes. - * @returns {p5.Geometry} The model that was built from the callback function. - */ - buildGeometry(callback) { - this.beginGeometry(); - callback(); - return this.endGeometry(); - } - - ////////////////////////////////////////////// - // Shape drawing - ////////////////////////////////////////////// - - beginShape(...args) { - super.beginShape(...args); - // TODO remove when shape refactor is complete - // this.shapeBuilder.beginShape(...args); - } - - curveDetail(d) { - if (d === undefined) { - return this.states.curveDetail; - } else { - this.states.setValue("curveDetail", d); - } - } - - drawShape(shape) { - const visitor = new PrimitiveToVerticesConverter({ - curveDetail: this.states.curveDetail, - }); - shape.accept(visitor); - this.shapeBuilder.constructFromContours(shape, visitor.contours); - - if (this.geometryBuilder) { - this.geometryBuilder.addImmediate( - this.shapeBuilder.geometry, - this.shapeBuilder.shapeMode - ); - } else if (this.states.fillColor || this.states.strokeColor) { - if (this.shapeBuilder.shapeMode === constants.POINTS) { - this._drawPoints( - this.shapeBuilder.geometry.vertices, - this.buffers.point - ); - } else { - this._drawGeometry(this.shapeBuilder.geometry, { - mode: this.shapeBuilder.shapeMode, - count: this.drawShapeCount, - }); - } - } - this.drawShapeCount = 1; - } - - endShape(mode, count) { - this.drawShapeCount = count; - super.endShape(mode, count); - } - - vertexProperty(...args) { - this.currentShape.vertexProperty(...args); - } - - normal(xorv, y, z) { - if (xorv instanceof Vector) { - this.states.setValue("_currentNormal", xorv); - } else { - this.states.setValue("_currentNormal", new Vector(xorv, y, z)); - } - this.updateShapeVertexProperties(); - } - - model(model, count = 1) { - if (model.vertices.length > 0) { - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(model); - } else { - if (!this.geometryInHash(model.gid)) { - model._edgesToVertices(); - this._getOrMakeCachedBuffers(model); - } - - this._drawGeometry(model, { count }); - } - } + setupContext() { + this._setAttributeDefaults(this._pInst); + this._initContext(); + // This redundant property is useful in reminding you that you are + // interacting with WebGLRenderingContext, still worth considering future removal + this.GL = this.drawingContext; } ////////////////////////////////////////////// @@ -646,22 +245,6 @@ class RendererGL extends Renderer { this.buffers.user = []; } - _drawGeometryScaled(model, scaleX, scaleY, scaleZ) { - let originalModelMatrix = this.states.uModelMatrix; - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - try { - this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); - - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(model); - } else { - this._drawGeometry(model); - } - } finally { - this.states.setValue("uModelMatrix", originalModelMatrix); - } - } - _drawFills(geometry, { count, mode } = {}) { this._useVertexColor = geometry.vertexColors.length > 0; @@ -1004,433 +587,15 @@ class RendererGL extends Renderer { } } - _update() { - // reset model view and apply initial camera transform - // (containing only look at info; no projection). - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - this.states.uModelMatrix.reset(); - this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone()); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); - - // reset light data for new frame. - - this.states.setValue("ambientLightColors", []); - this.states.setValue("specularColors", [1, 1, 1]); - - this.states.setValue("directionalLightDirections", []); - this.states.setValue("directionalLightDiffuseColors", []); - this.states.setValue("directionalLightSpecularColors", []); - - this.states.setValue("pointLightPositions", []); - this.states.setValue("pointLightDiffuseColors", []); - this.states.setValue("pointLightSpecularColors", []); - - this.states.setValue("spotLightPositions", []); - this.states.setValue("spotLightDirections", []); - this.states.setValue("spotLightDiffuseColors", []); - this.states.setValue("spotLightSpecularColors", []); - this.states.setValue("spotLightAngle", []); - this.states.setValue("spotLightConc", []); - - this.states.setValue("enableLighting", false); - - //reset tint value for new frame - this.states.setValue("tint", [255, 255, 255, 255]); - - //Clear depth every frame + _resetBuffersBeforeDraw() { this.GL.clearStencil(0); this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT); if (!this._userEnabledStencil) { this._internalDisable.call(this.GL, this.GL.STENCIL_TEST); } - - } - - /** - * [background description] - */ - background(...args) { - const _col = this._pInst.color(...args); - this.clear(..._col._getRGBA()); - } - - ////////////////////////////////////////////// - // Positioning - ////////////////////////////////////////////// - - get uModelMatrix() { - return this.states.uModelMatrix; - } - - get uViewMatrix() { - return this.states.uViewMatrix; - } - - get uPMatrix() { - return this.states.uPMatrix; - } - - get uMVMatrix() { - const m = this.uModelMatrix.copy(); - m.mult(this.uViewMatrix); - return m; - } - - /** - * Get a matrix from world-space to screen-space - */ - getWorldToScreenMatrix() { - const modelMatrix = this.states.uModelMatrix; - const viewMatrix = this.states.uViewMatrix; - const projectionMatrix = this.states.uPMatrix; - const projectedToScreenMatrix = new Matrix(4); - projectedToScreenMatrix.scale(this.width, this.height, 1); - projectedToScreenMatrix.translate([0.5, 0.5, 0.5]); - projectedToScreenMatrix.scale(0.5, -0.5, 0.5); - - const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); - const modelViewProjectionMatrix = modelViewMatrix.mult(projectionMatrix); - const worldToScreenMatrix = modelViewProjectionMatrix.mult(projectedToScreenMatrix); - return worldToScreenMatrix; - } - - ////////////////////////////////////////////// - // COLOR - ////////////////////////////////////////////// - /** - * Basic fill material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @chainable - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } - * - * function draw() { - * background(0); - * noStroke(); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
- * - * @alt - * black canvas with purple cube spinning - */ - fill(...args) { - super.fill(...args); - //see material.js for more info on color blending in webgl - // const color = fn.color.apply(this._pInst, arguments); - const color = this.states.fillColor; - this.states.setValue("curFillColor", color._array); - this.states.setValue("drawMode", constants.FILL); - this.states.setValue("_useNormalMaterial", false); - this.states.setValue("_tex", null); - } - - /** - * Basic stroke material for geometry with a given color - * @param {Number|Number[]|String|p5.Color} v1 gray value, - * red or hue value (depending on the current color mode), - * or color Array, or CSS color string - * @param {Number} [v2] green or saturation value - * @param {Number} [v3] blue or brightness value - * @param {Number} [a] opacity - * @example - *
- * - * function setup() { - * createCanvas(200, 200, WEBGL); - * } - * - * function draw() { - * background(0); - * stroke(240, 150, 150); - * fill(100, 100, 240); - * rotateX(frameCount * 0.01); - * rotateY(frameCount * 0.01); - * box(75, 75, 75); - * } - * - *
- * - * @alt - * black canvas with purple cube with pink outline spinning - */ - stroke(...args) { - super.stroke(...args); - // const color = fn.color.apply(this._pInst, arguments); - this.states.setValue("curStrokeColor", this.states.strokeColor._array); - } - - getCommonVertexProperties() { - return { - ...super.getCommonVertexProperties(), - stroke: this.states.strokeColor, - fill: this.states.fillColor, - normal: this.states._currentNormal, - }; - } - - getSupportedIndividualVertexProperties() { - return { - textureCoordinates: true, - }; - } - - strokeCap(cap) { - this.curStrokeCap = cap; - } - - strokeJoin(join) { - this.curStrokeJoin = join; - } - getFilterLayer() { - if (!this.filterLayer) { - this.filterLayer = new Framebuffer(this); - } - return this.filterLayer; } - getFilterLayerTemp() { - if (!this.filterLayerTemp) { - this.filterLayerTemp = new Framebuffer(this); - } - return this.filterLayerTemp; - } - matchSize(fboToMatch, target) { - if ( - fboToMatch.width !== target.width || - fboToMatch.height !== target.height - ) { - fboToMatch.resize(target.width, target.height); - } - - if (fboToMatch.pixelDensity() !== target.pixelDensity()) { - fboToMatch.pixelDensity(target.pixelDensity()); - } - } - filter(...args) { - let fbo = this.getFilterLayer(); - - // use internal shader for filter constants BLUR, INVERT, etc - let filterParameter = undefined; - let operation = undefined; - if (typeof args[0] === "string") { - operation = args[0]; - let useDefaultParam = - operation in filterParamDefaults && args[1] === undefined; - filterParameter = useDefaultParam - ? filterParamDefaults[operation] - : args[1]; - - // Create and store shader for constants once on initial filter call. - // Need to store multiple in case user calls different filters, - // eg. filter(BLUR) then filter(GRAY) - if (!(operation in this.defaultFilterShaders)) { - this.defaultFilterShaders[operation] = new Shader( - fbo.renderer, - filterShaderVert, - filterShaderFrags[operation] - ); - } - this.states.setValue( - "filterShader", - this.defaultFilterShaders[operation] - ); - } - // use custom user-supplied shader - else { - this.states.setValue("filterShader", args[0]); - } - - // Setting the target to the framebuffer when applying a filter to a framebuffer. - - const target = this.activeFramebuffer() || this; - - // Resize the framebuffer 'fbo' and adjust its pixel density if it doesn't match the target. - this.matchSize(fbo, target); - - fbo.draw(() => this.clear()); // prevent undesirable feedback effects accumulating secretly. - - let texelSize = [ - 1 / (target.width * target.pixelDensity()), - 1 / (target.height * target.pixelDensity()), - ]; - - // apply blur shader with multiple passes. - if (operation === constants.BLUR) { - // Treating 'tmp' as a framebuffer. - const tmp = this.getFilterLayerTemp(); - // Resize the framebuffer 'tmp' and adjust its pixel density if it doesn't match the target. - this.matchSize(tmp, target); - // setup - this.push(); - this.states.setValue("strokeColor", null); - this.blendMode(constants.BLEND); - - // draw main to temp buffer - this.shader(this.states.filterShader); - this.states.filterShader.setUniform("texelSize", texelSize); - this.states.filterShader.setUniform("canvasSize", [ - target.width, - target.height, - ]); - this.states.filterShader.setUniform( - "radius", - Math.max(1, filterParameter) - ); - - // Horiz pass: draw `target` to `tmp` - tmp.draw(() => { - this.states.filterShader.setUniform("direction", [1, 0]); - this.states.filterShader.setUniform("tex0", target); - this.clear(); - this.shader(this.states.filterShader); - this.noLights(); - this.plane(target.width, target.height); - }); - - // Vert pass: draw `tmp` to `fbo` - fbo.draw(() => { - this.states.filterShader.setUniform("direction", [0, 1]); - this.states.filterShader.setUniform("tex0", tmp); - this.clear(); - this.shader(this.states.filterShader); - this.noLights(); - this.plane(target.width, target.height); - }); - - this.pop(); - } - // every other non-blur shader uses single pass - else { - fbo.draw(() => { - this.states.setValue("strokeColor", null); - this.blendMode(constants.BLEND); - this.shader(this.states.filterShader); - this.states.filterShader.setUniform("tex0", target); - this.states.filterShader.setUniform("texelSize", texelSize); - this.states.filterShader.setUniform("canvasSize", [ - target.width, - target.height, - ]); - // filterParameter uniform only used for POSTERIZE, and THRESHOLD - // but shouldn't hurt to always set - this.states.filterShader.setUniform("filterParameter", filterParameter); - this.noLights(); - this.plane(target.width, target.height); - }); - } - // draw fbo contents onto main renderer. - this.push(); - this.states.setValue("strokeColor", null); - this.clear(); - this.push(); - this.states.setValue("imageMode", constants.CORNER); - this.blendMode(constants.BLEND); - target.filterCamera._resize(); - this.setCamera(target.filterCamera); - this.resetMatrix(); - this._drawingFilter = true; - this.image( - fbo, - 0, - 0, - this.width, - this.height, - -target.width / 2, - -target.height / 2, - target.width, - target.height - ); - this._drawingFilter = false; - this.clearDepth(); - this.pop(); - this.pop(); - } - - // Pass this off to the host instance so that we can treat a renderer and a - // framebuffer the same in filter() - - pixelDensity(newDensity) { - if (newDensity) { - return this._pInst.pixelDensity(newDensity); - } - return this._pInst.pixelDensity(); - } - - blendMode(mode) { - if ( - mode === constants.DARKEST || - mode === constants.LIGHTEST || - mode === constants.ADD || - mode === constants.BLEND || - mode === constants.SUBTRACT || - mode === constants.SCREEN || - mode === constants.EXCLUSION || - mode === constants.REPLACE || - mode === constants.MULTIPLY || - mode === constants.REMOVE - ) - this.states.setValue("curBlendMode", mode); - else if ( - mode === constants.BURN || - mode === constants.OVERLAY || - mode === constants.HARD_LIGHT || - mode === constants.SOFT_LIGHT || - mode === constants.DODGE - ) { - console.warn( - "BURN, OVERLAY, HARD_LIGHT, SOFT_LIGHT, and DODGE only work for blendMode in 2D mode." - ); - } - } - - erase(opacityFill, opacityStroke) { - if (!this._isErasing) { - this.preEraseBlend = this.states.curBlendMode; - this._isErasing = true; - this.blendMode(constants.REMOVE); - this._cachedFillStyle = this.states.curFillColor.slice(); - this.states.setValue("curFillColor", [1, 1, 1, opacityFill / 255]); - this._cachedStrokeStyle = this.states.curStrokeColor.slice(); - this.states.setValue("curStrokeColor", [1, 1, 1, opacityStroke / 255]); - } - } - - noErase() { - if (this._isErasing) { - // Restore colors - this.states.setValue("curFillColor", this._cachedFillStyle.slice()); - this.states.setValue("curStrokeColor", this._cachedStrokeStyle.slice()); - // Restore blend mode - this.states.setValue("curBlendMode", this.preEraseBlend); - this.blendMode(this.preEraseBlend); - // Ensure that _applyBlendMode() sets preEraseBlend back to the original blend mode - this._isErasing = false; - this._applyBlendMode(); - } - } - - drawTarget() { - return this.activeFramebuffers[this.activeFramebuffers.length - 1] || this; - } - - beginClip(options = {}) { - super.beginClip(options); - - this.drawTarget()._isClipApplied = true; + _applyClip() { const gl = this.GL; gl.clearStencil(0); gl.clear(gl.STENCIL_BUFFER_BIT); @@ -1447,16 +612,9 @@ class RendererGL extends Renderer { gl.REPLACE // what to do if both tests pass ); gl.disable(gl.DEPTH_TEST); - - this.push(); - this.resetShader(); - if (this.states.fillColor) this.fill(0, 0); - if (this.states.strokeColor) this.stroke(0, 0); } - endClip() { - this.pop(); - + _unapplyClip() { const gl = this.GL; gl.stencilOp( gl.KEEP, // what to do if the stencil test fails @@ -1469,21 +627,11 @@ class RendererGL extends Renderer { 0xff // mask ); gl.enable(gl.DEPTH_TEST); - - // Mark the depth at which the clip has been applied so that we can clear it - // when we pop past this depth - this._clipDepths.push(this._pushPopDepth); - - super.endClip(); } - _clearClip() { + _clearClipBuffer() { this.GL.clearStencil(1); this.GL.clear(this.GL.STENCIL_BUFFER_BIT); - if (this._clipDepths.length > 0) { - this._clipDepths.pop(); - } - this.drawTarget()._isClipApplied = false; } // x,y are canvas-relative (pre-scaled by _pixelDensity) @@ -1558,95 +706,25 @@ class RendererGL extends Renderer { this.GL.clear(this.GL.DEPTH_BUFFER_BIT); } - /** - * @private - * @returns {p5.Framebuffer} A p5.Framebuffer set to match the size and settings - * of the renderer's canvas. It will be created if it does not yet exist, and - * reused if it does. - */ - _getTempFramebuffer() { - if (!this._tempFramebuffer) { - this._tempFramebuffer = new Framebuffer(this, { - format: constants.UNSIGNED_BYTE, - useDepth: this._pInst._glAttributes.depth, - depthFormat: constants.UNSIGNED_INT, - antialias: this._pInst._glAttributes.antialias, - }); - } - return this._tempFramebuffer; - } - ////////////////////////////////////////////// - // HASH | for geometry - ////////////////////////////////////////////// - - geometryInHash(gid) { - return this.geometryBufferCache.isCached(gid); - } viewport(w, h) { this._viewport = [0, 0, w, h]; this.GL.viewport(0, 0, w, h); } - /** - * [resize description] - * @private - * @param {Number} w [description] - * @param {Number} h [description] - */ - resize(w, h) { - super.resize(w, h); - - // save canvas properties - const props = {}; - for (const key in this.drawingContext) { - const val = this.drawingContext[key]; - if (typeof val !== "object" && typeof val !== "function") { - props[key] = val; - } - } - - const dimensions = this._adjustDimensions(w, h); - w = dimensions.adjustedWidth; - h = dimensions.adjustedHeight; - - this.width = w; - this.height = h; - - this.canvas.width = w * this._pixelDensity; - this.canvas.height = h * this._pixelDensity; - this.canvas.style.width = `${w}px`; - this.canvas.style.height = `${h}px`; + _updateViewport() { this._origViewport = { width: this.GL.drawingBufferWidth, height: this.GL.drawingBufferHeight, }; this.viewport(this._origViewport.width, this._origViewport.height); + } - this.states.curCamera._resize(); - - //resize pixels buffer - if (typeof this.pixels !== "undefined") { - this.pixels = new Uint8Array( - this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 - ); - } - - for (const framebuffer of this.framebuffers) { - // Notify framebuffers of the resize so that any auto-sized framebuffers - // can also update their size - framebuffer._canvasSizeChanged(); - } - - // reset canvas properties - for (const savedKey in props) { - try { - this.drawingContext[savedKey] = props[savedKey]; - } catch (err) { - // ignore read-only property errors - } - } + _createPixelsArray() { + this.pixels = new Uint8Array( + this.GL.drawingBufferWidth * this.GL.drawingBufferHeight * 4 + ); } /** @@ -1693,107 +771,6 @@ class RendererGL extends Renderer { this.GL.clear(this.GL.DEPTH_BUFFER_BIT); } - applyMatrix(a, b, c, d, e, f) { - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - if (arguments.length === 16) { - // this.states.uModelMatrix.apply(arguments); - Matrix.prototype.apply.apply(this.states.uModelMatrix, arguments); - } else { - this.states.uModelMatrix.apply([ - a, - b, - 0, - 0, - c, - d, - 0, - 0, - 0, - 0, - 1, - 0, - e, - f, - 0, - 1, - ]); - } - } - - /** - * [translate description] - * @private - * @param {Number} x [description] - * @param {Number} y [description] - * @param {Number} z [description] - * @chainable - * @todo implement handle for components or vector as args - */ - translate(x, y, z) { - if (x instanceof Vector) { - z = x.z; - y = x.y; - x = x.x; - } - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - this.states.uModelMatrix.translate([x, y, z]); - return this; - } - - /** - * Scales the Model View Matrix by a vector - * @private - * @param {Number | p5.Vector | Array} x [description] - * @param {Number} [y] y-axis scalar - * @param {Number} [z] z-axis scalar - * @chainable - */ - scale(x, y, z) { - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - this.states.uModelMatrix.scale(x, y, z); - return this; - } - - rotate(rad, axis) { - if (typeof axis === "undefined") { - return this.rotateZ(rad); - } - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - Matrix.prototype.rotate4x4.apply(this.states.uModelMatrix, arguments); - return this; - } - - rotateX(rad) { - this.rotate(rad, 1, 0, 0); - return this; - } - - rotateY(rad) { - this.rotate(rad, 0, 1, 0); - return this; - } - - rotateZ(rad) { - this.rotate(rad, 0, 0, 1); - return this; - } - - pop(...args) { - if ( - this._clipDepths.length > 0 && - this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] - ) { - this._clearClip(); - if (!this._userEnabledStencil) { - this._internalDisable.call(this.GL, this.GL.STENCIL_TEST); - } - - // Reset saved state - // this._userEnabledStencil = this._savedStencilTestState; - } - super.pop(...args); - this._applyStencilTestIfClipping(); - } _applyStencilTestIfClipping() { const drawTarget = this.drawTarget(); if (drawTarget._isClipApplied !== this._stencilTestOn) { @@ -1808,13 +785,7 @@ class RendererGL extends Renderer { } } } - resetMatrix() { - this.states.setValue("uModelMatrix", this.states.uModelMatrix.clone()); - this.states.uModelMatrix.reset(); - this.states.setValue("uViewMatrix", this.states.uViewMatrix.clone()); - this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); - return this; - } + ////////////////////////////////////////////// // SHADER @@ -1826,15 +797,7 @@ class RendererGL extends Renderer { * and the shader must be valid in that context. */ - _getStrokeShader() { - // select the stroke shader to use - const stroke = this.states.userStrokeShader; - if (stroke) { - return stroke; - } - return this._getLineShader(); - } - + // TODO move to super class _getSphereMapping(img) { if (!this.sphereMapping) { this.sphereMapping = this._pInst.createFilterShader(sphereMapping); @@ -1848,53 +811,13 @@ class RendererGL extends Renderer { return this.sphereMapping; } - /* - * This method will handle both image shaders and - * fill shaders, returning the appropriate shader - * depending on the current context (image or shape). - */ - _getFillShader() { - // If drawing an image, check for user-defined image shader and filters - if (this._drawingImage) { - // Use user-defined image shader if available and no filter is applied - if (this.states.userImageShader && !this._drawingFilter) { - return this.states.userImageShader; - } else { - return this._getLightShader(); // Fallback to light shader - } - } - // If user has defined a fill shader, return that - else if (this.states.userFillShader) { - return this.states.userFillShader; - } - // Use normal shader if normal material is active - else if (this.states._useNormalMaterial) { - return this._getNormalShader(); - } - // Use light shader if lighting or textures are enabled - else if (this.states.enableLighting || this.states._tex) { - return this._getLightShader(); - } - // Default to color shader if no other conditions are met - return this._getColorShader(); - } - - _getPointShader() { - // select the point shader to use - const point = this.states.userPointShader; - if (!point || !point.isPointShader()) { - return this._getPointShader(); - } - return point; - } - baseMaterialShader() { if (!this._pInst._glAttributes.perPixelLighting) { throw new Error( "The material shader does not support hooks without perPixelLighting. Try turning it back on." ); } - return this._getLightShader(); + return super.baseMaterialShader(); } _getLightShader() { @@ -1945,10 +868,6 @@ class RendererGL extends Renderer { return this._defaultLightShader; } - baseNormalShader() { - return this._getNormalShader(); - } - _getNormalShader() { if (!this._defaultNormalShader) { this._defaultNormalShader = new Shader( @@ -1977,10 +896,6 @@ class RendererGL extends Renderer { return this._defaultNormalShader; } - baseColorShader() { - return this._getColorShader(); - } - _getColorShader() { if (!this._defaultColorShader) { this._defaultColorShader = new Shader( @@ -2009,34 +924,6 @@ class RendererGL extends Renderer { return this._defaultColorShader; } - /** - * TODO(dave): un-private this when there is a way to actually override the - * shader used for points - * - * Get the shader used when drawing points with `point()`. - * - * You can call `pointShader().modify()` - * and change any of the following hooks: - * - `void beforeVertex`: Called at the start of the vertex shader. - * - `vec3 getLocalPosition`: Update the position of vertices before transforms are applied. It takes in `vec3 position` and must return a modified version. - * - `vec3 getWorldPosition`: Update the position of vertices after transforms are applied. It takes in `vec3 position` and pust return a modified version. - * - `float getPointSize`: Update the size of the point. It takes in `float size` and must return a modified version. - * - `void afterVertex`: Called at the end of the vertex shader. - * - `void beforeFragment`: Called at the start of the fragment shader. - * - `bool shouldDiscard`: Points are drawn inside a square, with the corners discarded in the fragment shader to create a circle. Use this to change this logic. It takes in a `bool willDiscard` and must return a modified version. - * - `vec4 getFinalColor`: Update the final color after mixing. It takes in a `vec4 color` and must return a modified version. - * - `void afterFragment`: Called at the end of the fragment shader. - * - * Call `pointShader().inspectHooks()` to see all the possible hooks and - * their default implementations. - * - * @returns {p5.Shader} The `point()` shader - * @private() - */ - pointShader() { - return this._getPointShader(); - } - _getPointShader() { if (!this._defaultPointShader) { this._defaultPointShader = new Shader( @@ -2065,10 +952,6 @@ class RendererGL extends Renderer { return this._defaultPointShader; } - baseStrokeShader() { - return this._getLineShader(); - } - _getLineShader() { if (!this._defaultLineShader) { this._defaultLineShader = new Shader( @@ -2186,6 +1069,8 @@ class RendererGL extends Renderer { this.textures.set(src, tex); return tex; } + + // TODO move to super class /* * used in imageLight, * To create a blurry image from the input non blurry img, if it doesn't already exist @@ -2228,6 +1113,7 @@ class RendererGL extends Renderer { return newFramebuffer; } + // TODO move to super class /* * used in imageLight, * To create a texture from the input non blurry image, if it doesn't already exist @@ -2286,210 +1172,6 @@ class RendererGL extends Renderer { return tex; } - /** - * @private - * @returns {p5.Framebuffer|null} The currently active framebuffer, or null if - * the main canvas is the current draw target. - */ - activeFramebuffer() { - return this.activeFramebuffers[this.activeFramebuffers.length - 1] || null; - } - - createFramebuffer(options) { - return new Framebuffer(this, options); - } - - _setGlobalUniforms(shader) { - const modelMatrix = this.states.uModelMatrix; - const viewMatrix = this.states.uViewMatrix; - const projectionMatrix = this.states.uPMatrix; - const modelViewMatrix = modelMatrix.copy().mult(viewMatrix); - - shader.setUniform( - "uPerspective", - this.states.curCamera.useLinePerspective ? 1 : 0 - ); - shader.setUniform("uViewMatrix", viewMatrix.mat4); - shader.setUniform("uProjectionMatrix", projectionMatrix.mat4); - shader.setUniform("uModelMatrix", modelMatrix.mat4); - shader.setUniform("uModelViewMatrix", modelViewMatrix.mat4); - if (shader.uniforms.uModelViewProjectionMatrix) { - const modelViewProjectionMatrix = modelViewMatrix.copy(); - modelViewProjectionMatrix.mult(projectionMatrix); - shader.setUniform( - "uModelViewProjectionMatrix", - modelViewProjectionMatrix.mat4 - ); - } - if (shader.uniforms.uNormalMatrix) { - this.scratchMat3.inverseTranspose4x4(modelViewMatrix); - shader.setUniform("uNormalMatrix", this.scratchMat3.mat3); - } - if (shader.uniforms.uModelNormalMatrix) { - this.scratchMat3.inverseTranspose4x4(this.states.uModelMatrix); - shader.setUniform("uModelNormalMatrix", this.scratchMat3.mat3); - } - if (shader.uniforms.uCameraNormalMatrix) { - this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); - shader.setUniform("uCameraNormalMatrix", this.scratchMat3.mat3); - } - if (shader.uniforms.uCameraRotation) { - this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); - shader.setUniform("uCameraRotation", this.scratchMat3.mat3); - } - shader.setUniform("uViewport", this._viewport); - } - - _setStrokeUniforms(strokeShader) { - // set the uniform values - strokeShader.setUniform("uSimpleLines", this._simpleLines); - strokeShader.setUniform("uUseLineColor", this._useLineColor); - strokeShader.setUniform("uMaterialColor", this.states.curStrokeColor); - strokeShader.setUniform("uStrokeWeight", this.states.strokeWeight); - strokeShader.setUniform("uStrokeCap", STROKE_CAP_ENUM[this.curStrokeCap]); - strokeShader.setUniform( - "uStrokeJoin", - STROKE_JOIN_ENUM[this.curStrokeJoin] - ); - } - - _setFillUniforms(fillShader) { - this.mixedSpecularColor = [...this.states.curSpecularColor]; - const empty = this._getEmptyTexture(); - - if (this.states._useMetalness > 0) { - this.mixedSpecularColor = this.mixedSpecularColor.map( - (mixedSpecularColor, index) => - this.states.curFillColor[index] * this.states._useMetalness + - mixedSpecularColor * (1 - this.states._useMetalness) - ); - } - - // TODO: optimize - fillShader.setUniform("uUseVertexColor", this._useVertexColor); - fillShader.setUniform("uMaterialColor", this.states.curFillColor); - fillShader.setUniform("isTexture", !!this.states._tex); - // We need to explicitly set uSampler back to an empty texture here. - // In general, we record the last set texture so we can re-apply it - // the next time a shader is used. However, the texture() function - // works differently and is global p5 state. If the p5 state has - // been cleared, we also need to clear the value in uSampler to match. - fillShader.setUniform("uSampler", this.states._tex || empty); - fillShader.setUniform("uTint", this.states.tint); - - fillShader.setUniform("uHasSetAmbient", this.states._hasSetAmbient); - fillShader.setUniform("uAmbientMatColor", this.states.curAmbientColor); - fillShader.setUniform("uSpecularMatColor", this.mixedSpecularColor); - fillShader.setUniform("uEmissiveMatColor", this.states.curEmissiveColor); - fillShader.setUniform("uSpecular", this.states._useSpecularMaterial); - fillShader.setUniform("uEmissive", this.states._useEmissiveMaterial); - fillShader.setUniform("uShininess", this.states._useShininess); - fillShader.setUniform("uMetallic", this.states._useMetalness); - - this._setImageLightUniforms(fillShader); - - fillShader.setUniform("uUseLighting", this.states.enableLighting); - - const pointLightCount = this.states.pointLightDiffuseColors.length / 3; - fillShader.setUniform("uPointLightCount", pointLightCount); - fillShader.setUniform( - "uPointLightLocation", - this.states.pointLightPositions - ); - fillShader.setUniform( - "uPointLightDiffuseColors", - this.states.pointLightDiffuseColors - ); - fillShader.setUniform( - "uPointLightSpecularColors", - this.states.pointLightSpecularColors - ); - - const directionalLightCount = - this.states.directionalLightDiffuseColors.length / 3; - fillShader.setUniform("uDirectionalLightCount", directionalLightCount); - fillShader.setUniform( - "uLightingDirection", - this.states.directionalLightDirections - ); - fillShader.setUniform( - "uDirectionalDiffuseColors", - this.states.directionalLightDiffuseColors - ); - fillShader.setUniform( - "uDirectionalSpecularColors", - this.states.directionalLightSpecularColors - ); - - // TODO: sum these here... - const ambientLightCount = this.states.ambientLightColors.length / 3; - this.mixedAmbientLight = [...this.states.ambientLightColors]; - - if (this.states._useMetalness > 0) { - this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors) => { - let mixing = ambientColors - this.states._useMetalness; - return Math.max(0, mixing); - }); - } - fillShader.setUniform("uAmbientLightCount", ambientLightCount); - fillShader.setUniform("uAmbientColor", this.mixedAmbientLight); - - const spotLightCount = this.states.spotLightDiffuseColors.length / 3; - fillShader.setUniform("uSpotLightCount", spotLightCount); - fillShader.setUniform("uSpotLightAngle", this.states.spotLightAngle); - fillShader.setUniform("uSpotLightConc", this.states.spotLightConc); - fillShader.setUniform( - "uSpotLightDiffuseColors", - this.states.spotLightDiffuseColors - ); - fillShader.setUniform( - "uSpotLightSpecularColors", - this.states.spotLightSpecularColors - ); - fillShader.setUniform("uSpotLightLocation", this.states.spotLightPositions); - fillShader.setUniform( - "uSpotLightDirection", - this.states.spotLightDirections - ); - - fillShader.setUniform( - "uConstantAttenuation", - this.states.constantAttenuation - ); - fillShader.setUniform("uLinearAttenuation", this.states.linearAttenuation); - fillShader.setUniform( - "uQuadraticAttenuation", - this.states.quadraticAttenuation - ); - } - - // getting called from _setFillUniforms - _setImageLightUniforms(shader) { - //set uniform values - shader.setUniform("uUseImageLight", this.states.activeImageLight != null); - // true - if (this.states.activeImageLight) { - // this.states.activeImageLight has image as a key - // look up the texture from the diffusedTexture map - let diffusedLight = this.getDiffusedTexture(this.states.activeImageLight); - shader.setUniform("environmentMapDiffused", diffusedLight); - let specularLight = this.getSpecularTexture(this.states.activeImageLight); - - shader.setUniform("environmentMapSpecular", specularLight); - } - } - - _setPointUniforms(pointShader) { - // set the uniform values - pointShader.setUniform("uMaterialColor", this.states.curStrokeColor); - // @todo is there an instance where this isn't stroke weight? - // should be they be same var? - pointShader.setUniform( - "uPointSize", - this.states.strokeWeight * this._pixelDensity - ); - } - /* Binds a buffer to the drawing context * when passed more than two arguments it also updates or initializes * the data associated with the buffer @@ -2508,6 +1190,14 @@ class RendererGL extends Renderer { } } + _makeFilterShader(renderer, operation) { + return new Shader( + renderer, + filterShaderVert, + filterShaderFrags[operation] + ); + } + /////////////////////////////// //// UTILITY FUNCTIONS ////////////////////////////// @@ -2614,7 +1304,7 @@ function rendererGL(p5, fn) { * } * * - * + * *
* * // Now with the antialias attribute set to true. diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js new file mode 100644 index 0000000000..268e597af3 --- /dev/null +++ b/src/webgpu/p5.RendererWebGPU.js @@ -0,0 +1,27 @@ +import { Renderer3D } from '../core/p5.Renderer3D'; + +class RendererWebGPU extends Renderer3D { + constructor(pInst, w, h, isMainCanvas, elt) { + super(pInst, w, h, isMainCanvas, elt) + } + + setupContext() { + // TODO + } + + _resetBuffersBeforeDraw() { + // TODO + } + + ////////////////////////////////////////////// + // Setting + ////////////////////////////////////////////// + _adjustDimensions(width, height) { + // TODO: find max texture size + return { adjustedWidth: width, adjustedHeight: height }; + } + + _applyStencilTestIfClipping() { + // TODO + } +} From 3af1624fb8a7c6dabec6e69e0da9d55036a233a7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 23 May 2025 19:01:27 -0400 Subject: [PATCH 02/98] Get background() working --- preview/index.html | 22 ++--- src/core/constants.js | 7 ++ src/core/p5.Renderer3D.js | 55 ++++++------ src/core/rendering.js | 6 +- src/webgl/3d_primitives.js | 44 ++++----- src/webgl/GeometryBufferCache.js | 61 +++---------- src/webgl/light.js | 20 ++--- src/webgl/material.js | 30 +++---- src/webgl/p5.Camera.js | 18 ++-- src/webgl/p5.Framebuffer.js | 2 +- src/webgl/p5.RendererGL.js | 148 ++++++++++--------------------- src/webgl/text.js | 8 +- src/webgl/utils.js | 99 +++++++++++++++++++++ src/webgpu/p5.RendererWebGPU.js | 125 +++++++++++++++++++++++++- 14 files changed, 390 insertions(+), 255 deletions(-) create mode 100644 src/webgl/utils.js diff --git a/preview/index.html b/preview/index.html index d0f3b329ae..2cc9391628 100644 --- a/preview/index.html +++ b/preview/index.html @@ -18,29 +18,21 @@ - \ No newline at end of file + diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index fb26a639c8..a2e40d83c5 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -557,13 +557,12 @@ export class Renderer3D extends Renderer { geometry.hasFillTransparency() ); - this._drawBuffers(geometry, { mode, count }); + this._drawBuffers(geometry, { mode, count }, false); shader.unbindShader(); } _drawStrokes(geometry, { count } = {}) { - const gl = this.GL; this._useLineColor = geometry.vertexStrokeColors.length > 0; @@ -584,22 +583,7 @@ export class Renderer3D extends Renderer { geometry.hasStrokeTransparency() ); - if (count === 1) { - gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); - } else { - try { - gl.drawArraysInstanced( - gl.TRIANGLES, - 0, - geometry.lineVertices.length / 3, - count - ); - } catch (e) { - console.log( - "🌸 p5.js says: Instancing is only supported in WebGL2 mode" - ); - } - } + this._drawBuffers(geometry, {count}, true) shader.unbindShader(); } @@ -1430,7 +1414,7 @@ export class Renderer3D extends Renderer { this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); shader.setUniform("uCameraRotation", this.scratchMat3.mat3); } - shader.setUniform("uViewport", this._viewport); + shader.setUniform("uViewport", [0, 0, 400, 400]); } _setStrokeUniforms(strokeShader) { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 7ef7e61bf7..e033e3b1c2 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -270,9 +270,33 @@ class RendererGL extends Renderer3D { } } + // Stroke version for now: + // +// { +// const gl = this.GL; +// // move this to _drawBuffers ? +// if (count === 1) { +// gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); +// } else { +// try { + // gl.drawArraysInstanced( + // gl.TRIANGLES, + // 0, + // geometry.lineVertices.length / 3, + // count + // ); + // } catch (e) { + // console.log( + // "🌸 p5.js says: Instancing is only supported in WebGL2 mode" + // ); + // } + // } + // } + _drawBuffers(geometry, { mode = constants.TRIANGLES, count }) { const gl = this.GL; const glBuffers = this.geometryBufferCache.getCached(geometry); + //console.log(glBuffers); if (!glBuffers) return; @@ -1157,7 +1181,7 @@ class RendererGL extends Renderer3D { if (indices) { const buffer = gl.createBuffer(); - this.renderer._bindBuffer(buffer, gl.ELEMENT_ARRAY_BUFFER, indices, indexType); + this._bindBuffer(buffer, gl.ELEMENT_ARRAY_BUFFER, indices, indexType); buffers.indexBuffer = buffer; @@ -1478,9 +1502,9 @@ class RendererGL extends Renderer3D { createTexture({ width, height, format, dataType }) { const gl = this.GL; const tex = gl.createTexture(); - this.gl.bindTexture(gl.TEXTURE_2D, tex); - this.gl.texImage2D(gl.TEXTURE_2D, 0, this.gl.RGBA, width, height, 0, - gl.RGBA, this.gl.UNSIGNED_BYTE, null); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); // TODO use format and data type return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE }; } diff --git a/src/webgl/shaders/line.vert b/src/webgl/shaders/line.vert index de422ad6b6..a00bf94ba8 100644 --- a/src/webgl/shaders/line.vert +++ b/src/webgl/shaders/line.vert @@ -271,6 +271,7 @@ void main() { } } else { vec2 tangent = aTangentIn == vec3(0.) ? tangentOut : tangentIn; + vTangent = tangent; vec2 normal = vec2(-tangent.y, tangent.x); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a58720d66f..ffb012e1f1 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -2,6 +2,7 @@ import { Renderer3D } from '../core/p5.Renderer3D'; import { Shader } from '../webgl/p5.Shader'; import * as constants from '../core/constants'; import { colorVertexShader, colorFragmentShader } from './shaders/color'; +import { lineVertexShader, lineFragmentShader} from './shaders/line'; class RendererWebGPU extends Renderer3D { constructor(pInst, w, h, isMainCanvas, elt) { @@ -248,7 +249,6 @@ class RendererWebGPU extends Renderer3D { .filter(u => !u.isSampler) .reduce((sum, u) => sum + u.alignedBytes, 0); shader._uniformData = new Float32Array(uniformSize / 4); - shader._uniformBuffer = this.device.createBuffer({ size: uniformSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, @@ -453,7 +453,6 @@ class RendererWebGPU extends Renderer3D { 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) || @@ -477,7 +476,6 @@ class RendererWebGPU extends Renderer3D { ], }); } - return layouts; } @@ -512,7 +510,9 @@ class RendererWebGPU extends Renderer3D { _useShader(shader, options) {} - _updateViewport() {} + _updateViewport() { + this._viewport = [0, 0, this.width, this.height]; + } zClipRange() { return [0, 1]; @@ -548,13 +548,14 @@ class RendererWebGPU extends Renderer3D { // Rendering ////////////////////////////////////////////// - _drawBuffers(geometry, { mode = constants.TRIANGLES, count = 1 }) { + _drawBuffers(geometry, { mode = constants.TRIANGLES, count = 1 }, stroke) { const buffers = this.geometryBufferCache.getCached(geometry); if (!buffers) return; const commandEncoder = this.device.createCommandEncoder(); + const currentTexture = this.drawingContext.getCurrentTexture(); const colorAttachment = { - view: this.drawingContext.getCurrentTexture().createView(), + view: currentTexture.createView(), loadOp: "load", storeOp: "store", }; @@ -578,7 +579,6 @@ class RendererWebGPU extends Renderer3D { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(this._curShader.getPipeline(this._shaderOptions({ mode }))); - // Bind vertex buffers for (const buffer of this._getVertexBuffers(this._curShader)) { passEncoder.setVertexBuffer( @@ -587,9 +587,9 @@ class RendererWebGPU extends Renderer3D { 0 ); } - // Bind uniforms this._packUniforms(this._curShader); + console.log(this._curShader); this.device.queue.writeBuffer( this._curShader._uniformBuffer, 0, @@ -621,18 +621,21 @@ class RendererWebGPU extends Renderer3D { layout, entries: bgEntries, }); - passEncoder.setBindGroup(group, bindGroup); } + if (buffers.lineVerticesBuffer && geometry.lineVertices && stroke) { + passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0); + } // Bind index buffer and issue draw + if (!stroke) { 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); - } + }} passEncoder.end(); this.queue.submit([commandEncoder.finish()]); @@ -644,6 +647,7 @@ class RendererWebGPU extends Renderer3D { _packUniforms(shader) { let offset = 0; + let i = 0; for (const name in shader.uniforms) { const uniform = shader.uniforms[name]; if (uniform.isSampler) continue; @@ -661,7 +665,7 @@ class RendererWebGPU extends Renderer3D { new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`) ); if (!structMatch) { - throw new Error(`Can't find a struct definition for ${structName}`); + throw new Error(`Can't find a struct defnition for ${structName}`); } const structBody = structMatch[1]; @@ -716,7 +720,6 @@ class RendererWebGPU extends Renderer3D { } const structType = uniformVarMatch[2]; const uniforms = this._parseStruct(shader.vertSrc(), structType); - // Extract samplers from group bindings const samplers = []; const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(\w+);/g; @@ -835,6 +838,28 @@ class RendererWebGPU extends Renderer3D { return this._defaultColorShader; } + _getLineShader() { + if (!this._defaultLineShader) { + this._defaultLineShader = new Shader( + this, + lineVertexShader, + lineFragmentShader, + { + vertex: { + "void beforeVertex": "() {}", + "Vertex getObjectInputs": "(inputs: Vertex) { return inputs; }", + "Vertex getWorldInputs": "(inputs: Vertex) { return inputs; }", + "Vertex getCameraInputs": "(inputs: Vertex) { return inputs; }", + }, + fragment: { + "vec4 getFinalColor": "(color: vec4) { return color; }" + }, + } + ); + } + return this._defaultLineShader; + } + ////////////////////////////////////////////// // Setting ////////////////////////////////////////////// @@ -921,7 +946,7 @@ class RendererWebGPU extends Renderer3D { } } - console.log(preMain + '\n' + defines + hooks + main + postMain) + //console.log(preMain + '\n' + defines + hooks + main + postMain) return preMain + '\n' + defines + hooks + main + postMain; } } diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js new file mode 100644 index 0000000000..a42e7b178e --- /dev/null +++ b/src/webgpu/shaders/line.js @@ -0,0 +1,317 @@ +import { getTexture } from './utils' + +const uniforms = ` +struct Uniforms { +// @p5 ifdef Vertex getWorldInputs + uModelMatrix: mat4x4, + uViewMatrix: mat4x4, +// @p5 endif +// @p5 ifndef Vertex getWorldInputs + uModelViewMatrix: mat4x4, +// @p5 endif + uMaterialColor: vec4, + uProjectionMatrix: mat4x4, + uStrokeWeight: f32, + uUseLineColor: f32, + uSimpleLines: f32, + uViewport: vec4, + uPerspective: i32, + uStrokeJoin: i32, +} +`; + +export const lineVertexShader = ` +struct StrokeVertexInput { + @location(0) aPosition: vec3, + @location(1) aSide: f32, + @location(2) aTangentIn: vec3, + @location(3) aTangentOut: vec3, + @location(4) aVertexColor: vec4, +}; + +struct StrokeVertexOutput { + @builtin(position) Position: vec4, + @location(0) vColor: vec4, + @location(1) vTangent: vec2, + @location(2) vCenter: vec2, + @location(3) vPosition: vec2, + @location(4) vMaxDist: f32, + @location(5) vCap: f32, + @location(6) vJoin: f32, + @location(7) vStrokeWeight: f32, +}; + +${uniforms} +@group(0) @binding(0) var uniforms: Uniforms; + +struct Vertex { + position: vec3, + tangentIn: vec3, + tangentOut: vec3, + color: vec4, + weight: f32, +} + +fn lineIntersection(aPoint: vec2f, aDir: vec2f, bPoint: vec2f, bDir: vec2f) -> vec2f { + // Rotate and translate so a starts at the origin and goes out to the right + var bMutPoint = bPoint; + bMutPoint -= aPoint; + var rotatedBFrom = vec2( + bMutPoint.x*aDir.x + bMutPoint.y*aDir.y, + bMutPoint.y*aDir.x - bMutPoint.x*aDir.y + ); + var bTo = bMutPoint + bDir; + var rotatedBTo = vec2( + bTo.x*aDir.x + bTo.y*aDir.y, + bTo.y*aDir.x - bTo.x*aDir.y + ); + var intersectionDistance = + rotatedBTo.x + (rotatedBFrom.x - rotatedBTo.x) * rotatedBTo.y / + (rotatedBTo.y - rotatedBFrom.y); + return aPoint + aDir * intersectionDistance; +} + +@vertex +fn main(input: StrokeVertexInput) -> StrokeVertexOutput { + HOOK_beforeVertex(); + var output: StrokeVertexOutput; + let viewport = vec4(0.,0.,400.,400.); + let simpleLines = (uniforms.uSimpleLines != 0.); + if (!simpleLines) { + if (all(input.aTangentIn == vec3()) != all(input.aTangentOut == vec3())) { + output.vCap = 1.; + } else { + output.vCap = 0.; + } + let conditionA = any(input.aTangentIn != vec3()); + let conditionB = any(input.aTangentOut != vec3()); + let conditionC = any(input.aTangentIn != input.aTangentOut); + if (conditionA && conditionB && conditionC) { + output.vJoin = 1.; + } else { + output.vJoin = 0.; + } + } + var lineColor: vec4; + if (uniforms.uUseLineColor != 0.) { + lineColor = input.aVertexColor; + } else { + lineColor = uniforms.uMaterialColor; + } + var inputs = Vertex( + input.aPosition.xyz, + input.aTangentIn, + input.aTangentOut, + lineColor, + uniforms.uStrokeWeight + ); + +// @p5 ifdef Vertex getObjectInputs + inputs = HOOK_getObjectInputs(inputs); +// @p5 endif + +// @p5 ifdef Vertex getWorldInputs + inputs.position = (uModelMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; + inputs.tangentOut = (uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; +// @p5 endif + +// @p5 ifdef Vertex getWorldInputs + // Already multiplied by the model matrix, just apply view + inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (uniforms.uViewMatrix * vec4(input.aTangentIn, 0.)).xyz; + inputs.tangentOut = (uniforms.uViewMatrix * vec4(input.aTangentOut, 0.)).xyz; +// @p5 endif +// @p5 ifndef Vertex getWorldInputs + // Apply both at once + inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (uniforms.uModelViewMatrix * vec4(input.aTangentIn, 0.)).xyz; + inputs.tangentOut = (uniforms.uModelViewMatrix * vec4(input.aTangentOut, 0.)).xyz; +// @p5 endif +// @p5 ifdef Vertex getCameraInputs + inputs = HOOK_getCameraInputs(inputs); +// @p5 endif + + var posp = vec4(inputs.position, 1.); + var posqIn = vec4(inputs.position + inputs.tangentIn, 1.); + var posqOut = vec4(inputs.position + inputs.tangentOut, 1.); + output.vStrokeWeight = inputs.weight; + + var facingCamera = pow( + // The word space tangent's z value is 0 if it's facing the camera + abs(normalize(posqIn-posp).z), + + // Using pow() here to ramp 'facingCamera' up from 0 to 1 really quickly + // so most lines get scaled and don't get clipped + 0.25 + ); + + // Moving vertices slightly toward the camera + // to avoid depth-fighting with the fill triangles. + // A mix of scaling and offsetting is used based on distance + // Discussion here: + // https://github.com/processing/p5.js/issues/7200 + + // using a scale <1 moves the lines towards nearby camera + // in order to prevent popping effects due to half of + // the line disappearing behind the geometry faces. + var zDistance = -posp.z; + var distanceFactor = smoothstep(0., 800., zDistance); + + // Discussed here: + // http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=252848 + var scale = mix(1., 0.995, facingCamera); + var dynamicScale = mix(scale, 1.0, distanceFactor); // Closer = more scale, farther = less + + posp = vec4(posp.xyz * dynamicScale, posp.w); + posqIn = vec4(posqIn.xyz * dynamicScale, posqIn.w); + posqOut= vec4(posqOut.xyz * dynamicScale, posqOut.w); + + // Moving vertices slightly toward camera when far away + // https://github.com/processing/p5.js/issues/6956 + var zOffset = mix(0., -1., facingCamera); + var dynamicZAdjustment = mix(0., zOffset, distanceFactor); // Closer = less zAdjustment, farther = more + + posp.z -= dynamicZAdjustment; + posqIn.z -= dynamicZAdjustment; + posqOut.z -= dynamicZAdjustment; + + var p = uniforms.uProjectionMatrix * posp; + var qIn = uniforms.uProjectionMatrix * posqIn; + var qOut = uniforms.uProjectionMatrix * posqOut; + + var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * viewport.zw); + var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * viewport.zw); + + var curPerspScale = vec2(); + if (uniforms.uPerspective == 1) { + // Perspective --- + // convert from world to clip by multiplying with projection scaling factor + // to get the right thickness (see https://github.com/processing/processing/issues/5182) + + // The y value of the projection matrix may be flipped if rendering to a Framebuffer. + // Multiplying again by its sign here negates the flip to get just the scale. + curPerspScale = (uniforms.uProjectionMatrix * vec4(1., sign(uniforms.uProjectionMatrix[1][1]), 0., 0.)).xy; + } else { + // No Perspective --- + // multiply by W (to cancel out division by W later in the pipeline) and + // convert from screen to clip (derived from clip to screen above) + curPerspScale = p.w / (0.5 * viewport.zw); + } + + var offset = vec2(); + if (output.vJoin == 1. && !simpleLines) { + output.vTangent = normalize(tangentIn + tangentOut); + var normalIn = vec2(-tangentIn.y, tangentIn.x); + var normalOut = vec2(-tangentOut.y, tangentOut.x); + var side = sign(input.aSide); + var sideEnum = abs(input.aSide); + + // We generate vertices for joins on either side of the centerline, but + // the "elbow" side is the only one needing a join. By not setting the + // offset for the other side, all its vertices will end up in the same + // spot and not render, effectively discarding it. + if (sign(dot(tangentOut, vec2(-tangentIn.y, tangentIn.x))) != side) { + // Side enums: + // 1: the side going into the join + // 2: the middle of the join + // 3: the side going out of the join + if (sideEnum == 2.) { + // Calculate the position + tangent on either side of the join, and + // find where the lines intersect to find the elbow of the join + var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * viewport.zw; + + var intersection = lineIntersection( + c + (side * normalIn * inputs.weight / 2.), + tangentIn, + c + (side * normalOut * inputs.weight / 2.), + tangentOut + ); + offset = intersection - c; + + + // When lines are thick and the angle of the join approaches 180, the + // elbow might be really far from the center. We'll apply a limit to + // the magnitude to avoid lines going across the whole screen when this + // happens. + var mag = length(offset); + var maxMag = 3 * inputs.weight; + if (mag > maxMag) { + offset = vec2(maxMag / mag); + } else if (sideEnum == 1.) { + offset = side * normalIn * inputs.weight / 2.; + } else if (sideEnum == 3.) { + offset = side * normalOut * inputs.weight / 2.; + } + } + } + if (uniforms.uStrokeJoin == 2) { + var avgNormal = vec2(-output.vTangent.y, output.vTangent.x); + output.vMaxDist = abs(dot(avgNormal, normalIn * inputs.weight / 2.)); + } else { + output.vMaxDist = inputs.weight / 2.; + } + } else { + var tangent: vec2; + if (all(input.aTangentIn == vec3())) { + tangent = tangentOut; + } else { + tangent = tangentIn; + } + output.vTangent = tangent; + var normal = vec2(-tangent.y, tangent.y); + + var normalOffset = sign(input.aSide); + // Caps will have side values of -2 or 2 on the edge of the cap that + // extends out from the line + var tangentOffset = abs(input.aSide) - 1.; + offset = (normal * normalOffset + tangent * tangentOffset) * + inputs.weight * 0.5; + output.vMaxDist = inputs.weight / 2.; + } + output.vCenter = p.xy; + output.vPosition = output.vCenter + offset; + output.vColor = inputs.color; + + output.Position = vec4( + p.xy + offset.xy * curPerspScale, + p.zy + ); + var clip_pos: vec4; + if (input.aSide == 1.0) { + clip_pos = vec4(-0.1, 0.1, 0.5, 1.); + } else if (input.aSide == -1.0) { + clip_pos = vec4(-0.5, 0.5, 0.5, 1.0); + } else { + clip_pos = vec4(0.0, -0.5, 0.5 ,1.0); + } + output.Position = clip_pos; + return output; +} + + +`; + +export const lineFragmentShader = ` +struct StrokeFragmentInput { + @location(0) vColor: vec4, + @location(1) vTangent: vec2, + @location(2) vCenter: vec2, + @location(3) vPosition: vec2, + @location(4) vMaxDist: f32, + @location(5) vCap: f32, + @location(6) vJoin: f32, + @location(7) vStrokeWeight: f32, +} + +${uniforms} +@group(0) @binding(0) var uniforms: Uniforms; + +${getTexture} + +@fragment +fn main(input: StrokeFragmentInput) -> @location(0) vec4 { + return vec4(1., 1., 1., 1.); +} +`; + From ae2c56685161418ecdfdc95478aef66cf4bd5efd Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 15 Jun 2025 16:40:17 -0400 Subject: [PATCH 11/98] Add material shader --- preview/index.html | 7 +- src/core/p5.Renderer3D.js | 23 ++- src/webgl/light.js | 16 +- src/webgl/p5.Shader.js | 7 +- src/webgl/shaders/basic.frag | 3 +- src/webgl/shaders/lighting.glsl | 2 - src/webgl/shaders/phong.frag | 5 +- src/webgl/shaders/phong.vert | 11 - src/webgpu/p5.RendererWebGPU.js | 160 ++++++++++++--- src/webgpu/shaders/color.js | 9 +- src/webgpu/shaders/material.js | 348 ++++++++++++++++++++++++++++++++ 11 files changed, 528 insertions(+), 63 deletions(-) create mode 100644 src/webgpu/shaders/material.js diff --git a/preview/index.html b/preview/index.html index 01a65cd11d..26a9c7196b 100644 --- a/preview/index.html +++ b/preview/index.html @@ -27,7 +27,7 @@ let sh; p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); - sh = p.baseColorShader().modify({ + sh = p.baseMaterialShader().modify({ uniforms: { 'f32 time': () => p.millis(), }, @@ -44,6 +44,11 @@ p.background(200); p.noStroke(); p.shader(sh); + p.ambientLight(50); + p.directionalLight(100, 100, 100, 0, 1, -1); + p.pointLight(155, 155, 155, 0, -200, 500); + p.specularMaterial(255); + p.shininess(300); for (const [i, c] of ['red', 'lime', 'blue'].entries()) { p.push(); p.fill(c); diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index fb26a639c8..c8ed32ea8a 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1515,17 +1515,20 @@ export class Renderer3D extends Renderer { ); // TODO: sum these here... - const ambientLightCount = this.states.ambientLightColors.length / 3; - this.mixedAmbientLight = [...this.states.ambientLightColors]; - - if (this.states._useMetalness > 0) { - this.mixedAmbientLight = this.mixedAmbientLight.map((ambientColors) => { - let mixing = ambientColors - this.states._useMetalness; - return Math.max(0, mixing); - }); + let mixedAmbientLight = [0, 0, 0]; + for (let i = 0; i < this.states.ambientLightColors.length; i += 3) { + for (let off = 0; off < 3; off++) { + if (this.states._useMetalness > 0) { + mixedAmbientLight[off] += Math.max( + 0, + this.states.ambientLightColors[i + off] - this.states._useMetalness + ); + } else { + mixedAmbientLight[off] += this.states.ambientLightColors[i + off]; + } + } } - fillShader.setUniform("uAmbientLightCount", ambientLightCount); - fillShader.setUniform("uAmbientColor", this.mixedAmbientLight); + fillShader.setUniform("uAmbientColor", mixedAmbientLight); const spotLightCount = this.states.spotLightDiffuseColors.length / 3; fillShader.setUniform("uSpotLightCount", spotLightCount); diff --git a/src/webgl/light.js b/src/webgl/light.js index 1c714ae02a..e57dd09686 100644 --- a/src/webgl/light.js +++ b/src/webgl/light.js @@ -1620,6 +1620,8 @@ function light(p5, fn){ angle, concentration ) { + if (this.states.spotLightDiffuseColors.length / 3 >= 4) return; + let color, position, direction; const length = arguments.length; @@ -1777,18 +1779,26 @@ function light(p5, fn){ return; } this.states.setValue('spotLightDiffuseColors', [ + ...this.states.spotLightDiffuseColors, color._array[0], color._array[1], color._array[2] ]); this.states.setValue('spotLightSpecularColors', [ + ...this.states.spotLightSpecularColors, ...this.states.specularColors ]); - this.states.setValue('spotLightPositions', [position.x, position.y, position.z]); + this.states.setValue('spotLightPositions', [ + ...this.states.spotLightPositions, + position.x, + position.y, + position.z + ]); direction.normalize(); this.states.setValue('spotLightDirections', [ + ...this.states.spotLightDirections, direction.x, direction.y, direction.z @@ -1808,8 +1818,8 @@ function light(p5, fn){ } angle = this._pInst._toRadians(angle); - this.states.setValue('spotLightAngle', [Math.cos(angle)]); - this.states.setValue('spotLightConc', [concentration]); + this.states.setValue('spotLightAngle', [...this.states.spotLightAngle, Math.cos(angle)]); + this.states.setValue('spotLightConc', [...this.states.spotLightConc, concentration]); this.states.setValue('enableLighting', true); } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 3f4015311c..fc3745e394 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -976,14 +976,17 @@ class Shader { * *
*/ - setUniform(uniformName, data) { + setUniform(uniformName, rawData) { this.init(); const uniform = this.uniforms[uniformName]; if (!uniform) { return; } - const gl = this._renderer.GL; + + const data = this._renderer._mapUniformData + ? this._renderer._mapUniformData(uniform, rawData) + : rawData; if (uniform.isArray) { if ( diff --git a/src/webgl/shaders/basic.frag b/src/webgl/shaders/basic.frag index e583955d36..1406964ca9 100644 --- a/src/webgl/shaders/basic.frag +++ b/src/webgl/shaders/basic.frag @@ -1,6 +1,7 @@ IN vec4 vColor; void main(void) { HOOK_beforeFragment(); - OUT_COLOR = HOOK_getFinalColor(vec4(vColor.rgb, 1.) * vColor.a); + OUT_COLOR = HOOK_getFinalColor(vColor); + OUT_COLOR.rgb *= OUT_COLOR.a; // Premultiply alpha before rendering HOOK_afterFragment(); } diff --git a/src/webgl/shaders/lighting.glsl b/src/webgl/shaders/lighting.glsl index b66ac083d1..85a4c79684 100644 --- a/src/webgl/shaders/lighting.glsl +++ b/src/webgl/shaders/lighting.glsl @@ -7,8 +7,6 @@ uniform mat4 uViewMatrix; uniform bool uUseLighting; -uniform int uAmbientLightCount; -uniform vec3 uAmbientColor[5]; uniform mat3 uCameraRotation; uniform int uDirectionalLightCount; uniform vec3 uLightingDirection[5]; diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index a424c6220c..78cfb76163 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -2,6 +2,7 @@ precision highp int; uniform bool uHasSetAmbient; +uniform vec3 uAmbientColor; uniform vec4 uSpecularMatColor; uniform vec4 uAmbientMatColor; uniform vec4 uEmissiveMatColor; @@ -13,7 +14,6 @@ uniform bool isTexture; IN vec3 vNormal; IN vec2 vTexCoord; IN vec3 vViewPosition; -IN vec3 vAmbientColor; IN vec4 vColor; struct ColorComponents { @@ -45,7 +45,7 @@ void main(void) { Inputs inputs; inputs.normal = normalize(vNormal); inputs.texCoord = vTexCoord; - inputs.ambientLight = vAmbientColor; + inputs.ambientLight = uAmbientColor; inputs.color = isTexture ? TEXTURE(uSampler, vTexCoord) * (vec4(uTint.rgb/255., 1.) * uTint.a/255.) : vColor; @@ -67,7 +67,6 @@ void main(void) { // Calculating final color as result of all lights (plus emissive term). - vec2 texCoord = inputs.texCoord; vec4 baseColor = inputs.color; ColorComponents c; c.opacity = baseColor.a; diff --git a/src/webgl/shaders/phong.vert b/src/webgl/shaders/phong.vert index 670da028c1..49a10933fc 100644 --- a/src/webgl/shaders/phong.vert +++ b/src/webgl/shaders/phong.vert @@ -7,8 +7,6 @@ IN vec3 aNormal; IN vec2 aTexCoord; IN vec4 aVertexColor; -uniform vec3 uAmbientColor[5]; - #ifdef AUGMENTED_HOOK_getWorldInputs uniform mat4 uModelMatrix; uniform mat4 uViewMatrix; @@ -19,7 +17,6 @@ uniform mat4 uModelViewMatrix; uniform mat3 uNormalMatrix; #endif uniform mat4 uProjectionMatrix; -uniform int uAmbientLightCount; uniform bool uUseVertexColor; uniform vec4 uMaterialColor; @@ -74,14 +71,6 @@ void main(void) { vNormal = inputs.normal; vColor = inputs.color; - // TODO: this should be a uniform - vAmbientColor = vec3(0.0); - for (int i = 0; i < 5; i++) { - if (i < uAmbientLightCount) { - vAmbientColor += uAmbientColor[i]; - } - } - gl_Position = uProjectionMatrix * vec4(inputs.position, 1.); HOOK_afterVertex(); } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a58720d66f..a2bcaca885 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -2,6 +2,7 @@ import { Renderer3D } from '../core/p5.Renderer3D'; import { Shader } from '../webgl/p5.Shader'; import * as constants from '../core/constants'; import { colorVertexShader, colorFragmentShader } from './shaders/color'; +import { materialVertexShader, materialFragmentShader } from './shaders/material'; class RendererWebGPU extends Renderer3D { constructor(pInst, w, h, isMainCanvas, elt) { @@ -244,13 +245,16 @@ class RendererWebGPU extends Renderer3D { } _finalizeShader(shader) { - const uniformSize = Object.values(shader.uniforms) - .filter(u => !u.isSampler) - .reduce((sum, u) => sum + u.alignedBytes, 0); - shader._uniformData = new Float32Array(uniformSize / 4); + const rawSize = Math.max( + 0, + ...Object.values(shader.uniforms).map(u => u.offsetEnd) + ); + const alignedSize = Math.ceil(rawSize / 16) * 16; + shader._uniformData = new Float32Array(alignedSize / 4); + shader._uniformDataView = new DataView(shader._uniformData.buffer); shader._uniformBuffer = this.device.createBuffer({ - size: uniformSize, + size: alignedSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); @@ -643,16 +647,16 @@ class RendererWebGPU extends Renderer3D { ////////////////////////////////////////////// _packUniforms(shader) { - let offset = 0; for (const name in shader.uniforms) { const uniform = shader.uniforms[name]; if (uniform.isSampler) continue; - if (uniform.size === 1) { - shader._uniformData.set([uniform._cachedData], offset); + if (uniform.type === 'u32') { + shader._uniformDataView.setUint32(uniform.offset, uniform._cachedData, true); + } else if (uniform.size === 4) { + shader._uniformData.set([uniform._cachedData], uniform.offset / 4); } else { - shader._uniformData.set(uniform._cachedData, offset); + shader._uniformData.set(uniform._cachedData, uniform.offset / 4); } - offset += uniform.alignedBytes / 4; } } @@ -668,33 +672,101 @@ class RendererWebGPU extends Renderer3D { const elements = {}; let match; let index = 0; + let offset = 0; const elementRegex = - /(?:@location\((\d+)\)\s+)?(\w+):\s+((?:mat[234]x[234]|vec[234]|float|int|uint|bool|f32|i32|u32|bool)(?:)?)/g + /(?:@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 }; + } + if (/^vec[2-4](|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 }; + } + if (/^mat[2-4](?:x[2-4])?(|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; + const pack = dim === 3 + ? (data) => [ + ...data.slice(0, 3), + 0, + ...data.slice(3, 6), + 0, + ...data.slice(6, 9), + 0 + ] + : undefined; + return { align, size, pack, items: dim * dim }; + } + 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] + } = 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, + }; + } + throw new Error(`Unknown type in WGSL struct: ${type}`); + }; + while ((match = elementRegex.exec(structBody)) !== null) { const [_, location, name, type] = match; - const size = type.startsWith('vec') - ? parseInt(type[3]) - : type.startsWith('mat') - ? Math.pow(parseInt(type[3]), 2) - : 1; - const bytes = 4 * size; // TODO handle non 32 bit sizes? - const alignedBytes = Math.ceil(bytes / 16) * 16; + const { size, align, pack } = baseAlignAndSize(type); + offset = Math.ceil(offset / align) * align; + const offsetEnd = offset + size; elements[name] = { name, location: location ? parseInt(location) : undefined, index, type, size, - bytes, - alignedBytes, + offset, + offsetEnd, + pack }; 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+)\s*\)/.exec(shader._vertSrc); if (!mainMatch) throw new Error("Can't find `fn main` in vertex shader source"); @@ -810,6 +882,40 @@ class RendererWebGPU extends Renderer3D { 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(0.0); + rgb += components.diffuse * components.baseColor; + rgb += components.ambient * components.ambientColor; + rgb += components.specular * components.specularColor; + rgb += components.emissive; + return vec4(rgb, components.opacity); + }`, + "vec4f getFinalColor": "(color: vec4) { return color; }", + "void afterFragment": "() {}", + }, + } + ); + } + return this._defaultLightShader; + } + _getColorShader() { if (!this._defaultColorShader) { this._defaultColorShader = new Shader( @@ -858,18 +964,23 @@ class RendererWebGPU extends Renderer3D { // way to add code if a hook is augmented. e.g.: // struct Uniforms { // // @p5 ifdef Vertex getWorldInputs - // uModelMatrix: mat4, - // uViewMatrix: mat4, + // uModelMatrix: mat4f, + // uViewMatrix: mat4f, // // @p5 endif // // @p5 ifndef Vertex getWorldInputs - // uModelViewMatrix: mat4, + // 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[shaderType][`${hookType} ${hookName}`] === target) { + if ( + ( + shader.hooks.modified.vertex[`${hookType} ${hookName}`] || + shader.hooks.modified.fragment[`${hookType} ${hookName}`] + ) === target + ) { return body; } else { return ''; @@ -921,7 +1032,6 @@ class RendererWebGPU extends Renderer3D { } } - console.log(preMain + '\n' + defines + hooks + main + postMain) return preMain + '\n' + defines + hooks + main + postMain; } } diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index aa82c347b6..b22818efa2 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -14,7 +14,7 @@ struct Uniforms { // @p5 endif uProjectionMatrix: mat4x4, uMaterialColor: vec4, - uUseVertexColor: f32, + uUseVertexColor: u32, }; `; @@ -48,7 +48,7 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0.0); + let useVertexColor = (uniforms.uUseVertexColor != 0); var inputs = Vertex( input.aPosition, input.aNormal, @@ -107,9 +107,8 @@ ${getTexture} @fragment fn main(input: FragmentInput) -> @location(0) vec4 { HOOK_beforeFragment(); - var outColor = HOOK_getFinalColor( - vec4(input.vColor.rgb * input.vColor.a, input.vColor.a) - ); + var outColor = HOOK_getFinalColor(input.vColor); + outColor = vec4(outColor.rgb * outColor.a, outColor.a); HOOK_afterFragment(); return outColor; } diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js new file mode 100644 index 0000000000..9722daad06 --- /dev/null +++ b/src/webgpu/shaders/material.js @@ -0,0 +1,348 @@ +import { getTexture } from './utils'; + +const uniforms = ` +struct Uniforms { +// @p5 ifdef Vertex getWorldInputs + uModelMatrix: mat4x4, + uModelNormalMatrix: mat3x3, + uCameraNormalMatrix: mat3x3, +// @p5 endif +// @p5 ifndef Vertex getWorldInputs + uModelViewMatrix: mat4x4, + uNormalMatrix: mat3x3, +// @p5 endif + uViewMatrix: mat4x4, + uProjectionMatrix: mat4x4, + uMaterialColor: vec4, + uUseVertexColor: u32, + + uHasSetAmbient: u32, + uAmbientColor: vec3, + uSpecularMatColor: vec4, + uAmbientMatColor: vec4, + uEmissiveMatColor: vec4, + + uTint: vec4, + isTexture: u32, + + uCameraRotation: mat3x3, + + uDirectionalLightCount: i32, + uLightingDirection: array, 5>, + uDirectionalDiffuseColors: array, 5>, + uDirectionalSpecularColors: array, 5>, + + uPointLightCount: i32, + uPointLightLocation: array, 5>, + uPointLightDiffuseColors: array, 5>, + uPointLightSpecularColors: array, 5>, + + uSpotLightCount: i32, + uSpotLightAngle: vec4, + uSpotLightConc: vec4, + uSpotLightDiffuseColors: array, 4>, + uSpotLightSpecularColors: array, 4>, + uSpotLightLocation: array, 4>, + uSpotLightDirection: array, 4>, + + uSpecular: u32, + uShininess: f32, + uMetallic: f32, + + uConstantAttenuation: f32, + uLinearAttenuation: f32, + uQuadraticAttenuation: f32, + + uUseImageLight: u32, + uUseLighting: u32, +}; +`; + +export const materialVertexShader = ` +struct VertexInput { + @location(0) aPosition: vec3, + @location(1) aNormal: vec3, + @location(2) aTexCoord: vec2, + @location(3) aVertexColor: vec4, +}; + +struct VertexOutput { + @builtin(position) Position: vec4, + @location(0) vNormal: vec3, + @location(1) vTexCoord: vec2, + @location(2) vViewPosition: vec3, + @location(4) vColor: vec4, +}; + +${uniforms} +@group(0) @binding(0) var uniforms: Uniforms; + +struct Vertex { + position: vec3, + normal: vec3, + texCoord: vec2, + color: vec4, +} + +@vertex +fn main(input: VertexInput) -> VertexOutput { + HOOK_beforeVertex(); + var output: VertexOutput; + + let useVertexColor = (uniforms.uUseVertexColor != 0); + var inputs = Vertex( + input.aPosition, + input.aNormal, + input.aTexCoord, + select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor) + ); + +// @p5 ifdef Vertex getObjectInputs + inputs = HOOK_getObjectInputs(inputs); +// @p5 endif + +// @p5 ifdef Vertex getWorldInputs + inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = uniforms.uModelNormalMatrix * inputs.normal; + inputs = HOOK_getWorldInputs(inputs); +// @p5 endif + +// @p5 ifdef Vertex getWorldInputs + // Already multiplied by the model matrix, just apply view + inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal; +// @p5 endif +// @p5 ifndef Vertex getWorldInputs + // Apply both at once + inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = uniforms.uNormalMatrix * inputs.normal; +// @p5 endif + +// @p5 ifdef Vertex getCameraInputs + inputs = HOOK_getCameraInputs(inputs); +// @p5 endif + + output.vViewPosition = inputs.position; + output.vTexCoord = inputs.texCoord; + output.vNormal = normalize(inputs.normal); + output.vColor = inputs.color; + + output.Position = uniforms.uProjectionMatrix * vec4(inputs.position, 1.0); + + HOOK_afterVertex(); + return output; +} +`; + +export const materialFragmentShader = ` +struct FragmentInput { + @location(0) vNormal: vec3, + @location(1) vTexCoord: vec2, + @location(2) vViewPosition: vec3, + @location(4) vColor: vec4, +}; + +${uniforms} +@group(0) @binding(0) var uniforms: Uniforms; + +struct ColorComponents { + baseColor: vec3, + opacity: f32, + ambientColor: vec3, + specularColor: vec3, + diffuse: vec3, + ambient: vec3, + specular: vec3, + emissive: vec3, +} + +struct Inputs { + normal: vec3, + texCoord: vec2, + ambientLight: vec3, + ambientMaterial: vec3, + specularMaterial: vec3, + emissiveMaterial: vec3, + color: vec4, + shininess: f32, + metalness: f32, +} + +${getTexture} + +struct LightResult { + diffuse: vec3, + specular: vec3, +} +struct LightIntensityResult { + diffuse: f32, + specular: f32, +} + +const specularFactor = 2.0; +const diffuseFactor = 0.73; + +fn phongSpecular( + lightDirection: vec3, + viewDirection: vec3, + surfaceNormal: vec3, + shininess: f32 +) -> f32 { + let R = reflect(lightDirection, surfaceNormal); + return pow(max(0.0, dot(R, viewDirection)), shininess); +} + +fn lambertDiffuse(lightDirection: vec3, surfaceNormal: vec3) -> f32 { + return max(0.0, dot(-lightDirection, surfaceNormal)); +} + +fn singleLight( + viewDirection: vec3, + normal: vec3, + lightVector: vec3, + shininess: f32, + metallic: f32 +) -> LightIntensityResult { + let lightDir = normalize(lightVector); + let specularIntensity = mix(1.0, 0.4, metallic); + let diffuseIntensity = mix(1.0, 0.1, metallic); + let diffuse = lambertDiffuse(lightDir, normal) * diffuseIntensity; + let specular = select( + 0., + phongSpecular(lightDir, viewDirection, normal, shininess) * specularIntensity, + uniforms.uSpecular == 1 + ); + return LightIntensityResult(diffuse, specular); +} + +fn totalLight( + modelPosition: vec3, + normal: vec3, + shininess: f32, + metallic: f32 +) -> LightResult { + var totalSpecular = vec3(0.0, 0.0, 0.0); + var totalDiffuse = vec3(0.0, 0.0, 0.0); + + if (uniforms.uUseLighting == 0) { + return LightResult(vec3(1.0, 1.0, 1.0), totalSpecular); + } + + let viewDirection = normalize(-modelPosition); + + for (var j = 0; j < 5; j++) { + if (j < uniforms.uDirectionalLightCount) { + let lightVector = (uniforms.uViewMatrix * vec4( + uniforms.uLightingDirection[j], + 0.0 + )).xyz; + let lightColor = uniforms.uDirectionalDiffuseColors[j]; + let specularColor = uniforms.uDirectionalSpecularColors[j]; + let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); + totalDiffuse += result.diffuse * lightColor; + totalSpecular += result.specular * specularColor; + } + + if (j < uniforms.uPointLightCount) { + let lightPosition = (uniforms.uViewMatrix * vec4( + uniforms.uPointLightLocation[j], + 1.0 + )).xyz; + let lightVector = modelPosition - lightPosition; + let lightDistance = length(lightVector); + let lightFalloff = 1.0 / ( + uniforms.uConstantAttenuation + + lightDistance * uniforms.uLinearAttenuation + + lightDistance * lightDistance * uniforms.uQuadraticAttenuation + ); + let lightColor = uniforms.uPointLightDiffuseColors[j] * lightFalloff; + let specularColor = uniforms.uPointLightSpecularColors[j] * lightFalloff; + let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); + totalDiffuse += result.diffuse * lightColor; + totalSpecular += result.specular * specularColor; + } + + if (j < uniforms.uSpotLightCount) { + let lightPosition = (uniforms.uViewMatrix * vec4( + uniforms.uSpotLightLocation[j], + 1.0 + )).xyz; + let lightVector = modelPosition - lightPosition; + let lightDistance = length(lightVector); + var lightFalloff = 1.0 / ( + uniforms.uConstantAttenuation + + lightDistance * uniforms.uLinearAttenuation + + lightDistance * lightDistance * uniforms.uQuadraticAttenuation + ); + let lightDirection = (uniforms.uViewMatrix * vec4( + uniforms.uSpotLightDirection[j], + 0.0 + )).xyz; + let spotDot = dot(normalize(lightVector), normalize(lightDirection)); + let spotFalloff = select( + 0.0, + pow(spotDot, uniforms.uSpotLightConc[j]), + spotDot < uniforms.uSpotLightAngle[j] + ); + lightFalloff *= spotFalloff; + let lightColor = uniforms.uSpotLightDiffuseColors[j]; + let specularColor = uniforms.uSpotLightSpecularColors[j]; + let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); + totalDiffuse += result.diffuse * lightColor; + totalSpecular += result.specular * specularColor; + } + } + + // TODO: image light + + return LightResult( + totalDiffuse * diffuseFactor, + totalSpecular * specularFactor + ); +} + +@fragment +fn main(input: FragmentInput) -> @location(0) vec4 { + HOOK_beforeFragment(); + + let color = input.vColor; // TODO: check isTexture and apply tint + var inputs = Inputs( + normalize(input.vNormal), + input.vTexCoord, + uniforms.uAmbientColor, + select(color.rgb, uniforms.uAmbientMatColor.rgb, uniforms.uHasSetAmbient == 1), + uniforms.uSpecularMatColor.rgb, + uniforms.uEmissiveMatColor.rgb, + color, + uniforms.uShininess, + uniforms.uMetallic + ); + inputs = HOOK_getPixelInputs(inputs); + + let light = totalLight( + input.vViewPosition, + inputs.normal, + inputs.shininess, + inputs.metalness + ); + + let baseColor = inputs.color; + let components = ColorComponents( + baseColor.rgb, + baseColor.a, + inputs.ambientMaterial, + inputs.specularMaterial, + light.diffuse, + inputs.ambientLight, + light.specular, + inputs.emissiveMaterial + ); + + var outColor = HOOK_getFinalColor( + HOOK_combineColors(components) + ); + outColor = vec4(outColor.rgb * outColor.a, outColor.a); + HOOK_afterFragment(); + return outColor; +} +`; From 5db2d3331476e490e40a48ff9431212c019f7147 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:40:27 +0100 Subject: [PATCH 12/98] changed drawbuffers to draw stroke and fill buffers depending on current shader --- src/core/p5.Renderer3D.js | 4 +-- src/webgpu/p5.RendererWebGPU.js | 46 +++++++++++++++++---------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index a2e40d83c5..223382f141 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -557,7 +557,7 @@ export class Renderer3D extends Renderer { geometry.hasFillTransparency() ); - this._drawBuffers(geometry, { mode, count }, false); + this._drawBuffers(geometry, { mode, count }); shader.unbindShader(); } @@ -583,7 +583,7 @@ export class Renderer3D extends Renderer { geometry.hasStrokeTransparency() ); - this._drawBuffers(geometry, {count}, true) + this._drawBuffers(geometry, {count}) shader.unbindShader(); } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index ffb012e1f1..5d1439c855 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -548,7 +548,7 @@ class RendererWebGPU extends Renderer3D { // Rendering ////////////////////////////////////////////// - _drawBuffers(geometry, { mode = constants.TRIANGLES, count = 1 }, stroke) { + _drawBuffers(geometry, { mode = constants.TRIANGLES, count = 1 }) { const buffers = this.geometryBufferCache.getCached(geometry); if (!buffers) return; @@ -578,33 +578,33 @@ class RendererWebGPU extends Renderer3D { }; const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(this._curShader.getPipeline(this._shaderOptions({ mode }))); + const currentShader = this._curShader; + passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode }))); // Bind vertex buffers - for (const buffer of this._getVertexBuffers(this._curShader)) { + for (const buffer of this._getVertexBuffers(currentShader)) { passEncoder.setVertexBuffer( - this._curShader.attributes[buffer.attr].location, + currentShader.attributes[buffer.attr].location, buffers[buffer.dst], 0 ); } // Bind uniforms this._packUniforms(this._curShader); - console.log(this._curShader); this.device.queue.writeBuffer( - this._curShader._uniformBuffer, + currentShader._uniformBuffer, 0, - this._curShader._uniformData.buffer, - this._curShader._uniformData.byteOffset, - this._curShader._uniformData.byteLength + currentShader._uniformData.buffer, + currentShader._uniformData.byteOffset, + currentShader._uniformData.byteLength ); // Bind sampler/texture uniforms - for (const [group, entries] of this._curShader._groupEntries) { + for (const [group, entries] of currentShader._groupEntries) { const bgEntries = entries.map(entry => { if (group === 0 && entry.binding === 0) { return { binding: 0, - resource: { buffer: this._curShader._uniformBuffer }, + resource: { buffer: currentShader._uniformBuffer }, }; } @@ -616,7 +616,7 @@ class RendererWebGPU extends Renderer3D { }; }); - const layout = this._curShader._bindGroupLayouts[group]; + const layout = currentShader._bindGroupLayouts[group]; const bindGroup = this.device.createBindGroup({ layout, entries: bgEntries, @@ -624,18 +624,20 @@ class RendererWebGPU extends Renderer3D { passEncoder.setBindGroup(group, bindGroup); } - if (buffers.lineVerticesBuffer && geometry.lineVertices && stroke) { + 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); + } + } + + if (buffers.lineVerticesBuffer && currentShader.shaderType === "stroke") { passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0); } - // Bind index buffer and issue draw - if (!stroke) { - 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); - }} passEncoder.end(); this.queue.submit([commandEncoder.finish()]); From a116e6fa7b7df59fb695b4a59e4faac55ba7836d Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:42:46 +0100 Subject: [PATCH 13/98] fixed uViewport uniform (uniform problem fixed in upstream) --- src/core/p5.Renderer3D.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 223382f141..a487d975f9 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1414,7 +1414,7 @@ export class Renderer3D extends Renderer { this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); shader.setUniform("uCameraRotation", this.scratchMat3.mat3); } - shader.setUniform("uViewport", [0, 0, 400, 400]); + shader.setUniform("uViewport", this._viewport); } _setStrokeUniforms(strokeShader) { From ddfeb05839ab830bed84fc2105a95af4e8637652 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:43:24 +0100 Subject: [PATCH 14/98] remove console log --- src/webgpu/p5.RendererWebGPU.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 5d1439c855..acbfa20661 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -948,7 +948,6 @@ class RendererWebGPU extends Renderer3D { } } - //console.log(preMain + '\n' + defines + hooks + main + postMain) return preMain + '\n' + defines + hooks + main + postMain; } } From 14f1857fddbf30d66c890158664a6f7639c5e348 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:44:09 +0100 Subject: [PATCH 15/98] remove unused variable --- src/webgl/p5.Shader.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 3f4015311c..b95066a70d 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -983,7 +983,6 @@ class Shader { if (!uniform) { return; } - const gl = this._renderer.GL; if (uniform.isArray) { if ( From 2c19cdcaa0abaf05476e8413ec9130d1392f14de Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:45:30 +0100 Subject: [PATCH 16/98] remove hardcoded viewport (uniform issue fixed upstream) --- src/webgpu/shaders/line.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index a42e7b178e..15c3684886 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -75,7 +75,6 @@ fn lineIntersection(aPoint: vec2f, aDir: vec2f, bPoint: vec2f, bDir: vec2f) -> v fn main(input: StrokeVertexInput) -> StrokeVertexOutput { HOOK_beforeVertex(); var output: StrokeVertexOutput; - let viewport = vec4(0.,0.,400.,400.); let simpleLines = (uniforms.uSimpleLines != 0.); if (!simpleLines) { if (all(input.aTangentIn == vec3()) != all(input.aTangentOut == vec3())) { @@ -219,7 +218,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { if (sideEnum == 2.) { // Calculate the position + tangent on either side of the join, and // find where the lines intersect to find the elbow of the join - var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * viewport.zw; + var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * uniforms.uViewport.zw; var intersection = lineIntersection( c + (side * normalIn * inputs.weight / 2.), From e7696f067a45c3f7afc9c5ccdcc9b386790fa1de Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:46:01 +0100 Subject: [PATCH 17/98] fix stroke shader bugs from porting process) --- src/webgpu/shaders/line.js | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index 15c3684886..96402aa1ee 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -179,8 +179,8 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { var qIn = uniforms.uProjectionMatrix * posqIn; var qOut = uniforms.uProjectionMatrix * posqOut; - var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * viewport.zw); - var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * viewport.zw); + var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * uniforms.uViewport.zw); + var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * uniforms.uViewport.zw); var curPerspScale = vec2(); if (uniforms.uPerspective == 1) { @@ -195,7 +195,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { // No Perspective --- // multiply by W (to cancel out division by W later in the pipeline) and // convert from screen to clip (derived from clip to screen above) - curPerspScale = p.w / (0.5 * viewport.zw); + curPerspScale = p.w / (0.5 * uniforms.uViewport.zw); } var offset = vec2(); @@ -234,14 +234,14 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { // the magnitude to avoid lines going across the whole screen when this // happens. var mag = length(offset); - var maxMag = 3 * inputs.weight; + var maxMag = 3. * inputs.weight; if (mag > maxMag) { - offset = vec2(maxMag / mag); - } else if (sideEnum == 1.) { + offset *= maxMag / mag; + } + } else if (sideEnum == 1.) { offset = side * normalIn * inputs.weight / 2.; - } else if (sideEnum == 3.) { + } else if (sideEnum == 3.) { offset = side * normalOut * inputs.weight / 2.; - } } } if (uniforms.uStrokeJoin == 2) { @@ -258,7 +258,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { tangent = tangentIn; } output.vTangent = tangent; - var normal = vec2(-tangent.y, tangent.y); + var normal = vec2(-tangent.y, tangent.x); var normalOffset = sign(input.aSide); // Caps will have side values of -2 or 2 on the edge of the cap that @@ -274,17 +274,8 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { output.Position = vec4( p.xy + offset.xy * curPerspScale, - p.zy + p.zw ); - var clip_pos: vec4; - if (input.aSide == 1.0) { - clip_pos = vec4(-0.1, 0.1, 0.5, 1.); - } else if (input.aSide == -1.0) { - clip_pos = vec4(-0.5, 0.5, 0.5, 1.0); - } else { - clip_pos = vec4(0.0, -0.5, 0.5 ,1.0); - } - output.Position = clip_pos; return output; } From ed88f919539a6cdbeb8179bc512198421d6f4413 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 12:46:45 +0100 Subject: [PATCH 18/98] stroke test --- preview/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/preview/index.html b/preview/index.html index 016dd60172..5b141511c1 100644 --- a/preview/index.html +++ b/preview/index.html @@ -36,10 +36,10 @@ // p.noStroke(); for (const [i, c] of ['red'].entries()) { p.stroke(0); - p.strokeWeight(10); + p.strokeWeight(2); p.push(); p.fill(c); - p.sphere(60, 4, 2); + p.sphere(60); p.pop(); } }; From ce4b31e9b1fcb1b3952462250a721df826a35520 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 16 Jun 2025 09:15:35 -0400 Subject: [PATCH 19/98] Coerce modified hooks to boolean --- src/webgpu/p5.RendererWebGPU.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a2bcaca885..fa738f852c 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -977,8 +977,8 @@ class RendererWebGPU extends Renderer3D { const target = condition === 'ifdef'; if ( ( - shader.hooks.modified.vertex[`${hookType} ${hookName}`] || - shader.hooks.modified.fragment[`${hookType} ${hookName}`] + !!shader.hooks.modified.vertex[`${hookType} ${hookName}`] || + !!shader.hooks.modified.fragment[`${hookType} ${hookName}`] ) === target ) { return body; From 4dc493dbe6ae986c5ef738cef6119c2986f5b7f1 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 16:19:29 +0100 Subject: [PATCH 20/98] add stroke to preview --- preview/index.html | 49 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/preview/index.html b/preview/index.html index 5b141511c1..99a5bc38d5 100644 --- a/preview/index.html +++ b/preview/index.html @@ -25,21 +25,52 @@ const sketch = function (p) { let fbo; let sh; + let ssh; + p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); + sh = p.baseMaterialShader().modify({ + uniforms: { + 'f32 time': () => p.millis(), + }, + 'Vertex getWorldInputs': `(inputs: Vertex) { + var result = inputs; + result.position.y += 40.0 * sin(uniforms.time * 0.01); + return result; + }`, + }) + ssh = p.baseStrokeShader().modify({ + uniforms: { + 'f32 time': () => p.millis(), + }, + 'StrokeVertex getWorldInputs': `(inputs: StrokeVertex) { + var result = inputs; + result.position.y += 40.0 * sin(uniforms.time * 0.01); + return result; + }`, + }) }; - p.disableFriendlyErrors = true; + p.draw = function () { - p.orbitControl() const t = p.millis() * 0.008; - p.background(0); - // p.noStroke(); - for (const [i, c] of ['red'].entries()) { - p.stroke(0); - p.strokeWeight(2); - p.push(); + p.background(200); + p.shader(sh); + p.strokeShader(ssh) + p.ambientLight(50); + p.directionalLight(100, 100, 100, 0, 1, -1); + p.pointLight(155, 155, 155, 0, -200, 500); + p.specularMaterial(255); + p.shininess(300); + p.stroke('white') + for (const [i, c] of ['red', 'lime', 'blue'].entries()) { + p.push(); p.fill(c); - p.sphere(60); + p.translate( + p.width/3 * p.sin(t + i * Math.E), + 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), + p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), + ) + p.sphere(30); p.pop(); } }; From d88fba92a6ab643d0205b02e41e107230ad3b647 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 16:21:44 +0100 Subject: [PATCH 21/98] change stroke shader constants/ preprocessor to be compatible with webgpu and webgl --- src/core/p5.Renderer3D.js | 8 ++++---- src/webgl/p5.RendererGL.js | 2 +- src/webgpu/p5.RendererWebGPU.js | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 394462f3d7..c437f34725 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -16,16 +16,16 @@ import { RenderBuffer } from "../webgl/p5.RenderBuffer"; import { Image } from "../image/p5.Image"; import { Texture } from "../webgl/p5.Texture"; -export function getStrokeDefs() { +export function getStrokeDefs(shaderConstant) { const STROKE_CAP_ENUM = {}; const STROKE_JOIN_ENUM = {}; let lineDefs = ""; const defineStrokeCapEnum = function (key, val) { - lineDefs += `#define STROKE_CAP_${key} ${val}\n`; + lineDefs += shaderConstant(`STROKE_CAP_${key}`, `${val}`, 'u32'); STROKE_CAP_ENUM[constants[key]] = val; }; const defineStrokeJoinEnum = function (key, val) { - lineDefs += `#define STROKE_JOIN_${key} ${val}\n`; + lineDefs += shaderConstant(`STROKE_JOIN_${key}`, `${val}`, 'u32'); STROKE_JOIN_ENUM[constants[key]] = val; }; @@ -41,7 +41,7 @@ export function getStrokeDefs() { return { STROKE_CAP_ENUM, STROKE_JOIN_ENUM, lineDefs }; } -const { STROKE_CAP_ENUM, STROKE_JOIN_ENUM } = getStrokeDefs(); +const { STROKE_CAP_ENUM, STROKE_JOIN_ENUM } = getStrokeDefs(()=>""); export class Renderer3D extends Renderer { constructor(pInst, w, h, isMainCanvas, elt) { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index e033e3b1c2..6cc8273a66 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -39,7 +39,7 @@ import filterInvertFrag from "./shaders/filters/invert.frag"; import filterThresholdFrag from "./shaders/filters/threshold.frag"; import filterShaderVert from "./shaders/filters/default.vert"; -const { lineDefs } = getStrokeDefs(); +const { lineDefs } = getStrokeDefs((n, v) => `#define ${n} ${v};\n`); const defaultShaders = { normalVert, diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 61e4254675..9af8bb4256 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1,10 +1,14 @@ -import { Renderer3D } from '../core/p5.Renderer3D'; +import { Renderer3D, getStrokeDefs } from '../core/p5.Renderer3D'; import { Shader } from '../webgl/p5.Shader'; import * as constants from '../core/constants'; + + import { colorVertexShader, colorFragmentShader } from './shaders/color'; import { lineVertexShader, lineFragmentShader} from './shaders/line'; import { materialVertexShader, materialFragmentShader } from './shaders/material'; +const { lineDefs } = getStrokeDefs((n, v, t) => `const ${n}: ${t} = ${v};\n`); + class RendererWebGPU extends Renderer3D { constructor(pInst, w, h, isMainCanvas, elt) { super(pInst, w, h, isMainCanvas, elt) From c48977aa13eb23f9422399783f93f584784f68dc Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 16:24:56 +0100 Subject: [PATCH 22/98] rename fillHooks to populateHooks (ambiguous with fill/stroke) --- src/webgl/p5.RendererGL.js | 2 +- src/webgl/p5.Shader.js | 2 +- src/webgpu/p5.RendererWebGPU.js | 19 ++++--- src/webgpu/shaders/line.js | 94 +++++++++++++++++++++++++-------- 4 files changed, 87 insertions(+), 30 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 6cc8273a66..e05e6839b1 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1683,7 +1683,7 @@ class RendererGL extends Renderer3D { ////////////////////////////////////////////// // Shader hooks ////////////////////////////////////////////// - fillHooks(shader, src, shaderType) { + populateHooks(shader, src, shaderType) { const main = 'void main'; if (!src.includes(main)) return src; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index fc3745e394..0134701c9c 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -123,7 +123,7 @@ class Shader { } shaderSrc(src, shaderType) { - return this._renderer.fillHooks(this, src, shaderType); + return this._renderer.populateHooks(this, src, shaderType); } /** diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 9af8bb4256..5d023e266f 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -954,17 +954,22 @@ class RendererWebGPU extends Renderer3D { if (!this._defaultLineShader) { this._defaultLineShader = new Shader( this, - lineVertexShader, - lineFragmentShader, + lineDefs + lineVertexShader, + lineDefs + lineFragmentShader, { vertex: { "void beforeVertex": "() {}", - "Vertex getObjectInputs": "(inputs: Vertex) { return inputs; }", - "Vertex getWorldInputs": "(inputs: Vertex) { return inputs; }", - "Vertex getCameraInputs": "(inputs: Vertex) { return inputs; }", + "StrokeVertex getObjectInputs": "(inputs: StrokeVertex) { return inputs; }", + "StrokeVertex getWorldInputs": "(inputs: StrokeVertex) { return inputs; }", + "StrokeVertex getCameraInputs": "(inputs: StrokeVertex) { return inputs; }", + "void afterVertex": "() {}", }, fragment: { - "vec4 getFinalColor": "(color: vec4) { return color; }" + "void beforeFragment": "() {}", + "Inputs getPixelInputs": "(inputs: Inputs) { return inputs; }", + "vec4 getFinalColor": "(color: vec4) { return color; }", + "bool shouldDiscard": "(outside: bool) { return outside; };", + "void afterFragment": "() {}", }, } ); @@ -987,7 +992,7 @@ class RendererWebGPU extends Renderer3D { ////////////////////////////////////////////// // Shader hooks ////////////////////////////////////////////// - fillHooks(shader, src, shaderType) { + populateHooks(shader, src, shaderType) { if (!src.includes('fn main')) return src; // Apply some p5-specific preprocessing. WGSL doesn't have preprocessor diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index 96402aa1ee..0aa9f5e72b 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -2,11 +2,11 @@ import { getTexture } from './utils' const uniforms = ` struct Uniforms { -// @p5 ifdef Vertex getWorldInputs +// @p5 ifdef StrokeVertex getWorldInputs uModelMatrix: mat4x4, uViewMatrix: mat4x4, // @p5 endif -// @p5 ifndef Vertex getWorldInputs +// @p5 ifndef StrokeVertex getWorldInputs uModelViewMatrix: mat4x4, // @p5 endif uMaterialColor: vec4, @@ -15,10 +15,10 @@ struct Uniforms { uUseLineColor: f32, uSimpleLines: f32, uViewport: vec4, - uPerspective: i32, - uStrokeJoin: i32, -} -`; + uPerspective: u32, + uStrokeCap: u32, + uStrokeJoin: u32, +}`; export const lineVertexShader = ` struct StrokeVertexInput { @@ -44,7 +44,7 @@ struct StrokeVertexOutput { ${uniforms} @group(0) @binding(0) var uniforms: Uniforms; -struct Vertex { +struct StrokeVertex { position: vec3, tangentIn: vec3, tangentOut: vec3, @@ -97,7 +97,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { } else { lineColor = uniforms.uMaterialColor; } - var inputs = Vertex( + var inputs = StrokeVertex( input.aPosition.xyz, input.aTangentIn, input.aTangentOut, @@ -105,29 +105,30 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { uniforms.uStrokeWeight ); -// @p5 ifdef Vertex getObjectInputs +// @p5 ifdef StrokeVertex getObjectInputs inputs = HOOK_getObjectInputs(inputs); // @p5 endif -// @p5 ifdef Vertex getWorldInputs - inputs.position = (uModelMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; - inputs.tangentOut = (uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; +// @p5 ifdef StrokeVertex getWorldInputs + inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (uniforms.uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; + inputs.tangentOut = (uniforms.uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; + inputs = HOOK_getWorldInputs(inputs); // @p5 endif -// @p5 ifdef Vertex getWorldInputs +// @p5 ifdef StrokeVertex getWorldInputs // Already multiplied by the model matrix, just apply view inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.)).xyz; inputs.tangentIn = (uniforms.uViewMatrix * vec4(input.aTangentIn, 0.)).xyz; inputs.tangentOut = (uniforms.uViewMatrix * vec4(input.aTangentOut, 0.)).xyz; // @p5 endif -// @p5 ifndef Vertex getWorldInputs +// @p5 ifndef StrokeVertex getWorldInputs // Apply both at once inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.)).xyz; inputs.tangentIn = (uniforms.uModelViewMatrix * vec4(input.aTangentIn, 0.)).xyz; inputs.tangentOut = (uniforms.uModelViewMatrix * vec4(input.aTangentOut, 0.)).xyz; // @p5 endif -// @p5 ifdef Vertex getCameraInputs +// @p5 ifdef StrokeVertex getCameraInputs inputs = HOOK_getCameraInputs(inputs); // @p5 endif @@ -276,11 +277,9 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { p.xy + offset.xy * curPerspScale, p.zw ); + HOOK_afterVertex(); return output; -} - - -`; +}`; export const lineFragmentShader = ` struct StrokeFragmentInput { @@ -299,9 +298,62 @@ ${uniforms} ${getTexture} +fn distSquared(a: vec2, b: vec2) -> f32 { + return dot(b - a, b - a); +} + +struct Inputs { + color: vec4, + tangent: vec2, + center: vec2, + position: vec2, + strokeWeight: f32, +} + @fragment fn main(input: StrokeFragmentInput) -> @location(0) vec4 { - return vec4(1., 1., 1., 1.); + HOOK_beforeFragment(); + + var inputs: Inputs; + inputs.color = input.vColor; + inputs.tangent = input.vTangent; + inputs.center = input.vCenter; + inputs.position = input.vPosition; + inputs.strokeWeight = input.vStrokeWeight; + inputs = HOOK_getPixelInputs(inputs); + + if (input.vCap > 0.) { + if ( + uniforms.uStrokeCap == STROKE_CAP_ROUND && + HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) + ) { + discard; + } else if ( + uniforms.uStrokeCap == STROKE_CAP_SQUARE && + HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.) + ) { + discard; + } else if (HOOK_shouldDiscard(false)) { + discard; + } + } else if (input.vJoin > 0.) { + if ( + uniforms.uStrokeJoin == STROKE_JOIN_ROUND && + HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) + ) { + discard; + } else if (uniforms.uStrokeJoin == STROKE_JOIN_BEVEL) { + let normal = vec2(-inputs.tangent.y, -inputs.tangent.x); + if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > input.vMaxDist)) { + discard; + } + } else if (HOOK_shouldDiscard(false)) { + discard; + } + } + var col = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a); + HOOK_afterFragment(); + return vec4(col); } `; From f0875d795be9297bcdf9b312dffcafeda4741536 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 17:40:17 +0100 Subject: [PATCH 23/98] add strokes back to WebGL mode --- src/webgl/p5.RendererGL.js | 42 +++++++++++++++----------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index e05e6839b1..70adb68c16 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -270,37 +270,29 @@ class RendererGL extends Renderer3D { } } - // Stroke version for now: - // -// { -// const gl = this.GL; -// // move this to _drawBuffers ? -// if (count === 1) { -// gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); -// } else { -// try { - // gl.drawArraysInstanced( - // gl.TRIANGLES, - // 0, - // geometry.lineVertices.length / 3, - // count - // ); - // } catch (e) { - // console.log( - // "🌸 p5.js says: Instancing is only supported in WebGL2 mode" - // ); - // } - // } - // } - _drawBuffers(geometry, { mode = constants.TRIANGLES, count }) { const gl = this.GL; const glBuffers = this.geometryBufferCache.getCached(geometry); - //console.log(glBuffers); if (!glBuffers) return; - if (glBuffers.indexBuffer) { + if (this._curShader.shaderType === 'stroke'){ + if (count === 1) { + gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); + } else { + try { + gl.drawArraysInstanced( + gl.TRIANGLES, + 0, + geometry.lineVertices.length / 3, + count + ); + } catch (e) { + console.log( + "🌸 p5.js says: Instancing is only supported in WebGL2 mode" + ); + } + } else if (glBuffers.indexBuffer) { this._bindBuffer(glBuffers.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); // If this model is using a Uint32Array we need to ensure the From e8bedfc470d01df99d2ebaf8d26a0e1f4afd5ad2 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 17:40:17 +0100 Subject: [PATCH 24/98] add strokes back to WebGL mode --- src/webgl/p5.RendererGL.js | 43 ++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index e05e6839b1..01dc2d035e 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -270,37 +270,30 @@ class RendererGL extends Renderer3D { } } - // Stroke version for now: - // -// { -// const gl = this.GL; -// // move this to _drawBuffers ? -// if (count === 1) { -// gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); -// } else { -// try { - // gl.drawArraysInstanced( - // gl.TRIANGLES, - // 0, - // geometry.lineVertices.length / 3, - // count - // ); - // } catch (e) { - // console.log( - // "🌸 p5.js says: Instancing is only supported in WebGL2 mode" - // ); - // } - // } - // } - _drawBuffers(geometry, { mode = constants.TRIANGLES, count }) { const gl = this.GL; const glBuffers = this.geometryBufferCache.getCached(geometry); - //console.log(glBuffers); if (!glBuffers) return; - if (glBuffers.indexBuffer) { + if (this._curShader.shaderType === 'stroke') { + if (count === 1) { + gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); + } else { + try { + gl.drawArraysInstanced( + gl.TRIANGLES, + 0, + geometry.lineVertices.length / 3, + count + ); + } catch (e) { + console.log( + "🌸 p5.js says: Instancing is only supported in WebGL2 mode" + ); + } + } + } else if (glBuffers.indexBuffer) { this._bindBuffer(glBuffers.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); // If this model is using a Uint32Array we need to ensure the From 0af8df9e37e94f189bbeb8945f581872fb9639b4 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 18:07:51 +0100 Subject: [PATCH 25/98] typo in string --- src/webgpu/p5.RendererWebGPU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 5d023e266f..0b9dd6fcfa 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -675,7 +675,7 @@ class RendererWebGPU extends Renderer3D { new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`) ); if (!structMatch) { - throw new Error(`Can't find a struct defnition for ${structName}`); + throw new Error(`Can't find a struct definition for ${structName}`); } const structBody = structMatch[1]; From f120ad95d83ed0bc007af275264e39a4caba7c14 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 18:07:51 +0100 Subject: [PATCH 26/98] typo in string --- src/webgl/p5.RendererGL.js | 7 ------- src/webgpu/p5.RendererWebGPU.js | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 676cb3b6e7..f683075df1 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -276,11 +276,7 @@ class RendererGL extends Renderer3D { if (!glBuffers) return; -<<<<<<< HEAD - if (this._curShader.shaderType === 'stroke') { -======= if (this._curShader.shaderType === 'stroke'){ ->>>>>>> f0875d795be9297bcdf9b312dffcafeda4741536 if (count === 1) { gl.drawArrays(gl.TRIANGLES, 0, geometry.lineVertices.length / 3); } else { @@ -296,10 +292,7 @@ class RendererGL extends Renderer3D { "🌸 p5.js says: Instancing is only supported in WebGL2 mode" ); } -<<<<<<< HEAD } -======= ->>>>>>> f0875d795be9297bcdf9b312dffcafeda4741536 } else if (glBuffers.indexBuffer) { this._bindBuffer(glBuffers.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 5d023e266f..0b9dd6fcfa 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -675,7 +675,7 @@ class RendererWebGPU extends Renderer3D { new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`) ); if (!structMatch) { - throw new Error(`Can't find a struct defnition for ${structName}`); + throw new Error(`Can't find a struct definition for ${structName}`); } const structBody = structMatch[1]; From 468b6384c0bc8e6cd3b64a7e52f52c210739e085 Mon Sep 17 00:00:00 2001 From: lukeplowden Date: Mon, 16 Jun 2025 18:15:18 +0100 Subject: [PATCH 27/98] multiply alpha after hook --- src/webgpu/shaders/line.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index 0aa9f5e72b..5c01ddd1bf 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -351,7 +351,8 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4 { discard; } } - var col = HOOK_getFinalColor(vec4(inputs.color.rgb, 1.) * inputs.color.a); + var col = HOOK_getFinalColor(inputs.color); + col = vec4(col.rgb, 1.0) * col.a; HOOK_afterFragment(); return vec4(col); } From 01a7c1c69d8ecc6e6e21b0cf9b00f0f39a663cbe Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 17 Jun 2025 19:26:36 -0400 Subject: [PATCH 28/98] Fix some RendererGL + filterRenderer2D tests --- src/image/filterRenderer2D.js | 173 ++++++++++++++- src/shape/custom_shapes.js | 64 +++--- src/webgl/3d_primitives.js | 8 +- src/webgl/p5.Framebuffer.js | 20 +- src/webgl/p5.Geometry.js | 16 +- src/webgl/p5.RendererGL.js | 366 ++----------------------------- src/webgl/p5.Shader.js | 14 +- src/webgl/p5.Texture.js | 7 +- src/webgl/shaders/line.vert | 2 +- src/webgl/utils.js | 351 +++++++++++++++++++++++++++++ test/unit/webgl/p5.RendererGL.js | 3 +- vitest.workspace.mjs | 3 +- 12 files changed, 628 insertions(+), 399 deletions(-) diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index 97eed42671..cfdf10eb8d 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -1,6 +1,13 @@ import { Shader } from "../webgl/p5.Shader"; import { Texture } from "../webgl/p5.Texture"; import { Image } from "./p5.Image"; +import { + getWebGLShaderAttributes, + getWebGLUniformMetadata, + populateGLSLHooks, + setWebGLTextureParams, + setWebGLUniformValue +} from "../webgl/utils"; import * as constants from '../core/constants'; import filterGrayFrag from '../webgl/shaders/filters/gray.frag'; @@ -42,6 +49,9 @@ class FilterRenderer2D { console.error("WebGL not supported, cannot apply filter."); return; } + + this.textures = new Map(); + // Minimal renderer object required by p5.Shader and p5.Texture this._renderer = { GL: this.gl, @@ -62,6 +72,167 @@ class FilterRenderer2D { } return this._emptyTexture; }, + _initShader: (shader) => { + const gl = this.gl; + + const vertShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertShader, shader.vertSrc()); + gl.compileShader(vertShader); + if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { + throw new Error(`Yikes! An error occurred compiling the vertex shader: ${ + gl.getShaderInfoLog(vertShader) + }`); + } + + const fragShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragShader, shader.fragSrc()); + gl.compileShader(fragShader); + if (!gl.getShaderParameter(fragShader, gl.COMPILE_STATUS)) { + throw new Error(`Darn! An error occurred compiling the fragment shader: ${ + gl.getShaderInfoLog(fragShader) + }`); + } + + const program = gl.createProgram(); + gl.attachShader(program, vertShader); + gl.attachShader(program, fragShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error( + `Snap! Error linking shader program: ${gl.getProgramInfoLog(program)}` + ); + } + + shader._glProgram = program; + shader._vertShader = vertShader; + shader._fragShader = fragShader; + }, + getTexture: (input) => { + let src = input; + if (src instanceof Framebuffer) { + src = src.color; + } + + const texture = this.textures.get(src); + if (texture) { + return texture; + } + + const tex = new Texture(this._renderer, src); + this.textures.set(src, tex); + return tex; + }, + populateHooks: (shader, src, shaderType) => { + return populateGLSLHooks(shader, src, shaderType); + }, + _getShaderAttributes: (shader) => { + return getWebGLShaderAttributes(shader, this.gl); + }, + getUniformMetadata: (shader) => { + return getWebGLUniformMetadata(shader, this.gl); + }, + _finalizeShader: () => {}, + _useShader: (shader) => { + this.gl.useProgram(shader._glProgram); + }, + bindTexture: (tex) => { + // bind texture using gl context + glTarget and + // generated gl texture object + this.gl.bindTexture(this.gl.TEXTURE_2D, tex.getTexture().texture); + }, + unbindTexture: () => { + // unbind per above, disable texturing on glTarget + this.gl.bindTexture(this.gl.TEXTURE_2D, null); + }, + _unbindFramebufferTexture: (uniform) => { + // Make sure an empty texture is bound to the slot so that we don't + // accidentally leave a framebuffer bound, causing a feedback loop + // when something else tries to write to it + const gl = this.gl; + const empty = this._getEmptyTexture(); + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + empty.bindTexture(); + gl.uniform1i(uniform.location, uniform.samplerIndex); + }, + createTexture: ({ width, height, format, dataType }) => { + const gl = this.gl; + const tex = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, tex); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); + // TODO use format and data type + return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE }; + }, + uploadTextureFromSource: ({ texture, glFormat, glDataType }, source) => { + const gl = this.gl; + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source); + }, + uploadTextureFromData: ({ texture, glFormat, glDataType }, data, width, height) => { + const gl = this.gl; + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + glFormat, + width, + height, + 0, + glFormat, + glDataType, + data + ); + }, + setTextureParams: (texture) => { + return setWebGLTextureParams(texture, this.gl, this._renderer.webglVersion); + }, + updateUniformValue: (shader, uniform, data) => { + return setWebGLUniformValue( + shader, + uniform, + data, + (tex) => this._renderer.getTexture(tex), + this.gl + ); + }, + _enableAttrib: (_shader, attr, size, type, normalized, stride, offset) => { + const loc = attr.location; + const gl = this.gl; + // Enable register even if it is disabled + if (!this._renderer.registerEnabled.has(loc)) { + gl.enableVertexAttribArray(loc); + // Record register availability + this._renderer.registerEnabled.add(loc); + } + gl.vertexAttribPointer( + loc, + size, + type || gl.FLOAT, + normalized || false, + stride || 0, + offset || 0 + ); + }, + _disableRemainingAttributes: (shader) => { + for (const location of this._renderer.registerEnabled.values()) { + if ( + !Object.keys(shader.attributes).some( + key => shader.attributes[key].location === location + ) + ) { + this.gl.disableVertexAttribArray(location); + this._renderer.registerEnabled.delete(location); + } + } + }, + _updateTexture: (uniform, tex) => { + const gl = this.gl; + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + tex.bindTexture(); + tex.update(); + gl.uniform1i(uniform.location, uniform.samplerIndex); + } }; this._baseFilterShader = undefined; @@ -257,7 +428,7 @@ class FilterRenderer2D { this._shader.enableAttrib(this._shader.attributes.aTexCoord, 2); this._shader.bindTextures(); - this._shader.disableRemainingAttributes(); + this._renderer._disableRemainingAttributes(this._shader); // Draw the quad gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); diff --git a/src/shape/custom_shapes.js b/src/shape/custom_shapes.js index 9e22f0b75c..3049094125 100644 --- a/src/shape/custom_shapes.js +++ b/src/shape/custom_shapes.js @@ -1170,10 +1170,12 @@ class PrimitiveToPath2DConverter extends PrimitiveVisitor { class PrimitiveToVerticesConverter extends PrimitiveVisitor { contours = []; curveDetail; + pointsToLines; - constructor({ curveDetail = 1 } = {}) { + constructor({ curveDetail = 1, pointsToLines = true } = {}) { super(); this.curveDetail = curveDetail; + this.pointsToLines = pointsToLines; } lastContour() { @@ -1246,7 +1248,11 @@ class PrimitiveToVerticesConverter extends PrimitiveVisitor { } } visitPoint(point) { - this.contours.push(point.vertices.slice()); + if (this.pointsToLines) { + this.contours.push(...point.vertices.map(v => [v, v])); + } else { + this.contours.push(point.vertices.slice()); + } } visitLine(line) { this.contours.push(line.vertices.slice()); @@ -1592,11 +1598,11 @@ function customShapes(p5, fn) { * one call to bezierVertex(), before * a number of `bezierVertex()` calls that is a multiple of the parameter * set by bezierOrder(...) (default 3). - * + * * Each curve of order 3 requires three calls to `bezierVertex`, so * 2 curves would need 7 calls to `bezierVertex()`: * (1 one initial anchor point, two sets of 3 curves describing the curves) - * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2. + * With `bezierOrder(2)`, two curves would need 5 calls: 1 + 2 + 2. * * Bézier curves can also be drawn in 3D using WebGL mode. * @@ -1605,7 +1611,7 @@ function customShapes(p5, fn) { * * @method bezierOrder * @param {Number} order The new order to set. Can be either 2 or 3, by default 3 - * + * * @example *
* @@ -1619,7 +1625,7 @@ function customShapes(p5, fn) { * * // Start drawing the shape. * beginShape(); - * + * * // set the order to 2 for a quadratic Bézier curve * bezierOrder(2); * @@ -2059,11 +2065,11 @@ function customShapes(p5, fn) { /** * Sets the property of a curve. - * + * * For example, set tightness, * use `splineProperty('tightness', t)`, with `t` between 0 and 1, * at 0 as default. - * + * * Spline curves are like cables that are attached to a set of points. * Adjusting tightness adjusts how tightly the cable is * attached to the points. The parameter, tightness, determines @@ -2072,33 +2078,33 @@ function customShapes(p5, fn) { * `splineProperty('tightness', 1)`, connects the curve's points * using straight lines. Values in the range from –5 to 5 * deform curves while leaving them recognizable. - * + * * This function can also be used to set 'ends' property * (see also: the curveDetail() example), * such as: `splineProperty('ends', EXCLUDE)` to exclude * vertices, or `splineProperty('ends', INCLUDE)` to include them. - * + * * @method splineProperty * @param {String} property * @param value Value to set the given property to. - * + * * @example *
* * // Move the mouse left and right to see the curve change. - * + * * function setup() { * createCanvas(100, 100); * describe('A black curve forms a sideways U shape. The curve deforms as the user moves the mouse from left to right'); * } - * + * * function draw() { * background(200); - * + * * // Set the curve's tightness using the mouse. * let t = map(mouseX, 0, 100, -5, 5, true); * splineProperty('tightness', t); - * + * * // Draw the curve. * noFill(); * beginShape(); @@ -2124,11 +2130,11 @@ function customShapes(p5, fn) { /** * Get or set multiple spline properties at once. - * + * * Similar to splineProperty(): * `splineProperty('tightness', t)` is the same as * `splineProperties({'tightness': t})` - * + * * @method splineProperties * @param {Object} properties An object containing key-value pairs to set. */ @@ -2307,7 +2313,7 @@ function customShapes(p5, fn) { * } * *
- * + * *
* * let vid; @@ -2315,28 +2321,28 @@ function customShapes(p5, fn) { * // Load a video and create a p5.MediaElement object. * vid = createVideo('/assets/fingers.mov'); * createCanvas(100, 100, WEBGL); - * + * * // Hide the video. * vid.hide(); - * + * * // Set the video to loop. * vid.loop(); - * + * * describe('A rectangle with video as texture'); * } - * + * * function draw() { * background(0); - * + * * // Rotate around the y-axis. * rotateY(frameCount * 0.01); - * + * * // Set the texture mode. * textureMode(NORMAL); - * + * * // Apply the video as a texture. * texture(vid); - * + * * // Draw a custom shape using uv coordinates. * beginShape(); * vertex(-40, -40, 0, 0); @@ -2489,7 +2495,7 @@ function customShapes(p5, fn) { }; /** - * Stops creating a hole within a flat shape. + * Stops creating a hole within a flat shape. * * The beginContour() and `endContour()` * functions allow for creating negative space within custom shapes that are @@ -2499,10 +2505,10 @@ function customShapes(p5, fn) { * called between beginShape() and * endShape(). * - * By default, + * By default, * the controur has an `OPEN` end, and to close it, * call `endContour(CLOSE)`. The CLOSE contour mode closes splines smoothly. - * + * * Transformations such as translate(), * rotate(), and scale() * don't work between beginContour() and diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 38c0b426e2..8c29e3ea2d 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -1667,11 +1667,9 @@ function primitives3D(p5, fn){ *
*/ Renderer3D.prototype.point = function(x, y, z = 0) { - - const _vertex = []; - _vertex.push(new Vector(x, y, z)); - // TODO - // this._drawPoints(_vertex, this.buffers.point); + this.beginShape(constants.POINTS); + this.vertex(x, y, z); + this.endShape(); return this; }; diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index d04d14839f..0ebb3c0daa 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -52,7 +52,8 @@ class FramebufferTexture { } rawTexture() { - return this.framebuffer[this.property]; + // TODO: handle webgpu texture handle + return { texture: this.framebuffer[this.property] }; } } @@ -586,7 +587,7 @@ class Framebuffer { if (this.useDepth) { this.depth = new FramebufferTexture(this, 'depthTexture'); - const depthFilter = gl.NEAREST; + const depthFilter = constants.NEAREST; this.depthP5Texture = new Texture( this.renderer, this.depth, @@ -600,8 +601,8 @@ class Framebuffer { this.color = new FramebufferTexture(this, 'colorTexture'); const filter = this.textureFiltering === constants.LINEAR - ? gl.LINEAR - : gl.NEAREST; + ? constants.LINEAR + : constants.NEAREST; this.colorP5Texture = new Texture( this.renderer, this.color, @@ -921,7 +922,7 @@ class Framebuffer { */ _deleteTexture(texture) { const gl = this.gl; - gl.deleteTexture(texture.rawTexture()); + gl.deleteTexture(texture.rawTexture().texture); this.renderer.textures.delete(texture); } @@ -1115,12 +1116,17 @@ class Framebuffer { gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.aaFramebuffer); gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer); const partsToCopy = { - colorTexture: [gl.COLOR_BUFFER_BIT, this.colorP5Texture.glMagFilter], + colorTexture: [ + gl.COLOR_BUFFER_BIT, + // TODO: move to renderer + this.colorP5Texture.magFilter === constants.LINEAR ? gl.LINEAR : gl.NEAREST + ], }; if (this.useDepth) { partsToCopy.depthTexture = [ gl.DEPTH_BUFFER_BIT, - this.depthP5Texture.glMagFilter + // TODO: move to renderer + this.depthP5Texture.magFilter === constants.LINEAR ? gl.LINEAR : gl.NEAREST ]; } const [flag, filter] = partsToCopy[property]; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 22e3a481c4..b74fe4c827 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -1419,6 +1419,7 @@ class Geometry { for (let i = 0; i < this.edges.length; i++) { const prevEdge = this.edges[i - 1]; const currEdge = this.edges[i]; + const isPoint = currEdge[0] === currEdge[1]; const begin = this.vertices[currEdge[0]]; const end = this.vertices[currEdge[1]]; const prevColor = (this.vertexStrokeColors.length > 0 && prevEdge) @@ -1439,10 +1440,12 @@ class Geometry { (currEdge[1] + 1) * 4 ) : [0, 0, 0, 0]; - const dir = end - .copy() - .sub(begin) - .normalize(); + const dir = isPoint + ? new Vector(0, 1, 0) + : end + .copy() + .sub(begin) + .normalize(); const dirOK = dir.magSq() > 0; if (dirOK) { this._addSegment(begin, end, fromColor, toColor, dir); @@ -1462,6 +1465,9 @@ class Geometry { this._addJoin(begin, lastValidDir, dir, fromColor); } } + } else if (isPoint) { + this._addCap(begin, dir.copy().mult(-1), fromColor); + this._addCap(begin, dir, fromColor); } else { // Start a new line if (dirOK && !connected.has(currEdge[0])) { @@ -1483,7 +1489,7 @@ class Geometry { }); } } - if (lastValidDir && !connected.has(prevEdge[1])) { + if (!isPoint && lastValidDir && !connected.has(prevEdge[1])) { const existingCap = potentialCaps.get(prevEdge[1]); if (existingCap) { this._addJoin( diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f683075df1..c0d9ef244a 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1,5 +1,13 @@ import * as constants from "../core/constants"; -import { readPixelsWebGL, readPixelWebGL } from './utils'; +import { + getWebGLShaderAttributes, + getWebGLUniformMetadata, + populateGLSLHooks, + readPixelsWebGL, + readPixelWebGL, + setWebGLTextureParams, + setWebGLUniformValue +} from './utils'; import { Renderer3D, getStrokeDefs } from "../core/p5.Renderer3D"; import { Shader } from "./p5.Shader"; import { Texture, MipmapTexture } from "./p5.Texture"; @@ -39,7 +47,7 @@ import filterInvertFrag from "./shaders/filters/invert.frag"; import filterThresholdFrag from "./shaders/filters/threshold.frag"; import filterShaderVert from "./shaders/filters/default.vert"; -const { lineDefs } = getStrokeDefs((n, v) => `#define ${n} ${v};\n`); +const { lineDefs } = getStrokeDefs((n, v) => `#define ${n} ${v}\n`); const defaultShaders = { normalVert, @@ -1219,7 +1227,7 @@ class RendererGL extends Renderer3D { if (!gl.getShaderParameter(vertShader, gl.COMPILE_STATUS)) { throw new Error(`Yikes! An error occurred compiling the vertex shader: ${ gl.getShaderInfoLog(vertShader) - }`); + } in:\n\n${shader.vertSrc()}`); } const fragShader = gl.createShader(gl.FRAGMENT_SHADER); @@ -1250,216 +1258,21 @@ class RendererGL extends Renderer3D { _finalizeShader() {} _getShaderAttributes(shader) { - const attributes = {}; - - const gl = this.GL; - - const numAttributes = gl.getProgramParameter( - shader._glProgram, - gl.ACTIVE_ATTRIBUTES - ); - for (let i = 0; i < numAttributes; ++i) { - const attributeInfo = gl.getActiveAttrib(shader._glProgram, i); - const name = attributeInfo.name; - const location = gl.getAttribLocation(shader._glProgram, name); - const attribute = {}; - attribute.name = name; - attribute.location = location; - attribute.index = i; - attribute.type = attributeInfo.type; - attribute.size = attributeInfo.size; - attributes[name] = attribute; - } - - return attributes; + return getWebGLShaderAttributes(shader, this.GL); } getUniformMetadata(shader) { - const gl = this.GL; - const program = shader._glProgram; - - const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); - const result = []; - - let samplerIndex = 0; - - for (let i = 0; i < numUniforms; ++i) { - const uniformInfo = gl.getActiveUniform(program, i); - const uniform = {}; - uniform.location = gl.getUniformLocation( - program, - uniformInfo.name - ); - uniform.size = uniformInfo.size; - let uniformName = uniformInfo.name; - //uniforms that are arrays have their name returned as - //someUniform[0] which is a bit silly so we trim it - //off here. The size property tells us that its an array - //so we dont lose any information by doing this - if (uniformInfo.size > 1) { - uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); - } - uniform.name = uniformName; - uniform.type = uniformInfo.type; - uniform._cachedData = undefined; - if (uniform.type === gl.SAMPLER_2D) { - uniform.isSampler = true; - uniform.samplerIndex = samplerIndex; - samplerIndex++; - } - - uniform.isArray = - uniformInfo.size > 1 || - uniform.type === gl.FLOAT_MAT3 || - uniform.type === gl.FLOAT_MAT4 || - uniform.type === gl.FLOAT_VEC2 || - uniform.type === gl.FLOAT_VEC3 || - uniform.type === gl.FLOAT_VEC4 || - uniform.type === gl.INT_VEC2 || - uniform.type === gl.INT_VEC4 || - uniform.type === gl.INT_VEC3; - - result.push(uniform); - } - - return result; + return getWebGLUniformMetadata(shader, this.GL); } updateUniformValue(shader, uniform, data) { - const gl = this.GL; - const location = uniform.location; - shader.useProgram(); - - switch (uniform.type) { - case gl.BOOL: - if (data === true) { - gl.uniform1i(location, 1); - } else { - gl.uniform1i(location, 0); - } - break; - case gl.INT: - if (uniform.size > 1) { - data.length && gl.uniform1iv(location, data); - } else { - gl.uniform1i(location, data); - } - break; - case gl.FLOAT: - if (uniform.size > 1) { - data.length && gl.uniform1fv(location, data); - } else { - gl.uniform1f(location, data); - } - break; - case gl.FLOAT_MAT3: - gl.uniformMatrix3fv(location, false, data); - break; - case gl.FLOAT_MAT4: - gl.uniformMatrix4fv(location, false, data); - break; - case gl.FLOAT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2fv(location, data); - } else { - gl.uniform2f(location, data[0], data[1]); - } - break; - case gl.FLOAT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3fv(location, data); - } else { - gl.uniform3f(location, data[0], data[1], data[2]); - } - break; - case gl.FLOAT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4fv(location, data); - } else { - gl.uniform4f(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.INT_VEC2: - if (uniform.size > 1) { - data.length && gl.uniform2iv(location, data); - } else { - gl.uniform2i(location, data[0], data[1]); - } - break; - case gl.INT_VEC3: - if (uniform.size > 1) { - data.length && gl.uniform3iv(location, data); - } else { - gl.uniform3i(location, data[0], data[1], data[2]); - } - break; - case gl.INT_VEC4: - if (uniform.size > 1) { - data.length && gl.uniform4iv(location, data); - } else { - gl.uniform4i(location, data[0], data[1], data[2], data[3]); - } - break; - case gl.SAMPLER_2D: - if (typeof data == 'number') { - if ( - data < gl.TEXTURE0 || - data > gl.TEXTURE31 || - data !== Math.ceil(data) - ) { - console.log( - '🌸 p5.js says: ' + - "You're trying to use a number as the data for a texture." + - 'Please use a texture.' - ); - return this; - } - gl.activeTexture(data); - gl.uniform1i(location, data); - } else { - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - uniform.texture = - data instanceof Texture ? data : this._renderer.getTexture(data); - gl.uniform1i(location, uniform.samplerIndex); - if (uniform.texture.src.gifProperties) { - uniform.texture.src._animateGif(this._renderer._pInst); - } - } - break; - case gl.SAMPLER_CUBE: - case gl.SAMPLER_3D: - case gl.SAMPLER_2D_SHADOW: - case gl.SAMPLER_2D_ARRAY: - case gl.SAMPLER_2D_ARRAY_SHADOW: - case gl.SAMPLER_CUBE_SHADOW: - case gl.INT_SAMPLER_2D: - case gl.INT_SAMPLER_3D: - case gl.INT_SAMPLER_CUBE: - case gl.INT_SAMPLER_2D_ARRAY: - case gl.UNSIGNED_INT_SAMPLER_2D: - case gl.UNSIGNED_INT_SAMPLER_3D: - case gl.UNSIGNED_INT_SAMPLER_CUBE: - case gl.UNSIGNED_INT_SAMPLER_2D_ARRAY: - if (typeof data !== 'number') { - break; - } - if ( - data < gl.TEXTURE0 || - data > gl.TEXTURE31 || - data !== Math.ceil(data) - ) { - console.log( - '🌸 p5.js says: ' + - "You're trying to use a number as the data for a texture." + - 'Please use a texture.' - ); - break; - } - gl.activeTexture(data); - gl.uniform1i(location, data); - break; - //@todo complete all types - } + return setWebGLUniformValue( + shader, + uniform, + data, + (tex) => this.getTexture(tex), + this.GL + ); } _updateTexture(uniform, tex) { @@ -1473,7 +1286,7 @@ class RendererGL extends Renderer3D { bindTexture(tex) { // bind texture using gl context + glTarget and // generated gl texture object - this.GL.bindTexture(this.GL.TEXTURE_2D, tex.getTexture()); + this.GL.bindTexture(this.GL.TEXTURE_2D, tex.getTexture().texture); } unbindTexture() { @@ -1486,7 +1299,7 @@ class RendererGL extends Renderer3D { // accidentally leave a framebuffer bound, causing a feedback loop // when something else tries to write to it const gl = this.GL; - const empty = this._renderer._getEmptyTexture(); + const empty = this._getEmptyTexture(); gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); empty.bindTexture(); gl.uniform1i(uniform.location, uniform.samplerIndex); @@ -1504,13 +1317,11 @@ class RendererGL extends Renderer3D { uploadTextureFromSource({ texture, glFormat, glDataType }, source) { const gl = this.GL; - gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source); } uploadTextureFromData({ texture, glFormat, glDataType }, data, width, height) { const gl = this.GL; - gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D( gl.TEXTURE_2D, 0, @@ -1537,95 +1348,7 @@ class RendererGL extends Renderer3D { } setTextureParams(texture) { - const gl = this.GL; - texture.bindTexture(); - const glMinFilter = texture.minFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; - const glMagFilter = texture.magFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; - - // for webgl 1 we need to check if the texture is power of two - // if it isn't we will set the wrap mode to CLAMP - // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet - const isPowerOfTwo = x => (x & (x - 1)) === 0; - const textureData = texture._getTextureDataFromSource(); - - let wrapWidth; - let wrapHeight; - - if (textureData.naturalWidth && textureData.naturalHeight) { - wrapWidth = textureData.naturalWidth; - wrapHeight = textureData.naturalHeight; - } else { - wrapWidth = this.width; - wrapHeight = this.height; - } - - const widthPowerOfTwo = isPowerOfTwo(wrapWidth); - const heightPowerOfTwo = isPowerOfTwo(wrapHeight); - let glWrapS, glWrapT; - - if (texture.wrapS === constants.REPEAT) { - if ( - this.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - glWrapS = gl.REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' - ); - glWrapS = gl.CLAMP_TO_EDGE; - } - } else if (texture.wrapS === constants.MIRROR) { - if ( - this.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - glWrapS = gl.MIRRORED_REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' - ); - glWrapS = gl.CLAMP_TO_EDGE; - } - } else { - // falling back to default if didn't get a proper mode - glWrapS = gl.CLAMP_TO_EDGE; - } - - if (texture.wrapT === constants.REPEAT) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - glWrapT = gl.REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' - ); - glWrapT = gl.CLAMP_TO_EDGE; - } - } else if (texture.wrapT === constants.MIRROR) { - if ( - this._renderer.webglVersion === constants.WEBGL2 || - (widthPowerOfTwo && heightPowerOfTwo) - ) { - glWrapT = gl.MIRRORED_REPEAT; - } else { - console.warn( - 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' - ); - glWrapT = gl.CLAMP_TO_EDGE; - } - } else { - // falling back to default if didn't get a proper mode - glWrapT = gl.CLAMP_TO_EDGE; - } - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, glMinFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, glMagFilter); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, glWrapS); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, glWrapT); - texture.unbindTexture(); + return setWebGLTextureParams(texture, this.GL, this.webglVersion); } deleteTexture({ texture }) { @@ -1677,48 +1400,7 @@ class RendererGL extends Renderer3D { // Shader hooks ////////////////////////////////////////////// populateHooks(shader, src, shaderType) { - const main = 'void main'; - if (!src.includes(main)) return src; - - let [preMain, postMain] = src.split(main); - - let hooks = ''; - let defines = ''; - for (const key in shader.hooks.uniforms) { - hooks += `uniform ${key};\n`; - } - if (shader.hooks.declarations) { - hooks += shader.hooks.declarations + '\n'; - } - if (shader.hooks[shaderType].declarations) { - hooks += shader.hooks[shaderType].declarations + '\n'; - } - for (const hookDef in shader.hooks.helpers) { - hooks += `${hookDef}${shader.hooks.helpers[hookDef]}\n`; - } - for (const hookDef in shader.hooks[shaderType]) { - if (hookDef === 'declarations') continue; - const [hookType, hookName] = hookDef.split(' '); - - // Add a #define so that if the shader wants to use preprocessor directives to - // optimize away the extra function calls in main, it can do so - if (shader.hooks.modified[shaderType][hookDef]) { - defines += '#define AUGMENTED_HOOK_' + hookName + '\n'; - } - - hooks += - hookType + ' HOOK_' + hookName + shader.hooks[shaderType][hookDef] + '\n'; - } - - // Allow shaders to specify the location of hook #define statements. Normally these - // go after function definitions, but one might want to have them defined earlier - // in order to only conditionally make uniforms. - if (preMain.indexOf('#define HOOK_DEFINES') !== -1) { - preMain = preMain.replace('#define HOOK_DEFINES', '\n' + defines + '\n'); - defines = ''; - } - - return preMain + '\n' + defines + hooks + main + postMain; + return populateGLSLHooks(shader, src, shaderType); } } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 0134701c9c..9a506a2801 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -709,7 +709,17 @@ class Shader { for (const uniform of this.samplers) { let tex = uniform.texture; - if (tex === undefined) { + if ( + tex === undefined || + ( + // Make sure we unbind a framebuffer uniform if it's the same + // framebuffer that is actvely being drawn to in order to + // prevent a feedback cycle + tex.isFramebufferTexture && + !tex.src.framebuffer.antialias && + tex.src.framebuffer === this._renderer.activeFramebuffer() + ) + ) { // user hasn't yet supplied a texture for this slot. // (or there may not be one--maybe just lighting), // so we supply a default texture instead. @@ -1026,7 +1036,7 @@ class Shader { } if (attr.location !== -1) { - this._renderer._enableAttrib(attr, size, type, normalized, stride, offset); + this._renderer._enableAttrib(this, attr, size, type, normalized, stride, offset); } } return this; diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index b9519cb606..e7103bd13c 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -130,7 +130,7 @@ class Texture { }); } - this._renderer.setTextureParams(this.textureHandle, { + this._renderer.setTextureParams(this, { minFilter: this.minFilter, magFilter: this.magFilter, wrapS: this.wrapS, @@ -179,14 +179,13 @@ class Texture { if (this._shouldUpdate(textureData)) { this.bindTexture(); this._renderer.uploadTextureFromSource(this.textureHandle, textureData); - this.unbindTexture(); updated = true; } return updated; } - shouldUpdate(textureData) { + _shouldUpdate(textureData) { const data = this.src; if (data.width === 0 || data.height === 0) { return false; // nothing to do! @@ -280,7 +279,7 @@ class Texture { if (this.isFramebufferTexture) { return this.src.rawTexture(); } else { - return this.glTex; + return this.textureHandle; } } diff --git a/src/webgl/shaders/line.vert b/src/webgl/shaders/line.vert index a00bf94ba8..65cd9502c6 100644 --- a/src/webgl/shaders/line.vert +++ b/src/webgl/shaders/line.vert @@ -127,7 +127,7 @@ void main() { inputs.tangentOut = (uModelViewMatrix * vec4(aTangentOut, 0.)).xyz; #endif #ifdef AUGMENTED_HOOK_getCameraInputs - inputs = hook_getCameraInputs(inputs); + inputs = HOOK_getCameraInputs(inputs); #endif vec4 posp = vec4(inputs.position, 1.); diff --git a/src/webgl/utils.js b/src/webgl/utils.js index b891e96d0b..70766ac522 100644 --- a/src/webgl/utils.js +++ b/src/webgl/utils.js @@ -1,3 +1,6 @@ +import * as constants from '../core/constants'; +import { Texture } from './p5.Texture'; + /** * @private * @param {Uint8Array|Float32Array|undefined} pixels An existing pixels array to reuse if the size is the same @@ -97,3 +100,351 @@ export function readPixelWebGL(gl, framebuffer, x, y, format, type, flipY) { return Array.from(pixels); } + +export function setWebGLTextureParams(texture, gl, webglVersion) { + texture.bindTexture(); + const glMinFilter = texture.minFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; + const glMagFilter = texture.magFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; + + // for webgl 1 we need to check if the texture is power of two + // if it isn't we will set the wrap mode to CLAMP + // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet + const isPowerOfTwo = x => (x & (x - 1)) === 0; + const textureData = texture._getTextureDataFromSource(); + + let wrapWidth; + let wrapHeight; + + if (textureData.naturalWidth && textureData.naturalHeight) { + wrapWidth = textureData.naturalWidth; + wrapHeight = textureData.naturalHeight; + } else { + wrapWidth = texture.width; + wrapHeight = texture.height; + } + + const widthPowerOfTwo = isPowerOfTwo(wrapWidth); + const heightPowerOfTwo = isPowerOfTwo(wrapHeight); + let glWrapS, glWrapT; + + if (texture.wrapS === constants.REPEAT) { + if ( + webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + glWrapS = gl.REPEAT; + } else { + console.warn( + 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' + ); + glWrapS = gl.CLAMP_TO_EDGE; + } + } else if (texture.wrapS === constants.MIRROR) { + if ( + webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + glWrapS = gl.MIRRORED_REPEAT; + } else { + console.warn( + 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' + ); + glWrapS = gl.CLAMP_TO_EDGE; + } + } else { + // falling back to default if didn't get a proper mode + glWrapS = gl.CLAMP_TO_EDGE; + } + + if (texture.wrapT === constants.REPEAT) { + if ( + webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + glWrapT = gl.REPEAT; + } else { + console.warn( + 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' + ); + glWrapT = gl.CLAMP_TO_EDGE; + } + } else if (texture.wrapT === constants.MIRROR) { + if ( + webglVersion === constants.WEBGL2 || + (widthPowerOfTwo && heightPowerOfTwo) + ) { + glWrapT = gl.MIRRORED_REPEAT; + } else { + console.warn( + 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' + ); + glWrapT = gl.CLAMP_TO_EDGE; + } + } else { + // falling back to default if didn't get a proper mode + glWrapT = gl.CLAMP_TO_EDGE; + } + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, glMinFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, glMagFilter); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, glWrapS); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, glWrapT); + texture.unbindTexture(); +} + +export function setWebGLUniformValue(shader, uniform, data, getTexture, gl) { + const location = uniform.location; + shader.useProgram(); + + switch (uniform.type) { + case gl.BOOL: + if (data === true) { + gl.uniform1i(location, 1); + } else { + gl.uniform1i(location, 0); + } + break; + case gl.INT: + if (uniform.size > 1) { + data.length && gl.uniform1iv(location, data); + } else { + gl.uniform1i(location, data); + } + break; + case gl.FLOAT: + if (uniform.size > 1) { + data.length && gl.uniform1fv(location, data); + } else { + gl.uniform1f(location, data); + } + break; + case gl.FLOAT_MAT3: + gl.uniformMatrix3fv(location, false, data); + break; + case gl.FLOAT_MAT4: + gl.uniformMatrix4fv(location, false, data); + break; + case gl.FLOAT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2fv(location, data); + } else { + gl.uniform2f(location, data[0], data[1]); + } + break; + case gl.FLOAT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3fv(location, data); + } else { + gl.uniform3f(location, data[0], data[1], data[2]); + } + break; + case gl.FLOAT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4fv(location, data); + } else { + gl.uniform4f(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.INT_VEC2: + if (uniform.size > 1) { + data.length && gl.uniform2iv(location, data); + } else { + gl.uniform2i(location, data[0], data[1]); + } + break; + case gl.INT_VEC3: + if (uniform.size > 1) { + data.length && gl.uniform3iv(location, data); + } else { + gl.uniform3i(location, data[0], data[1], data[2]); + } + break; + case gl.INT_VEC4: + if (uniform.size > 1) { + data.length && gl.uniform4iv(location, data); + } else { + gl.uniform4i(location, data[0], data[1], data[2], data[3]); + } + break; + case gl.SAMPLER_2D: + if (typeof data == 'number') { + if ( + data < gl.TEXTURE0 || + data > gl.TEXTURE31 || + data !== Math.ceil(data) + ) { + console.log( + '🌸 p5.js says: ' + + "You're trying to use a number as the data for a texture." + + 'Please use a texture.' + ); + return this; + } + gl.activeTexture(data); + gl.uniform1i(location, data); + } else { + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + uniform.texture = + data instanceof Texture ? data : getTexture(data); + gl.uniform1i(location, uniform.samplerIndex); + if (uniform.texture.src.gifProperties) { + uniform.texture.src._animateGif(this._pInst); + } + } + break; + case gl.SAMPLER_CUBE: + case gl.SAMPLER_3D: + case gl.SAMPLER_2D_SHADOW: + case gl.SAMPLER_2D_ARRAY: + case gl.SAMPLER_2D_ARRAY_SHADOW: + case gl.SAMPLER_CUBE_SHADOW: + case gl.INT_SAMPLER_2D: + case gl.INT_SAMPLER_3D: + case gl.INT_SAMPLER_CUBE: + case gl.INT_SAMPLER_2D_ARRAY: + case gl.UNSIGNED_INT_SAMPLER_2D: + case gl.UNSIGNED_INT_SAMPLER_3D: + case gl.UNSIGNED_INT_SAMPLER_CUBE: + case gl.UNSIGNED_INT_SAMPLER_2D_ARRAY: + if (typeof data !== 'number') { + break; + } + if ( + data < gl.TEXTURE0 || + data > gl.TEXTURE31 || + data !== Math.ceil(data) + ) { + console.log( + '🌸 p5.js says: ' + + "You're trying to use a number as the data for a texture." + + 'Please use a texture.' + ); + break; + } + gl.activeTexture(data); + gl.uniform1i(location, data); + break; + //@todo complete all types + } +} + +export function getWebGLUniformMetadata(shader, gl) { + const program = shader._glProgram; + + const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + const result = []; + + let samplerIndex = 0; + + for (let i = 0; i < numUniforms; ++i) { + const uniformInfo = gl.getActiveUniform(program, i); + const uniform = {}; + uniform.location = gl.getUniformLocation( + program, + uniformInfo.name + ); + uniform.size = uniformInfo.size; + let uniformName = uniformInfo.name; + //uniforms that are arrays have their name returned as + //someUniform[0] which is a bit silly so we trim it + //off here. The size property tells us that its an array + //so we dont lose any information by doing this + if (uniformInfo.size > 1) { + uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); + } + uniform.name = uniformName; + uniform.type = uniformInfo.type; + uniform._cachedData = undefined; + if (uniform.type === gl.SAMPLER_2D) { + uniform.isSampler = true; + uniform.samplerIndex = samplerIndex; + samplerIndex++; + } + + uniform.isArray = + uniformInfo.size > 1 || + uniform.type === gl.FLOAT_MAT3 || + uniform.type === gl.FLOAT_MAT4 || + uniform.type === gl.FLOAT_VEC2 || + uniform.type === gl.FLOAT_VEC3 || + uniform.type === gl.FLOAT_VEC4 || + uniform.type === gl.INT_VEC2 || + uniform.type === gl.INT_VEC4 || + uniform.type === gl.INT_VEC3; + + result.push(uniform); + } + + return result; +} + +export function getWebGLShaderAttributes(shader, gl) { + const attributes = {}; + + const numAttributes = gl.getProgramParameter( + shader._glProgram, + gl.ACTIVE_ATTRIBUTES + ); + for (let i = 0; i < numAttributes; ++i) { + const attributeInfo = gl.getActiveAttrib(shader._glProgram, i); + const name = attributeInfo.name; + const location = gl.getAttribLocation(shader._glProgram, name); + const attribute = {}; + attribute.name = name; + attribute.location = location; + attribute.index = i; + attribute.type = attributeInfo.type; + attribute.size = attributeInfo.size; + attributes[name] = attribute; + } + + return attributes; +} + +export function populateGLSLHooks(shader, src, shaderType) { + const main = 'void main'; + if (!src.includes(main)) return src; + + let [preMain, postMain] = src.split(main); + + let hooks = ''; + let defines = ''; + for (const key in shader.hooks.uniforms) { + hooks += `uniform ${key};\n`; + } + if (shader.hooks.declarations) { + hooks += shader.hooks.declarations + '\n'; + } + if (shader.hooks[shaderType].declarations) { + hooks += shader.hooks[shaderType].declarations + '\n'; + } + for (const hookDef in shader.hooks.helpers) { + hooks += `${hookDef}${shader.hooks.helpers[hookDef]}\n`; + } + for (const hookDef in shader.hooks[shaderType]) { + if (hookDef === 'declarations') continue; + const [hookType, hookName] = hookDef.split(' '); + + // Add a #define so that if the shader wants to use preprocessor directives to + // optimize away the extra function calls in main, it can do so + if ( + shader.hooks.modified.vertex[hookDef] || + shader.hooks.modified.fragment[hookDef] + ) { + defines += '#define AUGMENTED_HOOK_' + hookName + '\n'; + } + + hooks += + hookType + ' HOOK_' + hookName + shader.hooks[shaderType][hookDef] + '\n'; + } + + // Allow shaders to specify the location of hook #define statements. Normally these + // go after function definitions, but one might want to have them defined earlier + // in order to only conditionally make uniforms. + if (preMain.indexOf('#define HOOK_DEFINES') !== -1) { + preMain = preMain.replace('#define HOOK_DEFINES', '\n' + defines + '\n'); + defines = ''; + } + + return preMain + '\n' + defines + hooks + main + postMain; +} diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 93d66c790a..34b64abdfd 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -107,10 +107,9 @@ suite('p5.RendererGL', function() { // Make a red texture const tex = myp5.createFramebuffer(); tex.draw(() => myp5.background('red')); - console.log(tex.get().canvas.toDataURL()); myp5.shader(myShader); - myp5.fill('red') + myp5.fill('blue') myp5.noStroke(); myShader.setUniform('myTex', tex); diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 14bac25ce4..7dfe0e6e82 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -5,7 +5,8 @@ const plugins = [ vitePluginString({ include: [ 'src/webgl/shaders/**/*' - ] + ], + compress: false, }) ]; From aef17d5c6f2ef15dddc5c2663a2304ae982cd4ec Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 17 Jun 2025 19:57:12 -0400 Subject: [PATCH 29/98] Fix the rest of the tests! --- src/image/filterRenderer2D.js | 4 +- src/webgl/p5.Texture.js | 4 +- test/unit/webgl/light.js | 1 + test/unit/webgl/p5.Framebuffer.js | 12 +++--- test/unit/webgl/p5.Shader.js | 1 - test/unit/webgl/p5.Texture.js | 64 +++++++++++++++++-------------- 6 files changed, 47 insertions(+), 39 deletions(-) diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index cfdf10eb8d..e2a5aa1f5d 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -60,8 +60,8 @@ class FilterRenderer2D { _emptyTexture: null, webglVersion, states: { - textureWrapX: this.gl.CLAMP_TO_EDGE, - textureWrapY: this.gl.CLAMP_TO_EDGE, + textureWrapX: constants.CLAMP, + textureWrapY: constants.CLAMP, }, _arraysEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b), _getEmptyTexture: () => { diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index e7103bd13c..c88389bb8e 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -23,8 +23,8 @@ class Texture { this.format = settings.format || 'rgba8unorm'; this.minFilter = settings.minFilter || constants.LINEAR; this.magFilter = settings.magFilter || constants.LINEAR; - this.wrapS = settings.wrapS || constants.CLAMP; - this.wrapT = settings.wrapT || constants.CLAMP; + this.wrapS = settings.wrapS || renderer.states.textureWrapX; + this.wrapT = settings.wrapT || renderer.states.textureWrapY; this.dataType = settings.dataType || 'uint8'; this.textureHandle = null; diff --git a/test/unit/webgl/light.js b/test/unit/webgl/light.js index 3f8785a5c9..38aa248003 100644 --- a/test/unit/webgl/light.js +++ b/test/unit/webgl/light.js @@ -67,6 +67,7 @@ suite('light', function() { }); suite('spotlight inputs', function() { + beforeEach(() => myp5.noLights()); let angle = Math.PI / 4; let defaultAngle = Math.cos(Math.PI / 3); let cosAngle = Math.cos(angle); diff --git a/test/unit/webgl/p5.Framebuffer.js b/test/unit/webgl/p5.Framebuffer.js index 8d52a1668c..f97cb6b57d 100644 --- a/test/unit/webgl/p5.Framebuffer.js +++ b/test/unit/webgl/p5.Framebuffer.js @@ -156,7 +156,7 @@ suite('p5.Framebuffer', function() { expect(fbo.density).to.equal(1); // The texture should not be recreated - expect(fbo.color.rawTexture()).to.equal(oldTexture); + expect(fbo.color.rawTexture().texture).to.equal(oldTexture.texture); }); test('manually-sized framebuffers can be made auto-sized', function() { @@ -216,7 +216,7 @@ suite('p5.Framebuffer', function() { expect(fbo.density).to.equal(2); // The texture should not be recreated - expect(fbo.color.rawTexture()).to.equal(oldTexture); + expect(fbo.color.rawTexture().texture).to.equal(oldTexture.texture); }); test('resizes the framebuffer by createFramebuffer based on max texture size', function() { @@ -638,10 +638,10 @@ suite('p5.Framebuffer', function() { }); assert.equal( - fbo.color.framebuffer.colorP5Texture.glMinFilter, fbo.gl.NEAREST + fbo.color.framebuffer.colorP5Texture.minFilter, myp5.NEAREST ); assert.equal( - fbo.color.framebuffer.colorP5Texture.glMagFilter, fbo.gl.NEAREST + fbo.color.framebuffer.colorP5Texture.magFilter, myp5.NEAREST ); }); test('can create a framebuffer that uses LINEAR texture filtering', @@ -651,10 +651,10 @@ suite('p5.Framebuffer', function() { const fbo = myp5.createFramebuffer({}); assert.equal( - fbo.color.framebuffer.colorP5Texture.glMinFilter, fbo.gl.LINEAR + fbo.color.framebuffer.colorP5Texture.minFilter, myp5.LINEAR ); assert.equal( - fbo.color.framebuffer.colorP5Texture.glMagFilter, fbo.gl.LINEAR + fbo.color.framebuffer.colorP5Texture.magFilter, myp5.LINEAR ); }); }); diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 00d4d00847..7a7d35021b 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -67,7 +67,6 @@ suite('p5.Shader', function() { 'uModelViewMatrix', 'uProjectionMatrix', 'uNormalMatrix', - 'uAmbientLightCount', 'uDirectionalLightCount', 'uPointLightCount', 'uAmbientColor', diff --git a/test/unit/webgl/p5.Texture.js b/test/unit/webgl/p5.Texture.js index 80512f0e49..60058b302d 100644 --- a/test/unit/webgl/p5.Texture.js +++ b/test/unit/webgl/p5.Texture.js @@ -67,6 +67,13 @@ suite('p5.Texture', function() { }; suite('p5.Texture', function() { + let texParamSpy; + beforeEach(() => { + texParamSpy = vi.spyOn(myp5._renderer.GL, 'texParameteri'); + }); + afterEach(() => { + vi.restoreAllMocks(); + }); test('Create and cache a single texture with p5.Image', function() { testTextureSet(texImg1); }); @@ -79,56 +86,57 @@ suite('p5.Texture', function() { test('Set filter mode to linear', function() { var tex = myp5._renderer.getTexture(texImg2); tex.setInterpolation(myp5.LINEAR, myp5.LINEAR); - assert.deepEqual(tex.glMinFilter, myp5._renderer.GL.LINEAR); - assert.deepEqual(tex.glMagFilter, myp5._renderer.GL.LINEAR); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_MIN_FILTER, myp5._renderer.GL.LINEAR); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_MAG_FILTER, myp5._renderer.GL.LINEAR); }); test('Set filter mode to nearest', function() { var tex = myp5._renderer.getTexture(texImg2); tex.setInterpolation(myp5.NEAREST, myp5.NEAREST); - assert.deepEqual(tex.glMinFilter, myp5._renderer.GL.NEAREST); - assert.deepEqual(tex.glMagFilter, myp5._renderer.GL.NEAREST); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_MIN_FILTER, myp5._renderer.GL.NEAREST); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_MAG_FILTER, myp5._renderer.GL.NEAREST); }); test('Set wrap mode to clamp', function() { var tex = myp5._renderer.getTexture(texImg2); tex.setWrapMode(myp5.CLAMP, myp5.CLAMP); - assert.deepEqual(tex.glWrapS, myp5._renderer.GL.CLAMP_TO_EDGE); - assert.deepEqual(tex.glWrapT, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.CLAMP_TO_EDGE); }); test('Set wrap mode to repeat', function() { var tex = myp5._renderer.getTexture(texImg2); tex.setWrapMode(myp5.REPEAT, myp5.REPEAT); - assert.deepEqual(tex.glWrapS, myp5._renderer.GL.REPEAT); - assert.deepEqual(tex.glWrapT, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.REPEAT); }); test('Set wrap mode to mirror', function() { var tex = myp5._renderer.getTexture(texImg2); tex.setWrapMode(myp5.MIRROR, myp5.MIRROR); - assert.deepEqual(tex.glWrapS, myp5._renderer.GL.MIRRORED_REPEAT); - assert.deepEqual(tex.glWrapT, myp5._renderer.GL.MIRRORED_REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.MIRRORED_REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.MIRRORED_REPEAT); }); test('Set wrap mode REPEAT if src dimensions is powerOfTwo', function() { const tex = myp5._renderer.getTexture(imgElementPowerOfTwo); tex.setWrapMode(myp5.REPEAT, myp5.REPEAT); - assert.deepEqual(tex.glWrapS, myp5._renderer.GL.REPEAT); - assert.deepEqual(tex.glWrapT, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.REPEAT); }); test( 'Set default wrap mode REPEAT if WEBGL2 and src dimensions != powerOfTwo', function() { const tex = myp5._renderer.getTexture(imgElementNotPowerOfTwo); tex.setWrapMode(myp5.REPEAT, myp5.REPEAT); - assert.deepEqual(tex.glWrapS, myp5._renderer.GL.REPEAT); - assert.deepEqual(tex.glWrapT, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.REPEAT); } ); test( 'Set default wrap mode CLAMP if WEBGL1 and src dimensions != powerOfTwo', function() { myp5.setAttributes({ version: 1 }); + texParamSpy = vi.spyOn(myp5._renderer.GL, 'texParameteri'); const tex = myp5._renderer.getTexture(imgElementNotPowerOfTwo); tex.setWrapMode(myp5.REPEAT, myp5.REPEAT); - assert.deepEqual(tex.glWrapS, myp5._renderer.GL.CLAMP_TO_EDGE); - assert.deepEqual(tex.glWrapT, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.CLAMP_TO_EDGE); } ); test('Set textureMode to NORMAL', function() { @@ -143,28 +151,28 @@ suite('p5.Texture', function() { myp5.textureWrap(myp5.CLAMP); var tex1 = myp5._renderer.getTexture(texImg1); var tex2 = myp5._renderer.getTexture(texImg2); - assert.deepEqual(tex1.glWrapS, myp5._renderer.GL.CLAMP_TO_EDGE); - assert.deepEqual(tex1.glWrapT, myp5._renderer.GL.CLAMP_TO_EDGE); - assert.deepEqual(tex2.glWrapS, myp5._renderer.GL.CLAMP_TO_EDGE); - assert.deepEqual(tex2.glWrapT, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.CLAMP_TO_EDGE); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.CLAMP_TO_EDGE); }); test('Set global wrap mode to repeat', function() { myp5.textureWrap(myp5.REPEAT); var tex1 = myp5._renderer.getTexture(texImg1); var tex2 = myp5._renderer.getTexture(texImg2); - assert.deepEqual(tex1.glWrapS, myp5._renderer.GL.REPEAT); - assert.deepEqual(tex1.glWrapT, myp5._renderer.GL.REPEAT); - assert.deepEqual(tex2.glWrapS, myp5._renderer.GL.REPEAT); - assert.deepEqual(tex2.glWrapT, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.REPEAT); }); test('Set global wrap mode to mirror', function() { myp5.textureWrap(myp5.MIRROR); var tex1 = myp5._renderer.getTexture(texImg1); var tex2 = myp5._renderer.getTexture(texImg2); - assert.deepEqual(tex1.glWrapS, myp5._renderer.GL.MIRRORED_REPEAT); - assert.deepEqual(tex1.glWrapT, myp5._renderer.GL.MIRRORED_REPEAT); - assert.deepEqual(tex2.glWrapS, myp5._renderer.GL.MIRRORED_REPEAT); - assert.deepEqual(tex2.glWrapT, myp5._renderer.GL.MIRRORED_REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.MIRRORED_REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.MIRRORED_REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_S, myp5._renderer.GL.MIRRORED_REPEAT); + expect(texParamSpy).toHaveBeenCalledWith(myp5._renderer.GL.TEXTURE_2D, myp5._renderer.GL.TEXTURE_WRAP_T, myp5._renderer.GL.MIRRORED_REPEAT); }); test('Handles changes to p5.Image size', function() { const tex = myp5._renderer.getTexture(texImg2); From abc40743d912e10aac4db2307fd42ddea1f36d13 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Wed, 25 Jun 2025 19:50:06 -0400 Subject: [PATCH 30/98] Get textures working --- preview/index.html | 34 +++++++++--- src/core/p5.Renderer3D.js | 16 ++++++ src/webgl/p5.RendererGL.js | 16 ------ src/webgl/p5.Shader.js | 1 + src/webgpu/p5.RendererWebGPU.js | 95 +++++++++++++++++++++++---------- src/webgpu/shaders/material.js | 9 +++- 6 files changed, 119 insertions(+), 52 deletions(-) diff --git a/preview/index.html b/preview/index.html index 99a5bc38d5..6e4915ab34 100644 --- a/preview/index.html +++ b/preview/index.html @@ -26,42 +26,59 @@ let fbo; let sh; let ssh; + let tex; p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); + + tex = p.createImage(100, 100); + tex.loadPixels(); + for (let x = 0; x < tex.width; x++) { + for (let y = 0; y < tex.height; y++) { + const off = (x + y * tex.width) * 4; + tex.pixels[off] = p.round((x / tex.width) * 255); + tex.pixels[off + 1] = p.round((y / tex.height) * 255); + tex.pixels[off + 2] = 0; + tex.pixels[off + 3] = 255; + } + } + tex.updatePixels(); + sh = p.baseMaterialShader().modify({ uniforms: { 'f32 time': () => p.millis(), }, 'Vertex getWorldInputs': `(inputs: Vertex) { var result = inputs; - result.position.y += 40.0 * sin(uniforms.time * 0.01); + result.position.y += 40.0 * sin(uniforms.time * 0.005); return result; }`, }) - ssh = p.baseStrokeShader().modify({ + /*ssh = p.baseStrokeShader().modify({ uniforms: { 'f32 time': () => p.millis(), }, 'StrokeVertex getWorldInputs': `(inputs: StrokeVertex) { var result = inputs; - result.position.y += 40.0 * sin(uniforms.time * 0.01); + result.position.y += 40.0 * sin(uniforms.time * 0.005); return result; }`, - }) + })*/ }; p.draw = function () { - const t = p.millis() * 0.008; + p.orbitControl(); + const t = p.millis() * 0.002; p.background(200); p.shader(sh); - p.strokeShader(ssh) - p.ambientLight(50); + // p.strokeShader(ssh) + p.ambientLight(150); p.directionalLight(100, 100, 100, 0, 1, -1); p.pointLight(155, 155, 155, 0, -200, 500); p.specularMaterial(255); p.shininess(300); - p.stroke('white') + //p.stroke('white'); + p.noStroke(); for (const [i, c] of ['red', 'lime', 'blue'].entries()) { p.push(); p.fill(c); @@ -70,6 +87,7 @@ 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), ) + p.texture(tex) p.sphere(30); p.pop(); } diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index c437f34725..f43326b908 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1586,6 +1586,22 @@ export class Renderer3D extends Renderer { return this._emptyTexture; } + getTexture(input) { + let src = input; + if (src instanceof Framebuffer) { + src = src.color; + } + + const texture = this.textures.get(src); + if (texture) { + return texture; + } + + const tex = new Texture(this, src); + this.textures.set(src, tex); + return tex; + } + ////////////////////////////////////////////// // Buffers ////////////////////////////////////////////// diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index c0d9ef244a..9025c0d31a 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -970,22 +970,6 @@ class RendererGL extends Renderer3D { return code; } - getTexture(input) { - let src = input; - if (src instanceof Framebuffer) { - src = src.color; - } - - const texture = this.textures.get(src); - if (texture) { - return texture; - } - - const tex = new Texture(this, src); - this.textures.set(src, tex); - return tex; - } - // TODO move to super class /* * used in imageLight, diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 9a506a2801..2a95af1f17 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -708,6 +708,7 @@ class Shader { const empty = this._renderer._getEmptyTexture(); for (const uniform of this.samplers) { + if (uniform.noData) continue; let tex = uniform.texture; if ( tex === undefined || diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 0b9dd6fcfa..8d434cc725 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1,5 +1,6 @@ import { Renderer3D, getStrokeDefs } from '../core/p5.Renderer3D'; import { Shader } from '../webgl/p5.Shader'; +import { Texture } from '../webgl/p5.Texture'; import * as constants from '../core/constants'; @@ -252,7 +253,7 @@ class RendererWebGPU extends Renderer3D { _finalizeShader(shader) { const rawSize = Math.max( 0, - ...Object.values(shader.uniforms).map(u => u.offsetEnd) + ...Object.values(shader.uniforms).filter(u => !u.isSampler).map(u => u.offsetEnd) ); const alignedSize = Math.ceil(rawSize / 16) * 16; shader._uniformData = new Float32Array(alignedSize / 4); @@ -277,14 +278,17 @@ class RendererWebGPU extends Renderer3D { for (const sampler of shader.samplers) { const group = sampler.group; const entries = groupEntries.get(group) || []; + if (!['sampler', 'texture_2d'].includes(sampler.type)) { + throw new Error(`Unsupported texture type: ${sampler.type}`); + } entries.push({ binding: sampler.binding, - visibility: GPUShaderStage.FRAGMENT, + visibility: sampler.visibility, sampler: sampler.type === 'sampler' ? { type: 'filtering' } : undefined, - texture: sampler.type === 'texture' + texture: sampler.type === 'texture_2d' ? { sampleType: 'float', viewDimension: '2d' } : undefined, uniform: sampler, @@ -300,6 +304,7 @@ class RendererWebGPU extends Renderer3D { } shader._groupEntries = groupEntries; + console.log(shader._groupEntries); shader._bindGroupLayouts = [...bindGroupLayouts.values()]; shader._pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: shader._bindGroupLayouts, @@ -617,11 +622,17 @@ class RendererWebGPU extends Renderer3D { }; } + if (!entry.uniform.isSampler) { + throw new Error( + 'All non-texture/sampler uniforms should be in the uniform struct!' + ); + } + return { binding: entry.binding, - resource: sampler.type === 'sampler' - ? sampler.uniform._cachedData.getSampler() - : sampler.uniform.textureHandle.view, + resource: entry.uniform.type === 'sampler' + ? (entry.uniform.textureSource.texture || this._getEmptyTexture()).getSampler() + : (entry.uniform.texture || this._getEmptyTexture()).textureHandle.view, }; }); @@ -799,29 +810,59 @@ class RendererWebGPU extends Renderer3D { const structType = uniformVarMatch[2]; const uniforms = this._parseStruct(shader.vertSrc(), structType); // Extract samplers from group bindings - const samplers = []; - const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(\w+);/g; - let match; - while ((match = samplerRegex.exec(shader._vertSrc)) !== null) { - const [_, group, binding, name, type] = match; - const groupIndex = parseInt(group); - // We're currently reserving group 0 for non-sampler stuff, which we parse - // above, so we can skip it here while we grab the remaining sampler - // uniforms - if (groupIndex === 0) continue; - - samplers.push({ - group: groupIndex, - binding: parseInt(binding), - name, - type, // e.g., 'sampler', 'texture_2d' - sampler: true, - }); + const samplers = {}; + // TODO: support other texture types + const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler);/g; + for (const [src, visibility] of [ + [shader._vertSrc, GPUShaderStage.VERTEX], + [shader._fragSrc, GPUShaderStage.FRAGMENT] + ]) { + let match; + while ((match = samplerRegex.exec(src)) !== null) { + const [_, group, binding, name, type] = match; + const groupIndex = parseInt(group); + const bindingIndex = parseInt(binding); + // We're currently reserving group 0 for non-sampler stuff, which we parse + // above, so we can skip it here while we grab the remaining sampler + // uniforms + if (groupIndex === 0 && bindingIndex === 0) 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; + } + } } - return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...samplers]; + return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; } - updateUniformValue(_shader, _uniform, _data) {} + updateUniformValue(_shader, uniform, data) { + if (uniform.isSampler) { + uniform.texture = + data instanceof Texture ? data : this.getTexture(data); + } + } _updateTexture(uniform, tex) { tex.update(); @@ -879,7 +920,7 @@ class RendererWebGPU extends Renderer3D { magFilter: constantMapping[texture.magFilter], minFilter: constantMapping[texture.minFilter], addressModeU: constantMapping[texture.wrapS], - addressModeV: constantMapping[params.addressModeV], + addressModeV: constantMapping[texture.wrapT], }); this.samplers.set(key, sampler); return sampler; diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 9722daad06..774f131bce 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -145,6 +145,9 @@ struct FragmentInput { ${uniforms} @group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var uSampler: texture_2d; +@group(0) @binding(2) var uSampler_sampler: sampler; + struct ColorComponents { baseColor: vec3, opacity: f32, @@ -305,7 +308,11 @@ fn totalLight( fn main(input: FragmentInput) -> @location(0) vec4 { HOOK_beforeFragment(); - let color = input.vColor; // TODO: check isTexture and apply tint + let color = select( + input.vColor, + getTexture(uSampler, uSampler_sampler, input.vTexCoord) * (uniforms.uTint/255.0), + uniforms.isTexture == 1 + ); // TODO: check isTexture and apply tint var inputs = Inputs( normalize(input.vNormal), input.vTexCoord, From 397c1d82a7deafcfd1881b80dc6b36873d51dd33 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 26 Jun 2025 07:53:18 -0400 Subject: [PATCH 31/98] Add visual tests --- src/webgpu/p5.RendererWebGPU.js | 3 +- test/unit/visual/cases/webgpu.js | 108 ++++++++++++++++++ .../Shaders/Shader hooks can be used/000.png | Bin 0 -> 474 bytes .../Shader hooks can be used/metadata.json | 3 + .../000.png | Bin 0 -> 427 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 1707 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 510 bytes .../metadata.json | 3 + 10 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 test/unit/visual/cases/webgpu.js create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/Shader hooks can be used/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/Shader hooks can be used/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/The color shader runs successfully/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/The color shader runs successfully/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/The material shader runs successfully/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/The material shader runs successfully/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/metadata.json diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 8d434cc725..8e8852ee41 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -304,7 +304,6 @@ class RendererWebGPU extends Renderer3D { } shader._groupEntries = groupEntries; - console.log(shader._groupEntries); shader._bindGroupLayouts = [...bindGroupLayouts.values()]; shader._pipelineLayout = this.device.createPipelineLayout({ bindGroupLayouts: shader._bindGroupLayouts, @@ -886,6 +885,7 @@ class RendererWebGPU extends Renderer3D { } uploadTextureFromSource({ gpuTexture }, source) { + this.uploadedTexture = true; this.queue.copyExternalImageToTexture( { source }, { texture: gpuTexture }, @@ -894,6 +894,7 @@ class RendererWebGPU extends Renderer3D { } uploadTextureFromData({ gpuTexture }, data, width, height) { + this.uploadedTexture = true; this.queue.writeTexture( { texture: gpuTexture }, data, diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js new file mode 100644 index 0000000000..7593c437e8 --- /dev/null +++ b/test/unit/visual/cases/webgpu.js @@ -0,0 +1,108 @@ +import { vi } from 'vitest'; +import p5 from '../../../../src/app'; +import { visualSuite, visualTest } from '../visualTest'; +import rendererWebGPU from '../../../../src/webgpu/p5.RendererWebGPU'; + +p5.registerAddon(rendererWebGPU); + +visualSuite('WebGPU', function() { + visualSuite('Shaders', function() { + visualTest('The color shader runs successfully', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background('white'); + for (const [i, color] of ['red', 'lime', 'blue'].entries()) { + p5.push(); + p5.rotate(p5.TWO_PI * (i / 3)); + p5.fill(color); + p5.translate(15, 0); + p5.noStroke(); + p5.circle(0, 0, 20); + p5.pop(); + } + screenshot(); + }); + + visualTest('The stroke shader runs successfully', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background('white'); + for (const [i, color] of ['red', 'lime', 'blue'].entries()) { + p5.push(); + p5.rotate(p5.TWO_PI * (i / 3)); + p5.translate(15, 0); + p5.stroke(color); + p5.strokeWeight(2); + p5.circle(0, 0, 20); + p5.pop(); + } + screenshot(); + }); + + visualTest('The material shader runs successfully', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background('white'); + p5.ambientLight(50); + p5.directionalLight(100, 100, 100, 0, 1, -1); + p5.pointLight(155, 155, 155, 0, -200, 500); + p5.specularMaterial(255); + p5.shininess(300); + for (const [i, color] of ['red', 'lime', 'blue'].entries()) { + p5.push(); + p5.rotate(p5.TWO_PI * (i / 3)); + p5.fill(color); + p5.translate(15, 0); + p5.noStroke(); + p5.sphere(10); + p5.pop(); + } + screenshot(); + }); + + visualTest('Shader hooks can be used', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const myFill = p5.baseMaterialShader().modify({ + 'Vertex getWorldInputs': `(inputs: Vertex) { + var result = inputs; + result.position.y += 10.0 * sin(inputs.position.x * 0.25); + return result; + }`, + }); + const myStroke = p5.baseStrokeShader().modify({ + 'StrokeVertex getWorldInputs': `(inputs: StrokeVertex) { + var result = inputs; + result.position.y += 10.0 * sin(inputs.position.x * 0.25); + return result; + }`, + }); + p5.background('black'); + p5.shader(myFill); + p5.strokeShader(myStroke); + p5.fill('red'); + p5.stroke('white'); + p5.strokeWeight(5); + p5.circle(0, 0, 30); + screenshot(); + }); + + // TODO: turns out textures are only available in the next animation frame! + // need to figure out a workaround before uncommenting this test. + /*visualTest('Textures in the material shader work', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const tex = p5.createImage(50, 50); + tex.loadPixels(); + for (let x = 0; x < tex.width; x++) { + for (let y = 0; y < tex.height; y++) { + const off = (x + y * tex.width) * 4; + tex.pixels[off] = p5.round((x / tex.width) * 255); + tex.pixels[off + 1] = p5.round((y / tex.height) * 255); + tex.pixels[off + 2] = 0; + tex.pixels[off + 3] = 255; + } + } + tex.updatePixels(); + p5.texture(tex); + p5.plane(p5.width, p5.height); + + screenshot(); + });*/ + }); +}); diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/Shader hooks can be used/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/Shader hooks can be used/000.png new file mode 100644 index 0000000000000000000000000000000000000000..f883a461b5b4ee71f22494a5cbbf31b750dee77d GIT binary patch literal 474 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 BBNhMv literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/The material shader runs successfully/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/The material shader runs successfully/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/The material shader runs successfully/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/000.png new file mode 100644 index 0000000000000000000000000000000000000000..cd8d8130754b37606c09f97debb75999a83fabc0 GIT binary patch literal 510 zcmVPx$xJg7oRA@u(m|G45AqYiv|BJS<(Fw64uj@?H+OH;~$l;2hxs+1U5B#zhBxj!f ziO`rqD8S5N>H9A6O(4}tOSG-yBamtkuT$Q6Rwxxn)S2jf4U`Ciasp+CgZd8;bQIC{ zs5(nU&qr;<(RV4*0XT;j-$jrbR!gJER^Hb3 zPi;v6qtVIu=PSEGMNZU29Z?mca}{_dRD@JmruO%?US%ryyQ)4hSE{3vgaxkJ07Wu1!U>eB4M_}wB}KDYIz~9;>jt4P^H-rL81`bU@UEX zPh3l0uotU#%1v$UzF48u5LFo)IQUi9cuGk{QXLrhKYpim2O*n+}YSB3_s~l(>A; zDneoU|Ik&}QS??*)LUyb5_)_9Lzr@8!`e`jFHIM#hLY?t*Z=?k07*qoM6N<$f}W_} A{{R30 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/The stroke shader runs successfully/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From e7c1d6b83f9ed40eb43883472dd625a42cb8a53e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 26 Jun 2025 18:35:29 -0400 Subject: [PATCH 32/98] Fix usage of textures in setup --- src/webgpu/p5.RendererWebGPU.js | 15 +++++++++++++-- test/unit/visual/cases/webgpu.js | 6 ++---- .../Textures in the material shader work/000.png | Bin 0 -> 275 bytes .../metadata.json | 3 +++ 4 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/Textures in the material shader work/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Shaders/Textures in the material shader work/metadata.json diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 8e8852ee41..9ded1277d2 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -36,6 +36,7 @@ class RendererWebGPU extends Renderer3D { // TODO disablable stencil this.depthFormat = 'depth24plus-stencil8'; this._updateSize(); + this._update(); } _updateSize() { @@ -662,6 +663,15 @@ class RendererWebGPU extends Renderer3D { this.queue.submit([commandEncoder.finish()]); } + async ensureTexture(source) { + await this.queue.onSubmittedWorkDone(); + await new Promise((res) => requestAnimationFrame(res)); + const tex = this.getTexture(source); + tex.update(); + await this.queue.onSubmittedWorkDone(); + await new Promise((res) => requestAnimationFrame(res)); + } + ////////////////////////////////////////////// // SHADER ////////////////////////////////////////////// @@ -885,7 +895,6 @@ class RendererWebGPU extends Renderer3D { } uploadTextureFromSource({ gpuTexture }, source) { - this.uploadedTexture = true; this.queue.copyExternalImageToTexture( { source }, { texture: gpuTexture }, @@ -894,7 +903,6 @@ class RendererWebGPU extends Renderer3D { } uploadTextureFromData({ gpuTexture }, data, width, height) { - this.uploadedTexture = true; this.queue.writeTexture( { texture: gpuTexture }, data, @@ -1118,6 +1126,9 @@ function rendererWebGPU(p5, fn) { p5.RendererWebGPU = RendererWebGPU; p5.renderers[constants.WEBGPU] = p5.RendererWebGPU; + fn.ensureTexture = function(source) { + return this._renderer.ensureTexture(source); + } } export default rendererWebGPU; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 7593c437e8..efd8cc7e93 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -83,9 +83,7 @@ visualSuite('WebGPU', function() { screenshot(); }); - // TODO: turns out textures are only available in the next animation frame! - // need to figure out a workaround before uncommenting this test. - /*visualTest('Textures in the material shader work', async function(p5, screenshot) { + visualTest('Textures in the material shader work', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); const tex = p5.createImage(50, 50); tex.loadPixels(); @@ -103,6 +101,6 @@ visualSuite('WebGPU', function() { p5.plane(p5.width, p5.height); screenshot(); - });*/ + }); }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/Textures in the material shader work/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/Textures in the material shader work/000.png new file mode 100644 index 0000000000000000000000000000000000000000..b0e4b614b344a0205eb32d901c4f48631d7d2fda GIT binary patch literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-#^NA%Cx&(BWL^R}XFXjULo%H2 zo);8mN@QTU@cu!QO6MdMPY}`olUTWn(q{c;J=@$mH?}g6YsnIaQ(Q|HIjrJZvdrNX*OG+}v$&Qlb-2Z~WU<38 zt|iMIesQHNaA@O7S>kYvD`k Date: Mon, 28 Jul 2025 11:04:11 -0400 Subject: [PATCH 33/98] Refactor framebuffers --- src/image/pixels.js | 2 +- src/webgl/p5.Framebuffer.js | 540 ++------------------------ src/webgl/p5.RendererGL.js | 597 ++++++++++++++++++++++++++++- src/webgl/p5.Texture.js | 21 - src/webgl/utils.js | 21 + src/webgpu/p5.RendererWebGPU.js | 382 ++++++++++++++++++ test/unit/visual/cases/webgpu.js | 164 ++++++++ test/unit/webgl/p5.RendererGL.js | 3 +- test/unit/webgpu/p5.Framebuffer.js | 247 ++++++++++++ 9 files changed, 1454 insertions(+), 523 deletions(-) create mode 100644 test/unit/webgpu/p5.Framebuffer.js diff --git a/src/image/pixels.js b/src/image/pixels.js index ebea101273..6c2ea58115 100644 --- a/src/image/pixels.js +++ b/src/image/pixels.js @@ -933,7 +933,7 @@ function pixels(p5, fn){ */ fn.loadPixels = function(...args) { // p5._validateParameters('loadPixels', args); - this._renderer.loadPixels(); + return this._renderer.loadPixels(); }; /** diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 0ebb3c0daa..af2ab279b5 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -5,11 +5,9 @@ import * as constants from '../core/constants'; import { RGB, RGBA } from '../color/creating_reading'; -import { checkWebGLCapabilities } from './p5.Texture'; -import { readPixelsWebGL, readPixelWebGL } from './utils'; +import { checkWebGLCapabilities } from './utils'; import { Camera } from './p5.Camera'; import { Texture } from './p5.Texture'; -import { Image } from '../image/p5.Image'; const constrain = (n, low, high) => Math.max(Math.min(n, high), low); @@ -52,7 +50,6 @@ class FramebufferTexture { } rawTexture() { - // TODO: handle webgpu texture handle return { texture: this.framebuffer[this.property] }; } } @@ -87,13 +84,11 @@ class Framebuffer { this.antialiasSamples = settings.antialias ? 2 : 0; } this.antialias = this.antialiasSamples > 0; - if (this.antialias && this.renderer.webglVersion !== constants.WEBGL2) { - console.warn('Antialiasing is unsupported in a WebGL 1 context'); + if (this.antialias && !this.renderer.supportsFramebufferAntialias()) { + console.warn('Framebuffer antialiasing is unsupported in this context'); this.antialias = false; } this.density = settings.density || this.renderer._pixelDensity; - const gl = this.renderer.GL; - this.gl = gl; if (settings.width && settings.height) { const dimensions = this.renderer._adjustDimensions(settings.width, settings.height); @@ -112,7 +107,8 @@ class Framebuffer { this.height = this.renderer.height; this._autoSized = true; } - this._checkIfFormatsAvailable(); + // Let renderer validate and adjust formats for this context + this.renderer.validateFramebufferFormats(this); if (settings.stencil && !this.useDepth) { console.warn('A stencil buffer can only be used if also using depth. Since the framebuffer has no depth buffer, the stencil buffer will be ignored.'); @@ -120,16 +116,8 @@ class Framebuffer { this.useStencil = this.useDepth && (settings.stencil === undefined ? true : settings.stencil); - this.framebuffer = gl.createFramebuffer(); - if (!this.framebuffer) { - throw new Error('Unable to create a framebuffer'); - } - if (this.antialias) { - this.aaFramebuffer = gl.createFramebuffer(); - if (!this.aaFramebuffer) { - throw new Error('Unable to create a framebuffer for antialiasing'); - } - } + // Let renderer create framebuffer resources with antialiasing support + this.renderer.createFramebufferResources(this); this._recreateTextures(); @@ -466,6 +454,10 @@ class Framebuffer { } } + _deleteTextures() { + this.renderer.deleteFramebufferTextures(this); + } + /** * Creates new textures and renderbuffers given the current size of the * framebuffer. @@ -473,117 +465,10 @@ class Framebuffer { * @private */ _recreateTextures() { - const gl = this.gl; - this._updateSize(); - const prevBoundTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); - const prevBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); - - const colorTexture = gl.createTexture(); - if (!colorTexture) { - throw new Error('Unable to create color texture'); - } - gl.bindTexture(gl.TEXTURE_2D, colorTexture); - const colorFormat = this._glColorFormat(); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - colorFormat.internalFormat, - this.width * this.density, - this.height * this.density, - 0, - colorFormat.format, - colorFormat.type, - null - ); - this.colorTexture = colorTexture; - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.TEXTURE_2D, - colorTexture, - 0 - ); - - if (this.useDepth) { - // Create the depth texture - const depthTexture = gl.createTexture(); - if (!depthTexture) { - throw new Error('Unable to create depth texture'); - } - const depthFormat = this._glDepthFormat(); - gl.bindTexture(gl.TEXTURE_2D, depthTexture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - depthFormat.internalFormat, - this.width * this.density, - this.height * this.density, - 0, - depthFormat.format, - depthFormat.type, - null - ); - - gl.framebufferTexture2D( - gl.FRAMEBUFFER, - this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, - gl.TEXTURE_2D, - depthTexture, - 0 - ); - this.depthTexture = depthTexture; - } - - // Create separate framebuffer for antialiasing - if (this.antialias) { - this.colorRenderbuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, this.colorRenderbuffer); - gl.renderbufferStorageMultisample( - gl.RENDERBUFFER, - Math.max( - 0, - Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) - ), - colorFormat.internalFormat, - this.width * this.density, - this.height * this.density - ); - - if (this.useDepth) { - const depthFormat = this._glDepthFormat(); - this.depthRenderbuffer = gl.createRenderbuffer(); - gl.bindRenderbuffer(gl.RENDERBUFFER, this.depthRenderbuffer); - gl.renderbufferStorageMultisample( - gl.RENDERBUFFER, - Math.max( - 0, - Math.min(this.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) - ), - depthFormat.internalFormat, - this.width * this.density, - this.height * this.density - ); - } - - gl.bindFramebuffer(gl.FRAMEBUFFER, this.aaFramebuffer); - gl.framebufferRenderbuffer( - gl.FRAMEBUFFER, - gl.COLOR_ATTACHMENT0, - gl.RENDERBUFFER, - this.colorRenderbuffer - ); - if (this.useDepth) { - gl.framebufferRenderbuffer( - gl.FRAMEBUFFER, - this.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, - gl.RENDERBUFFER, - this.depthRenderbuffer - ); - } - } + // Let renderer handle texture creation and framebuffer setup + this.renderer.recreateFramebufferTextures(this); if (this.useDepth) { this.depth = new FramebufferTexture(this, 'depthTexture'); @@ -612,131 +497,6 @@ class Framebuffer { } ); this.renderer.textures.set(this.color, this.colorP5Texture); - - gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); - gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); - } - - /** - * To create a WebGL texture, one needs to supply three pieces of information: - * the type (the data type each channel will be stored as, e.g. int or float), - * the format (the color channels that will each be stored in the previously - * specified type, e.g. rgb or rgba), and the internal format (the specifics - * of how data for each channel, in the aforementioned type, will be packed - * together, such as how many bits to use, e.g. RGBA32F or RGB565.) - * - * The format and channels asked for by the user hint at what these values - * need to be, and the WebGL version affects what options are avaiable. - * This method returns the values for these three properties, given the - * framebuffer's settings. - * - * @private - */ - _glColorFormat() { - let type, format, internalFormat; - const gl = this.gl; - - if (this.format === constants.FLOAT) { - type = gl.FLOAT; - } else if (this.format === constants.HALF_FLOAT) { - type = this.renderer.webglVersion === constants.WEBGL2 - ? gl.HALF_FLOAT - : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES; - } else { - type = gl.UNSIGNED_BYTE; - } - - if (this.channels === RGBA) { - format = gl.RGBA; - } else { - format = gl.RGB; - } - - if (this.renderer.webglVersion === constants.WEBGL2) { - // https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html - const table = { - [gl.FLOAT]: { - [gl.RGBA]: gl.RGBA32F - // gl.RGB32F is not available in Firefox without an alpha channel - }, - [gl.HALF_FLOAT]: { - [gl.RGBA]: gl.RGBA16F - // gl.RGB16F is not available in Firefox without an alpha channel - }, - [gl.UNSIGNED_BYTE]: { - [gl.RGBA]: gl.RGBA8, // gl.RGBA4 - [gl.RGB]: gl.RGB8 // gl.RGB565 - } - }; - internalFormat = table[type][format]; - } else if (this.format === constants.HALF_FLOAT) { - internalFormat = gl.RGBA; - } else { - internalFormat = format; - } - - return { internalFormat, format, type }; - } - - /** - * To create a WebGL texture, one needs to supply three pieces of information: - * the type (the data type each channel will be stored as, e.g. int or float), - * the format (the color channels that will each be stored in the previously - * specified type, e.g. rgb or rgba), and the internal format (the specifics - * of how data for each channel, in the aforementioned type, will be packed - * together, such as how many bits to use, e.g. RGBA32F or RGB565.) - * - * This method takes into account the settings asked for by the user and - * returns values for these three properties that can be used for the - * texture storing depth information. - * - * @private - */ - _glDepthFormat() { - let type, format, internalFormat; - const gl = this.gl; - - if (this.useStencil) { - if (this.depthFormat === constants.FLOAT) { - type = gl.FLOAT_32_UNSIGNED_INT_24_8_REV; - } else if (this.renderer.webglVersion === constants.WEBGL2) { - type = gl.UNSIGNED_INT_24_8; - } else { - type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL; - } - } else { - if (this.depthFormat === constants.FLOAT) { - type = gl.FLOAT; - } else { - type = gl.UNSIGNED_INT; - } - } - - if (this.useStencil) { - format = gl.DEPTH_STENCIL; - } else { - format = gl.DEPTH_COMPONENT; - } - - if (this.useStencil) { - if (this.depthFormat === constants.FLOAT) { - internalFormat = gl.DEPTH32F_STENCIL8; - } else if (this.renderer.webglVersion === constants.WEBGL2) { - internalFormat = gl.DEPTH24_STENCIL8; - } else { - internalFormat = gl.DEPTH_STENCIL; - } - } else if (this.renderer.webglVersion === constants.WEBGL2) { - if (this.depthFormat === constants.FLOAT) { - internalFormat = gl.DEPTH_COMPONENT32F; - } else { - internalFormat = gl.DEPTH_COMPONENT24; - } - } else { - internalFormat = gl.DEPTH_COMPONENT; - } - - return { internalFormat, format, type }; } /** @@ -775,17 +535,7 @@ class Framebuffer { * @private */ _handleResize() { - const oldColor = this.color; - const oldDepth = this.depth; - const oldColorRenderbuffer = this.colorRenderbuffer; - const oldDepthRenderbuffer = this.depthRenderbuffer; - - this._deleteTexture(oldColor); - if (oldDepth) this._deleteTexture(oldDepth); - const gl = this.gl; - if (oldColorRenderbuffer) gl.deleteRenderbuffer(oldColorRenderbuffer); - if (oldDepthRenderbuffer) gl.deleteRenderbuffer(oldDepthRenderbuffer); - + this._deleteTextures(); this._recreateTextures(); this.defaultCamera._resize(); } @@ -913,20 +663,6 @@ class Framebuffer { return cam; } - /** - * Given a raw texture wrapper, delete its stored texture from WebGL memory, - * and remove it from p5's list of active textures. - * - * @param {p5.FramebufferTexture} texture - * @private - */ - _deleteTexture(texture) { - const gl = this.gl; - gl.deleteTexture(texture.rawTexture().texture); - - this.renderer.textures.delete(texture); - } - /** * Deletes the framebuffer from GPU memory. * @@ -996,19 +732,11 @@ class Framebuffer { *
*/ remove() { - const gl = this.gl; - this._deleteTexture(this.color); - if (this.depth) this._deleteTexture(this.depth); - gl.deleteFramebuffer(this.framebuffer); - if (this.aaFramebuffer) { - gl.deleteFramebuffer(this.aaFramebuffer); - } - if (this.depthRenderbuffer) { - gl.deleteRenderbuffer(this.depthRenderbuffer); - } - if (this.colorRenderbuffer) { - gl.deleteRenderbuffer(this.colorRenderbuffer); - } + this._deleteTextures(); + + // Let renderer clean up framebuffer resources + this.renderer.deleteFramebufferResources(this); + this.renderer.framebuffers.delete(this); } @@ -1095,14 +823,7 @@ class Framebuffer { * @private */ _framebufferToBind() { - if (this.antialias) { - // If antialiasing, draw to an antialiased renderbuffer rather - // than directly to the texture. In end() we will copy from the - // renderbuffer to the texture. - return this.aaFramebuffer; - } else { - return this.framebuffer; - } + return this.renderer.getFramebufferToBind(this); } /** @@ -1111,45 +832,9 @@ class Framebuffer { * @property {'colorTexutre'|'depthTexture'} property The property to update */ _update(property) { - if (this.dirty[property] && this.antialias) { - const gl = this.gl; - gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.aaFramebuffer); - gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.framebuffer); - const partsToCopy = { - colorTexture: [ - gl.COLOR_BUFFER_BIT, - // TODO: move to renderer - this.colorP5Texture.magFilter === constants.LINEAR ? gl.LINEAR : gl.NEAREST - ], - }; - if (this.useDepth) { - partsToCopy.depthTexture = [ - gl.DEPTH_BUFFER_BIT, - // TODO: move to renderer - this.depthP5Texture.magFilter === constants.LINEAR ? gl.LINEAR : gl.NEAREST - ]; - } - const [flag, filter] = partsToCopy[property]; - gl.blitFramebuffer( - 0, - 0, - this.width * this.density, - this.height * this.density, - 0, - 0, - this.width * this.density, - this.height * this.density, - flag, - filter - ); + if (this.dirty[property]) { + this.renderer.updateFramebufferTexture(this, property); this.dirty[property] = false; - - const activeFbo = this.renderer.activeFramebuffer(); - if (activeFbo) { - gl.bindFramebuffer(gl.FRAMEBUFFER, activeFbo._framebufferToBind()); - } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } } } @@ -1159,8 +844,7 @@ class Framebuffer { * @private */ _beforeBegin() { - const gl = this.gl; - gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebufferToBind()); + this.renderer.bindFramebuffer(this); this.renderer.viewport( this.width * this.density, this.height * this.density @@ -1236,7 +920,7 @@ class Framebuffer { if (this.prevFramebuffer) { this.prevFramebuffer._beforeBegin(); } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); + this.renderer.bindFramebuffer(null); this.renderer.viewport( this.renderer._origViewport.width, this.renderer._origViewport.height @@ -1355,25 +1039,19 @@ class Framebuffer { */ loadPixels() { this._update('colorTexture'); - const gl = this.gl; - const prevFramebuffer = this.renderer.activeFramebuffer(); - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); - const colorFormat = this._glColorFormat(); - this.pixels = readPixelsWebGL( - this.pixels, - gl, - this.framebuffer, - 0, - 0, - this.width * this.density, - this.height * this.density, - colorFormat.format, - colorFormat.type - ); - if (prevFramebuffer) { - gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer._framebufferToBind()); + const result = this.renderer.readFramebufferPixels(this); + + // Check if renderer returned a Promise (WebGPU) or data directly (WebGL) + if (result && typeof result.then === 'function') { + // WebGPU async case - return Promise + return result.then(pixels => { + this.pixels = pixels; + return pixels; + }); } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); + // WebGL sync case - assign directly + this.pixels = result; + return result; } } @@ -1415,7 +1093,7 @@ class Framebuffer { get(x, y, w, h) { this._update('colorTexture'); // p5._validateParameters('p5.Framebuffer.get', arguments); - const colorFormat = this._glColorFormat(); + if (x === undefined && y === undefined) { x = 0; y = 0; @@ -1430,14 +1108,7 @@ class Framebuffer { y = constrain(y, 0, this.height - 1); } - return readPixelWebGL( - this.gl, - this.framebuffer, - x * this.density, - y * this.density, - colorFormat.format, - colorFormat.type - ); + return this.renderer.readFramebufferPixel(this, x * this.density, y * this.density); } x = constrain(x, 0, this.width - 1); @@ -1445,60 +1116,7 @@ class Framebuffer { w = constrain(w, 1, this.width - x); h = constrain(h, 1, this.height - y); - const rawData = readPixelsWebGL( - undefined, - this.gl, - this.framebuffer, - x * this.density, - y * this.density, - w * this.density, - h * this.density, - colorFormat.format, - colorFormat.type - ); - // Framebuffer data might be either a Uint8Array or Float32Array - // depending on its format, and it may or may not have an alpha channel. - // To turn it into an image, we have to normalize the data into a - // Uint8ClampedArray with alpha. - const fullData = new Uint8ClampedArray( - w * h * this.density * this.density * 4 - ); - - // Default channels that aren't in the framebuffer (e.g. alpha, if the - // framebuffer is in RGB mode instead of RGBA) to 255 - fullData.fill(255); - - const channels = colorFormat.type === this.gl.RGB ? 3 : 4; - for (let y = 0; y < h * this.density; y++) { - for (let x = 0; x < w * this.density; x++) { - for (let channel = 0; channel < 4; channel++) { - const idx = (y * w * this.density + x) * 4 + channel; - if (channel < channels) { - // Find the index of this pixel in `rawData`, which might have a - // different number of channels - const rawDataIdx = channels === 4 - ? idx - : (y * w * this.density + x) * channels + channel; - fullData[idx] = rawData[rawDataIdx]; - } - } - } - } - - // Create an image from the data - const region = new Image(w * this.density, h * this.density); - region.imageData = region.canvas.getContext('2d').createImageData( - region.width, - region.height - ); - region.imageData.data.set(fullData); - region.pixels = region.imageData.data; - region.updatePixels(); - if (this.density !== 1) { - // TODO: support get() at a pixel density > 1 - region.resize(w, h); - } - return region; + return this.renderer.readFramebufferRegion(this, x, y, w, h); } /** @@ -1550,85 +1168,9 @@ class Framebuffer { * */ updatePixels() { - const gl = this.gl; - this.colorP5Texture.bindTexture(); - const colorFormat = this._glColorFormat(); - - const channels = colorFormat.format === gl.RGBA ? 4 : 3; - const len = - this.width * this.height * this.density * this.density * channels; - const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE - ? Uint8Array - : Float32Array; - if ( - !(this.pixels instanceof TypedArrayClass) || this.pixels.length !== len - ) { - throw new Error( - 'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().' - ); - } - - gl.texImage2D( - gl.TEXTURE_2D, - 0, - colorFormat.internalFormat, - this.width * this.density, - this.height * this.density, - 0, - colorFormat.format, - colorFormat.type, - this.pixels - ); - this.colorP5Texture.unbindTexture(); + // Let renderer handle the pixel update process + this.renderer.updateFramebufferPixels(this); this.dirty.colorTexture = false; - - const prevFramebuffer = this.renderer.activeFramebuffer(); - if (this.antialias) { - // We need to make sure the antialiased framebuffer also has the updated - // pixels so that if more is drawn to it, it goes on top of the updated - // pixels instead of replacing them. - // We can't blit the framebuffer to the multisampled antialias - // framebuffer to leave both in the same state, so instead we have - // to use image() to put the framebuffer texture onto the antialiased - // framebuffer. - this.begin(); - this.renderer.push(); - // this.renderer.imageMode(constants.CENTER); - this.renderer.states.setValue('imageMode', constants.CORNER); - this.renderer.setCamera(this.filterCamera); - this.renderer.resetMatrix(); - this.renderer.states.setValue('strokeColor', null); - this.renderer.clear(); - this.renderer._drawingFilter = true; - this.renderer.image( - this, - 0, 0, - this.width, this.height, - -this.renderer.width / 2, -this.renderer.height / 2, - this.renderer.width, this.renderer.height - ); - this.renderer._drawingFilter = false; - this.renderer.pop(); - if (this.useDepth) { - gl.clearDepth(1); - gl.clear(gl.DEPTH_BUFFER_BIT); - } - this.end(); - } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); - if (this.useDepth) { - gl.clearDepth(1); - gl.clear(gl.DEPTH_BUFFER_BIT); - } - if (prevFramebuffer) { - gl.bindFramebuffer( - gl.FRAMEBUFFER, - prevFramebuffer._framebufferToBind() - ); - } else { - gl.bindFramebuffer(gl.FRAMEBUFFER, null); - } - } } } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 9025c0d31a..5a9482baa2 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -6,14 +6,17 @@ import { readPixelsWebGL, readPixelWebGL, setWebGLTextureParams, - setWebGLUniformValue + setWebGLUniformValue, + checkWebGLCapabilities } from './utils'; import { Renderer3D, getStrokeDefs } from "../core/p5.Renderer3D"; import { Shader } from "./p5.Shader"; import { Texture, MipmapTexture } from "./p5.Texture"; import { Framebuffer } from "./p5.Framebuffer"; import { Graphics } from "../core/p5.Graphics"; +import { RGB, RGBA } from '../color/creating_reading'; import { Element } from "../dom/p5.Element"; +import { Image } from '../image/p5.Image'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -1386,6 +1389,598 @@ class RendererGL extends Renderer3D { populateHooks(shader, src, shaderType) { return populateGLSLHooks(shader, src, shaderType); } + + ////////////////////////////////////////////// + // Framebuffer methods + ////////////////////////////////////////////// + + supportsFramebufferAntialias() { + return this.webglVersion === constants.WEBGL2; + } + + createFramebufferResources(framebuffer) { + const gl = this.GL; + + framebuffer.framebuffer = gl.createFramebuffer(); + if (!framebuffer.framebuffer) { + throw new Error('Unable to create a framebuffer'); + } + + if (framebuffer.antialias) { + framebuffer.aaFramebuffer = gl.createFramebuffer(); + if (!framebuffer.aaFramebuffer) { + throw new Error('Unable to create a framebuffer for antialiasing'); + } + } + } + + validateFramebufferFormats(framebuffer) { + const gl = this.GL; + + if ( + framebuffer.useDepth && + this.webglVersion === constants.WEBGL && + !gl.getExtension('WEBGL_depth_texture') + ) { + console.warn( + 'Unable to create depth textures in this environment. Falling back ' + + 'to a framebuffer without depth.' + ); + framebuffer.useDepth = false; + } + + if ( + framebuffer.useDepth && + this.webglVersion === constants.WEBGL && + framebuffer.depthFormat === constants.FLOAT + ) { + console.warn( + 'FLOAT depth format is unavailable in WebGL 1. ' + + 'Defaulting to UNSIGNED_INT.' + ); + framebuffer.depthFormat = constants.UNSIGNED_INT; + } + + if (![ + constants.UNSIGNED_BYTE, + constants.FLOAT, + constants.HALF_FLOAT + ].includes(framebuffer.format)) { + console.warn( + 'Unknown Framebuffer format. ' + + 'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' + + 'Defaulting to UNSIGNED_BYTE.' + ); + framebuffer.format = constants.UNSIGNED_BYTE; + } + if (framebuffer.useDepth && ![ + constants.UNSIGNED_INT, + constants.FLOAT + ].includes(framebuffer.depthFormat)) { + console.warn( + 'Unknown Framebuffer depth format. ' + + 'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.' + ); + framebuffer.depthFormat = constants.FLOAT; + } + + const support = checkWebGLCapabilities(this); + if (!support.float && framebuffer.format === constants.FLOAT) { + console.warn( + 'This environment does not support FLOAT textures. ' + + 'Falling back to UNSIGNED_BYTE.' + ); + framebuffer.format = constants.UNSIGNED_BYTE; + } + if ( + framebuffer.useDepth && + !support.float && + framebuffer.depthFormat === constants.FLOAT + ) { + console.warn( + 'This environment does not support FLOAT depth textures. ' + + 'Falling back to UNSIGNED_INT.' + ); + framebuffer.depthFormat = constants.UNSIGNED_INT; + } + if (!support.halfFloat && framebuffer.format === constants.HALF_FLOAT) { + console.warn( + 'This environment does not support HALF_FLOAT textures. ' + + 'Falling back to UNSIGNED_BYTE.' + ); + framebuffer.format = constants.UNSIGNED_BYTE; + } + + if ( + framebuffer.channels === RGB && + [constants.FLOAT, constants.HALF_FLOAT].includes(framebuffer.format) + ) { + console.warn( + 'FLOAT and HALF_FLOAT formats do not work cross-platform with only ' + + 'RGB channels. Falling back to RGBA.' + ); + framebuffer.channels = RGBA; + } + } + + recreateFramebufferTextures(framebuffer) { + const gl = this.GL; + + const prevBoundTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); + const prevBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + + const colorTexture = gl.createTexture(); + if (!colorTexture) { + throw new Error('Unable to create color texture'); + } + gl.bindTexture(gl.TEXTURE_2D, colorTexture); + const colorFormat = this._getFramebufferColorFormat(framebuffer); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + colorFormat.internalFormat, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density, + 0, + colorFormat.format, + colorFormat.type, + null + ); + framebuffer.colorTexture = colorTexture; + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.framebuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + colorTexture, + 0 + ); + + if (framebuffer.useDepth) { + // Create the depth texture + const depthTexture = gl.createTexture(); + if (!depthTexture) { + throw new Error('Unable to create depth texture'); + } + const depthFormat = this._getFramebufferDepthFormat(framebuffer); + gl.bindTexture(gl.TEXTURE_2D, depthTexture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + depthFormat.internalFormat, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density, + 0, + depthFormat.format, + depthFormat.type, + null + ); + + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + framebuffer.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, + gl.TEXTURE_2D, + depthTexture, + 0 + ); + framebuffer.depthTexture = depthTexture; + } + + // Create separate framebuffer for antialiasing + if (framebuffer.antialias) { + framebuffer.colorRenderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, framebuffer.colorRenderbuffer); + gl.renderbufferStorageMultisample( + gl.RENDERBUFFER, + Math.max( + 0, + Math.min(framebuffer.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) + ), + colorFormat.internalFormat, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density + ); + + if (framebuffer.useDepth) { + const depthFormat = this._getFramebufferDepthFormat(framebuffer); + framebuffer.depthRenderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, framebuffer.depthRenderbuffer); + gl.renderbufferStorageMultisample( + gl.RENDERBUFFER, + Math.max( + 0, + Math.min(framebuffer.antialiasSamples, gl.getParameter(gl.MAX_SAMPLES)) + ), + depthFormat.internalFormat, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density + ); + } + + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.aaFramebuffer); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.RENDERBUFFER, + framebuffer.colorRenderbuffer + ); + if (framebuffer.useDepth) { + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + framebuffer.useStencil ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, + framebuffer.depthRenderbuffer + ); + } + } + + gl.bindTexture(gl.TEXTURE_2D, prevBoundTexture); + gl.bindFramebuffer(gl.FRAMEBUFFER, prevBoundFramebuffer); + } + + /** + * To create a WebGL texture, one needs to supply three pieces of information: + * the type (the data type each channel will be stored as, e.g. int or float), + * the format (the color channels that will each be stored in the previously + * specified type, e.g. rgb or rgba), and the internal format (the specifics + * of how data for each channel, in the aforementioned type, will be packed + * together, such as how many bits to use, e.g. RGBA32F or RGB565.) + * + * The format and channels asked for by the user hint at what these values + * need to be, and the WebGL version affects what options are avaiable. + * This method returns the values for these three properties, given the + * framebuffer's settings. + * + * @private + */ + _getFramebufferColorFormat(framebuffer) { + let type, format, internalFormat; + const gl = this.GL; + + if (framebuffer.format === constants.FLOAT) { + type = gl.FLOAT; + } else if (framebuffer.format === constants.HALF_FLOAT) { + type = this.webglVersion === constants.WEBGL2 + ? gl.HALF_FLOAT + : gl.getExtension('OES_texture_half_float').HALF_FLOAT_OES; + } else { + type = gl.UNSIGNED_BYTE; + } + + if (framebuffer.channels === RGBA) { + format = gl.RGBA; + } else { + format = gl.RGB; + } + + if (this.webglVersion === constants.WEBGL2) { + // https://webgl2fundamentals.org/webgl/lessons/webgl-data-textures.html + const table = { + [gl.FLOAT]: { + [gl.RGBA]: gl.RGBA32F + // gl.RGB32F is not available in Firefox without an alpha channel + }, + [gl.HALF_FLOAT]: { + [gl.RGBA]: gl.RGBA16F + // gl.RGB16F is not available in Firefox without an alpha channel + }, + [gl.UNSIGNED_BYTE]: { + [gl.RGBA]: gl.RGBA8, // gl.RGBA4 + [gl.RGB]: gl.RGB8 // gl.RGB565 + } + }; + internalFormat = table[type][format]; + } else if (framebuffer.format === constants.HALF_FLOAT) { + internalFormat = gl.RGBA; + } else { + internalFormat = format; + } + + return { internalFormat, format, type }; + } + + /** + * To create a WebGL texture, one needs to supply three pieces of information: + * the type (the data type each channel will be stored as, e.g. int or float), + * the format (the color channels that will each be stored in the previously + * specified type, e.g. rgb or rgba), and the internal format (the specifics + * of how data for each channel, in the aforementioned type, will be packed + * together, such as how many bits to use, e.g. RGBA32F or RGB565.) + * + * This method takes into account the settings asked for by the user and + * returns values for these three properties that can be used for the + * texture storing depth information. + * + * @private + */ + _getFramebufferDepthFormat(framebuffer) { + let type, format, internalFormat; + const gl = this.GL; + + if (framebuffer.useStencil) { + if (framebuffer.depthFormat === constants.FLOAT) { + type = gl.FLOAT_32_UNSIGNED_INT_24_8_REV; + } else if (this.webglVersion === constants.WEBGL2) { + type = gl.UNSIGNED_INT_24_8; + } else { + type = gl.getExtension('WEBGL_depth_texture').UNSIGNED_INT_24_8_WEBGL; + } + } else { + if (framebuffer.depthFormat === constants.FLOAT) { + type = gl.FLOAT; + } else { + type = gl.UNSIGNED_INT; + } + } + + if (framebuffer.useStencil) { + format = gl.DEPTH_STENCIL; + } else { + format = gl.DEPTH_COMPONENT; + } + + if (framebuffer.useStencil) { + if (framebuffer.depthFormat === constants.FLOAT) { + internalFormat = gl.DEPTH32F_STENCIL8; + } else if (this.webglVersion === constants.WEBGL2) { + internalFormat = gl.DEPTH24_STENCIL8; + } else { + internalFormat = gl.DEPTH_STENCIL; + } + } else if (this.webglVersion === constants.WEBGL2) { + if (framebuffer.depthFormat === constants.FLOAT) { + internalFormat = gl.DEPTH_COMPONENT32F; + } else { + internalFormat = gl.DEPTH_COMPONENT24; + } + } else { + internalFormat = gl.DEPTH_COMPONENT; + } + + return { internalFormat, format, type }; + } + + _deleteFramebufferTexture(texture) { + const gl = this.GL; + gl.deleteTexture(texture.rawTexture().texture); + this.textures.delete(texture); + } + + deleteFramebufferTextures(framebuffer) { + this._deleteFramebufferTexture(framebuffer.color) + if (framebuffer.depth) this._deleteFramebufferTexture(framebuffer.depth); + const gl = this.GL; + if (framebuffer.colorRenderbuffer) gl.deleteRenderbuffer(framebuffer.colorRenderbuffer); + if (framebuffer.depthRenderbuffer) gl.deleteRenderbuffer(framebuffer.depthRenderbuffer); + } + + deleteFramebufferResources(framebuffer) { + const gl = this.GL; + gl.deleteFramebuffer(framebuffer.framebuffer); + if (framebuffer.aaFramebuffer) { + gl.deleteFramebuffer(framebuffer.aaFramebuffer); + } + if (framebuffer.depthRenderbuffer) { + gl.deleteRenderbuffer(framebuffer.depthRenderbuffer); + } + if (framebuffer.colorRenderbuffer) { + gl.deleteRenderbuffer(framebuffer.colorRenderbuffer); + } + } + + getFramebufferToBind(framebuffer) { + if (framebuffer.antialias) { + return framebuffer.aaFramebuffer; + } else { + return framebuffer.framebuffer; + } + } + + updateFramebufferTexture(framebuffer, property) { + if (framebuffer.antialias) { + const gl = this.GL; + gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffer.aaFramebuffer); + gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffer.framebuffer); + const partsToCopy = { + colorTexture: [ + gl.COLOR_BUFFER_BIT, + framebuffer.colorP5Texture.magFilter === constants.LINEAR ? gl.LINEAR : gl.NEAREST + ], + }; + if (framebuffer.useDepth) { + partsToCopy.depthTexture = [ + gl.DEPTH_BUFFER_BIT, + framebuffer.depthP5Texture.magFilter === constants.LINEAR ? gl.LINEAR : gl.NEAREST + ]; + } + const [flag, filter] = partsToCopy[property]; + gl.blitFramebuffer( + 0, + 0, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density, + 0, + 0, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density, + flag, + filter + ); + + const activeFbo = this.activeFramebuffer(); + this.bindFramebuffer(activeFbo); + } + } + + bindFramebuffer(framebuffer) { + const gl = this.GL; + gl.bindFramebuffer( + gl.FRAMEBUFFER, + framebuffer + ? this.getFramebufferToBind(framebuffer) + : null + ); + } + + readFramebufferPixels(framebuffer) { + const gl = this.GL; + const prevFramebuffer = this.activeFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.framebuffer); + const colorFormat = this._getFramebufferColorFormat(framebuffer); + const pixels = readPixelsWebGL( + framebuffer.pixels, + gl, + framebuffer.framebuffer, + 0, + 0, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density, + colorFormat.format, + colorFormat.type + ); + this.bindFramebuffer(prevFramebuffer); + return pixels; + } + + readFramebufferPixel(framebuffer, x, y) { + const colorFormat = this._getFramebufferColorFormat(framebuffer); + return readPixelWebGL( + this.GL, + framebuffer.framebuffer, + x, + y, + colorFormat.format, + colorFormat.type + ); + } + + readFramebufferRegion(framebuffer, x, y, w, h) { + const gl = this.GL; + const colorFormat = this._getFramebufferColorFormat(framebuffer); + + const rawData = readPixelsWebGL( + undefined, + gl, + framebuffer.framebuffer, + x * framebuffer.density, + y * framebuffer.density, + w * framebuffer.density, + h * framebuffer.density, + colorFormat.format, + colorFormat.type + ); + + // Framebuffer data might be either a Uint8Array or Float32Array + // depending on its format, and it may or may not have an alpha channel. + // To turn it into an image, we have to normalize the data into a + // Uint8ClampedArray with alpha. + const fullData = new Uint8ClampedArray( + w * h * framebuffer.density * framebuffer.density * 4 + ); + // Default channels that aren't in the framebuffer (e.g. alpha, if the + // framebuffer is in RGB mode instead of RGBA) to 255 + fullData.fill(255); + + const channels = colorFormat.format === gl.RGB ? 3 : 4; + for (let yPos = 0; yPos < h * framebuffer.density; yPos++) { + for (let xPos = 0; xPos < w * framebuffer.density; xPos++) { + for (let channel = 0; channel < 4; channel++) { + const idx = (yPos * w * framebuffer.density + xPos) * 4 + channel; + if (channel < channels) { + // Find the index of this pixel in `rawData`, which might have a + // different number of channels + const rawDataIdx = channels === 4 + ? idx + : (yPos * w * framebuffer.density + xPos) * channels + channel; + fullData[idx] = rawData[rawDataIdx]; + } + } + } + } + + // Create image from data + const region = new Image(w * framebuffer.density, h * framebuffer.density); + region.imageData = region.canvas.getContext('2d').createImageData( + region.width, + region.height + ); + region.imageData.data.set(fullData); + region.pixels = region.imageData.data; + region.updatePixels(); + if (framebuffer.density !== 1) { + region.pixelDensity(framebuffer.density); + } + return region; + } + + updateFramebufferPixels(framebuffer) { + const gl = this.GL; + framebuffer.colorP5Texture.bindTexture(); + const colorFormat = this._getFramebufferColorFormat(framebuffer); + + const channels = colorFormat.format === gl.RGBA ? 4 : 3; + const len = framebuffer.width * framebuffer.height * framebuffer.density * framebuffer.density * channels; + const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE ? Uint8Array : Float32Array; + + if (!(framebuffer.pixels instanceof TypedArrayClass) || framebuffer.pixels.length !== len) { + throw new Error( + 'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().' + ); + } + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + colorFormat.internalFormat, + framebuffer.width * framebuffer.density, + framebuffer.height * framebuffer.density, + 0, + colorFormat.format, + colorFormat.type, + framebuffer.pixels + ); + framebuffer.colorP5Texture.unbindTexture(); + + const prevFramebuffer = this.activeFramebuffer(); + if (framebuffer.antialias) { + // We need to make sure the antialiased framebuffer also has the updated + // pixels so that if more is drawn to it, it goes on top of the updated + // pixels instead of replacing them. + // We can't blit the framebuffer to the multisampled antialias + // framebuffer to leave both in the same state, so instead we have + // to use image() to put the framebuffer texture onto the antialiased + // framebuffer. + framebuffer.begin(); + this.push(); + this.states.setValue('imageMode', constants.CORNER); + this.setCamera(framebuffer.filterCamera); + this.resetMatrix(); + this.states.setValue('strokeColor', null); + this.clear(); + this._drawingFilter = true; + this.image( + framebuffer, + 0, 0, + framebuffer.width, framebuffer.height, + -this.width / 2, -this.height / 2, + this.width, this.height + ); + this._drawingFilter = false; + this.pop(); + if (framebuffer.useDepth) { + gl.clearDepth(1); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + framebuffer.end(); + } else { + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.framebuffer); + if (framebuffer.useDepth) { + gl.clearDepth(1); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + this.bindFramebuffer(prevFramebuffer); + } + } } function rendererGL(p5, fn) { diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index c88389bb8e..4ea07a1fba 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -382,27 +382,6 @@ function texture(p5, fn){ p5.MipmapTexture = MipmapTexture; } -export function checkWebGLCapabilities({ GL, webglVersion }) { - const gl = GL; - const supportsFloat = webglVersion === constants.WEBGL2 - ? (gl.getExtension('EXT_color_buffer_float') && - gl.getExtension('EXT_float_blend')) - : gl.getExtension('OES_texture_float'); - const supportsFloatLinear = supportsFloat && - gl.getExtension('OES_texture_float_linear'); - const supportsHalfFloat = webglVersion === constants.WEBGL2 - ? gl.getExtension('EXT_color_buffer_float') - : gl.getExtension('OES_texture_half_float'); - const supportsHalfFloatLinear = supportsHalfFloat && - gl.getExtension('OES_texture_half_float_linear'); - return { - float: supportsFloat, - floatLinear: supportsFloatLinear, - halfFloat: supportsHalfFloat, - halfFloatLinear: supportsHalfFloatLinear - }; -} - export default texture; export { Texture, MipmapTexture }; diff --git a/src/webgl/utils.js b/src/webgl/utils.js index 70766ac522..0727e91e1f 100644 --- a/src/webgl/utils.js +++ b/src/webgl/utils.js @@ -448,3 +448,24 @@ export function populateGLSLHooks(shader, src, shaderType) { return preMain + '\n' + defines + hooks + main + postMain; } + +export function checkWebGLCapabilities({ GL, webglVersion }) { + const gl = GL; + const supportsFloat = webglVersion === constants.WEBGL2 + ? (gl.getExtension('EXT_color_buffer_float') && + gl.getExtension('EXT_float_blend')) + : gl.getExtension('OES_texture_float'); + const supportsFloatLinear = supportsFloat && + gl.getExtension('OES_texture_float_linear'); + const supportsHalfFloat = webglVersion === constants.WEBGL2 + ? gl.getExtension('EXT_color_buffer_float') + : gl.getExtension('OES_texture_half_float'); + const supportsHalfFloatLinear = supportsHalfFloat && + gl.getExtension('OES_texture_half_float_linear'); + return { + float: supportsFloat, + floatLinear: supportsFloatLinear, + halfFloat: supportsHalfFloat, + halfFloatLinear: supportsHalfFloatLinear + }; +} diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 9ded1277d2..a8732d22dd 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1,6 +1,8 @@ import { Renderer3D, getStrokeDefs } from '../core/p5.Renderer3D'; import { Shader } from '../webgl/p5.Shader'; import { Texture } from '../webgl/p5.Texture'; +import { Image } from '../image/p5.Image'; +import { RGB, RGBA } from '../color/creating_reading'; import * as constants from '../core/constants'; @@ -17,6 +19,10 @@ class RendererWebGPU extends Renderer3D { this.renderPass = {}; this.samplers = new Map(); + + // Single reusable staging buffer for pixel reading + this.pixelReadBuffer = null; + this.pixelReadBufferSize = 0; } async setupContext() { @@ -1120,6 +1126,382 @@ class RendererWebGPU extends Renderer3D { return preMain + '\n' + defines + hooks + main + postMain; } + + ////////////////////////////////////////////// + // 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.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; + } + + ////////////////////////////////////////////// + // Framebuffer methods + ////////////////////////////////////////////// + + supportsFramebufferAntialias() { + return true; + } + + createFramebufferResources(framebuffer) { + } + + validateFramebufferFormats(framebuffer) { + if (![ + constants.UNSIGNED_BYTE, + constants.FLOAT, + constants.HALF_FLOAT + ].includes(framebuffer.format)) { + console.warn( + 'Unknown Framebuffer format. ' + + 'Please use UNSIGNED_BYTE, FLOAT, or HALF_FLOAT. ' + + 'Defaulting to UNSIGNED_BYTE.' + ); + framebuffer.format = constants.UNSIGNED_BYTE; + } + + if (framebuffer.useDepth && ![ + constants.UNSIGNED_INT, + constants.FLOAT + ].includes(framebuffer.depthFormat)) { + console.warn( + 'Unknown Framebuffer depth format. ' + + 'Please use UNSIGNED_INT or FLOAT. Defaulting to FLOAT.' + ); + framebuffer.depthFormat = constants.FLOAT; + } + } + + recreateFramebufferTextures(framebuffer) { + if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) { + framebuffer.colorTexture.destroy(); + } + if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) { + framebuffer.depthTexture.destroy(); + } + + const colorTextureDescriptor = { + size: { + width: framebuffer.width * framebuffer.density, + height: framebuffer.height * framebuffer.density, + depthOrArrayLayers: 1, + }, + format: this._getWebGPUColorFormat(framebuffer), + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, + sampleCount: framebuffer.antialias ? framebuffer.antialiasSamples : 1, + }; + + framebuffer.colorTexture = this.device.createTexture(colorTextureDescriptor); + + if (framebuffer.useDepth) { + const depthTextureDescriptor = { + size: { + width: framebuffer.width * framebuffer.density, + height: framebuffer.height * framebuffer.density, + depthOrArrayLayers: 1, + }, + format: this._getWebGPUDepthFormat(framebuffer), + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + sampleCount: framebuffer.antialias ? framebuffer.antialiasSamples : 1, + }; + + framebuffer.depthTexture = this.device.createTexture(depthTextureDescriptor); + } + } + + _getWebGPUColorFormat(framebuffer) { + if (framebuffer.format === constants.FLOAT) { + return framebuffer.channels === RGBA ? 'rgba32float' : 'rgba32float'; + } else if (framebuffer.format === constants.HALF_FLOAT) { + return framebuffer.channels === RGBA ? 'rgba16float' : 'rgba16float'; + } else { + return framebuffer.channels === RGBA ? 'rgba8unorm' : 'rgba8unorm'; + } + } + + _getWebGPUDepthFormat(framebuffer) { + if (framebuffer.useStencil) { + return framebuffer.depthFormat === constants.FLOAT ? 'depth32float-stencil8' : 'depth24plus-stencil8'; + } else { + return framebuffer.depthFormat === constants.FLOAT ? 'depth32float' : 'depth24plus'; + } + } + + _deleteFramebufferTexture(texture) { + const handle = texture.rawTexture(); + if (handle.texture && handle.texture.destroy) { + handle.texture.destroy(); + } + this.textures.delete(texture); + } + + deleteFramebufferTextures(framebuffer) { + this._deleteFramebufferTexture(framebuffer.color) + if (framebuffer.depth) this._deleteFramebufferTexture(framebuffer.depth); + } + + deleteFramebufferResources(framebuffer) { + if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) { + framebuffer.colorTexture.destroy(); + } + if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) { + framebuffer.depthTexture.destroy(); + } + } + + getFramebufferToBind(framebuffer) { + } + + updateFramebufferTexture(framebuffer, property) { + // No-op for WebGPU since antialiasing is handled at pipeline level + } + + bindFramebuffer(framebuffer) {} + + async readFramebufferPixels(framebuffer) { + const width = framebuffer.width * framebuffer.density; + const height = framebuffer.height * framebuffer.density; + const bytesPerPixel = 4; + const bufferSize = width * height * bytesPerPixel; + + const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); + + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { texture: framebuffer.colorTexture }, + { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { width, height, depthOrArrayLayers: 1 } + ); + + this.device.queue.submit([commandEncoder.finish()]); + + await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); + const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); + const result = new Uint8Array(mappedRange.slice(0, bufferSize)); + + stagingBuffer.unmap(); + return result; + } + + async readFramebufferPixel(framebuffer, x, y) { + const bytesPerPixel = 4; + const stagingBuffer = this._ensurePixelReadBuffer(bytesPerPixel); + + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { + texture: framebuffer.colorTexture, + origin: { x, y, z: 0 } + }, + { buffer: stagingBuffer, bytesPerRow: bytesPerPixel }, + { width: 1, height: 1, depthOrArrayLayers: 1 } + ); + + this.device.queue.submit([commandEncoder.finish()]); + + await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bytesPerPixel); + const mappedRange = stagingBuffer.getMappedRange(0, bytesPerPixel); + const pixelData = new Uint8Array(mappedRange); + const result = [pixelData[0], pixelData[1], pixelData[2], pixelData[3]]; + + stagingBuffer.unmap(); + return result; + } + + async readFramebufferRegion(framebuffer, x, y, w, h) { + const width = w * framebuffer.density; + const height = h * framebuffer.density; + const bytesPerPixel = 4; + const bufferSize = width * height * bytesPerPixel; + + const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); + + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { + texture: framebuffer.colorTexture, + origin: { x: x * framebuffer.density, y: y * framebuffer.density, z: 0 } + }, + { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { width, height, 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.slice(0, bufferSize)); + + // 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(); + 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 + ////////////////////////////////////////////// + + async loadPixels() { + const width = this.width * this._pixelDensity; + const height = this.height * this._pixelDensity; + const bytesPerPixel = 4; + const bufferSize = width * height * bytesPerPixel; + + const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); + + // Get the current canvas texture + const canvasTexture = this.drawingContext.getCurrentTexture(); + + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { texture: canvasTexture }, + { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { width, height, depthOrArrayLayers: 1 } + ); + + this.device.queue.submit([commandEncoder.finish()]); + + await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); + const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); + this.pixels = new Uint8Array(mappedRange.slice(0, bufferSize)); + + stagingBuffer.unmap(); + return this.pixels; + } + + async _getPixel(x, y) { + const bytesPerPixel = 4; + const stagingBuffer = this._ensurePixelReadBuffer(bytesPerPixel); + + const canvasTexture = this.drawingContext.getCurrentTexture(); + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { + texture: canvasTexture, + origin: { x, y, z: 0 } + }, + { buffer: stagingBuffer, bytesPerRow: bytesPerPixel }, + { width: 1, height: 1, depthOrArrayLayers: 1 } + ); + + this.device.queue.submit([commandEncoder.finish()]); + + await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bytesPerPixel); + const mappedRange = stagingBuffer.getMappedRange(0, bytesPerPixel); + const pixelData = new Uint8Array(mappedRange); + const result = [pixelData[0], pixelData[1], pixelData[2], pixelData[3]]; + + stagingBuffer.unmap(); + return result; + } + + async get(x, y, w, h) { + const pd = this._pixelDensity; + + if (typeof x === 'undefined' && typeof y === 'undefined') { + // get() - return entire canvas + x = y = 0; + w = this.width; + h = this.height; + } else { + x *= pd; + y *= pd; + + if (typeof w === 'undefined' && typeof h === 'undefined') { + // get(x,y) - single pixel + if (x < 0 || y < 0 || x >= this.width * pd || y >= this.height * pd) { + return [0, 0, 0, 0]; + } + + return this._getPixel(x, y); + } + // get(x,y,w,h) - region + } + + // Read region and create p5.Image + const width = w * pd; + const height = h * pd; + const bytesPerPixel = 4; + const bufferSize = width * height * bytesPerPixel; + + const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); + + const canvasTexture = this.drawingContext.getCurrentTexture(); + const commandEncoder = this.device.createCommandEncoder(); + commandEncoder.copyTextureToBuffer( + { + texture: canvasTexture, + origin: { x, y, z: 0 } + }, + { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { width, height, 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.slice(0, bufferSize)); + + const region = new Image(width, height); + region.pixelDensity(pd); + region.imageData = region.canvas.getContext('2d').createImageData(width, height); + region.imageData.data.set(pixelData); + region.pixels = region.imageData.data; + region.updatePixels(); + + stagingBuffer.unmap(); + return region; + } } function rendererWebGPU(p5, fn) { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index efd8cc7e93..363626807a 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -103,4 +103,168 @@ visualSuite('WebGPU', function() { screenshot(); }); }); + + visualSuite('Framebuffers', function() { + visualTest('Basic framebuffer draw to canvas', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create a framebuffer + const fbo = p5.createFramebuffer({ width: 25, height: 25 }); + + // Draw to the framebuffer + fbo.draw(() => { + p5.background(255, 0, 0); // Red background + p5.fill(0, 255, 0); // Green circle + p5.noStroke(); + p5.circle(12.5, 12.5, 20); + }); + + // Draw the framebuffer to the main canvas + p5.background(0, 0, 255); // Blue background + p5.texture(fbo); + p5.noStroke(); + p5.plane(25, 25); + + screenshot(); + }); + + visualTest('Framebuffer with different sizes', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create two different sized framebuffers + const fbo1 = p5.createFramebuffer({ width: 20, height: 20 }); + const fbo2 = p5.createFramebuffer({ width: 15, height: 15 }); + + // Draw to first framebuffer + fbo1.draw(() => { + p5.background(255, 100, 100); + p5.fill(255, 255, 0); + p5.noStroke(); + p5.rect(5, 5, 10, 10); + }); + + // Draw to second framebuffer + fbo2.draw(() => { + p5.background(100, 255, 100); + p5.fill(255, 0, 255); + p5.noStroke(); + p5.circle(7.5, 7.5, 10); + }); + + // Draw both to main canvas + p5.background(50); + p5.push(); + p5.translate(-12.5, -12.5); + p5.texture(fbo1); + p5.noStroke(); + p5.plane(20, 20); + p5.pop(); + + p5.push(); + p5.translate(12.5, 12.5); + p5.texture(fbo2); + p5.noStroke(); + p5.plane(15, 15); + p5.pop(); + + screenshot(); + }); + + visualTest('Auto-sized framebuffer', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create auto-sized framebuffer (should match canvas size) + const fbo = p5.createFramebuffer(); + + // Draw to the framebuffer + fbo.draw(() => { + p5.background(0); + p5.stroke(255); + p5.strokeWeight(2); + p5.noFill(); + // Draw a grid pattern to verify size + for (let x = 0; x < 50; x += 10) { + p5.line(x, 0, x, 50); + } + for (let y = 0; y < 50; y += 10) { + p5.line(0, y, 50, y); + } + p5.fill(255, 0, 0); + p5.noStroke(); + p5.circle(25, 25, 15); + }); + + // Draw the framebuffer to fill the main canvas + p5.texture(fbo); + p5.noStroke(); + p5.plane(50, 50); + + screenshot(); + }); + + visualTest('Auto-sized framebuffer after canvas resize', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create auto-sized framebuffer + const fbo = p5.createFramebuffer(); + + // Resize the canvas (framebuffer should auto-resize) + p5.resizeCanvas(30, 30); + + // Draw to the framebuffer after resize + fbo.draw(() => { + p5.background(100, 0, 100); + p5.fill(0, 255, 255); + p5.noStroke(); + // Draw a shape that fills the new size + p5.rect(5, 5, 20, 20); + p5.fill(255, 255, 0); + p5.circle(15, 15, 10); + }); + + // Draw the framebuffer to the main canvas + p5.texture(fbo); + p5.noStroke(); + p5.plane(30, 30); + + screenshot(); + }); + + visualTest('Fixed-size framebuffer after manual resize', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create fixed-size framebuffer + const fbo = p5.createFramebuffer({ width: 20, height: 20 }); + + // Draw initial content + fbo.draw(() => { + p5.background(255, 200, 100); + p5.fill(0, 100, 200); + p5.noStroke(); + p5.circle(10, 10, 15); + }); + + // Manually resize the framebuffer + fbo.resize(35, 25); + + // Draw new content to the resized framebuffer + fbo.draw(() => { + p5.background(200, 255, 100); + p5.fill(200, 0, 100); + p5.noStroke(); + // Draw content that uses the new size + p5.rect(5, 5, 25, 15); + p5.fill(0, 0, 255); + p5.circle(17.5, 12.5, 8); + }); + + // Draw the resized framebuffer to the main canvas + p5.background(50); + p5.texture(fbo); + p5.noStroke(); + p5.plane(35, 25); + + screenshot(); + }); + }); }); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 34b64abdfd..f437ac4c20 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1098,7 +1098,7 @@ suite('p5.RendererGL', function() { assert.isTrue(img.length === 4); }); - test('updatePixels() matches 2D mode', function() { + test.only('updatePixels() matches 2D mode', function() { myp5.createCanvas(20, 20); myp5.pixelDensity(1); const getColors = function(mode) { @@ -1120,6 +1120,7 @@ suite('p5.RendererGL', function() { }; const p2d = getColors(myp5.P2D); + debugger const webgl = getColors(myp5.WEBGL); myp5.image(p2d, 0, 0); myp5.blendMode(myp5.DIFFERENCE); diff --git a/test/unit/webgpu/p5.Framebuffer.js b/test/unit/webgpu/p5.Framebuffer.js new file mode 100644 index 0000000000..9fec2f070d --- /dev/null +++ b/test/unit/webgpu/p5.Framebuffer.js @@ -0,0 +1,247 @@ +import p5 from '../../../src/app.js'; + +suite('WebGPU p5.Framebuffer', function() { + let myp5; + let prevPixelRatio; + + beforeAll(async function() { + prevPixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; + myp5 = new p5(function(p) { + p.setup = function() {}; + p.draw = function() {}; + }); + }); + + afterAll(function() { + myp5.remove(); + window.devicePixelRatio = prevPixelRatio; + }); + + suite('Creation and basic properties', function() { + test('framebuffers can be created with WebGPU renderer', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + expect(fbo).to.be.an('object'); + expect(fbo.width).to.equal(10); + expect(fbo.height).to.equal(10); + expect(fbo.autoSized()).to.equal(true); + }); + + test('framebuffers can be created with custom dimensions', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer({ width: 20, height: 30 }); + + expect(fbo.width).to.equal(20); + expect(fbo.height).to.equal(30); + expect(fbo.autoSized()).to.equal(false); + }); + + test('framebuffers have color texture', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + expect(fbo.color).to.be.an('object'); + expect(fbo.color.rawTexture).to.be.a('function'); + }); + + test('framebuffers can specify different formats', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer({ + format: 'float', + channels: 'rgb' + }); + + expect(fbo).to.be.an('object'); + expect(fbo.width).to.equal(10); + expect(fbo.height).to.equal(10); + }); + }); + + suite('Auto-sizing behavior', function() { + test('auto-sized framebuffers change size with canvas', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + myp5.pixelDensity(1); + const fbo = myp5.createFramebuffer(); + + expect(fbo.autoSized()).to.equal(true); + expect(fbo.width).to.equal(10); + expect(fbo.height).to.equal(10); + expect(fbo.density).to.equal(1); + + myp5.resizeCanvas(15, 20); + myp5.pixelDensity(2); + expect(fbo.width).to.equal(15); + expect(fbo.height).to.equal(20); + expect(fbo.density).to.equal(2); + }); + + test('manually-sized framebuffers do not change size with canvas', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + myp5.pixelDensity(3); + const fbo = myp5.createFramebuffer({ width: 25, height: 30, density: 1 }); + + expect(fbo.autoSized()).to.equal(false); + expect(fbo.width).to.equal(25); + expect(fbo.height).to.equal(30); + expect(fbo.density).to.equal(1); + + myp5.resizeCanvas(5, 15); + myp5.pixelDensity(2); + expect(fbo.width).to.equal(25); + expect(fbo.height).to.equal(30); + expect(fbo.density).to.equal(1); + }); + + test('manually-sized framebuffers can be made auto-sized', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + myp5.pixelDensity(1); + const fbo = myp5.createFramebuffer({ width: 25, height: 30, density: 2 }); + + expect(fbo.autoSized()).to.equal(false); + expect(fbo.width).to.equal(25); + expect(fbo.height).to.equal(30); + expect(fbo.density).to.equal(2); + + // Make it auto-sized + fbo.autoSized(true); + expect(fbo.autoSized()).to.equal(true); + + myp5.resizeCanvas(8, 12); + myp5.pixelDensity(3); + expect(fbo.width).to.equal(8); + expect(fbo.height).to.equal(12); + expect(fbo.density).to.equal(3); + }); + }); + + suite('Manual resizing', function() { + test('framebuffers can be manually resized', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + myp5.pixelDensity(1); + const fbo = myp5.createFramebuffer(); + + expect(fbo.width).to.equal(10); + expect(fbo.height).to.equal(10); + expect(fbo.density).to.equal(1); + + fbo.resize(20, 25); + expect(fbo.width).to.equal(20); + expect(fbo.height).to.equal(25); + expect(fbo.autoSized()).to.equal(false); + }); + + test('resizing affects pixel density', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + myp5.pixelDensity(1); + const fbo = myp5.createFramebuffer(); + + fbo.pixelDensity(3); + expect(fbo.density).to.equal(3); + + fbo.resize(15, 20); + fbo.pixelDensity(2); + expect(fbo.width).to.equal(15); + expect(fbo.height).to.equal(20); + expect(fbo.density).to.equal(2); + }); + }); + + suite('Drawing functionality', function() { + test('can draw to framebuffer with draw() method', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + let drawCallbackExecuted = false; + fbo.draw(() => { + drawCallbackExecuted = true; + myp5.background(255, 0, 0); + myp5.fill(0, 255, 0); + myp5.noStroke(); + myp5.circle(5, 5, 8); + }); + + expect(drawCallbackExecuted).to.equal(true); + }); + + test('can use framebuffer as texture', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + fbo.draw(() => { + myp5.background(255, 0, 0); + }); + + // Should not throw when used as texture + expect(() => { + myp5.texture(fbo); + myp5.plane(10, 10); + }).to.not.throw(); + }); + }); + + suite('Pixel access', function() { + test('loadPixels returns a promise in WebGPU', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + fbo.draw(() => { + myp5.background(255, 0, 0); + }); + + const result = fbo.loadPixels(); + expect(result).to.be.a('promise'); + + const pixels = await result; + expect(pixels).to.be.an('array'); + expect(pixels.length).to.equal(10 * 10 * 4); + }); + + test('pixels property is set after loadPixels resolves', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + fbo.draw(() => { + myp5.background(100, 150, 200); + }); + + const pixels = await fbo.loadPixels(); + expect(fbo.pixels).to.equal(pixels); + expect(fbo.pixels.length).to.equal(10 * 10 * 4); + }); + + test('get() returns a promise for single pixel in WebGPU', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + fbo.draw(() => { + myp5.background(100, 150, 200); + }); + + const result = fbo.get(5, 5); + expect(result).to.be.a('promise'); + + const color = await result; + expect(color).to.be.an('array'); + expect(color).to.have.length(4); + }); + + test('get() returns a promise for region in WebGPU', async function() { + await myp5.createCanvas(10, 10, myp5.WEBGPU); + const fbo = myp5.createFramebuffer(); + + fbo.draw(() => { + myp5.background(100, 150, 200); + }); + + const result = fbo.get(2, 2, 4, 4); + expect(result).to.be.a('promise'); + + const region = await result; + expect(region).to.be.an('object'); // Should be a p5.Image + expect(region.width).to.equal(4); + expect(region.height).to.equal(4); + }); + }); +}); From f94f8981f051cfdc3299058500e48e8b1b59d2a3 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 28 Jul 2025 11:15:28 -0400 Subject: [PATCH 34/98] Fix ordering of dirty flag --- src/webgl/p5.Framebuffer.js | 1 - src/webgl/p5.RendererGL.js | 1 + test/unit/webgl/p5.RendererGL.js | 3 +-- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index af2ab279b5..297e3dd09f 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -1170,7 +1170,6 @@ class Framebuffer { updatePixels() { // Let renderer handle the pixel update process this.renderer.updateFramebufferPixels(this); - this.dirty.colorTexture = false; } } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 5a9482baa2..507839e1fe 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1940,6 +1940,7 @@ class RendererGL extends Renderer3D { framebuffer.pixels ); framebuffer.colorP5Texture.unbindTexture(); + framebuffer.dirty.colorTexture = false; const prevFramebuffer = this.activeFramebuffer(); if (framebuffer.antialias) { diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index f437ac4c20..34b64abdfd 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1098,7 +1098,7 @@ suite('p5.RendererGL', function() { assert.isTrue(img.length === 4); }); - test.only('updatePixels() matches 2D mode', function() { + test('updatePixels() matches 2D mode', function() { myp5.createCanvas(20, 20); myp5.pixelDensity(1); const getColors = function(mode) { @@ -1120,7 +1120,6 @@ suite('p5.RendererGL', function() { }; const p2d = getColors(myp5.P2D); - debugger const webgl = getColors(myp5.WEBGL); myp5.image(p2d, 0, 0); myp5.blendMode(myp5.DIFFERENCE); From 5dfcd2456eee86e63ae2b8332b9c363bcab1dbd6 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 28 Jul 2025 17:04:02 -0400 Subject: [PATCH 35/98] Make sure textures are cleared at start --- preview/index.html | 7 +- src/webgl/p5.Framebuffer.js | 4 +- src/webgl/p5.RendererGL.js | 15 +++ src/webgl/p5.Texture.js | 2 + src/webgpu/p5.RendererWebGPU.js | 218 +++++++++++++++++++++++++++---- test/unit/visual/cases/webgpu.js | 72 +++++----- test/unit/visual/visualTest.js | 94 ++++++------- 7 files changed, 304 insertions(+), 108 deletions(-) diff --git a/preview/index.html b/preview/index.html index 6e4915ab34..4092992316 100644 --- a/preview/index.html +++ b/preview/index.html @@ -30,6 +30,7 @@ p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); + fbo = p.createFramebuffer(); tex = p.createImage(100, 100); tex.loadPixels(); @@ -43,6 +44,10 @@ } } tex.updatePixels(); + fbo.draw(() => { + p.imageMode(p.CENTER); + p.image(tex, 0, 0, p.width, p.height); + }); sh = p.baseMaterialShader().modify({ uniforms: { @@ -87,7 +92,7 @@ 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), ) - p.texture(tex) + p.texture(fbo) p.sphere(30); p.pop(); } diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 297e3dd09f..0fb5504d25 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -67,7 +67,7 @@ class Framebuffer { this.format = settings.format || constants.UNSIGNED_BYTE; this.channels = settings.channels || ( - this.renderer._pInst._glAttributes.alpha + this.renderer.defaultFramebufferAlpha() ? RGBA : RGB ); @@ -75,7 +75,7 @@ class Framebuffer { this.depthFormat = settings.depthFormat || constants.FLOAT; this.textureFiltering = settings.textureFiltering || constants.LINEAR; if (settings.antialias === undefined) { - this.antialiasSamples = this.renderer._pInst._glAttributes.antialias + this.antialiasSamples = this.renderer.defaultFramebufferAntialias() ? 2 : 0; } else if (typeof settings.antialias === 'number') { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 507839e1fe..c6fbfa45a6 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1302,6 +1302,11 @@ class RendererGL extends Renderer3D { return { texture: tex, glFormat: gl.RGBA, glDataType: gl.UNSIGNED_BYTE }; } + createFramebufferTextureHandle(framebufferTexture) { + // For WebGL, framebuffer texture handles are designed to be null + return null; + } + uploadTextureFromSource({ texture, glFormat, glDataType }, source) { const gl = this.GL; gl.texImage2D(gl.TEXTURE_2D, 0, glFormat, glFormat, glDataType, source); @@ -1394,6 +1399,16 @@ class RendererGL extends Renderer3D { // Framebuffer methods ////////////////////////////////////////////// + defaultFramebufferAlpha() { + return this._pInst._glAttributes.alpha; + } + + defaultFramebufferAntialias() { + return this.supportsFramebufferAntialias() + ? this._pInst._glAttributes.antialias + : false; + } + supportsFramebufferAntialias() { return this.webglVersion === constants.WEBGL2; } diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 4ea07a1fba..d1c45b84f1 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -128,6 +128,8 @@ class Texture { width: textureData.width, height: textureData.height, }); + } else { + this.textureHandle = this._renderer.createFramebufferTextureHandle(this.src); } this._renderer.setTextureParams(this, { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a8732d22dd..29ffcbf40d 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -27,7 +27,10 @@ class RendererWebGPU extends Renderer3D { async setupContext() { this.adapter = await navigator.gpu?.requestAdapter(); - this.device = await this.adapter?.requestDevice(); + this.device = await this.adapter?.requestDevice({ + // Todo: check support + requiredFeatures: ['depth32float-stencil8'] + }); if (!this.device) { throw new Error('Your browser does not support WebGPU.'); } @@ -36,7 +39,8 @@ class RendererWebGPU extends Renderer3D { this.presentationFormat = navigator.gpu.getPreferredCanvasFormat(); this.drawingContext.configure({ device: this.device, - format: this.presentationFormat + format: this.presentationFormat, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, }); // TODO disablable stencil @@ -193,12 +197,34 @@ class RendererWebGPU extends Renderer3D { freeDefs(this.renderer.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 }) { + const activeFramebuffer = this.activeFramebuffer(); + const format = activeFramebuffer ? + this._getWebGPUColorFormat(activeFramebuffer) : + this.presentationFormat; + + const requestedSampleCount = activeFramebuffer ? + (activeFramebuffer.antialias ? activeFramebuffer.antialiasSamples : 1) : + (this.antialias || 1); + const sampleCount = this._getValidSampleCount(requestedSampleCount); + + const depthFormat = activeFramebuffer && activeFramebuffer.useDepth ? + this._getWebGPUDepthFormat(activeFramebuffer) : + this.depthFormat; + return { topology: mode === constants.TRIANGLE_STRIP ? 'triangle-strip' : 'triangle-list', blendMode: this.states.curBlendMode, - sampleCount: (this.activeFramebuffer() || this).antialias || 1, // TODO - format: this.activeFramebuffer()?.format || this.presentationFormat, // TODO + sampleCount, + format, + depthFormat, } } @@ -209,8 +235,8 @@ class RendererWebGPU extends Renderer3D { shader.fragModule = device.createShaderModule({ code: shader.fragSrc() }); shader._pipelineCache = new Map(); - shader.getPipeline = ({ topology, blendMode, sampleCount, format }) => { - const key = `${topology}_${blendMode}_${sampleCount}_${format}`; + shader.getPipeline = ({ topology, blendMode, sampleCount, format, depthFormat }) => { + const key = `${topology}_${blendMode}_${sampleCount}_${format}_${depthFormat}`; if (!shader._pipelineCache.has(key)) { const pipeline = device.createRenderPipeline({ layout: shader._pipelineLayout, @@ -230,7 +256,7 @@ class RendererWebGPU extends Renderer3D { primitive: { topology }, multisample: { count: sampleCount }, depthStencil: { - format: this.depthFormat, + format: depthFormat, depthWriteEnabled: true, depthCompare: 'less', stencilFront: { @@ -531,9 +557,15 @@ class RendererWebGPU extends Renderer3D { _useShader(shader, options) {} _updateViewport() { + this._origViewport = { + width: this.width, + height: this.height, + }; this._viewport = [0, 0, this.width, this.height]; } + viewport() {} + zClipRange() { return [0, 1]; } @@ -573,14 +605,27 @@ class RendererWebGPU extends Renderer3D { if (!buffers) return; const commandEncoder = this.device.createCommandEncoder(); - const currentTexture = this.drawingContext.getCurrentTexture(); + + // Use framebuffer texture if active, otherwise use canvas texture + const activeFramebuffer = this.activeFramebuffer(); + const colorTexture = activeFramebuffer ? + (activeFramebuffer.aaColorTexture || activeFramebuffer.colorTexture) : + this.drawingContext.getCurrentTexture(); + const colorAttachment = { - view: currentTexture.createView(), + view: colorTexture.createView(), loadOp: "load", storeOp: "store", + // If using multisampled texture, resolve to non-multisampled texture + resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture ? + activeFramebuffer.colorTexture.createView() : undefined, }; - const depthTextureView = this.depthTexture?.createView(); + // Use framebuffer depth texture if active, otherwise use canvas depth texture + const depthTexture = activeFramebuffer ? + (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : + this.depthTexture; + const depthTextureView = depthTexture?.createView(); const renderPassDescriptor = { colorAttachments: [colorAttachment], depthStencilAttachment: depthTextureView @@ -1155,6 +1200,14 @@ class RendererWebGPU extends Renderer3D { // Framebuffer methods ////////////////////////////////////////////// + defaultFramebufferAlpha() { + return true + } + + defaultFramebufferAntialias() { + return true; + } + supportsFramebufferAntialias() { return true; } @@ -1189,40 +1242,138 @@ class RendererWebGPU extends Renderer3D { } recreateFramebufferTextures(framebuffer) { + // Clean up existing textures if (framebuffer.colorTexture && framebuffer.colorTexture.destroy) { framebuffer.colorTexture.destroy(); } + if (framebuffer.aaColorTexture && framebuffer.aaColorTexture.destroy) { + framebuffer.aaColorTexture.destroy(); + } if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) { framebuffer.depthTexture.destroy(); } + if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) { + framebuffer.aaDepthTexture.destroy(); + } + // Clear cached views when recreating textures + framebuffer._colorTextureView = null; - const colorTextureDescriptor = { + const baseDescriptor = { size: { width: framebuffer.width * framebuffer.density, height: framebuffer.height * framebuffer.density, depthOrArrayLayers: 1, }, format: this._getWebGPUColorFormat(framebuffer), - usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, - sampleCount: framebuffer.antialias ? framebuffer.antialiasSamples : 1, }; + // Create non-multisampled texture for texture binding (always needed) + const colorTextureDescriptor = { + ...baseDescriptor, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC, + sampleCount: 1, + }; framebuffer.colorTexture = this.device.createTexture(colorTextureDescriptor); + // 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); + } + if (framebuffer.useDepth) { - const depthTextureDescriptor = { + const depthBaseDescriptor = { size: { width: framebuffer.width * framebuffer.density, height: framebuffer.height * framebuffer.density, depthOrArrayLayers: 1, }, format: this._getWebGPUDepthFormat(framebuffer), - usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, - sampleCount: framebuffer.antialias ? framebuffer.antialiasSamples : 1, }; + // Create non-multisampled depth texture for texture binding (always needed) + const depthTextureDescriptor = { + ...depthBaseDescriptor, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING, + sampleCount: 1, + }; framebuffer.depthTexture = this.device.createTexture(depthTextureDescriptor); + + // 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); + } + } + + // Clear the framebuffer textures after creation + this._clearFramebufferTextures(framebuffer); + } + + _clearFramebufferTextures(framebuffer) { + const commandEncoder = this.device.createCommandEncoder(); + + // Clear the color texture (and multisampled texture if it exists) + const colorTexture = framebuffer.aaColorTexture || framebuffer.colorTexture; + const colorAttachment = { + view: colorTexture.createView(), + loadOp: "clear", + storeOp: "store", + clearValue: { r: 0, g: 0, b: 0, a: 0 }, + resolveTarget: framebuffer.aaColorTexture ? + framebuffer.colorTexture.createView() : undefined, + }; + + // Clear the depth texture if it exists + const depthTexture = framebuffer.aaDepthTexture || framebuffer.depthTexture; + const depthStencilAttachment = depthTexture ? { + view: depthTexture.createView(), + 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.queue.submit([commandEncoder.finish()]); + } + + _getFramebufferColorTextureView(framebuffer) { + if (!framebuffer._colorTextureView && framebuffer.colorTexture) { + framebuffer._colorTextureView = framebuffer.colorTexture.createView(); } + return framebuffer._colorTextureView; + } + + createFramebufferTextureHandle(framebufferTexture) { + const src = framebufferTexture; + let renderer = this; + return { + get view() { + return renderer._getFramebufferColorTextureView(src.framebuffer); + }, + get gpuTexture() { + return src.framebuffer.colorTexture; + } + }; } _getWebGPUColorFormat(framebuffer) { @@ -1263,6 +1414,9 @@ class RendererWebGPU extends Renderer3D { if (framebuffer.depthTexture && framebuffer.depthTexture.destroy) { framebuffer.depthTexture.destroy(); } + if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) { + framebuffer.aaDepthTexture.destroy(); + } } getFramebufferToBind(framebuffer) { @@ -1275,6 +1429,9 @@ class RendererWebGPU extends Renderer3D { bindFramebuffer(framebuffer) {} async readFramebufferPixels(framebuffer) { + // 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; @@ -1284,8 +1441,8 @@ class RendererWebGPU extends Renderer3D { const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyTextureToBuffer( - { texture: framebuffer.colorTexture }, - { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { texture: framebuffer.colorTexture, origin: { x: 0, y: 0, z: 0 } }, + { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel, rowsPerImage: height }, { width, height, depthOrArrayLayers: 1 } ); @@ -1300,6 +1457,9 @@ class RendererWebGPU extends Renderer3D { } async readFramebufferPixel(framebuffer, x, y) { + // Ensure all pending GPU work is complete before reading pixels + await this.queue.onSubmittedWorkDone(); + const bytesPerPixel = 4; const stagingBuffer = this._ensurePixelReadBuffer(bytesPerPixel); @@ -1325,6 +1485,9 @@ class RendererWebGPU extends Renderer3D { } async readFramebufferRegion(framebuffer, x, y, w, h) { + // 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; @@ -1391,6 +1554,9 @@ class RendererWebGPU extends Renderer3D { ////////////////////////////////////////////// async loadPixels() { + // Ensure all pending GPU work is complete before reading pixels + await this.queue.onSubmittedWorkDone(); + const width = this.width * this._pixelDensity; const height = this.height * this._pixelDensity; const bytesPerPixel = 4; @@ -1419,6 +1585,9 @@ class RendererWebGPU extends Renderer3D { } async _getPixel(x, y) { + // Ensure all pending GPU work is complete before reading pixels + await this.queue.onSubmittedWorkDone(); + const bytesPerPixel = 4; const stagingBuffer = this._ensurePixelReadBuffer(bytesPerPixel); @@ -1467,6 +1636,9 @@ class RendererWebGPU extends Renderer3D { // get(x,y,w,h) - region } + // Ensure all pending GPU work is complete before reading pixels + await this.queue.onSubmittedWorkDone(); + // Read region and create p5.Image const width = w * pd; const height = h * pd; @@ -1487,17 +1659,19 @@ class RendererWebGPU extends Renderer3D { ); this.device.queue.submit([commandEncoder.finish()]); + await this.queue.onSubmittedWorkDone(); await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); const pixelData = new Uint8Array(mappedRange.slice(0, bufferSize)); + console.log(pixelData) const region = new Image(width, height); region.pixelDensity(pd); - region.imageData = region.canvas.getContext('2d').createImageData(width, height); - region.imageData.data.set(pixelData); - region.pixels = region.imageData.data; - region.updatePixels(); + const ctx = region.canvas.getContext('2d'); + const imageData = ctx.createImageData(width, height); + imageData.data.set(pixelData); + ctx.putImageData(imageData, 0, 0); stagingBuffer.unmap(); return region; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 363626807a..9c0502ce39 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -19,7 +19,7 @@ visualSuite('WebGPU', function() { p5.circle(0, 0, 20); p5.pop(); } - screenshot(); + await screenshot(); }); visualTest('The stroke shader runs successfully', async function(p5, screenshot) { @@ -34,7 +34,7 @@ visualSuite('WebGPU', function() { p5.circle(0, 0, 20); p5.pop(); } - screenshot(); + await screenshot(); }); visualTest('The material shader runs successfully', async function(p5, screenshot) { @@ -54,7 +54,7 @@ visualSuite('WebGPU', function() { p5.sphere(10); p5.pop(); } - screenshot(); + await screenshot(); }); visualTest('Shader hooks can be used', async function(p5, screenshot) { @@ -80,7 +80,7 @@ visualSuite('WebGPU', function() { p5.stroke('white'); p5.strokeWeight(5); p5.circle(0, 0, 30); - screenshot(); + await screenshot(); }); visualTest('Textures in the material shader work', async function(p5, screenshot) { @@ -100,17 +100,17 @@ visualSuite('WebGPU', function() { p5.texture(tex); p5.plane(p5.width, p5.height); - screenshot(); + await screenshot(); }); }); visualSuite('Framebuffers', function() { visualTest('Basic framebuffer draw to canvas', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + // Create a framebuffer const fbo = p5.createFramebuffer({ width: 25, height: 25 }); - + // Draw to the framebuffer fbo.draw(() => { p5.background(255, 0, 0); // Red background @@ -118,23 +118,23 @@ visualSuite('WebGPU', function() { p5.noStroke(); p5.circle(12.5, 12.5, 20); }); - + // Draw the framebuffer to the main canvas p5.background(0, 0, 255); // Blue background p5.texture(fbo); p5.noStroke(); p5.plane(25, 25); - - screenshot(); + + await screenshot(); }); visualTest('Framebuffer with different sizes', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + // Create two different sized framebuffers const fbo1 = p5.createFramebuffer({ width: 20, height: 20 }); const fbo2 = p5.createFramebuffer({ width: 15, height: 15 }); - + // Draw to first framebuffer fbo1.draw(() => { p5.background(255, 100, 100); @@ -142,15 +142,15 @@ visualSuite('WebGPU', function() { p5.noStroke(); p5.rect(5, 5, 10, 10); }); - - // Draw to second framebuffer + + // Draw to second framebuffer fbo2.draw(() => { p5.background(100, 255, 100); p5.fill(255, 0, 255); p5.noStroke(); p5.circle(7.5, 7.5, 10); }); - + // Draw both to main canvas p5.background(50); p5.push(); @@ -159,23 +159,23 @@ visualSuite('WebGPU', function() { p5.noStroke(); p5.plane(20, 20); p5.pop(); - + p5.push(); p5.translate(12.5, 12.5); p5.texture(fbo2); p5.noStroke(); p5.plane(15, 15); p5.pop(); - - screenshot(); + + await screenshot(); }); visualTest('Auto-sized framebuffer', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + // Create auto-sized framebuffer (should match canvas size) const fbo = p5.createFramebuffer(); - + // Draw to the framebuffer fbo.draw(() => { p5.background(0); @@ -193,24 +193,24 @@ visualSuite('WebGPU', function() { p5.noStroke(); p5.circle(25, 25, 15); }); - + // Draw the framebuffer to fill the main canvas p5.texture(fbo); p5.noStroke(); p5.plane(50, 50); - - screenshot(); + + await screenshot(); }); visualTest('Auto-sized framebuffer after canvas resize', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + // Create auto-sized framebuffer const fbo = p5.createFramebuffer(); - + // Resize the canvas (framebuffer should auto-resize) p5.resizeCanvas(30, 30); - + // Draw to the framebuffer after resize fbo.draw(() => { p5.background(100, 0, 100); @@ -221,21 +221,21 @@ visualSuite('WebGPU', function() { p5.fill(255, 255, 0); p5.circle(15, 15, 10); }); - + // Draw the framebuffer to the main canvas p5.texture(fbo); p5.noStroke(); p5.plane(30, 30); - - screenshot(); + + await screenshot(); }); visualTest('Fixed-size framebuffer after manual resize', async function(p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + // Create fixed-size framebuffer const fbo = p5.createFramebuffer({ width: 20, height: 20 }); - + // Draw initial content fbo.draw(() => { p5.background(255, 200, 100); @@ -243,10 +243,10 @@ visualSuite('WebGPU', function() { p5.noStroke(); p5.circle(10, 10, 15); }); - + // Manually resize the framebuffer fbo.resize(35, 25); - + // Draw new content to the resized framebuffer fbo.draw(() => { p5.background(200, 255, 100); @@ -257,14 +257,14 @@ visualSuite('WebGPU', function() { p5.fill(0, 0, 255); p5.circle(17.5, 12.5, 8); }); - + // Draw the resized framebuffer to the main canvas p5.background(50); p5.texture(fbo); p5.noStroke(); p5.plane(35, 25); - - screenshot(); + + await screenshot(); }); }); }); diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index 120ce79565..7d301d142b 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -89,43 +89,43 @@ export function visualSuite( /** * Image Diff Algorithm for p5.js Visual Tests - * + * * This algorithm addresses the challenge of cross-platform rendering differences in p5.js visual tests. * Different operating systems and browsers render graphics with subtle variations, particularly with * anti-aliasing, text rendering, and sub-pixel positioning. This can cause false negatives in tests * when the visual differences are acceptable rendering variations rather than actual bugs. - * + * * Key components of the approach: - * + * * 1. Initial pixel-by-pixel comparison: * - Uses pixelmatch to identify differences between expected and actual images * - Sets a moderate threshold (0.5) to filter out minor color/intensity variations * - Produces a diff image with red pixels marking differences - * + * * 2. Cluster identification using BFS (Breadth-First Search): * - Groups connected difference pixels into clusters * - Uses a queue-based BFS algorithm to find all connected pixels * - Defines connectivity based on 8-way adjacency (all surrounding pixels) - * + * * 3. Cluster categorization by type: * - Analyzes each pixel's neighborhood characteristics * - Specifically identifies "line shift" clusters - differences that likely represent * the same visual elements shifted by 1px due to platform rendering differences * - Line shifts are identified when >80% of pixels in a cluster have ≤2 neighboring diff pixels - * + * * 4. Intelligent failure criteria: * - Filters out clusters smaller than MIN_CLUSTER_SIZE pixels (noise reduction) * - Applies different thresholds for regular differences vs. line shifts * - Considers both the total number of significant pixels and number of distinct clusters - * - * This approach balances the need to catch genuine visual bugs (like changes to shape geometry, + * + * This approach balances the need to catch genuine visual bugs (like changes to shape geometry, * colors, or positioning) while tolerating acceptable cross-platform rendering variations. - * + * * Parameters: * - MIN_CLUSTER_SIZE: Minimum size for a cluster to be considered significant (default: 4) * - MAX_TOTAL_DIFF_PIXELS: Maximum allowed non-line-shift difference pixels (default: 40) * Note: These can be adjusted for further updation - * + * * Note for contributors: When running tests locally, you may not see these differences as they * mainly appear when tests run on different operating systems or browser rendering engines. * However, the same code may produce slightly different renderings on CI environments, particularly @@ -140,7 +140,7 @@ export async function checkMatch(actual, expected, p5) { if (narrow) { scale *= 2; } - + for (const img of [actual, expected]) { img.resize( Math.ceil(img.width * scale), @@ -151,28 +151,28 @@ export async function checkMatch(actual, expected, p5) { // Ensure both images have the same dimensions const width = expected.width; const height = expected.height; - + // Create canvases with background color const actualCanvas = p5.createGraphics(width, height); const expectedCanvas = p5.createGraphics(width, height); actualCanvas.pixelDensity(1); expectedCanvas.pixelDensity(1); - + actualCanvas.background(BG); expectedCanvas.background(BG); - + actualCanvas.image(actual, 0, 0); expectedCanvas.image(expected, 0, 0); - + // Load pixel data actualCanvas.loadPixels(); expectedCanvas.loadPixels(); - + // Create diff output canvas const diffCanvas = p5.createGraphics(width, height); diffCanvas.pixelDensity(1); diffCanvas.loadPixels(); - + // Run pixelmatch const diffCount = pixelmatch( actualCanvas.pixels, @@ -180,13 +180,13 @@ export async function checkMatch(actual, expected, p5) { diffCanvas.pixels, width, height, - { + { threshold: 0.5, includeAA: false, alpha: 0.1 } ); - + // If no differences, return early if (diffCount === 0) { actualCanvas.remove(); @@ -194,19 +194,19 @@ export async function checkMatch(actual, expected, p5) { diffCanvas.updatePixels(); return { ok: true, diff: diffCanvas }; } - + // Post-process to identify and filter out isolated differences const visited = new Set(); const clusterSizes = []; - + for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const pos = (y * width + x) * 4; - + // If this is a diff pixel (red in pixelmatch output) and not yet visited if ( - diffCanvas.pixels[pos] === 255 && - diffCanvas.pixels[pos + 1] === 0 && + diffCanvas.pixels[pos] === 255 && + diffCanvas.pixels[pos + 1] === 0 && diffCanvas.pixels[pos + 2] === 0 && !visited.has(pos) ) { @@ -216,37 +216,37 @@ export async function checkMatch(actual, expected, p5) { } } } - + // Define significance thresholds const MIN_CLUSTER_SIZE = 4; // Minimum pixels in a significant cluster const MAX_TOTAL_DIFF_PIXELS = 40; // Maximum total different pixels // Determine if the differences are significant const nonLineShiftClusters = clusterSizes.filter(c => !c.isLineShift && c.size >= MIN_CLUSTER_SIZE); - + // Calculate significant differences excluding line shifts const significantDiffPixels = nonLineShiftClusters.reduce((sum, c) => sum + c.size, 0); // Update the diff canvas diffCanvas.updatePixels(); - + // Clean up canvases actualCanvas.remove(); expectedCanvas.remove(); - + // Determine test result const ok = ( - diffCount === 0 || + diffCount === 0 || ( - significantDiffPixels === 0 || + significantDiffPixels === 0 || ( - (significantDiffPixels <= MAX_TOTAL_DIFF_PIXELS) && + (significantDiffPixels <= MAX_TOTAL_DIFF_PIXELS) && (nonLineShiftClusters.length <= 2) // Not too many significant clusters ) ) ); - return { + return { ok, diff: diffCanvas, details: { @@ -264,31 +264,31 @@ function findClusterSize(pixels, startX, startY, width, height, radius, visited) const queue = [{x: startX, y: startY}]; let size = 0; const clusterPixels = []; - + while (queue.length > 0) { const {x, y} = queue.shift(); const pos = (y * width + x) * 4; - + // Skip if already visited if (visited.has(pos)) continue; - + // Skip if not a diff pixel if (pixels[pos] !== 255 || pixels[pos + 1] !== 0 || pixels[pos + 2] !== 0) continue; - + // Mark as visited visited.add(pos); size++; clusterPixels.push({x, y}); - + // Add neighbors to queue for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { const nx = x + dx; const ny = y + dy; - + // Skip if out of bounds if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; - + // Skip if already visited const npos = (ny * width + nx) * 4; if (!visited.has(npos)) { @@ -302,20 +302,20 @@ function findClusterSize(pixels, startX, startY, width, height, radius, visited) if (clusterPixels.length > 0) { // Count pixels with limited neighbors (line-like characteristic) let linelikePixels = 0; - + for (const {x, y} of clusterPixels) { // Count neighbors let neighbors = 0; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; // Skip self - + const nx = x + dx; const ny = y + dy; - + // Skip if out of bounds if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; - + const npos = (ny * width + nx) * 4; // Check if neighbor is a diff pixel if (pixels[npos] === 255 && pixels[npos + 1] === 0 && pixels[npos + 2] === 0) { @@ -323,13 +323,13 @@ function findClusterSize(pixels, startX, startY, width, height, radius, visited) } } } - + // Line-like pixels typically have 1-2 neighbors if (neighbors <= 2) { linelikePixels++; } } - + // If most pixels (>80%) in the cluster have ≤2 neighbors, it's likely a line shift isLineShift = linelikePixels / clusterPixels.length > 0.8; } @@ -407,8 +407,8 @@ export function visualTest( const actual = []; // Generate screenshots - await callback(myp5, () => { - const img = myp5.get(); + await callback(myp5, async () => { + const img = await myp5.get(); img.pixelDensity(1); actual.push(img); }); From 4fd6c19b68fee4632793ba5d23604165547f2eb7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 28 Jul 2025 18:29:43 -0400 Subject: [PATCH 36/98] Add fixes for other gl-specific cases --- src/webgl/3d_primitives.js | 2 +- src/webgpu/p5.RendererWebGPU.js | 29 +- test/unit/visual/cases/webgpu.js | 424 ++++++++++++++++-------------- test/unit/webgl/p5.Framebuffer.js | 13 +- 4 files changed, 246 insertions(+), 222 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 8c29e3ea2d..386a64535d 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -1869,7 +1869,7 @@ function primitives3D(p5, fn){ if (typeof args[4] === 'undefined') { // Use the retained mode for drawing rectangle, // if args for rounding rectangle is not provided by user. - const perPixelLighting = this._pInst._glAttributes.perPixelLighting; + const perPixelLighting = this._pInst._glAttributes?.perPixelLighting; const detailX = args[4] || (perPixelLighting ? 1 : 24); const detailY = args[5] || (perPixelLighting ? 1 : 16); const gid = `rect|${detailX}|${detailY}`; diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 29ffcbf40d..cad16a1765 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -210,7 +210,7 @@ class RendererWebGPU extends Renderer3D { this._getWebGPUColorFormat(activeFramebuffer) : this.presentationFormat; - const requestedSampleCount = activeFramebuffer ? + const requestedSampleCount = activeFramebuffer ? (activeFramebuffer.antialias ? activeFramebuffer.antialiasSamples : 1) : (this.antialias || 1); const sampleCount = this._getValidSampleCount(requestedSampleCount); @@ -564,6 +564,12 @@ class RendererWebGPU extends Renderer3D { this._viewport = [0, 0, this.width, this.height]; } + _createPixelsArray() { + this.pixels = new Uint8Array( + this.width * this.pixelDensity() * this.height * this.pixelDensity() * 4 + ); + } + viewport() {} zClipRange() { @@ -605,25 +611,25 @@ class RendererWebGPU extends Renderer3D { if (!buffers) return; const commandEncoder = this.device.createCommandEncoder(); - + // Use framebuffer texture if active, otherwise use canvas texture const activeFramebuffer = this.activeFramebuffer(); - const colorTexture = activeFramebuffer ? - (activeFramebuffer.aaColorTexture || activeFramebuffer.colorTexture) : + const colorTexture = activeFramebuffer ? + (activeFramebuffer.aaColorTexture || activeFramebuffer.colorTexture) : this.drawingContext.getCurrentTexture(); - + const colorAttachment = { view: colorTexture.createView(), loadOp: "load", storeOp: "store", // If using multisampled texture, resolve to non-multisampled texture - resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture ? + resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture ? activeFramebuffer.colorTexture.createView() : undefined, }; // Use framebuffer depth texture if active, otherwise use canvas depth texture - const depthTexture = activeFramebuffer ? - (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : + const depthTexture = activeFramebuffer ? + (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : this.depthTexture; const depthTextureView = depthTexture?.createView(); const renderPassDescriptor = { @@ -1313,14 +1319,14 @@ class RendererWebGPU extends Renderer3D { framebuffer.aaDepthTexture = this.device.createTexture(aaDepthTextureDescriptor); } } - + // Clear the framebuffer textures after creation this._clearFramebufferTextures(framebuffer); } _clearFramebufferTextures(framebuffer) { const commandEncoder = this.device.createCommandEncoder(); - + // Clear the color texture (and multisampled texture if it exists) const colorTexture = framebuffer.aaColorTexture || framebuffer.colorTexture; const colorAttachment = { @@ -1328,7 +1334,7 @@ class RendererWebGPU extends Renderer3D { loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 }, - resolveTarget: framebuffer.aaColorTexture ? + resolveTarget: framebuffer.aaColorTexture ? framebuffer.colorTexture.createView() : undefined, }; @@ -1664,7 +1670,6 @@ class RendererWebGPU extends Renderer3D { await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); const pixelData = new Uint8Array(mappedRange.slice(0, bufferSize)); - console.log(pixelData) const region = new Image(width, height); region.pixelDensity(pd); diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 9c0502ce39..334abc1be1 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -1,176 +1,194 @@ -import { vi } from 'vitest'; -import p5 from '../../../../src/app'; -import { visualSuite, visualTest } from '../visualTest'; -import rendererWebGPU from '../../../../src/webgpu/p5.RendererWebGPU'; +import { vi } from "vitest"; +import p5 from "../../../../src/app"; +import { visualSuite, visualTest } from "../visualTest"; +import rendererWebGPU from "../../../../src/webgpu/p5.RendererWebGPU"; p5.registerAddon(rendererWebGPU); -visualSuite('WebGPU', function() { - visualSuite('Shaders', function() { - visualTest('The color shader runs successfully', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - p5.background('white'); - for (const [i, color] of ['red', 'lime', 'blue'].entries()) { - p5.push(); - p5.rotate(p5.TWO_PI * (i / 3)); - p5.fill(color); - p5.translate(15, 0); - p5.noStroke(); - p5.circle(0, 0, 20); - p5.pop(); - } - await screenshot(); - }); - - visualTest('The stroke shader runs successfully', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - p5.background('white'); - for (const [i, color] of ['red', 'lime', 'blue'].entries()) { - p5.push(); - p5.rotate(p5.TWO_PI * (i / 3)); - p5.translate(15, 0); - p5.stroke(color); - p5.strokeWeight(2); - p5.circle(0, 0, 20); - p5.pop(); - } - await screenshot(); - }); - - visualTest('The material shader runs successfully', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - p5.background('white'); - p5.ambientLight(50); - p5.directionalLight(100, 100, 100, 0, 1, -1); - p5.pointLight(155, 155, 155, 0, -200, 500); - p5.specularMaterial(255); - p5.shininess(300); - for (const [i, color] of ['red', 'lime', 'blue'].entries()) { - p5.push(); - p5.rotate(p5.TWO_PI * (i / 3)); - p5.fill(color); - p5.translate(15, 0); - p5.noStroke(); - p5.sphere(10); - p5.pop(); - } - await screenshot(); - }); +visualSuite("WebGPU", function () { + visualSuite("Shaders", function () { + visualTest( + "The color shader runs successfully", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background("white"); + for (const [i, color] of ["red", "lime", "blue"].entries()) { + p5.push(); + p5.rotate(p5.TWO_PI * (i / 3)); + p5.fill(color); + p5.translate(15, 0); + p5.noStroke(); + p5.circle(0, 0, 20); + p5.pop(); + } + await screenshot(); + }, + ); + + visualTest( + "The stroke shader runs successfully", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background("white"); + for (const [i, color] of ["red", "lime", "blue"].entries()) { + p5.push(); + p5.rotate(p5.TWO_PI * (i / 3)); + p5.translate(15, 0); + p5.stroke(color); + p5.strokeWeight(2); + p5.circle(0, 0, 20); + p5.pop(); + } + await screenshot(); + }, + ); + + visualTest( + "The material shader runs successfully", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background("white"); + p5.ambientLight(50); + p5.directionalLight(100, 100, 100, 0, 1, -1); + p5.pointLight(155, 155, 155, 0, -200, 500); + p5.specularMaterial(255); + p5.shininess(300); + for (const [i, color] of ["red", "lime", "blue"].entries()) { + p5.push(); + p5.rotate(p5.TWO_PI * (i / 3)); + p5.fill(color); + p5.translate(15, 0); + p5.noStroke(); + p5.sphere(10); + p5.pop(); + } + await screenshot(); + }, + ); - visualTest('Shader hooks can be used', async function(p5, screenshot) { + visualTest("Shader hooks can be used", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); const myFill = p5.baseMaterialShader().modify({ - 'Vertex getWorldInputs': `(inputs: Vertex) { + "Vertex getWorldInputs": `(inputs: Vertex) { var result = inputs; result.position.y += 10.0 * sin(inputs.position.x * 0.25); return result; }`, }); const myStroke = p5.baseStrokeShader().modify({ - 'StrokeVertex getWorldInputs': `(inputs: StrokeVertex) { + "StrokeVertex getWorldInputs": `(inputs: StrokeVertex) { var result = inputs; result.position.y += 10.0 * sin(inputs.position.x * 0.25); return result; }`, }); - p5.background('black'); + p5.background("black"); p5.shader(myFill); p5.strokeShader(myStroke); - p5.fill('red'); - p5.stroke('white'); + p5.fill("red"); + p5.stroke("white"); p5.strokeWeight(5); p5.circle(0, 0, 30); await screenshot(); }); - visualTest('Textures in the material shader work', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - const tex = p5.createImage(50, 50); - tex.loadPixels(); - for (let x = 0; x < tex.width; x++) { - for (let y = 0; y < tex.height; y++) { - const off = (x + y * tex.width) * 4; - tex.pixels[off] = p5.round((x / tex.width) * 255); - tex.pixels[off + 1] = p5.round((y / tex.height) * 255); - tex.pixels[off + 2] = 0; - tex.pixels[off + 3] = 255; + visualTest( + "Textures in the material shader work", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const tex = p5.createImage(50, 50); + tex.loadPixels(); + for (let x = 0; x < tex.width; x++) { + for (let y = 0; y < tex.height; y++) { + const off = (x + y * tex.width) * 4; + tex.pixels[off] = p5.round((x / tex.width) * 255); + tex.pixels[off + 1] = p5.round((y / tex.height) * 255); + tex.pixels[off + 2] = 0; + tex.pixels[off + 3] = 255; + } } - } - tex.updatePixels(); - p5.texture(tex); - p5.plane(p5.width, p5.height); + tex.updatePixels(); + p5.texture(tex); + p5.plane(p5.width, p5.height); - await screenshot(); - }); + await screenshot(); + }, + ); }); - visualSuite('Framebuffers', function() { - visualTest('Basic framebuffer draw to canvas', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - - // Create a framebuffer - const fbo = p5.createFramebuffer({ width: 25, height: 25 }); - - // Draw to the framebuffer - fbo.draw(() => { - p5.background(255, 0, 0); // Red background - p5.fill(0, 255, 0); // Green circle + visualSuite("Framebuffers", function () { + visualTest( + "Basic framebuffer draw to canvas", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create a framebuffer + const fbo = p5.createFramebuffer({ width: 25, height: 25 }); + + // Draw to the framebuffer + fbo.draw(() => { + p5.background(255, 0, 0); // Red background + p5.fill(0, 255, 0); // Green circle + p5.noStroke(); + p5.circle(12.5, 12.5, 20); + }); + + // Draw the framebuffer to the main canvas + p5.background(0, 0, 255); // Blue background + p5.texture(fbo); p5.noStroke(); - p5.circle(12.5, 12.5, 20); - }); - - // Draw the framebuffer to the main canvas - p5.background(0, 0, 255); // Blue background - p5.texture(fbo); - p5.noStroke(); - p5.plane(25, 25); - - await screenshot(); - }); - - visualTest('Framebuffer with different sizes', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - - // Create two different sized framebuffers - const fbo1 = p5.createFramebuffer({ width: 20, height: 20 }); - const fbo2 = p5.createFramebuffer({ width: 15, height: 15 }); - - // Draw to first framebuffer - fbo1.draw(() => { - p5.background(255, 100, 100); - p5.fill(255, 255, 0); + p5.plane(25, 25); + + await screenshot(); + }, + ); + + visualTest( + "Framebuffer with different sizes", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create two different sized framebuffers + const fbo1 = p5.createFramebuffer({ width: 20, height: 20 }); + const fbo2 = p5.createFramebuffer({ width: 15, height: 15 }); + + // Draw to first framebuffer + fbo1.draw(() => { + p5.background(255, 100, 100); + p5.fill(255, 255, 0); + p5.noStroke(); + p5.rect(5, 5, 10, 10); + }); + + // Draw to second framebuffer + fbo2.draw(() => { + p5.background(100, 255, 100); + p5.fill(255, 0, 255); + p5.noStroke(); + p5.circle(7.5, 7.5, 10); + }); + + // Draw both to main canvas + p5.background(50); + p5.push(); + p5.translate(-12.5, -12.5); + p5.texture(fbo1); p5.noStroke(); - p5.rect(5, 5, 10, 10); - }); + p5.plane(20, 20); + p5.pop(); - // Draw to second framebuffer - fbo2.draw(() => { - p5.background(100, 255, 100); - p5.fill(255, 0, 255); + p5.push(); + p5.translate(12.5, 12.5); + p5.texture(fbo2); p5.noStroke(); - p5.circle(7.5, 7.5, 10); - }); - - // Draw both to main canvas - p5.background(50); - p5.push(); - p5.translate(-12.5, -12.5); - p5.texture(fbo1); - p5.noStroke(); - p5.plane(20, 20); - p5.pop(); + p5.plane(15, 15); + p5.pop(); - p5.push(); - p5.translate(12.5, 12.5); - p5.texture(fbo2); - p5.noStroke(); - p5.plane(15, 15); - p5.pop(); + await screenshot(); + }, + ); - await screenshot(); - }); - - visualTest('Auto-sized framebuffer', async function(p5, screenshot) { + visualTest("Auto-sized framebuffer", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); // Create auto-sized framebuffer (should match canvas size) @@ -202,69 +220,75 @@ visualSuite('WebGPU', function() { await screenshot(); }); - visualTest('Auto-sized framebuffer after canvas resize', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - - // Create auto-sized framebuffer - const fbo = p5.createFramebuffer(); - - // Resize the canvas (framebuffer should auto-resize) - p5.resizeCanvas(30, 30); - - // Draw to the framebuffer after resize - fbo.draw(() => { - p5.background(100, 0, 100); - p5.fill(0, 255, 255); + visualTest( + "Auto-sized framebuffer after canvas resize", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create auto-sized framebuffer + const fbo = p5.createFramebuffer(); + + // Resize the canvas (framebuffer should auto-resize) + p5.resizeCanvas(30, 30); + + // Draw to the framebuffer after resize + fbo.draw(() => { + p5.background(100, 0, 100); + p5.fill(0, 255, 255); + p5.noStroke(); + // Draw a shape that fills the new size + p5.rect(5, 5, 20, 20); + p5.fill(255, 255, 0); + p5.circle(15, 15, 10); + }); + + // Draw the framebuffer to the main canvas + p5.texture(fbo); p5.noStroke(); - // Draw a shape that fills the new size - p5.rect(5, 5, 20, 20); - p5.fill(255, 255, 0); - p5.circle(15, 15, 10); - }); - - // Draw the framebuffer to the main canvas - p5.texture(fbo); - p5.noStroke(); - p5.plane(30, 30); - - await screenshot(); - }); - - visualTest('Fixed-size framebuffer after manual resize', async function(p5, screenshot) { - await p5.createCanvas(50, 50, p5.WEBGPU); - - // Create fixed-size framebuffer - const fbo = p5.createFramebuffer({ width: 20, height: 20 }); - - // Draw initial content - fbo.draw(() => { - p5.background(255, 200, 100); - p5.fill(0, 100, 200); - p5.noStroke(); - p5.circle(10, 10, 15); - }); - - // Manually resize the framebuffer - fbo.resize(35, 25); - - // Draw new content to the resized framebuffer - fbo.draw(() => { - p5.background(200, 255, 100); - p5.fill(200, 0, 100); + p5.plane(30, 30); + + await screenshot(); + }, + ); + + visualTest( + "Fixed-size framebuffer after manual resize", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + + // Create fixed-size framebuffer + const fbo = p5.createFramebuffer({ width: 20, height: 20 }); + + // Draw initial content + fbo.draw(() => { + p5.background(255, 200, 100); + p5.fill(0, 100, 200); + p5.noStroke(); + p5.circle(10, 10, 15); + }); + + // Manually resize the framebuffer + fbo.resize(35, 25); + + // Draw new content to the resized framebuffer + fbo.draw(() => { + p5.background(200, 255, 100); + p5.fill(200, 0, 100); + p5.noStroke(); + // Draw content that uses the new size + p5.rect(5, 5, 25, 15); + p5.fill(0, 0, 255); + p5.circle(17.5, 12.5, 8); + }); + + // Draw the resized framebuffer to the main canvas + p5.background(50); + p5.texture(fbo); p5.noStroke(); - // Draw content that uses the new size - p5.rect(5, 5, 25, 15); - p5.fill(0, 0, 255); - p5.circle(17.5, 12.5, 8); - }); + p5.plane(35, 25); - // Draw the resized framebuffer to the main canvas - p5.background(50); - p5.texture(fbo); - p5.noStroke(); - p5.plane(35, 25); - - await screenshot(); - }); + await screenshot(); + }, + ); }); }); diff --git a/test/unit/webgl/p5.Framebuffer.js b/test/unit/webgl/p5.Framebuffer.js index f97cb6b57d..6a6d556351 100644 --- a/test/unit/webgl/p5.Framebuffer.js +++ b/test/unit/webgl/p5.Framebuffer.js @@ -461,7 +461,7 @@ suite('p5.Framebuffer', function() { } }); - test('get() creates a p5.Image with 1x pixel density', function() { + test('get() creates a p5.Image matching the source pixel density', function() { const mainCanvas = myp5.createCanvas(20, 20, myp5.WEBGL); myp5.pixelDensity(2); const fbo = myp5.createFramebuffer(); @@ -482,22 +482,17 @@ suite('p5.Framebuffer', function() { myp5.pop(); }); const img = fbo.get(); - const p2d = myp5.createGraphics(20, 20); - p2d.pixelDensity(1); myp5.image(fbo, -10, -10); - p2d.image(mainCanvas, 0, 0); fbo.loadPixels(); img.loadPixels(); - p2d.loadPixels(); expect(img.width).to.equal(fbo.width); expect(img.height).to.equal(fbo.height); - expect(img.pixels.length).to.equal(fbo.pixels.length / 4); - // The pixels should be approximately the same in the 1x image as when we - // draw the framebuffer onto a 1x canvas + expect(img.pixels.length).to.equal(fbo.pixels.length); + // The pixels should be approximately the same as the framebuffer's for (let i = 0; i < img.pixels.length; i++) { - expect(img.pixels[i]).to.be.closeTo(p2d.pixels[i], 2); + expect(img.pixels[i]).to.be.closeTo(fbo.pixels[i], 2); } }); }); From edce0299a0a6e89cbc867ad2b9d47f59df4aad57 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Mon, 28 Jul 2025 21:00:19 -0400 Subject: [PATCH 37/98] Fix canvas readback --- src/core/main.js | 4 +- src/webgpu/p5.RendererWebGPU.js | 253 ++++++++++++++++++++++++----- test/unit/webgpu/p5.Framebuffer.js | 35 ++-- 3 files changed, 235 insertions(+), 57 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index a5c9a6c93d..f9fc1c6559 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -468,11 +468,11 @@ for (const k in constants) { * If `setup()` is declared `async` (e.g. `async function setup()`), * execution pauses at each `await` until its promise resolves. * For example, `font = await loadFont(...)` waits for the font asset - * to load because `loadFont()` function returns a promise, and the await + * to load because `loadFont()` function returns a promise, and the await * keyword means the program will wait for the promise to resolve. * This ensures that all assets are fully loaded before the sketch continues. - * + * * loading assets. * * Note: `setup()` doesn’t have to be declared, but it’s common practice to do so. diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index cad16a1765..383cf9f97f 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -23,6 +23,9 @@ class RendererWebGPU extends Renderer3D { // Single reusable staging buffer for pixel reading this.pixelReadBuffer = null; this.pixelReadBufferSize = 0; + + // Lazy readback texture for main canvas pixel reading + this.canvasReadbackTexture = null; } async setupContext() { @@ -62,6 +65,12 @@ class RendererWebGPU extends Renderer3D { format: this.depthFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT, }); + + // Destroy existing readback texture when size changes + if (this.canvasReadbackTexture && this.canvasReadbackTexture.destroy) { + this.canvasReadbackTexture.destroy(); + this.canvasReadbackTexture = null; + } } clear(...args) { @@ -71,16 +80,28 @@ class RendererWebGPU extends Renderer3D { const _a = args[3] || 0; const commandEncoder = this.device.createCommandEncoder(); - const textureView = this.drawingContext.getCurrentTexture().createView(); + + // Use framebuffer texture if active, otherwise use canvas texture + const activeFramebuffer = this.activeFramebuffer(); + const colorTexture = activeFramebuffer ? + (activeFramebuffer.aaColorTexture || activeFramebuffer.colorTexture) : + this.drawingContext.getCurrentTexture(); const colorAttachment = { - view: textureView, + view: colorTexture.createView(), 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.colorTexture.createView() : undefined, }; - const depthTextureView = this.depthTexture?.createView(); + // Use framebuffer depth texture if active, otherwise use canvas depth texture + const depthTexture = activeFramebuffer ? + (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : + this.depthTexture; + const depthTextureView = depthTexture?.createView(); const depthAttachment = depthTextureView ? { view: depthTextureView, @@ -1202,6 +1223,11 @@ class RendererWebGPU extends Renderer3D { 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 ////////////////////////////////////////////// @@ -1435,31 +1461,56 @@ class RendererWebGPU extends Renderer3D { bindFramebuffer(framebuffer) {} async readFramebufferPixels(framebuffer) { - // 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 bufferSize = width * height * bytesPerPixel; - - const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); + 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 } }, - { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel, rowsPerImage: height }, + { + 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); - const result = new Uint8Array(mappedRange.slice(0, bufferSize)); - stagingBuffer.unmap(); - return result; + // If alignment was needed, extract the actual pixel data + if (alignedBytesPerRow === unalignedBytesPerRow) { + const result = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel)); + stagingBuffer.unmap(); + return result; + } else { + // Need to extract pixel data from aligned buffer + const 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(); + return result; + } } async readFramebufferPixel(framebuffer, x, y) { @@ -1467,7 +1518,10 @@ class RendererWebGPU extends Renderer3D { await this.queue.onSubmittedWorkDone(); const bytesPerPixel = 4; - const stagingBuffer = this._ensurePixelReadBuffer(bytesPerPixel); + const alignedBytesPerRow = this._alignBytesPerRow(bytesPerPixel); + const bufferSize = alignedBytesPerRow; + + const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyTextureToBuffer( @@ -1475,14 +1529,14 @@ class RendererWebGPU extends Renderer3D { texture: framebuffer.colorTexture, origin: { x, y, z: 0 } }, - { buffer: stagingBuffer, bytesPerRow: bytesPerPixel }, + { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, { width: 1, height: 1, depthOrArrayLayers: 1 } ); this.device.queue.submit([commandEncoder.finish()]); - await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bytesPerPixel); - const mappedRange = stagingBuffer.getMappedRange(0, bytesPerPixel); + 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]]; @@ -1497,7 +1551,9 @@ class RendererWebGPU extends Renderer3D { const width = w * framebuffer.density; const height = h * framebuffer.density; const bytesPerPixel = 4; - const bufferSize = width * height * bytesPerPixel; + const unalignedBytesPerRow = width * bytesPerPixel; + const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow); + const bufferSize = alignedBytesPerRow * height; const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); @@ -1505,9 +1561,10 @@ class RendererWebGPU extends Renderer3D { commandEncoder.copyTextureToBuffer( { texture: framebuffer.colorTexture, + mipLevel: 0, origin: { x: x * framebuffer.density, y: y * framebuffer.density, z: 0 } }, - { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, { width, height, depthOrArrayLayers: 1 } ); @@ -1515,7 +1572,20 @@ class RendererWebGPU extends Renderer3D { await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); - const pixelData = new Uint8Array(mappedRange.slice(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); + } + } // WebGPU doesn't need vertical flipping unlike WebGL const region = new Image(width, height); @@ -1559,24 +1629,75 @@ class RendererWebGPU extends Renderer3D { // Main canvas pixel methods ////////////////////////////////////////////// - async loadPixels() { - // Ensure all pending GPU work is complete before reading pixels - await this.queue.onSubmittedWorkDone(); + _ensureCanvasReadbackTexture() { + if (!this.canvasReadbackTexture) { + const width = Math.ceil(this.width * this._pixelDensity); + const height = Math.ceil(this.height * this._pixelDensity); + + this.canvasReadbackTexture = this.device.createTexture({ + size: { width, height, depthOrArrayLayers: 1 }, + format: this.presentationFormat, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC, + }); + } + return this.canvasReadbackTexture; + } + + _copyCanvasToReadbackTexture() { + // Get the current canvas texture BEFORE any awaiting + const canvasTexture = this.drawingContext.getCurrentTexture(); + + // Ensure readback texture exists + const readbackTexture = this._ensureCanvasReadbackTexture(); + + // Copy canvas texture to readback texture immediately + const copyEncoder = this.device.createCommandEncoder(); + copyEncoder.copyTextureToTexture( + { texture: canvasTexture }, + { texture: readbackTexture }, + { + width: Math.ceil(this.width * this._pixelDensity), + height: Math.ceil(this.height * this._pixelDensity), + depthOrArrayLayers: 1 + } + ); + this.device.queue.submit([copyEncoder.finish()]); + + return readbackTexture; + } + + _convertBGRtoRGB(pixelData) { + // Convert BGR to RGB by swapping red and blue channels + for (let i = 0; i < pixelData.length; i += 4) { + const temp = pixelData[i]; // Store red + pixelData[i] = pixelData[i + 2]; // Red = Blue + pixelData[i + 2] = temp; // Blue = Red + // Green (i + 1) and Alpha (i + 3) stay the same + } + return pixelData; + } + async loadPixels() { const width = this.width * this._pixelDensity; const height = this.height * this._pixelDensity; + + // Copy canvas to readback texture + const readbackTexture = this._copyCanvasToReadbackTexture(); + + // Now we can safely await + await this.queue.onSubmittedWorkDone(); + const bytesPerPixel = 4; - const bufferSize = width * height * bytesPerPixel; + const unalignedBytesPerRow = width * bytesPerPixel; + const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow); + const bufferSize = alignedBytesPerRow * height; const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); - // Get the current canvas texture - const canvasTexture = this.drawingContext.getCurrentTexture(); - const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyTextureToBuffer( - { texture: canvasTexture }, - { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { texture: readbackTexture }, + { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, { width, height, depthOrArrayLayers: 1 } ); @@ -1584,36 +1705,58 @@ class RendererWebGPU extends Renderer3D { await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); - this.pixels = new Uint8Array(mappedRange.slice(0, bufferSize)); + + if (alignedBytesPerRow === unalignedBytesPerRow) { + this.pixels = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel)); + } else { + // Need to extract pixel data from aligned buffer + this.pixels = 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; + this.pixels.set(mappedData.subarray(srcOffset, srcOffset + unalignedBytesPerRow), dstOffset); + } + } + + // Convert BGR to RGB for main canvas + this._convertBGRtoRGB(this.pixels); stagingBuffer.unmap(); return this.pixels; } async _getPixel(x, y) { - // Ensure all pending GPU work is complete before reading pixels + // Copy canvas to readback texture + const readbackTexture = this._copyCanvasToReadbackTexture(); + + // Now we can safely await await this.queue.onSubmittedWorkDone(); const bytesPerPixel = 4; - const stagingBuffer = this._ensurePixelReadBuffer(bytesPerPixel); + const alignedBytesPerRow = this._alignBytesPerRow(bytesPerPixel); + const bufferSize = alignedBytesPerRow; + + const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); - const canvasTexture = this.drawingContext.getCurrentTexture(); const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyTextureToBuffer( { - texture: canvasTexture, + texture: readbackTexture, origin: { x, y, z: 0 } }, - { buffer: stagingBuffer, bytesPerRow: bytesPerPixel }, + { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, { width: 1, height: 1, depthOrArrayLayers: 1 } ); this.device.queue.submit([commandEncoder.finish()]); - await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bytesPerPixel); - const mappedRange = stagingBuffer.getMappedRange(0, bytesPerPixel); + 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]]; + + // Convert BGR to RGB for main canvas - swap red and blue + const result = [pixelData[2], pixelData[1], pixelData[0], pixelData[3]]; stagingBuffer.unmap(); return result; @@ -1642,25 +1785,29 @@ class RendererWebGPU extends Renderer3D { // get(x,y,w,h) - region } - // Ensure all pending GPU work is complete before reading pixels + // Copy canvas to readback texture + const readbackTexture = this._copyCanvasToReadbackTexture(); + + // Now we can safely await await this.queue.onSubmittedWorkDone(); // Read region and create p5.Image const width = w * pd; const height = h * pd; const bytesPerPixel = 4; - const bufferSize = width * height * bytesPerPixel; + const unalignedBytesPerRow = width * bytesPerPixel; + const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow); + const bufferSize = alignedBytesPerRow * height; const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); - const canvasTexture = this.drawingContext.getCurrentTexture(); const commandEncoder = this.device.createCommandEncoder(); commandEncoder.copyTextureToBuffer( { - texture: canvasTexture, + texture: readbackTexture, origin: { x, y, z: 0 } }, - { buffer: stagingBuffer, bytesPerRow: width * bytesPerPixel }, + { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, { width, height, depthOrArrayLayers: 1 } ); @@ -1669,7 +1816,23 @@ class RendererWebGPU extends Renderer3D { await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); - const pixelData = new Uint8Array(mappedRange.slice(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); + } + } + + // Convert BGR to RGB for main canvas + this._convertBGRtoRGB(pixelData); const region = new Image(width, height); region.pixelDensity(pd); diff --git a/test/unit/webgpu/p5.Framebuffer.js b/test/unit/webgpu/p5.Framebuffer.js index 9fec2f070d..452585b6c8 100644 --- a/test/unit/webgpu/p5.Framebuffer.js +++ b/test/unit/webgpu/p5.Framebuffer.js @@ -1,4 +1,7 @@ import p5 from '../../../src/app.js'; +import rendererWebGPU from "../../../src/webgpu/p5.RendererWebGPU"; + +p5.registerAddon(rendererWebGPU); suite('WebGPU p5.Framebuffer', function() { let myp5; @@ -9,7 +12,6 @@ suite('WebGPU p5.Framebuffer', function() { window.devicePixelRatio = 1; myp5 = new p5(function(p) { p.setup = function() {}; - p.draw = function() {}; }); }); @@ -153,16 +155,26 @@ suite('WebGPU p5.Framebuffer', function() { await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); - let drawCallbackExecuted = false; + myp5.background(0, 255, 0); + fbo.draw(() => { - drawCallbackExecuted = true; - myp5.background(255, 0, 0); - myp5.fill(0, 255, 0); - myp5.noStroke(); - myp5.circle(5, 5, 8); + myp5.background(0, 0, 255); + // myp5.fill(0, 255, 0); }); - - expect(drawCallbackExecuted).to.equal(true); + await myp5.loadPixels(); + // Drawing should have gone to the framebuffer, leaving the main + // canvas the same + expect([...myp5.pixels.slice(0, 3)]).toEqual([0, 255, 0]); + await fbo.loadPixels(); + // The framebuffer should have content + expect([...fbo.pixels.slice(0, 3)]).toEqual([0, 0, 255]); + + // The content can be drawn back to the main canvas + myp5.imageMode(myp5.CENTER); + myp5.image(fbo, 0, 0); + await myp5.loadPixels(); + expect([...fbo.pixels.slice(0, 3)]).toEqual([0, 0, 255]); + expect([...myp5.pixels.slice(0, 3)]).toEqual([0, 0, 255]); }); test('can use framebuffer as texture', async function() { @@ -194,8 +206,9 @@ suite('WebGPU p5.Framebuffer', function() { expect(result).to.be.a('promise'); const pixels = await result; - expect(pixels).to.be.an('array'); + expect(pixels).toBeInstanceOf(Uint8Array); expect(pixels.length).to.equal(10 * 10 * 4); + expect([...pixels.slice(0, 4)]).toEqual([255, 0, 0, 255]); }); test('pixels property is set after loadPixels resolves', async function() { @@ -225,6 +238,7 @@ suite('WebGPU p5.Framebuffer', function() { const color = await result; expect(color).to.be.an('array'); expect(color).to.have.length(4); + expect([...color]).toEqual([100, 150, 200, 255]); }); test('get() returns a promise for region in WebGPU', async function() { @@ -242,6 +256,7 @@ suite('WebGPU p5.Framebuffer', function() { expect(region).to.be.an('object'); // Should be a p5.Image expect(region.width).to.equal(4); expect(region.height).to.equal(4); + expect([...region.pixels.slice(0, 4)]).toEqual([100, 150, 200, 255]); }); }); }); From 9fc319f996db258fbd6839ec5afb65682970d4f1 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 11:32:18 -0400 Subject: [PATCH 38/98] Start adding tests --- test/unit/visual/cases/webgpu.js | 11 +++++++---- .../000.png | Bin 0 -> 132 bytes .../metadata.json | 3 +++ .../Framebuffers/Auto-sized framebuffer/000.png | Bin 0 -> 396 bytes .../Auto-sized framebuffer/metadata.json | 3 +++ .../Basic framebuffer draw to canvas/000.png | Bin 0 -> 521 bytes .../metadata.json | 3 +++ .../000.png | Bin 0 -> 291 bytes .../metadata.json | 3 +++ .../Framebuffer with different sizes/000.png | Bin 0 -> 402 bytes .../metadata.json | 3 +++ 11 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Basic framebuffer draw to canvas/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Basic framebuffer draw to canvas/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/metadata.json diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 334abc1be1..5613d39091 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -130,7 +130,7 @@ visualSuite("WebGPU", function () { p5.background(255, 0, 0); // Red background p5.fill(0, 255, 0); // Green circle p5.noStroke(); - p5.circle(12.5, 12.5, 20); + p5.circle(0, 0, 20); }); // Draw the framebuffer to the main canvas @@ -157,7 +157,7 @@ visualSuite("WebGPU", function () { p5.background(255, 100, 100); p5.fill(255, 255, 0); p5.noStroke(); - p5.rect(5, 5, 10, 10); + p5.rect(-5, -5, 10, 10); }); // Draw to second framebuffer @@ -165,7 +165,7 @@ visualSuite("WebGPU", function () { p5.background(100, 255, 100); p5.fill(255, 0, 255); p5.noStroke(); - p5.circle(7.5, 7.5, 10); + p5.circle(0, 0, 10); }); // Draw both to main canvas @@ -197,6 +197,7 @@ visualSuite("WebGPU", function () { // Draw to the framebuffer fbo.draw(() => { p5.background(0); + p5.translate(-fbo.width / 2, -fbo.height / 2) p5.stroke(255); p5.strokeWeight(2); p5.noFill(); @@ -234,6 +235,7 @@ visualSuite("WebGPU", function () { // Draw to the framebuffer after resize fbo.draw(() => { p5.background(100, 0, 100); + p5.translate(-fbo.width / 2, -fbo.height / 2) p5.fill(0, 255, 255); p5.noStroke(); // Draw a shape that fills the new size @@ -264,7 +266,7 @@ visualSuite("WebGPU", function () { p5.background(255, 200, 100); p5.fill(0, 100, 200); p5.noStroke(); - p5.circle(10, 10, 15); + p5.circle(0, 0, 15); }); // Manually resize the framebuffer @@ -273,6 +275,7 @@ visualSuite("WebGPU", function () { // Draw new content to the resized framebuffer fbo.draw(() => { p5.background(200, 255, 100); + p5.translate(-fbo.width / 2, -fbo.height / 2) p5.fill(200, 0, 100); p5.noStroke(); // Draw content that uses the new size diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png b/test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png new file mode 100644 index 0000000000000000000000000000000000000000..972571631e30265fef58735d0666a400049f596e GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3jKx9jP7LeL$-D$|>^xl@Lp;3S zUf#&dV8Fp*c%we^*B5@*GYkrm?$Px$MoC0LRA@u(Spg1%FbIU7xtBR#Hh;!OQ;k_{hm~q|XJWEMHaWQC(6NXJGgIym zk#M=yxR4uwthDf~&Y>eIJ)t9s%2*2nYJpm@0#?dc<7R4Q-S8q8gCSld*KC?__O-O~B3B zp9{*+kCzz~sWmdwBU6=AG!hoJX>BtCQNX$PH-`fW-6#a29np!lXXSC5T6=)#6w~hm zNUVU1*wyr2cc50lPza@v9p^N*`NR894i`oqb7#+|sC?(bZZSYdw!F`G3hZ zzE)v!ly>ov=?2!_$UDw$MF9|}|1Qyyl<2&BKD1+58M<1l`k q9lxMfp*XGFx5nL1BP|--)|}p3hPVXA^49VI0000Px$!%0LzRA@u(m=DduFc8Jx9N+*PfCF#<4wwUQKyUyKh68W_4!{98z`@8%v#@;M zn(pu2SniT$BxP%V_x7&sA|OpMfQSsFXDXT^Jre|`h$+%h2JF@9D6zF4Yl_rPT}L-o zRwQM`?l>Y;-~Jg$NR;;o$hXlOm^=YqL!qt+Cw`x~_8J$1Odr7F>Y#j~3i6x)=M`{S zIuJq?1px>$zxjdGKnQV)fn*KvcbOnid;-?Ve|1zq>Of?mJ{T2{BnI}o@=E+wi!hrb z5GUV4APzz`2tk-8LC@^=&NB>gkfv&6@GQv$*&Y-?Se2;8VcSHD@4bW|45367VTmOO zOKuxMj}U~O5e2y}90;#%#OmxoX7?Wl$s1l&k+xqgHAMzvcKU)9i$3w~tIB$Z2B$x0 zc$IaeEyL$4mI?@!;G%fkG~1%&)ldQ9po7^o)`jWaHa;^nm7!;Db9eQfOSlE$qPM&N zEV)G|MbUM{SCKNH$JD{lN1S}t#xDoL<~zUl0we<@KoQ7Ke0(UoZiN0PqDW{GTK$zj z&}mIHG_HY!G9=w^ib!WXG@dCE%8+!wDI%Tm(0HauC_~cygHhxIAOktx0yLiA00000 LNkvXXu0mjfpm^EI literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Basic framebuffer draw to canvas/metadata.json b/test/unit/visual/screenshots/WebGPU/Framebuffers/Basic framebuffer draw to canvas/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Framebuffers/Basic framebuffer draw to canvas/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/000.png b/test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/000.png new file mode 100644 index 0000000000000000000000000000000000000000..1fb817b6b53c94fe1a9aa8d3412e4fc2a846cb6d GIT binary patch literal 291 zcmV+;0o?wHP)Px#-AP12RA@u(n9&V_Fc^e?R;mNwJT8GQ;XF8iuB7m2gf`_kLz<6QdRP9tuL;nl zl-|+6)iKDyTBMPK6%sT;Bc=pE%M|QpTN>FMok^H&3>r&m_QAoZDQ3L2U7 zYx|6h5%JJ4DXRO3IDfC&AFbQ8!L|4(+Jf1CV36Ms7taWdRL}-mD`A0F1x1TXK?}50 zhF0{T1zMn$Pv9?Sq?y-v6HO)C{l7;_JDTn@NK@bJCQ2-}{QI@UDwxuU5S~R!BbHbN pQyLM%vq)*g602ZJBSLr<=?C8Wx=p=J5DWkS002ovPDHLkV1j}$d4K=_ literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/metadata.json b/test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Framebuffers/Fixed-size framebuffer after manual resize/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/000.png b/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/000.png new file mode 100644 index 0000000000000000000000000000000000000000..155638a0c818aa5432045a7fc8a9c574122ea8e3 GIT binary patch literal 402 zcmV;D0d4+?P)Px$Oi4sRRA@u(n9UJ_Fc5{8GHk%yV*zRb&;=cESMPRU7f^s&0J+BoC<7;qGV;4l zjIzY##*k$4?R#&@B#=^;;K37k2a-$aRF{=fPS?X0s}*68fl{#|n1=?w|B1Ck0kACa z8r>EEdeIn1%UYoVh~DTvsRYQHU5`O<{d-4@XM`02U~93p9;e^lBMN7PGh&$#fbsV0 z&7a-6KGdwmAgqYB2mHHyur39WiL3|_A?j_gBCN=lD!|tIw6HR_ziU>j?#<3*ig*tq zE&{|=RD!3wv{!i2ibRJ9!hP7;c(%_vKx9FNAf8lh7_uOz;Dq*$xQy#DBtbAWp1VJu zkDxcYdYv`MtG!5qm@buU6VzJXfRN9Kkpj!fY`WGM%&h(+uK_XYm`AU5-KE+AA{llx wv^Nd9TV*yT%r+73jhEB07*qoM6N<$f)?JfjsO4v literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/metadata.json b/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Framebuffers/Framebuffer with different sizes/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From 76c010898d0623ae5da5e7b383df936698165d82 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 16:03:34 -0400 Subject: [PATCH 39/98] Add another test --- test/unit/visual/cases/webgpu.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 5613d39091..a57caa1ff1 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -116,6 +116,23 @@ visualSuite("WebGPU", function () { ); }); + visualSuite("Canvas Resizing", function () { + visualTest( + "Main canvas drawing after resize", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + // Resize the canvas + p5.resizeCanvas(30, 30); + // Draw to the main canvas after resize + p5.background(100, 0, 100); + p5.fill(0, 255, 255); + p5.noStroke(); + p5.circle(0, 0, 20); + await screenshot(); + }, + ); + }); + visualSuite("Framebuffers", function () { visualTest( "Basic framebuffer draw to canvas", @@ -251,6 +268,7 @@ visualSuite("WebGPU", function () { await screenshot(); }, + { focus: true } ); visualTest( From a3daa14cb606b36a67cbb1436cd49043f9be0ad2 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 16:13:43 -0400 Subject: [PATCH 40/98] Fix main canvas not being drawable after resizing --- src/webgpu/p5.RendererWebGPU.js | 11 ++++++----- test/unit/visual/cases/webgpu.js | 1 - .../000.png | Bin 132 -> 167 bytes 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 383cf9f97f..f85fd4607b 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -66,6 +66,9 @@ class RendererWebGPU extends Renderer3D { usage: GPUTextureUsage.RENDER_ATTACHMENT, }); + // Clear the main canvas after resize + this.clear(); + // Destroy existing readback texture when size changes if (this.canvasReadbackTexture && this.canvasReadbackTexture.destroy) { this.canvasReadbackTexture.destroy(); @@ -1287,8 +1290,6 @@ class RendererWebGPU extends Renderer3D { if (framebuffer.aaDepthTexture && framebuffer.aaDepthTexture.destroy) { framebuffer.aaDepthTexture.destroy(); } - // Clear cached views when recreating textures - framebuffer._colorTextureView = null; const baseDescriptor = { size: { @@ -1389,10 +1390,10 @@ class RendererWebGPU extends Renderer3D { } _getFramebufferColorTextureView(framebuffer) { - if (!framebuffer._colorTextureView && framebuffer.colorTexture) { - framebuffer._colorTextureView = framebuffer.colorTexture.createView(); + if (framebuffer.colorTexture) { + return framebuffer.colorTexture.createView(); } - return framebuffer._colorTextureView; + return null; } createFramebufferTextureHandle(framebufferTexture) { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index a57caa1ff1..28382dda25 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -268,7 +268,6 @@ visualSuite("WebGPU", function () { await screenshot(); }, - { focus: true } ); visualTest( diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png b/test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png index 972571631e30265fef58735d0666a400049f596e..01be2eb74e88adf3364c6690d614b349cfa91f03 100644 GIT binary patch delta 125 zcmV-@0D}L70jB|wF?L}|L_t(YOJhu7Ncqn&0Dy7SVtR%8;0o$F|7TOx<0*`(80KO1 z@up(Xm%RSPNaswXaO=>fWXttXaPcM_CZ%qbatZDB4YFpu2v>7 fE~Zq?$n!A(ky>$a7wkmT00000NkvXXu0mjfc*i*f delta 90 zcmV-g0Hyz@0fYgNF;hNCL_t(YOYPIK4FE6*1ToluY5MdJMa%#oSx48=^wHgNcugKP w>X?AIVzlpK)TmDwV{wTqCh%We1L^kwA6Dmx82|tP07*qoM6N<$g69P$<^TWy From 01110c9f77923fd1b704b1a87efdd3152fed0567 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 16:13:59 -0400 Subject: [PATCH 41/98] Add screenshots --- .../Main canvas drawing after resize/000.png | Bin 0 -> 225 bytes .../metadata.json | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGPU/Canvas Resizing/Main canvas drawing after resize/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Canvas Resizing/Main canvas drawing after resize/metadata.json diff --git a/test/unit/visual/screenshots/WebGPU/Canvas Resizing/Main canvas drawing after resize/000.png b/test/unit/visual/screenshots/WebGPU/Canvas Resizing/Main canvas drawing after resize/000.png new file mode 100644 index 0000000000000000000000000000000000000000..96849ce04c21325da234ba7192bc8c1bc63bce67 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3jKx9jP7LeL$-D$|W_!9ghIn|t zofgfFo`syt)gx-5OIFIp(nbZ(PLshJD(tw9jF0d>^YO=xU_ Date: Tue, 29 Jul 2025 18:06:51 -0400 Subject: [PATCH 42/98] Try setting different launch options --- vitest.workspace.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 7dfe0e6e82..e11dd11c53 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -38,7 +38,15 @@ export default defineWorkspace([ enabled: true, name: 'chrome', provider: 'webdriverio', - screenshotFailures: false + screenshotFailures: false, + launchOptions: { + args: [ + '--enable-unsafe-webgpu', + '--headless=new', + '--disable-gpu-sandbox', + '--no-sandbox', + ], + }, } } } From 02eef85af474bae627f5d4d8492b0db648b9da19 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 18:13:01 -0400 Subject: [PATCH 43/98] Test different options --- vitest.workspace.mjs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index e11dd11c53..636e7a8db3 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -39,14 +39,17 @@ export default defineWorkspace([ name: 'chrome', provider: 'webdriverio', screenshotFailures: false, - launchOptions: { - args: [ - '--enable-unsafe-webgpu', - '--headless=new', - '--disable-gpu-sandbox', - '--no-sandbox', - ], - }, + providerOptions: { + capabilities: { + 'goog:chromeOptions': { + args: [ + '--enable-unsafe-webgpu', + '--enable-features=Vulkan', + '--disable-vulkan-fallback-to-gl-for-testing' + ] + } + } + } } } } From 3c6c19506f1543a7f07fa8abd77d586a2ca3e700 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 18:31:00 -0400 Subject: [PATCH 44/98] Try sequential --- test/unit/visual/cases/webgpu.js | 2 +- test/unit/visual/visualTest.js | 5 ++++- test/unit/webgpu/p5.Framebuffer.js | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 28382dda25..dde49a9d16 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -311,4 +311,4 @@ visualSuite("WebGPU", function () { }, ); }); -}); +}, { sequential: true }); diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index 7d301d142b..7841c5a84d 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -52,7 +52,7 @@ let shiftThreshold = 2; export function visualSuite( name, callback, - { focus = false, skip = false, shiftThreshold: newShiftThreshold } = {} + { focus = false, skip = false, sequential = false, shiftThreshold: newShiftThreshold } = {} ) { let suiteFn = describe; if (focus) { @@ -61,6 +61,9 @@ export function visualSuite( if (skip) { suiteFn = suiteFn.skip; } + if (sequential) { + suiteFn = suiteFn.sequential; + } suiteFn(name, () => { let lastShiftThreshold let lastPrefix; diff --git a/test/unit/webgpu/p5.Framebuffer.js b/test/unit/webgpu/p5.Framebuffer.js index 452585b6c8..08789e92d1 100644 --- a/test/unit/webgpu/p5.Framebuffer.js +++ b/test/unit/webgpu/p5.Framebuffer.js @@ -1,9 +1,10 @@ +import describe from '../../../src/accessibility/describe.js'; import p5 from '../../../src/app.js'; import rendererWebGPU from "../../../src/webgpu/p5.RendererWebGPU"; p5.registerAddon(rendererWebGPU); -suite('WebGPU p5.Framebuffer', function() { +suite.sequential('WebGPU p5.Framebuffer', function() { let myp5; let prevPixelRatio; From e4509570a43e8744ab8ab17e68fc6fd464eed227 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 19:15:43 -0400 Subject: [PATCH 45/98] Attempt to install later chrome --- .github/workflows/ci-test.yml | 9 +++++++++ test/unit/visual/cases/webgpu.js | 2 +- test/unit/visual/visualTest.js | 5 +---- test/unit/webgpu/p5.Framebuffer.js | 3 +-- vitest.workspace.mjs | 10 +++++++--- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index bd3ca55ba0..31d8e36566 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -19,6 +19,15 @@ jobs: uses: actions/setup-node@v1 with: node-version: 20.x + - name: Install Chrome (latest stable) + run: | + sudo apt-get update + sudo apt-get install -y wget gnupg + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' + sudo apt-get update + sudo apt-get install -y google-chrome-stable + which google-chrome - name: Get node modules run: npm ci env: diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index dde49a9d16..28382dda25 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -311,4 +311,4 @@ visualSuite("WebGPU", function () { }, ); }); -}, { sequential: true }); +}); diff --git a/test/unit/visual/visualTest.js b/test/unit/visual/visualTest.js index 7841c5a84d..7d301d142b 100644 --- a/test/unit/visual/visualTest.js +++ b/test/unit/visual/visualTest.js @@ -52,7 +52,7 @@ let shiftThreshold = 2; export function visualSuite( name, callback, - { focus = false, skip = false, sequential = false, shiftThreshold: newShiftThreshold } = {} + { focus = false, skip = false, shiftThreshold: newShiftThreshold } = {} ) { let suiteFn = describe; if (focus) { @@ -61,9 +61,6 @@ export function visualSuite( if (skip) { suiteFn = suiteFn.skip; } - if (sequential) { - suiteFn = suiteFn.sequential; - } suiteFn(name, () => { let lastShiftThreshold let lastPrefix; diff --git a/test/unit/webgpu/p5.Framebuffer.js b/test/unit/webgpu/p5.Framebuffer.js index 08789e92d1..452585b6c8 100644 --- a/test/unit/webgpu/p5.Framebuffer.js +++ b/test/unit/webgpu/p5.Framebuffer.js @@ -1,10 +1,9 @@ -import describe from '../../../src/accessibility/describe.js'; import p5 from '../../../src/app.js'; import rendererWebGPU from "../../../src/webgpu/p5.RendererWebGPU"; p5.registerAddon(rendererWebGPU); -suite.sequential('WebGPU p5.Framebuffer', function() { +suite('WebGPU p5.Framebuffer', function() { let myp5; let prevPixelRatio; diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 636e7a8db3..611754fcdc 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -40,15 +40,19 @@ export default defineWorkspace([ provider: 'webdriverio', screenshotFailures: false, providerOptions: { - capabilities: { + capabilities: process.env.CI ? { 'goog:chromeOptions': { + binary: '/usr/bin/google-chrome', args: [ '--enable-unsafe-webgpu', + '--disable-dawn-features=disallow_unsafe_apis', + '--use-angle=default', '--enable-features=Vulkan', - '--disable-vulkan-fallback-to-gl-for-testing' + '--no-sandbox', + '--disable-dev-shm-usage', ] } - } + } : undefined } } } From f44629bac26cbc86c0eb04aca1549f19056da40e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 19:23:01 -0400 Subject: [PATCH 46/98] Add some debug info --- .github/workflows/ci-test.yml | 1 + vitest.workspace.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 31d8e36566..6a1bd0b549 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -28,6 +28,7 @@ jobs: sudo apt-get update sudo apt-get install -y google-chrome-stable which google-chrome + google-chrome --version - name: Get node modules run: npm ci env: diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 611754fcdc..2774fe1286 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -1,5 +1,6 @@ import { defineWorkspace } from 'vitest/config'; import vitePluginString from 'vite-plugin-string'; +console.log(`CI: ${process.env.CI}`) const plugins = [ vitePluginString({ From b281d332b5acfecacc32b3372f0929e2dc7ea226 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 19:39:48 -0400 Subject: [PATCH 47/98] Try different flags --- vitest.workspace.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 2774fe1286..1aa54097e5 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -48,9 +48,10 @@ export default defineWorkspace([ '--enable-unsafe-webgpu', '--disable-dawn-features=disallow_unsafe_apis', '--use-angle=default', - '--enable-features=Vulkan', + '--enable-features=Vulkan,SharedArrayBuffer', '--no-sandbox', '--disable-dev-shm-usage', + '--headless=new', ] } } : undefined From 777334131df1ad9e122aaf809911909e71e91175 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Tue, 29 Jul 2025 19:47:19 -0400 Subject: [PATCH 48/98] Does it work in xvfb? --- .github/workflows/ci-test.yml | 2 +- vitest.workspace.mjs | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 6a1bd0b549..67684ad745 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -34,7 +34,7 @@ jobs: env: CI: true - name: build and test - run: npm test + run: xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' npm test env: CI: true - name: report test coverage diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 1aa54097e5..9540289d24 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -46,12 +46,11 @@ export default defineWorkspace([ binary: '/usr/bin/google-chrome', args: [ '--enable-unsafe-webgpu', - '--disable-dawn-features=disallow_unsafe_apis', - '--use-angle=default', '--enable-features=Vulkan,SharedArrayBuffer', + '--disable-dawn-features=disallow_unsafe_apis', + '--disable-gpu-sandbox', '--no-sandbox', - '--disable-dev-shm-usage', - '--headless=new', + '--disable-dev-shm-usage' ] } } : undefined From cfeac932d62f32ea7cca9c88c52532d4a8432807 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 31 Jul 2025 20:42:15 -0400 Subject: [PATCH 49/98] Try enabling swiftshader --- vitest.workspace.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 9540289d24..d39212ef8e 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -45,12 +45,14 @@ export default defineWorkspace([ 'goog:chromeOptions': { binary: '/usr/bin/google-chrome', args: [ - '--enable-unsafe-webgpu', - '--enable-features=Vulkan,SharedArrayBuffer', '--disable-dawn-features=disallow_unsafe_apis', '--disable-gpu-sandbox', '--no-sandbox', - '--disable-dev-shm-usage' + '--disable-dev-shm-usage', + + '--enable-unsafe-webgpu', + '--use-angle=swiftshader', + '--enable-features=ReduceOpsTaskSplitting,Vulkan,VulkanFromANGLE,DefaultANGLEVulkan', ] } } : undefined From af5194a25a34ea21d2ba24c5b64ba99149cee4f4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 31 Jul 2025 20:46:35 -0400 Subject: [PATCH 50/98] less flags --- vitest.workspace.mjs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index d39212ef8e..7fadc2434e 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -45,11 +45,6 @@ export default defineWorkspace([ 'goog:chromeOptions': { binary: '/usr/bin/google-chrome', args: [ - '--disable-dawn-features=disallow_unsafe_apis', - '--disable-gpu-sandbox', - '--no-sandbox', - '--disable-dev-shm-usage', - '--enable-unsafe-webgpu', '--use-angle=swiftshader', '--enable-features=ReduceOpsTaskSplitting,Vulkan,VulkanFromANGLE,DefaultANGLEVulkan', From 88b4fe4d31f604be349493cbb569f10b40b9d569 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Thu, 31 Jul 2025 20:53:42 -0400 Subject: [PATCH 51/98] Try disabling dawn --- vitest.workspace.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 7fadc2434e..4220f9aa26 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -46,8 +46,13 @@ export default defineWorkspace([ binary: '/usr/bin/google-chrome', args: [ '--enable-unsafe-webgpu', - '--use-angle=swiftshader', - '--enable-features=ReduceOpsTaskSplitting,Vulkan,VulkanFromANGLE,DefaultANGLEVulkan', + '--enable-features=Vulkan', + '--use-cmd-decoder=passthrough', + '--disable-gpu-sandbox', + '--disable-software-rasterizer=false', + '--disable-dawn-features=disallow_unsafe_apis', + '--use-angle=vulkan', + '--use-vulkan=swiftshader', ] } } : undefined From ff83226f86cd39817fea848732fc4bcb46cac391 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 1 Aug 2025 08:05:42 -0400 Subject: [PATCH 52/98] Try installing swiftshader? --- .github/workflows/ci-test.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 67684ad745..4e4c749ca1 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -19,6 +19,17 @@ jobs: uses: actions/setup-node@v1 with: node-version: 20.x + - name: Install Vulkan SwiftShader + run: | + sudo apt-get update + sudo apt-get install -y libvulkan1 vulkan-tools mesa-vulkan-drivers + mkdir -p $HOME/swiftshader + curl -L https://github.com/google/swiftshader/releases/download/latest/Linux.zip -o swiftshader.zip + unzip swiftshader.zip -d $HOME/swiftshader + export VK_ICD_FILENAMES=$HOME/swiftshader/Linux/vk_swiftshader_icd.json + export VK_LAYER_PATH=$HOME/swiftshader/Linux + echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES" >> $GITHUB_ENV + echo "VK_LAYER_PATH=$VK_LAYER_PATH" >> $GITHUB_ENV - name: Install Chrome (latest stable) run: | sudo apt-get update @@ -34,7 +45,7 @@ jobs: env: CI: true - name: build and test - run: xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' npm test + run: npm test env: CI: true - name: report test coverage From 4314cf2d21d860cd0e41ae993b127c1c79bca201 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 1 Aug 2025 08:11:57 -0400 Subject: [PATCH 53/98] Just vulkan --- .github/workflows/ci-test.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 4e4c749ca1..12a6cde3b9 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -19,17 +19,10 @@ jobs: uses: actions/setup-node@v1 with: node-version: 20.x - - name: Install Vulkan SwiftShader + - name: Install Vulkan run: | sudo apt-get update sudo apt-get install -y libvulkan1 vulkan-tools mesa-vulkan-drivers - mkdir -p $HOME/swiftshader - curl -L https://github.com/google/swiftshader/releases/download/latest/Linux.zip -o swiftshader.zip - unzip swiftshader.zip -d $HOME/swiftshader - export VK_ICD_FILENAMES=$HOME/swiftshader/Linux/vk_swiftshader_icd.json - export VK_LAYER_PATH=$HOME/swiftshader/Linux - echo "VK_ICD_FILENAMES=$VK_ICD_FILENAMES" >> $GITHUB_ENV - echo "VK_LAYER_PATH=$VK_LAYER_PATH" >> $GITHUB_ENV - name: Install Chrome (latest stable) run: | sudo apt-get update From c01dee7285d2a90e5e535c9e2067c3c8b0307b23 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Fri, 1 Aug 2025 08:36:49 -0400 Subject: [PATCH 54/98] Try with xvfb --- .github/workflows/ci-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 12a6cde3b9..5289728040 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -38,7 +38,7 @@ jobs: env: CI: true - name: build and test - run: npm test + run: xvfb-run --auto-servernum --server-args='-screen 0 1280x1024x24' npm test env: CI: true - name: report test coverage From 7b3ed67261ce104e8ec1ad21aa7e5fafb2dc5dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Wed, 6 Aug 2025 12:05:41 -0700 Subject: [PATCH 55/98] Test ci flow with warp. --- .github/workflows/ci-test.yml | 22 ++++++---------------- vitest.workspace.mjs | 14 ++++++-------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 5289728040..0d3569d0a4 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -11,34 +11,24 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Use Node.js 20.x - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: 20.x - - name: Install Vulkan - run: | - sudo apt-get update - sudo apt-get install -y libvulkan1 vulkan-tools mesa-vulkan-drivers - name: Install Chrome (latest stable) run: | - sudo apt-get update - sudo apt-get install -y wget gnupg - wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' - sudo apt-get update - sudo apt-get install -y google-chrome-stable - which google-chrome - google-chrome --version + choco install googlechrome + & "C:\Program Files\Google\Chrome\Application\chrome.exe" --version - name: Get node modules run: npm ci env: CI: true - name: build and test - run: xvfb-run --auto-servernum --server-args='-screen 0 1280x1024x24' npm test + run: npm test env: CI: true - name: report test coverage diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 4220f9aa26..943943eabd 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -43,16 +43,14 @@ export default defineWorkspace([ providerOptions: { capabilities: process.env.CI ? { 'goog:chromeOptions': { - binary: '/usr/bin/google-chrome', args: [ '--enable-unsafe-webgpu', - '--enable-features=Vulkan', - '--use-cmd-decoder=passthrough', - '--disable-gpu-sandbox', - '--disable-software-rasterizer=false', - '--disable-dawn-features=disallow_unsafe_apis', - '--use-angle=vulkan', - '--use-vulkan=swiftshader', + '--headless=new', + '--no-sandbox', + '--disable-dev-shm-usage', + '--use-gl=angle', + '--use-angle=d3d11-warp', + '--disable-gpu-sandbox' ] } } : undefined From 22f5294a35bc6a8ec9f6942dd173e15b9ed91a3f Mon Sep 17 00:00:00 2001 From: charlotte Date: Thu, 7 Aug 2025 16:44:09 -0700 Subject: [PATCH 56/98] Fixes for CI. --- .github/workflows/ci-test.yml | 41 ++++++++++++++++++---- src/webgpu/p5.RendererWebGPU.js | 2 +- src/webgpu/shaders/utils.js | 4 +-- vitest.workspace.mjs | 61 ++++++++++++++++++++++++++++----- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 0d3569d0a4..416d01777d 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -11,27 +11,54 @@ on: jobs: test: - runs-on: windows-latest + strategy: + matrix: + include: + - os: windows-latest + browser: firefox + test-workspace: unit-tests-firefox + - os: ubuntu-latest + browser: chrome + test-workspace: unit-tests-chrome + + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - name: Use Node.js 20.x uses: actions/setup-node@v4 with: node-version: 20.x - - name: Install Chrome (latest stable) + + - name: Verify Firefox (Windows) + if: matrix.os == 'windows-latest' && matrix.browser == 'firefox' run: | - choco install googlechrome - & "C:\Program Files\Google\Chrome\Application\chrome.exe" --version + & "C:\Program Files\Mozilla Firefox\firefox.exe" --version + + - name: Verify Chrome (Ubuntu) + if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' + run: | + google-chrome --version + - name: Get node modules run: npm ci env: CI: true - - name: build and test - run: npm test + + - name: Build and test (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: npm test -- --project=${{ matrix.test-workspace }} + env: + CI: true + + - name: Build and test (Windows) + if: matrix.os == 'windows-latest' + run: npm test -- --project=${{ matrix.test-workspace }} env: CI: true - - name: report test coverage + + - name: Report test coverage run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json env: CI: true diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index f85fd4607b..2a4a9b3e8d 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -631,7 +631,7 @@ class RendererWebGPU extends Renderer3D { ////////////////////////////////////////////// _drawBuffers(geometry, { mode = constants.TRIANGLES, count = 1 }) { - const buffers = this.geometryBufferCache.getCached(geometry); + const buffers = this.geometryBufferCache.ensureCached(geometry); if (!buffers) return; const commandEncoder = this.device.createCommandEncoder(); diff --git a/src/webgpu/shaders/utils.js b/src/webgpu/shaders/utils.js index 0a313dfaf8..a6b79426e9 100644 --- a/src/webgpu/shaders/utils.js +++ b/src/webgpu/shaders/utils.js @@ -1,6 +1,6 @@ export const getTexture = ` -fn getTexture(texture: texture_2d, sampler: sampler, coord: vec2) -> vec4 { - let color = textureSample(texture, sampler, coord); +fn getTexture(texture: texture_2d, texSampler: sampler, coord: vec2) -> vec4 { + let color = textureSample(texture, texSampler, coord); let alpha = color.a; return vec4( select(color.rgb / alpha, vec3(0.0), alpha == 0.0), diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 943943eabd..23055b6680 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -23,7 +23,7 @@ export default defineWorkspace([ ], }, test: { - name: 'unit', + name: 'unit-tests-chrome', root: './', include: [ './test/unit/**/*.js', @@ -33,7 +33,7 @@ export default defineWorkspace([ './test/unit/assets/**/*', './test/unit/visual/visualTest.js', ], - testTimeout: 1000, + testTimeout: 10000, globals: true, browser: { enabled: true, @@ -44,18 +44,63 @@ export default defineWorkspace([ capabilities: process.env.CI ? { 'goog:chromeOptions': { args: [ - '--enable-unsafe-webgpu', - '--headless=new', '--no-sandbox', - '--disable-dev-shm-usage', - '--use-gl=angle', - '--use-angle=d3d11-warp', - '--disable-gpu-sandbox' + '--headless=new', + '--use-angle=vulkan', + '--enable-features=Vulkan', + '--disable-vulkan-surface', + '--enable-unsafe-webgpu', ] } } : undefined } } } + }, + { + plugins, + publicDir: './test', + bench: { + name: 'bench', + root: './', + include: [ + './test/bench/**/*.js' + ], + }, + test: { + name: 'unit-tests-firefox', + root: './', + include: [ + './test/unit/**/*.js', + ], + exclude: [ + './test/unit/spec.js', + './test/unit/assets/**/*', + './test/unit/visual/visualTest.js', + ], + testTimeout: 10000, + globals: true, + browser: { + enabled: true, + name: 'firefox', + provider: 'webdriverio', + screenshotFailures: false, + providerOptions: { + capabilities: process.env.CI ? { + 'moz:firefoxOptions': { + args: [ + '--headless', + '--enable-webgpu', + ], + prefs: { + 'dom.webgpu.enabled': true, + 'gfx.webgpu.force-enabled': true, + 'dom.webgpu.testing.assert-on-warnings': false, + } + } + } : undefined + } + } + } } ]); \ No newline at end of file From a8dea1506b25e776d02024855bce3d5dd23c4dcb Mon Sep 17 00:00:00 2001 From: charlotte Date: Thu, 7 Aug 2025 16:48:56 -0700 Subject: [PATCH 57/98] Revert change. --- src/webgpu/p5.RendererWebGPU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2a4a9b3e8d..f85fd4607b 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -631,7 +631,7 @@ class RendererWebGPU extends Renderer3D { ////////////////////////////////////////////// _drawBuffers(geometry, { mode = constants.TRIANGLES, count = 1 }) { - const buffers = this.geometryBufferCache.ensureCached(geometry); + const buffers = this.geometryBufferCache.getCached(geometry); if (!buffers) return; const commandEncoder = this.device.createCommandEncoder(); From 9c49827445dfdcd1bf425d5bcda7a23c620331f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 8 Aug 2025 14:40:02 -0700 Subject: [PATCH 58/98] Add setAttributes api to WebGPU renderer. --- src/core/main.js | 1 + src/core/p5.Renderer3D.js | 75 ++++++++++++++++++++++++++++++ src/webgl/p5.RendererGL.js | 73 +---------------------------- src/webgpu/p5.RendererWebGPU.js | 71 +++++++++++++++++++++++++++- test/unit/visual/cases/webgpu.js | 38 +++++++++++++-- test/unit/webgpu/p5.Framebuffer.js | 22 +++------ 6 files changed, 187 insertions(+), 93 deletions(-) diff --git a/src/core/main.js b/src/core/main.js index f9fc1c6559..9a3d929f3f 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -58,6 +58,7 @@ class p5 { this._curElement = null; this._elements = []; this._glAttributes = null; + this._webgpuAttributes = null; this._requestAnimId = 0; this._isGlobal = false; this._loop = true; diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index e422c2940f..bbf42b330c 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1,4 +1,5 @@ import * as constants from "../core/constants"; +import { Graphics } from "../core/p5.Graphics"; import { Renderer } from './p5.Renderer'; import GeometryBuilder from "../webgl/GeometryBuilder"; import { Matrix } from "../math/p5.Matrix"; @@ -350,6 +351,80 @@ export class Renderer3D extends Renderer { }; } + //This is helper function to reset the context anytime the attributes + //are changed with setAttributes() + + async _resetContext(options, callback, ctor = Renderer3D) { + const w = this.width; + const h = this.height; + const defaultId = this.canvas.id; + const isPGraphics = this._pInst instanceof Graphics; + + // Preserve existing position and styles before recreation + const prevStyle = { + position: this.canvas.style.position, + top: this.canvas.style.top, + left: this.canvas.style.left, + }; + + if (isPGraphics) { + // Handle PGraphics: remove and recreate the canvas + const pg = this._pInst; + pg.canvas.parentNode.removeChild(pg.canvas); + pg.canvas = document.createElement("canvas"); + const node = pg._pInst._userNode || document.body; + node.appendChild(pg.canvas); + Element.call(pg, pg.canvas, pg._pInst); + // Restore previous width and height + pg.width = w; + pg.height = h; + } else { + // Handle main canvas: remove and recreate it + let c = this.canvas; + if (c) { + c.parentNode.removeChild(c); + } + c = document.createElement("canvas"); + c.id = defaultId; + // Attach the new canvas to the correct parent node + if (this._pInst._userNode) { + this._pInst._userNode.appendChild(c); + } else { + document.body.appendChild(c); + } + this._pInst.canvas = c; + this.canvas = c; + + // Restore the saved position + this.canvas.style.position = prevStyle.position; + this.canvas.style.top = prevStyle.top; + this.canvas.style.left = prevStyle.left; + } + + const renderer = new ctor( + this._pInst, + w, + h, + !isPGraphics, + this._pInst.canvas + ); + this._pInst._renderer = renderer; + + renderer._applyDefaults(); + + if (renderer.contextReady) { + await renderer.contextReady + } + + if (typeof callback === "function") { + //setTimeout with 0 forces the task to the back of the queue, this ensures that + //we finish switching out the renderer + setTimeout(() => { + callback.apply(window._renderer, options); + }, 0); + } + } + remove() { this.wrappedElt.remove(); this.wrappedElt = null; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index c6fbfa45a6..ab15ea3d81 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -13,7 +13,6 @@ import { Renderer3D, getStrokeDefs } from "../core/p5.Renderer3D"; import { Shader } from "./p5.Shader"; import { Texture, MipmapTexture } from "./p5.Texture"; import { Framebuffer } from "./p5.Framebuffer"; -import { Graphics } from "../core/p5.Graphics"; import { RGB, RGBA } from '../color/creating_reading'; import { Element } from "../dom/p5.Element"; import { Image } from '../image/p5.Image'; @@ -450,76 +449,6 @@ class RendererGL extends Renderer3D { return { adjustedWidth, adjustedHeight }; } - //This is helper function to reset the context anytime the attributes - //are changed with setAttributes() - - _resetContext(options, callback) { - const w = this.width; - const h = this.height; - const defaultId = this.canvas.id; - const isPGraphics = this._pInst instanceof Graphics; - - // Preserve existing position and styles before recreation - const prevStyle = { - position: this.canvas.style.position, - top: this.canvas.style.top, - left: this.canvas.style.left, - }; - - if (isPGraphics) { - // Handle PGraphics: remove and recreate the canvas - const pg = this._pInst; - pg.canvas.parentNode.removeChild(pg.canvas); - pg.canvas = document.createElement("canvas"); - const node = pg._pInst._userNode || document.body; - node.appendChild(pg.canvas); - Element.call(pg, pg.canvas, pg._pInst); - // Restore previous width and height - pg.width = w; - pg.height = h; - } else { - // Handle main canvas: remove and recreate it - let c = this.canvas; - if (c) { - c.parentNode.removeChild(c); - } - c = document.createElement("canvas"); - c.id = defaultId; - // Attach the new canvas to the correct parent node - if (this._pInst._userNode) { - this._pInst._userNode.appendChild(c); - } else { - document.body.appendChild(c); - } - this._pInst.canvas = c; - this.canvas = c; - - // Restore the saved position - this.canvas.style.position = prevStyle.position; - this.canvas.style.top = prevStyle.top; - this.canvas.style.left = prevStyle.left; - } - - const renderer = new RendererGL( - this._pInst, - w, - h, - !isPGraphics, - this._pInst.canvas - ); - this._pInst._renderer = renderer; - - renderer._applyDefaults(); - - if (typeof callback === "function") { - //setTimeout with 0 forces the task to the back of the queue, this ensures that - //we finish switching out the renderer - setTimeout(() => { - callback.apply(window._renderer, options); - }, 0); - } - } - _resetBuffersBeforeDraw() { this.GL.clearStencil(0); this.GL.clear(this.GL.DEPTH_BUFFER_BIT | this.GL.STENCIL_BUFFER_BIT); @@ -2196,7 +2125,7 @@ function rendererGL(p5, fn) { } } - this._renderer._resetContext(); + this._renderer._resetContext(null, null, RendererGL); if (this._renderer.states.curCamera) { this._renderer.states.curCamera._renderer = this._renderer; diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index f85fd4607b..58c17f35aa 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -9,6 +9,8 @@ import * as constants from '../core/constants'; import { colorVertexShader, colorFragmentShader } from './shaders/color'; import { lineVertexShader, lineFragmentShader} from './shaders/line'; import { materialVertexShader, materialFragmentShader } from './shaders/material'; +import {Graphics} from "../core/p5.Graphics"; +import {Element} from "../dom/p5.Element"; const { lineDefs } = getStrokeDefs((n, v, t) => `const ${n}: ${t} = ${v};\n`); @@ -29,7 +31,25 @@ class RendererWebGPU extends Renderer3D { } async setupContext() { - this.adapter = await navigator.gpu?.requestAdapter(); + 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'] @@ -1854,6 +1874,55 @@ function rendererWebGPU(p5, fn) { fn.ensureTexture = function(source) { return this._renderer.ensureTexture(source); } + + fn.setAttributes = async function (key, value) { + if (typeof this._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._webgpuAttributes === null) { + this._webgpuAttributes = {}; + } + if (this._webgpuAttributes[key] !== value) { + //changing value of previously altered attribute + this._webgpuAttributes[key] = value; + unchanged = false; + } + //setting all attributes with some change + } else if (key instanceof Object) { + if (this._webgpuAttributes !== key) { + this._webgpuAttributes = key; + unchanged = false; + } + } + //@todo_FES + if (!this._renderer.isP3D || unchanged) { + return; + } + + if (!this._setupDone) { + if (this._renderer.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._renderer._resetContext(null, null, RendererWebGPU); + + if (this._renderer.states.curCamera) { + this._renderer.states.curCamera._renderer = this._renderer; + } + } } export default rendererWebGPU; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 28382dda25..130dabf83b 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -11,6 +11,9 @@ visualSuite("WebGPU", function () { "The color shader runs successfully", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.setAttributes({ + forceFallbackAdapter: true + }); p5.background("white"); for (const [i, color] of ["red", "lime", "blue"].entries()) { p5.push(); @@ -29,6 +32,9 @@ visualSuite("WebGPU", function () { "The stroke shader runs successfully", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.setAttributes({ + forceFallbackAdapter: true + }); p5.background("white"); for (const [i, color] of ["red", "lime", "blue"].entries()) { p5.push(); @@ -47,6 +53,9 @@ visualSuite("WebGPU", function () { "The material shader runs successfully", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.setAttributes({ + forceFallbackAdapter: true + }); p5.background("white"); p5.ambientLight(50); p5.directionalLight(100, 100, 100, 0, 1, -1); @@ -68,6 +77,9 @@ visualSuite("WebGPU", function () { visualTest("Shader hooks can be used", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.setAttributes({ + forceFallbackAdapter: true + }); const myFill = p5.baseMaterialShader().modify({ "Vertex getWorldInputs": `(inputs: Vertex) { var result = inputs; @@ -96,6 +108,9 @@ visualSuite("WebGPU", function () { "Textures in the material shader work", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.setAttributes({ + forceFallbackAdapter: true + }); const tex = p5.createImage(50, 50); tex.loadPixels(); for (let x = 0; x < tex.width; x++) { @@ -121,6 +136,9 @@ visualSuite("WebGPU", function () { "Main canvas drawing after resize", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); + await p5.setAttributes({ + forceFallbackAdapter: true + }); // Resize the canvas p5.resizeCanvas(30, 30); // Draw to the main canvas after resize @@ -138,7 +156,9 @@ visualSuite("WebGPU", function () { "Basic framebuffer draw to canvas", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + await p5.setAttributes({ + forceFallbackAdapter: true + }); // Create a framebuffer const fbo = p5.createFramebuffer({ width: 25, height: 25 }); @@ -164,7 +184,9 @@ visualSuite("WebGPU", function () { "Framebuffer with different sizes", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + await p5.setAttributes({ + forceFallbackAdapter: true + }); // Create two different sized framebuffers const fbo1 = p5.createFramebuffer({ width: 20, height: 20 }); const fbo2 = p5.createFramebuffer({ width: 15, height: 15 }); @@ -207,7 +229,9 @@ visualSuite("WebGPU", function () { visualTest("Auto-sized framebuffer", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + await p5.setAttributes({ + forceFallbackAdapter: true + }); // Create auto-sized framebuffer (should match canvas size) const fbo = p5.createFramebuffer(); @@ -242,7 +266,9 @@ visualSuite("WebGPU", function () { "Auto-sized framebuffer after canvas resize", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + await p5.setAttributes({ + forceFallbackAdapter: true + }); // Create auto-sized framebuffer const fbo = p5.createFramebuffer(); @@ -274,7 +300,9 @@ visualSuite("WebGPU", function () { "Fixed-size framebuffer after manual resize", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - + await p5.setAttributes({ + forceFallbackAdapter: true + }); // Create fixed-size framebuffer const fbo = p5.createFramebuffer({ width: 20, height: 20 }); diff --git a/test/unit/webgpu/p5.Framebuffer.js b/test/unit/webgpu/p5.Framebuffer.js index 452585b6c8..97cb8a13dd 100644 --- a/test/unit/webgpu/p5.Framebuffer.js +++ b/test/unit/webgpu/p5.Framebuffer.js @@ -15,6 +15,13 @@ suite('WebGPU p5.Framebuffer', function() { }); }); + beforeEach(async function() { + const renderer = await myp5.createCanvas(10, 10, 'webgpu'); + await myp5.setAttributes({ + forceFallbackAdapter: true + }); + }) + afterAll(function() { myp5.remove(); window.devicePixelRatio = prevPixelRatio; @@ -22,7 +29,6 @@ suite('WebGPU p5.Framebuffer', function() { suite('Creation and basic properties', function() { test('framebuffers can be created with WebGPU renderer', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); expect(fbo).to.be.an('object'); @@ -32,7 +38,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('framebuffers can be created with custom dimensions', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer({ width: 20, height: 30 }); expect(fbo.width).to.equal(20); @@ -41,7 +46,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('framebuffers have color texture', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); expect(fbo.color).to.be.an('object'); @@ -49,7 +53,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('framebuffers can specify different formats', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer({ format: 'float', channels: 'rgb' @@ -63,7 +66,6 @@ suite('WebGPU p5.Framebuffer', function() { suite('Auto-sizing behavior', function() { test('auto-sized framebuffers change size with canvas', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); myp5.pixelDensity(1); const fbo = myp5.createFramebuffer(); @@ -80,7 +82,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('manually-sized framebuffers do not change size with canvas', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); myp5.pixelDensity(3); const fbo = myp5.createFramebuffer({ width: 25, height: 30, density: 1 }); @@ -97,7 +98,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('manually-sized framebuffers can be made auto-sized', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); myp5.pixelDensity(1); const fbo = myp5.createFramebuffer({ width: 25, height: 30, density: 2 }); @@ -120,7 +120,6 @@ suite('WebGPU p5.Framebuffer', function() { suite('Manual resizing', function() { test('framebuffers can be manually resized', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); myp5.pixelDensity(1); const fbo = myp5.createFramebuffer(); @@ -135,7 +134,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('resizing affects pixel density', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); myp5.pixelDensity(1); const fbo = myp5.createFramebuffer(); @@ -152,7 +150,6 @@ suite('WebGPU p5.Framebuffer', function() { suite('Drawing functionality', function() { test('can draw to framebuffer with draw() method', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); myp5.background(0, 255, 0); @@ -178,7 +175,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('can use framebuffer as texture', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); fbo.draw(() => { @@ -195,7 +191,6 @@ suite('WebGPU p5.Framebuffer', function() { suite('Pixel access', function() { test('loadPixels returns a promise in WebGPU', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); fbo.draw(() => { @@ -212,7 +207,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('pixels property is set after loadPixels resolves', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); fbo.draw(() => { @@ -225,7 +219,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('get() returns a promise for single pixel in WebGPU', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); fbo.draw(() => { @@ -242,7 +235,6 @@ suite('WebGPU p5.Framebuffer', function() { }); test('get() returns a promise for region in WebGPU', async function() { - await myp5.createCanvas(10, 10, myp5.WEBGPU); const fbo = myp5.createFramebuffer(); fbo.draw(() => { From 695e9e60a1f4d58ff159d6eccb17ee70cf9654e7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 09:02:58 -0400 Subject: [PATCH 59/98] Try ignore-blocklist flag --- vitest.workspace.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 23055b6680..90b4173e2f 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -32,6 +32,7 @@ export default defineWorkspace([ './test/unit/spec.js', './test/unit/assets/**/*', './test/unit/visual/visualTest.js', + './test/unit/visual/cases/webgpu.js', ], testTimeout: 10000, globals: true, @@ -71,7 +72,8 @@ export default defineWorkspace([ name: 'unit-tests-firefox', root: './', include: [ - './test/unit/**/*.js', + './test/unit/visual/cases/webgpu.js', + // './test/unit/**/*.js', ], exclude: [ './test/unit/spec.js', @@ -96,6 +98,7 @@ export default defineWorkspace([ 'dom.webgpu.enabled': true, 'gfx.webgpu.force-enabled': true, 'dom.webgpu.testing.assert-on-warnings': false, + 'gfx.webgpu.ignore-blocklist': true, } } } : undefined From 48ce3209d3d1c22f67164e60924262f4a2d1f784 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 09:34:32 -0400 Subject: [PATCH 60/98] Go back to ubuntu for now, try different swiftshader flags --- .github/workflows/ci-test.yml | 14 -------- vitest.workspace.mjs | 60 ++++------------------------------- 2 files changed, 7 insertions(+), 67 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 416d01777d..328d090e23 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -14,9 +14,6 @@ jobs: strategy: matrix: include: - - os: windows-latest - browser: firefox - test-workspace: unit-tests-firefox - os: ubuntu-latest browser: chrome test-workspace: unit-tests-chrome @@ -31,11 +28,6 @@ jobs: with: node-version: 20.x - - name: Verify Firefox (Windows) - if: matrix.os == 'windows-latest' && matrix.browser == 'firefox' - run: | - & "C:\Program Files\Mozilla Firefox\firefox.exe" --version - - name: Verify Chrome (Ubuntu) if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' run: | @@ -52,12 +44,6 @@ jobs: env: CI: true - - name: Build and test (Windows) - if: matrix.os == 'windows-latest' - run: npm test -- --project=${{ matrix.test-workspace }} - env: - CI: true - - name: Report test coverage run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json env: diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 90b4173e2f..bca96ad5a8 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -26,13 +26,14 @@ export default defineWorkspace([ name: 'unit-tests-chrome', root: './', include: [ - './test/unit/**/*.js', + // './test/unit/**/*.js', + './test/unit/visual/cases/webgpu.js', ], exclude: [ './test/unit/spec.js', './test/unit/assets/**/*', './test/unit/visual/visualTest.js', - './test/unit/visual/cases/webgpu.js', + // './test/unit/visual/cases/webgpu.js', ], testTimeout: 10000, globals: true, @@ -47,10 +48,11 @@ export default defineWorkspace([ args: [ '--no-sandbox', '--headless=new', - '--use-angle=vulkan', - '--enable-features=Vulkan', - '--disable-vulkan-surface', '--enable-unsafe-webgpu', + '--enable-features=Vulkan', + '--use-vulkan=swiftshader', + '--use-webgpu-adapter=swiftshader', + '--no-sandbox', ] } } : undefined @@ -58,52 +60,4 @@ export default defineWorkspace([ } } }, - { - plugins, - publicDir: './test', - bench: { - name: 'bench', - root: './', - include: [ - './test/bench/**/*.js' - ], - }, - test: { - name: 'unit-tests-firefox', - root: './', - include: [ - './test/unit/visual/cases/webgpu.js', - // './test/unit/**/*.js', - ], - exclude: [ - './test/unit/spec.js', - './test/unit/assets/**/*', - './test/unit/visual/visualTest.js', - ], - testTimeout: 10000, - globals: true, - browser: { - enabled: true, - name: 'firefox', - provider: 'webdriverio', - screenshotFailures: false, - providerOptions: { - capabilities: process.env.CI ? { - 'moz:firefoxOptions': { - args: [ - '--headless', - '--enable-webgpu', - ], - prefs: { - 'dom.webgpu.enabled': true, - 'gfx.webgpu.force-enabled': true, - 'dom.webgpu.testing.assert-on-warnings': false, - 'gfx.webgpu.ignore-blocklist': true, - } - } - } : undefined - } - } - } - } ]); \ No newline at end of file From 47535445966433bbcc36a0db56ca7a32c4dc83a7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 09:39:31 -0400 Subject: [PATCH 61/98] Different flag --- vitest.workspace.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index bca96ad5a8..6802ae3588 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -49,9 +49,9 @@ export default defineWorkspace([ '--no-sandbox', '--headless=new', '--enable-unsafe-webgpu', - '--enable-features=Vulkan', '--use-vulkan=swiftshader', '--use-webgpu-adapter=swiftshader', + '--use-angle=vulkan', '--no-sandbox', ] } From 9eb541d8ff2be5745a6be13d60d4655a1a8b3db7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 09:41:45 -0400 Subject: [PATCH 62/98] Check if the adapter is defined --- src/webgpu/p5.RendererWebGPU.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 58c17f35aa..daa7eeffc0 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -50,6 +50,11 @@ class RendererWebGPU extends Renderer3D { async _initContext() { this.adapter = await navigator.gpu?.requestAdapter(this._webgpuAttributes); + console.log('Adapter:'); + console.log(this.adapter); + if (this.adapter) { + console.log([...this.adapter.features]); + } this.device = await this.adapter?.requestDevice({ // Todo: check support requiredFeatures: ['depth32float-stencil8'] From 74c167243661e3e867c104d2717f42800386a556 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 09:58:15 -0400 Subject: [PATCH 63/98] Try without setAttributes since the adapter seems to exist before that --- src/webgpu/p5.RendererWebGPU.js | 2 ++ test/unit/visual/cases/webgpu.js | 33 -------------------------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index daa7eeffc0..8d3f8c2e24 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -59,6 +59,8 @@ class RendererWebGPU extends Renderer3D { // Todo: check support requiredFeatures: ['depth32float-stencil8'] }); + console.log('Device:'); + console.log(this.device); if (!this.device) { throw new Error('Your browser does not support WebGPU.'); } diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 130dabf83b..bffc88c219 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -11,9 +11,6 @@ visualSuite("WebGPU", function () { "The color shader runs successfully", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); p5.background("white"); for (const [i, color] of ["red", "lime", "blue"].entries()) { p5.push(); @@ -32,9 +29,6 @@ visualSuite("WebGPU", function () { "The stroke shader runs successfully", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); p5.background("white"); for (const [i, color] of ["red", "lime", "blue"].entries()) { p5.push(); @@ -53,9 +47,6 @@ visualSuite("WebGPU", function () { "The material shader runs successfully", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); p5.background("white"); p5.ambientLight(50); p5.directionalLight(100, 100, 100, 0, 1, -1); @@ -77,9 +68,6 @@ visualSuite("WebGPU", function () { visualTest("Shader hooks can be used", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); const myFill = p5.baseMaterialShader().modify({ "Vertex getWorldInputs": `(inputs: Vertex) { var result = inputs; @@ -108,9 +96,6 @@ visualSuite("WebGPU", function () { "Textures in the material shader work", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); const tex = p5.createImage(50, 50); tex.loadPixels(); for (let x = 0; x < tex.width; x++) { @@ -136,9 +121,6 @@ visualSuite("WebGPU", function () { "Main canvas drawing after resize", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); // Resize the canvas p5.resizeCanvas(30, 30); // Draw to the main canvas after resize @@ -156,9 +138,6 @@ visualSuite("WebGPU", function () { "Basic framebuffer draw to canvas", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); // Create a framebuffer const fbo = p5.createFramebuffer({ width: 25, height: 25 }); @@ -184,9 +163,6 @@ visualSuite("WebGPU", function () { "Framebuffer with different sizes", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); // Create two different sized framebuffers const fbo1 = p5.createFramebuffer({ width: 20, height: 20 }); const fbo2 = p5.createFramebuffer({ width: 15, height: 15 }); @@ -229,9 +205,6 @@ visualSuite("WebGPU", function () { visualTest("Auto-sized framebuffer", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); // Create auto-sized framebuffer (should match canvas size) const fbo = p5.createFramebuffer(); @@ -266,9 +239,6 @@ visualSuite("WebGPU", function () { "Auto-sized framebuffer after canvas resize", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); // Create auto-sized framebuffer const fbo = p5.createFramebuffer(); @@ -300,9 +270,6 @@ visualSuite("WebGPU", function () { "Fixed-size framebuffer after manual resize", async function (p5, screenshot) { await p5.createCanvas(50, 50, p5.WEBGPU); - await p5.setAttributes({ - forceFallbackAdapter: true - }); // Create fixed-size framebuffer const fbo = p5.createFramebuffer({ width: 20, height: 20 }); From 498bb836fd1c2b54a293009a66be78486eb82b10 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 10:05:41 -0400 Subject: [PATCH 64/98] Try installing chrome with swiftshader --- .github/workflows/ci-test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 328d090e23..d1e98902a2 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -27,6 +27,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.x + + - name: Install Chrome with SwiftShader + run: | + sudo apt-get update + curl -sSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o chrome.deb + sudo apt-get install -y ./chrome.deb + ls -R /opt/google/chrome/swiftshader - name: Verify Chrome (Ubuntu) if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' From bc3501a10b98271888ffe0e68f344caf0bae61f4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 10:18:44 -0400 Subject: [PATCH 65/98] Revert "Try installing chrome with swiftshader" This reverts commit 498bb836fd1c2b54a293009a66be78486eb82b10. --- .github/workflows/ci-test.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index d1e98902a2..328d090e23 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -27,13 +27,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.x - - - name: Install Chrome with SwiftShader - run: | - sudo apt-get update - curl -sSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o chrome.deb - sudo apt-get install -y ./chrome.deb - ls -R /opt/google/chrome/swiftshader - name: Verify Chrome (Ubuntu) if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' From 88dec1b8b3a9bffda5b3e8dffdf274d74306fa35 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 10:26:08 -0400 Subject: [PATCH 66/98] Try chrome on windows --- .github/workflows/ci-test.yml | 22 ++++++++++++---- vitest.workspace.mjs | 49 ++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 328d090e23..1f0c7f4ca9 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -14,9 +14,10 @@ jobs: strategy: matrix: include: - - os: ubuntu-latest + #- os: ubuntu-latest + # browser: chrome + - os: windows-latest browser: chrome - test-workspace: unit-tests-chrome runs-on: ${{ matrix.os }} @@ -32,18 +33,29 @@ jobs: if: matrix.os == 'ubuntu-latest' && matrix.browser == 'chrome' run: | google-chrome --version + + - name: Verify Chrome (Windows) + if: matrix.os == 'windows-latest' && matrix.browser == 'chrome' + run: | + & "C:\Program Files\Google\Chrome\Application\chrome.exe" --version - name: Get node modules run: npm ci env: CI: true - + - name: Build and test (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: npm test -- --project=${{ matrix.test-workspace }} + if: matrix.os == 'windows-latest' + run: npm test -- --project=unit-tests-webgpu env: CI: true + #- name: Build and test (Ubuntu) + # if: matrix.os == 'ubuntu-latest' + # run: npm test -- --project=unit-tests + # env: + # CI: true + - name: Report test coverage run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json env: diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 6802ae3588..d2d95bb049 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -23,7 +23,54 @@ export default defineWorkspace([ ], }, test: { - name: 'unit-tests-chrome', + name: 'unit-tests', + root: './', + include: [ + './test/unit/**/*.js', + ], + exclude: [ + './test/unit/spec.js', + './test/unit/assets/**/*', + './test/unit/visual/visualTest.js', + './test/unit/visual/cases/webgpu.js', + ], + testTimeout: 10000, + globals: true, + browser: { + enabled: true, + name: 'chrome', + provider: 'webdriverio', + screenshotFailures: false, + providerOptions: { + capabilities: process.env.CI ? { + 'goog:chromeOptions': { + args: [ + '--no-sandbox', + '--headless=new', + '--enable-unsafe-webgpu', + '--use-vulkan=swiftshader', + '--use-webgpu-adapter=swiftshader', + '--use-angle=vulkan', + '--no-sandbox', + ] + } + } : undefined + } + } + } + }, + { + plugins, + publicDir: './test', + bench: { + name: 'bench', + root: './', + include: [ + './test/bench/**/*.js' + ], + }, + test: { + name: 'unit-tests-webgpu', root: './', include: [ // './test/unit/**/*.js', From d5f584f4e0fe1ad5f7cc615f2b17dcd2fa745e54 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 10:43:57 -0400 Subject: [PATCH 67/98] Don't run webgpu tests on CI for now --- .github/workflows/ci-test.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 1f0c7f4ca9..04dcd79306 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -14,11 +14,11 @@ jobs: strategy: matrix: include: - #- os: ubuntu-latest - # browser: chrome - - os: windows-latest + - os: ubuntu-latest browser: chrome - + # - os: windows-latest + # browser: chrome + runs-on: ${{ matrix.os }} steps: @@ -44,18 +44,18 @@ jobs: env: CI: true - - name: Build and test (Ubuntu) - if: matrix.os == 'windows-latest' - run: npm test -- --project=unit-tests-webgpu - env: - CI: true - #- name: Build and test (Ubuntu) - # if: matrix.os == 'ubuntu-latest' - # run: npm test -- --project=unit-tests + # if: matrix.os == 'windows-latest' + # run: npm test -- --project=unit-tests-webgpu # env: # CI: true - + + - name: Build and test (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: npm test -- --project=unit-tests + env: + CI: true + - name: Report test coverage run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json env: From 3ea32edaaefffef9d0e8b04f7ce8fa89d7df927b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 10:48:51 -0400 Subject: [PATCH 68/98] Exclude other webgpu tests --- vitest.workspace.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index d2d95bb049..13dca58dbe 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -33,6 +33,7 @@ export default defineWorkspace([ './test/unit/assets/**/*', './test/unit/visual/visualTest.js', './test/unit/visual/cases/webgpu.js', + './test/unit/webgpu/*.js', ], testTimeout: 10000, globals: true, @@ -75,6 +76,7 @@ export default defineWorkspace([ include: [ // './test/unit/**/*.js', './test/unit/visual/cases/webgpu.js', + './test/unit/webgpu/*.js', ], exclude: [ './test/unit/spec.js', From 724b41a11f6504d071c883036480f938efd64117 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 14 Sep 2025 11:06:51 -0400 Subject: [PATCH 69/98] Move setAttributes implementation to renderer --- src/webgl/p5.RendererGL.js | 94 +++++++++++++------------- src/webgpu/p5.RendererWebGPU.js | 105 +++++++++++++++-------------- test/unit/webgpu/p5.Framebuffer.js | 5 +- vitest.workspace.mjs | 5 +- 4 files changed, 107 insertions(+), 102 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index ab15ea3d81..d22668cc10 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -382,6 +382,54 @@ class RendererGL extends Renderer3D { return; } + _setAttributes(key, value) { + if (typeof this._pInst._glAttributes === "undefined") { + console.log( + "You are trying to use setAttributes on a p5.Graphics object " + + "that does not use a WEBGL renderer." + ); + return; + } + let unchanged = true; + if (typeof value !== "undefined") { + //first time modifying the attributes + if (this._pInst._glAttributes === null) { + this._pInst._glAttributes = {}; + } + if (this._pInst._glAttributes[key] !== value) { + //changing value of previously altered attribute + this._pInst._glAttributes[key] = value; + unchanged = false; + } + //setting all attributes with some change + } else if (key instanceof Object) { + if (this._pInst._glAttributes !== key) { + this._pInst._glAttributes = 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; + } + } + + this._resetContext(null, null, RendererGL); + + if (this.states.curCamera) { + this.states.curCamera._renderer = this._renderer; + } + } + _initContext() { if (this._pInst._glAttributes?.version !== 1) { // Unless WebGL1 is explicitly asked for, try to create a WebGL2 context @@ -2085,51 +2133,7 @@ function rendererGL(p5, fn) { * @param {Object} obj object with key-value pairs */ fn.setAttributes = function (key, value) { - if (typeof this._glAttributes === "undefined") { - console.log( - "You are trying to use setAttributes on a p5.Graphics object " + - "that does not use a WEBGL renderer." - ); - return; - } - let unchanged = true; - if (typeof value !== "undefined") { - //first time modifying the attributes - if (this._glAttributes === null) { - this._glAttributes = {}; - } - if (this._glAttributes[key] !== value) { - //changing value of previously altered attribute - this._glAttributes[key] = value; - unchanged = false; - } - //setting all attributes with some change - } else if (key instanceof Object) { - if (this._glAttributes !== key) { - this._glAttributes = key; - unchanged = false; - } - } - //@todo_FES - if (!this._renderer.isP3D || unchanged) { - return; - } - - if (!this._setupDone) { - if (this._renderer.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; - } - } - - this._renderer._resetContext(null, null, RendererGL); - - if (this._renderer.states.curCamera) { - this._renderer.states.curCamera._renderer = this._renderer; - } + return this._renderer._setAttributes(key, value); }; /** diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 8d3f8c2e24..2bcf39949d 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -50,8 +50,8 @@ class RendererWebGPU extends Renderer3D { async _initContext() { this.adapter = await navigator.gpu?.requestAdapter(this._webgpuAttributes); - console.log('Adapter:'); - console.log(this.adapter); + // console.log('Adapter:'); + // console.log(this.adapter); if (this.adapter) { console.log([...this.adapter.features]); } @@ -59,8 +59,8 @@ class RendererWebGPU extends Renderer3D { // Todo: check support requiredFeatures: ['depth32float-stencil8'] }); - console.log('Device:'); - console.log(this.device); + // console.log('Device:'); + // console.log(this.device); if (!this.device) { throw new Error('Your browser does not support WebGPU.'); } @@ -79,6 +79,55 @@ class RendererWebGPU extends Renderer3D { this._update(); } + 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._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.depthTexture && this.depthTexture.destroy) { this.depthTexture.destroy(); @@ -1882,53 +1931,9 @@ function rendererWebGPU(p5, fn) { return this._renderer.ensureTexture(source); } + // TODO: move this and the duplicate in the WebGL renderer to another file fn.setAttributes = async function (key, value) { - if (typeof this._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._webgpuAttributes === null) { - this._webgpuAttributes = {}; - } - if (this._webgpuAttributes[key] !== value) { - //changing value of previously altered attribute - this._webgpuAttributes[key] = value; - unchanged = false; - } - //setting all attributes with some change - } else if (key instanceof Object) { - if (this._webgpuAttributes !== key) { - this._webgpuAttributes = key; - unchanged = false; - } - } - //@todo_FES - if (!this._renderer.isP3D || unchanged) { - return; - } - - if (!this._setupDone) { - if (this._renderer.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._renderer._resetContext(null, null, RendererWebGPU); - - if (this._renderer.states.curCamera) { - this._renderer.states.curCamera._renderer = this._renderer; - } + return this._renderer._setAttributes(key, value); } } diff --git a/test/unit/webgpu/p5.Framebuffer.js b/test/unit/webgpu/p5.Framebuffer.js index 97cb8a13dd..ccbadbc7a0 100644 --- a/test/unit/webgpu/p5.Framebuffer.js +++ b/test/unit/webgpu/p5.Framebuffer.js @@ -16,10 +16,7 @@ suite('WebGPU p5.Framebuffer', function() { }); beforeEach(async function() { - const renderer = await myp5.createCanvas(10, 10, 'webgpu'); - await myp5.setAttributes({ - forceFallbackAdapter: true - }); + await myp5.createCanvas(10, 10, 'webgpu'); }) afterAll(function() { diff --git a/vitest.workspace.mjs b/vitest.workspace.mjs index 13dca58dbe..a8da776a67 100644 --- a/vitest.workspace.mjs +++ b/vitest.workspace.mjs @@ -1,6 +1,5 @@ import { defineWorkspace } from 'vitest/config'; import vitePluginString from 'vite-plugin-string'; -console.log(`CI: ${process.env.CI}`) const plugins = [ vitePluginString({ @@ -35,7 +34,7 @@ export default defineWorkspace([ './test/unit/visual/cases/webgpu.js', './test/unit/webgpu/*.js', ], - testTimeout: 10000, + testTimeout: 1000, globals: true, browser: { enabled: true, @@ -84,7 +83,7 @@ export default defineWorkspace([ './test/unit/visual/visualTest.js', // './test/unit/visual/cases/webgpu.js', ], - testTimeout: 10000, + testTimeout: 1000, globals: true, browser: { enabled: true, From 254d1a66f545badcc53529c492731eaf2c0be069 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 16 Nov 2025 10:14:54 -0500 Subject: [PATCH 70/98] Add whole-module addon for webgpu, prevent duplicate addon registers --- src/core/main.js | 7 +++++++ src/webgl/index.js | 34 +++++++++++++++--------------- src/webgpu/index.js | 37 +++++++++++++++++++++++++++++++++ src/webgpu/p5.RendererWebGPU.js | 4 ++++ 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 src/webgpu/index.js diff --git a/src/core/main.js b/src/core/main.js index fe8076ca9a..8e2d292f1d 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -172,9 +172,16 @@ class p5 { return this._renderer.drawingContext; } + static _registeredAddons = new Set(); static registerAddon(addon) { const lifecycles = {}; + // Don't re-register an addon. This allows addons + // to register dependency addons without worrying about + // them getting double-added. + if (p5._registeredAddons.has(addon)) return; + p5._registeredAddons.add(addon); + addon(p5, p5.prototype, lifecycles); const validLifecycles = Object.keys(p5.lifecycleHooks); diff --git a/src/webgl/index.js b/src/webgl/index.js index 52292100e8..2f84a9ec19 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -17,21 +17,21 @@ import rendererGL from './p5.RendererGL'; import strands from '../strands/p5.strands'; export default function(p5){ - rendererGL(p5, p5.prototype); - primitives3D(p5, p5.prototype); - interaction(p5, p5.prototype); - light(p5, p5.prototype); - loading(p5, p5.prototype); - material(p5, p5.prototype); - text(p5, p5.prototype); - renderBuffer(p5, p5.prototype); - quat(p5, p5.prototype); - matrix(p5, p5.prototype); - geometry(p5, p5.prototype); - camera(p5, p5.prototype); - framebuffer(p5, p5.prototype); - dataArray(p5, p5.prototype); - shader(p5, p5.prototype); - texture(p5, p5.prototype); - strands(p5, p5.prototype); + p5.registerAddon(rendererGL); + p5.registerAddon(primitives3D); + p5.registerAddon(interaction); + p5.registerAddon(light); + p5.registerAddon(loading); + p5.registerAddon(material); + p5.registerAddon(text); + p5.registerAddon(renderBuffer); + p5.registerAddon(quat); + p5.registerAddon(matrix); + p5.registerAddon(geometry); + p5.registerAddon(camera); + p5.registerAddon(framebuffer); + p5.registerAddon(dataArray); + p5.registerAddon(shader); + p5.registerAddon(texture); + p5.registerAddon(strands); } diff --git a/src/webgpu/index.js b/src/webgpu/index.js new file mode 100644 index 0000000000..015a140eab --- /dev/null +++ b/src/webgpu/index.js @@ -0,0 +1,37 @@ +import primitives3D from '../webgl/3d_primitives'; +import interaction from '../webgl/interaction'; +import light from '../webgl/light'; +import loading from '../webgl/loading'; +import material from '../webgl/material'; +import text from '../webgl/text'; +import renderBuffer from '../webgl/p5.RenderBuffer'; +import quat from '../webgl/p5.Quat'; +import matrix from '../math/p5.Matrix'; +import geometry from '../webgl/p5.Geometry'; +import framebuffer from '../webgl/p5.Framebuffer'; +import dataArray from '../webgl/p5.DataArray'; +import shader from '../webgl/p5.Shader'; +import camera from '../webgl/p5.Camera'; +import texture from '../webgl/p5.Texture'; +import rendererGL from '../webgl/p5.RendererGL'; +import strands from '../strands/p5.strands'; + +export default function(p5){ + p5.registerAddon(rendererGL); + p5.registerAddon(primitives3D); + p5.registerAddon(interaction); + p5.registerAddon(light); + p5.registerAddon(loading); + p5.registerAddon(material); + p5.registerAddon(text); + p5.registerAddon(renderBuffer); + p5.registerAddon(quat); + p5.registerAddon(matrix); + p5.registerAddon(geometry); + p5.registerAddon(camera); + p5.registerAddon(framebuffer); + p5.registerAddon(dataArray); + p5.registerAddon(shader); + p5.registerAddon(texture); + p5.registerAddon(strands); +} diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2bcf39949d..128bbf431b 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1939,3 +1939,7 @@ function rendererWebGPU(p5, fn) { export default rendererWebGPU; export { RendererWebGPU }; + +if (typeof p5 !== "undefined") { + rendererWebGPU(p5, p5.prototype); +} From cb221da8b1e6169d81f29fc4b7fcbf283409a9ad Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 16 Nov 2025 10:30:15 -0500 Subject: [PATCH 71/98] Add webgpu to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cb63d3caae..f0b148ae1d 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "./math": "./dist/math/index.js", "./utilities": "./dist/utilities/index.js", "./webgl": "./dist/webgl/index.js", + "./webgpu": "./dist/webgpu/index.js", "./type": "./dist/type/index.js" }, "files": [ From ef28cbc7dbd9990c9e855cebb4adee8a6d6ecc7e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 16 Nov 2025 12:12:08 -0500 Subject: [PATCH 72/98] Get font rendering working on WebGPU --- preview/index.html | 20 ++ src/core/p5.Renderer2D.js | 103 ++++++ src/core/p5.Renderer3D.js | 108 ++++++- src/type/p5.Font.js | 3 + src/type/textCore.js | 205 +----------- src/webgl/3d_primitives.js | 2 +- src/webgl/p5.Camera.js | 8 +- src/webgl/p5.RendererGL.js | 74 +---- src/webgl/shaders/font.vert | 2 - src/webgl/shaders/light_texture.frag | 13 +- src/webgl/shaders/phong.frag | 7 +- src/webgl/shaders/point.frag | 29 -- src/webgl/shaders/point.vert | 19 -- src/webgl/shaders/webgl2Compatibility.glsl | 6 +- src/webgl/text.js | 22 +- src/webgpu/p5.RendererWebGPU.js | 156 +++++++-- src/webgpu/shaders/font.js | 283 +++++++++++++++++ test/unit/visual/cases/webgpu.js | 300 ++++++++++++++++++ .../Main canvas drawing after resize/000.png | Bin 225 -> 274 bytes .../000.png | Bin 167 -> 306 bytes .../Auto-sized framebuffer/000.png | Bin 396 -> 593 bytes .../Basic framebuffer draw to canvas/000.png | Bin 521 -> 656 bytes .../000.png | Bin 291 -> 546 bytes .../Framebuffer with different sizes/000.png | Bin 402 -> 491 bytes .../Shaders/Shader hooks can be used/000.png | Bin 474 -> 667 bytes .../000.png | Bin 275 -> 975 bytes .../000.png | Bin 427 -> 539 bytes .../000.png | Bin 1707 -> 1813 bytes .../000.png | Bin 510 -> 735 bytes .../000.png | Bin 0 -> 2325 bytes .../001.png | Bin 0 -> 2316 bytes .../002.png | Bin 0 -> 2318 bytes .../003.png | Bin 0 -> 2321 bytes .../004.png | Bin 0 -> 2306 bytes .../005.png | Bin 0 -> 2305 bytes .../006.png | Bin 0 -> 2324 bytes .../007.png | Bin 0 -> 2301 bytes .../008.png | Bin 0 -> 2303 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 3583 bytes .../001.png | Bin 0 -> 3603 bytes .../002.png | Bin 0 -> 3584 bytes .../003.png | Bin 0 -> 3632 bytes .../004.png | Bin 0 -> 3648 bytes .../005.png | Bin 0 -> 3625 bytes .../006.png | Bin 0 -> 3559 bytes .../007.png | Bin 0 -> 3636 bytes .../008.png | Bin 0 -> 3566 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 3592 bytes .../001.png | Bin 0 -> 3589 bytes .../002.png | Bin 0 -> 3571 bytes .../003.png | Bin 0 -> 3526 bytes .../004.png | Bin 0 -> 3527 bytes .../005.png | Bin 0 -> 3523 bytes .../006.png | Bin 0 -> 3530 bytes .../007.png | Bin 0 -> 3538 bytes .../008.png | Bin 0 -> 3532 bytes .../metadata.json | 3 + .../all alignments with single line/000.png | Bin 0 -> 6315 bytes .../all alignments with single line/001.png | Bin 0 -> 7284 bytes .../all alignments with single line/002.png | Bin 0 -> 5242 bytes .../all alignments with single line/003.png | Bin 0 -> 6337 bytes .../all alignments with single line/004.png | Bin 0 -> 7209 bytes .../all alignments with single line/005.png | Bin 0 -> 5271 bytes .../all alignments with single line/006.png | Bin 0 -> 6323 bytes .../all alignments with single line/007.png | Bin 0 -> 7307 bytes .../all alignments with single line/008.png | Bin 0 -> 5229 bytes .../metadata.json | 3 + .../all alignments with single word/000.png | Bin 0 -> 6527 bytes .../all alignments with single word/001.png | Bin 0 -> 6996 bytes .../all alignments with single word/002.png | Bin 0 -> 4957 bytes .../all alignments with single word/003.png | Bin 0 -> 6522 bytes .../all alignments with single word/004.png | Bin 0 -> 6968 bytes .../all alignments with single word/005.png | Bin 0 -> 4971 bytes .../all alignments with single word/006.png | Bin 0 -> 6524 bytes .../all alignments with single word/007.png | Bin 0 -> 6973 bytes .../all alignments with single word/008.png | Bin 0 -> 4976 bytes .../metadata.json | 3 + .../with a font file in WebGPU/000.png | Bin 0 -> 2276 bytes .../with a font file in WebGPU/metadata.json | 3 + .../000.png | Bin 0 -> 2844 bytes .../001.png | Bin 0 -> 2868 bytes .../002.png | Bin 0 -> 2872 bytes .../003.png | Bin 0 -> 2883 bytes .../004.png | Bin 0 -> 2893 bytes .../metadata.json | 3 + 87 files changed, 983 insertions(+), 398 deletions(-) delete mode 100644 src/webgl/shaders/point.frag delete mode 100644 src/webgl/shaders/point.vert create mode 100644 src/webgpu/shaders/font.js create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/001.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/002.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/003.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/004.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/005.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/006.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/007.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/008.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/001.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/002.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/003.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/004.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/005.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/006.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/007.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/008.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/001.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/002.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/003.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/004.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/005.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/006.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/007.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/008.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/001.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/002.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/003.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/004.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/005.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/006.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/007.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/008.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/001.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/002.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/003.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/004.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/005.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/006.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/007.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/008.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textFont/with a font file in WebGPU/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textFont/with a font file in WebGPU/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/001.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/002.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/003.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/004.png create mode 100644 test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/metadata.json diff --git a/preview/index.html b/preview/index.html index 4092992316..ba1f0bd282 100644 --- a/preview/index.html +++ b/preview/index.html @@ -27,9 +27,13 @@ let sh; let ssh; let tex; + let font; p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); + font = await p.loadFont( + 'font/PlayfairDisplay.ttf' + ); fbo = p.createFramebuffer(); tex = p.createImage(100, 100); @@ -72,6 +76,22 @@ }; p.draw = function () { + p.clear(); + p.orbitControl(); + p.push(); + p.textAlign(p.CENTER, p.CENTER); + p.textFont(font); + p.textSize(85) + p.fill('red') + p.noStroke() + p.rect(0, 0, 100, 100); + p.fill(0); + p.push() + p.rotate(p.millis() * 0.001) + p.text('Hello!', 0, 0); + p.pop() + p.pop(); + return; p.orbitControl(); const t = p.millis() * 0.002; p.background(200); diff --git a/src/core/p5.Renderer2D.js b/src/core/p5.Renderer2D.js index b9a7e12d00..132049d530 100644 --- a/src/core/p5.Renderer2D.js +++ b/src/core/p5.Renderer2D.js @@ -9,6 +9,7 @@ import { RGBHDR } from '../color/creating_reading'; import FilterRenderer2D from '../image/filterRenderer2D'; import { Matrix } from '../math/p5.Matrix'; import { PrimitiveToPath2DConverter } from '../shape/custom_shapes'; +import { DefaultFill, textCoreConstants } from '../type/textCore'; const styleEmpty = 'rgba(0,0,0,0)'; @@ -1054,6 +1055,108 @@ class Renderer2D extends Renderer { super.pop(style); } + + // Text support methods + textCanvas() { + return this.canvas; + } + + textDrawingContext() { + return this.drawingContext; + } + + _renderText(text, x, y, maxY, minY) { + let states = this.states; + let context = this.textDrawingContext(); + + if (y < minY || y >= maxY) { + return; // don't render lines beyond minY/maxY + } + + this.push(); + + // no stroke unless specified by user + if (states.strokeColor && states.strokeSet) { + context.strokeText(text, x, y); + } + + if (!this._clipping && states.fillColor) { + + // if fill hasn't been set by user, use default text fill + if (!states.fillSet) { + this._setFill(DefaultFill); + } + context.fillText(text, x, y); + } + + this.pop(); + } + + /* + Position the lines of text based on their textAlign/textBaseline properties + */ + _positionLines(x, y, width, height, lines) { + let { textLeading, textAlign } = this.states; + let adjustedX, lineData = new Array(lines.length); + let adjustedW = typeof width === 'undefined' ? 0 : width; + let adjustedH = typeof height === 'undefined' ? 0 : height; + + for (let i = 0; i < lines.length; i++) { + switch (textAlign) { + case textCoreConstants.START: + throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT + case constants.LEFT: + adjustedX = x; + break; + case constants.CENTER: + adjustedX = x + adjustedW / 2; + break; + case constants.RIGHT: + adjustedX = x + adjustedW; + break; + case textCoreConstants.END: + throw new Error('textBounds: END not yet supported for textAlign'); + } + lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; + } + + return this._yAlignOffset(lineData, adjustedH); + } + + /* + Get the y-offset for text given the height, leading, line-count and textBaseline property + */ + _yAlignOffset(dataArr, height) { + if (typeof height === 'undefined') { + throw Error('_yAlignOffset: height is required'); + } + + let { textLeading, textBaseline } = this.states; + let yOff = 0, numLines = dataArr.length; + let ydiff = height - (textLeading * (numLines - 1)); + + switch (textBaseline) { // drawingContext ? + case constants.TOP: + break; // ?? + case constants.BASELINE: + break; + case textCoreConstants._CTX_MIDDLE: + yOff = ydiff / 2; + break; + case constants.BOTTOM: + yOff = ydiff; + break; + case textCoreConstants.IDEOGRAPHIC: + console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES? + break; + case textCoreConstants.HANGING: + console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES? + break; + } + + dataArr.forEach(ele => ele.y += yOff); + return dataArr; + } } function renderer2D(p5, fn){ diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 1c828d7a2e..0f93a7a910 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -13,6 +13,7 @@ import { Color } from "../color/p5.Color"; import { Element } from "../dom/p5.Element"; import { Framebuffer } from "../webgl/p5.Framebuffer"; import { DataArray } from "../webgl/p5.DataArray"; +import { textCoreConstants } from "../type/textCore"; import { RenderBuffer } from "../webgl/p5.RenderBuffer"; import { Image } from "../image/p5.Image"; import { Texture } from "../webgl/p5.Texture"; @@ -1643,17 +1644,6 @@ export class Renderer3D extends Renderer { } } - _setPointUniforms(pointShader) { - // set the uniform values - pointShader.setUniform("uMaterialColor", this.states.curStrokeColor); - // @todo is there an instance where this isn't stroke weight? - // should be they be same var? - pointShader.setUniform( - "uPointSize", - this.states.strokeWeight * this._pixelDensity - ); - } - /** * @private * Note: DO NOT CALL THIS while in the middle of binding another texture, @@ -1731,4 +1721,100 @@ export class Renderer3D extends Renderer { _vToNArray(arr) { return arr.flatMap((item) => [item.x, item.y, item.z]); } + + /////////////////////////////// + //// TEXT SUPPORT METHODS + ////////////////////////////// + + textCanvas() { + if (!this._textCanvas) { + this._textCanvas = document.createElement('canvas'); + this._textCanvas.width = 1; + this._textCanvas.height = 1; + this._textCanvas.style.display = 'none'; + // Has to be added to the DOM for measureText to work properly! + this.canvas.parentElement.insertBefore(this._textCanvas, this.canvas); + } + return this._textCanvas; + } + + textDrawingContext() { + if (!this._textDrawingContext) { + const textCanvas = this.textCanvas(); + this._textDrawingContext = textCanvas.getContext('2d'); + } + return this._textDrawingContext; + } + + _positionLines(x, y, width, height, lines) { + let { textLeading, textAlign } = this.states; + const widths = lines.map(line => this._fontWidthSingle(line)); + let adjustedX, lineData = new Array(lines.length); + let adjustedW = typeof width === 'undefined' ? Math.max(0, ...widths) : width; + let adjustedH = typeof height === 'undefined' ? 0 : height; + + for (let i = 0; i < lines.length; i++) { + switch (textAlign) { + case textCoreConstants.START: + throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT + case constants.LEFT: + adjustedX = x; + break; + case constants.CENTER: + adjustedX = x + + (adjustedW - widths[i]) / 2 - + adjustedW / 2 + + (width || 0) / 2; + break; + case constants.RIGHT: + adjustedX = x + adjustedW - widths[i] - adjustedW + (width || 0); + break; + case textCoreConstants.END: + throw new Error('textBounds: END not yet supported for textAlign'); + default: + adjustedX = x; + break; + } + lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; + } + + return this._yAlignOffset(lineData, adjustedH); + } + + _yAlignOffset(dataArr, height) { + if (typeof height === 'undefined') { + throw Error('_yAlignOffset: height is required'); + } + + let { textLeading, textBaseline, textSize, textFont } = this.states; + let yOff = 0, numLines = dataArr.length; + let totalHeight = textSize * numLines + + ((textLeading - textSize) * (numLines - 1)); + switch (textBaseline) { // drawingContext ? + case constants.TOP: + yOff = textSize; + break; + case constants.BASELINE: + break; + case textCoreConstants._CTX_MIDDLE: + yOff = -totalHeight / 2 + textSize + (height || 0) / 2; + break; + case constants.BOTTOM: + yOff = -(totalHeight - textSize) + (height || 0); + break; + default: + console.warn(`${textBaseline} is not supported in WebGL mode.`); // FES? + break; + } + yOff += this.states.textFont.font?._verticalAlign(textSize) || 0; + dataArr.forEach(ele => ele.y += yOff); + return dataArr; + } + + remove() { + if (this._textCanvas) { + this._textCanvas.parentElement.removeChild(this._textCanvas); + } + super.remove(); + } } diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index 26fbdd4996..bb5fdeada3 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -1064,6 +1064,9 @@ async function create(pInst, name, path, descriptors, rawFont) { // ensure the font is ready to be rendered await document.fonts.ready; + // Await loading of the font via CSS in case it also loads other resources + await document.fonts.load(`1em "${name}"`); + // return a new p5.Font return new Font(pInst, face, name, path, rawFont); } diff --git a/src/type/textCore.js b/src/type/textCore.js index cd55559b12..4ba10dc5d2 100644 --- a/src/type/textCore.js +++ b/src/type/textCore.js @@ -5,6 +5,8 @@ import { Renderer } from '../core/p5.Renderer'; +export const DefaultFill = '#000000'; + export const textCoreConstants = { IDEOGRAPHIC: 'ideographic', RIGHT_TO_LEFT: 'rtl', @@ -19,7 +21,6 @@ export const textCoreConstants = { function textCore(p5, fn) { const LeadingScale = 1.275; - const DefaultFill = '#000000'; const LinebreakRe = /\r?\n/g; const CommaDelimRe = /,\s+/; const QuotedRe = /^".*"$/; @@ -2525,208 +2526,6 @@ function textCore(p5, fn) { return this._pInst; }; - - if (p5.Renderer2D) { - p5.Renderer2D.prototype.textCanvas = function () { - return this.canvas; - }; - p5.Renderer2D.prototype.textDrawingContext = function () { - return this.drawingContext; - }; - - p5.Renderer2D.prototype._renderText = function (text, x, y, maxY, minY) { - let states = this.states; - let context = this.textDrawingContext(); - - if (y < minY || y >= maxY) { - return; // don't render lines beyond minY/maxY - } - - this.push(); - - // no stroke unless specified by user - if (states.strokeColor && states.strokeSet) { - context.strokeText(text, x, y); - } - - if (!this._clipping && states.fillColor) { - - // if fill hasn't been set by user, use default text fill - if (!states.fillSet) { - this._setFill(DefaultFill); - } - context.fillText(text, x, y); - } - - this.pop(); - }; - - /* - Position the lines of text based on their textAlign/textBaseline properties - */ - p5.Renderer2D.prototype._positionLines = function ( - x, y, - width, height, - lines - ) { - - let { textLeading, textAlign } = this.states; - let adjustedX, lineData = new Array(lines.length); - let adjustedW = typeof width === 'undefined' ? 0 : width; - let adjustedH = typeof height === 'undefined' ? 0 : height; - - for (let i = 0; i < lines.length; i++) { - switch (textAlign) { - case textCoreConstants.START: - throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT - case fn.LEFT: - adjustedX = x; - break; - case fn.CENTER: - adjustedX = x + adjustedW / 2; - break; - case fn.RIGHT: - adjustedX = x + adjustedW; - break; - case textCoreConstants.END: - throw new Error('textBounds: END not yet supported for textAlign'); - } - lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; - } - - return this._yAlignOffset(lineData, adjustedH); - }; - - /* - Get the y-offset for text given the height, leading, line-count and textBaseline property - */ - p5.Renderer2D.prototype._yAlignOffset = function (dataArr, height) { - - if (typeof height === 'undefined') { - throw Error('_yAlignOffset: height is required'); - } - - let { textLeading, textBaseline } = this.states; - let yOff = 0, numLines = dataArr.length; - let ydiff = height - (textLeading * (numLines - 1)); - switch (textBaseline) { // drawingContext ? - case fn.TOP: - break; // ?? - case fn.BASELINE: - break; - case textCoreConstants._CTX_MIDDLE: - yOff = ydiff / 2; - break; - case fn.BOTTOM: - yOff = ydiff; - break; - case textCoreConstants.IDEOGRAPHIC: - console.warn('textBounds: IDEOGRAPHIC not yet supported for textBaseline'); // FES? - break; - case textCoreConstants.HANGING: - console.warn('textBounds: HANGING not yet supported for textBaseline'); // FES? - break; - } - dataArr.forEach(ele => ele.y += yOff); - return dataArr; - }; - } - - if (p5.RendererGL) { - p5.RendererGL.prototype.textCanvas = function() { - if (!this._textCanvas) { - this._textCanvas = document.createElement('canvas'); - this._textCanvas.width = 1; - this._textCanvas.height = 1; - this._textCanvas.style.display = 'none'; - // Has to be added to the DOM for measureText to work properly! - this.canvas.parentElement.insertBefore(this._textCanvas, this.canvas); - } - return this._textCanvas; - }; - p5.RendererGL.prototype.textDrawingContext = function() { - if (!this._textDrawingContext) { - const textCanvas = this.textCanvas(); - this._textDrawingContext = textCanvas.getContext('2d'); - } - return this._textDrawingContext; - }; - const oldRemove = p5.RendererGL.prototype.remove; - p5.RendererGL.prototype.remove = function() { - if (this._textCanvas) { - this._textCanvas.parentElement.removeChild(this._textCanvas); - } - oldRemove.call(this); - }; - - p5.RendererGL.prototype._positionLines = function ( - x, y, - width, height, - lines - ) { - - let { textLeading, textAlign } = this.states; - const widths = lines.map(line => this._fontWidthSingle(line)); - let adjustedX, lineData = new Array(lines.length); - let adjustedW = typeof width === 'undefined' ? Math.max(0, ...widths) : width; - let adjustedH = typeof height === 'undefined' ? 0 : height; - - for (let i = 0; i < lines.length; i++) { - switch (textAlign) { - case textCoreConstants.START: - throw new Error('textBounds: START not yet supported for textAlign'); // default to LEFT - case fn.LEFT: - adjustedX = x; - break; - case fn.CENTER: - adjustedX = x + - (adjustedW - widths[i]) / 2 - - adjustedW / 2 + - (width || 0) / 2; - break; - case fn.RIGHT: - adjustedX = x + adjustedW - widths[i] - adjustedW + (width || 0); - break; - case textCoreConstants.END: - throw new Error('textBounds: END not yet supported for textAlign'); - } - lineData[i] = { text: lines[i], x: adjustedX, y: y + i * textLeading }; - } - - return this._yAlignOffset(lineData, adjustedH); - }; - - p5.RendererGL.prototype._yAlignOffset = function (dataArr, height) { - - if (typeof height === 'undefined') { - throw Error('_yAlignOffset: height is required'); - } - - let { textLeading, textBaseline, textSize, textFont } = this.states; - let yOff = 0, numLines = dataArr.length; - let totalHeight = textSize * numLines + - ((textLeading - textSize) * (numLines - 1)); - switch (textBaseline) { // drawingContext ? - case fn.TOP: - yOff = textSize; - break; - case fn.BASELINE: - break; - case textCoreConstants._CTX_MIDDLE: - yOff = -totalHeight / 2 + textSize + (height || 0) / 2; - break; - case fn.BOTTOM: - yOff = -(totalHeight - textSize) + (height || 0); - break; - default: - console.warn(`${textBaseline} is not supported in WebGL mode.`); // FES? - break; - } - yOff += this.states.textFont.font?._verticalAlign(textSize) || 0; // Does this function exist? - dataArr.forEach(ele => ele.y += yOff); - return dataArr; - }; - } } export default textCore; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 7fa69a7351..6ef5721e7b 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -1874,7 +1874,7 @@ function primitives3D(p5, fn){ if (typeof args[4] === 'undefined') { // Use the retained mode for drawing rectangle, // if args for rounding rectangle is not provided by user. - const perPixelLighting = this._pInst._glAttributes?.perPixelLighting; + const perPixelLighting = this._pInst._glAttributes?.perPixelLighting ?? true; const detailX = args[4] || (perPixelLighting ? 1 : 24); const detailY = args[5] || (perPixelLighting ? 1 : 16); const gid = `rect|${detailX}|${detailY}`; diff --git a/src/webgl/p5.Camera.js b/src/webgl/p5.Camera.js index 300d2f1d47..24f5d5fd79 100644 --- a/src/webgl/p5.Camera.js +++ b/src/webgl/p5.Camera.js @@ -238,8 +238,8 @@ class Camera { let A, B; if (range[0] === 0) { // WebGPU clip space, z in [0, 1] - A = far * nf; - B = far * near * nf; + A = far / (near - far); + B = (far * near) / (near - far); } else { // WebGL clip space, z in [-1, 1] A = (far + near) * nf; @@ -1792,8 +1792,8 @@ class Camera { this.defaultCenterX = 0; this.defaultCenterY = 0; this.defaultCenterZ = 0; - this.defaultCameraNear = this.defaultEyeZ * 0.1; - this.defaultCameraFar = this.defaultEyeZ * 10; + this.defaultCameraNear = this.defaultEyeZ * this._renderer.defaultNearScale(); + this.defaultCameraFar = this.defaultEyeZ * this._renderer.defaultFarScale(); } //detect if user didn't set the camera diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 77c8aa57de..07181bcf61 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -31,8 +31,6 @@ import fontVert from "./shaders/font.vert"; import fontFrag from "./shaders/font.frag"; import lineVert from "./shaders/line.vert"; import lineFrag from "./shaders/line.frag"; -import pointVert from "./shaders/point.vert"; -import pointFrag from "./shaders/point.frag"; import imageLightVert from "./shaders/imageLight.vert"; import imageLightDiffusedFrag from "./shaders/imageLightDiffused.frag"; import imageLightSpecularFrag from "./shaders/imageLightSpecular.frag"; @@ -63,8 +61,6 @@ const defaultShaders = { fontFrag, lineVert: lineDefs + lineVert, lineFrag: lineDefs + lineFrag, - pointVert, - pointFrag, imageLightVert, imageLightDiffusedFrag, imageLightSpecularFrag, @@ -143,31 +139,6 @@ class RendererGL extends Renderer3D { // Rendering ////////////////////////////////////////////// - /*_drawPoints(vertices, vertexBuffer) { - const gl = this.GL; - const pointShader = this._getPointShader(); - pointShader.bindShader(); - this._setGlobalUniforms(pointShader); - this._setPointUniforms(pointShader); - pointShader.bindTextures(); - - this._bindBuffer( - vertexBuffer, - gl.ARRAY_BUFFER, - this._vToNArray(vertices), - Float32Array, - gl.STATIC_DRAW - ); - - pointShader.enableAttrib(pointShader.attributes.aPosition, 3); - - this._applyColorBlend(this.states.curStrokeColor); - - gl.drawArrays(gl.Points, 0, vertices.length); - - pointShader.unbindShader(); - }*/ - /** * @private sets blending in gl context to curBlendMode * @param {Number[]} color [description] @@ -302,6 +273,9 @@ class RendererGL extends Renderer3D { ); } } + } else if (this._curShader.shaderType === 'text') { + // Text rendering uses a fixed quad geometry with 6 indices + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); } else if (glBuffers.indexBuffer) { this._bindBuffer(glBuffers.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); @@ -457,10 +431,9 @@ class RendererGL extends Renderer3D { gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - // Make sure all images are loaded into the canvas premultiplied so that - // they match the way we render colors. This will make framebuffer textures - // be encoded the same way as textures from everything else. - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); + // Make sure all images are loaded into the canvas non-premultiplied so that + // they can be handled consistently in shaders. + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); this._viewport = this.drawingContext.getParameter( this.drawingContext.VIEWPORT ); @@ -618,6 +591,12 @@ class RendererGL extends Renderer3D { zClipRange() { return [-1, 1]; } + defaultNearScale() { + return 0.1; + } + defaultFarScale() { + return 10; + } viewport(w, h) { this._viewport = [0, 0, w, h]; @@ -835,34 +814,6 @@ class RendererGL extends Renderer3D { return this._defaultColorShader; } - _getPointShader() { - if (!this._defaultPointShader) { - this._defaultPointShader = new Shader( - this, - this._webGL2CompatibilityPrefix("vert", "mediump") + - defaultShaders.pointVert, - this._webGL2CompatibilityPrefix("frag", "mediump") + - defaultShaders.pointFrag, - { - vertex: { - "void beforeVertex": "() {}", - "vec3 getLocalPosition": "(vec3 position) { return position; }", - "vec3 getWorldPosition": "(vec3 position) { return position; }", - "float getPointSize": "(float size) { return size; }", - "void afterVertex": "() {}", - }, - fragment: { - "void beforeFragment": "() {}", - "vec4 getFinalColor": "(vec4 color) { return color; }", - "bool shouldDiscard": "(bool outside) { return outside; }", - "void afterFragment": "() {}", - }, - } - ); - } - return this._defaultPointShader; - } - _getLineShader() { if (!this._defaultLineShader) { this._defaultLineShader = new Shader( @@ -1341,7 +1292,6 @@ class RendererGL extends Renderer3D { hasTransparency || this.states.userFillShader || this.states.userStrokeShader || - this.states.userPointShader || isTexture || this.states.curBlendMode !== constants.BLEND || colors[colors.length - 1] < 1.0 || diff --git a/src/webgl/shaders/font.vert b/src/webgl/shaders/font.vert index ce8b84ab18..6d893e8826 100644 --- a/src/webgl/shaders/font.vert +++ b/src/webgl/shaders/font.vert @@ -7,7 +7,6 @@ uniform vec4 uGlyphRect; uniform float uGlyphOffset; OUT vec2 vTexCoord; -OUT float w; void main() { vec4 positionVec4 = vec4(aPosition, 1.0); @@ -40,5 +39,4 @@ void main() { gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; vTexCoord = aTexCoord + textureOffset; - w = gl_Position.w; } diff --git a/src/webgl/shaders/light_texture.frag b/src/webgl/shaders/light_texture.frag index e02083b97b..f61fc39cca 100644 --- a/src/webgl/shaders/light_texture.frag +++ b/src/webgl/shaders/light_texture.frag @@ -14,13 +14,12 @@ void main(void) { } else { vec4 baseColor = isTexture - // Textures come in with premultiplied alpha. To apply tint and still have - // premultiplied alpha output, we need to multiply the RGB channels by the - // tint RGB, and all channels by the tint alpha. - ? TEXTURE(uSampler, vVertTexCoord) * vec4(uTint.rgb/255., 1.) * (uTint.a/255.) - // Colors come in with unmultiplied alpha, so we need to multiply the RGB - // channels by alpha to convert it to premultiplied alpha. - : vec4(vColor.rgb * vColor.a, vColor.a); + // Textures come in with non-premultiplied alpha. Apply tint. + ? TEXTURE(uSampler, vVertTexCoord) * (uTint/255.) + // Colors come in with non-premultiplied alpha. + : vColor; + // Convert to premultiplied alpha for consistent output + baseColor.rgb *= baseColor.a; OUT_COLOR = vec4(baseColor.rgb * vDiffuseColor + vSpecularColor, baseColor.a); } } diff --git a/src/webgl/shaders/phong.frag b/src/webgl/shaders/phong.frag index 78cfb76163..5711a01e6d 100644 --- a/src/webgl/shaders/phong.frag +++ b/src/webgl/shaders/phong.frag @@ -47,13 +47,8 @@ void main(void) { inputs.texCoord = vTexCoord; inputs.ambientLight = uAmbientColor; inputs.color = isTexture - ? TEXTURE(uSampler, vTexCoord) * (vec4(uTint.rgb/255., 1.) * uTint.a/255.) + ? TEXTURE(uSampler, vTexCoord) * (uTint/255.) : vColor; - if (isTexture && inputs.color.a > 0.0) { - // Textures come in with premultiplied alpha. Temporarily unpremultiply it - // so hooks users don't have to think about premultiplied alpha. - inputs.color.rgb /= inputs.color.a; - } inputs.shininess = uShininess; inputs.metalness = uMetallic; inputs.ambientMaterial = uHasSetAmbient ? uAmbientMatColor.rgb : inputs.color.rgb; diff --git a/src/webgl/shaders/point.frag b/src/webgl/shaders/point.frag deleted file mode 100644 index d87cbf0c61..0000000000 --- a/src/webgl/shaders/point.frag +++ /dev/null @@ -1,29 +0,0 @@ -precision mediump int; -uniform vec4 uMaterialColor; -IN float vStrokeWeight; - -void main(){ - HOOK_beforeFragment(); - float mask = 0.0; - - // make a circular mask using the gl_PointCoord (goes from 0 - 1 on a point) - // might be able to get a nicer edge on big strokeweights with smoothstep but slightly less performant - - mask = step(0.98, length(gl_PointCoord * 2.0 - 1.0)); - - // if strokeWeight is 1 or less lets just draw a square - // this prevents weird artifacting from carving circles when our points are really small - // if strokeWeight is larger than 1, we just use it as is - - mask = mix(0.0, mask, clamp(floor(vStrokeWeight - 0.5),0.0,1.0)); - - // throw away the borders of the mask - // otherwise we get weird alpha blending issues - - if(HOOK_shouldDiscard(mask > 0.98)){ - discard; - } - - OUT_COLOR = HOOK_getFinalColor(vec4(uMaterialColor.rgb, 1.) * uMaterialColor.a); - HOOK_afterFragment(); -} diff --git a/src/webgl/shaders/point.vert b/src/webgl/shaders/point.vert deleted file mode 100644 index 6eeb741a64..0000000000 --- a/src/webgl/shaders/point.vert +++ /dev/null @@ -1,19 +0,0 @@ -IN vec3 aPosition; -uniform float uPointSize; -OUT float vStrokeWeight; -uniform mat4 uModelViewMatrix; -uniform mat4 uProjectionMatrix; - -void main() { - HOOK_beforeVertex(); - vec4 viewModelPosition = vec4(HOOK_getWorldPosition( - (uModelViewMatrix * vec4(HOOK_getLocalPosition(aPosition), 1.0)).xyz - ), 1.); - gl_Position = uProjectionMatrix * viewModelPosition; - - float pointSize = HOOK_getPointSize(uPointSize); - - gl_PointSize = pointSize; - vStrokeWeight = pointSize; - HOOK_afterVertex(); -} diff --git a/src/webgl/shaders/webgl2Compatibility.glsl b/src/webgl/shaders/webgl2Compatibility.glsl index 8c9dbddec6..15bfe7ed24 100644 --- a/src/webgl/shaders/webgl2Compatibility.glsl +++ b/src/webgl/shaders/webgl2Compatibility.glsl @@ -26,9 +26,5 @@ out vec4 outColor; #endif #ifdef FRAGMENT_SHADER -vec4 getTexture(in sampler2D content, vec2 coord) { - vec4 color = TEXTURE(content, coord); - if (color.a > 0.) color.rgb /= color.a; - return color; -} +#define getTexture TEXTURE #endif diff --git a/src/webgl/text.js b/src/webgl/text.js index b5a9842345..c8db34fc87 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -25,17 +25,6 @@ function text(p5, fn) { } }; - // Text/Typography (see src/type/textCore.js) - /* - Renderer3D.prototype.textWidth = function(s) { - if (this._isOpenType()) { - return this.states.textFont.font._textWidth(s, this.states.textSize); - } - - return 0; // TODO: error - }; - */ - // rendering constants // the number of rows/columns dividing each glyph @@ -729,7 +718,6 @@ function text(p5, fn) { this.scale(scale, scale, 1); // initialize the font shader - const gl = this.GL; const initializeShader = !this._defaultFontShader; const sh = this._getFontShader(); sh.init(); @@ -745,7 +733,7 @@ function text(p5, fn) { const curFillColor = this.states.fillSet ? this.states.curFillColor - : [0, 0, 0, 255]; + : [0, 0, 0, 1]; this._setGlobalUniforms(sh); this._applyColorBlend(curFillColor); @@ -775,14 +763,9 @@ function text(p5, fn) { for (const buff of this.buffers.text) { buff._prepareBuffer(g, sh); } - this._bindBuffer( - this.geometryBufferCache.cache.glyph.indexBuffer, - gl.ELEMENT_ARRAY_BUFFER - ); // this will have to do for now... sh.setUniform('uMaterialColor', curFillColor); - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); this.glyphDataCache = this.glyphDataCache || new Set(); @@ -834,7 +817,7 @@ function text(p5, fn) { sh.bindTextures(); // afterwards, only textures need updating // draw it - gl.drawElements(gl.TRIANGLES, 6, this.GL.UNSIGNED_SHORT, 0); + this._drawBuffers(g, { mode: constants.TRIANGLES, count: 1 }); } } } finally { @@ -843,7 +826,6 @@ function text(p5, fn) { this.states.setValue('strokeColor', doStroke); this.states.setValue('drawMode', drawMode); - gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); this.pop(); } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 128bbf431b..2332a1d8f9 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -9,6 +9,7 @@ import * as constants from '../core/constants'; import { colorVertexShader, colorFragmentShader } from './shaders/color'; import { lineVertexShader, lineFragmentShader} from './shaders/line'; import { materialVertexShader, materialFragmentShader } from './shaders/material'; +import { fontVertexShader, fontFragmentShader } from './shaders/font'; import {Graphics} from "../core/p5.Graphics"; import {Element} from "../dom/p5.Element"; @@ -50,17 +51,10 @@ class RendererWebGPU extends Renderer3D { async _initContext() { this.adapter = await navigator.gpu?.requestAdapter(this._webgpuAttributes); - // console.log('Adapter:'); - // console.log(this.adapter); - if (this.adapter) { - console.log([...this.adapter.features]); - } this.device = await this.adapter?.requestDevice({ // Todo: check support requiredFeatures: ['depth32float-stencil8'] }); - // console.log('Device:'); - // console.log(this.device); if (!this.device) { throw new Error('Your browser does not support WebGPU.'); } @@ -71,6 +65,7 @@ class RendererWebGPU extends Renderer3D { device: this.device, format: this.presentationFormat, usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + alphaMode: 'premultiplied', }); // TODO disablable stencil @@ -203,6 +198,47 @@ class RendererWebGPU extends Renderer3D { this.queue.submit([commandEncoder.finish()]); } + /** + * Resets all depth information so that nothing previously drawn will + * occlude anything subsequently drawn. + */ + clearDepth(depth = 1) { + 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 depthTexture = activeFramebuffer ? + (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : + this.depthTexture; + const depthTextureView = depthTexture?.createView(); + + 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.queue.submit([commandEncoder.finish()]); + } + _prepareBuffer(renderBuffer, geometry, shader) { const attr = shader.attributes[renderBuffer.attr]; if (!attr) return; @@ -319,12 +355,18 @@ class RendererWebGPU extends Renderer3D { this._getWebGPUDepthFormat(activeFramebuffer) : this.depthFormat; + const drawTarget = this.drawTarget(); + const clipping = this._clipping; + const clipApplied = drawTarget._isClipApplied; + return { topology: mode === constants.TRIANGLE_STRIP ? 'triangle-strip' : 'triangle-list', blendMode: this.states.curBlendMode, sampleCount, format, depthFormat, + clipping, + clipApplied, } } @@ -335,8 +377,8 @@ class RendererWebGPU extends Renderer3D { shader.fragModule = device.createShaderModule({ code: shader.fragSrc() }); shader._pipelineCache = new Map(); - shader.getPipeline = ({ topology, blendMode, sampleCount, format, depthFormat }) => { - const key = `${topology}_${blendMode}_${sampleCount}_${format}_${depthFormat}`; + 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, @@ -358,21 +400,21 @@ class RendererWebGPU extends Renderer3D { depthStencil: { format: depthFormat, depthWriteEnabled: true, - depthCompare: 'less', + depthCompare: 'less-equal', stencilFront: { - compare: 'always', + compare: clipping ? 'always' : (clipApplied ? 'equal' : 'always'), failOp: 'keep', depthFailOp: 'keep', - passOp: 'keep', + passOp: clipping ? 'replace' : 'keep', }, stencilBack: { - compare: 'always', + compare: clipping ? 'always' : (clipApplied ? 'equal' : 'always'), failOp: 'keep', depthFailOp: 'keep', - passOp: 'keep', + passOp: clipping ? 'replace' : 'keep', }, - stencilReadMask: 0xFFFFFFFF, // TODO - stencilWriteMask: 0xFFFFFFFF, + stencilReadMask: clipApplied ? 0xFFFFFFFF : 0x00000000, + stencilWriteMask: clipping ? 0xFFFFFFFF : 0x00000000, stencilLoadOp: "load", stencilStoreOp: "store", }, @@ -675,6 +717,12 @@ class RendererWebGPU extends Renderer3D { zClipRange() { return [0, 1]; } + defaultNearScale() { + return 0.01; + } + defaultFarScale() { + return 100; + } _resetBuffersBeforeDraw() { const commandEncoder = this.device.createCommandEncoder(); @@ -753,11 +801,9 @@ class RendererWebGPU extends Renderer3D { passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode }))); // Bind vertex buffers for (const buffer of this._getVertexBuffers(currentShader)) { - passEncoder.setVertexBuffer( - currentShader.attributes[buffer.attr].location, - buffers[buffer.dst], - 0 - ); + const location = currentShader.attributes[buffer.attr].location; + const gpuBuffer = buffers[buffer.dst]; + passEncoder.setVertexBuffer(location, gpuBuffer, 0); } // Bind uniforms this._packUniforms(this._curShader); @@ -810,6 +856,13 @@ class RendererWebGPU extends Renderer3D { } 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") { @@ -837,11 +890,34 @@ class RendererWebGPU extends Renderer3D { for (const name in shader.uniforms) { const uniform = shader.uniforms[name]; if (uniform.isSampler) continue; - if (uniform.type === 'u32') { - shader._uniformDataView.setUint32(uniform.offset, uniform._cachedData, true); + + if (uniform.baseType === 'u32') { + if (uniform.size === 4) { + // Single u32 + shader._uniformDataView.setUint32(uniform.offset, uniform._cachedData, true); + } else { + // Vector of u32s + const data = uniform._cachedData; + for (let i = 0; i < data.length; i++) { + shader._uniformDataView.setUint32(uniform.offset + i * 4, data[i], true); + } + } + } else if (uniform.baseType === 'i32') { + if (uniform.size === 4) { + // Single i32 + shader._uniformDataView.setInt32(uniform.offset, uniform._cachedData, true); + } else { + // Vector of i32s + const data = uniform._cachedData; + for (let i = 0; i < data.length; i++) { + shader._uniformDataView.setInt32(uniform.offset + i * 4, data[i], true); + } + } } else if (uniform.size === 4) { + // Single float value shader._uniformData.set([uniform._cachedData], uniform.offset / 4); } else { + // Float array (including vec2, vec3, vec4, mat4x4) shader._uniformData.set(uniform._cachedData, uniform.offset / 4); } } @@ -866,13 +942,21 @@ class RendererWebGPU extends Renderer3D { const baseAlignAndSize = (type) => { if (['f32', 'i32', 'u32', 'bool'].includes(type)) { - return { align: 4, size: 4, items: 1 }; + return { align: 4, size: 4, items: 1, baseType: type }; } if (/^vec[2-4](|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 }; + 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])?(|f)$/.test(type)) { if (type[4] === 'x' && type[3] !== type[5]) { @@ -892,7 +976,7 @@ class RendererWebGPU extends Renderer3D { 0 ] : undefined; - return { align, size, pack, items: dim * dim }; + return { align, size, pack, items: dim * dim, baseType: 'f32' }; } if (/^array<.+>$/.test(type)) { const [, subtype, rawLength] = type.match(/^array<(.+),\s*(\d+)>/); @@ -901,7 +985,8 @@ class RendererWebGPU extends Renderer3D { align: elemAlign, size: elemSize, items: elemItems, - pack: elemPack = (data) => [...data] + pack: elemPack = (data) => [...data], + baseType: elemBaseType } = baseAlignAndSize(subtype); const stride = Math.ceil(elemSize / elemAlign) * elemAlign; const pack = (data) => { @@ -920,6 +1005,7 @@ class RendererWebGPU extends Renderer3D { size: stride * length, items: elemItems * length, pack, + baseType: elemBaseType }; } throw new Error(`Unknown type in WGSL struct: ${type}`); @@ -927,7 +1013,7 @@ class RendererWebGPU extends Renderer3D { while ((match = elementRegex.exec(structBody)) !== null) { const [_, location, name, type] = match; - const { size, align, pack } = baseAlignAndSize(type); + const { size, align, pack, baseType } = baseAlignAndSize(type); offset = Math.ceil(offset / align) * align; const offsetEnd = offset + size; elements[name] = { @@ -938,7 +1024,8 @@ class RendererWebGPU extends Renderer3D { size, offset, offsetEnd, - pack + pack, + baseType }; index++; offset = offsetEnd; @@ -1184,6 +1271,17 @@ class RendererWebGPU extends Renderer3D { return this._defaultLineShader; } + _getFontShader() { + if (!this._defaultFontShader) { + this._defaultFontShader = new Shader( + this, + fontVertexShader, + fontFragmentShader + ); + } + return this._defaultFontShader; + } + ////////////////////////////////////////////// // Setting ////////////////////////////////////////////// diff --git a/src/webgpu/shaders/font.js b/src/webgpu/shaders/font.js new file mode 100644 index 0000000000..7cd92c6bff --- /dev/null +++ b/src/webgpu/shaders/font.js @@ -0,0 +1,283 @@ +const uniforms = ` +struct Uniforms { + uModelViewMatrix: mat4x4, + uProjectionMatrix: mat4x4, + uStrokeImageSize: vec2, + uCellsImageSize: vec2, + uGridImageSize: vec2, + uGridOffset: vec2, + uGridSize: vec2, + uGlyphRect: vec4, + uGlyphOffset: f32, + uMaterialColor: vec4, +}; +`; + +export const fontVertexShader = ` +struct VertexInput { + @location(0) aPosition: vec3, + @location(1) aTexCoord: vec2, +}; + +struct VertexOutput { + @builtin(position) Position: vec4, + @location(0) vTexCoord: vec2, +}; + +${uniforms} +@group(0) @binding(0) var uniforms: Uniforms; + +@vertex +fn main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + var positionVec4 = vec4(input.aPosition, 1.0); + + // scale by the size of the glyph's rectangle + positionVec4.x = positionVec4.x * (uniforms.uGlyphRect.z - uniforms.uGlyphRect.x); + positionVec4.y = positionVec4.y * (uniforms.uGlyphRect.w - uniforms.uGlyphRect.y); + + // Expand glyph bounding boxes by 1px on each side to give a bit of room + // for antialiasing + let newOrigin = (uniforms.uModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; + let newDX = (uniforms.uModelViewMatrix * vec4(1.0, 0.0, 0.0, 1.0)).xyz; + let newDY = (uniforms.uModelViewMatrix * vec4(0.0, 1.0, 0.0, 1.0)).xyz; + let pixelScale = vec2( + 1.0 / length(newOrigin - newDX), + 1.0 / length(newOrigin - newDY) + ); + let offset = pixelScale * normalize(input.aTexCoord - vec2(0.5, 0.5)); + let textureOffset = offset * (1.0 / vec2( + uniforms.uGlyphRect.z - uniforms.uGlyphRect.x, + uniforms.uGlyphRect.w - uniforms.uGlyphRect.y + )); + + // move to the corner of the glyph + positionVec4.x = positionVec4.x + uniforms.uGlyphRect.x; + positionVec4.y = positionVec4.y + uniforms.uGlyphRect.y; + + // move to the letter's line offset + positionVec4.x = positionVec4.x + uniforms.uGlyphOffset; + + positionVec4.x = positionVec4.x + offset.x; + positionVec4.y = positionVec4.y + offset.y; + + output.Position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4; + output.vTexCoord = input.aTexCoord + textureOffset; + + return output; +} +`; + +export const fontFragmentShader = ` +struct FragmentInput { + @location(0) vTexCoord: vec2, +}; + +${uniforms} +@group(0) @binding(0) var uniforms: Uniforms; + +@group(1) @binding(0) var uSamplerStrokes: texture_2d; +@group(1) @binding(1) var uSamplerStrokes_sampler: sampler; +@group(1) @binding(2) var uSamplerRowStrokes: texture_2d; +@group(1) @binding(3) var uSamplerRowStrokes_sampler: sampler; +@group(1) @binding(4) var uSamplerRows: texture_2d; +@group(1) @binding(5) var uSamplerRows_sampler: sampler; +@group(1) @binding(6) var uSamplerColStrokes: texture_2d; +@group(1) @binding(7) var uSamplerColStrokes_sampler: sampler; +@group(1) @binding(8) var uSamplerCols: texture_2d; +@group(1) @binding(9) var uSamplerCols_sampler: sampler; + +// some helper functions +fn ROUND_f32(v: f32) -> i32 { return i32(floor(v + 0.5)); } +fn ROUND_vec2(v: vec2) -> vec2 { return vec2(floor(v + 0.5)); } +fn saturate_f32(v: f32) -> f32 { return clamp(v, 0.0, 1.0); } +fn saturate_vec2(v: vec2) -> vec2 { return clamp(v, vec2(0.0), vec2(1.0)); } + +fn mul_f32_i32(v1: f32, v2: i32) -> i32 { + return i32(floor(v1 * f32(v2))); +} + +fn mul_vec2_ivec2(v1: vec2, v2: vec2) -> vec2 { + return vec2(floor(v1 * vec2(v2) + 0.5)); +} + +// unpack a 16-bit integer from a float vec2 +fn getInt16(v: vec2) -> i32 { + let iv = ROUND_vec2(v * 255.0); + return iv.x * 128 + iv.y; +} + +const minDistance: f32 = 1.0/8192.0; +const hardness: f32 = 1.05; // amount of antialias + +// the maximum number of curves in a glyph +const N: i32 = 250; + +// retrieves an indexed pixel from a texture +fn getTexel(texture: texture_2d, samp: sampler, pos: i32, size: vec2) -> vec4 { + let width = size.x; + let x = pos % width; + let y = pos / width; + + return textureLoad(texture, vec2(x, y), 0); +} + +fn calculateCrossings(p0: vec2, p1: vec2, p2: vec2, vTexCoord: vec2, pixelScale: vec2) -> array, 2> { + // get the coefficients of the quadratic in t + var a = p0 - p1 * 2.0 + p2; + var b = p0 - p1; + a = vec2( + select(a.x, sign(a.x) * 1e-6, abs(a.x) < 1e-6), + select(a.y, sign(a.y) * 1e-6, abs(a.y) < 1e-6) + ); + b = vec2( + select(b.x, sign(b.x) * 1e-6, abs(b.x) < 1e-6), + select(b.y, sign(b.y) * 1e-6, abs(b.y) < 1e-6) + ); + let c = p0 - vTexCoord; + + // found out which values of 't' it crosses the axes + let surd = sqrt(max(vec2(0.0), b * b - a * c)); + let t1 = ((b - surd) / a).yx; + let t2 = ((b + surd) / a).yx; + + // approximate straight lines to avoid rounding errors + var t1_fixed = t1; + var t2_fixed = t2; + if (abs(a.y) < 0.001) { + t1_fixed.x = c.y / (2.0 * b.y); + t2_fixed.x = c.y / (2.0 * b.y); + } + + if (abs(a.x) < 0.001) { + t1_fixed.y = c.x / (2.0 * b.x); + t2_fixed.y = c.x / (2.0 * b.x); + } + + // plug into quadratic formula to find the coordinates of the crossings + let C1 = ((a * t1_fixed - b * 2.0) * t1_fixed + c) * pixelScale; + let C2 = ((a * t2_fixed - b * 2.0) * t2_fixed + c) * pixelScale; + + return array, 2>(C1, C2); +} + +fn coverageX(p0: vec2, p1: vec2, p2: vec2, vTexCoord: vec2, pixelScale: vec2, coverage: ptr>, weight: ptr>) { + let crossings = calculateCrossings(p0, p1, p2, vTexCoord, pixelScale); + let C1 = crossings[0]; + let C2 = crossings[1]; + + // determine on which side of the x-axis the points lie + let y0 = p0.y > vTexCoord.y; + let y1 = p1.y > vTexCoord.y; + let y2 = p2.y > vTexCoord.y; + + // could we be under the curve (after t1)? + if ((y1 && !y2) || (!y1 && y0)) { + // add the coverage for t1 + (*coverage).x = (*coverage).x + saturate_f32(C1.x + 0.5); + // calculate the anti-aliasing for t1 + (*weight).x = min((*weight).x, abs(C1.x)); + } + + // are we outside the curve (after t2)? + if ((y1 && !y0) || (!y1 && y2)) { + // subtract the coverage for t2 + (*coverage).x = (*coverage).x - saturate_f32(C2.x + 0.5); + // calculate the anti-aliasing for t2 + (*weight).x = min((*weight).x, abs(C2.x)); + } +} + +// this is essentially the same as coverageX, but with the axes swapped +fn coverageY(p0: vec2, p1: vec2, p2: vec2, vTexCoord: vec2, pixelScale: vec2, coverage: ptr>, weight: ptr>) { + let crossings = calculateCrossings(p0, p1, p2, vTexCoord, pixelScale); + let C1 = crossings[0]; + let C2 = crossings[1]; + + let x0 = p0.x > vTexCoord.x; + let x1 = p1.x > vTexCoord.x; + let x2 = p2.x > vTexCoord.x; + + if ((x1 && !x2) || (!x1 && x0)) { + (*coverage).y = (*coverage).y - saturate_f32(C1.y + 0.5); + weight.y = min(weight.y, abs(C1.y)); + } + + if ((x1 && !x0) || (!x1 && x2)) { + (*coverage).y = (*coverage).y + saturate_f32(C2.y + 0.5); + (*weight).y = min((*weight).y, abs(C2.y)); + } +} + +@fragment +fn main(input: FragmentInput) -> @location(0) vec4 { + // var pixelScale: vec2; + var coverage: vec2 = vec2(0.0); + var weight: vec2 = vec2(0.5); + let pixelScale = hardness / fwidth(input.vTexCoord); + + // which grid cell is this pixel in? + let gridCoord = vec2(floor(input.vTexCoord * vec2(uniforms.uGridSize))); + + // intersect curves in this row + { + // the index into the row info bitmap + let rowIndex = gridCoord.y + uniforms.uGridOffset.y; + // fetch the info texel + let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, uniforms.uGridImageSize); + // unpack the rowInfo + let rowStrokeIndex = getInt16(rowInfo.xy); + let rowStrokeCount = getInt16(rowInfo.zw); + + for (var iRowStroke = 0; iRowStroke < N; iRowStroke = iRowStroke + 1) { + if (iRowStroke >= rowStrokeCount) { + break; + } + + // each stroke is made up of 3 points: the start and control point + // and the start of the next curve. + // fetch the indices of this pair of strokes: + let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, uniforms.uCellsImageSize); + + // unpack the stroke index + let strokePos = getInt16(strokeIndices.xy); + + // fetch the two strokes + let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize); + let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize); + + // calculate the coverage + coverageX(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight); + } + } + + // intersect curves in this column + { + let colIndex = gridCoord.x + uniforms.uGridOffset.x; + let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, uniforms.uGridImageSize); + let colStrokeIndex = getInt16(colInfo.xy); + let colStrokeCount = getInt16(colInfo.zw); + + for (var iColStroke = 0; iColStroke < N; iColStroke = iColStroke + 1) { + if (iColStroke >= colStrokeCount) { + break; + } + + let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, uniforms.uCellsImageSize); + + let strokePos = getInt16(strokeIndices.xy); + let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize); + let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize); + coverageY(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight); + } + } + + weight = saturate_vec2(vec2(1.0) - weight * 2.0); + let distance = max(weight.x + weight.y, minDistance); // manhattan approx. + let antialias = abs(dot(coverage, weight) / distance); + let cover = min(abs(coverage.x), abs(coverage.y)); + var outColor = vec4(uniforms.uMaterialColor.rgb, 1.0) * uniforms.uMaterialColor.a; + outColor = outColor * saturate_f32(max(antialias, cover)); + return outColor; +} +`; diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index bffc88c219..8eef145710 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -306,4 +306,304 @@ visualSuite("WebGPU", function () { }, ); }); + + visualSuite('Typography', function () { + visualSuite('textFont', function () { + visualTest('with a font file in WebGPU', async function (p5, screenshot) { + await p5.createCanvas(100, 100, p5.WEBGPU); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textFont(font); + p5.textAlign(p5.LEFT, p5.TOP); + p5.textSize(35); + p5.text('p5*js', -p5.width / 2, -p5.height / 2 + 10, p5.width); + await screenshot(); + }); + }); + + visualSuite('textWeight', function () { + visualTest('can control variable fonts from files in WebGPU', async function (p5, screenshot) { + await p5.createCanvas(100, 100, p5.WEBGPU); + const font = await p5.loadFont( + '/unit/assets/BricolageGrotesque-Variable.ttf' + ); + for (let weight = 400; weight <= 800; weight += 100) { + p5.push(); + p5.background(255); + p5.translate(-p5.width/2, -p5.height/2); + p5.textFont(font); + p5.textAlign(p5.LEFT, p5.TOP); + p5.textSize(35); + p5.textWeight(weight); + p5.text('p5*js', 0, 10, p5.width); + p5.pop(); + await screenshot(); + } + }); + }); + + visualSuite('textAlign', function () { + visualSuite('webgpu mode', () => { + visualTest('all alignments with single word', async function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM } + ]; + + await p5.createCanvas(300, 300, p5.WEBGPU); + p5.translate(-p5.width/2, -p5.height/2); + p5.textSize(60); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textFont(font); + for (const alignment of alignments) { + p5.background(255); + p5.textAlign(alignment.alignX, alignment.alignY); + const bb = p5.textBounds('Single Line', p5.width / 2, p5.height / 2); + p5.push(); + p5.push() + p5.noFill(); + p5.stroke('red'); + p5.rect(bb.x, bb.y, bb.w, bb.h); + p5.pop() + p5.fill(0) + p5.text('Single Line', p5.width / 2, p5.height / 2); + p5.pop(); + await screenshot(); + } + }); + + visualTest('all alignments with single line', async function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM } + ]; + + await p5.createCanvas(300, 300, p5.WEBGPU); + p5.translate(-p5.width/2, -p5.height/2); + p5.textSize(45); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textFont(font); + for (const alignment of alignments) { + p5.background(255); + p5.textAlign(alignment.alignX, alignment.alignY); + p5.text('Single Line', p5.width / 2, p5.height / 2); + const bb = p5.textBounds('Single Line', p5.width / 2, p5.height / 2); + p5.push(); + p5.noFill(); + p5.stroke('red'); + p5.rect(bb.x, bb.y, bb.w, bb.h); + p5.pop(); + await screenshot(); + } + }); + + visualTest('all alignments with multi-lines and wrap word', + async function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM } + ]; + + await p5.createCanvas(150, 100, p5.WEBGPU); + p5.translate(-p5.width/2, -p5.height/2); + p5.textSize(20); + p5.textWrap(p5.WORD); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textFont(font); + + let xPos = 20; + let yPos = 20; + const boxWidth = 100; + const boxHeight = 60; + + for (const alignment of alignments) { + p5.background(255); + p5.push(); + p5.textAlign(alignment.alignX, alignment.alignY); + + p5.noFill(); + p5.strokeWeight(2); + p5.stroke(200); + p5.rect(xPos, yPos, boxWidth, boxHeight); + + p5.fill(0); + p5.noStroke(); + p5.text( + 'A really long text that should wrap automatically as it reaches the end of the box', + xPos, + yPos, + boxWidth, + boxHeight + ); + const bb = p5.textBounds( + 'A really long text that should wrap automatically as it reaches the end of the box', + xPos, + yPos, + boxWidth, + boxHeight + ); + p5.noFill(); + p5.stroke('red'); + p5.rect(bb.x, bb.y, bb.w, bb.h); + p5.pop(); + + await screenshot(); + } + } + ); + + visualTest( + 'all alignments with multi-lines and wrap char', + async function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM } + ]; + + await p5.createCanvas(150, 100, p5.WEBGPU); + p5.translate(-p5.width/2, -p5.height/2); + p5.textSize(19); + p5.textWrap(p5.CHAR); + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textFont(font); + + let xPos = 20; + let yPos = 20; + const boxWidth = 100; + const boxHeight = 60; + + for (const alignment of alignments) { + p5.background(255); + p5.push(); + p5.textAlign(alignment.alignX, alignment.alignY); + + p5.noFill(); + p5.strokeWeight(2); + p5.stroke(200); + p5.rect(xPos, yPos, boxWidth, boxHeight); + + p5.fill(0); + p5.noStroke(); + p5.text( + 'A really long text that should wrap automatically as it reaches the end of the box', + xPos, + yPos, + boxWidth, + boxHeight + ); + const bb = p5.textBounds( + 'A really long text that should wrap automatically as it reaches the end of the box', + xPos, + yPos, + boxWidth, + boxHeight + ); + p5.noFill(); + p5.stroke('red'); + p5.rect(bb.x, bb.y, bb.w, bb.h); + p5.pop(); + + await screenshot(); + } + } + ); + + visualTest( + 'all alignments with multi-line manual text', + async function (p5, screenshot) { + const alignments = [ + { alignX: p5.LEFT, alignY: p5.TOP }, + { alignX: p5.CENTER, alignY: p5.TOP }, + { alignX: p5.RIGHT, alignY: p5.TOP }, + { alignX: p5.LEFT, alignY: p5.CENTER }, + { alignX: p5.CENTER, alignY: p5.CENTER }, + { alignX: p5.RIGHT, alignY: p5.CENTER }, + { alignX: p5.LEFT, alignY: p5.BOTTOM }, + { alignX: p5.CENTER, alignY: p5.BOTTOM }, + { alignX: p5.RIGHT, alignY: p5.BOTTOM } + ]; + + await p5.createCanvas(150, 100, p5.WEBGPU); + p5.translate(-p5.width/2, -p5.height/2); + p5.textSize(20); + + const font = await p5.loadFont( + '/unit/assets/Inconsolata-Bold.ttf' + ); + p5.textFont(font); + + let xPos = 20; + let yPos = 20; + const boxWidth = 100; + const boxHeight = 60; + + for (const alignment of alignments) { + p5.background(255); + p5.push(); + p5.textAlign(alignment.alignX, alignment.alignY); + + p5.noFill(); + p5.stroke(200); + p5.strokeWeight(2); + p5.rect(xPos, yPos, boxWidth, boxHeight); + + p5.fill(0); + p5.noStroke(); + p5.text('Line 1\nLine 2\nLine 3', xPos, yPos, boxWidth, boxHeight); + const bb = p5.textBounds( + 'Line 1\nLine 2\nLine 3', + xPos, + yPos, + boxWidth, + boxHeight + ); + p5.noFill(); + p5.stroke('red'); + p5.rect(bb.x, bb.y, bb.w, bb.h); + p5.pop(); + + await screenshot(); + } + } + ); + }); + }); + }); }); diff --git a/test/unit/visual/screenshots/WebGPU/Canvas Resizing/Main canvas drawing after resize/000.png b/test/unit/visual/screenshots/WebGPU/Canvas Resizing/Main canvas drawing after resize/000.png index 96849ce04c21325da234ba7192bc8c1bc63bce67..eeefb557bee12ea18f03d64a9faf5350a49a65c4 100644 GIT binary patch delta 246 zcmVx8`|1S-vU3j`0}d%`>$E5c1(i><^i&}u1mpHL*#3Sbq~R;i8e+Zq~Fi?5PVL& z``-m;9W#wY9xj+MzLH~tqfVfw*IW~$bFWjx zSVbQ5O^m6cjuH2SI>?LTJ)IZ^5Rw_OxhYs+Fofi;PsAYo?%ipy7{J_;#M!e9W{tDF w1J4ZX&taL?s3+ZT0V1+#eTh7Vl93a%PWb- zjzHgiyB~qNA58!HNV`|Pe%M&600V0!+1)1rsJn4-4pB0a00000NkvXXu0mjfCL>W} diff --git a/test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png b/test/unit/visual/screenshots/WebGPU/Framebuffers/Auto-sized framebuffer after canvas resize/000.png index 01be2eb74e88adf3364c6690d614b349cfa91f03..096873ebad1954614a7c152ca22186eb84ebbb1d 100644 GIT binary patch delta 279 zcmV+y0qFjx0kQ&+B!A~gL_t(|0qm5~4TCTYL=P4#>_X=N=4f{zo26VUOZ{*XTe9FM zmQIM+@rzG^alw-B(1@F&gD*2)wfdc3h?F*s3f2mv*E%@b(PBTh)e{m9%*YtN+OUIy zGhAQ-2qj!ND>hKf!4YOK8xv6K&D{TiN_Dbs20xI{iuJU;RevLSGuSeXrN#}G4N*_? z#0e8x!Ta5+k-QnapLbU_MLf-5PF$OaIeu8yLz%&B$i@Uve5ZJz83&kyBV1tiC4l<< zw=xJNoH>u&U{CW}Z3oxf3C*ciFT)d&(oU>`wZiDN4vu!T*zW@X0RR6TaaA<{000I_ dL_t&o0GVu=G}1JMMKBYy%0Nklekpo9kl!bXhNjs3*3rK6CxiL;st&fA}_2eJ~XMwQjtK7?KfIk1(*&vw)3AyNN zqHvG1bhxY{9sHV05^}Lh*lwR&S0a5<&U>9i^5-&ccUENF+2Px$MoC0LRA@u(Spg1%FbIU7xtBR#Hh;!O zQ;k_{hm~q|XJWEMHaWQC(6NXJGgIymk#M=yxR4uwthDf~&Y>eIJ)t9s%2*2nYJpm@ z0#?dc<7R4Q-S8q8gCSld*KC?__O1!H6Q~s^$8&jA0WN+6DOR##ZAD?*`Eu_(2tiH6sa{b(<4)rQ#2A5wrOoM0#U%Z z_cwKuRB5 zRv9?^t(|>Ig=^f>sGiZ)S{iFTk1Y9r$u+)KVRDpq@sa5U*4@ZE&TT~j5U2kx(UFws zyt!}vJvjT{I6HjgtJRw|a&!f)30mVYew!V?pjM$ct=zZ9-A^Me8r;^L-dl#a1jh2# Q@&Et;07*qoM6N<$g0<+m&M7)3I5(1Hrt6wMj zy_5Fzc)QtutA8Z{Mv7cQaDy1>oqWt!G?5G#C^QS!rI3yq}xb@_2NE`UPS*qb#E3j z`<95*i>B)6MNHL=<;_C6;K;?gj4KGZ{POiO3t=+zPk)KvvnQ_KCBoCgE#&4+t|vAN zPxR`;*MkALgmmF~EncPNi%en=0oLn=TL=vxbGynH0}c^j`AjcQxEzTWLj*z(k3mon z#XjcxYZqL^fxBl3%%R#o_RzkF5H5hwGKfG7asMPO!n1=kD>=@{5$wT$EfK|pL^6#2)nTRQ|78eppL3AFzo*~VOK+a;m^qMWC{?Bt;lg4Zzrd$31)Pz(Xa(4UX zNcPAo1DYe12b|r$Ig&lH%7ErbPx$!%0LzRA@u(m=DduFc8Jx9N+*PfCF#< z4wwUQKyUyKh68W_4!{98z`@8%v#@;Mn(pu2SniT$BxP%V_x7&sA|OpMfQSsFXDXT^ zJre|`h$+%h2JF@9D6zF4Yl_rPT}L-oRwQM`?l>Y;-~Jg$NPm>~2*|h58<;!+UPGa- z2q%7@x%L_tgG?X5;_9G$p$hVx0Ou8OSvn9x6$JqZGQatO)IbPvih*Pe@OPOYP<#T` z$$xcJKRf{m2BM>LwLLd%8H3&hNCPB~a_Rcd5age5JWbiD> z1lb-GKv_BGs9|y@B zUR05`UoAC724i;mf)$HC@$IY1dWQz5KWKQBb)+rB=PQ;92$kTXc-%DGqU6<30pXy7 z*)-OL>D@LyGc=WvH%yZ|h@MJGkkb!o&`kuspi)WOh4oP5^CF9*Wr zJHPh=Bm*Qs5y($`d?>qag#IU@NN5sT{gpq^X-zaVu7QLyB;9X{NM}4Wo+%Q_kaWK( mBAxNjc&11wL(=_&QRD+413BITG@jo80000i)0-^+vBYy$cNklhr zk~Sl~Bi<3C3HVpbXs|Pv^^VLuI-`Tj;mFlXKw2Wo$mMV(YJUo8%6muBA5!6-ZREa1(EtMy2a#9kiA_ZO;PeF|bdZDlNst+U zOLRL{P~IKIVY~o(XuT${Q!{zQ!#AiNR$8BCd$| zQ|^!ufSUU*dy+HYzyzc(R9+!*q)dD4Z>Ilj+)q=GxPRg_=k*{}SKj*Huk>(jG)oV% z>&l-ShxBkQG)oVnsXoVNJ;)q~JyolTj+||Uq$Q$gCW1sAPE+247`ve^?;TN(%=mzJ z#CX8!^4<~k$cztoM~nxoF2692`~v_0|Nlrd#C8Ax00v1!K~w_(Iw>!X26UK?00000 MNkvXXt^-0~g0fukk^lez delta 265 zcmV+k0rvi)1fv3wBYyw^b5ch_0Itp)=>Px#-AP12RA@u(n9&V_Fc^e?R;mNwJT8GQ z;XF8iuB7m2gf`_kLz<6QdRP9tuL;nll-|+6)iKDyTBMPK6%sT;Bc=pE%M|QpTN>FM zok^H&3>r&m_QAoZDQ3L2U7Yx|6h5%JJ4DXRO3IDdby+8?dkw86FbDB6PA ze_)W`5Est~ja1MES}S3JRs}_iOF;{?REAdcpaoi>l~3R=XQY|ecoR(}+x@>sN;{hF zGe}e4>?TSqw*33G#44E5h!CDdN+Xt71ydRk!m~(e#1gAuN+Uve7U>7x`npZMO%MzK P0000L2KuCy3eVdUdC@{DEyM-%35iIL+l5GP4}W+*3khg8YgA5zWmCYW zoJc^kS)(2#k>6*cTDgHfmT&iKvM17MA&nN|0{ZU9k#)0O2OnU=$s_{u zho=K=-?vom%6}eMqt4Njgbexr04M_+AL6F^?HhnrEzyI13)~I9Og=B=BiGnzQzf0V?#(5_vghSfY6HQ3FZfP!( z5@I4FsYOdfLfUjT3o*G8v-S@F0RR7U;vEtI000I_L_t&o0D83@j)WQ2RsaA107*qo IM6N<$f~AYnssI20 delta 376 zcmV-;0f+wU1Cj%fBYyw^b5ch_0Itp)=>Px$Oi4sRRA@u(n9UJ_Fc5{8GHk%yV*zRb z&;=cESMPRU7f^s&0J+BoC<7;qGV;4ljIzY##*k$4?R#&@B#=^;;K37k2a-$aRF{=f zPS?X0s}*68fl{#|n1=?w|B1Ck0kACa8r>EEdeIn1%UYoVh=1PbKB)xAnq7}UaQ%Bn zjAw)u0bpyfwH~M6-XjWUgfn895P&>6tx<1sb#UQMRwFmsWe6TJBk%_Db5h3br zup+F;mny*4`n0ezx4&yvtM1LtWQuqXA}#{NRaAnfyR=t$(uzcf2*Q2X*?6|kIzVJW zh9I6)Z5Xm3r*Ytf_KvuW>oFujFg2dLKc0`EH@bSAHOQ;INP?Iym24B#THb(=&xnx% z%gJoI))>sJ{v)peG3uB{uXWv}+5sXNb~NQZ0~o}nYQLT{*)iTVD1*4J=%?#>qsSc- W?H*jB5ljmJ0000<14YB+u^SR>D3jm$K&~a?|;Lq{WTy(^h6yyLz^rK zh0GwPdtU%kSBlDAFlLY<$o$+469*}$J|Y{$S9it)V!BomSk)DVSHhS;SlnfUt}xds zm>9@JAVrGSb%kqIz^FmO*NTcKMi^SXgf(gP+-R%@86|Md3tcIB3QP@9kITCLc@D&I z4K>2Q2hvw&SAU9?82vIfK2;qNPsnh>Z%v~;1fY%L?*!w43S_*!iNu@$1Ph-BkpxR~ zRA;dBAO>-krMDo<{yu6|>_BaG%sIXVappa~?H@!ua=t>v!2ZcK9U%a1g72?D@{syYk+Mj1lQTOa}5S_c3?hHVz>oTDIsU+5jqQ4op&_P`tmAxv)op8q2B^A`UX009605(=DE00006 dNklPx$lu1NERA@u(n$fz#APj@q`#&`2bIOq^ zvrU_pZR$0S{U#{|TuLdmI6t*MAF(Wp^J%uP-)41z^bq|kWCOWCHV{pTN$PMwMs}*T zWU68Y8LIJO@65EAKr|4IDGMeXo5r9A#wt7{ErHl>4X{!jBLx3=BrUr?yl~e@UoSL5Pa3D>cb#DQ5(l`h?AL2IZ z0VApMT_9Qc7^=fmq$;iJVX9k&SE2bh-L73~v^*|=(V_ofq`NakC3_;(t zHG8eX4@7LDgnt1*(7B4ViCJ0725DPWdUeYJ8SS;HFv@1JKQH@uKl1i+!YMnYIDl%X zGIH+{uqy&LOcV%OfLI{XBDlFB^IpA$tUq8r)Yd{3Y2#OA&~j|B)ei(w2v)^lt8hhH z#Z%kMXsbMsdmk2m}IwvV}k(5C{YUfwF}_pa}1Mcd=(5%kJ#X*cr#i?3nYv|L@*AbLZa6 zNcvjas&9u{F4R$(1B zVH={b3;S>g$8ZYga0%CN3o*EdM|cM7V1z}GDbm}Q39>}VtMp7)5?>~$B4sbqBlki) ze@P~O5U}nQdXnzBnG?Coiu~bcfT(zoVmytMoHGh0VHJQ@sp4^U%j5J*&ay2M00EnL z&7E}3cwmpe5PuYrx*O?|0qu*xMn9FkH=kmQj~eYewmper%fgo2w`%ni5vvzNU^o9hu_Vp2 zyy9lecr5+md}L}tnjNP^TZ3*xSQ9sLJMgD?kvVC4Qto!Aq`ZR;k^N`(r%0+_lz5q(PS1e=IarjplmJO_7Zush@pfeyd!P%{HnPyrz*gAynLJoFC$0RR8Ku$8+2000I_ cL_t&o0F_1XGvUFtcmMzZ07*qoM6N<$f<%76Q~&?~ delta 248 zcmX@lKACBPayB$3xk6fx5|FU!MGX`~OR~(;A0MTq=tlHgTyeclgA$WPw8$ z*ODa;r?{3Za#+Q+q;Q$TE3PFA9cFPYS?X|$Ysq4VU0h3+JN)8GS>Vvdm9oU)7+1<7 zhhxkJnyoxnGh#1f1R^6%qxc9n#k!5OifXN+LanvSNUxaQ zddmdzE+YWmJxn8`ra6j78mMVRD!kkNHnLA-(ggNTYtmqwX@Bo|B6wGGq1ZHyoTpM9 zNV#~5c!^iZn?@wgq5FzJwh!;^Ccc{JTVZ68IYLIj4{;^1rcrXZ)7;E;*hnn~VrkTQ^*K*c#dwn!SC<8|i-8^DVzY^0XX(OY@0X2f2|2t-Djn)qvy z5k3~1Iow7}QEZp=`thX&m&-`2iK1Vv&l4?J_Z@YJT;MX2vGZ;agz$D?u%;1pn%>42 z&cy?PhzrktkwY*^(+E>De+y!{P&(h!h$2`@Z6nfTzI(O5Ph|4s!}R}iB4--&SK}xi z>8SqaL@7dOZOE0RR7W=||fD000I_L_t&o0CYt=tjD|)bpQYW07*qoM6N<$ Eg4+}DC;$Ke delta 402 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 b+E9}(O&6<%lI$_q00000NkvXXu0mjfqXOQI diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/000.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/000.png new file mode 100644 index 0000000000000000000000000000000000000000..f1698b508f6c9f6c67cc76e489dd0eb93076429b GIT binary patch literal 2325 zcma)8YdjNd8=stJPHl@q4pEH^IXp;tm}5*HiCNMxtY1(Tk->hxI zV$;b^@Nw8VsPzv3ppqQjNMR6m--5I-V&NohJ05QyGfP%SIiC zL6Q112hGUU<52BZwh42Af{sTNPd%Wb7tPz^ZP_f4)b3_PKg)4-O&Z=rhdiax640)) ziE1OcZdg&UB_!rKCszdHHb1&NtUejLX^(w~^iDbm=ZKJr44f8Vk)}LK( zuDn!WjJSO&{HHK%_>tC?Ze4MiYkEv=G)0+TDZ_?W6&(``vF9Gn8I z^>M@SUJj>xWwM#eB0(1yxfS<@_f*Hod3jLXgFl*R7Dd0crN&(s&*SFb9}e){yD&(5qS zVhkvm8?MDmKW)@D0wEJ?8}si(6imZr^v>hwIm*P%g>2KrkP#+g$Rgqk_VFI0g2Fy{*nG#yTGL+GRxx&G$3 zoki^a;pfDy;Ewt2>YxGQokV|q)Yz`j8(!z%1Nj%k2}dEA=BCG)V0)I+^_I5Jz4!$;eE3gY zB_#WSa?JOcE|P{?5&A=ocSRR=b!zEDjRf}d+01~0(9d^sBUUD;c9|+VPkfsY{`cPs zoPs7A@mSRKx8A#HhqD~3csy*!Lr*{bKqmg}>8-MRzz4DzUqhn*dgls zl*xQQI6F57QXaeJsF7Ig9X8hiooTo=z0mUzRu}t&Sjw6%9!>eaF~4U>cfbX)21P7n zxS~R*$&zRDpT?xTW35Y{Pztk(Bh4m(mZCM<{|{cGDD1n@!2*q^CevSQS;O}?2fxiz zvwXTI!!0cpYneiFyczE0+(a(+zQ#8N8qZecF(;Zdpq204SN>q7r3w^Ri^xRdpfue*D}IL?E5&? z8r5o?i&EWF@NSFNG7c*=k0SRkZ=oAC2ZHjC$rPe@R;OWCf^j%y-`1!dT|~c?S3YxA zaN=tD6U6TJ@-?%_^^?lKMx#TH;#?L!O4k1X-tcFHnSWt%}cW$5{J>%*2Zh`Slb@-Jo+J!o$vp$v`da&AY zMC`Y(ACF|%^~6SUI2=1LSe37S*t8Z^?P;6U*jlB>;sl07v0I&6)8zFqAvYv;5Vp3q zyz?9bj~a|zq*qsc$f){ytSmZo`NKV#WnmcVB%~zu1nEms*9{UfPZx~B^OBX1JuWc0 zr4D|M=i6oTM4JV2JzAg{I?;F{y!rO3Jfrw6-Ql5P$o^M9?a6U5XSg1Kufnd%+*U4?yzMjnVQutmDEJs)d*L8Xl>F^a`QW|=7`t|J$dU<)* znekdb@zG9rSo5e!-=wQos2-+8GYv8G^T%HL1=R^KP_B03t=i}JC5i6$E39CPf0d@V z!~W`Ti`iQcq*j_){TkobGWw&nm&o8f^xJ#VrGm#G`QRfKhZs<5f``fpJ&^hFub65% zZB(i}GiXH@0{WF@EBHyYd*d`7R^5-N24*~YcDn96ZgG%eoV&9^dT!OyR5^uNS$!?3 zJhop~jCe^PSb43L5E7VcK7zu?j)F%H~wwdmz__@YDh0oF3AN`Ww1_`MdsqoqhlnGU4Y)j&encvhzjd=kzqU>x z_+`97?GUdz!{jvC*t*)-e^at0m@j^tKcHx}7J! r@IVLvI5;Q_Eqy0wZp8lSYJ>qJ1#4sP0_-dVD*y*uXPakMJ_-K@RU~fX literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/001.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/001.png new file mode 100644 index 0000000000000000000000000000000000000000..aec0fbf45f06db8357c1839c697d6f713e4bb02b GIT binary patch literal 2316 zcmZvedpr|fAICQ&x4F$E_uOg8C3liqm`hVXY+{RrtXxt=L`X4=+~%%Gu5+0vnrm&Y zMTI2|%`MFR78U*0f6wcAo+mSD3Y3bxhx_PXgRb|DSd8>mB8;;czDQ6XyP5T#ZB$2w>?X|%P2Hg4vT z=d)2qA7K2t|L^RR4XLls2FokQo#T$YjL*aaKFz1+*(h>E3s*gT8imqHv5>qw)m(8V zezy4t!KtTD;S=1N(7pz?1?4v;Di~7}y78vg%$eSdp%a1Nrdyg*E0|9klIc=85;|2&<8i zk>N7LlZ?x_p;FNDSYu+TeV(N%qR_4)8TxFb3i%IRv!j2WNFl*`f{Vq4T^`aoe|bP_Y{7qK47vbnAz@3Fp%ErNqVt(VVGbh?2Y!OdRE4$h22Tb@nRn*oRSQ^(; zmm6GOcuy@NFJ82`d-v|v{71$90bAg7IA-bD^*ou8WXkrhp)v&5!eo1U9{zcI#4mlb zd%F&97W8UY)h(-JX_0VW%M@9H%n6rzm~cgDY1iw@_jv}0{Aq$NN1sTQS3i*}P|)C_ z$+ZD#*29i_)6_gA(oH8y;52tagIB-!&x3u+t9HQrrlOZVk(nOptkiti^~> zhdreGBYhX|zg!(9D-xuQE@gW(S%@XQ&pLU<=Uuio)^XBsCWW6PRlymDQ)k*&7iBAs zhJIUNH0xTdamQt^kMJhH3Ybhjhll|>JC;{gioO*LRe77($v&6Q&#eVR+EmOL&v?BX zbm%Q`xNpU1(CP%b!K*f|-sOmwb(SWY>cG_|`G)iNgTq2p32|=cb!VQuD-eOEHWs>qn!ar5xG$aLa&>l!(R{nOdTI)5oAu<$$)NH8bpl6QWM?LSK-87? z$miWDazkT{Hwd~1r@iIo1`1fUMUUyp0_3v^av}YPfGR|Ot=y`JxIyX$=$OH`SOB=6 z7ee2&jKa@Ko%QeFWiJ40kTwZy-5{0`1?I4Wsyd>+OR@C;p4n5vdME8jBlN;|d~*#0v6W6GUu4X}GKiXJo6YZoNBGT4jEM7U zI#dlPR0&#O1Cg=|i1w?+wAS$x+`SLYjjfJTvIJ#}m57xdk`6w`*`>*_M<~oR%|?bH z7_vIC+<4u62Q|1mMdK$&O>Zp>d1}^nQ)wOf-sK+bm%ln;rt`d}W`X0g1gX439o%-@kq1N$=69UUEEC^D$vda*Q_TS(UK zPN(ViBe-jwI}jvy+KnXguCGslk*hV-zy=@7tQ?Pg`v??^>&woF3)}y>{cE)wzGSIR zi~~tou7~C?z532zFvMxz8YG1q*@?e1;`IYBt#B4kyk*4Ntt#Q1FjGAKah;_-|?G#j+|GCW?<#lO~ zY#p6G9NwKQb6otQ8|9Au{GAns`c9-iSjEm7waYB4t{ZVmMqvP5y2-edy=Aq$K`&I& zI`i$}avr4GV}TfTnnUEl?;I`k(b+2^0uQ2w%M+WoMyr%AgIVL0keX&vb#>!_-`AIf zC2Zl%@KJ+7%5oru{XMoAf;fIGe??|EjrrJ_v(j%8gMn5 zqI@*{)ML&JKwC*eMvsK-O(H4w_K2QdtnqzKO#V}=@k&Yvtr`0}=wr&xMw}qTcsLqV zQqzW+PpE^j;$*hkQzTM{RkqC2CpR`=;cM%0>)#695!d(6KSEvatcg6uR3&OZ-5q>! zZ8%%vSu!2rPx)f>Wi(hVFZkk1+hx}0tNaqEQMB^9D$({^AN^^@x7ONw4t_(NB@ zYyV+Eg6D+A!Tmrn=RGSnnR4g+JArHhIn_uTDQq&$A2DJ$*_n{P|2axSeMrJ^#x++% z7z<~aned(9z$uF#OlsQ#y<{IyPkY8h;FO(JINYBwVNrOBKG z_54V{L-xcT=>k-o4L3#iFxupq5(kr$cF^TZnyT(9MCts-%{SCfnzOe!ys^EVF8^f} zpCC8%H_SxkpwfV!X)<8jEIZrebiDG>n`nkMIjFZ`#_-P}0N~G`N}&%L?3#xAZ!yCO YNN8Uhi$9II!Bzn9^A}(>XWe7|1ss56RsaA1 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/002.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/002.png new file mode 100644 index 0000000000000000000000000000000000000000..4f4fc3243538b4f764d4391dc337e4dbcfcb8901 GIT binary patch literal 2318 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/003.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/003.png new file mode 100644 index 0000000000000000000000000000000000000000..45eaf8a5f135e665c13178a29df3dbcb7a21b30b GIT binary patch literal 2321 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/005.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/005.png new file mode 100644 index 0000000000000000000000000000000000000000..8fcf4b9b999bb142e4a4fc31d0b049de28013658 GIT binary patch literal 2305 zcmV+c3I6tpP)L<{1ES35u^U{Ozj)%7l}p@5k=6f z;DIQDXpF~-nka~f3X0+_9-z+lSJ<;X&efgiu0HAw^SWxPU%glLy^lU>swyMQA4b3d z1Tu&}jDQ0OFtHo}l}!>cv6uypAi%_O1Xec6#9|IOf&dfC5m-HFRC1G;Sdsx(#T^q% zl}JnOClgCD;HtP|VyO~o$^B$vNd{aMcT6l*A}zU}Of1QO|L-o?IVM)Hq~7Qd6U!TM2wW?eSb+JxrMZ1e$^#bI80C&6;Hj{*NC&(#)AN>FCj;G;Z8DQ*~r3RaaLN`czd_CEIme zf65YT>(;H*sZ*!8p!{Cgym@oV&(EheZQ2Ne{eIrf-|bLP;-jT`Ci-MiGM zPam_@r06lRQovkVT1uq`*}Z#rOpQZ_4pCWI8I2q{l17ahMF$QXpo0ewireSOlP9C} z2MroTQ?!==Ty0RVxVV^Fw{9J!W97%<#f!zQ&`KQ{x=gGTD1#V~-o1OL&?LHa>5_QP z!4+iKuwg=f)v8q@#Q`r1H*ek~*n0Ek4ZVN=o(2ydOz+;kqw(X%LnSc6#7Ys$2@@s| zB!!wdjjPXs1q;L#WX+m2qU_F{J0itlzcyrn^!V}P;wsgmMGLB|tV9u=I(15Z@fc6P*C%a$!gK8$fJEr%#HaCr9Y8InK<6H8JUKYjWX z@$~lXTQW+oUcCs?q)C&Aj?0;}-7~S$hR~dWu3fuE6l-g1>EXkNG18$!hlq~fGfXT= z5yTZKD=SNQGd|FxV8@OfNCG8F6RW(uoI;^cjOYW7A3l7b9zA*psjsgWfkz#bZP>6u z{I+t~vuBS`-@bjj7@AnLXb}w*{4lM~@y&j~+du$B!S=_3PKg z-$YlgT*0Zd1kY^nhf)8u4B1p!Sz_6QUxUqb@7|p-3Xeaw8*IeG6`5Eb4vw*De#fJD)gl zfBynpoO1uTD{rmT)#*G_`)zDZch+7lpulMWMFQ!qLdxdGrm;{KaGp$;+ zqJ8`J#pszS!1Lbu^XF;cz=09@K6kir;c0Y_wV1uYUq3S z?uiXLpoe^c_S}O!1dAw!4jn2c`pug+PfSV1qE##b!-F?;tO{0M8|zf@ZZk$Wyqm|= zVF*?)r)p)pcJ0J8W{?ES=b?`&$VeerPly!D?|1IpDQ;i--VNA-2zGB%1B@wPdx;dSQSU-rer%#{Kg9i_grL3$h@e+Xf@3E`D2;O`A5+ zvSrIewY_`yiqy>m6U#>QF@&L;%gBg^7IWP7b0bEKps{1e3c;;WFUP7NJ#Qt&)p5y^ zC87%U$ihxG;)jce=-#{7kf6J6+O(Dpbk=D?+E1G z7ABUY2w>p^TwyPVZr!?3VPRp6;Jj7Q-w0Q%SV3MP75d;Lv0&e`64qMwcX>IHb_Fpb zb@laxIwjiM-0%AT-o7o)mEjrZm%Z$q98rck(9hHcW08map}iTR%npG)$Y0pq=9h0_ zm)!j*ODuO`559Nxh!qY~Wkp3&F>Z^UNH?C2u?%^=F7ihCr@ET{EH0*e?E$_>yCv4* z|3|G|MjbPG-F~99=IT|~7_1(#G^jBknM^d5$TyK*L_y2wI$AdBo2VSS_u)l zZ^p&=#w+ literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/006.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/006.png new file mode 100644 index 0000000000000000000000000000000000000000..c59c052cc576d4c09d1d30d4caecd3283f6b54e8 GIT binary patch literal 2324 zcmV+v3G4QWP)M*Sc(Dve>i{Vm{|UfuWQ~h zv0Q`R-vcwT{2gD{yklay2ED%rW@7m}zOH!}Q(~2tmJ*BiFmQuiF(sB84#aRw?ka`| zNFi26Mh3CCk3kH1;J5jOSl)<(@2p^A`7Wg1aEOWJjX3zu z3QrTucW7(m5EDzIV5@YMiKP;4jmBbPX%uXgt}?MyqOH+bOe~Fptttkrd74I zL=L2?vXcHia>S92lm27g)^wCJp%9QptTSiM5G2%~p~`yn=t0w_O`~z+#)YbOz3lbt z*N*&g-C&mukVdS`%uKp-=Z`SifJuV24XRaF)3-o4wYs{vQ<=#bfN-n@Bce$Spgl%Jn3&aYg#LRYU|75P)APSM)6Yek+9Y7lUpScQd! zR2Y%?;6ru8SOLx&DR)wnOKsHmXNpFfK-T%kZR$occGQsR2WJ+b?Ric z;f!1ad_%1M{rgjk7A*u?7cN|&wr$&*f+t6ui%LEyK|m_83JMBn$dDloq`bUbIQQw( zCz>;7ju6}sA0YNZP!3QM>NF!Et zbv0Fo5n~p+Wy_Xf+pJiz!UO;>v(Z;xUY=Q&mX>DfnuWm(UJ1Q<^M>~C-!E?9PnH8qvOi4|WbZLE688@k9_1vBrMbq5hAPo8Y8 ziaH-|tf)KmxuP^;`5ZSQb}%9WOst6TaAmXzFtMUVj`boUz{H9O4_8Ku023=(Trw9l>}*)QckiC~7Q>e>UufmZmBPxyhYyPshY=%2Afbf|7YZWb=L_5NQ8;VXEF_JR z8#itc_AEB46HSy|Cl)?m1hIoRzS&?r5E$QMz>_3&aaFr@>lU)KVZ#Qo92rZJmo8mO z_`7}kHsOkfd^Ga#;X@iWY?x5BZk!bbd}x4x>%?kcQG|lNefyfV&6_tj^T<4Y{1}Ol zq@*Nj(xi!y&Ye4pG904a3H|crOG-~qM;GFLu!z5rC9sHeNQhJ!Pj$FD8TQVfKc7Mg zrZtQG1K;+97-e{pvz{B}e@d}4$JpY9=?0_y_wV1uS7(fSF{9j03Q`(R zlbFCQD=Q;I?C9&`s;d literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/007.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/007.png new file mode 100644 index 0000000000000000000000000000000000000000..fe0039fd7d0eef8f46b71834d7f5d4ee1fc484de GIT binary patch literal 2301 zcmVHgz z-Umkf5w9p11x+wP4C)Ur6mR{Z+4%~)w(U()OP5GX_LGSv8E{>?V`AwNX~}*vu_OboOLt5xT_P>nPbQXR!2jz`?Hm(J?f53l zJ0@1bpjW$NCYIXqO_+B~tb{?YcE?OCwd0#G@8U|V^73+G@g4?=U{_p;l?Vso*e5p? zM+96UR#sLPu_PXYIP%1CQ?3xp@hI3pIRuzk%7xS$4l%L35eMZ)!NgK7q~36diRFzr zC^rgE6H7U?HFAiFrBSeDy2`|oiMB?4F|jlXwoF%%?C0VY=C@MG%;2>63ofdG*XLhAf^pI9K2I(SF-L?K_jPb?!8b#+7zq^7!> z{yBWuk&Tn~t`t5>fa`6G33ap=r9 z#LCXjrrWn~+jcoQIa%m->C#1{UOD{y`IGYU@+dVmmEcoJNr_j6W*tYP^S&XLQ%}Eq z`9d`{HMDEjE~loh>2KV)k(xGbN_+S2b*&GtG^`x49zJ|XhJ642JuJ@m?%gAZQF%Cc zFLw0kQNq>4D6^(--@c_IM~={e0|)5wVM_ zuOC@u&z?OEVlr#iEE+Ii04-a#EI5l1plQ>lQEqOoxSBkD`ZPp;)v8t0zI}U|GGz+E zu4m7lG;!j@5FNWG?b@}Y($Z3zK7G1f6W{77N36w*7Zb!zM7C|)Mq|c|p@Rnxn(A}s z&NcJ9ckiac!a{L=`SNADa^;H1pFDYz)~s10@_Y!7fO5ntE-t3xp!Dk1E4;^+(jh~J z(1i;ZXvT~gbn)UvQFG(Q4Uyuoa^*@SVt4nbQKM+&$dOc5Rwl}}Z{IFbK7>a=Ibwx( zGkmG_C#ge+4noPeFRQAmqEDYbi85TFKr+a=bLT|a*RNmc<;#~M&xeKxXicp5@835> z)CKAx%H+R~a>(Sp^wb!)R4Ple&6Q>RX*4rf9UU}A;h zVV?Ex-=A8vXd%wepFdA++qN|YPmVYjm3#<`fUk&ER8&NR2M=x_m6eskxsM+|((Ku@ zh2S0ug?H}U5$9XCZpEo1S-WeaMx;XKYl!6UMvs@2!e?UK7>WUH^i!~t)(?*B zR7J2l=6_6cU<|k##@vX94Shqb^z?L!BvyQ#=veKLH*Ar&3TECh>kcALnl#B;6>~mP zUoki6F+<-F%VW3^@qnTtz{H9Q4_C&D023=#5hq|OQa?HsVuRwv$Mr3pi7r7Nru#3 zcOO1{FjuJBv+$ReZ-~{WPaoU6W3YS%FPfh{dnVSudnqKRPMxB(v^2s(LcBD`jv{PV z^U@gOF_xrwG$Nx7thvIrc{_a}U^{l~AiOX>cI=oP6Dc|v%Y+FN==t;Kgl%LX7f1zi zNg05|PJBZwrB=G7m_uwlb!^ytyFWy==AVm|ydb?eqmY+$Xgi}ReE9O~M&D|PJHQS3u}^5hAM z9SgRWVO8qn$&*dnIdkTid6hEC5$o*Pvt*TU!NNTlHlUt3ae_8&+C=Z(y(6q+#|Bme z-mv-cNOji0b)&6*Lees~z@-n~0jS63Tqj?xVqHiYP7!Cy#$A}2|T_+h$%kAXn! zY*@W}_pbOB!{^VRX~l{aqK!j`4v7?p;lqa`p#=*T2qNL<3)}KhICJJqB#n~m*RK=y zEHEFuuotCrQ}is&@0{O=M~P`t@QtGL|GSS+a!ick9+I z!W9kqSmeQj2Q+l(P+@A_I4cSiI4DOfCo$>Ux38(*ym@mokIbV-kB|sSNlBq5O_~Vl z+_|$T!y(q4&@W!Rpv=rnG$HN>OBDP@R+8|J4-F8&)yXKFH*X$=6HIFuI(AdEYmgID z514PWYwIUHCRT)K89$MzUb=Lt*!G1dOslkM(+1U!#JKmv&!95*S78wF9kH%myGHfJ z>KDH42{Fp>BxgN0%KtCL&KzTl7p5Bw{cqpCiLcHW_hLr5ofM=ro+dGYTTxL#hS;(9 zla6nQRTo6Hu&9Xs4nC>Xm*AWL8IqEkN|4H`DuPW;Zmu{t>XF_L-Z-PK08fH}fUwQT z%oO@4hYqAH_|%5;iu?D44$A&WPNveUS1Bz$-7G`79m&hjH~aYWufNDDq}ZSLiG@#a zD+YQ*cNFr~`@}*N0JshUUlkEoh*e%*F1~`tX<~6mBu8B#Rw5nZzP*7!T#1#Hl|?LG zz#w!CiJ)3siIoTkxKD2)z{K)qTzo$&CYJ9*=uHQhSl*0_??=VN@_h)s=>QYUn{n~| zsF+xCLa1FYOf2m}F2`XemK>b5>xGG>UC8A)%*2v|vv$2Ov9t@h9EX`$a&XqJ7j229 zU8E&D%*2umxGvo>v2=;FWIvf$k^$GHJ0_Mck(TV|zW@LL|Nlkx!H56=00v1!K~w_( XD>DS?TE>S!00000NkvXXu0mjf!NXCs literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/008.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/008.png new file mode 100644 index 0000000000000000000000000000000000000000..602f16efcf55d2b7b5e77528a2ee8a7480d921f6 GIT binary patch literal 2303 zcmZuzdpy%^8`p|a#H=Bwgo%cl!;mzw(HuiG$27+hG8r|egf`6C9CCQdS&L{yPdYG3 zBBv&YmN^ThrI6G6d;WZ%_xy-uJ~qNqtn8gno>|5BR&O1lJM4jfhj)vgH3%_sZyzAyTTiuzy> z+Dq;wbLOJ7!s($Z)v3Eud%$An6nDJ#$r{H}XZWg4D?^~k&J%(lD|K_vnX~!$_j}C6 zJP8J%hhT8dX}AjA`;{ZIlVuMd$lUfO-~`-Yp;qdSiW(!f{{wBM8N}q~;dfH&0yxNe zw>z9KiTVF;HDbZw@rZ;U@&ZARyPXIv+*lgwrm76&dOY6Q*_kzYMgYZQ@y@4;b@SAl z?|cWm+0ZQGT&GZHX!`3x5?1P85{6_BBI}<+z+ZaW23b70(ftN2mc8mHEq5P67smCd ztCe~}oW`J|3f+ncYv}kdh)pRMJGpcn;|9oY?QlP27h&RZN;vrT*FZ3yY;5UtI^N05ta;xN*f4E{oChfVWnyg z6@7U~ulJ;knl;{xrR9#kk%132r>MyKY0zgaS}>8=(6>BPo%Oagb8G#_9UhOTs;iqb z;ef+YExA( zJy&?kV^7mZ!@qOj6n2Daga&E)Eu?J(^H6&>nDhL8`(4q&o$bwq{!;TdDwZ=hf6n%= zaNC9as*8=tE?*a!T2RFPwx~a=M@yGDt($f7+J3!dfc@Y=x#b;OBuFRh>xFkcJuttl zW46UZgO^%}@W#L~x{7VF)%g*%ervvl-r3N{#yCscRSm%^#ksFF`wl%T{r+vxA68>&&01W-tzMw^z7( z0Ygi{+KlrzH-86)Joo9Akafw_4PXmKxs6Y8XMwQP(C88YVJQU7QBL(*gmKTa3=}RHWRn5nyH+ld02YV)9(4Gga~0#ZW&~3`1pG zt;Vsf0CDY}-jpt=DQc6{c3`U1Jgs0s5ep7IjE+7HC@JMJO?VnqZLFyVrFdeT3D{T4 z-h*uk2ZSu$Tmbque5SpVGb9N<`6Qd1K*5Ki`#Mg%r*v)E6e_cWq0}OpoFP3WF5#w# zk@Tq>sx)ccBvdfR3jh9YNrk5SGl~d9BrK#bE6cRVFuVeI%H$|Y+7Yx_qt9!$7Q>=Q z=Sn6g{P)txt&tuOpu#_Xy@f|h~C%t&O;I>lLZs%lPB>0+U*tX`O#mFl*0&H20$8nQ{-fPFxiB8 zYP{=kTH_yo1Jdk7fKFfgl)!X4p!%e`UjJc2l{P}5oTNtuH^X4CGU^DjaKM3%4?BxF zkeoFCa-zt^ROk#;!P?&5LCD_O5j&ribq|r+NSa=YlkD|JyejfkQ268P?Ylo}$P{cZ z@jReCq@$H=0uI|%KIX8HgUF^A*ACz3q)gAuEuwaohtpC_G2qe9o4%3OIuAiyI9TNn7C-S(0n}@iXz98^HJ8)2zJ3$iov& zstRE$bmGz6sgqF4gT3#ZFB8-xi3SI?>!&9e|AOr3_;*;BhW3ByG*h_w_-r1&2IJCD p@)D?Cc6SAQL)L+bz(4iNUcRpt{0pR2>`h>Y4~MnE)S7t4{Rf?cPt*Va literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/metadata.json b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/metadata.json new file mode 100644 index 0000000000..5147b8612d --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-line manual text/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 9 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/000.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/000.png new file mode 100644 index 0000000000000000000000000000000000000000..6423e4d84d6f551aa739fc72cc75ff6b928d6627 GIT binary patch literal 3583 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/003.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/003.png new file mode 100644 index 0000000000000000000000000000000000000000..9d0b130b1982bbc561f118eb2ee9fda4b30c3827 GIT binary patch literal 3632 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/005.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/005.png new file mode 100644 index 0000000000000000000000000000000000000000..3aaa5d29093e809862fb7595f78d6e14f83d3117 GIT binary patch literal 3625 zcmZ`+cQhMr8;zzlfA&_Zs1>7D5v#4eSEUJByH;%~W}~gz)JTk4MQ909rP^AFB1RjV zpH;*jErJp?KHoXtzuzD4^PYRpdCqyxdCzoS9=m7u#qp^{m&E<`{JnppA z|E&&vyO;b=W4*ig;Z)m2G_N`Ixe4>Ga#jP0WghuT&snW>IPjROK;yg2P1?iK-3T-q zg;Gbdel)UD6yjH3D-2=MC+0twCdK@X3Xl076h-*`%WUwcztbMLbt87}?4+Zpa^6au zaHh_9wKPtVbY4h(We5R;08GL}^!c&;^jC&2k+4tLJpn8~_5XnR-M6TkWrPQ8G2f;T z2&p`1TzLw1hC$$!9BT-ql$PI@A|@z`vskKI-Pyy$(!sw9i1LURl9FrFWrA+Zb-ZmN zQh}7mtHSaB2R?8%FHkn-M;9}7-pQ@U(U((cqc-#?@8a$t?9e8hK^*N&(Ot%!Wml*m zolSwfBNjj=sP`oL{je|%W^6s7u_hh}nGW;zQSPMzshr&S<&!yrGtuz#w0gmhRebl+ z@Af#=K;WRZSIz>+fTgLO1&VvVZ@e*`KQa z_tZzLC_a@ZP+{}h0u=`Io-IxH1n53A>8$>TjN@mk{#I&g#X>oo(4X#YxU(zzt1L5(@w8=h{A=->967W12KRKVmMf$qO!5%3Dm z@&CF}DY7;5$;)A&rmp}_u@opuNRwnGRnIP~2td?_t>HQ%#e>u6SViIAa68oxLgwOrY(sYfXh z)pUPdQ*8MT81hi!7O%rtu2 z#s&B`f~?>G)*9UA&$gD^aYXUmx$yZ#WTHVD-%Z;pi*aI}dPwE=OtYmyD$k9b_P_q^ zt$Zpzy-_=D5(!KKYzW$Qzz~88NFHqrn1AFmUX4bykdi^m3-}i!H4o&!te2&bM~`{o zf2KSS3E0O!3a+?%Gw5WQ%C|KFIvNf7|HT}4T%6Z*x1DU(fRoJ%)vPBB!X5i%-tG_S zGH-l+JXownKHVQ2c;FO+&N?~Tkry>g&v_Hj4nxVn%HG5WE%j`CTq};Uh~YO#9*}D> zgL>JPFkZjC{;nzV(1%w$TFp2b-II2`rgdYqY9N^tq}&>|1%Y9m;{e;B`c|6=^xOw( z3=;I9fA&iV+UH=X?#WaG#TKt~ahjdO^!IQpx8!0_=VEMw)nIF)cCR90fAvafY2kVh zfli`Ocx$p=9jaVxxm<(+-hz|CO`Ze8TXW#Pl*O;fY_-iCG zK9|d#5&Bpw?CfxoLE~PXTsgku{Dh%iFO;&9%bh=ncy9eqDGxYs0kJyuC8X-)vNNk6 z2^zSW>saO-FjN|s$4Gr=Oix=wzIFGxi95IP!rz?^0{f0pb?RazBY zLo6C&OX(UG`1;hVg``a+BJWO?{WUJz(q_)1x$u(0;C`u2aDv65w(0HYbIKY+3du-J zBsH;6XuRIZsXO?mJ|fI6Til$s|8i*AqxokY7AY7YawABavdkX$Ps*(>WgmD7xBP_i zE@Q?mcT!KsYJu)Dpt?_-Vm-F3y~U3$B|k!z`ZDdZLM@lpo2&suB5{!7no zE(R@`=lIA(O~MsX066*>TUXRhS-rmaJ$YXaDBktWtvPuR{AA~6Tck3cH6CX6ak#V< zI`+ZMuuJkoY$}raQQCTbfjC+1^j%{6Ue9$noo&#e2{dnz7ekdYd;>DKs9RO^)E-07 zC-(w5^Ktu#CiWtlQVY#Jy}|6+g|Nwo5}Iz+ufF-Si#$neNG{VG!D1ZP2Y&?-wm%tmRWGld{S+s1U7UF2glMyk0|D@HJqh&LJsBXcf zhYKiKhvf{Na~7t+X1|%rHBYV@=UPb`8Q*H6Y$Ep@|Lca##csHq>X{=A=3r|y$14*E zF$vW@vFoG)ot-G(v|mOb+2&ryA_H;K-X&k7yM1s}HPFM)2{*?t9m`l{jm%j?MT2o2 zUlK&!zx0QQf~9Nf6WS#)e_(I z6h-MKMvL9~mQ9pYt-jm1$^6zRzB=>G95v=>= z&tL^UfZRGG34(Ncs*@;7};``0nS;k3Qqlf}pU?G2^>F zb-X~xCgYyyke)RS#Y*|z1+6Zh@z4z+_XgHiW|}hdd{}4)K+b>`<7BqxHqO75n;iT( zl(3?n0|e~8^(>B{woEfq^`DijwQm|x^W}06AsF)psqrMaT6$##i*hW$>rLIjJ<}hssTj#?BS8+l^$uAe}cS)bZAMlj{z@V^3r8q5H)sKI3F3YCt!o_KQ6QgY9Mj2 zLKS^+q)JJYl2a@NeS5XKrf!7w743tBJw}(>&YWIG(@}LtRlG^QvRTyeDp0_&Sd?fF z)L|v9R!^Sf`S$G!i>}QCbc_oFdcMBu9gy!xm6(lDT_IZyGqsF#9NR7%iZ8ncsm<9k_>^eW8qJEige-Ya{?W2z^@s?wXNCt_{!TL)7WqAS9a7?@j> z$^LVA%Lx8jiv1Rw<*U;iq?uN01cUGJ-?6n@^H#gbspCmM*YxA_3-VBT!MFB1N3NCI zDm8E*>m}PMo95Ps3UA_?=VKjQSs{jxrt-Glz^#wS8FrO@LK!<%7$lC=|Su`@6fN#T;s zn(=QjKR^E)P-en!m-mDE`g7I!Xj=*o-UqjY{UnmCna)3CS{5m+!(qyGdP~a4nbRwm zq7pp$Mt}{aAcj+{v@SQex+G4-8GTnLayv!eFiYCLJrz+PKfSB@2z)qZd)R>&)zUE4 z_U)2%1AL!O`*C&feSjF-aQ%u|zM_7b=z=c4;ljt>s*h9}0FQ)TNrIB@_ByQ_U|{{# zqkn1P8S-#J=I$&`%XZ;WD3I%@1vW&^zwv4PNiBtm^HX$XTwYPg!S1DR@}l^ko$mQC z)0Plj&n``4kbRF%%s?tJL_twOizH!^dTaY&bD|c+^QqzN;&9SwzdP{suDY3Dg-dh4 z^)-YrYl-#Aujcv2y52aNpy<1EKSJr5JH-PJ)`#!CDS?ydZpaf9Zc^kP$iQ+~#bH25 zDCnluz`F6#MdZQjr@v-Qe}ThtXy&c5E6uMd21S!t7U_q>ebq%2!={yQIKB_|h~rc7 zm)VstFP5nQ(KRItavppqqU0X7n2h_3)AzkCoEUzeprtW-%k)D=$5jE&|v zowx40k|WMbHpkJrg!GZxsBnsW<;id+ORVv`*D`cRgM!#jzbepN1NG%5Z4t^(hnUtg zOkW*#igp(*K#Y*` z$?7iEaW)JxwQV6bxNY`x+vq2`2~D| zIFlc-Qin~!IO)TOc?FFBA2*D~^FBxhLVCi!6j5t9PYs9kAV~;h&_n;K%N+CcpfCun z=Ir5^JL8Q7q6D-&9i?~~FsWPK1B_2`4VYassmst~8#x`UA%K+uY1$@-;QDilKg|*^ pfB?Y7h1Pwb$z|Z9{%_I)0$^e=uF`bW?Mnt=tZ$)LchC9xe*oyO6`BA5 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/006.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/006.png new file mode 100644 index 0000000000000000000000000000000000000000..0a56e9283c6ebffb3f508161ff03c4c6e57d358c GIT binary patch literal 3559 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/007.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/007.png new file mode 100644 index 0000000000000000000000000000000000000000..a994b635f2462395fac1289f4923b3ec5dab40bb GIT binary patch literal 3636 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/metadata.json b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/metadata.json new file mode 100644 index 0000000000..5147b8612d --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap char/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 9 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/000.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/000.png new file mode 100644 index 0000000000000000000000000000000000000000..50e032251cb03bd00aa1ae617c7f6535643bb001 GIT binary patch literal 3592 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/001.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/001.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b77df3b7bac4f855906b66518c8ce8b14a9db2 GIT binary patch literal 3589 zcmV+g4*KzlP)`XP|HxIV$Nj(BRS`sb0~6A z964u-9KQX%qW7EI8}4?w-JaQ-zPi`pbx*(U*Z=J_{U&dp`*Rw|f(G)${W%R}K?4q0 zSx~AgQp5qvp&%<7aKOrnT3w+ISPlbO(SQS1R@558LM>Ox0n4(&qaw}$D=Iy0xt$JJ zmK7cqaSm8f>1oUDbilH#@TiD$z=}#wTW+TVmSu(iUtIYPIbfCV{tc{k4p@PyzI+RI zz$)MU8(8ZcumV$k`4;SeRlfT-u-0W1SYyYI6-VLIK@jN5D6oQPKn7*HqB5v~tN^Q^ zpgIuu7=ep@$wScinYY2|2=>l$Vz$+qP{} zDl9CN!Z`WWHr0H7&*mC06u|1xp@ZtJd-v|@=bH25k3Y(a6)R-s%$X%tJ=J_A7Lp^0 zp#aujfBltEmls?KrVHep20{U>jvYHD6xX(G+eA3s!1(dwrDn~VN}D!qk{fTlQ7Trf zDAlW1m-pX)zgTe(J@k;|=jY4mr=Kpj-FBPIpFh7?o@vvj$sKpxA(bjs(tMOZVZwxB z`7*`m*|VqIdFP!<|NQe$kzx)1^Upso!VhC4aA0-m(na-h-+lMV@y8!8g9Z(f_3PIw z?_iw1eftVz%38g8wM?HrU0!?bHJLhfsusCp$By{vM@;_y`)@hyu)|btpFVx$oO8~R zxpU`g9-PUOCrgbQHKbRsUb1D&7TLOWtCoHF<(JF2apTC6aRS7{hYuG3wtf5dBIOSp zI8gZjtVlu#BY8pBsa?BvT7}0Se_U?5=_U>T`|rQ%m(^ip*%#l1B7@NjkWl6gH{5_H z4H`61AkCUJOIoyOp}abE>Zp%%$t9OamH2LtH6UQ$uwjGTbI&~_$p;^Npnf)-#~yo3 zi~Zt@FVx4GIB}u?vy*x{LQ>(;I1%rnoF6HYim7`<-3`DWp7 z&z?PaS)udJJ5MgX^inO`0Lm;Go_z92<%iwL3$lVg{q&QHXYEkAa%F*(5qG}(?z^~q z@c*0{`8~}DW%$oM_ni3AamO8}SvXf~)vA>&UAk26zyE$=RI6XVzNTf{dGW;;Rm9If z|16BKi{b~aEEU*{XhO)H^njJ&#+ajpKmPbbzWw&w1R(|)&s5x`NfQk>Yt~HtTyO#d7P~N}6Q-hv zAAVTs)vKrU$Rm%a-k6R?j2NNeOga@RRFG?~xkiiRxIqJPhm&(g1V)B-FVC|>B+SB{Hxswteml{epum8|2!)33z4u@j1Y;03=)N_ytB(@xWKsoJ$`Yv<3NJzLMPKKke*VOMCLWrX6)h_xT145KO=4m#Pp zhM8*RB;tOS4Oap9>TiuyR2;LDZ2K)x3UU6ubWV>$M6W ze)ysKx#T!tg{-$Y>fkk#8Lb(;%xS;Hs+L)l16G+;nArkYzwO+)Q&{)$Zt}hN-pi~2 zOQktrSyFVmV$MJReDPb*r&~@?`5mx=(tJ6W>ws0xJ2<(DD)?+zRU#r04(9cUbq9a$$gV2CgVDXy5OT6Sgh&sDRazF!CfyIqc zUf||{UP`SZH#@l|e8m-4l$tk_8G@3VZX2uu3qt@d)1_OfBp4Wz0#8W?4(JP1TuU~yttOatyIJF`ds3}WQ$P~J~oD2gBdes z4AGQ{%Yo3EHEZ-4?$#5Bl#%3+k98#@&73)Nl2S!5h=r09;6a8@`4Hv7D@`tWf}~Ia z>*beU7B0A9w#5xh?x}O(&Z{U!Kg_$h02hnJ1iM78ICF)Ud*a@_S6_X#+AO~M>Z|h1 zGtcO)(Tgv>Se}3WdHLaoAG}#h4Raq0d&7YP2WnoPCk`>1D09EV%>-_hvaY1Q*aTuq zwm(@tY#kv?(UcK)Fyeq`%;DlJwZ=vUKM>+xxx~Xpp#v6ok+{i;qQ3g-EBXHW?+K~} zFdd{3kMqeVpQw5KFTebf;Ced*;9=!bwQ5zhEo{}QmF7FLhQx@rH(R1`i&LCvDoa5$@Y_zn#m4FwHZB zQI9M^` zGX-M5$jE>d4kq)AWa(PIVBg2|j0r#GVrWYoy7ksuVJW@x$}8&gvEN&6xkX1z+62>k zm;bu!t`kOP__b=)QsY>{SPpSFi!fA@T++ivp#oO7ZrxNrOlJT_BXNgmizy`@)NecB z8)CFDVeYaM#uObN!q5~S6M1~@I+?Jy<48+~u#6`E$tRzTSA?*V-n@CU@))528N_{g znB24CN;F}J^&v(i{D8OyU9DO*wRa>fnM#tIoHxjLsDNdZ&DUR&m4ej^JhONu3>lCR z_M5@|wvIUB2<7uRB7O{p*s&7U#Qn~-*Iuh3%)QJx zAL~D5dYl8v^<)W6&?>Ol>bGp!GST?fty}B%pJM`;EUH*H+&Yku9TkzQ(vMVO+B|o{|^st2UY*vJw8DwMN|!+7hhw}k19on9;!695LE1-{vQv~Al~uytfqVlhv8qehKn`SRri6XYj1 zkr<+YRbUmyJJgSorzYMfa_aoqXP*h@RGf~pZTISpH4h2Z@{?=Q!sJ^8)_E`0F2U9>UAwBLQjuWqlJLMc}1N)T?=m7SgD3W2wrwy?n~4 zP_d%aefnu>8GoBln=i! zV?%&6ApSa~O?;zJzg|6Q*r0*5Z`V#XZ`vdhV4boeJ6k9@h;r=!>mVwY$w%12vw-F{ z13N9&Fr0K}Qidz5s0OkLtfG=!EK(Y9z=~9N+iR=?mOWiZN|FOsq`KQ)V;!*U={i!9 z9IztQ-S!&mfMrkD&hj@X>$+mXA^#tLz$Zz$&{AT?sxK z$O^EyJLPDs1g!&~{twRv#%P6pdXn>O$P7OF< zg|lCBzE%!cIp2lhY=8q+IQu2%Yvq8I^IaIu1~_1avtM$)Rt{Kpx-fE59IztS<#rnE zfMrL|k(1(p6}c|A(_jZIJ9>_s6bG!xb-A4eJ7C$-bL6B%7Fdz%Y0C|Ez_P6HsEBjG zib_vgZl?p5WrasYoC8)=dfIY3{|5j7|Nm+0Pg(!~00v1!K~w_(EX5xd@@$lh00000 LNkvXXu0mjfXSnt2 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/002.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/002.png new file mode 100644 index 0000000000000000000000000000000000000000..82828bc8b1d7ffa05815127eae267d9724a807ea GIT binary patch literal 3571 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 oUM8`foOV literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/003.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/003.png new file mode 100644 index 0000000000000000000000000000000000000000..b8cf7e1646a99f9277b512848b61d61804e92e93 GIT binary patch literal 3526 zcmV;%4LS0OP)Xt5TTBy@hpiPB(D!9A*hif1ZHbAi8?yf zVZZrZdgpsH-|RBGJMYb#_b%CUm0Qk(&nUwYc%b{b$gEO5UR zXMp9Go_4sM23QUY+%Lr$VELt|9d4%qmcs)7Uvb`c8DM$4f8*9V1FX1--rEHmV0pWL zXG$47pDR@jzEhffbJi#IQ`0RSYFy3$O|lC?JO7 zFM}B3B-2^h0<28e!X)sV1Prh|*QM@gkO7uE`oVLxFu?L$m%5`t23YRs2hY{Q)xh$c zw0+bd11ukPwv$bbDXG-#Oi1}PYSc)M9XlqcPMylI{?W$EFcXgjW)4`HZ2Yic!&Hjv z*RRX+<;#^jk&L(Bep{fYpME;SY7+-~RJ;r`@mQd(z*@3oNl;t^1`G(gv*PI0tCv8e z9Y21&lrLXis#dKkn>KBN6+(=VwQALpN|h?f)TvWL!mOU@)2B1oijtF)1u#sT zHcci>m>}PL^Nq}#H!qm&7hilKrAwDq>!Qh%CrgD26(m=#T)}W0w{G1MMwfEs$_XHy zG-;C5uU|iKLXsr+@82hiE!4bubAhb&rJ$ZYdn*3s&71Lhf@}lUh!G={fT~ofB1MZ9 zRqpJ7OJxu+jvqfR9XocE)vH%a*|KHj-FM#&=3l*fb=kIUn|$!W2XguHWx00kntb`? zmvZ&$Rr%+ie*&o={yp=|Gjiw79R<1r2M);o{rly^4?k3a6)RR$wjIXbfB&tr9W`o{ zvN34S*|TRQckbNs_19m+@&writVN3!DFOZXcF&B-yubpD?1m|Ni@uH*a2P*RCBw3S`P* zgVC~OOO-1>u)BBfDz~mz;lhQL-z$bKz+yg}m}) z(O??1CVl6fca)crN;v`sYo@Qh`bwZ0HEIMn5C5#YyGxfYNy(BWC11XL^4e>!iSG2< zw{Jg;Y=c>-P$5+oS?lSL=^TI4rcJRtL$&}*5A{2D?v&SGe_c&iT4F4$wL2>|(^#Z* zH*ekyvRT_%iRlu0^2sMvoE|-T$oJoWFN|#G&!3l2Ac|s=-02G96fqe zDW81w(MOU$e}33*p={T~7GN>dQ!^IP-+lL;K)B%z95@gw=JduJZv^vXv48mRVF{;~ zUw&B_k=nLxtKu>Zu@>94YnRZ`ifOY^r%oMJ!p4mogF8eP^or>r!xmtjIdevdXv&l+ zGGoRJMcuk}Q@kBJcBl?lo){X8NXda6BUyIn&;ct*Lxv0qx(^;a7ptZO80-Y=)vG6rcH6ga4{$>Ne*gV<$&)9KaD<^l ze)!=B#lzOTXyfVlty;CxPPL=u=p)GRh{G0Oz4FQ{628brUM!>u(+gI3(GuyPjNna^%RN zrcS0$%7$=5!AJo|wDAH_)t^4w!&AFk1P-Y|o zxS5Ryph?3~2llhiJ}dfc3%{4h7GSwSKzzGn3gh50z4|acavYLgcr5vRdg8c&bti|m z1-ovG#-ibw$%9LP z+x0XrjNZOKTJQJiSQGE;7}c{&v@?!a2Ve!aZ+-h09ombcz|OX=6?o1j_1od5z;Qc{%iESC)N zL*zjjk;)}J5h$*}TC--2y8n&R11e%%dPBRWZr!?Kokkyf>@lUGj5ainjOZJfxQi7l z7L+r0o%DTo^aRnwS-pC-pu>ks807>bN8td>Xz=K$xHV}WHlyRW-+rqu7}l#-PhEvZ zS(Y2KTFr|ePf7qpD9UObLPn%cojNJy*_A6-l9i%p%T=HXR(GO(0Y@zYuB!@1Tf*YhV|*wM;0$$tiq!;&_jd(6KTi;KV((J z0JMZpoH$WfU!o+T-;A)fgCB;z_fHzuZKMN$`Sa(;sfhfcNad;s31GwxSXSj#>lB5M z_4Q+bW|bkMf%cEljLPF8pBX&cia zJl0#*Is$t5yce-<-8w-}QvaCXTPdS*t?}IGjvKI8b084Zp|Gulb~1iC1zQt$#I+j* z2JO~1Z#m#DRH%^flFTo^{Gz5ov~je?G~t=02M->oB-Zj3BVbrJ2u!_b>_AM7fnfD# z1vR7Y$dMxzA49LBaEDVy<%Y*~n?G*AViAilZrr$m6+*0uV6f=Mj+W4H)=C`Z%9WE< zt5yk94{Nb#^9YZpJ2#xK{dz=9N=i~-2GHT8M=zbmdL=o%v%W`_tMhWJ9XDX1l7&!M zXY*uYLpOA~gr zWy+M1a3Wk&vu4eNk(g1LT_kHM>l82l{!6die}i3b*#ayUu2ZK@6^;_L1kmr(e0Z#p zI1<3kkpQC#ZyEgj^Utt^BL1lBWAW)U%(URyDR@wyHF zWgd^d5t$upi4rAn3ez7uMBb5LEk&61$N?E>RepA`a|5UOBt*9{H8mAy2t_J4B!Y`Q zwg3y|PlkBC>!bY~CBS3ngdakf?0h&>>CFqwxWoX@vtu9TDM^7u$8s zLscFg1dn3~@?g!w$vh8z@U)QzDAP|?Kglr4bno6>{`%`L;oJ<38+L3g#3>s>s~YVa zempGRnGOLU2eyRMCPq-|YaK!R_wTP>ALKivo?eL$MJgA5B9JY>;>aK)3KtC16=zd) zjP+du9Z%<_{o1zj^|Zp8hf{o}Qavi@_~B1RYJN0!n>KBP*J0G-4I9K-cCOsH z6`!!Y`SOK?S>q*)TN2}6Oo5de;RWNw5JQ61GOz=6dyQiXEE=o8MZ*nAO#X|LNODBb zZlVZvi8-($lwnvtlz;)24?Eq7CL3TmA!Q$yVSwesPIscowg8J0bwjD@sqJ{m!3hAR zSB5RXO0O`(_m2c(3M@X(8Y&?1mqBP9;sNKF0xKR3FlD+^0tQ&_?3aYEl>t`5ccD8Q zV1VV$eo6RR8DJ%R7rL_n23YRwmxQmC0hTje=(|!3uzc6$&NSEn%NaTQt`q|--*ve& z4K~1XM$Wz~#Q@89UG7YS4X~V%v+qjrEwFso(+)S-0Lx*4`=vMoEWh-$!|gP{a#-Mg zDb4`PFFoyWJO2v+0RR7t*C~7e000I_L_t&o0Jnk@7C}uWU;qFB07*qoM6N<$f{gE` A)&Kwi literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/004.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/004.png new file mode 100644 index 0000000000000000000000000000000000000000..d84ea1c64ebd10c4e8103ffdb7efa7f18172ad65 GIT binary patch literal 3527 zcmai%XH*l+(#Jy+1ThdqN+=>AQbj2uQHoS)QbTwY2}nn20*TZB(n+YH2uMdtq=R&j zCiOv@G?5Y#DFGoMy}a?9_tQQ1ocm#S&wuvI?Cj2OcA^Xow3r#W836zQ^Al}#W6GLB z8MX|U{?5<(w<&i2C+e!ENGehmeXOY&$JY@XH8wO{3mv2R1yhyN8}ews9A#-_r0j>j z)Om?Pa_^@fv)mr{jas_L5#Q8)x4r{)2JS+TUq5$|$o7a|tq_@1QX}C<;8u{U-@$Vo zLSv8t^^YPI??w zwt`}>?2su`pvUR0t)b@D{sNsQZ>g@CjfE5*#qB`}?-s+X6AmT-yGxOXl~pn@z~p31Ru-A%`MniSr% zc%M4=4xMJHx_s5EFrPiO*~w04cwttpOff}sP0Xmu%PGL^?4lHO%CB^P2>JqGQkfaI zH%|fxv0SOtQzMhr$KuZq<`KsDm^t`!jrrfcT3Z(}c$Q?Nav|pV2%~V>4zM84l zIHThV#Iq+WVGpCI#GgoRe9H|b`mcTyNLTR4YclC?SS!k)Z8}_zVfrNRk@KcUTsG<2 z?SJzPaVQuY$0^`4Q7ph8c$mBQ zdqLSMZA!*{QRQfF{3#(nP6@OtxE-Ea+x&KyNZmtZ0!9<&R)M^*8vEhIvy+2Cmx|UR zGMAil@~p;P;&6sY(CJQuv5!wZAe@Tkl6J+5)|CV;z64f5P4$}Ai}Mr0JYr$e+i0OJ zpwRcvx^aw-QLbu{=tok_VTS|l&(x9C-}Z=@@$>U;6#ZD;p|``^_gp< z@cRpg?0{QL*CpcT+}bVoH|Ip?fqcpId`Ok6JPJ1|TF5#=f707^r5e>S{L1Ay&+449 zxaW<=TMhWD%%_W?zzKu!9rEBEmwL-4FC%d1K54p2Z6HNb9QbIDMdGg~Xf7E|y1kX8 zr;Ll8CO_8VAEw>&^z?k^&sBFzFAealL@SQZ8OW<>s#P*5)vVX#y;0OMny*!cPQYS? z#i2MB&DuHFW_>Z!;u}F1XZKYZ?|BP-Io$lbAZs277cs~*{9H0sR?$`(IEOY2?ul>n zJ8*40_~}O&5U)t)u<|z|^iY2YYw?YClxFr#`su$rylz)utkn%4-lI6rxOem`TT8*= zyrsFh)$d?{5&}tfacJ^Jd9#7uM}CM8NpD3c&8*aPw7PFrFGBCvVbJlKh{T4dDLg$l zaAxcvEptn%1y7z~UsdR0_~T*L4@0NK^l?}EmKy_Ui_oHI4i-2M1CWvSR#%|4rn zW#vseob$5M{g*Nrqa8={!f;mYFR-RtExQIKrX?Vftx44`kR5jAPc2Gg?`7PhTP&`~ zPtQL0ig`4hA8+Kpt)8}O_W9I3Hfe4p8LJ(1_9wO1j@w4E-nJiMqZ*pbE$f)mq7if5DWdEnEwg3;sSlGN;WAko z#$M>NIjb>WW?q45vO+F}6`y1Wokuk#tu-u#gZCmdxn9rKei_SW^DaOciQyEu;VxH#SQ5F6dOa)cZ*jcOL^C#lcXzb=b8;SV}B+G_m8cO*p~{ypnbF_{NDC=s-& zWW!n16iLrYK5vlj?7uiT-5t?AA``*{udtE3*`D48&TXIe%Ir|C8-btTmDJ)l3MXK` zq2&Az-5ZJTU4(oR&@C`+Hv8+i?NiWNPUo`n!H+k-oj}gR%x)>KwXs6-b{CN8nQxNM zY^_sMr2XFdbXC5qG|bJ?qlev-yz+!iUk58zXd*H$VyD)S>sS!k<<_EYdM4xGhip3~ z>*A7gzoF{9H=vv9Z@L$;oGzCYk(2|y*ncoj+R~;^r$eeneF)gMu)O9rU8zbO$-YB6 zhm9cdGP8#CMX!7+UfrX-GyRzwhq(BEy9t9-cD;hA49AB6eQu0m<(<|&1BdudDH?R@b4OmYVK5ze8*7+gKXE{L9UMO zkNIpTJH_sg%3orXNCqW(%gBy~bKFjwpF^@5E=E7L_ujuqUbS_ zFqq2W`YA=q@*l&sw4|i`=<4UUyMYPxcHQEs?sI*LaQzH<{olU%izr1-jvwC&j*d4f zdLZZzh@Wpu(Xm7sV07ACO2YyYk1oz^oU+}j&u5GLK1@L{UBIJ>_S+)dGZ8e>`JZ0q zEz8>?r(-d3O2@@7ZvD-W7k-*|Q|78P3uE}78qliW?%@tl)htl-UN6ogt@J1L>-4)d za?enADZ;r>?v|W((7=9T;3-h~)IFuI0rj<2STRP9hqdLi{KixTP#3`F>h`xR*-{A2 zy)~usi-lURKmd3eZ@xzT4fL}a;kJZfXJjP?Ft|YutPzP4LBsZPqiwa1U4*N}o=@ zc+AG7oE7pq@?^0WT^-^9&Om_O%}b0mkaEOyhYzDpEW_Y@UVUVq*YTc{{<5SM7i(9u zDtui7LT}?7OS_THF%jF81wlZS!Y#CxFom51Xf(KJK@46Mslgcm(;xA7oj0c(ix(XfWlWeLD#*a7t1o|juXQr9o(eA-uC^OIy%h%@f!y=e;ww@C z8g#*Ky4C&#YSB;o-GbR}XTT`r zvPvqDk2X#Yq($BE00cK(nh*Jy9mt*H-i@*;@%87ncaU=$6AB0oW{pL#!bT2CcTN!5fZv5_T98Y|((VPQ4go0B22)>uDuH{Xw znk}BKOZ5#jbl25dX8s5OFrdY%H=RHMJs%|=G0$5)chbzA70RvKV;EV?ejrI{Yi3?9(E(<+pik!#t7L5C^WsW}?v zJ9WiH^gzZ104p6S?z7ztIsvVMj)N(Ze+pIG)ZVXQ4-H#9m*zBssbgM-hTMiq>zQZj zkKX-x|BBwjq_`dM+~ECjMi)MVophbyV)*h`C7{z`AW8Y+)C3u2KV4}(s)e^1-|1TP zNB`kvLe|#lXX0=YJKv|atC+LpxC8GTiB5r)uOMj&Ngd3AfRU}Ob4@;@58>1pw+ns z^N;j4wV?d#&lTTM)q&`soXqZav!Yb=5|9&Nf(rqKC0jZ=(C}1}NU6ycSQ_X6A~%cK zy`#Fu%D4@B752r;x)-~Q!g^Tp>4oxEh9tcCUox?(W1!)~)dorvLdEpZB>GLqKr%&U t{fE4u2LLWE!u97&DDBv#zkL`Dz_W?|IxWL9QHlldM8iP6{4qS_{{XhE=Q;oY literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/005.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/005.png new file mode 100644 index 0000000000000000000000000000000000000000..c0f307805c2437ca43d0cf6e2cacd3bb0207e9a8 GIT binary patch literal 3523 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 FzW`U+%##2B literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/006.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/006.png new file mode 100644 index 0000000000000000000000000000000000000000..c054f24ae7437b86890501368b8b9b1dc89356e2 GIT binary patch literal 3530 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/007.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/007.png new file mode 100644 index 0000000000000000000000000000000000000000..a0de90bc28d0da691cfb909ce75cbfaa7ca67b49 GIT binary patch literal 3538 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 M07*qoM6N<$f?$ieTmS$7 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/008.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with multi-lines and wrap word/008.png new file mode 100644 index 0000000000000000000000000000000000000000..7cc71eff769d8bcce44254f19efefedc82454b4d GIT binary patch literal 3532 zcmV;-4KwnIP)7p|2wvGhJqvR&uPF14HSs`a~kkL0}fa|DAgq?;(+B) z;EM(vuzXRgOVk0&VZav+IAHmr)+|gk;37F-1+4I}WaoetmX;2~16IHa4@-6qSYc`Dfa~di6|lnppIv_Y9I*1ceXZ5b0n3`|^E+b)to&|YYqfL0 zvZng{&e#DfzuVVZ?J^3iNs}gtqwLdx4RmD`ST-tej8B=3ChzK5g1GdHe0R<7m~YRYeMys$7v5 zvxt#Xz*?|ifwXGXN?NpNkwq#@y(qWddaIVdTeogWUrJRjX(Ax8E+Ge)_4r^wLYw-H(Wj z9z8mi?jL{rAs1bAkn^3SW5>p&&pa-UVIhz+!0Oqvr>f_RFTRimAAC@@Y}q2CMvYQlNgl0R zx0Z(=fd&m4;FO3eRHz_)@s%rAmes3QOYPdVW9j|=`|s+5Syk@wzvPyYP#PYv^+r@%d4Vn|8!?#7K9V{E|ZB@=Pz&>{6sojO(LW*>a;fiQg1 zTR|F`p+|Aalq*+GquNFn414zMQGlIy-g(Ni`N#=iwQJW-b#(su=W9w(3* zXXebAs-rn`=E&yFo0UvYHDt&T*|~G4#%J#uRFQq}{@OibAl zxb)IXW6GkJxbVUYHOw4HJsEzmQY1+ltV)$CI?S*tr7@XD5qHfs*Wf6z9YL@0>Z`Bn zkf==0gJGPXq<;PN*BF;oBfPiXdMn0DRnELXPig#I#&Yeo*P5U;H|v4Lb^zLW`Q?{! z;^>uEUeU03xvh*0J($axM*HWVe}w#+H*c=-+YC!ml`}7}cV_%Q|NOJ@T6MD?SoB7$3aeGCRzy+kmJ^3e zk;o(rdK}(^PCohMB$A@T+lCDr6c9|{*_b_i_^|GMnHMSBiWMt_EfMyym_!4z#ful~ zoY2VCsZ&R{B*?cgHEY()h)Jq)49UD?hHU(AyzvH(b;^2RvFQjErzzr~;^K?r^A1#8 zyqC|;JHs49m|2B+BY#1HYMN|h4RV>1L>E>~S`VLqjZFs+{?SSRC zhC`aZ16D{I-tTJX5wQGLl}kSz4LD$>qb(bXm4xP4_IVuA0i6@@Gi6cztxnlt%i{F0xP2;fnQ&05q?YG}XpCXuOVXcf+ zwKAfQl`B_jV@JIy|C>B{vKqV61Mw2Q3*dyFdFB~dN|@o}`SQyz1t4L&XnIrcQD)9p z0X*UxHEJZ^eDjSSSH9<-d&Dc3EMM}mNd-1DPbF{eiZZ=YlO|1KuW&4x09HF=#*D!m zBa9Jlyzxd2GqJ@3U~nOh4CB&M;9`fzDN+0Y5;q-UXofHm<2}su2}6V-dQ-Dn{P*8~ zg`pk#{`>ERzX=m22pSMccFa1D_@lq_E(8?>ACz%n<&U|7oh$EzTQ zLc)C6xQ4LkF@A;><2NqDAbu>H={Y!Wj)5vejJb4#_;hLi{{8xj*CtUJ(Fc~?QAhtV zrvrSDccRY#2m`_jUA;48~0fG8o8j| ztOwR@x80^XS-*b08memG=z}xgcmW4)s>B3;*REZ<3jF-@&-Km)=403@rkX}FIp%@<$(i6mQDJiVB{P?56dqEMNtGgzh#2b}Ce_^Q zfRQWP1Ej$)MqTi;2*HH_1Gm_C=Ya5x3@cpB=aFUN&YTjzp-3cgz9!z5YaLk3H&D7! z0FzO8DADlfVen(OcjlRA>P2V<99SL`mkt>qGBM`bAy$}NR>u5_iGMn2d1Vc7xKDv$ z8Y5S>2#gFiiTGzk_s}Adh+g={ypC-S%0V1t%Eu~@#RrzvCT(*k7IKD6oHt(+XUnw? zEE+Tsv7cm=!XzDoPDV@!&lc?*+b?!*EF z@BpuOq75%qVvdH1I|ig@opqM56=GJPq`?&d47uz{@$s;c%F2`tT~>w2u(t+}GSgeZ zgP0e>yW@^Kid<=#C|};ON!EeIUQ!}r2imI`cG3B=`LUIqXZ-iwcb`{86;4_hmYN8z zQ-lY>qle=J1;(8WQw*<|rE`Y@z(fY%;vNsNFNTRc+Y6Xz0TORIEE?#!SOM}=48WtO zF){#?vO=Xwl`?DrsPG`l9g8^XDV*<7wl&8WVDZ%*i()m7_CV3?XbFnSl`k(xM5Hg5 zwiU{I)#}w6SGr6Y393B4*KVAc&2Uff~Tqsi-aKOrxCS5vl z8gRghQ-=#=N&^m9nbM?7Cr$$nSaIrbflO(@0V`9QOrB2YVjZwT*W|%e*a0gTJ%=tA z2dvOFc`y}rzzRmsq07YqD|AgBOobh=g3)v6atST4Lf6s(SJ(k7V1H?C`GD()JPF& zLg2B{r1!4UtMnqA;k~Z&e$V;wogZFqwtLU4Su=aMXBlQ>aE^hNlNN6Icmmr|HtSYXFr*bi2aNy!u9J4c8;3hKO!`U zo_m)%4YKyZsDkM6Ks{(U?c;_1$-tpx&)w3(!t%n@@djo6@ur5BdF7$X`KI`du^E;Z z>j%+TLiJ(Hg(Ev^C=AX$1V#S+FjayXgOfNw&1!K&YlH?(BRG*Rm4|6NUz?eu*ya~{;3%ddG#8ht0FK70Q1m<)vBLz)r9W&R)RV>#6I%1eNlAY! zA;}1XHBbIHNsqy4YoMsQ8qdmbL4;%km!s~XG{EsiBk2|#m=voUhTx5q0NU)OrXb#7Kvr)(WT${!JBOlXCDhk*O8iyFtAH~V4NHJSdmOAHv94fs z8LTeTJEJkU2|av?6e28FBu$UTUeO0Qu>!N;q7;a_si9X3M0CR8oUnAaXKZkj4SrxF zY2Qa7GbbRrab^(+*%Gi@2Jv!WG;vAK<{F~xAOqv<#O5zHJ@W+AC` zT1MmnL7xUsl<`uoy&v=#T(Vc*Z^UH^0?_pE{5}eYn+zcBSU8ayL=c5lvAfj05W>Ir z{$a|oj37k-;uW|)Z5srIu5bRhN~90&Wg<|R%HKQ! zLRx^-l(04)D8E-Ia@3JytiWRm#G%QYafY3SRCp4g=p7B_fU{SDysKI-p=3mO0m~@1 z0hWr)0_qe+D>WJ{kSYmrN#czW432USMRs_=L&0Eh57kiAN9eHQi!??|7$0!`Crz&p z%%y|t^B%27!1XC8M#HiqhF~FP0lORjd+txd*(U(=iPffM5emu&3Ui8+gB1#}09-pf z&#;B+*-LWQw@g&9D~*w=F#KT7=X?-@`v%=4O37XkKo>yCJ7`9F!@YhQc|5UHDrw(; z8WcAAM0m}4SRO$h4`JYM4(KLQd0C;!^h_8Rpv7e|f(DK4J`4PR=jz6wu|>dt@n;Qw zK*k=-H+!6@lW7dj1VVv!T5rWr6cP!qAkrxH5EMQw6a&l5q|)2{*!Jge;9S0xBUTJ*OwVKbk6KvUQ>plTT%eGL-_*=BfQFgZ4y@Y*LyRPAal4l=yP1G z`yq1$0MWrOXa|x6B80r(B8q^@D*bpU^>%Mpg)btKD_ ziJBED$3ce(89=l7c#ne{$pW(eXK!klQ~^hWSBH{=q@y8hRs3o_Af@YMNeg&Thgi8*}R_P;lJE+&hay=g*3a(9lM-ARCT|Z z_vaRh*6wW0mdvX({Y;vXeRgK&&ip+B&w-uQ5#$({X-R;G%V=d=+R^E)su+Hc-cc}l! z%ii6ZE6;x|VMl%~?Ht*{TzkyBJ2`uk?h>=(YX`Qa*^;5nj4>c`US$l0!Ln8DD`je)wfVTuS|+ruW}J4lVtw@S z)3L-FpOGafprL6UPAQ6&3dazGi z&-0qy;Hb1yM9-BM`n|4?=4rdGd)*xhSo2vVW&L!!8t-x6VmoNxAqme$nHYtU5axz^ zjH*s={N@x=v%j4Pm#>NY6|;V4|K)A@iKKfKeRe7|QSyR8=7z>Yceh+T`&>#m!n(h3 zx!rJlWp^M|a`0xosN?qf;#k7%LCN{bHJc~g1>&bYM$Z;DDNesS8BMI2>60=PP+H;p z1Es%+F<((j?epI_u{HQqjq)c+H?9(iwsI{Uyu3TyY2_xvsJsvqIMrYim=<&CrjC{0 zNmUPwsfCqGG12hMXPv_{#tBn)>ZZOn&YMYlkGPT9P`Twjo%NzD=hJjzc{r;Q3xT4D zI<9mq*rjAZwg7Fd5brlv`rJpBf2vNaM9t$y`7oKTK~KE0yZpX2sPbGZ1~Zt~joL7#D%*l#;!kq#qzlKJJG7-wt7wNk9{a!- zQL@@USU2Zhp6ePHuCm(gZoD)W=f)Y9l$admzcP}zHJ!s zTUEO1GHMviu5qK*Uo)jc=W9EYP$4%qS3bWwHsH%_auiP#95$BW4%TG1Zm)p%=|UH{ z(qS=pb~UH4>Dp@V_52SG^>zK8KUAK&j@SIuk!xIX-q`LCZ5TE-?cwgQ&%&MLP&V2( zoRzjcr;>x1LobAsx5QeF6b+ZEpC*qh9-=F5jf5{2rfcmw=Lt(k%LXR9@@@NMi$=+V zf0ZWFUi3x_3ti9~5;$IE7FhO{VO;P$Z#>(ZkZ>lMs;24usyQCttxVO5Cr1OF0&?A# z%2u`}GZN2dyL{+zq_dE8t60>!U>S)Z!ZgJ-Q>KL{2IeN1Ej;lF{NY|^p3rV%4%yF@ zj=Ns3Ww{`|E2x=!Gs~-*%pfSuJC%HIG>&D7MI^gJm^`j|G;rg0jQ#-qF~#ptK&v>khUBQeV^o@&57H!8BuM{?Ga1v6q1}e{9s=t8LX%lc~yF;jH59D`FGyi(4r+W97F}u4~Kt=SJ`m zgoL!+pb4>{@7TI+$$3$dIPzw>*v67#Xq(5`be+97Nw{Qq_4}#BkZiwk>gk>LF~-Bs z7P?vFd!3}8gs1_y8m$BGsvn|ISkpZ%;CQxX2ocgO(oK7 z#uBc+@u|pPX%)Tlu_g0%+xBToQG$glRdh;*W53;j`UxW4{KhOxF^}>c_bV1<4AD+b zG+Z=!vZAdSQC!P4#d%IeCe!ueRYxz{hA~wv@bjA5xVP!GkK9yzR#sr95NIc~FqH3tC(@C?Aj&x0i75lRAHdm~OG%s|SNhTsC(!X6u%XOyv{UG~8CvJoHw?v1%}pr2s)8T!@R@vDow zE6FJtuLhPyg6E1%&dJU45(k=7ZXCy0#$D3OozOeE?A-V6aAiU2ThqL5Ba;_4ABghThYV-bF(RbFGW-2S&g(@a> zI7cklSXlx6`SN6&cTEV*V2nt-SLek+b`AlQv3+`XS{U;wPW{en1qud+zSf%aC-{46 zrj(0cV}bdqZ8vCD7W=N1bX9b#to8rQ<>;ko03Zx5Q?l=(mR{K3CMk zdGqws1n=JOOUbSy2jSbf=%H;rHLZa_F~OuMBBMAXcc+i>|CDvejjlV$jGt6n`0=+g zgnA~gjhmYFqJ-nRGs5}we#<4BW>h85WZ-zxr}okJU%91d-I{ICA0D0VinpMIhd*H8 za)e`tu9xr<&Adw)F1PWp#Z4L?S*lxKbc~eL@5^jSZH!3PXe|4?6=$}!vb3snh{JyF zs8ss9oGDvX`w(M4ul{t2?Rx|pgnwn*=lzO6`>w(hPKDNqUV-l)a>)kQo0zF})6b&3 zd2wo_<|H+(Y>A`36Ukab{4B<4f;!KC(VlEB_^$t2Mvp+bHs*fcN)?^!`PQw{TsbPG ztC)OG9aA(l1+T)f$l_tJ`T^;S`8aeaQ&C9;cPZ9fWxGK@^khM2XA;lnV{R#I4VM(!V;8)KN1W3 zK-mk1nS&fdk<`N!{gwbd!fxD75t#xJCoAqNhd6SSKZv9-qkR4WVE}tenCSJh_6`h# zk6{>OQ`mO_hQU>sv{wDU!PqPUGy26}^nK^4+}Lg~P|$dydmqf(p#Bw~)i{9QNnBuG z_8Ey0slyLzC;l&^dyNqY6V$rV+d;BmUL8DzlG2$$u%iQEfQ`i&Kns8zX@+%Fe0K<~ zWwJN+p5I*F)21{*T%ctSg`x5E-dNq5;Rf*wWA@XTTYNA+f`Pe^2W;>hjR)mJ7DT@r ztA{>-L>2%(0^HprdC{?C!WhKU@VphjCl5e_7B48;n^Ivmt~)Bopq_LWdBiD6He+$# z3`QX)V8i4@8c3x{0$|uUZ4!h8ZIC#0pGpMm+tWM#0oCdA&$EIs|6(8y`wglQh=dyq zbDz>x!*gMZB0=+vqUDJK(8u>woYr)4IKc`hSXG4}lbTHKdXz_e8n%!%uRWFN5?Kub z*;7e`Pp83wP8geWJZTR6Jr|USbAoiqNb!O&KzRw{(!0wNbeMl_3PclT2LGcnz?A+H zfC}~PTV@29aP6~}rWyot0}S_YYY&|fB_zoL7rh^2gVn180Q{Ef6$F>b0CdadIVPFm zjn*XbM6X>2(l*ZhAyiRBp#&?&3q-Guzj8r~8r8g*VkVIX+vIlu7lC*%v^o$&0>q&7 zmu&Vwo2>Y1oDfJ7S!q@rEJ)VgD&3ZY75Vob-jb4~V6L?vjg^N~-0Tm2#TcvzflvK% z`9h2S3dDsgNmOnC<+mZ3bL+HvyheTy$!y?$P|g3l7ZDw_v~pNv-1-{|Z?snn0sf8J zyu->+mp<&LQP_5vz*&d7UWT9$U7#dt22}=n#VDkCv@nGa^q|ua{gs2wl?e!?&Hy+` zxJwm+bdlj2I+PEx{O?VI7@4(&5rY2zRtjZ|LBb!)q7BKw$y!4>PC^8e>&9GA=Mhln z_#0*9pgyFogqtcfbdb1LRPdrcBbt(sEq{n%L$w-&FFd>hVgAhJ&mfQDwDq6{^&&Qb zWIM=VjZxcSsAmusqoVOEyv^r>UdDqHh2Xar;o=@otLL2$gWwhhCzy$`@R$ouEu4Y> mY1rKjcdu21#eu!=i+w1*+j`bbebHvHgVND7IGd+VBL5%ahvobL literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/001.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/001.png new file mode 100644 index 0000000000000000000000000000000000000000..db63b2bf385b6ab0cba1fbb4ec34761ca8909c9b GIT binary patch literal 7284 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 F{|B>l^+*5! literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/003.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/003.png new file mode 100644 index 0000000000000000000000000000000000000000..c2e2aae979b49990ec47535eec7c66216b6ed10f GIT binary patch literal 6337 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/004.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/004.png new file mode 100644 index 0000000000000000000000000000000000000000..548d6e5d00f8510e8813d848a4d58f27a00518e8 GIT binary patch literal 7209 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�}$@f{n#GJk&fEnHFJ*75di?@ z0KfndA{N$y8qKU05%{k&`wrzVR%A5fQ#ZeRKQu^? zAl-M3lddlnqR@#Ez!Nsp0R!~UEWq>5>3-z@`ucb<{K4#)msMFLHK&m>(8NsV^;D|x zD!{KRl)Nt#{qq5?2qmZk2SEVW1oa4+Ib-=>{6mI`^bj zF4jN?tcyolCD{Mt^B@X1{Nv0FH1>i2f8>0ykKu=RFeN%u1*Moy890t}v{APdd2v`J zK;UqXN}JmN4=We+Nf7_>VN!6bn`g`#1Gs6za6vHNnRQ9w`?ysY*}uNeA-YuH+E(OzbcvqtYp+XPHO2DKD zS3z}OrMrp&9^Zt0H_zlDAeb=3HP0)efGGpkkD@DnlJ(t2=fPE^MK5Ix<64Kh*7xGr z;oT8PyGcB5#ROa{rQIzBLQ)G|#ou>Fu=Wx9ksYvO2fAGj3Hz%$t3X^8gY|@~NH}!x z4u?4fyALcL{Cie%3V0whytNP%xxAsA9f!C`2`7O8sa**h=b+>aN3DloUV(7;UKTt%-Nk_ip{c5;Qg+3L@IO88qLKgr literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/005.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/005.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d04d1f92c67fefc91b43a649629164d4a9529d GIT binary patch literal 5271 zcmZ`-c|4SD+rH;ExNRAPCzL^Di6~JBQ%K5QF%(7~Q4wWL4Kq)ej>avoQNrMa;HuLLiGAOa@)4XqIb zr~LB=;{xP7HQEnB2#AT{p2LB-$$qyisUv^Xe_hy`)#GB+>7LgkB&oS0>*_V(EjEV< zqIe@tp;z4SM@CjjvlG*NJ|N2X_;^h9!u(UQ3zu0`Tg{f1zxq>qwcJMIYIjxW6uFuI zsNUID-M2Z_WW@l%;ZdP6Y*mJapmdBtrmwwrQkF);=<)3VxPwMSUmUt+mUnvyURapO z5LVLs9Wq64Y>;Efswg+_CGLaw>T?P=<7GvO|3J-$k3`GRXbC9(c7m(3HAbYsE~ZLS zU{;kAQ&w%ROTQyRNsq;2d9gV!jtT8QMq?gWlBA8u5pE)MQR|Y#EocN^Cx71aIkFy# z?NB07G@4#fFruX~jhXYHp#meeXCd^r3IXk*+s@;PZrJ2M0vA5u(GtQiJH6AUd!8f*3JY z+`H76qV*ud1y5k8kSM)k8TxpF_h!<6VEKd6x zk|+YSVKvkYQ`@Hj{@oGJ2TzS)Wpwe%K7@V>oYGLWm!pxcqkFh*uAkwLLQoZ8g34X> zjVKlg-ba0^w}V7qf<&X*2GKH1Ma-0kKt6;m#UQ9Nyz1N;VJAyt#vyc#%gj}bICP6S zR6!tzdX{tJ@ViBbjNMzXH+vBJQxxAtk^Hg;!L>n7+jy-*v}b9gD)6AH#MKou1Y1V7 z)?#75ps#!z%l4vMDsC20@O40}y8QL(#zZaP+TYNAEx;rIgVb+%U039R-@BH`*qbNe zv+r-{VW~%#WH^w?c)cAbY69Mx#3dQY5`Kf?c=I-HBtx))swB#JC)v$c5mXlvB>&VF zPndS3F=uKF1uSBR3Daa)m+vwaz~N`W>48vPc>W73Lx<eN~7551`f^LF-65hW zhgB&UDE+uj9EVrpBaq`8cj4$cXmQylAn46>X&~rUAgKHa1W5ZE2Jvd`qyUH+(wMyI zmZl;&ydkDs!$%+y|A6`>T5sBBB^JT7grq142EZ|70SAZ0Tgq09s0RHe(ex}8$aR2{ z63ml#wsRMd%O0{~@9bs?M28bW!Z^wY8d4N$Cpym#g{D%3m=$eOHRK3zOo+MGMqaJBxV5pJ4f>ZuNt(R0yJNI=6x9yD~45x z1Fqf}p?5$+PAW;TRhj%Q(8k9L)K~^YS%~ER@xrzE1rsiho>8Ul<2(=l`XW#7SvElY&y zeYEf}f!oJhQ?$-~N(~s@S2c8r@8fW|bnAK-$X*ND$klbrLQj$PsPfF<{7bo*#0+IA z>S&T%Wc7RhKF|6OQijYcBvYzy>0t0;V%MRQD$>;18-JEr=>0JCAEYiEpdLQC+w@}X$oJd) zA-~G8lLmX$!xtaaF3ntj@BK_*`-ORrijmt#KYnY|571YTBNCt$H15Z4{i3%t{cPmD zPg{Jbpk9yB!fXVo7?NL{;p&V%mmk{An3ZY2u3DoIqdT9tdv)pC3~bQwaIE*5W6=1f z^GjbFMxu5OFt!&7>MvQ2zDkz2*!?xEYGL4l4Le-~UoYe>Mow@FuJEY(T3^_*z4@hP zpNCxK=h9>k_*FPr*xWRl?6r00;Kgsdz6ngEm}&MJmi8RKey?wzr03xvGxiBbs?oN} z?8pAoi&M4DLdH@uA|fJ%b6rQ9A60%)O7<+Z3kd1Tvzuv<4skE)=sR6dz4^lDk_6?# zm$HQeL9E7>Bz4yW2mc!anmr^j2VX0#AsUBaXr6Dl%#J^!2l&W>%FBsu=bBaIGx^zf`(5 zZc2!g^s0K@)2+MkUhbgsAheEr9X+=5nVsltF(txqqrAO5AC&KV#>v0&O$#+$$Cd@epT*kW_k zQ$veA_FyC@_t8xQLEYl1VBPi$XLHz$DQKWL z;JA6T?mW3-JTuu)x^->ec;%}(v-lSu?oF`_jyrgH9pbY+-674-PXBJ9zq~Omn4LW) zDlQJQZE}9Szy1Gp(Y0Qx+eIs+}O-{nQ*C_Ix*|<$1pNa zUigNf{iAJZX~}7hQOwztYb#-0pBU$&9{RUxv+}a_Kd_y;@AMz5zUAojvRy4jYwmfV zg*OB^S(DSsWYgo$jL%`Jbm)aLTSyrELADrfbAhEa0C>7h}Jm~eRVANDf^iH z7orYE7GnsLGt=IQJ6-(B%$~@#Wj8sOKDBkCZknq4W-a9)Ox_l1FO%dyRkp%rE%YC`SYoMfVejGFzK2^>&~@IZu=ds5m%G8b zJT*^*v(}T@9o020si{LBKcze@Tch-dx?k4u(p*BqYnleGQ8Xr=BMHO`m{zD zNKirTf4%eSn%qIfhbi}8o$CW;r}nvV?;pWJzQAudyd0P@pZ0IrXFePlF~_i_vW!id^-EF-Lv$e4sxDtZeA(@ zdb3NcxQ=%VnRPw7iO5K_~I1A_JUf>qc zOo<8OyqqqJzRENexpZYq9foPa-tlC@L$l<~ZrxkXaBllo7lm zDMaVZXVrG0LHKxNIKbq@@+Pc1B1t(=@TG+EHpBDAe zxi|E5`#!zI*&h());G=_wJ4#jcURhSr@-Sb`_mC!#%XUlCT-LGW5SEccJ`-gSQYp` z0U9;_JS+oL-p`uV!vBM*I_eqX&|YSfLEXUf*@>hep8kR0-hv<#r>$j2ZJHf)s-@cR z4i@i=%Bl5B+F%o{9kVEuB}>s@_)?ZXuBK@DJn3HUTDD3q9Wf|+5O=52N%|2>P_TN& zJ;`b=M-qDU@|fR;=HiC}s2^TC$TLUjXp%g6F7$CwuS@lG-woppz0?|mH1Uqp)hvDY zyS9vo%j-^P>pH)S?eTUC?|m#3|2iEMn3~#t z{!?mqK(wRw=Dy6pqpBHsQyFgrOIfvPS#8edX?oR*HQLmk+J67OMxeJUyXyAJ!ToIS z+8*vjteh}5R+63Lab)wor|BX0WY)jp_+SoEhR)~c{x4%Ng%iACysjw9aDzcpnMASU%Fy{YR?@ESK%|juXw0Tl z-Z1@E!6aR^Rqz&wvpj?z|8__d{mi3wjt6E%ys#MzBZ8orVzkxx{&S`#9HjiX*QgB7 zvtcNYOK1gkm5I<@58bNy2XnyM-JZZW@}$IV1jB<%oVMFHN0NR)-&HLWW#~r`XBe25 zK*lWP3#fx1AgF%F%$Ad0bD%dsS=|tg8zB;kU}E(dJ~X)lLGYxXcL9EOlD{6OE5+77 z;mO-E(V`@DbuW#1?S7;(O5Xu<`7dbV_6>cQtno09hAC|`#uLUM*A84OFuxPvJwncN zDNo*ZTP8TpeBm;IrGu)>usPdxo%9=Isll-R=3EV9_YH{OYFi>Nit)o>du7pE6$BGV z*7Jw?2Q%v~5~j^S9OcWlK!v%$5XZ@K4jld>h`)=u*tb1*1?Z&ujSN(A?HfY`Cxl*~ zLO}=Y109fg*mNxpmd*s9ts`YKn+kuNdQ|Cg>#1 zxph!cCCF5KxqBpndV)RA33W{rJ1p$Ji=p%?svd*dn?vo-GC~nl4~Ba^+wcmw4fsFI z#ftWbzyOhAC!b*ei~*5SaWlgSSoCkKL&W{m3%t#tj!5{v1Z=(w^MzXhGq7o9qP_;j zI)P88M7pME8$fZSKqXrvY#>W?$kHC6L=?s1AoM1!*St6#bk_PH2>KTdji92_K}Dx) zx_~PxKs_;Oc~IWBP-jk?TiYW+dJTYqf%q^U&>=+7A+h`oKsX(sX5~$_d#jw^Md-os z*K?)+$W0iAhZS%)?!M~DR`T0TxxLbKt$>=157Fdh}&uf#xe_zoE%xKQW^ z(l7xes3J&EJMj|`2N$?ZtGkEc$$R6#Vw|h174{q+zp%5lxlH;EqO8f=)!mc>MD+%u zPNUnYcx$*Mvd+;wZS*(Pl?Kid;F?9$JU>wpZ3HJ8L--IHX&3CgtK@hl)+vguTtv5g z7cNBb9UuuivNg6C5d&evUrz?aSeqcKkfV5X%S;BP=+%@HVYUl;?cfmYS(?Et$N?I(EXS^%GsY5&7gs)5# z?f@qiVYR41;ewB>X-p-aBem<`W{2be1|AVV0aaY!1(#0#Fa-4jQa-tq5<{v*jHQ4z z(Ufs99DWjFnKv63f-&efY>68|R#%Ub`19a;k?ZGK!hwW$37JP7`7jO$Vq#=&_-HTX G(*FQ#;{t#H literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/006.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/006.png new file mode 100644 index 0000000000000000000000000000000000000000..f74f5d443a105c295a9b2b0e5f0751f5087984ac GIT binary patch literal 6323 zcmZ{Jc{tSV_y5co8icWyeF&8$$-a+0dp#H;ODb8iJc+V1LYpliLzWcE5ZQN=#3Qnk z>}$wcmWce0?{)n?pXd9>_Yar*I-UD0uXE1reO^7dV4}xB$3=%gAQ<$|YyX8nAjJ=V zFf<_X3ySeUAg~C1Z4L9g$ff7hWFDR4E#Ds2*m%)DaS&jm@6z-Vdu8>=Fl>TF2rEt* z%|a6cY)-8-^U@)S2~#G2pS~~_a)tl**4D}v;f8OB@werbrFQx8z54Ax70SxWgPtr0 zI0O=-pqZI5$&ZGk9tLL>gQk-gKp?MT$z1y!T^5*169na!D)ARn7#B0X$d&D8H63!0 z7H3tchdmI$>SVs~!w__+3@dOKbWj)qd$vIa?!rYl{a0pg{!cgpO%@7uxWvhkMns@r zfSZfvt^X2=#vmlBw87D7>tA$XBvx?rhaSHzFu{UEe|qISDTa16!M$?mPvaYnLZIn{ zLmk*3rhddB!er1`vdl&vJrcuafS`!7RrE$rs-O){hB_qjp?E?O=qVO_(bSU{JZRhn zB_uk313Q60FP(76*TW6cl% z&5b$(&2afKmzq{)4#Y&FsrMMoY3K(yEKbC1pipIS>KAOk&4T*33~N~AszEe?LJYN$c1|jK3hZay|jM3kYM^V_;1Ss-J z+nZXMZD@meZlVS1HVHca3DoV$-`O0{%qU0#JugFx#0Z^$*r$`~ISvP=5Tds|k&OZk z4$u()Q3~^U+ZaJv}riRP3Mx<*-d)k+*%JzBjMYs0d zniZfyg#k80(wBFUjRC85HzJR6Iu6)}1eJVC4meMi* z@3XU-E56L+5$0aJGK%%2iH!RGhFOiEZAQSs{&t7sBMB$n7#^3EhJ#>f#}63kp=${i zcBR4lo0ih9eR(kp`ETnNNiKZsPrhguJ-Lu=B8sxK4=UqS@XAaH-d*T;YG&f%^z2Gx zdht$V%AGO44&Pz-8y*e&z6o>V-Fp1Im-ZW}m>##E?B#`4RxZ9ZW@fu=BtLrFDfhYW zxvMQPvg!wW-Wh5^y9>EA^Pk&M z&{8Jkv@LT>RW8%r*<^di&FLm4b(v2R6={mAwLcvbnK-`s4BCcA)%_m6Sz^2z%zh_E zaqjqPIG&#-*njbLu4umn`ct=8^~~1wWLqZ_mGuwWXZXDuk#l=LJ6GQn-dmq~Bq>KP zUKDES8Lz`3B|qX7e(A<@pW5j+f2`aJ%8V3@RX6(*7tataUVD8VOc@V37(L3bR{KwJ z)wlFvZtS4@gdTs;H9o%?=2aIvGfMhVTZxUE)w>OYb^$5t(=Dm`2_l@TQX_y;%^TAz z%FrXeA77obTUZnwNpZF?x{ zEkcTW`A}_cP#xz|9|@j??`z-2ca&EuvtP$IF)EW!cXmGe`y*$E{CMCrKMDGx@J>Uy z^6lEg!F@!0u7a8OmAq+bX*^lGRxA===S88`oi?5Ho}7k$8vLL?>AxJaS$ZyQ?*~gC ztBC@|88s)GPj-|WzA5cJmvV!YSHJ6H?XJEv!;JB&w=bH>5+Sues}7@#Fd$P52o#*8yT(MlkJr7BdHAXBSr2#dxc(ggqH0Q?^t<$ zBcHw7P*eZr?a@2-(!ZBVQm@9U4ydd(Qq93V+3JTX8(ms;8o>`@Ezf?8-QOM`ui+g? zxbPrBq-*i?qK#+Wb&sl9juXto4&e;#rM)I@eJ!aT)&D%^mj7J4zcoBJdS^VkTXRcC z#ORsBz-4dalKB^pNOk@nX=iiOwbk}FddKcr1a-~J*{`?O`!u;S_~D!94$w(<{tE|8 ze}-#z=2AO?wnw+e= zB+AdyOV)b5%+g|1{1CvaXUQB-?dY_)b#=ZgOXQg&!Pxv}zlEhN%wAp9O7(XmF%7kq z%4tFO?@R|@-QKz@G-I+<4i#TR?wqn#bb?EMNbil1gJ8z3i zybM{+KFA3qa7jBERo=h&Cnwe8>(dMwDvE(YjDIn$%HCRxt2q{8w!lG~Z=uqFQB zUs>0O2q!n5lzeD1t$y!fA?IE{7XGx-`KBT2^{~wjZ~5?I`9fZH4BatV&)W6Kc+Boe znYg!ddsjomihC!v9*Pw@%Z&$mt1E9Wj|Hrb_C@j`RJOic9iu`Vyu7y%tNv$=DWC%0 zsa*M$fz)`%BNPK!?sHHA;9rdykX_VTlCaca+ApClSel$oFXG@Kl|5M;1A(ZF9_z3W-g70nll z=1y~bc)C{p2+{9P{paT}mVd6SZl!sDUKlkk33qsvr8oKoT{>boG8_(WM~u{VW^zUo zFD*q%i+@>^Gp*MhJ*Q*r-SzZ-rXgvaUxjg{6P`5e)=rL#Cu#mEUaA$Q66`8w^zppA_B0Ljs3y={)c2ktfBEzfm3S ziR^SCX(~vnV0eXHgP+3-)iJ(Ve$oBz$y<9_n`Q6ke7Wa$| z(N?|PwI|ZyN0-#U$WECS+-mqW^(esFVfc*7P69>x~;}ta-R?M zNzgv0nNKyDNn_sZlXOY&Uq0nx+rUShO>nSLLPmF&T6>N}4!5dHdsFaj+ zswqFS_)(`oqchd5w-X9$f3z6|>K2`&+>iG~Z?OFQth_o>cs|2>az{^z zQ#P+4@K}O~?IQK}%vJ09OO}86qjELeZ7;gE--uP+!f=QMh74U#c6XI_%?UQkN;`)o zr!_im*`xRa*TS~-17U=LsAt&Ww3_)2b&CnQc$y(1`kjnNg**2som^UO}Hg_*5 zxq`&Ty`iq6D0BBr{=ST_jQyEK+b)f88XuYb%~|CFkM8dCdGH;EdVi%H{a!3`abH;? znyhFwnY2-6XI-V~XIWKi~H&!xf$kTzA)6$(PSKq(xAIbGnJWzP^`;WB)#ruaIzu-kv=9U=V?*%_! z9e3M5D^bNtPIcm^7-+>-KftK`ng}1OsBxak646?`KBo9Bt@pRJm^LESuO}!M5-^%MLR*6-27eN;J2y z)PKaa*nE3A*BC>vCiZz6W{$SelFq)#7u{zrCw?X8geO@v?j4hEZo8UdOL0#pqVLk0dGCby?>-}j1Cy+6iQtB$>Bmg%sG?nYZo(qC0m55<(&H$S_vI6c{bKZkys*}?agU0f#eZUx(A zcdv2u(GUFW+_5PbEbQkbPOJT1uE?lhi}pB>Da^@dS6_HNc{(?3%5rsdv0QP_vDAcS z*yP+=}T3qzs=FbOx5Yn%Zy6En#X8y%4 z_m#){Ss!xlHI3ga@e}I?Vg=V^a0SIG4CQV-`b|to@b4vV)hWjcvP+JXX+;E6b_*NUJ=wxTQkumyl}970J9aC-aN! zN$R$2b893x<|K!fh1xIDJmG4sN>5LxPPGbHE(?#6*v=N|`Fc*$*gmHiz9_hxFI22i z9buHQ{_yttjla_IMSr1QDxs&aT{B%?iVuA9JwL2}a&0#xPVoP>p$%`a&y^rr{5Tj?GKm} z0X|0iMudYRs<5+R`l=yzFZm*NGn(?^Ped0+KY3Xovrsyx!c1P{=WM~Y#y3t-!lMaS z6y`I-AJ-e7GBG3R7rsVY%PE@dh$d?kz2ULlbrMDvc)#+j8weEbup6XfB(Y>#SemNs zT+1%VIquvRO7mS-2X978UNWMX2v|{5z18*J5EMg^;@&JUfUeJStilu~yTGZ$tqo6D zHg9GmS|pzDVZ!uhLi+Y<76&^mwS=P36?!NSYbtZE?3ipy#aS`I4h0Vc*3_Rw|ML#I-D1!0$qgFO_>(+~F*(C;L|9jp$;P>kS_hmo&PE7JpA%}%af&uW0{I)@|%F-#0n4uB#DsHH_9(BgtX>9HLh zzD(iAe9?uRBm{p!?MnfkGVLZ>z)ucfcV~uweyY(JFUKsdhl)rAYf>1F25R^%4OVYx zMq~p?K}>93MaV(TGy#zaha5m3M-QGj*ovVfY`|=YqNStFqY$d6ftIik3Hl0LS2YCS z6Y=OhM3n-CIJeL%2hky^PQUutz3x3DAIvLk?3X zK8F{=lm!E_?>A_QfJ^-VI5tmfpN8B~kUOGkESIB0F^L4`zZkn?}dTqcACh{V`LI*p-HOK51IACbx z!0;00r6)fiXCxLPLTsgu1A0dGn(-mO=-7d;W zP@)cVh0_~OOMmLHf6MN3kX^SD?O>o|4*QV_x1yD40&pqmq}FRT{tdWHBaK$oIB*k; zVE?(*PjygdPL+x{WgZMBh^R+?IQy1BXa5?fH4bTw8bL%;8~Rd;H5NPNk$$v7_f#()a93>#$0w+r;j?D0E0`;B9YSj*}&k*acBM8L7 lf&4wE68NtNn#1o5Dg^(gp)EaLgc~FXeH|0+LQMza{{zg!)iwYC literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/007.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/007.png new file mode 100644 index 0000000000000000000000000000000000000000..5bdb47a2617ffecf6d6a5fdf382ec82b3f8df455 GIT binary patch literal 7307 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/008.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/008.png new file mode 100644 index 0000000000000000000000000000000000000000..5736881e2e44eed1e52f6b134aa4d73397a80b58 GIT binary patch literal 5229 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/metadata.json b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/metadata.json new file mode 100644 index 0000000000..5147b8612d --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single line/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 9 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/000.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/000.png new file mode 100644 index 0000000000000000000000000000000000000000..af308bd7e6aa34ac6d5498e8443117bfec461dad GIT binary patch literal 6527 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/001.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/001.png new file mode 100644 index 0000000000000000000000000000000000000000..5bfb2d71e54dd54a95cb4dd2f1ea3339bc32c7ef GIT binary patch literal 6996 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 Q06U1=iW&-qH%;;X1^TyXlmGw# literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/002.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/002.png new file mode 100644 index 0000000000000000000000000000000000000000..609491054220428d2b22674d0512c7876779b433 GIT binary patch literal 4957 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/003.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/003.png new file mode 100644 index 0000000000000000000000000000000000000000..09291b10b02be95773c3add76600abedcd0db272 GIT binary patch literal 6522 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 Iq9y)+0R?VDasU7T literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/005.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/005.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5288bd132680b60b75826d3b55fd5c70296dea GIT binary patch literal 4971 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/006.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/006.png new file mode 100644 index 0000000000000000000000000000000000000000..2f71661d84e403510ea30e56fcdf3532b0c57dfa GIT binary patch literal 6524 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/008.png b/test/unit/visual/screenshots/WebGPU/Typography/textAlign/webgpu mode/all alignments with single word/008.png new file mode 100644 index 0000000000000000000000000000000000000000..d3b8189f30bc7b4bfcb5f8be82c0601f4586efb7 GIT binary patch literal 4976 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/metadata.json b/test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/metadata.json new file mode 100644 index 0000000000..01dd8f26ca --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Typography/textWeight/can control variable fonts from files in WebGPU/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 5 +} \ No newline at end of file From 39fb383e8dbc1d25a2c9c9661508a5a0e05f3c5c Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 10:17:28 -0500 Subject: [PATCH 73/98] Read backend off of renderer --- src/strands/p5.strands.js | 64 ++++++++++++------- src/webgl/p5.RendererGL.js | 2 + src/{strands => webgl}/strands_glslBackend.js | 6 +- 3 files changed, 45 insertions(+), 27 deletions(-) rename src/{strands => webgl}/strands_glslBackend.js (98%) diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index 2eee0bfb64..b55429dc72 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -1,18 +1,25 @@ /** -* @module 3D -* @submodule strands -* @for p5 -* @requires core -*/ -import { glslBackend } from './strands_glslBackend'; + * @module 3D + * @submodule strands + * @for p5 + * @requires core + */ -import { transpileStrandsToJS } from './strands_transpiler'; -import { BlockType } from './ir_types'; +import { transpileStrandsToJS } from "./strands_transpiler"; +import { BlockType } from "./ir_types"; -import { createDirectedAcyclicGraph } from './ir_dag' -import { createControlFlowGraph, createBasicBlock, pushBlock, popBlock } from './ir_cfg'; -import { generateShaderCode } from './strands_codegen'; -import { initGlobalStrandsAPI, createShaderHooksFunctions } from './strands_api'; +import { createDirectedAcyclicGraph } from "./ir_dag"; +import { + createControlFlowGraph, + createBasicBlock, + pushBlock, + popBlock, +} from "./ir_cfg"; +import { generateShaderCode } from "./strands_codegen"; +import { + initGlobalStrandsAPI, + createShaderHooksFunctions, +} from "./strands_api"; function strands(p5, fn) { ////////////////////////////////////////////// @@ -56,18 +63,20 @@ function strands(p5, fn) { const strandsContext = {}; initStrandsContext(strandsContext); - initGlobalStrandsAPI(p5, fn, strandsContext) + initGlobalStrandsAPI(p5, fn, strandsContext); ////////////////////////////////////////////// // Entry Point ////////////////////////////////////////////// const oldModify = p5.Shader.prototype.modify; - p5.Shader.prototype.modify = function(shaderModifier, scope = {}) { + p5.Shader.prototype.modify = function (shaderModifier, scope = {}) { if (shaderModifier instanceof Function) { // Reset the context object every time modify is called; // const backend = glslBackend; - initStrandsContext(strandsContext, glslBackend, { active: true }); + initStrandsContext(strandsContext, this._renderer.strandsBackend, { + active: true, + }); createShaderHooksFunctions(strandsContext, fn, this); // TODO: expose this, is internal for debugging for now. const options = { parser: true, srcLocations: false }; @@ -78,13 +87,21 @@ function strands(p5, fn) { // #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing // https://github.com/acornjs/acorn/issues/1385 const sourceString = `(${shaderModifier.toString()})`; - strandsCallback = transpileStrandsToJS(p5, sourceString, options.srcLocations, scope); + strandsCallback = transpileStrandsToJS( + p5, + sourceString, + options.srcLocations, + scope, + ); } else { strandsCallback = shaderModifier; } // 2. Build the IR from JavaScript API - const globalScope = createBasicBlock(strandsContext.cfg, BlockType.GLOBAL); + const globalScope = createBasicBlock( + strandsContext.cfg, + BlockType.GLOBAL, + ); pushBlock(strandsContext.cfg, globalScope); strandsCallback(); popBlock(strandsContext.cfg); @@ -98,17 +115,16 @@ function strands(p5, fn) { // Call modify with the generated hooks object return oldModify.call(this, hooksObject); + } else { + return oldModify.call(this, shaderModifier); } - else { - return oldModify.call(this, shaderModifier) - } - } + }; } export default strands; -if (typeof p5 !== 'undefined') { - p5.registerAddon(strands) +if (typeof p5 !== "undefined") { + p5.registerAddon(strands); } /* ------------------------------------------------------------- */ @@ -143,7 +159,7 @@ if (typeof p5 !== 'undefined') { * getWorldInputs(inputs => { * // Move the vertex up and down in a wave in world space * // In world space, moving the object (e.g., with translate()) will affect these coordinates -* // The sphere is ~50 units tall here, so 20 gives a noticeable wave + * // The sphere is ~50 units tall here, so 20 gives a noticeable wave * inputs.position.y += 20 * sin(t * 0.001 + inputs.position.x * 0.05); * return inputs; * }); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 07181bcf61..d84b4deb3c 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -15,6 +15,7 @@ import { MipmapTexture } from './p5.Texture'; import { Framebuffer } from './p5.Framebuffer'; import { RGB, RGBA } from '../color/creating_reading'; import { Image } from '../image/p5.Image'; +import { glslBackend } from './strands_glslBackend'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -125,6 +126,7 @@ class RendererGL extends Renderer3D { }; this._cachedBlendMode = undefined; + this.strandsBackend = glslBackend; } setupContext() { diff --git a/src/strands/strands_glslBackend.js b/src/webgl/strands_glslBackend.js similarity index 98% rename from src/strands/strands_glslBackend.js rename to src/webgl/strands_glslBackend.js index 178a08263c..5dc038df0d 100644 --- a/src/strands/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -1,6 +1,6 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "./ir_types"; -import { getNodeDataFromID, extractNodeTypeInfo } from "./ir_dag"; -import * as FES from './strands_FES' +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "../strands/ir_types"; +import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; +import * as FES from '../strands/strands_FES' function shouldCreateTemp(dag, nodeID) { const nodeType = dag.nodeTypes[nodeID]; if (nodeType !== NodeType.OPERATION) return false; From 7c592e25e1666c7e795dc90b7850cc4625c198ce Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 13:29:13 -0500 Subject: [PATCH 74/98] First pass of WebGPU backend --- src/strands/strands_codegen.js | 10 +- src/webgl/p5.Shader.js | 5 + src/webgl/strands_glslBackend.js | 7 + src/webgl/utils.js | 115 ++++++---- src/webgpu/p5.RendererWebGPU.js | 56 +++++ src/webgpu/strands_wgslBackend.js | 364 ++++++++++++++++++++++++++++++ 6 files changed, 502 insertions(+), 55 deletions(-) create mode 100644 src/webgpu/strands_wgslBackend.js diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 5441932076..39d9fbbe06 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -11,6 +11,7 @@ export function generateShaderCode(strandsContext) { const hooksObj = { uniforms: {}, + varyingVariables: [], }; for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { @@ -62,15 +63,14 @@ export function generateShaderCode(strandsContext) { if (strandsContext.sharedVariables) { for (const [varName, varInfo] of strandsContext.sharedVariables) { if (varInfo.usedInVertex && varInfo.usedInFragment) { - // Used in both shaders - declare as varying - vertexDeclarations.add(`OUT ${varInfo.typeInfo.fnName} ${varName};`); - fragmentDeclarations.add(`IN ${varInfo.typeInfo.fnName} ${varName};`); + // Used in both shaders - this is a true varying variable + hooksObj.varyingVariables.push(backend.generateVaryingVariable(varName, varInfo.typeInfo)); } else if (varInfo.usedInVertex) { // Only used in vertex shader - declare as local variable - vertexDeclarations.add(`${varInfo.typeInfo.fnName} ${varName};`); + vertexDeclarations.add(backend.generateLocalDeclaration(varName, varInfo.typeInfo)); } else if (varInfo.usedInFragment) { // Only used in fragment shader - declare as local variable - fragmentDeclarations.add(`${varInfo.typeInfo.fnName} ${varName};`); + fragmentDeclarations.add(backend.generateLocalDeclaration(varName, varInfo.typeInfo)); } // If not used anywhere, don't declare it } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 186b23ddd6..6c2fb27282 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -30,6 +30,9 @@ class Shader { // Stores custom uniform + helper declarations as a string. declarations: options.declarations, + // Stores an array of variable names + types passed between the vertex and fragment shader + varyingVariables: options.varyingVariables || [], + // Stores helper functions to prepend to shaders. helpers: options.helpers || {}, @@ -423,6 +426,7 @@ class Shader { for (const key in hooks) { if (key === 'declarations') continue; if (key === 'uniforms') continue; + if (key === 'varyingVariables') continue; if (key === 'vertexDeclarations') { newHooks.vertex.declarations = (newHooks.vertex.declarations || '') + '\n' + hooks[key]; @@ -452,6 +456,7 @@ class Shader { declarations: (this.hooks.declarations || '') + '\n' + (hooks.declarations || ''), uniforms: Object.assign({}, this.hooks.uniforms, hooks.uniforms || {}), + varyingVariables: (hooks.varyingVariables || []).concat(this.hooks.varyingVariables || []), fragment: Object.assign({}, this.hooks.fragment, newHooks.fragment || {}), vertex: Object.assign({}, this.hooks.vertex, newHooks.vertex || {}), helpers: Object.assign({}, this.hooks.helpers, newHooks.helpers || {}), diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index 5dc038df0d..d99a1ba5bf 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -178,6 +178,13 @@ export const glslBackend = { generateUniformDeclaration(name, typeInfo) { return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; }, + generateVaryingVariable(varName, typeInfo) { + return `${typeInfo.fnName} ${varName}`; + }, + generateLocalDeclaration(varName, typeInfo) { + const typeName = typeInfo.fnName; + return `${typeName} ${varName};`; + }, generateStatement(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); const semicolon = generationContext.suppressSemicolon ? '' : ';'; diff --git a/src/webgl/utils.js b/src/webgl/utils.js index 0727e91e1f..3d2701542e 100644 --- a/src/webgl/utils.js +++ b/src/webgl/utils.js @@ -1,5 +1,5 @@ -import * as constants from '../core/constants'; -import { Texture } from './p5.Texture'; +import * as constants from "../core/constants"; +import { Texture } from "./p5.Texture"; /** * @private @@ -26,7 +26,7 @@ export function readPixelsWebGL( height, format, type, - flipY + flipY, ) { // Record the currently bound framebuffer so we can go back to it after, and // bind the framebuffer we want to read from @@ -49,7 +49,7 @@ export function readPixelsWebGL( height, format, type, - pixels + pixels, ); // Re-bind whatever was previously bound @@ -103,13 +103,15 @@ export function readPixelWebGL(gl, framebuffer, x, y, format, type, flipY) { export function setWebGLTextureParams(texture, gl, webglVersion) { texture.bindTexture(); - const glMinFilter = texture.minFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; - const glMagFilter = texture.magFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; + const glMinFilter = + texture.minFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; + const glMagFilter = + texture.magFilter === constants.NEAREST ? gl.NEAREST : gl.LINEAR; // for webgl 1 we need to check if the texture is power of two // if it isn't we will set the wrap mode to CLAMP // webgl2 will support npot REPEAT and MIRROR but we don't check for it yet - const isPowerOfTwo = x => (x & (x - 1)) === 0; + const isPowerOfTwo = (x) => (x & (x - 1)) === 0; const textureData = texture._getTextureDataFromSource(); let wrapWidth; @@ -135,7 +137,7 @@ export function setWebGLTextureParams(texture, gl, webglVersion) { glWrapS = gl.REPEAT; } else { console.warn( - 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' + "You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapS = gl.CLAMP_TO_EDGE; } @@ -147,7 +149,7 @@ export function setWebGLTextureParams(texture, gl, webglVersion) { glWrapS = gl.MIRRORED_REPEAT; } else { console.warn( - 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' + "You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapS = gl.CLAMP_TO_EDGE; } @@ -164,7 +166,7 @@ export function setWebGLTextureParams(texture, gl, webglVersion) { glWrapT = gl.REPEAT; } else { console.warn( - 'You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead' + "You tried to set the wrap mode to REPEAT but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapT = gl.CLAMP_TO_EDGE; } @@ -176,7 +178,7 @@ export function setWebGLTextureParams(texture, gl, webglVersion) { glWrapT = gl.MIRRORED_REPEAT; } else { console.warn( - 'You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead' + "You tried to set the wrap mode to MIRROR but the texture size is not a power of two. Setting to CLAMP instead", ); glWrapT = gl.CLAMP_TO_EDGE; } @@ -267,16 +269,16 @@ export function setWebGLUniformValue(shader, uniform, data, getTexture, gl) { } break; case gl.SAMPLER_2D: - if (typeof data == 'number') { + if (typeof data == "number") { if ( data < gl.TEXTURE0 || data > gl.TEXTURE31 || data !== Math.ceil(data) ) { console.log( - '🌸 p5.js says: ' + + "🌸 p5.js says: " + "You're trying to use a number as the data for a texture." + - 'Please use a texture.' + "Please use a texture.", ); return this; } @@ -284,8 +286,7 @@ export function setWebGLUniformValue(shader, uniform, data, getTexture, gl) { gl.uniform1i(location, data); } else { gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - uniform.texture = - data instanceof Texture ? data : getTexture(data); + uniform.texture = data instanceof Texture ? data : getTexture(data); gl.uniform1i(location, uniform.samplerIndex); if (uniform.texture.src.gifProperties) { uniform.texture.src._animateGif(this._pInst); @@ -306,7 +307,7 @@ export function setWebGLUniformValue(shader, uniform, data, getTexture, gl) { case gl.UNSIGNED_INT_SAMPLER_3D: case gl.UNSIGNED_INT_SAMPLER_CUBE: case gl.UNSIGNED_INT_SAMPLER_2D_ARRAY: - if (typeof data !== 'number') { + if (typeof data !== "number") { break; } if ( @@ -315,9 +316,9 @@ export function setWebGLUniformValue(shader, uniform, data, getTexture, gl) { data !== Math.ceil(data) ) { console.log( - '🌸 p5.js says: ' + + "🌸 p5.js says: " + "You're trying to use a number as the data for a texture." + - 'Please use a texture.' + "Please use a texture.", ); break; } @@ -339,10 +340,7 @@ export function getWebGLUniformMetadata(shader, gl) { for (let i = 0; i < numUniforms; ++i) { const uniformInfo = gl.getActiveUniform(program, i); const uniform = {}; - uniform.location = gl.getUniformLocation( - program, - uniformInfo.name - ); + uniform.location = gl.getUniformLocation(program, uniformInfo.name); uniform.size = uniformInfo.size; let uniformName = uniformInfo.name; //uniforms that are arrays have their name returned as @@ -350,7 +348,7 @@ export function getWebGLUniformMetadata(shader, gl) { //off here. The size property tells us that its an array //so we dont lose any information by doing this if (uniformInfo.size > 1) { - uniformName = uniformName.substring(0, uniformName.indexOf('[0]')); + uniformName = uniformName.substring(0, uniformName.indexOf("[0]")); } uniform.name = uniformName; uniform.type = uniformInfo.type; @@ -383,7 +381,7 @@ export function getWebGLShaderAttributes(shader, gl) { const numAttributes = gl.getProgramParameter( shader._glProgram, - gl.ACTIVE_ATTRIBUTES + gl.ACTIVE_ATTRIBUTES, ); for (let i = 0; i < numAttributes; ++i) { const attributeInfo = gl.getActiveAttrib(shader._glProgram, i); @@ -402,28 +400,43 @@ export function getWebGLShaderAttributes(shader, gl) { } export function populateGLSLHooks(shader, src, shaderType) { - const main = 'void main'; + const main = "void main"; if (!src.includes(main)) return src; let [preMain, postMain] = src.split(main); - let hooks = ''; - let defines = ''; + let hooks = ""; + let defines = ""; for (const key in shader.hooks.uniforms) { hooks += `uniform ${key};\n`; } if (shader.hooks.declarations) { - hooks += shader.hooks.declarations + '\n'; + hooks += shader.hooks.declarations + "\n"; } if (shader.hooks[shaderType].declarations) { - hooks += shader.hooks[shaderType].declarations + '\n'; + hooks += shader.hooks[shaderType].declarations + "\n"; + } + + // Handle varying variables from p5.strands + if ( + shader.hooks.varyingVariables && + shader.hooks.varyingVariables.length > 0 + ) { + for (const varyingVar of shader.hooks.varyingVariables) { + // Generate OUT declaration for vertex shader, IN declaration for fragment shader + if (shaderType === "vertex") { + hooks += `OUT ${varyingVar};\n`; + } else if (shaderType === "fragment") { + hooks += `IN ${varyingVar};\n`; + } + } } for (const hookDef in shader.hooks.helpers) { hooks += `${hookDef}${shader.hooks.helpers[hookDef]}\n`; } for (const hookDef in shader.hooks[shaderType]) { - if (hookDef === 'declarations') continue; - const [hookType, hookName] = hookDef.split(' '); + if (hookDef === "declarations") continue; + const [hookType, hookName] = hookDef.split(" "); // Add a #define so that if the shader wants to use preprocessor directives to // optimize away the extra function calls in main, it can do so @@ -431,41 +444,43 @@ export function populateGLSLHooks(shader, src, shaderType) { shader.hooks.modified.vertex[hookDef] || shader.hooks.modified.fragment[hookDef] ) { - defines += '#define AUGMENTED_HOOK_' + hookName + '\n'; + defines += "#define AUGMENTED_HOOK_" + hookName + "\n"; } hooks += - hookType + ' HOOK_' + hookName + shader.hooks[shaderType][hookDef] + '\n'; + hookType + " HOOK_" + hookName + shader.hooks[shaderType][hookDef] + "\n"; } // Allow shaders to specify the location of hook #define statements. Normally these // go after function definitions, but one might want to have them defined earlier // in order to only conditionally make uniforms. - if (preMain.indexOf('#define HOOK_DEFINES') !== -1) { - preMain = preMain.replace('#define HOOK_DEFINES', '\n' + defines + '\n'); - defines = ''; + if (preMain.indexOf("#define HOOK_DEFINES") !== -1) { + preMain = preMain.replace("#define HOOK_DEFINES", "\n" + defines + "\n"); + defines = ""; } - return preMain + '\n' + defines + hooks + main + postMain; + return preMain + "\n" + defines + hooks + main + postMain; } export function checkWebGLCapabilities({ GL, webglVersion }) { const gl = GL; - const supportsFloat = webglVersion === constants.WEBGL2 - ? (gl.getExtension('EXT_color_buffer_float') && - gl.getExtension('EXT_float_blend')) - : gl.getExtension('OES_texture_float'); - const supportsFloatLinear = supportsFloat && - gl.getExtension('OES_texture_float_linear'); - const supportsHalfFloat = webglVersion === constants.WEBGL2 - ? gl.getExtension('EXT_color_buffer_float') - : gl.getExtension('OES_texture_half_float'); - const supportsHalfFloatLinear = supportsHalfFloat && - gl.getExtension('OES_texture_half_float_linear'); + const supportsFloat = + webglVersion === constants.WEBGL2 + ? gl.getExtension("EXT_color_buffer_float") && + gl.getExtension("EXT_float_blend") + : gl.getExtension("OES_texture_float"); + const supportsFloatLinear = + supportsFloat && gl.getExtension("OES_texture_float_linear"); + const supportsHalfFloat = + webglVersion === constants.WEBGL2 + ? gl.getExtension("EXT_color_buffer_float") + : gl.getExtension("OES_texture_half_float"); + const supportsHalfFloatLinear = + supportsHalfFloat && gl.getExtension("OES_texture_half_float_linear"); return { float: supportsFloat, floatLinear: supportsFloatLinear, halfFloat: supportsHalfFloat, - halfFloatLinear: supportsHalfFloatLinear + halfFloatLinear: supportsHalfFloatLinear, }; } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 2332a1d8f9..d8342ec860 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -12,6 +12,7 @@ import { materialVertexShader, materialFragmentShader } from './shaders/material import { fontVertexShader, fontFragmentShader } from './shaders/font'; import {Graphics} from "../core/p5.Graphics"; import {Element} from "../dom/p5.Element"; +import { wgslBackend } from './strands_wgslBackend'; const { lineDefs } = getStrokeDefs((n, v, t) => `const ${n}: ${t} = ${v};\n`); @@ -29,6 +30,7 @@ class RendererWebGPU extends Renderer3D { // Lazy readback texture for main canvas pixel reading this.canvasReadbackTexture = null; + this.strandsBackend = wgslBackend; } async setupContext() { @@ -1338,6 +1340,37 @@ class RendererWebGPU extends Renderer3D { } preMain = preMain.replace(/struct\s+Uniforms\s+\{/, `$&\n${uniforms}`); + // 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) { + const member = this.strandsBackend.generateVaryingDeclaration( + varyingVar.name, + varyingVar.typeInfo, + shaderType, + nextLocationIndex++ + ); + 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}}` + ); + } + } + let hooks = ''; let defines = ''; if (shader.hooks.declarations) { @@ -1376,6 +1409,29 @@ class RendererWebGPU extends Renderer3D { 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; + } + ////////////////////////////////////////////// // Buffer management for pixel reading ////////////////////////////////////////////// diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js new file mode 100644 index 0000000000..b2f5b3f675 --- /dev/null +++ b/src/webgpu/strands_wgslBackend.js @@ -0,0 +1,364 @@ +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "../strands/ir_types"; +import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; +import * as FES from '../strands/strands_FES' +function shouldCreateTemp(dag, nodeID) { + const nodeType = dag.nodeTypes[nodeID]; + if (nodeType !== NodeType.OPERATION) return false; + if (dag.baseTypes[nodeID] === BaseType.SAMPLER2D) return false; + const uses = dag.usedBy[nodeID] || []; + return uses.length > 1; +} +const TypeNames = { + 'float1': 'f32', + 'float2': 'vec2', + 'float3': 'vec3', + 'float4': 'vec4', + 'int1': 'i32', + 'int2': 'vec2', + 'int3': 'vec3', + 'int4': 'vec4', + 'bool1': 'bool', + 'bool2': 'vec2', + 'bool3': 'vec3', + 'bool4': 'vec4', + 'mat2': 'mat2x2', + 'mat3': 'mat3x3', + 'mat4': 'mat4x4', +} +const cfgHandlers = { + [BlockType.DEFAULT]: (blockID, strandsContext, generationContext) => { + const { dag, cfg } = strandsContext; + const instructions = cfg.blockInstructions[blockID] || []; + for (const nodeID of instructions) { + const nodeType = dag.nodeTypes[nodeID]; + if (shouldCreateTemp(dag, nodeID)) { + const declaration = wgslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (nodeType === NodeType.STATEMENT) { + wgslBackend.generateStatement(generationContext, dag, nodeID); + } + if (nodeType === NodeType.ASSIGNMENT) { + wgslBackend.generateAssignment(generationContext, dag, nodeID); + generationContext.visitedNodes.add(nodeID); + } + } + }, + [BlockType.BRANCH](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + // Find all phi nodes in this branch block and declare them + const blockInstructions = cfg.blockInstructions[blockID] || []; + for (const nodeID of blockInstructions) { + const node = getNodeDataFromID(dag, nodeID); + if (node.nodeType === NodeType.PHI) { + // Check if the phi node's first dependency already has a temp name + const dependsOn = node.dependsOn || []; + if (dependsOn.length > 0) { + const firstDependency = dependsOn[0]; + const existingTempName = generationContext.tempNames[firstDependency]; + if (existingTempName) { + // Reuse the existing temp name instead of creating a new one + generationContext.tempNames[nodeID] = existingTempName; + continue; // Skip declaration, just alias to existing variable + } + } + + // Otherwise, create a new temp variable for the phi node + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + const T = extractNodeTypeInfo(dag, nodeID); + const typeName = wgslBackend.getTypeName(T.baseType, T.dimension); + generationContext.write(`${typeName} ${tmp};`); + } + } + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, + [BlockType.IF_COND](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + const conditionID = cfg.blockConditions[blockID]; + const condExpr = wgslBackend.generateExpression(generationContext, dag, conditionID); + generationContext.write(`if (${condExpr})`); + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, + [BlockType.ELSE_COND](blockID, strandsContext, generationContext) { + generationContext.write(`else`); + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, + [BlockType.IF_BODY](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + this.assignPhiNodeValues(blockID, strandsContext, generationContext); + }, + [BlockType.SCOPE_START](blockID, strandsContext, generationContext) { + generationContext.write(`{`); + generationContext.indent++; + }, + [BlockType.SCOPE_END](blockID, strandsContext, generationContext) { + generationContext.indent--; + generationContext.write(`}`); + }, + [BlockType.MERGE](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, + [BlockType.FUNCTION](blockID, strandsContext, generationContext) { + this[BlockType.DEFAULT](blockID, strandsContext, generationContext); + }, + [BlockType.FOR](blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + const instructions = cfg.blockInstructions[blockID] || []; + + generationContext.write(`for (`); + + // Set flag to suppress semicolon on the last statement + const originalSuppressSemicolon = generationContext.suppressSemicolon; + + for (let i = 0; i < instructions.length; i++) { + const nodeID = instructions[i]; + const node = getNodeDataFromID(dag, nodeID); + const isLast = i === instructions.length - 1; + + // Suppress semicolon on the last statement + generationContext.suppressSemicolon = isLast; + + if (shouldCreateTemp(dag, nodeID)) { + const declaration = wgslBackend.generateDeclaration(generationContext, dag, nodeID); + generationContext.write(declaration); + } + if (node.nodeType === NodeType.STATEMENT) { + wgslBackend.generateStatement(generationContext, dag, nodeID); + } + if (node.nodeType === NodeType.ASSIGNMENT) { + wgslBackend.generateAssignment(generationContext, dag, nodeID); + generationContext.visitedNodes.add(nodeID); + } + } + + // Restore original flag + generationContext.suppressSemicolon = originalSuppressSemicolon; + + generationContext.write(`)`); + }, + assignPhiNodeValues(blockID, strandsContext, generationContext) { + const { dag, cfg } = strandsContext; + // Find all phi nodes that this block feeds into + const successors = cfg.outgoingEdges[blockID] || []; + for (const successorBlockID of successors) { + const instructions = cfg.blockInstructions[successorBlockID] || []; + for (const nodeID of instructions) { + const node = getNodeDataFromID(dag, nodeID); + if (node.nodeType === NodeType.PHI) { + // Find which input of this phi node corresponds to our block + const branchIndex = node.phiBlocks?.indexOf(blockID); + if (branchIndex !== -1 && branchIndex < node.dependsOn.length) { + const sourceNodeID = node.dependsOn[branchIndex]; + const tempName = generationContext.tempNames[nodeID]; + if (tempName && sourceNodeID !== null) { + const sourceExpr = wgslBackend.generateExpression(generationContext, dag, sourceNodeID); + generationContext.write(`${tempName} = ${sourceExpr};`); + } + } + } + } + } + }, +} +export const wgslBackend = { + hookEntry(hookType) { + const firstLine = `(${hookType.parameters.flatMap((param) => { + return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; + }).join(', ')}) {`; + return firstLine; + }, + getTypeName(baseType, dimension) { + const primitiveTypeName = TypeNames[baseType + dimension] + if (!primitiveTypeName) { + return baseType; + } + return primitiveTypeName; + }, + generateUniformDeclaration(name, typeInfo) { + return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; + }, + generateVaryingVariable(varName, typeInfo) { + const typeName = this.getTypeName(typeInfo.baseType, typeInfo.dimension); + return `${varName}: ${typeName}`; + }, + generateLocalDeclaration(varName, typeInfo) { + const typeName = this.getTypeName(typeInfo.baseType, typeInfo.dimension); + return `var ${varName}: ${typeName};`; + }, + generateStatement(generationContext, dag, nodeID) { + const node = getNodeDataFromID(dag, nodeID); + const semicolon = generationContext.suppressSemicolon ? '' : ';'; + if (node.statementType === StatementType.DISCARD) { + generationContext.write(`discard${semicolon}`); + } else if (node.statementType === StatementType.BREAK) { + generationContext.write(`break${semicolon}`); + } else if (node.statementType === StatementType.EXPRESSION) { + // Generate the expression followed by semicolon (unless suppressed) + const exprNodeID = node.dependsOn[0]; + const expr = this.generateExpression(generationContext, dag, exprNodeID); + generationContext.write(`${expr}${semicolon}`); + } else if (node.statementType === StatementType.EMPTY) { + // Generate just a semicolon (unless suppressed) + generationContext.write(semicolon); + } + }, + generateAssignment(generationContext, dag, nodeID) { + const node = getNodeDataFromID(dag, nodeID); + // dependsOn[0] = targetNodeID, dependsOn[1] = sourceNodeID + const targetNodeID = node.dependsOn[0]; + const sourceNodeID = node.dependsOn[1]; + + // Generate the target expression (could be variable or swizzle) + const targetExpr = this.generateExpression(generationContext, dag, targetNodeID); + const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + const semicolon = generationContext.suppressSemicolon ? '' : ';'; + + // Generate assignment if we have both target and source + if (targetExpr && sourceExpr && targetExpr !== sourceExpr) { + generationContext.write(`${targetExpr} = ${sourceExpr}${semicolon}`); + } + }, + generateDeclaration(generationContext, dag, nodeID) { + const expr = this.generateExpression(generationContext, dag, nodeID); + const tmp = `T${generationContext.nextTempID++}`; + generationContext.tempNames[nodeID] = tmp; + const T = extractNodeTypeInfo(dag, nodeID); + const typeName = this.getTypeName(T.baseType, T.dimension); + return `${typeName} ${tmp} = ${expr};`; + }, + generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { + const dag = strandsContext.dag; + const rootNode = getNodeDataFromID(dag, rootNodeID); + if (isStructType(rootNode.baseType)) { + const structTypeInfo = returnType; + for (let i = 0; i < structTypeInfo.properties.length; i++) { + const prop = structTypeInfo.properties[i]; + const val = this.generateExpression(generationContext, dag, rootNode.dependsOn[i]); + if (prop.name !== val) { + generationContext.write( + `${rootNode.identifier}.${prop.name} = ${val};` + ) + } + } + } + generationContext.write(`return ${this.generateExpression(generationContext, dag, rootNodeID)};`); + }, + generateExpression(generationContext, dag, nodeID) { + const node = getNodeDataFromID(dag, nodeID); + if (generationContext.tempNames?.[nodeID]) { + return generationContext.tempNames[nodeID]; + } + switch (node.nodeType) { + case NodeType.LITERAL: + if (node.baseType === BaseType.FLOAT) { + return node.value.toFixed(4); + } + else { + return node.value; + } + case NodeType.VARIABLE: + // Track shared variable usage context + if (generationContext.shaderContext && generationContext.strandsContext?.sharedVariables?.has(node.identifier)) { + const sharedVar = generationContext.strandsContext.sharedVariables.get(node.identifier); + if (generationContext.shaderContext === 'vertex') { + sharedVar.usedInVertex = true; + } else if (generationContext.shaderContext === 'fragment') { + sharedVar.usedInFragment = true; + } + } + return node.identifier; + case NodeType.OPERATION: + const useParantheses = node.usedBy.length > 0; + if (node.opCode === OpCode.Nary.CONSTRUCTOR) { + // TODO: differentiate casts and constructors for more efficient codegen. + // if (node.dependsOn.length === 1 && node.dimension === 1) { + // return this.generateExpression(generationContext, dag, node.dependsOn[0]); + // } + if (node.baseType === BaseType.SAMPLER2D) { + return this.generateExpression(generationContext, dag, node.dependsOn[0]); + } + const T = this.getTypeName(node.baseType, node.dimension); + const deps = node.dependsOn.map((dep) => this.generateExpression(generationContext, dag, dep)); + return `${T}(${deps.join(', ')})`; + } + if (node.opCode === OpCode.Nary.FUNCTION_CALL) { + const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); + return `${node.identifier}(${functionArgs.join(', ')})`; + } + if (node.opCode === OpCode.Binary.MEMBER_ACCESS) { + const [lID, rID] = node.dependsOn; + const lName = this.generateExpression(generationContext, dag, lID); + const rName = this.generateExpression(generationContext, dag, rID); + return `${lName}.${rName}`; + } + if (node.opCode === OpCode.Unary.SWIZZLE) { + const parentID = node.dependsOn[0]; + const parentExpr = this.generateExpression(generationContext, dag, parentID); + return `${parentExpr}.${node.swizzle}`; + } + if (node.dependsOn.length === 2) { + const [lID, rID] = node.dependsOn; + const left = this.generateExpression(generationContext, dag, lID); + const right = this.generateExpression(generationContext, dag, rID); + + // In WGSL, % operator works for both floats and integers + if (node.opCode === OpCode.Binary.MODULO) { + return `(${left} % ${right})`; + } + + const opSym = OpCodeToSymbol[node.opCode]; + if (useParantheses) { + return `(${left} ${opSym} ${right})`; + } else { + return `${left} ${opSym} ${right}`; + } + } + if (node.opCode === OpCode.Unary.LOGICAL_NOT + || node.opCode === OpCode.Unary.NEGATE + || node.opCode === OpCode.Unary.PLUS + ) { + const [i] = node.dependsOn; + const val = this.generateExpression(generationContext, dag, i); + const sym = OpCodeToSymbol[node.opCode]; + return `${sym}${val}`; + } + case NodeType.PHI: + // Phi nodes represent conditional merging of values + // If this phi node has an identifier (like varying variables), use that + if (node.identifier) { + return node.identifier; + } + // Otherwise, they should have been declared as temporary variables + // and assigned in the appropriate branches + if (generationContext.tempNames?.[nodeID]) { + return generationContext.tempNames[nodeID]; + } else { + // If no temp was created, this phi node only has one input + // so we can just use that directly + const validInputs = node.dependsOn.filter(id => id !== null); + if (validInputs.length > 0) { + return this.generateExpression(generationContext, dag, validInputs[0]); + } else { + throw new Error(`No valid inputs for node`) + // Fallback: create a default value + const typeName = this.getTypeName(node.baseType, node.dimension); + if (node.dimension === 1) { + return node.baseType === BaseType.FLOAT ? '0.0' : '0'; + } else { + return `${typeName}(0.0)`; + } + } + } + case NodeType.ASSIGNMENT: + FES.internalError(`ASSIGNMENT nodes should not be used as expressions`) + default: + FES.internalError(`${NodeTypeToName[node.nodeType]} code generation not implemented yet`) + } + }, + generateBlock(blockID, strandsContext, generationContext) { + const type = strandsContext.cfg.blockTypes[blockID]; + const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; + handler.call(cfgHandlers, blockID, strandsContext, generationContext); + } +} From 56c0bef50304d0068b6cf2df81dc3f6cd68c55a1 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 14:22:57 -0500 Subject: [PATCH 75/98] Refactor hook type system to be backend-agnostic --- src/strands/strands_api.js | 10 +- src/strands/strands_codegen.js | 12 +- src/webgl/p5.RendererGL.js | 75 +++ src/webgl/p5.Shader.js | 71 +-- src/webgpu/p5.RendererWebGPU.js | 120 ++++ test/unit/webgl/p5.Shader.js | 41 +- test/unit/webgpu/p5.Shader.js | 1059 +++++++++++++++++++++++++++++++ 7 files changed, 1308 insertions(+), 80 deletions(-) create mode 100644 test/unit/webgpu/p5.Shader.js diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index fb88413749..011e43c44e 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -266,7 +266,10 @@ function createHookArguments(strandsContext, parameters){ args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { - const typeInfo = TypeInfoFromGLSLName[param.type.typeName]; + if (!param.type.dataType) { + throw new Error(`Missing dataType for parameter ${param.name} of type ${param.type.typeName}`); + } + const typeInfo = param.type.dataType; const { id, dimension } = build.variableNode(strandsContext, typeInfo, param.name); const arg = createStrandsNode(id, dimension, strandsContext); args.push(arg); @@ -373,7 +376,10 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { } } else /*if(isNativeType(expectedReturnType.typeName))*/ { - const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; + if (!expectedReturnType.dataType) { + throw new Error(`Missing dataType for return type ${expectedReturnType.typeName}`); + } + const expectedTypeInfo = expectedReturnType.dataType; rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); } const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`; diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 39d9fbbe06..2705549a66 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -52,9 +52,15 @@ export function generateShaderCode(strandsContext) { strandsContext.globalAssignments = []; const firstLine = backend.hookEntry(hookType); - let returnType = hookType.returnType.properties - ? structType(hookType.returnType) - : TypeInfoFromGLSLName[hookType.returnType.typeName]; + let returnType; + if (hookType.returnType.properties) { + returnType = structType(hookType.returnType); + } else { + if (!hookType.returnType.dataType) { + throw new Error(`Missing dataType for return type ${hookType.returnType.typeName}`); + } + returnType = hookType.returnType.dataType; + } backend.generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType); hooksObj[`${hookType.returnType.typeName} ${hookType.name}`] = [firstLine, ...generationContext.codeLines, '}'].join('\n'); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index d84b4deb3c..6def0e10a1 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -16,6 +16,7 @@ import { Framebuffer } from './p5.Framebuffer'; import { RGB, RGBA } from '../color/creating_reading'; import { Image } from '../image/p5.Image'; import { glslBackend } from './strands_glslBackend'; +import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -1323,6 +1324,80 @@ class RendererGL extends Renderer3D { return populateGLSLHooks(shader, src, shaderType); } + getShaderHookTypes(shader, hookName) { + let fullSrc = shader._vertSrc; + let body = shader.hooks.vertex[hookName]; + if (!body) { + body = shader.hooks.fragment[hookName]; + fullSrc = shader._fragSrc; + } + 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 => { + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); + if (!structDefMatch) return undefined; + const properties = []; + for (const defSrc of structDefMatch[1].split(';')) { + // E.g. `int var1, var2;` or `MyStruct prop;` + const parts = defSrc.trim().split(/\s+|,/g); + const typeName = parts.shift(); + const names = [...parts]; + const typeProperties = structProperties(typeName); + for (const name of names) { + const dataType = TypeInfoFromGLSLName[typeName] || null; + properties.push({ + name, + type: { + typeName, + qualifiers: [], + properties: typeProperties, + dataType, + } + }); + } + } + return properties; + }; + const parameters = parameterMatch[1].split(',').map(paramString => { + // e.g. `int prop` or `in sampler2D prop` or `const float prop` + const parts = paramString.trim().split(/\s+/g); + const name = parts.pop(); + const typeName = parts.pop(); + const qualifiers = [...parts]; + const properties = structProperties(typeName); + const dataType = TypeInfoFromGLSLName[typeName] || null; + return { + name, + type: { + typeName, + qualifiers, + properties, + dataType, + } + }; + }); + const dataType = TypeInfoFromGLSLName[returnType] || null; + return { + name: functionName, + returnType: { + typeName: returnType, + qualifiers: returnQualifiers, + properties: structProperties(returnType), + dataType, + }, + parameters + }; + } + ////////////////////////////////////////////// // Framebuffer methods ////////////////////////////////////////////// diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 6c2fb27282..b0e7029d96 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -51,76 +51,7 @@ class Shader { } hookTypes(hookName) { - let fullSrc = this._vertSrc; - let body = this.hooks.vertex[hookName]; - if (!body) { - body = this.hooks.fragment[hookName]; - fullSrc = this._fragSrc; - } - 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 => { - const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); - if (!structDefMatch) return undefined; - - const properties = []; - for (const defSrc of structDefMatch[1].split(';')) { - // E.g. `int var1, var2;` or `MyStruct prop;` - const parts = defSrc.trim().split(/\s+|,/g); - const typeName = parts.shift(); - const names = [...parts]; - const typeProperties = structProperties(typeName); - for (const name of names) { - properties.push({ - name, - type: { - typeName, - qualifiers: [], - properties: typeProperties - } - }); - } - } - return properties; - }; - - const parameters = parameterMatch[1].split(',').map(paramString => { - // e.g. `int prop` or `in sampler2D prop` or `const float prop` - const parts = paramString.trim().split(/\s+/g); - const name = parts.pop(); - const typeName = parts.pop(); - const qualifiers = [...parts]; - const properties = structProperties(typeName); - return { - name, - type: { - typeName, - qualifiers, - properties - } - }; - }); - - return { - name: functionName, - returnType: { - typeName: returnType, - qualifiers: returnQualifiers, - properties: structProperties(returnType) - }, - parameters - }; + return this._renderer.getShaderHookTypes(this, hookName); } shaderSrc(src, shaderType) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index d8342ec860..0f8155c246 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -4,6 +4,7 @@ import { Texture } from '../webgl/p5.Texture'; import { Image } from '../image/p5.Image'; import { RGB, RGBA } from '../color/creating_reading'; import * as constants from '../core/constants'; +import { DataType } from '../strands/ir_types.js'; import { colorVertexShader, colorFragmentShader } from './shaders/color'; @@ -1432,6 +1433,125 @@ class RendererWebGPU extends Renderer3D { return maxLocation + 1; } + getShaderHookTypes(shader, hookName) { + // Create mapping from WGSL types to DataType entries + const wgslToDataType = { + 'f32': DataType.float1, + 'vec2': DataType.float2, + 'vec3': DataType.float3, + 'vec4': DataType.float4, + 'i32': DataType.int1, + 'vec2': DataType.int2, + 'vec3': DataType.int3, + 'vec4': DataType.int4, + 'bool': DataType.bool1, + 'vec2': DataType.bool2, + 'vec3': DataType.bool3, + 'vec4': DataType.bool4, + 'mat2x2': DataType.mat2, + 'mat3x3': DataType.mat3, + 'mat4x4': DataType.mat4, + 'texture_2d': DataType.sampler2D + }; + + let fullSrc = shader._vertSrc; + let body = shader.hooks.vertex[hookName]; + if (!body) { + body = shader.hooks.fragment[hookName]; + fullSrc = shader._fragSrc; + } + 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,") + 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 + 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" -> sampler2D DataType + if (typeName.includes('texture_2d')) { + typeName = 'texture_2d'; + } + + 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 ////////////////////////////////////////////// diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 7b06df3a44..6da4dd61e7 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -328,7 +328,13 @@ suite('p5.Shader', function() { returnType: { typeName: 'vec4', qualifiers: [], - properties: undefined + properties: undefined, + dataType: { + baseType: 'float', + dimension: 4, + fnName: 'vec4', + priority: 3 + } }, parameters: [ { @@ -336,13 +342,20 @@ suite('p5.Shader', function() { type: { typeName: 'FilterInputs', qualifiers: [], + dataType: null, properties: [ { name: 'texCoord', type: { typeName: 'vec2', qualifiers: [], - properties: undefined + properties: undefined, + dataType: { + baseType: 'float', + dimension: 2, + fnName: 'vec2', + priority: 3 + } } }, { @@ -350,7 +363,13 @@ suite('p5.Shader', function() { type: { typeName: 'vec2', qualifiers: [], - properties: undefined + properties: undefined, + dataType: { + baseType: 'float', + dimension: 2, + fnName: 'vec2', + priority: 3 + } } }, { @@ -358,7 +377,13 @@ suite('p5.Shader', function() { type: { typeName: 'vec2', qualifiers: [], - properties: undefined + properties: undefined, + dataType: { + baseType: 'float', + dimension: 2, + fnName: 'vec2', + priority: 3 + } } } ] @@ -369,7 +394,13 @@ suite('p5.Shader', function() { type: { typeName: 'sampler2D', qualifiers: ['in'], - properties: undefined + properties: undefined, + dataType: { + baseType: 'sampler2D', + dimension: 1, + fnName: 'sampler2D', + priority: -10 + } } } ] diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js new file mode 100644 index 0000000000..ebdedc8060 --- /dev/null +++ b/test/unit/webgpu/p5.Shader.js @@ -0,0 +1,1059 @@ +import p5 from '../../../src/app.js'; +import rendererWebGPU from "../../../src/webgpu/p5.RendererWebGPU"; + +p5.registerAddon(rendererWebGPU); + +suite('WebGPU p5.Shader', function() { + var myp5; + + beforeAll(function() { + window.IS_MINIFIED = true; + myp5 = new p5(function(p) { + p.setup = function() { + p.createCanvas(100, 100, 'webgpu'); + p.pointLight(250, 250, 250, 100, 100, 0); + p.ambientMaterial(250); + }; + }); + }); + + afterAll(function() { + myp5.remove(); + }); + + suite('p5.strands', () => { + test('does not break when arrays are in uniform callbacks', () => { + myp5.createCanvas(5, 5, 'webgpu'); + const myShader = myp5.baseMaterialShader().modify(() => { + const size = myp5.uniformVector2(() => [myp5.width, myp5.height]); + myp5.getPixelInputs(inputs => { + inputs.color = [ + size / 1000, + 0, + 1 + ]; + return inputs; + }); + }, { myp5 }); + expect(() => { + myp5.shader(myShader); + myp5.plane(myp5.width, myp5.height); + }).not.toThrowError(); + }); + + suite('if statement conditionals', () => { + test('handle simple if statement with true condition', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + if (condition > 0.5) { + color = myp5.float(1.0); // set to white in if branch + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is white (condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 + }); + + test('handle simple if statement with simpler assignment', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + myp5.getPixelInputs(inputs => { + let color = 1; // initial gray + if (condition > 0.5) { + color = 1; // set to white in if branch + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is white (condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 + }); + + test('handle simple if statement with false condition', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 0.0); // false condition + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + if (condition > 0.5) { + color = myp5.float(1.0); // set to white in if branch + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is gray (condition was false, original value kept) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 + }); + + test('handle if-else statement', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 0.0); // false condition + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + if (condition > 0.5) { + color = myp5.float(1.0); // white for true + } else { + color = myp5.float(0.0); // black for false + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is black (else branch executed) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~0 (black) + assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~0 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 + }); + + test('handle multiple variable assignments in if statement', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + myp5.getPixelInputs(inputs => { + let red = myp5.float(0.0); + let green = myp5.float(0.0); + let blue = myp5.float(0.0); + if (condition > 0.5) { + red = myp5.float(1.0); + green = myp5.float(0.5); + blue = myp5.float(0.0); + } + inputs.color = [red, green, blue, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 + }); + + test('handle modifications after if statement', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const condition = myp5.uniformFloat(() => 1.0); // true condition + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); // start with black + if (condition > 0.5) { + color = myp5.float(1.0); // set to white in if branch + } else { + color = myp5.float(0.5); // set to gray in else branch + } + // Modify the color after the if statement + color = color * 0.5; // Should result in 0.5 * 1.0 = 0.5 (gray) + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is gray (white * 0.5 = gray) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 + }); + + test('handle modifications after if statement in both branches', () => { + myp5.createCanvas(100, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const uv = inputs.texCoord; + const condition = uv.x > 0.5; // left half false, right half true + let color = myp5.float(0.0); + if (condition) { + color = myp5.float(1.0); // white on right side + } else { + color = myp5.float(0.8); // light gray on left side + } + // Multiply by 0.5 after the if statement + color = color * 0.5; + // Right side: 1.0 * 0.5 = 0.5 (medium gray) + // Left side: 0.8 * 0.5 = 0.4 (darker gray) + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check left side (false condition) + const leftPixel = myp5.get(25, 25); + assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102 + // Check right side (true condition) + const rightPixel = myp5.get(75, 25); + assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 + }); + + test('handle if-else-if chains', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const value = myp5.uniformFloat(() => 0.5); // middle value + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + if (value > 0.8) { + color = myp5.float(1.0); // white for high values + } else if (value > 0.3) { + color = myp5.float(0.5); // gray for medium values + } else { + color = myp5.float(0.0); // black for low values + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is gray (medium condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 + }); + + test('handle if-else-if chains in the else branch', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const value = myp5.uniformFloat(() => 0.2); // middle value + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + if (value > 0.8) { + color = myp5.float(1.0); // white for high values + } else if (value > 0.3) { + color = myp5.float(0.5); // gray for medium values + } else { + color = myp5.float(0.0); // black for low values + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is black (else condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 0, 5); + assert.approximately(pixelColor[1], 0, 5); + assert.approximately(pixelColor[2], 0, 5); + }); + + test('handle conditional assignment in if-else-if chains', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const val = myp5.uniformFloat(() => Math.PI * 8); + myp5.getPixelInputs(inputs => { + let shininess = 0 + let color = 0 + if (val > 5) { + const elevation = myp5.sin(val) + if (elevation > 0.4) { + shininess = 0; + } else if (elevation > 0.25) { + shininess = 30; + } else { + color = 1; + shininess = 100; + } + } else { + shininess += 25; + } + inputs.shininess = shininess; + inputs.color = [color, color, color, 1]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is 255 (hit nested else statement) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); + assert.approximately(pixelColor[1], 255, 5); + assert.approximately(pixelColor[2], 255, 5); + }); + + test('handle nested if statements', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const outerCondition = myp5.uniformFloat(() => 1.0); // true + const innerCondition = myp5.uniformFloat(() => 1.0); // true + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + if (outerCondition > 0.5) { + if (innerCondition > 0.5) { + color = myp5.float(1.0); // white for both conditions true + } else { + color = myp5.float(0.5); // gray for outer true, inner false + } + } else { + color = myp5.float(0.0); // black for outer false + } + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is white (both conditions were true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 + }); + + // Keep one direct API test for completeness + test('handle direct StrandsIf API usage', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const conditionValue = myp5.uniformFloat(() => 1.0); // true condition + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.5); // initial gray + const assignments = myp5.strandsIf( + conditionValue.greaterThan(0), + () => { + let tmp = color.copy(); + tmp = myp5.float(1.0); // set to white in if branch + return { color: tmp }; + } + ).Else(() => { + return { color: color }; // keep original in else branch + }); + color = assignments.color; + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is white (condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) + assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 + assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 + }); + + test('handle direct StrandsIf ElseIf API usage', () => { + myp5.createCanvas(50, 50, 'webgpu'); + const testShader = myp5.baseMaterialShader().modify(() => { + const value = myp5.uniformFloat(() => 0.5); // middle value + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); // initial black + const assignments = myp5.strandsIf( + value.greaterThan(0.8), + () => { + let tmp = color.copy(); + tmp = myp5.float(1.0); // white for high values + return { color: tmp }; + } + ).ElseIf( + value.greaterThan(0.3), + () => { + let tmp = color.copy(); + tmp = myp5.float(0.5); // gray for medium values + return { color: tmp }; + } + ).Else(() => { + let tmp = color.copy(); + tmp = myp5.float(0.0); // black for low values + return { color: tmp }; + }); + color = assignments.color; + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + // Check that the center pixel is gray (medium condition was true) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 + }); + }); + + suite('for loop statements', () => { + test('handle simple for loop with known iteration count', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let color = myp5.float(0.0); + + for (let i = 0; i < 3; i++) { + color = color + 0.1; + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle swizzle assignments in loops', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let color = [0, 0, 0, 1]; + + for (let i = 0; i < 3; i++) { + color.rgb += 0.1; + } + + inputs.color = color; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle for loop with variable as loop bound', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + const maxIterations = myp5.uniformInt(() => 2); + + myp5.getPixelInputs(inputs => { + let result = myp5.float(0.0); + + for (let i = 0; i < maxIterations; i++) { + result = result + 0.25; + } + + inputs.color = [result, result, result, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 2 times: 0.0 + 0.25 + 0.25 = 0.5 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 127, 5); + }); + + test('handle for loop modifying multiple variables', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let red = myp5.float(0.0); + let green = myp5.float(0.0); + + for (let i = 0; i < 4; i++) { + red = red + 0.125; // 4 * 0.125 = 0.5 + green = green + 0.25; // 4 * 0.25 = 1.0 + } + + inputs.color = [red, green, 0.0, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(pixelColor[1], 255, 5); // 1.0 * 255 = 255 + assert.approximately(pixelColor[2], 0, 5); // 0.0 * 255 = 0 + }); + + test('handle for loop with conditional inside', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let sum = myp5.float(0.0); + + for (let i = 0; i < 5; i++) { + if (i % 2 === 0) { + sum = sum + 0.1; // Add on even iterations: 0, 2, 4 + } + } + + inputs.color = [sum, sum, sum, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should add 0.1 three times (iterations 0, 2, 4): 3 * 0.1 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle nested for loops', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let total = myp5.float(0.0); + + for (let i = 0; i < 2; i++) { + for (let j = 0; j < 3; j++) { + total = total + 0.05; // 2 * 3 = 6 iterations + } + } + + inputs.color = [total, total, total, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should run 6 times: 6 * 0.05 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle complex nested for loops with multiple phi assignments', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let outerSum = myp5.float(0.0); + let globalCounter = myp5.float(0.0); + + // Outer for loop modifying multiple variables + for (let i = 0; i < 2; i++) { + let innerSum = myp5.float(0.0); + let localCounter = myp5.float(0.0); + + // Inner for loop also modifying multiple variables + for (let j = 0; j < 2; j++) { + innerSum = innerSum + 0.1; + localCounter = localCounter + 1.0; + globalCounter = globalCounter + 0.5; // This modifies outer scope + } + + // Complex state modification between loops involving all variables + innerSum = innerSum * localCounter; // 0.2 * 2.0 = 0.4 + outerSum = outerSum + innerSum; // Add to outer sum + globalCounter = globalCounter * 0.5; // Modify global again + } + + // Final result should be: 2 iterations * 0.4 = 0.8 for outerSum + // globalCounter: ((0 + 2*0.5)*0.5 + 2*0.5)*0.5 = ((1)*0.5 + 1)*0.5 = 1.5*0.5 = 0.75 + inputs.color = [outerSum, globalCounter, 0.0, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 204, 5); // 0.8 * 255 ≈ 204 + assert.approximately(pixelColor[1], 191, 5); // 0.75 * 255 ≈ 191 + assert.approximately(pixelColor[2], 0, 5); + }); + + test('handle nested for loops with state modification between loops', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let total = myp5.float(0.0); + + // Outer for loop + for (let i = 0; i < 2; i++) { + let innerSum = myp5.float(0.0); + + // Inner for loop + for (let j = 0; j < 3; j++) { + innerSum = innerSum + 0.1; // 3 * 0.1 = 0.3 per outer iteration + } + + // State modification between inner and outer loop + innerSum = innerSum * 0.5; // Multiply by 0.5: 0.3 * 0.5 = 0.15 + total = total + innerSum; // Add to total: 2 * 0.15 = 0.3 + } + + inputs.color = [total, total, total, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should be: 2 iterations * (3 * 0.1 * 0.5) = 2 * 0.15 = 0.3 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 + assert.approximately(pixelColor[1], 77, 5); + assert.approximately(pixelColor[2], 77, 5); + }); + + test('handle for loop using loop variable in calculations', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let sum = myp5.float(0.0); + + for (let i = 1; i <= 3; i++) { + sum = sum + (i * 0.1); // 1*0.1 + 2*0.1 + 3*0.1 = 0.6 + } + + inputs.color = [sum, sum, sum, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should be: 0.1 + 0.2 + 0.3 = 0.6 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 153, 5); // 0.6 * 255 ≈ 153 + assert.approximately(pixelColor[1], 153, 5); + assert.approximately(pixelColor[2], 153, 5); + }); + + // Keep one direct API test for completeness + test('handle direct StrandsFor API usage', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let accumulator = myp5.float(0.0); + + const loopResult = myp5.strandsFor( + () => 0, + (loopVar) => loopVar < 4, + (loopVar) => loopVar + 1, + (loopVar, vars) => { + let newValue = vars.accumulator.copy(); + newValue = newValue + 0.125; + return { accumulator: newValue }; + }, + { accumulator: accumulator.copy() }, + ); + + accumulator = loopResult.accumulator; + inputs.color = [accumulator, accumulator, accumulator, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should loop 4 times: 4 * 0.125 = 0.5 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(pixelColor[1], 127, 5); + assert.approximately(pixelColor[2], 127, 5); + }); + + test('handle for loop with break statement', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + let color = 0; + let maxIterations = 5; + + for (let i = 0; i < 100; i++) { + if (i >= maxIterations) { + break; + } + color = color + 0.1; + } + + inputs.color = [color, color, color, 1.0]; + return inputs; + }); + }, { myp5 }); + + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Should break after 5 iterations: 5 * 0.1 = 0.5 + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 + }); + }); + + suite('passing data between shaders', () => { + test('handle passing a value from a vertex hook to a fragment hook', () => { + myp5.createCanvas(50, 50, 'webgpu'); + myp5.pixelDensity(1); + + const testShader = myp5.baseMaterialShader().modify(() => { + let worldPos = myp5.varyingVec3(); + myp5.getWorldInputs((inputs) => { + worldPos = inputs.position.xyz; + return inputs; + }); + myp5.getFinalColor((c) => { + return [myp5.abs(worldPos / 25), 1]; + }); + }, { myp5 }); + + myp5.background(0, 0, 255); // Make the background blue to tell it apart + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // The middle should have position 0,0 which translates to black + const midColor = myp5.get(25, 25); + assert.approximately(midColor[0], 0, 5); + assert.approximately(midColor[1], 0, 5); + assert.approximately(midColor[2], 0, 5); + + // The corner should have position 1,1 which translates to yellow + const cornerColor = myp5.get(0, 0); + assert.approximately(cornerColor[0], 255, 5); + assert.approximately(cornerColor[1], 255, 5); + assert.approximately(cornerColor[2], 0, 5); + }); + + test('handle passing a value from a vertex hook to a fragment hook with swizzle assignment', () => { + myp5.createCanvas(50, 50, 'webgpu'); + myp5.pixelDensity(1); + + const testShader = myp5.baseMaterialShader().modify(() => { + let worldPos = myp5.varyingVec3(); + myp5.getWorldInputs((inputs) => { + worldPos.xyz = inputs.position.xyz; + return inputs; + }); + myp5.getFinalColor((c) => { + return [myp5.abs(worldPos / 25), 1]; + }); + }, { myp5 }); + + myp5.background(0, 0, 255); // Make the background blue to tell it apart + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // The middle should have position 0,0 which translates to black + const midColor = myp5.get(25, 25); + assert.approximately(midColor[0], 0, 5); + assert.approximately(midColor[1], 0, 5); + assert.approximately(midColor[2], 0, 5); + + // The corner should have position 1,1 which translates to yellow + const cornerColor = myp5.get(0, 0); + assert.approximately(cornerColor[0], 255, 5); + assert.approximately(cornerColor[1], 255, 5); + assert.approximately(cornerColor[2], 0, 5); + }); + + test('handle passing a value from a vertex hook to a fragment hook as part of hook output', () => { + myp5.createCanvas(50, 50, 'webgpu'); + myp5.pixelDensity(1); + + const testShader = myp5.baseMaterialShader().modify(() => { + let worldPos = myp5.varyingVec3(); + myp5.getWorldInputs((inputs) => { + worldPos = inputs.position.xyz; + inputs.position.xyz = worldPos + [25, 25, 0]; + return inputs; + }); + myp5.getFinalColor((c) => { + return [myp5.abs(worldPos / 25), 1]; + }); + }, { myp5 }); + + myp5.background(0, 0, 255); // Make the background blue to tell it apart + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // The middle (shifted +25,25) should have position 0,0 which translates to black + const midColor = myp5.get(49, 49); + assert.approximately(midColor[0], 0, 5); + assert.approximately(midColor[1], 0, 5); + assert.approximately(midColor[2], 0, 5); + + // The corner (shifted +25,25) should have position 1,1 which translates to yellow + const cornerColor = myp5.get(25, 25); + assert.approximately(cornerColor[0], 255, 5); + assert.approximately(cornerColor[1], 255, 5); + assert.approximately(cornerColor[2], 0, 5); + }); + + test('handle passing a value between fragment hooks only', () => { + myp5.createCanvas(50, 50, 'webgpu'); + myp5.pixelDensity(1); + + const testShader = myp5.baseMaterialShader().modify(() => { + let processedNormal = myp5.sharedVec3(); + myp5.getPixelInputs((inputs) => { + processedNormal = myp5.normalize(inputs.normal); + return inputs; + }); + myp5.getFinalColor((c) => { + // Use the processed normal to create a color - should be [0, 0, 1] for plane facing camera + return [myp5.abs(processedNormal), 1]; + }); + }, { myp5 }); + + myp5.background(255, 0, 0); // Red background to distinguish from result + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // Normal of plane facing camera should be [0, 0, 1], so color should be [0, 0, 255] + const centerColor = myp5.get(25, 25); + assert.approximately(centerColor[0], 0, 5); // Red component + assert.approximately(centerColor[1], 0, 5); // Green component + assert.approximately(centerColor[2], 255, 5); // Blue component + }); + + test('handle passing a value from a vertex hook to a fragment hook using shared*', () => { + myp5.createCanvas(50, 50, 'webgpu'); + myp5.pixelDensity(1); + + const testShader = myp5.baseMaterialShader().modify(() => { + let worldPos = myp5.sharedVec3(); + myp5.getWorldInputs((inputs) => { + worldPos = inputs.position.xyz; + return inputs; + }); + myp5.getFinalColor((c) => { + return [myp5.abs(worldPos / 25), 1]; + }); + }, { myp5 }); + + myp5.background(0, 0, 255); // Make the background blue to tell it apart + myp5.noStroke(); + myp5.shader(testShader); + myp5.plane(myp5.width, myp5.height); + + // The middle should have position 0,0 which translates to black + const midColor = myp5.get(25, 25); + assert.approximately(midColor[0], 0, 5); + assert.approximately(midColor[1], 0, 5); + assert.approximately(midColor[2], 0, 5); + + // The corner should have position 1,1 which translates to yellow + const cornerColor = myp5.get(0, 0); + assert.approximately(cornerColor[0], 255, 5); + assert.approximately(cornerColor[1], 255, 5); + assert.approximately(cornerColor[2], 0, 5); + }); + }); + + suite('filter shader hooks', () => { + test('handle getColor hook with non-struct return type', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor((inputs, canvasContent) => { + // Simple test - just return a constant color + return [1.0, 0.5, 0.0, 1.0]; // Orange color + }); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(0, 0, 255); // Blue background + + // Apply the filter + myp5.filter(testShader); + + // Check that the filter was applied (should be orange) + const pixelColor = myp5.get(25, 25); + assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 + assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 + assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 + }); + + test('simple vector multiplication in filter shader', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor((inputs, canvasContent) => { + // Test simple scalar * vector operation + const scalar = 0.5; + const vector = [1, 2]; + const result = scalar * vector; + return [result.x, result.y, 0, 1]; + }); + }, { myp5 }); + }); + + test('handle complex filter shader with for loop and vector operations', () => { + myp5.createCanvas(50, 50, 'webgpu'); + + const testShader = myp5.baseFilterShader().modify(() => { + const r = myp5.uniformFloat(() => 3); // Small value for testing + myp5.getColor((inputs, canvasContent) => { + let sum = [0, 0, 0, 0]; + let samples = 1; + + for (let i = 0; i < r; i++) { + samples++; + sum += myp5.texture(canvasContent, inputs.texCoord + (i / r) * [ + myp5.sin(4 * myp5.PI * i / r), + myp5.cos(4 * myp5.PI * i / r) + ]); + } + + return sum / samples; + }); + }, { myp5 }); + + // Create a simple scene to filter + myp5.background(255, 0, 0); // Red background + + // Apply the filter + myp5.filter(testShader); + + // The result should be some variation of the red background + const pixelColor = myp5.get(25, 25); + // Just verify it ran without crashing - exact color will depend on sampling + assert.isNumber(pixelColor[0]); + assert.isNumber(pixelColor[1]); + assert.isNumber(pixelColor[2]); + }); + }); + + suite('noise()', () => { + for (let i = 1; i <= 3; i++) { + test(`works with ${i}D vectors`, () => { + expect(() => { + myp5.createCanvas(50, 50, 'webgpu'); + const input = new Array(i).fill(10); + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor(() => { + return [myp5.noise(input), 0, 0, 1]; + }); + }, { myp5, input }); + myp5.shader(testShader); + myp5.plane(10, 10); + }).not.toThrowError(); + }); + + test(`works with ${i}D positional arguments`, () => { + expect(() => { + myp5.createCanvas(50, 50, 'webgpu'); + const input = new Array(i).fill(10); + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor(() => { + return [myp5.noise(...input), 0, 0, 1]; + }); + }, { myp5, input }); + myp5.shader(testShader); + myp5.plane(10, 10); + }).not.toThrowError(); + }); + } + + for (const i of [0, 4]) { + test(`Does not work in ${i}D`, () => { + expect(() => { + myp5.createCanvas(50, 50, 'webgpu'); + const input = new Array(i).fill(10); + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor(() => { + return [myp5.noise(input), 0, 0, 1]; + }); + }, { myp5, input }); + myp5.shader(testShader); + myp5.plane(10, 10); + }).toThrowError(); + }); + + test(`Does not work in ${i}D with positional arguments`, () => { + expect(() => { + myp5.createCanvas(50, 50, 'webgpu'); + const input = new Array(i).fill(10); + const testShader = myp5.baseFilterShader().modify(() => { + myp5.getColor(() => { + return [myp5.noise(...input), 0, 0, 1]; + }); + }, { myp5, input }); + myp5.shader(testShader); + myp5.plane(10, 10); + }).toThrowError(); + }); + } + }); + }); +}); \ No newline at end of file From 9ec22e7f3ded402975570185381faf89354412c3 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 16:26:03 -0500 Subject: [PATCH 76/98] Fix some issues with async, braceless ifs, and incorrect type objects --- src/strands/ir_types.js | 35 +---- src/strands/strands_api.js | 4 +- src/strands/strands_codegen.js | 4 +- src/strands/strands_for.js | 14 +- src/webgl/strands_glslBackend.js | 4 +- src/webgpu/p5.RendererWebGPU.js | 57 ++++---- src/webgpu/strands_wgslBackend.js | 38 +++-- test/unit/webgpu/p5.Shader.js | 235 +++++++++++++++--------------- 8 files changed, 196 insertions(+), 195 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 966e9d2ec2..1deae63a34 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -74,43 +74,16 @@ export const structType = function (hookType) { }; // TODO: handle struct properties that are themselves structs for (const prop of T.properties) { - const propType = TypeInfoFromGLSLName[prop.type.typeName]; + const propType = prop.type.dataType; structType.properties.push( {name: prop.name, dataType: propType } ); } return structType; }; -export function isStructType(typeName) { - return !isNativeType(typeName); -} -export function isNativeType(typeName) { - // Check if it's in DataType keys (internal names like 'float4') - if (Object.keys(DataType).includes(typeName)) { - return true; - } - - // Check if it's a GLSL type name (like 'vec4', 'float', etc.) - const glslNativeTypes = { - 'float': true, - 'vec2': true, - 'vec3': true, - 'vec4': true, - 'int': true, - 'ivec2': true, - 'ivec3': true, - 'ivec4': true, - 'bool': true, - 'bvec2': true, - 'bvec3': true, - 'bvec4': true, - 'mat2': true, - 'mat3': true, - 'mat4': true, - 'sampler2D': true - }; - - return !!glslNativeTypes[typeName]; +export function isStructType(typeInfo) { + if (typeInfo.baseType === 'Inputs') debugger + return !!(typeInfo && typeInfo.properties); } export const GenType = { FLOAT: { baseType: BaseType.FLOAT, dimension: null, priority: 3 }, diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 011e43c44e..fe518ef831 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -224,7 +224,7 @@ function createHookArguments(strandsContext, parameters){ const args = []; const dag = strandsContext.dag; for (const param of parameters) { - if(isStructType(param.type.typeName)) { + if(isStructType(param.type)) { const structTypeInfo = structType(param); const { id, dimension } = build.structInstanceNode(strandsContext, structTypeInfo, param.name, []); const structNode = createStrandsNode(id, dimension, strandsContext); @@ -338,7 +338,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { const userReturned = hookUserCallback(...args); const expectedReturnType = hookType.returnType; let rootNodeID = null; - if(isStructType(expectedReturnType.typeName)) { + if(isStructType(expectedReturnType)) { const expectedStructType = structType(expectedReturnType); if (userReturned instanceof StrandsNode) { const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); diff --git a/src/strands/strands_codegen.js b/src/strands/strands_codegen.js index 2705549a66..ef6c875af5 100644 --- a/src/strands/strands_codegen.js +++ b/src/strands/strands_codegen.js @@ -15,8 +15,8 @@ export function generateShaderCode(strandsContext) { }; for (const {name, typeInfo, defaultValue} of strandsContext.uniforms) { - const declaration = backend.generateUniformDeclaration(name, typeInfo); - hooksObj.uniforms[declaration] = defaultValue; + const key = backend.generateHookUniformKey(name, typeInfo); + hooksObj.uniforms[key] = defaultValue; } for (const { hookType, rootNodeID, entryBlockID, shaderContext } of strandsContext.hooks) { diff --git a/src/strands/strands_for.js b/src/strands/strands_for.js index a9f0749c44..76eb703cbf 100644 --- a/src/strands/strands_for.js +++ b/src/strands/strands_for.js @@ -246,8 +246,12 @@ export class StrandsFor { CFG.addEdge(cfg, breakCheckBlock, breakConditionBlock); cfg.blockConditions[breakConditionBlock] = negatedCondition.id; + // Add scope start block for break statement + const breakScopeStartBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_START); + CFG.addEdge(cfg, breakConditionBlock, breakScopeStartBlock); + const breakStatementBlock = CFG.createBasicBlock(cfg, BlockType.DEFAULT); - CFG.addEdge(cfg, breakConditionBlock, breakStatementBlock); + CFG.addEdge(cfg, breakScopeStartBlock, breakStatementBlock); // Create the break statement in the break statement block CFG.pushBlock(cfg, breakStatementBlock); @@ -261,8 +265,12 @@ export class StrandsFor { CFG.recordInBasicBlock(cfg, breakStatementBlock, breakStatementID); CFG.popBlock(cfg); - // The break statement block leads to the merge block (exits the loop) - CFG.addEdge(cfg, breakStatementBlock, mergeBlock); + // Add scope end block for break statement + const breakScopeEndBlock = CFG.createBasicBlock(cfg, BlockType.SCOPE_END); + CFG.addEdge(cfg, breakStatementBlock, breakScopeEndBlock); + + // The break scope end block leads to the merge block (exits the loop) + CFG.addEdge(cfg, breakScopeEndBlock, mergeBlock); CFG.popBlock(cfg); diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index d99a1ba5bf..0e6e7dd1e9 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -175,7 +175,7 @@ export const glslBackend = { } return primitiveTypeName; }, - generateUniformDeclaration(name, typeInfo) { + generateHookUniformKey(name, typeInfo) { return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; }, generateVaryingVariable(varName, typeInfo) { @@ -229,7 +229,7 @@ export const glslBackend = { generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); - if (isStructType(rootNode.baseType)) { + if (isStructType(returnType)) { const structTypeInfo = returnType; for (let i = 0; i < structTypeInfo.properties.length; i++) { const prop = structTypeInfo.properties[i]; diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 0f8155c246..4994f82fee 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1336,8 +1336,8 @@ class RendererWebGPU extends Renderer3D { let uniforms = ''; for (const key in shader.hooks.uniforms) { - const [type, name] = key.split(/\s+/); - uniforms += `${name}: ${type},\n`; + // WGSL format: "name: type" + uniforms += `${key},\n`; } preMain = preMain.replace(/struct\s+Uniforms\s+\{/, `$&\n${uniforms}`); @@ -1346,14 +1346,10 @@ class RendererWebGPU extends Renderer3D { // Generate struct members for varying variables let nextLocationIndex = this._getNextAvailableLocation(preMain, shaderType); let varyingMembers = ''; - + for (const varyingVar of shader.hooks.varyingVariables) { - const member = this.strandsBackend.generateVaryingDeclaration( - varyingVar.name, - varyingVar.typeInfo, - shaderType, - nextLocationIndex++ - ); + // varyingVar is a string like "varName: vec3" + const member = `@location(${nextLocationIndex++}) ${varyingVar},`; varyingMembers += member + '\n'; } @@ -1414,12 +1410,12 @@ class RendererWebGPU extends Renderer3D { // 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) { @@ -1429,7 +1425,7 @@ class RendererWebGPU extends Renderer3D { } } } - + return maxLocation + 1; } @@ -1437,12 +1433,15 @@ class RendererWebGPU extends Renderer3D { // Create mapping from WGSL types to DataType entries const wgslToDataType = { 'f32': DataType.float1, - 'vec2': DataType.float2, + 'vec2': DataType.float2, 'vec3': DataType.float3, 'vec4': DataType.float4, + 'vec2f': DataType.float2, + 'vec3f': DataType.float3, + 'vec4f': DataType.float4, 'i32': DataType.int1, 'vec2': DataType.int2, - 'vec3': DataType.int3, + 'vec3': DataType.int3, 'vec4': DataType.int4, 'bool': DataType.bool1, 'vec2': DataType.bool2, @@ -1453,7 +1452,7 @@ class RendererWebGPU extends Renderer3D { 'mat4x4': DataType.mat4, 'texture_2d': DataType.sampler2D }; - + let fullSrc = shader._vertSrc; let body = shader.hooks.vertex[hookName]; if (!body) { @@ -1471,28 +1470,28 @@ class RendererWebGPU extends Renderer3D { 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,") 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, @@ -1506,25 +1505,25 @@ class RendererWebGPU extends Renderer3D { } return properties; }; - + const parameters = parameterMatch[1].split(',').map(paramString => { // WGSL function parameters: name: type or name: binding 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" -> sampler2D DataType if (typeName.includes('texture_2d')) { typeName = 'texture_2d'; } - + const dataType = wgslToDataType[typeName] || null; - + const properties = structProperties(typeName); return { name, @@ -1536,10 +1535,10 @@ class RendererWebGPU extends Renderer3D { } }; }).filter(Boolean); - + // Convert WGSL return type to DataType const returnDataType = wgslToDataType[returnType] || null; - + return { name: functionName, returnType: { diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index b2f5b3f675..aad7b9f886 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -68,7 +68,9 @@ const cfgHandlers = { generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); const typeName = wgslBackend.getTypeName(T.baseType, T.dimension); - generationContext.write(`${typeName} ${tmp};`); + // Initialize with default value - WGSL requires initialization + const defaultValue = T.baseType === 'float' ? '0.0' : '0'; + generationContext.write(`var ${tmp}: ${typeName} = ${defaultValue};`); } } this[BlockType.DEFAULT](blockID, strandsContext, generationContext); @@ -163,10 +165,21 @@ const cfgHandlers = { } export const wgslBackend = { hookEntry(hookType) { - const firstLine = `(${hookType.parameters.flatMap((param) => { - return `${param.qualifiers?.length ? param.qualifiers.join(' ') : ''}${param.type.typeName} ${param.name}`; - }).join(', ')}) {`; - return firstLine; + const params = hookType.parameters.map((param) => { + // For struct types, use a raw prefix since we'll create a mutable copy + const paramName = param.type.properties ? `_p5_strands_raw_${param.name}` : param.name; + return `${paramName}: ${param.type.typeName}`; + }).join(', '); + + const firstLine = `(${params}) {`; + + // Generate mutable copies for struct parameters with original names + const mutableCopies = hookType.parameters + .filter(param => param.type.properties) // Only struct types + .map(param => ` var ${param.name} = _p5_strands_raw_${param.name};`) + .join('\n'); + + return mutableCopies ? firstLine + '\n' + mutableCopies : firstLine; }, getTypeName(baseType, dimension) { const primitiveTypeName = TypeNames[baseType + dimension] @@ -175,8 +188,8 @@ export const wgslBackend = { } return primitiveTypeName; }, - generateUniformDeclaration(name, typeInfo) { - return `${this.getTypeName(typeInfo.baseType, typeInfo.dimension)} ${name}`; + generateHookUniformKey(name, typeInfo) { + return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`; }, generateVaryingVariable(varName, typeInfo) { const typeName = this.getTypeName(typeInfo.baseType, typeInfo.dimension); @@ -225,12 +238,12 @@ export const wgslBackend = { generationContext.tempNames[nodeID] = tmp; const T = extractNodeTypeInfo(dag, nodeID); const typeName = this.getTypeName(T.baseType, T.dimension); - return `${typeName} ${tmp} = ${expr};`; + return `var ${tmp}: ${typeName} = ${expr};`; }, generateReturnStatement(strandsContext, generationContext, rootNodeID, returnType) { const dag = strandsContext.dag; const rootNode = getNodeDataFromID(dag, rootNodeID); - if (isStructType(rootNode.baseType)) { + if (isStructType(returnType)) { const structTypeInfo = returnType; for (let i = 0; i < structTypeInfo.properties.length; i++) { const prop = structTypeInfo.properties[i]; @@ -267,6 +280,13 @@ export const wgslBackend = { sharedVar.usedInFragment = true; } } + + // Check if this is a uniform variable + const isUniform = generationContext.strandsContext?.uniforms?.some(uniform => uniform.name === node.identifier); + if (isUniform) { + return `uniforms.${node.identifier}`; + } + return node.identifier; case NodeType.OPERATION: const useParantheses = node.usedBy.length > 0; diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index ebdedc8060..2171661e53 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -22,8 +22,8 @@ suite('WebGPU p5.Shader', function() { }); suite('p5.strands', () => { - test('does not break when arrays are in uniform callbacks', () => { - myp5.createCanvas(5, 5, 'webgpu'); + test('does not break when arrays are in uniform callbacks', async () => { + await myp5.createCanvas(5, 5, myp5.WEBGPU); const myShader = myp5.baseMaterialShader().modify(() => { const size = myp5.uniformVector2(() => [myp5.width, myp5.height]); myp5.getPixelInputs(inputs => { @@ -42,8 +42,8 @@ suite('WebGPU p5.Shader', function() { }); suite('if statement conditionals', () => { - test('handle simple if statement with true condition', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle simple if statement with true condition', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition myp5.getPixelInputs(inputs => { @@ -59,14 +59,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is white (condition was true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); - test('handle simple if statement with simpler assignment', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle simple if statement with simpler assignment', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition myp5.getPixelInputs(inputs => { @@ -82,14 +82,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is white (condition was true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); - test('handle simple if statement with false condition', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle simple if statement with false condition', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 0.0); // false condition myp5.getPixelInputs(inputs => { @@ -105,14 +105,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is gray (condition was false, original value kept) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - test('handle if-else statement', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle if-else statement', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 0.0); // false condition myp5.getPixelInputs(inputs => { @@ -130,14 +130,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is black (else branch executed) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 0, 5); // Red channel should be ~0 (black) assert.approximately(pixelColor[1], 0, 5); // Green channel should be ~0 assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); - test('handle multiple variable assignments in if statement', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle multiple variable assignments in if statement', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition myp5.getPixelInputs(inputs => { @@ -157,14 +157,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel has the expected color (red=1.0, green=0.5, blue=0.0) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 0, 5); // Blue channel should be ~0 }); - test('handle modifications after if statement', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle modifications after if statement', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const condition = myp5.uniformFloat(() => 1.0); // true condition myp5.getPixelInputs(inputs => { @@ -184,14 +184,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is gray (white * 0.5 = gray) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - test('handle modifications after if statement in both branches', () => { - myp5.createCanvas(100, 50, 'webgpu'); + test('handle modifications after if statement in both branches', async () => { + await myp5.createCanvas(100, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { const uv = inputs.texCoord; @@ -214,15 +214,15 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check left side (false condition) - const leftPixel = myp5.get(25, 25); + const leftPixel = await myp5.get(25, 25); assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102 // Check right side (true condition) - const rightPixel = myp5.get(75, 25); + const rightPixel = await myp5.get(75, 25); assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 }); - test('handle if-else-if chains', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle if-else-if chains', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const value = myp5.uniformFloat(() => 0.5); // middle value myp5.getPixelInputs(inputs => { @@ -242,14 +242,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is gray (medium condition was true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 }); - test('handle if-else-if chains in the else branch', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle if-else-if chains in the else branch', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const value = myp5.uniformFloat(() => 0.2); // middle value myp5.getPixelInputs(inputs => { @@ -269,14 +269,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is black (else condition was true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 0, 5); assert.approximately(pixelColor[1], 0, 5); assert.approximately(pixelColor[2], 0, 5); }); - test('handle conditional assignment in if-else-if chains', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle conditional assignment in if-else-if chains', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const val = myp5.uniformFloat(() => Math.PI * 8); myp5.getPixelInputs(inputs => { @@ -304,14 +304,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is 255 (hit nested else statement) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); assert.approximately(pixelColor[1], 255, 5); assert.approximately(pixelColor[2], 255, 5); }); - test('handle nested if statements', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle nested if statements', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const outerCondition = myp5.uniformFloat(() => 1.0); // true const innerCondition = myp5.uniformFloat(() => 1.0); // true @@ -334,15 +334,15 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is white (both conditions were true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); // Keep one direct API test for completeness - test('handle direct StrandsIf API usage', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle direct StrandsIf API usage', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const conditionValue = myp5.uniformFloat(() => 1.0); // true condition myp5.getPixelInputs(inputs => { @@ -366,14 +366,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is white (condition was true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 (white) assert.approximately(pixelColor[1], 255, 5); // Green channel should be 255 assert.approximately(pixelColor[2], 255, 5); // Blue channel should be 255 }); - test('handle direct StrandsIf ElseIf API usage', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle direct StrandsIf ElseIf API usage', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const value = myp5.uniformFloat(() => 0.5); // middle value myp5.getPixelInputs(inputs => { @@ -406,7 +406,7 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check that the center pixel is gray (medium condition was true) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // Red channel should be ~127 (gray) assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127 @@ -414,8 +414,8 @@ suite('WebGPU p5.Shader', function() { }); suite('for loop statements', () => { - test('handle simple for loop with known iteration count', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle simple for loop with known iteration count', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -435,14 +435,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 assert.approximately(pixelColor[1], 77, 5); assert.approximately(pixelColor[2], 77, 5); }); - test('handle swizzle assignments in loops', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle swizzle assignments in loops', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -462,14 +462,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should loop 3 times: 0.0 + 0.1 + 0.1 + 0.1 = 0.3 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 assert.approximately(pixelColor[1], 77, 5); assert.approximately(pixelColor[2], 77, 5); }); - test('handle for loop with variable as loop bound', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle for loop with variable as loop bound', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { const maxIterations = myp5.uniformInt(() => 2); @@ -491,14 +491,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should loop 2 times: 0.0 + 0.25 + 0.25 = 0.5 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 assert.approximately(pixelColor[1], 127, 5); assert.approximately(pixelColor[2], 127, 5); }); - test('handle for loop modifying multiple variables', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle for loop modifying multiple variables', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -519,14 +519,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 assert.approximately(pixelColor[1], 255, 5); // 1.0 * 255 = 255 assert.approximately(pixelColor[2], 0, 5); // 0.0 * 255 = 0 }); - test('handle for loop with conditional inside', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle for loop with conditional inside', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -548,14 +548,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should add 0.1 three times (iterations 0, 2, 4): 3 * 0.1 = 0.3 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 assert.approximately(pixelColor[1], 77, 5); assert.approximately(pixelColor[2], 77, 5); }); - test('handle nested for loops', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle nested for loops', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -577,14 +577,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should run 6 times: 6 * 0.05 = 0.3 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 assert.approximately(pixelColor[1], 77, 5); assert.approximately(pixelColor[2], 77, 5); }); - test('handle complex nested for loops with multiple phi assignments', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle complex nested for loops with multiple phi assignments', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -620,14 +620,14 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 204, 5); // 0.8 * 255 ≈ 204 assert.approximately(pixelColor[1], 191, 5); // 0.75 * 255 ≈ 191 assert.approximately(pixelColor[2], 0, 5); }); - test('handle nested for loops with state modification between loops', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle nested for loops with state modification between loops', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -657,14 +657,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should be: 2 iterations * (3 * 0.1 * 0.5) = 2 * 0.15 = 0.3 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 77, 5); // 0.3 * 255 ≈ 77 assert.approximately(pixelColor[1], 77, 5); assert.approximately(pixelColor[2], 77, 5); }); - test('handle for loop using loop variable in calculations', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle for loop using loop variable in calculations', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -684,15 +684,15 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should be: 0.1 + 0.2 + 0.3 = 0.6 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 153, 5); // 0.6 * 255 ≈ 153 assert.approximately(pixelColor[1], 153, 5); assert.approximately(pixelColor[2], 153, 5); }); // Keep one direct API test for completeness - test('handle direct StrandsFor API usage', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle direct StrandsFor API usage', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -721,14 +721,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should loop 4 times: 4 * 0.125 = 0.5 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 assert.approximately(pixelColor[1], 127, 5); assert.approximately(pixelColor[2], 127, 5); }); - test('handle for loop with break statement', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle for loop with break statement', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { @@ -752,14 +752,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Should break after 5 iterations: 5 * 0.1 = 0.5 - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 127, 5); // 0.5 * 255 ≈ 127 }); }); suite('passing data between shaders', () => { - test('handle passing a value from a vertex hook to a fragment hook', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test.only('handle passing a value from a vertex hook to a fragment hook', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { @@ -772,6 +772,7 @@ suite('WebGPU p5.Shader', function() { return [myp5.abs(worldPos / 25), 1]; }); }, { myp5 }); + console.log(testShader.vertSrc()) myp5.background(0, 0, 255); // Make the background blue to tell it apart myp5.noStroke(); @@ -779,20 +780,20 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // The middle should have position 0,0 which translates to black - const midColor = myp5.get(25, 25); + const midColor = await myp5.get(25, 25); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner should have position 1,1 which translates to yellow - const cornerColor = myp5.get(0, 0); + const cornerColor = await myp5.get(0, 0); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); }); - test('handle passing a value from a vertex hook to a fragment hook with swizzle assignment', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle passing a value from a vertex hook to a fragment hook with swizzle assignment', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { @@ -812,20 +813,20 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // The middle should have position 0,0 which translates to black - const midColor = myp5.get(25, 25); + const midColor = await myp5.get(25, 25); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner should have position 1,1 which translates to yellow - const cornerColor = myp5.get(0, 0); + const cornerColor = await myp5.get(0, 0); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); }); - test('handle passing a value from a vertex hook to a fragment hook as part of hook output', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle passing a value from a vertex hook to a fragment hook as part of hook output', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { @@ -846,20 +847,20 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // The middle (shifted +25,25) should have position 0,0 which translates to black - const midColor = myp5.get(49, 49); + const midColor = await myp5.get(49, 49); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner (shifted +25,25) should have position 1,1 which translates to yellow - const cornerColor = myp5.get(25, 25); + const cornerColor = await myp5.get(25, 25); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); }); - test('handle passing a value between fragment hooks only', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle passing a value between fragment hooks only', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { @@ -880,14 +881,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // Normal of plane facing camera should be [0, 0, 1], so color should be [0, 0, 255] - const centerColor = myp5.get(25, 25); + const centerColor = await myp5.get(25, 25); assert.approximately(centerColor[0], 0, 5); // Red component assert.approximately(centerColor[1], 0, 5); // Green component assert.approximately(centerColor[2], 255, 5); // Blue component }); - test('handle passing a value from a vertex hook to a fragment hook using shared*', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle passing a value from a vertex hook to a fragment hook using shared*', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { @@ -907,22 +908,22 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // The middle should have position 0,0 which translates to black - const midColor = myp5.get(25, 25); + const midColor = await myp5.get(25, 25); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner should have position 1,1 which translates to yellow - const cornerColor = myp5.get(0, 0); + const cornerColor = await myp5.get(0, 0); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); }); }); - suite('filter shader hooks', () => { - test('handle getColor hook with non-struct return type', () => { - myp5.createCanvas(50, 50, 'webgpu'); + suite.todo('filter shader hooks', () => { + test('handle getColor hook with non-struct return type', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor((inputs, canvasContent) => { @@ -938,14 +939,14 @@ suite('WebGPU p5.Shader', function() { myp5.filter(testShader); // Check that the filter was applied (should be orange) - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255 assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127 assert.approximately(pixelColor[2], 0, 5); // Blue channel should be 0 }); - test('simple vector multiplication in filter shader', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('simple vector multiplication in filter shader', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor((inputs, canvasContent) => { @@ -958,8 +959,8 @@ suite('WebGPU p5.Shader', function() { }, { myp5 }); }); - test('handle complex filter shader with for loop and vector operations', () => { - myp5.createCanvas(50, 50, 'webgpu'); + test('handle complex filter shader with for loop and vector operations', async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const testShader = myp5.baseFilterShader().modify(() => { const r = myp5.uniformFloat(() => 3); // Small value for testing @@ -986,7 +987,7 @@ suite('WebGPU p5.Shader', function() { myp5.filter(testShader); // The result should be some variation of the red background - const pixelColor = myp5.get(25, 25); + const pixelColor = await myp5.get(25, 25); // Just verify it ran without crashing - exact color will depend on sampling assert.isNumber(pixelColor[0]); assert.isNumber(pixelColor[1]); @@ -994,11 +995,11 @@ suite('WebGPU p5.Shader', function() { }); }); - suite('noise()', () => { + suite.todo('noise()', () => { for (let i = 1; i <= 3; i++) { - test(`works with ${i}D vectors`, () => { - expect(() => { - myp5.createCanvas(50, 50, 'webgpu'); + test(`works with ${i}D vectors`, async () => { + expect(async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor(() => { @@ -1010,9 +1011,9 @@ suite('WebGPU p5.Shader', function() { }).not.toThrowError(); }); - test(`works with ${i}D positional arguments`, () => { - expect(() => { - myp5.createCanvas(50, 50, 'webgpu'); + test(`works with ${i}D positional arguments`, async () => { + expect(async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor(() => { @@ -1026,9 +1027,9 @@ suite('WebGPU p5.Shader', function() { } for (const i of [0, 4]) { - test(`Does not work in ${i}D`, () => { - expect(() => { - myp5.createCanvas(50, 50, 'webgpu'); + test(`Does not work in ${i}D`, async () => { + expect(async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor(() => { @@ -1040,9 +1041,9 @@ suite('WebGPU p5.Shader', function() { }).toThrowError(); }); - test(`Does not work in ${i}D with positional arguments`, () => { - expect(() => { - myp5.createCanvas(50, 50, 'webgpu'); + test(`Does not work in ${i}D with positional arguments`, async () => { + expect(async () => { + await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { myp5.getColor(() => { @@ -1056,4 +1057,4 @@ suite('WebGPU p5.Shader', function() { } }); }); -}); \ No newline at end of file +}); From 9eee9aaeb30f7e610024d94410193e367a5bd884 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 17:06:00 -0500 Subject: [PATCH 77/98] Fix issues with swizzle assignment and updating uniforms --- src/webgl/p5.RendererGL.js | 3 ++ src/webgl/p5.Shader.js | 2 +- src/webgpu/p5.RendererWebGPU.js | 50 ++++++++++++++++++++++++++++ src/webgpu/strands_wgslBackend.js | 55 +++++++++++++++++++++++++++---- test/unit/webgpu/p5.Shader.js | 3 +- 5 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 6def0e10a1..d865b5017c 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1320,6 +1320,9 @@ class RendererGL extends Renderer3D { ////////////////////////////////////////////// // Shader hooks ////////////////////////////////////////////// + uniformNameFromHookKey(key) { + return key.slice(key.indexOf(' ') + 1); + } populateHooks(shader, src, shaderType) { return populateGLSLHooks(shader, src, shaderType); } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index b0e7029d96..cf138d89bc 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -432,7 +432,7 @@ class Shader { */ setDefaultUniforms() { for (const key in this.hooks.uniforms) { - const [, name] = key.split(' '); + const name = this._renderer.uniformNameFromHookKey(key); const initializer = this.hooks.uniforms[key]; let value; if (initializer instanceof Function) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 4994f82fee..bbf03294a1 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1300,6 +1300,9 @@ class RendererWebGPU extends Renderer3D { ////////////////////////////////////////////// // Shader hooks ////////////////////////////////////////////// + uniformNameFromHookKey(key) { + return key.slice(0, key.indexOf(':')); + } populateHooks(shader, src, shaderType) { if (!src.includes('fn main')) return src; @@ -1368,6 +1371,53 @@ class RendererWebGPU extends Renderer3D { } } + // 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" + const [varName, varType] = varyingVar.split(':').map(s => s.trim()); + varyingDeclarations += `var ${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`; + } + + // Find the input parameter name from the main function signature (anchored to start) + const inputMatch = postMain.match(/^\s*\((\w+):\s*\w+\)/); + if (inputMatch) { + const inputVarName = inputMatch[1]; + initStatements = initStatements.replace(/INPUT_VAR/g, inputVarName); + // Insert after the main function parameter but before any other code (anchored to start) + postMain = postMain.replace(/^(\s*\(\w+:\s*\w+\)\s*[^{]*\{)/, `$1\n${initStatements}`); + } + } + } + let hooks = ''; let defines = ''; if (shader.hooks.declarations) { diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index aad7b9f886..44326d7d60 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -197,7 +197,7 @@ export const wgslBackend = { }, generateLocalDeclaration(varName, typeInfo) { const typeName = this.getTypeName(typeInfo.baseType, typeInfo.dimension); - return `var ${varName}: ${typeName};`; + return `var ${varName}: ${typeName};`; }, generateStatement(generationContext, dag, nodeID) { const node = getNodeDataFromID(dag, nodeID); @@ -222,14 +222,55 @@ export const wgslBackend = { const targetNodeID = node.dependsOn[0]; const sourceNodeID = node.dependsOn[1]; - // Generate the target expression (could be variable or swizzle) - const targetExpr = this.generateExpression(generationContext, dag, targetNodeID); - const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + const targetNode = getNodeDataFromID(dag, targetNodeID); const semicolon = generationContext.suppressSemicolon ? '' : ';'; - // Generate assignment if we have both target and source - if (targetExpr && sourceExpr && targetExpr !== sourceExpr) { - generationContext.write(`${targetExpr} = ${sourceExpr}${semicolon}`); + // Check if target is a swizzle assignment + if (targetNode.opCode === OpCode.Unary.SWIZZLE) { + const parentID = targetNode.dependsOn[0]; + const parentNode = getNodeDataFromID(dag, parentID); + const parentExpr = this.generateExpression(generationContext, dag, parentID); + const swizzle = targetNode.swizzle; + const parentDimension = parentNode.dimension; + const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + + // Create an array for each element of the target variable + const componentMap = []; + for (let i = 0; i < parentDimension; i++) { + componentMap[i] = { target: 'self', index: i }; + } + + // Map swizzle characters to component indices + const getComponentIndex = (char) => { + if ('xyzw'.includes(char)) return 'xyzw'.indexOf(char); + if ('rgba'.includes(char)) return 'rgba'.indexOf(char); + return -1; + }; + + // Update the component map based on the swizzle assignment + for (let i = 0; i < swizzle.length; i++) { + const targetComponentIndex = getComponentIndex(swizzle[i]); + if (targetComponentIndex >= 0 && targetComponentIndex < parentDimension) { + componentMap[targetComponentIndex] = { target: 'rhs', index: i }; + } + } + + // Generate the reconstruction expression + const vectorTypeName = this.getTypeName(parentNode.baseType, parentDimension); + const components = componentMap.map(({ target, index }) => { + return `${target === 'self' ? parentExpr : sourceExpr}.${'xyzw'[index]}` + }); + + generationContext.write(`${parentExpr} = ${vectorTypeName}(${components.join(', ')})${semicolon}`); + } else { + // Regular assignment + const targetExpr = this.generateExpression(generationContext, dag, targetNodeID); + const sourceExpr = this.generateExpression(generationContext, dag, sourceNodeID); + + // Generate assignment if we have both target and source + if (targetExpr && sourceExpr && targetExpr !== sourceExpr) { + generationContext.write(`${targetExpr} = ${sourceExpr}${semicolon}`); + } } }, generateDeclaration(generationContext, dag, nodeID) { diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index 2171661e53..7e327f4df9 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -758,7 +758,7 @@ suite('WebGPU p5.Shader', function() { }); suite('passing data between shaders', () => { - test.only('handle passing a value from a vertex hook to a fragment hook', async () => { + test('handle passing a value from a vertex hook to a fragment hook', async () => { await myp5.createCanvas(50, 50, myp5.WEBGPU); myp5.pixelDensity(1); @@ -772,7 +772,6 @@ suite('WebGPU p5.Shader', function() { return [myp5.abs(worldPos / 25), 1]; }); }, { myp5 }); - console.log(testShader.vertSrc()) myp5.background(0, 0, 255); // Make the background blue to tell it apart myp5.noStroke(); From d8e08a473a94f8eb5754bc9d0194615bf3d588f9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 18:56:04 -0500 Subject: [PATCH 78/98] Fix more webgpu tests --- src/strands/ir_types.js | 1 - test/unit/webgpu/p5.Shader.js | 30 ++++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 1deae63a34..5906548d41 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -82,7 +82,6 @@ export const structType = function (hookType) { return structType; }; export function isStructType(typeInfo) { - if (typeInfo.baseType === 'Inputs') debugger return !!(typeInfo && typeInfo.properties); } export const GenType = { diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index 7e327f4df9..6cefbbf127 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -192,6 +192,7 @@ suite('WebGPU p5.Shader', function() { test('handle modifications after if statement in both branches', async () => { await myp5.createCanvas(100, 50, myp5.WEBGPU); + myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { const uv = inputs.texCoord; @@ -214,11 +215,12 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check left side (false condition) - const leftPixel = await myp5.get(25, 25); - assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102 + await myp5.loadPixels(); + const leftPixel = myp5.pixels[(25 * myp5.width + 25) * 4] + const rightPixel = myp5.pixels[(25 * myp5.width + 75) * 4] + assert.approximately(leftPixel, 102, 5); // 0.4 * 255 ≈ 102 // Check right side (true condition) - const rightPixel = await myp5.get(75, 25); - assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(rightPixel, 127, 5); // 0.5 * 255 ≈ 127 }); test('handle if-else-if chains', async () => { @@ -779,13 +781,14 @@ suite('WebGPU p5.Shader', function() { myp5.plane(myp5.width, myp5.height); // The middle should have position 0,0 which translates to black - const midColor = await myp5.get(25, 25); + await myp5.loadPixels(); + const midColor = myp5.pixels.slice((25 * myp5.width + 25) * 4, (25 * myp5.width + 25) * 4 + 4); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner should have position 1,1 which translates to yellow - const cornerColor = await myp5.get(0, 0); + const cornerColor = myp5.pixels.slice((0 * myp5.width + 0) * 4, (0 * myp5.width + 0) * 4 + 4); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); @@ -811,14 +814,15 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); + await myp5.loadPixels(); // The middle should have position 0,0 which translates to black - const midColor = await myp5.get(25, 25); + const midColor = await myp5.pixels.slice((25 * myp5.width + 25) * 4, (25 * myp5.width + 25) * 4 + 4); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner should have position 1,1 which translates to yellow - const cornerColor = await myp5.get(0, 0); + const cornerColor = await myp5.pixels.slice((0 * myp5.width + 0) * 4, (0 * myp5.width + 0) * 4 + 4); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); @@ -845,14 +849,15 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); + await myp5.loadPixels(); // The middle (shifted +25,25) should have position 0,0 which translates to black - const midColor = await myp5.get(49, 49); + const midColor = myp5.pixels.slice((49 * myp5.width + 49) * 4, (49 * myp5.width + 49) * 4 + 4); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner (shifted +25,25) should have position 1,1 which translates to yellow - const cornerColor = await myp5.get(25, 25); + const cornerColor = myp5.pixels.slice((25 * myp5.width + 25) * 4, (25 * myp5.width + 25) * 4 + 4); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); @@ -906,14 +911,15 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); + await myp5.loadPixels(); // The middle should have position 0,0 which translates to black - const midColor = await myp5.get(25, 25); + const midColor = myp5.pixels.slice((25 * myp5.width + 25) * 4, (25 * myp5.width + 25) * 4 + 4); assert.approximately(midColor[0], 0, 5); assert.approximately(midColor[1], 0, 5); assert.approximately(midColor[2], 0, 5); // The corner should have position 1,1 which translates to yellow - const cornerColor = await myp5.get(0, 0); + const cornerColor = myp5.pixels.slice((0 * myp5.width + 0) * 4, (0 * myp5.width + 0) * 4 + 4); assert.approximately(cornerColor[0], 255, 5); assert.approximately(cornerColor[1], 255, 5); assert.approximately(cornerColor[2], 0, 5); From 66fe4c03a7ac8fd053f4e07497c3c6565c9e08a4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 19:25:48 -0500 Subject: [PATCH 79/98] Get webgpu strands tests working --- src/strands/ir_types.js | 3 + src/strands/strands_api.js | 15 ++- src/type/p5.Font.js | 3 - src/webgl/p5.RendererGL.js | 5 + src/webgpu/p5.RendererWebGPU.js | 25 +++++ src/webgpu/shaders/filters/base.js | 71 +++++++++++++ src/webgpu/shaders/functions/noise3DWGSL.js | 106 ++++++++++++++++++++ test/unit/webgpu/p5.Shader.js | 6 +- 8 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 src/webgpu/shaders/filters/base.js create mode 100644 src/webgpu/shaders/functions/noise3DWGSL.js diff --git a/src/strands/ir_types.js b/src/strands/ir_types.js index 5906548d41..a80b1fb232 100644 --- a/src/strands/ir_types.js +++ b/src/strands/ir_types.js @@ -37,6 +37,7 @@ export const BaseType = { MAT: "mat", DEFER: "defer", SAMPLER2D: "sampler2D", + SAMPLER: "sampler", }; export const BasePriority = { [BaseType.FLOAT]: 3, @@ -45,6 +46,7 @@ export const BasePriority = { [BaseType.MAT]: 0, [BaseType.DEFER]: -1, [BaseType.SAMPLER2D]: -10, + [BaseType.SAMPLER]: -11, }; export const DataType = { float1: { fnName: "float", baseType: BaseType.FLOAT, dimension:1, priority: 3, }, @@ -64,6 +66,7 @@ export const DataType = { mat4: { fnName: "mat4x4", baseType: BaseType.MAT, dimension:4, priority: 0, }, defer: { fnName: null, baseType: BaseType.DEFER, dimension: null, priority: -1 }, sampler2D: { fnName: "sampler2D", baseType: BaseType.SAMPLER2D, dimension: 1, priority: -10 }, + sampler: { fnName: "sampler", baseType: BaseType.SAMPLER, dimension: 1, priority: -11 }, } export const structType = function (hookType) { let T = hookType.type === undefined ? hookType : hookType.type; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index fe518ef831..1ca341b375 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -18,7 +18,6 @@ import * as CFG from './ir_cfg' import * as FES from './strands_FES' import { getNodeDataFromID } from './ir_dag' import { StrandsNode, createStrandsNode } from './strands_node' -import noiseGLSL from '../webgl/shaders/functions/noise3DGLSL.glsl'; ////////////////////////////////////////////// // User nodes @@ -102,14 +101,16 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } - // Add GLSL noise. TODO: Replace this with a backend-agnostic implementation + // Add noise function with backend-agnostic implementation const originalNoise = fn.noise; fn.noise = function (...args) { if (!strandsContext.active) { return originalNoise.apply(this, args); // fallback to regular p5.js noise } - strandsContext.vertexDeclarations.add(noiseGLSL); - strandsContext.fragmentDeclarations.add(noiseGLSL); + // Get noise shader snippet from the current renderer + const noiseSnippet = this._renderer.getNoiseShaderSnippet(); + strandsContext.vertexDeclarations.add(noiseSnippet); + strandsContext.fragmentDeclarations.add(noiseSnippet); // Make each input into a strands node so that we can check their dimensions const strandsArgs = args.map(arg => p5.strandsNode(arg)); @@ -145,7 +146,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // variant or also one more directly translated from GLSL, or to be more compatible with // APIs we documented at the release of 2.x and have to continue supporting. for (const type in DataType) { - if (type === BaseType.DEFER) { + if (type === BaseType.DEFER || type === 'sampler') { continue; } const typeInfo = DataType[type]; @@ -266,6 +267,10 @@ function createHookArguments(strandsContext, parameters){ args.push(structNode); } else /*if(isNativeType(paramType.typeName))*/ { + // Skip sampler parameters - they don't need strands nodes + if (param.type.typeName === 'sampler') { + continue; + } if (!param.type.dataType) { throw new Error(`Missing dataType for parameter ${param.name} of type ${param.type.typeName}`); } diff --git a/src/type/p5.Font.js b/src/type/p5.Font.js index bb5fdeada3..26fbdd4996 100644 --- a/src/type/p5.Font.js +++ b/src/type/p5.Font.js @@ -1064,9 +1064,6 @@ async function create(pInst, name, path, descriptors, rawFont) { // ensure the font is ready to be rendered await document.fonts.ready; - // Await loading of the font via CSS in case it also loads other resources - await document.fonts.load(`1em "${name}"`); - // return a new p5.Font return new Font(pInst, face, name, path, rawFont); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index d865b5017c..51a0ef9338 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -17,6 +17,7 @@ import { RGB, RGBA } from '../color/creating_reading'; import { Image } from '../image/p5.Image'; import { glslBackend } from './strands_glslBackend'; import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; +import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -2003,6 +2004,10 @@ class RendererGL extends Renderer3D { this.bindFramebuffer(prevFramebuffer); } } + + getNoiseShaderSnippet() { + return noiseGLSL; + } } function rendererGL(p5, fn) { diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index bbf03294a1..e401914bf9 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -14,6 +14,8 @@ import { fontVertexShader, fontFragmentShader } from './shaders/font'; import {Graphics} from "../core/p5.Graphics"; import {Element} from "../dom/p5.Element"; import { wgslBackend } from './strands_wgslBackend'; +import noiseWGSL from './shaders/functions/noise3DWGSL'; +import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base'; const { lineDefs } = getStrokeDefs((n, v, t) => `const ${n}: ${t} = ${v};\n`); @@ -2244,6 +2246,29 @@ class RendererWebGPU extends Renderer3D { stagingBuffer.unmap(); return region; } + + getNoiseShaderSnippet() { + return noiseWGSL; + } + + baseFilterShader() { + if (!this._baseFilterShader) { + this._baseFilterShader = new Shader( + this, + baseFilterVertexShader, + baseFilterFragmentShader, + { + vertex: {}, + fragment: { + "vec4 getColor": `(inputs: FilterInputs, tex: texture_2d, texSampler: sampler) -> vec4 { + return textureSample(tex, texSampler, inputs.texCoord); + }`, + }, + } + ); + } + return this._baseFilterShader; + } } function rendererWebGPU(p5, fn) { diff --git a/src/webgpu/shaders/filters/base.js b/src/webgpu/shaders/filters/base.js new file mode 100644 index 0000000000..cd9ee59319 --- /dev/null +++ b/src/webgpu/shaders/filters/base.js @@ -0,0 +1,71 @@ +const filterUniforms = ` +struct FilterUniforms { + uModelViewMatrix: mat4x4, + uProjectionMatrix: mat4x4, + canvasSize: vec2, + texelSize: vec2, +} + +@group(0) @binding(0) var uniforms: FilterUniforms; +@group(0) @binding(1) var tex0: texture_2d; +@group(0) @binding(2) var tex0_sampler: sampler; +`; + +export const baseFilterVertexShader = filterUniforms + ` +struct VertexInput { + @location(0) aPosition: vec3, + @location(1) aTexCoord: vec2, +} + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) vTexCoord: vec2, +} + +@vertex +fn main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + // transferring texcoords for the frag shader + output.vTexCoord = input.aTexCoord; + + // copy position with a fourth coordinate for projection (1.0 is normal) + let positionVec4 = vec4(input.aPosition, 1.0); + + // project to 3D space + output.position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4; + + return output; +} +`; + +export const baseFilterFragmentShader = filterUniforms + ` +struct FilterInputs { + texCoord: vec2, + canvasSize: vec2, + texelSize: vec2, +} + +struct FragmentInput { + @location(0) vTexCoord: vec2, +} + +struct FragmentOutput { + @location(0) color: vec4, +} + +@fragment +fn main(input: FragmentInput) -> FragmentOutput { + var output: FragmentOutput; + var inputs: FilterInputs; + inputs.texCoord = input.vTexCoord; + inputs.canvasSize = uniforms.canvasSize; + inputs.texelSize = uniforms.texelSize; + + var outColor = HOOK_getColor(inputs, tex0, tex0_sampler); + outColor = vec4(outColor.rgb * outColor.a, outColor.a); + output.color = outColor; + + return output; +} +`; \ No newline at end of file diff --git a/src/webgpu/shaders/functions/noise3DWGSL.js b/src/webgpu/shaders/functions/noise3DWGSL.js new file mode 100644 index 0000000000..d21f8e80b3 --- /dev/null +++ b/src/webgpu/shaders/functions/noise3DWGSL.js @@ -0,0 +1,106 @@ +// Based on https://github.com/stegu/webgl-noise/blob/22434e04d7753f7e949e8d724ab3da2864c17a0f/src/noise3D.glsl +// MIT licensed, adapted for p5.strands and converted to WGSL + +export default `fn mod289Vec3(x: vec3) -> vec3 { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +fn mod289Vec4(x: vec4) -> vec4 { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +fn permute(x: vec4) -> vec4 { + return mod289Vec4(((x*34.0)+10.0)*x); +} + +fn taylorInvSqrt(r: vec4) -> vec4 { + return vec4(1.79284291400159) - vec4(0.85373472095314) * r; +} + +fn baseNoise(v: vec3) -> f32 { + let C = vec2(1.0/6.0, 1.0/3.0); + let D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + var i = floor(v + dot(v, C.yyy)); + let x0 = v - i + dot(i, C.xxx); + + // Other corners + let g = step(x0.yzx, x0.xyz); + let l = vec3(1.0) - g; + let i1 = min(g.xyz, l.zxy); + let i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0.0 + 0.0 * C.xxx; + // x1 = x0 - i1 + 1.0 * C.xxx; + // x2 = x0 - i2 + 2.0 * C.xxx; + // x3 = x0 - 1.0 + 3.0 * C.xxx; + let x1 = x0 - i1 + C.xxx; + let x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y + let x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y + + // Permutations + i = mod289Vec3(i); + let p = permute( permute( permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); + + // Gradients: 7x7 points over a square, mapped onto an octahedron. + // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) + let n_ = 0.142857142857; // 1.0/7.0 + let ns = n_ * D.wyz - D.xzx; + + let j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) + + let x_ = floor(j * ns.z); + let y_ = floor(j - 7.0 * x_ ); // mod(j,N) + + let x = x_ *ns.x + ns.yyyy; + let y = y_ *ns.x + ns.yyyy; + let h = vec4(1.0) - abs(x) - abs(y); + + let b0 = vec4( x.xy, y.xy ); + let b1 = vec4( x.zw, y.zw ); + + //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; + //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; + let s0 = floor(b0)*2.0 + vec4(1.0); + let s1 = floor(b1)*2.0 + vec4(1.0); + let sh = -step(h, vec4(0.0)); + + let a0 = b0.xzyw + s0.xzyw*sh.xxyy; + let a1 = b1.xzyw + s1.xzyw*sh.zzww; + + let p0 = vec3(a0.xy, h.x); + let p1 = vec3(a0.zw, h.y); + let p2 = vec3(a1.xy, h.z); + let p3 = vec3(a1.zw, h.w); + + //Normalise gradients + let norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); + let p0_norm = p0 * norm.x; + let p1_norm = p1 * norm.y; + let p2_norm = p2 * norm.z; + let p3_norm = p3 * norm.w; + + // Mix final noise value + var m = max(vec4(0.5) - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), vec4(0.0)); + m = m * m; + return 105.0 * dot( m*m, vec4( dot(p0_norm,x0), dot(p1_norm,x1), + dot(p2_norm,x2), dot(p3_norm,x3) ) ); +} + +fn noise(st: vec3) -> f32 { + var result = 0.0; + var amplitude = 1.0; + var frequency = 1.0; + + for (var i = 0; i < 4; i++) { + result += amplitude * baseNoise(st * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + + return result; +}`; \ No newline at end of file diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index 6cefbbf127..6dd28b29f9 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -1000,7 +1000,7 @@ suite('WebGPU p5.Shader', function() { }); }); - suite.todo('noise()', () => { + suite('noise()', () => { for (let i = 1; i <= 3; i++) { test(`works with ${i}D vectors`, async () => { expect(async () => { @@ -1043,7 +1043,7 @@ suite('WebGPU p5.Shader', function() { }, { myp5, input }); myp5.shader(testShader); myp5.plane(10, 10); - }).toThrowError(); + }).rejects.toThrowError(); }); test(`Does not work in ${i}D with positional arguments`, async () => { @@ -1057,7 +1057,7 @@ suite('WebGPU p5.Shader', function() { }, { myp5, input }); myp5.shader(testShader); myp5.plane(10, 10); - }).toThrowError(); + }).rejects.toThrowError(); }); } }); From 7bcfa7623f9be6b9410c5be0a9d00a5354fcb9b9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 19:31:52 -0500 Subject: [PATCH 80/98] end to end preview sketch --- preview/index.html | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/preview/index.html b/preview/index.html index ba1f0bd282..b8d204972e 100644 --- a/preview/index.html +++ b/preview/index.html @@ -53,7 +53,7 @@ p.image(tex, 0, 0, p.width, p.height); }); - sh = p.baseMaterialShader().modify({ + /*sh = p.baseMaterialShader().modify({ uniforms: { 'f32 time': () => p.millis(), }, @@ -62,7 +62,14 @@ result.position.y += 40.0 * sin(uniforms.time * 0.005); return result; }`, - }) + })*/ + sh = p.baseMaterialShader().modify(() => { + const time = p.uniformFloat(() => p.millis()); + p.getWorldInputs((inputs) => { + inputs.position.y += 40 * p.sin(time * 0.005); + return inputs; + }); + }, { p }) /*ssh = p.baseStrokeShader().modify({ uniforms: { 'f32 time': () => p.millis(), @@ -77,7 +84,7 @@ p.draw = function () { p.clear(); - p.orbitControl(); + /*p.orbitControl(); p.push(); p.textAlign(p.CENTER, p.CENTER); p.textFont(font); @@ -91,7 +98,7 @@ p.text('Hello!', 0, 0); p.pop() p.pop(); - return; + return;*/ p.orbitControl(); const t = p.millis() * 0.002; p.background(200); From fee47a099e3752d3d463c2ac50ddd882aca31115 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sat, 22 Nov 2025 20:25:49 -0500 Subject: [PATCH 81/98] Start using buffer pooling --- preview/index.html | 12 ++- src/core/p5.Renderer.js | 5 + src/core/structure.js | 4 + src/webgpu/p5.RendererWebGPU.js | 163 +++++++++++++++++++++++++------- 4 files changed, 146 insertions(+), 38 deletions(-) diff --git a/preview/index.html b/preview/index.html index b8d204972e..dc1ace4e32 100644 --- a/preview/index.html +++ b/preview/index.html @@ -36,7 +36,7 @@ ); fbo = p.createFramebuffer(); - tex = p.createImage(100, 100); + /*tex = p.createImage(100, 100); tex.loadPixels(); for (let x = 0; x < tex.width; x++) { for (let y = 0; y < tex.height; y++) { @@ -51,7 +51,7 @@ fbo.draw(() => { p.imageMode(p.CENTER); p.image(tex, 0, 0, p.width, p.height); - }); + });*/ /*sh = p.baseMaterialShader().modify({ uniforms: { @@ -119,8 +119,12 @@ 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), ) - p.texture(fbo) - p.sphere(30); + // p.texture(fbo) + if (i % 2 === 0) { + p.box(30); + } else { + p.sphere(30); + } p.pop(); } }; diff --git a/src/core/p5.Renderer.js b/src/core/p5.Renderer.js index 75d01bc04c..fd6fdb4ff8 100644 --- a/src/core/p5.Renderer.js +++ b/src/core/p5.Renderer.js @@ -383,6 +383,11 @@ class Renderer { return this; } + finishDraw() { + // Default no-op implementation + // Override in specific renderers as needed + } + }; function renderer(p5, fn){ diff --git a/src/core/structure.js b/src/core/structure.js index 03f86cf6ec..f0c75879c9 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -385,6 +385,10 @@ function structure(p5, fn){ } await this._runLifecycleHook('postdraw'); } + // Finish drawing - submit any pending GPU work (WebGPU specific) + if (this._renderer && typeof this._renderer.finishDraw === 'function') { + this._renderer.finishDraw(); + } } }; diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index e401914bf9..e3ff7cc270 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -34,6 +34,13 @@ class RendererWebGPU extends Renderer3D { // Lazy readback texture for main canvas pixel reading this.canvasReadbackTexture = null; this.strandsBackend = wgslBackend; + + // Registry to track all shaders for uniform data pooling + this._shadersWithPools = []; + + // Flag to track if any draws have happened that need queue submission + this._hasPendingDraws = false; + this._pendingCommandEncoders = []; } async setupContext() { @@ -200,7 +207,8 @@ class RendererWebGPU extends Renderer3D { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.end(); - this.queue.submit([commandEncoder.finish()]); + this._pendingCommandEncoders.push(commandEncoder.finish()); + this._hasPendingDraws = true; } /** @@ -241,7 +249,8 @@ class RendererWebGPU extends Renderer3D { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.end(); - this.queue.submit([commandEncoder.finish()]); + this._pendingCommandEncoders.push(commandEncoder.finish()); + this._hasPendingDraws = true; } _prepareBuffer(renderBuffer, geometry, shader) { @@ -439,10 +448,34 @@ class RendererWebGPU extends Renderer3D { shader._uniformData = new Float32Array(alignedSize / 4); shader._uniformDataView = new DataView(shader._uniformData.buffer); - shader._uniformBuffer = this.device.createBuffer({ + // Create pools for uniform buffers (both GPU buffers and data arrays.) This + // is so that we can queue up multiple things to be able to be drawn and have + // the GPU go through them as fast as possible. If we're overwriting the same + // data again and again, we would have to wait for the GPU after each primitive + // that we draw. + shader._uniformBufferPool = []; + shader._uniformBuffersInUse = []; + shader._uniformBufferSize = alignedSize; + + // Create the first buffer for the pool + const firstGPUBuffer = this.device.createBuffer({ size: alignedSize, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); + const firstData = new Float32Array(alignedSize / 4); + const firstDataView = new DataView(firstData.buffer); + + shader._uniformBufferPool.push({ + buffer: firstGPUBuffer, + data: firstData, + dataView: firstDataView + }); + + // Keep backward compatibility reference + shader._uniformBuffer = firstGPUBuffer; + + // 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 @@ -752,7 +785,72 @@ class RendererWebGPU extends Renderer3D { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.end(); - this.queue.submit([commandEncoder.finish()]); + this._pendingCommandEncoders.push(commandEncoder.finish()); + this._hasPendingDraws = true; + } + + ////////////////////////////////////////////// + // Uniform buffer pool management + ////////////////////////////////////////////// + + _getUniformBufferFromPool(shader) { + // Try to get a buffer from the pool + if (shader._uniformBufferPool.length > 0) { + const bufferInfo = shader._uniformBufferPool.pop(); + shader._uniformBuffersInUse.push(bufferInfo); + return bufferInfo; + } + + // No buffers available, create a new one + const newBuffer = this.device.createBuffer({ + size: shader._uniformBufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + const newData = new Float32Array(shader._uniformBufferSize / 4); + const newDataView = new DataView(newData.buffer); + + const bufferInfo = { + buffer: newBuffer, + data: newData, + dataView: newDataView + }; + + shader._uniformBuffersInUse.push(bufferInfo); + return bufferInfo; + } + + _returnUniformBuffersToPool() { + // Return all used buffers back to their pools for all registered shaders + for (const shader of this._shadersWithPools) { + if (shader._uniformBuffersInUse && shader._uniformBuffersInUse.length > 0) { + this._returnShaderBuffersToPool(shader); + } + } + } + + _returnShaderBuffersToPool(shader) { + // Move all buffers from inUse back to pool + while (shader._uniformBuffersInUse.length > 0) { + const bufferInfo = shader._uniformBuffersInUse.pop(); + shader._uniformBufferPool.push(bufferInfo); + } + } + + finishDraw() { + // Only submit if we actually had any draws + if (this._hasPendingDraws) { + // Submit all pending command encoders + if (this._pendingCommandEncoders.length > 0) { + this.queue.submit(this._pendingCommandEncoders); + this._pendingCommandEncoders = []; + } + + // Reset the flag + this._hasPendingDraws = false; + } + + // Return all uniform buffers to their pools + this._returnUniformBuffersToPool(); } ////////////////////////////////////////////// @@ -810,14 +908,15 @@ class RendererWebGPU extends Renderer3D { const gpuBuffer = buffers[buffer.dst]; passEncoder.setVertexBuffer(location, gpuBuffer, 0); } - // Bind uniforms - this._packUniforms(this._curShader); + // Bind uniforms - get a buffer from the pool + const uniformBufferInfo = this._getUniformBufferFromPool(currentShader); + this._packUniforms(currentShader, uniformBufferInfo); this.device.queue.writeBuffer( - currentShader._uniformBuffer, + uniformBufferInfo.buffer, 0, - currentShader._uniformData.buffer, - currentShader._uniformData.byteOffset, - currentShader._uniformData.byteLength + uniformBufferInfo.data.buffer, + uniformBufferInfo.data.byteOffset, + uniformBufferInfo.data.byteLength ); // Bind sampler/texture uniforms @@ -826,7 +925,7 @@ class RendererWebGPU extends Renderer3D { if (group === 0 && entry.binding === 0) { return { binding: 0, - resource: { buffer: currentShader._uniformBuffer }, + resource: { buffer: uniformBufferInfo.buffer }, }; } @@ -875,23 +974,22 @@ class RendererWebGPU extends Renderer3D { } passEncoder.end(); - this.queue.submit([commandEncoder.finish()]); - } - async ensureTexture(source) { - await this.queue.onSubmittedWorkDone(); - await new Promise((res) => requestAnimationFrame(res)); - const tex = this.getTexture(source); - tex.update(); - await this.queue.onSubmittedWorkDone(); - await new Promise((res) => requestAnimationFrame(res)); + // Store the command encoder for later submission + this._pendingCommandEncoders.push(commandEncoder.finish()); + + // Mark that we have pending draws that need submission + this._hasPendingDraws = true; } ////////////////////////////////////////////// // SHADER ////////////////////////////////////////////// - _packUniforms(shader) { + _packUniforms(shader, bufferInfo) { + const data = bufferInfo.data; + const dataView = bufferInfo.dataView; + for (const name in shader.uniforms) { const uniform = shader.uniforms[name]; if (uniform.isSampler) continue; @@ -899,31 +997,31 @@ class RendererWebGPU extends Renderer3D { if (uniform.baseType === 'u32') { if (uniform.size === 4) { // Single u32 - shader._uniformDataView.setUint32(uniform.offset, uniform._cachedData, true); + dataView.setUint32(uniform.offset, uniform._cachedData, true); } else { // Vector of u32s - const data = uniform._cachedData; - for (let i = 0; i < data.length; i++) { - shader._uniformDataView.setUint32(uniform.offset + i * 4, data[i], true); + const uniformData = uniform._cachedData; + for (let i = 0; i < uniformData.length; i++) { + dataView.setUint32(uniform.offset + i * 4, uniformData[i], true); } } } else if (uniform.baseType === 'i32') { if (uniform.size === 4) { // Single i32 - shader._uniformDataView.setInt32(uniform.offset, uniform._cachedData, true); + dataView.setInt32(uniform.offset, uniform._cachedData, true); } else { // Vector of i32s - const data = uniform._cachedData; - for (let i = 0; i < data.length; i++) { - shader._uniformDataView.setInt32(uniform.offset + i * 4, data[i], true); + const uniformData = uniform._cachedData; + for (let i = 0; i < uniformData.length; i++) { + dataView.setInt32(uniform.offset + i * 4, uniformData[i], true); } } } else if (uniform.size === 4) { // Single float value - shader._uniformData.set([uniform._cachedData], uniform.offset / 4); + data.set([uniform._cachedData], uniform.offset / 4); } else { // Float array (including vec2, vec3, vec4, mat4x4) - shader._uniformData.set(uniform._cachedData, uniform.offset / 4); + data.set(uniform._cachedData, uniform.offset / 4); } } } @@ -2275,9 +2373,6 @@ function rendererWebGPU(p5, fn) { p5.RendererWebGPU = RendererWebGPU; p5.renderers[constants.WEBGPU] = p5.RendererWebGPU; - fn.ensureTexture = function(source) { - return this._renderer.ensureTexture(source); - } // TODO: move this and the duplicate in the WebGL renderer to another file fn.setAttributes = async function (key, value) { From 5d3874cde814dc29c0c4552e7a5507fb806f3717 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 07:47:13 -0500 Subject: [PATCH 82/98] Use pools to push everything into one onSubmit --- preview/index.html | 38 +++++++- src/webgl/p5.Geometry.js | 3 + src/webgpu/p5.RendererWebGPU.js | 151 ++++++++++++++++++++++++++++---- 3 files changed, 174 insertions(+), 18 deletions(-) diff --git a/preview/index.html b/preview/index.html index dc1ace4e32..9b6feb63f3 100644 --- a/preview/index.html +++ b/preview/index.html @@ -36,7 +36,7 @@ ); fbo = p.createFramebuffer(); - /*tex = p.createImage(100, 100); + tex = p.createImage(100, 100); tex.loadPixels(); for (let x = 0; x < tex.width; x++) { for (let y = 0; y < tex.height; y++) { @@ -51,7 +51,7 @@ fbo.draw(() => { p.imageMode(p.CENTER); p.image(tex, 0, 0, p.width, p.height); - });*/ + }); /*sh = p.baseMaterialShader().modify({ uniforms: { @@ -119,14 +119,46 @@ 0, //p.width/3 * p.sin(t * 0.9 + i * Math.E + 0.2), p.width/3 * p.sin(t * 1.2 + i * Math.E + 0.3), ) - // p.texture(fbo) if (i % 2 === 0) { + if (i === 0) { + p.texture(fbo) + } p.box(30); } else { p.sphere(30); } p.pop(); } + + // Test beginShape/endShape with immediate mode shapes + p.push(); + p.translate(0, 100, 0); + p.fill('yellow'); + p.noStroke(); + + // Draw a circle using beginShape/endShape + p.beginShape(); + const numPoints = 16; + for (let i = 0; i < numPoints; i++) { + const angle = (i / numPoints) * Math.PI * 2; + const x = Math.cos(angle) * 50; + const y = Math.sin(angle) * 50; + p.vertex(x, y); + } + p.endShape(p.CLOSE); + + p.translate(100, 0, 0); + p.fill('purple'); + + // Draw a square using beginShape/endShape + p.beginShape(); + p.vertex(-30, -30); + p.vertex(30, -30); + p.vertex(30, 30); + p.vertex(-30, 30); + p.endShape(p.CLOSE); + + p.pop(); }; }; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 7dd8cb0522..2466e4bb61 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -206,6 +206,9 @@ class Geometry { } reset() { + // Notify renderer that geometry is being reset (for buffer cleanup) + this.renderer?.onReset?.(this); + this._hasFillTransparency = undefined; this._hasStrokeTransparency = undefined; diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index e3ff7cc270..26a625f594 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -38,9 +38,15 @@ class RendererWebGPU extends Renderer3D { // 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 = []; + + // Retired buffers to destroy at end of frame + this._retiredBuffers = []; } async setupContext() { @@ -274,22 +280,19 @@ class RendererWebGPU extends Renderer3D { const raw = map ? map(srcData) : srcData; const typed = this._normalizeBufferData(raw, Float32Array); - let buffer = buffers[dst]; - let recreated = false; - if (!buffer || buffer.size < typed.byteLength) { - recreated = true; - if (buffer) buffer.destroy(); - buffer = device.createBuffer({ - size: typed.byteLength, - usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - }); - buffers[dst] = buffer; - } + // Always use pooled buffers - let the pool system handle sizing and reuse + const pooledBufferInfo = this._getVertexBufferFromPool(geometry, dst, typed.byteLength); - if (recreated || geometry.dirtyFlags[src] !== false) { - device.queue.writeBuffer(buffer, 0, typed); - geometry.dirtyFlags[src] = false; - } + // 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); } @@ -789,6 +792,113 @@ class RendererWebGPU extends Renderer3D { this._hasPendingDraws = true; } + ////////////////////////////////////////////// + // 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 ////////////////////////////////////////////// @@ -851,6 +961,17 @@ class RendererWebGPU extends Renderer3D { // Return all uniform buffers to their pools this._returnUniformBuffersToPool(); + + // Return all vertex buffers to their pools + this._returnVertexBuffersToPool(); + + // Destroy all retired buffers + for (const buffer of this._retiredBuffers) { + if (buffer && buffer.destroy) { + buffer.destroy(); + } + } + this._retiredBuffers = []; } ////////////////////////////////////////////// From 745d0296ead3bcc1a3ba8fb233f6dcce804c93fa Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 09:52:43 -0500 Subject: [PATCH 83/98] Make sure framebuffers submit separately --- preview/index.html | 2 ++ src/core/structure.js | 6 ++--- src/webgl/p5.Framebuffer.js | 13 ++++++--- src/webgl/p5.Texture.js | 9 ++++--- src/webgpu/p5.RendererWebGPU.js | 48 +++++++++++++++++++++++++++------ 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/preview/index.html b/preview/index.html index 9b6feb63f3..3771bb4805 100644 --- a/preview/index.html +++ b/preview/index.html @@ -49,6 +49,8 @@ } tex.updatePixels(); fbo.draw(() => { + //p.clear(); + //p.background('orange'); p.imageMode(p.CENTER); p.image(tex, 0, 0, p.width, p.height); }); diff --git a/src/core/structure.js b/src/core/structure.js index f0c75879c9..3e5c828e4f 100644 --- a/src/core/structure.js +++ b/src/core/structure.js @@ -385,10 +385,8 @@ function structure(p5, fn){ } await this._runLifecycleHook('postdraw'); } - // Finish drawing - submit any pending GPU work (WebGPU specific) - if (this._renderer && typeof this._renderer.finishDraw === 'function') { - this._renderer.finishDraw(); - } + // Finish drawing + await this._renderer.finishDraw?.(); } }; diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 0839fb329c..111890b3e5 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -121,10 +121,8 @@ class Framebuffer { this._recreateTextures(); - const prevCam = this.renderer.states.curCamera; this.defaultCamera = this.createCamera(); this.filterCamera = this.createCamera(); - this.renderer.states.setValue('curCamera', prevCam); this.draw(() => this.renderer.clear()); } @@ -849,6 +847,9 @@ class Framebuffer { this.width * this.density, this.height * this.density ); + if (this.renderer.flushDraw) { + this.renderer.flushDraw(); + } } /** @@ -860,6 +861,12 @@ class Framebuffer { if (this.antialias) { this.dirty = { colorTexture: true, depthTexture: true }; } + // TODO + // This should work but flushes more often than we need to. Ideally we only do this + // right before the fbo is read as a texture. + if (this.renderer.flushDraw) { + this.renderer.flushDraw(); + } } /** @@ -910,8 +917,8 @@ class Framebuffer { * */ end() { - const gl = this.gl; this.renderer.pop(); + const fbo = this.renderer.activeFramebuffers.pop(); if (fbo !== this) { throw new Error("It looks like you've called end() while another Framebuffer is active."); diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index d1c45b84f1..7e3021d2a1 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -149,10 +149,11 @@ class Texture { 1 ); } else if (!this.isFramebufferTexture) { - this._renderer.uploadTextureFromSource( - this.textureHandle, - textureData - ); + this.update() + // this._renderer.uploadTextureFromSource( + // this.textureHandle, + // textureData + // ); } this.unbindTexture(); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 26a625f594..a5af391533 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -47,6 +47,9 @@ class RendererWebGPU extends Renderer3D { // Retired buffers to destroy at end of frame this._retiredBuffers = []; + + // Promise chain for GPU work completion + this._gpuWorkPromise = null; } async setupContext() { @@ -946,17 +949,37 @@ class RendererWebGPU extends Renderer3D { } } - finishDraw() { + flushDraw() { // Only submit if we actually had any draws if (this._hasPendingDraws) { - // Submit all pending command encoders - if (this._pendingCommandEncoders.length > 0) { - this.queue.submit(this._pendingCommandEncoders); - this._pendingCommandEncoders = []; + // Create a copy of pending command encoders + const commandsToSubmit = this._pendingCommandEncoders.slice(); + this._pendingCommandEncoders = []; + this._hasPendingDraws = false; + + // Chain the submission through the existing promise + const submit = () => { + // Submit the commands + this.queue.submit(commandsToSubmit); + // Return promise that resolves when GPU work is done + return this.queue.onSubmittedWorkDone(); + }; + if (this._gpuWorkPromise) { + this._gpuWorkPromise = this._gpuWorkPromise.then(submit); + } else { + this._gpuWorkPromise = submit(); } + } + } - // Reset the flag - this._hasPendingDraws = false; + async finishDraw() { + // First flush any pending draws + this.flushDraw(); + + // Wait for all GPU work to complete + if (this._gpuWorkPromise) { + await this._gpuWorkPromise; + this._gpuWorkPromise = null; } // Return all uniform buffers to their pools @@ -1368,6 +1391,10 @@ class RendererWebGPU extends Renderer3D { { 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) { @@ -1377,6 +1404,10 @@ class RendererWebGPU extends Renderer3D { { 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) {} @@ -2006,7 +2037,8 @@ class RendererWebGPU extends Renderer3D { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.end(); - this.queue.submit([commandEncoder.finish()]); + this._pendingCommandEncoders.push(commandEncoder.finish()); + this._hasPendingDraws = true; } _getFramebufferColorTextureView(framebuffer) { From 80ea75890f47d58b90d8393d3ce2d7754eff0b3b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 17:32:20 -0500 Subject: [PATCH 84/98] Fix some bugs --- src/strands/strands_api.js | 2 +- src/webgl/p5.Texture.js | 10 +- src/webgpu/p5.RendererWebGPU.js | 250 ++++++++++---------------------- test/unit/webgpu/p5.Shader.js | 26 ++-- 4 files changed, 92 insertions(+), 196 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 1ca341b375..fd7963ab38 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -113,7 +113,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { strandsContext.fragmentDeclarations.add(noiseSnippet); // Make each input into a strands node so that we can check their dimensions - const strandsArgs = args.map(arg => p5.strandsNode(arg)); + const strandsArgs = args.flat().map(arg => p5.strandsNode(arg)); let nodeArgs; if (strandsArgs.length === 3) { nodeArgs = [fn.vec3(strandsArgs[0], strandsArgs[1], strandsArgs[2])]; diff --git a/src/webgl/p5.Texture.js b/src/webgl/p5.Texture.js index 7e3021d2a1..b6618cc972 100644 --- a/src/webgl/p5.Texture.js +++ b/src/webgl/p5.Texture.js @@ -149,11 +149,11 @@ class Texture { 1 ); } else if (!this.isFramebufferTexture) { - this.update() - // this._renderer.uploadTextureFromSource( - // this.textureHandle, - // textureData - // ); + // this.update() + this._renderer.uploadTextureFromSource( + this.textureHandle, + textureData + ); } this.unbindTexture(); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index a5af391533..fcf0c6ee19 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -31,8 +31,6 @@ class RendererWebGPU extends Renderer3D { this.pixelReadBuffer = null; this.pixelReadBufferSize = 0; - // Lazy readback texture for main canvas pixel reading - this.canvasReadbackTexture = null; this.strandsBackend = wgslBackend; // Registry to track all shaders for uniform data pooling @@ -50,6 +48,10 @@ class RendererWebGPU extends Renderer3D { // Promise chain for GPU work completion this._gpuWorkPromise = null; + + // 2D canvas for pixel reading fallback + this._pixelReadCanvas = null; + this._pixelReadCtx = null; } async setupContext() { @@ -160,12 +162,7 @@ class RendererWebGPU extends Renderer3D { // Clear the main canvas after resize this.clear(); - - // Destroy existing readback texture when size changes - if (this.canvasReadbackTexture && this.canvasReadbackTexture.destroy) { - this.canvasReadbackTexture.destroy(); - this.canvasReadbackTexture = null; - } + // this._gpuWorkPromise = this.queue.onSubmittedWorkDone(); } clear(...args) { @@ -964,12 +961,36 @@ class RendererWebGPU extends Renderer3D { // Return promise that resolves when GPU work is done return this.queue.onSubmittedWorkDone(); }; - if (this._gpuWorkPromise) { - this._gpuWorkPromise = this._gpuWorkPromise.then(submit); - } else { - this._gpuWorkPromise = submit(); - } + submit(); + // this._gpuWorkPromise = submit() + // if (this._gpuWorkPromise) { + // this._gpuWorkPromise = this._gpuWorkPromise.then(submit); + // } else { + // this._gpuWorkPromise = submit(); + // } + } + } + + _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() { @@ -1163,7 +1184,7 @@ class RendererWebGPU extends Renderer3D { } else if (uniform.size === 4) { // Single float value data.set([uniform._cachedData], uniform.offset / 4); - } else { + } else if (uniform._cachedData !== undefined) { // Float array (including vec2, vec3, vec4, mat4x4) data.set(uniform._cachedData, uniform.offset / 4); } @@ -2282,43 +2303,6 @@ class RendererWebGPU extends Renderer3D { // Main canvas pixel methods ////////////////////////////////////////////// - _ensureCanvasReadbackTexture() { - if (!this.canvasReadbackTexture) { - const width = Math.ceil(this.width * this._pixelDensity); - const height = Math.ceil(this.height * this._pixelDensity); - - this.canvasReadbackTexture = this.device.createTexture({ - size: { width, height, depthOrArrayLayers: 1 }, - format: this.presentationFormat, - usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC, - }); - } - return this.canvasReadbackTexture; - } - - _copyCanvasToReadbackTexture() { - // Get the current canvas texture BEFORE any awaiting - const canvasTexture = this.drawingContext.getCurrentTexture(); - - // Ensure readback texture exists - const readbackTexture = this._ensureCanvasReadbackTexture(); - - // Copy canvas texture to readback texture immediately - const copyEncoder = this.device.createCommandEncoder(); - copyEncoder.copyTextureToTexture( - { texture: canvasTexture }, - { texture: readbackTexture }, - { - width: Math.ceil(this.width * this._pixelDensity), - height: Math.ceil(this.height * this._pixelDensity), - depthOrArrayLayers: 1 - } - ); - this.device.queue.submit([copyEncoder.finish()]); - - return readbackTexture; - } - _convertBGRtoRGB(pixelData) { // Convert BGR to RGB by swapping red and blue channels for (let i = 0; i < pixelData.length; i += 4) { @@ -2331,91 +2315,44 @@ class RendererWebGPU extends Renderer3D { } async loadPixels() { - const width = this.width * this._pixelDensity; - const height = this.height * this._pixelDensity; - - // Copy canvas to readback texture - const readbackTexture = this._copyCanvasToReadbackTexture(); + // Wait for all GPU work to complete first + await this.finishDraw(); - // Now we can safely await - await this.queue.onSubmittedWorkDone(); - - 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: readbackTexture }, - { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, - { width, height, depthOrArrayLayers: 1 } - ); - - this.device.queue.submit([commandEncoder.finish()]); + // Get canvas dimensions accounting for pixel density + const width = Math.ceil(this.width * this._pixelDensity); + const height = Math.ceil(this.height * this._pixelDensity); - await stagingBuffer.mapAsync(GPUMapMode.READ, 0, bufferSize); - const mappedRange = stagingBuffer.getMappedRange(0, bufferSize); + // Get 2D canvas for pixel reading + const { ctx } = this._ensurePixelReadCanvas(width, height); - if (alignedBytesPerRow === unalignedBytesPerRow) { - this.pixels = new Uint8Array(mappedRange.slice(0, width * height * bytesPerPixel)); - } else { - // Need to extract pixel data from aligned buffer - this.pixels = 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; - this.pixels.set(mappedData.subarray(srcOffset, srcOffset + unalignedBytesPerRow), dstOffset); - } - } + // Draw the WebGPU canvas onto the 2D canvas + ctx.clearRect(0, 0, width, height); + ctx.drawImage(this.canvas, 0, 0, width, height); - // Convert BGR to RGB for main canvas - this._convertBGRtoRGB(this.pixels); + // Get pixel data from 2D canvas + const imageData = ctx.getImageData(0, 0, width, height); + this.pixels = imageData.data; - stagingBuffer.unmap(); return this.pixels; } async _getPixel(x, y) { - // Copy canvas to readback texture - const readbackTexture = this._copyCanvasToReadbackTexture(); + // Get 2D canvas sized to match main canvas for single pixel read + const canvasWidth = Math.ceil(this.width * this._pixelDensity); + const canvasHeight = Math.ceil(this.height * this._pixelDensity); + const { ctx, canvas } = this._ensurePixelReadCanvas(canvasWidth, canvasHeight); + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + ctx.drawImage(this.canvas, 0, 0, canvasWidth, canvasHeight); + console.log(canvas.toDataURL()) - // Now we can safely await - 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: readbackTexture, - 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); - - // Convert BGR to RGB for main canvas - swap red and blue - const result = [pixelData[2], pixelData[1], pixelData[0], pixelData[3]]; - - stagingBuffer.unmap(); - return result; + const imageData = ctx.getImageData(x, y, 1, 1); + return Array.from(imageData.data); } async get(x, y, w, h) { + // Wait for all GPU work to complete first + await this.finishDraw(); + const pd = this._pixelDensity; if (typeof x === 'undefined' && typeof y === 'undefined') { @@ -2436,65 +2373,26 @@ class RendererWebGPU extends Renderer3D { return this._getPixel(x, y); } // get(x,y,w,h) - region + w *= pd; + h *= pd; } - // Copy canvas to readback texture - const readbackTexture = this._copyCanvasToReadbackTexture(); - - // Now we can safely await - await this.queue.onSubmittedWorkDone(); - - // Read region and create p5.Image - const width = w * pd; - const height = h * pd; - const bytesPerPixel = 4; - const unalignedBytesPerRow = width * bytesPerPixel; - const alignedBytesPerRow = this._alignBytesPerRow(unalignedBytesPerRow); - const bufferSize = alignedBytesPerRow * height; + // Get 2D canvas sized to match main canvas for region reading + const canvasWidth = Math.ceil(this.width * this._pixelDensity); + const canvasHeight = Math.ceil(this.height * this._pixelDensity); + const { ctx } = this._ensurePixelReadCanvas(canvasWidth, canvasHeight); + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + ctx.drawImage(this.canvas, 0, 0, canvasWidth, canvasHeight); - const stagingBuffer = this._ensurePixelReadBuffer(bufferSize); + // Get the region + const regionImageData = ctx.getImageData(x, y, w, h); - const commandEncoder = this.device.createCommandEncoder(); - commandEncoder.copyTextureToBuffer( - { - texture: readbackTexture, - origin: { x, y, z: 0 } - }, - { buffer: stagingBuffer, bytesPerRow: alignedBytesPerRow }, - { width, height, depthOrArrayLayers: 1 } - ); - - this.device.queue.submit([commandEncoder.finish()]); - await this.queue.onSubmittedWorkDone(); - - 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); - } - } - - // Convert BGR to RGB for main canvas - this._convertBGRtoRGB(pixelData); - - const region = new Image(width, height); + // Create p5.Image for the region + const region = new Image(w, h); region.pixelDensity(pd); - const ctx = region.canvas.getContext('2d'); - const imageData = ctx.createImageData(width, height); - imageData.data.set(pixelData); - ctx.putImageData(imageData, 0, 0); + const regionCtx = region.canvas.getContext('2d'); + regionCtx.putImageData(regionImageData, 0, 0); - stagingBuffer.unmap(); return region; } diff --git a/test/unit/webgpu/p5.Shader.js b/test/unit/webgpu/p5.Shader.js index 6dd28b29f9..32e7030973 100644 --- a/test/unit/webgpu/p5.Shader.js +++ b/test/unit/webgpu/p5.Shader.js @@ -192,7 +192,6 @@ suite('WebGPU p5.Shader', function() { test('handle modifications after if statement in both branches', async () => { await myp5.createCanvas(100, 50, myp5.WEBGPU); - myp5.pixelDensity(1); const testShader = myp5.baseMaterialShader().modify(() => { myp5.getPixelInputs(inputs => { const uv = inputs.texCoord; @@ -215,12 +214,11 @@ suite('WebGPU p5.Shader', function() { myp5.shader(testShader); myp5.plane(myp5.width, myp5.height); // Check left side (false condition) - await myp5.loadPixels(); - const leftPixel = myp5.pixels[(25 * myp5.width + 25) * 4] - const rightPixel = myp5.pixels[(25 * myp5.width + 75) * 4] - assert.approximately(leftPixel, 102, 5); // 0.4 * 255 ≈ 102 + const leftPixel = await myp5.get(25, 25) + const rightPixel = await myp5.get(75, 25) + assert.approximately(leftPixel[0], 102, 5); // 0.4 * 255 ≈ 102 // Check right side (true condition) - assert.approximately(rightPixel, 127, 5); // 0.5 * 255 ≈ 127 + assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 }); test('handle if-else-if chains', async () => { @@ -1003,7 +1001,7 @@ suite('WebGPU p5.Shader', function() { suite('noise()', () => { for (let i = 1; i <= 3; i++) { test(`works with ${i}D vectors`, async () => { - expect(async () => { + await expect((async () => { await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { @@ -1013,11 +1011,11 @@ suite('WebGPU p5.Shader', function() { }, { myp5, input }); myp5.shader(testShader); myp5.plane(10, 10); - }).not.toThrowError(); + })()).resolves.not.toThrowError(); }); test(`works with ${i}D positional arguments`, async () => { - expect(async () => { + await expect((async () => { await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { @@ -1027,13 +1025,13 @@ suite('WebGPU p5.Shader', function() { }, { myp5, input }); myp5.shader(testShader); myp5.plane(10, 10); - }).not.toThrowError(); + })()).resolves.not.toThrowError(); }); } for (const i of [0, 4]) { test(`Does not work in ${i}D`, async () => { - expect(async () => { + await expect((async () => { await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { @@ -1043,11 +1041,11 @@ suite('WebGPU p5.Shader', function() { }, { myp5, input }); myp5.shader(testShader); myp5.plane(10, 10); - }).rejects.toThrowError(); + })()).rejects.toThrowError(); }); test(`Does not work in ${i}D with positional arguments`, async () => { - expect(async () => { + await expect((async () => { await myp5.createCanvas(50, 50, myp5.WEBGPU); const input = new Array(i).fill(10); const testShader = myp5.baseFilterShader().modify(() => { @@ -1057,7 +1055,7 @@ suite('WebGPU p5.Shader', function() { }, { myp5, input }); myp5.shader(testShader); myp5.plane(10, 10); - }).rejects.toThrowError(); + })()).rejects.toThrowError(); }); } }); From bf769e19cad51f5f4966a566aa53668dd218e5a3 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 18:23:08 -0500 Subject: [PATCH 85/98] Use a main framebuffer instead of other hacks to get pixels --- src/core/p5.Renderer3D.js | 16 ++++++++--- src/webgl/p5.Framebuffer.js | 6 +--- src/webgl/p5.RendererGL.js | 8 ++++++ src/webgpu/p5.RendererWebGPU.js | 50 +++++++++++++++++++++++++++++++-- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 0f93a7a910..29d3cb6a29 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -116,7 +116,10 @@ export class Renderer3D extends Renderer { this.states.uViewMatrix = new Matrix(4); this.states.uPMatrix = new Matrix(4); - this.states.curCamera = new Camera(this); + this.mainCamera = new Camera(this); + if (!this.states.curCamera) { + this.states.curCamera = this.mainCamera; + } this.states.uPMatrix.set(this.states.curCamera.projMatrix); this.states.uViewMatrix.set(this.states.curCamera.cameraMatrix); @@ -197,8 +200,8 @@ export class Renderer3D extends Renderer { this.registerEnabled = new Set(); // Camera - this.states.curCamera._computeCameraDefaultSettings(); - this.states.curCamera._setDefaultCamera(); + this.mainCamera._computeCameraDefaultSettings(); + this.mainCamera._setDefaultCamera(); // FilterCamera this.filterCamera = new Camera(this); @@ -1218,7 +1221,10 @@ export class Renderer3D extends Renderer { this._updateViewport(); this._updateSize(); - this.states.curCamera._resize(); + this.mainCamera._resize(); + if (this.states.curCamera !== this.mainCamera) { + this.states.curCamera._resize(); + } //resize pixels buffer if (typeof this.pixels !== "undefined") { @@ -1228,8 +1234,10 @@ export class Renderer3D extends Renderer { for (const framebuffer of this.framebuffers) { // Notify framebuffers of the resize so that any auto-sized framebuffers // can also update their size + this.flushDraw?.(); framebuffer._canvasSizeChanged(); } + this.flushDraw?.(); // reset canvas properties for (const savedKey in props) { diff --git a/src/webgl/p5.Framebuffer.js b/src/webgl/p5.Framebuffer.js index 111890b3e5..6f49e0fb9e 100644 --- a/src/webgl/p5.Framebuffer.js +++ b/src/webgl/p5.Framebuffer.js @@ -16,11 +16,7 @@ class FramebufferCamera extends Camera { super(framebuffer.renderer); this.fbo = framebuffer; - // WebGL textures are upside-down compared to textures that come from - // images and graphics. Framebuffer cameras need to invert their y - // axes when being rendered to so that the texture comes out rightway up - // when read in shaders or image(). - this.yScale = -1; + this.yScale = framebuffer.renderer.framebufferYScale(); } _computeCameraDefaultSettings() { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 51a0ef9338..9b4a06805c 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1844,6 +1844,14 @@ class RendererGL extends Renderer3D { ); } + framebufferYScale() { + // WebGL textures are upside-down compared to textures that come from + // images and graphics. Framebuffer cameras need to invert their y + // axes when being rendered to so that the texture comes out rightway up + // when read in shaders or image(). + return -1; + } + readFramebufferPixels(framebuffer) { const gl = this.GL; const prevFramebuffer = this.activeFramebuffer(); diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index fcf0c6ee19..60b1336776 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -52,6 +52,7 @@ class RendererWebGPU extends Renderer3D { // 2D canvas for pixel reading fallback this._pixelReadCanvas = null; this._pixelReadCtx = null; + this.mainFramebuffer = null; } async setupContext() { @@ -93,6 +94,7 @@ class RendererWebGPU extends Renderer3D { // TODO disablable stencil this.depthFormat = 'depth24plus-stencil8'; + this.mainFramebuffer = this.createFramebuffer(); this._updateSize(); this._update(); } @@ -766,6 +768,9 @@ class RendererWebGPU extends Renderer3D { } _resetBuffersBeforeDraw() { + if (!this.activeFramebuffer()) { + this.mainFramebuffer.begin(); + } const commandEncoder = this.device.createCommandEncoder(); const depthTextureView = this.depthTexture?.createView(); @@ -994,7 +999,19 @@ class RendererWebGPU extends Renderer3D { } async finishDraw() { - // First flush any pending draws + this.flushDraw(); + const states = []; + while (this.activeFramebuffers.length > 0) { + const fbo = this.activeFramebuffers.pop(); + states.unshift({ fbo, diff: { ...this.states } }); + } + this.flushDraw(); + + // this._pInst.background('red'); + this._pInst.push(); + this._pInst.imageMode(this._pInst.CENTER); + this._pInst.image(this.mainFramebuffer, 0, 0); + this._pInst.pop(); this.flushDraw(); // Wait for all GPU work to complete @@ -1016,6 +1033,13 @@ class RendererWebGPU extends Renderer3D { } } this._retiredBuffers = []; + + for (const { fbo, diff } of states) { + fbo.begin(); + for (const key in diff) { + this.states.setValue(key, diff[key]); + } + } } ////////////////////////////////////////////// @@ -2134,7 +2158,15 @@ class RendererWebGPU extends Renderer3D { bindFramebuffer(framebuffer) {} + framebufferYScale() { + return 1; + } + async readFramebufferPixels(framebuffer) { + 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; @@ -2188,8 +2220,9 @@ class RendererWebGPU extends Renderer3D { } async readFramebufferPixel(framebuffer, x, y) { + await this.finishDraw(); // Ensure all pending GPU work is complete before reading pixels - await this.queue.onSubmittedWorkDone(); + // await this.queue.onSubmittedWorkDone(); const bytesPerPixel = 4; const alignedBytesPerRow = this._alignBytesPerRow(bytesPerPixel); @@ -2219,8 +2252,14 @@ class RendererWebGPU extends Renderer3D { } async readFramebufferRegion(framebuffer, x, y, w, h) { + 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(); + // await this.queue.onSubmittedWorkDone(); const width = w * framebuffer.density; const height = h * framebuffer.density; @@ -2273,6 +2312,7 @@ class RendererWebGPU extends Renderer3D { } stagingBuffer.unmap(); + // if (wasActive) framebuffer.begin(); return region; } @@ -2315,6 +2355,9 @@ class RendererWebGPU extends Renderer3D { } async loadPixels() { + await this.mainFramebuffer.loadPixels(); + this.pixels = this.mainFramebuffer.pixels.slice(); + return // Wait for all GPU work to complete first await this.finishDraw(); @@ -2350,6 +2393,7 @@ class RendererWebGPU extends Renderer3D { } async get(x, y, w, h) { + return this.mainFramebuffer.get(x, y, w, h); // Wait for all GPU work to complete first await this.finishDraw(); From 5aedaa6dc9dff891d72db89778b5a2e2b1fbb212 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 20:09:22 -0500 Subject: [PATCH 86/98] Clean up code --- src/webgpu/p5.RendererWebGPU.js | 103 +------------------------------- 1 file changed, 2 insertions(+), 101 deletions(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 60b1336776..db7ec7597e 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -46,9 +46,6 @@ class RendererWebGPU extends Renderer3D { // Retired buffers to destroy at end of frame this._retiredBuffers = []; - // Promise chain for GPU work completion - this._gpuWorkPromise = null; - // 2D canvas for pixel reading fallback this._pixelReadCanvas = null; this._pixelReadCtx = null; @@ -164,7 +161,6 @@ class RendererWebGPU extends Renderer3D { // Clear the main canvas after resize this.clear(); - // this._gpuWorkPromise = this.queue.onSubmittedWorkDone(); } clear(...args) { @@ -959,20 +955,8 @@ class RendererWebGPU extends Renderer3D { this._pendingCommandEncoders = []; this._hasPendingDraws = false; - // Chain the submission through the existing promise - const submit = () => { - // Submit the commands - this.queue.submit(commandsToSubmit); - // Return promise that resolves when GPU work is done - return this.queue.onSubmittedWorkDone(); - }; - submit(); - // this._gpuWorkPromise = submit() - // if (this._gpuWorkPromise) { - // this._gpuWorkPromise = this._gpuWorkPromise.then(submit); - // } else { - // this._gpuWorkPromise = submit(); - // } + // Submit the commands + this.queue.submit(commandsToSubmit); } } @@ -1014,12 +998,6 @@ class RendererWebGPU extends Renderer3D { this._pInst.pop(); this.flushDraw(); - // Wait for all GPU work to complete - if (this._gpuWorkPromise) { - await this._gpuWorkPromise; - this._gpuWorkPromise = null; - } - // Return all uniform buffers to their pools this._returnUniformBuffersToPool(); @@ -2357,87 +2335,10 @@ class RendererWebGPU extends Renderer3D { async loadPixels() { await this.mainFramebuffer.loadPixels(); this.pixels = this.mainFramebuffer.pixels.slice(); - return - // Wait for all GPU work to complete first - await this.finishDraw(); - - // Get canvas dimensions accounting for pixel density - const width = Math.ceil(this.width * this._pixelDensity); - const height = Math.ceil(this.height * this._pixelDensity); - - // Get 2D canvas for pixel reading - const { ctx } = this._ensurePixelReadCanvas(width, height); - - // Draw the WebGPU canvas onto the 2D canvas - ctx.clearRect(0, 0, width, height); - ctx.drawImage(this.canvas, 0, 0, width, height); - - // Get pixel data from 2D canvas - const imageData = ctx.getImageData(0, 0, width, height); - this.pixels = imageData.data; - - return this.pixels; - } - - async _getPixel(x, y) { - // Get 2D canvas sized to match main canvas for single pixel read - const canvasWidth = Math.ceil(this.width * this._pixelDensity); - const canvasHeight = Math.ceil(this.height * this._pixelDensity); - const { ctx, canvas } = this._ensurePixelReadCanvas(canvasWidth, canvasHeight); - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - ctx.drawImage(this.canvas, 0, 0, canvasWidth, canvasHeight); - console.log(canvas.toDataURL()) - - const imageData = ctx.getImageData(x, y, 1, 1); - return Array.from(imageData.data); } async get(x, y, w, h) { return this.mainFramebuffer.get(x, y, w, h); - // Wait for all GPU work to complete first - await this.finishDraw(); - - const pd = this._pixelDensity; - - if (typeof x === 'undefined' && typeof y === 'undefined') { - // get() - return entire canvas - x = y = 0; - w = this.width; - h = this.height; - } else { - x *= pd; - y *= pd; - - if (typeof w === 'undefined' && typeof h === 'undefined') { - // get(x,y) - single pixel - if (x < 0 || y < 0 || x >= this.width * pd || y >= this.height * pd) { - return [0, 0, 0, 0]; - } - - return this._getPixel(x, y); - } - // get(x,y,w,h) - region - w *= pd; - h *= pd; - } - - // Get 2D canvas sized to match main canvas for region reading - const canvasWidth = Math.ceil(this.width * this._pixelDensity); - const canvasHeight = Math.ceil(this.height * this._pixelDensity); - const { ctx } = this._ensurePixelReadCanvas(canvasWidth, canvasHeight); - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - ctx.drawImage(this.canvas, 0, 0, canvasWidth, canvasHeight); - - // Get the region - const regionImageData = ctx.getImageData(x, y, w, h); - - // Create p5.Image for the region - const region = new Image(w, h); - region.pixelDensity(pd); - const regionCtx = region.canvas.getContext('2d'); - regionCtx.putImageData(regionImageData, 0, 0); - - return region; } getNoiseShaderSnippet() { From 263ed1dc924a265ae63aadf9b32c1038fc117526 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 20:24:56 -0500 Subject: [PATCH 87/98] Update regex --- src/webgpu/p5.RendererWebGPU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index db7ec7597e..12cb16612b 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1798,7 +1798,7 @@ class RendererWebGPU extends Renderer3D { const structProperties = structName => { // WGSL struct parsing: struct StructName { field1: Type, field2: Type } - const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]*)\}`).exec(fullSrc); if (!structDefMatch) return undefined; const properties = []; From 0a97a35fb4eb1697f66eaeba216c81c1b7c0ae13 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 20:28:56 -0500 Subject: [PATCH 88/98] one more regex fix --- src/webgpu/p5.RendererWebGPU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 12cb16612b..54b5e229ba 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1798,7 +1798,7 @@ class RendererWebGPU extends Renderer3D { const structProperties = structName => { // WGSL struct parsing: struct StructName { field1: Type, field2: Type } - const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]*)\}`).exec(fullSrc); + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]*)\\}`).exec(fullSrc); if (!structDefMatch) return undefined; const properties = []; From a51a15ee5bc5afc712c635539f6f5f4e688bba97 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 23 Nov 2025 20:44:36 -0500 Subject: [PATCH 89/98] ok we arent escaping regex braces --- src/webgpu/p5.RendererWebGPU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 54b5e229ba..9d98b7474c 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -1798,7 +1798,7 @@ class RendererWebGPU extends Renderer3D { const structProperties = structName => { // WGSL struct parsing: struct StructName { field1: Type, field2: Type } - const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]*)\\}`).exec(fullSrc); + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*{([^}]*)}`).exec(fullSrc); if (!structDefMatch) return undefined; const properties = []; From 34338cf0e1eca749e01e54049ff73f28da8dc1db Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 10:01:59 -0500 Subject: [PATCH 90/98] Fix texture usage to get baseFilterShader working --- preview/index.html | 12 +++++++++++ src/strands/strands_api.js | 22 +++++++++++++++++++++ src/strands/strands_builtins.js | 4 ---- src/webgl/p5.RendererGL.js | 1 + src/webgl/strands_glslBackend.js | 17 ++++++++++++++-- src/webgpu/p5.RendererWebGPU.js | 8 +++++--- src/webgpu/shaders/color.js | 3 --- src/webgpu/shaders/line.js | 3 --- src/webgpu/shaders/material.js | 5 +---- src/webgpu/shaders/utils.js | 10 ---------- src/webgpu/strands_wgslBackend.js | 33 +++++++++++++++++++++++++++++-- 11 files changed, 87 insertions(+), 31 deletions(-) delete mode 100644 src/webgpu/shaders/utils.js diff --git a/preview/index.html b/preview/index.html index 3771bb4805..d4902b9de3 100644 --- a/preview/index.html +++ b/preview/index.html @@ -28,6 +28,7 @@ let ssh; let tex; let font; + let redFilter; p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); @@ -36,6 +37,15 @@ ); fbo = p.createFramebuffer(); + redFilter = p.baseFilterShader().modify(() => { + p.getColor((inputs, canvasContent) => { + let col = p.getTexture(canvasContent, inputs.texCoord); + col.g = col.r; + col.b = col.r; + return col; + }) + }, { p }) + tex = p.createImage(100, 100); tex.loadPixels(); for (let x = 0; x < tex.width; x++) { @@ -161,6 +171,8 @@ p.endShape(p.CLOSE); p.pop(); + + p.filter(redFilter); }; }; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index fd7963ab38..1c6ddd0a4c 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -101,6 +101,28 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { } } } + + fn.getTexture = function (...rawArgs) { + if (strandsContext.active) { + const { id, dimension } = strandsContext.backend.createGetTextureCall(strandsContext, rawArgs); + return createStrandsNode(id, dimension, strandsContext); + } else { + p5._friendlyError( + `It looks like you've called getTexture outside of a shader's modify() function.` + ) + } + } + + // Add texture function as alias for getTexture with p5 fallback + const originalTexture = fn.texture; + fn.texture = function (...args) { + if (strandsContext.active) { + return this.getTexture(...args); + } else { + return originalTexture.apply(this, args); + } + } + // Add noise function with backend-agnostic implementation const originalNoise = fn.noise; fn.noise = function (...args) { diff --git a/src/strands/strands_builtins.js b/src/strands/strands_builtins.js index eccfc74170..d6080ea428 100644 --- a/src/strands/strands_builtins.js +++ b/src/strands/strands_builtins.js @@ -105,10 +105,6 @@ const builtInGLSLFunctions = { ], reflect: [{ params: [GenType.FLOAT, GenType.FLOAT], returnType: GenType.FLOAT, isp5Function: false}], refract: [{ params: [GenType.FLOAT, GenType.FLOAT,DataType.float1], returnType: GenType.FLOAT, isp5Function: false}], - - ////////// Texture sampling ////////// - texture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}], - getTexture: [{params: [DataType.sampler2D, DataType.float2], returnType: DataType.float4, isp5Function: true}] } export const strandsBuiltinFunctions = { diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 9b4a06805c..ad4110b2de 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -2016,6 +2016,7 @@ class RendererGL extends Renderer3D { getNoiseShaderSnippet() { return noiseGLSL; } + } function rendererGL(p5, fn) { diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index 0e6e7dd1e9..42f4f60fb3 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -1,6 +1,7 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "../strands/ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; -import * as FES from '../strands/strands_FES' +import * as FES from '../strands/strands_FES'; +import * as build from '../strands/ir_builders'; function shouldCreateTemp(dag, nodeID) { const nodeType = dag.nodeTypes[nodeID]; if (nodeType !== NodeType.OPERATION) return false; @@ -366,5 +367,17 @@ export const glslBackend = { const type = strandsContext.cfg.blockTypes[blockID]; const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; handler.call(cfgHandlers, blockID, strandsContext, generationContext); + }, + + createGetTextureCall(strandsContext, args) { + // In GLSL, getTexture is straightforward - just pass through the args + // First argument should be a texture (sampler2D), second should be coordinates + const { id, dimension } = build.functionCallNode(strandsContext, 'getTexture', args, { + overloads: [{ + params: [DataType.sampler2D, DataType.float2], + returnType: DataType.float4 + }] + }); + return { id, dimension }; } } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 9d98b7474c..39337f9bb9 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -993,6 +993,7 @@ class RendererWebGPU extends Renderer3D { // this._pInst.background('red'); this._pInst.push(); + this._pInst.resetShader(); this._pInst.imageMode(this._pInst.CENTER); this._pInst.image(this.mainFramebuffer, 0, 0); this._pInst.pop(); @@ -1322,7 +1323,7 @@ class RendererWebGPU extends Renderer3D { getUniformMetadata(shader) { // Currently, for ease of parsing, we enforce that the first bind group is a // struct, which contains all non-sampler uniforms. Then, any subsequent - // groups are individual samplers. + // groups contain samplers. // Extract the struct name from the uniform variable declaration const uniformVarRegex = /@group\(0\)\s+@binding\(0\)\s+var\s+(\w+)\s*:\s*(\w+);/; @@ -2345,6 +2346,7 @@ class RendererWebGPU extends Renderer3D { return noiseWGSL; } + baseFilterShader() { if (!this._baseFilterShader) { this._baseFilterShader = new Shader( @@ -2354,8 +2356,8 @@ class RendererWebGPU extends Renderer3D { { vertex: {}, fragment: { - "vec4 getColor": `(inputs: FilterInputs, tex: texture_2d, texSampler: sampler) -> vec4 { - return textureSample(tex, texSampler, inputs.texCoord); + "vec4 getColor": `(inputs: FilterInputs, tex: texture_2d, tex_sampler: sampler) -> vec4 { + return textureSample(tex, tex_sampler, inputs.texCoord); }`, }, } diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index b22818efa2..99a5c12b3b 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -1,5 +1,3 @@ -import { getTexture } from './utils'; - const uniforms = ` struct Uniforms { // @p5 ifdef Vertex getWorldInputs @@ -102,7 +100,6 @@ struct FragmentInput { ${uniforms} @group(0) @binding(0) var uniforms: Uniforms; -${getTexture} @fragment fn main(input: FragmentInput) -> @location(0) vec4 { diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index 5c01ddd1bf..5562d6775c 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -1,5 +1,3 @@ -import { getTexture } from './utils' - const uniforms = ` struct Uniforms { // @p5 ifdef StrokeVertex getWorldInputs @@ -296,7 +294,6 @@ struct StrokeFragmentInput { ${uniforms} @group(0) @binding(0) var uniforms: Uniforms; -${getTexture} fn distSquared(a: vec2, b: vec2) -> f32 { return dot(b - a, b - a); diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 774f131bce..1b462c1b69 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -1,5 +1,3 @@ -import { getTexture } from './utils'; - const uniforms = ` struct Uniforms { // @p5 ifdef Vertex getWorldInputs @@ -171,7 +169,6 @@ struct Inputs { metalness: f32, } -${getTexture} struct LightResult { diffuse: vec3, @@ -310,7 +307,7 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let color = select( input.vColor, - getTexture(uSampler, uSampler_sampler, input.vTexCoord) * (uniforms.uTint/255.0), + textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (uniforms.uTint/255.0), uniforms.isTexture == 1 ); // TODO: check isTexture and apply tint var inputs = Inputs( diff --git a/src/webgpu/shaders/utils.js b/src/webgpu/shaders/utils.js deleted file mode 100644 index a6b79426e9..0000000000 --- a/src/webgpu/shaders/utils.js +++ /dev/null @@ -1,10 +0,0 @@ -export const getTexture = ` -fn getTexture(texture: texture_2d, texSampler: sampler, coord: vec2) -> vec4 { - let color = textureSample(texture, texSampler, coord); - let alpha = color.a; - return vec4( - select(color.rgb / alpha, vec3(0.0), alpha == 0.0), - alpha - ); -} -`; diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 44326d7d60..6912a134e0 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -1,6 +1,8 @@ -import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType } from "../strands/ir_types"; +import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType } from "../strands/ir_types"; import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag"; -import * as FES from '../strands/strands_FES' +import * as FES from '../strands/strands_FES'; +import * as build from '../strands/ir_builders'; +import { createStrandsNode } from '../strands/strands_node'; function shouldCreateTemp(dag, nodeID) { const nodeType = dag.nodeTypes[nodeID]; if (nodeType !== NodeType.OPERATION) return false; @@ -421,5 +423,32 @@ export const wgslBackend = { const type = strandsContext.cfg.blockTypes[blockID]; const handler = cfgHandlers[type] || cfgHandlers[BlockType.DEFAULT]; handler.call(cfgHandlers, blockID, strandsContext, generationContext); + }, + + createGetTextureCall(strandsContext, args) { + // In WebGPU, we need to add a sampler argument for the texture call + // First argument should be a texture, second should be coordinates + // We need to augment with a sampler argument based on the texture name + const textureArg = args[0]; + const coordsArg = args[1]; + + // Create a sampler variable node - add "_sampler" suffix to the texture identifier + const { dag } = strandsContext; + const textureNode = getNodeDataFromID(dag, textureArg.id); + const samplerIdentifier = textureNode.identifier + '_sampler'; + + const samplerVariable = build.variableNode(strandsContext, { baseType: BaseType.SAMPLER, dimension: 1 }, samplerIdentifier); + const samplerNode = createStrandsNode(samplerVariable.id, samplerVariable.dimension, strandsContext); + + // Create the augmented args: [texture, sampler, coords] + const augmentedArgs = [textureArg, samplerNode, coordsArg]; + + const { id, dimension } = build.functionCallNode(strandsContext, 'textureSample', augmentedArgs, { + overloads: [{ + params: [DataType.sampler2D, DataType.sampler, DataType.float2], + returnType: DataType.float4 + }] + }); + return { id, dimension }; } } From 6b7b0a7edf60cd87f5ee7b7d5901d1fc5a0c3f30 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 14:55:19 -0500 Subject: [PATCH 91/98] Migrate all filter shaders to p5.strands --- preview/index.html | 2 +- src/app.js | 6 + src/core/filterShaders.js | 177 ++++++++++++++++++++++++++++ src/core/p5.Renderer3D.js | 7 ++ src/image/filterRenderer2D.js | 106 +++++++---------- src/strands/p5.strands.js | 1 + src/strands/strands_api.js | 6 + src/strands/strands_conditionals.js | 3 + src/strands/strands_for.js | 3 + src/strands/strands_phi_utils.js | 7 +- src/webgl/index.js | 4 - src/webgl/p5.RendererGL.js | 102 +--------------- src/webgl/shaderHookUtils.js | 78 ++++++++++++ src/webgpu/shaders/filters/base.js | 14 +-- src/webgpu/strands_wgslBackend.js | 23 +++- 15 files changed, 361 insertions(+), 178 deletions(-) create mode 100644 src/core/filterShaders.js create mode 100644 src/webgl/shaderHookUtils.js diff --git a/preview/index.html b/preview/index.html index d4902b9de3..53fa4df90c 100644 --- a/preview/index.html +++ b/preview/index.html @@ -172,7 +172,7 @@ p.pop(); - p.filter(redFilter); + p.filter(p.BLUR, 10) }; }; diff --git a/src/app.js b/src/app.js index ce3d1fcd4a..b0fc757d67 100644 --- a/src/app.js +++ b/src/app.js @@ -54,6 +54,12 @@ webgl(p5); import type from './type'; type(p5); +// Shaders + filters +import shader from './webgl/p5.Shader'; +p5.registerAddon(shader); +import strands from './strands/p5.strands'; +p5.registerAddon(strands); + import { waitForDocumentReady, waitingForTranslator, _globalInit } from './core/init'; Promise.all([waitForDocumentReady(), waitingForTranslator]).then(_globalInit); diff --git a/src/core/filterShaders.js b/src/core/filterShaders.js new file mode 100644 index 0000000000..4ba28d3e53 --- /dev/null +++ b/src/core/filterShaders.js @@ -0,0 +1,177 @@ +import * as constants from './constants'; + +/* + * Creates p5.strands filter shaders for cross-platform compatibility + */ +export function makeFilterShader(renderer, operation, p5) { + switch (operation) { + case constants.GRAY: + return renderer.baseFilterShader().modify(() => { + p5.getColor((inputs, canvasContent) => { + const tex = p5.getTexture(canvasContent, inputs.texCoord); + // weighted grayscale with luminance values + const gray = p5.dot(tex.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); + return p5.vec4(gray, gray, gray, tex.a); + }); + }, { p5 }); + + case constants.INVERT: + return renderer.baseFilterShader().modify(() => { + p5.getColor((inputs, canvasContent) => { + const color = p5.getTexture(canvasContent, inputs.texCoord); + const invertedColor = p5.vec3(1.0) - color.rgb; + return p5.vec4(invertedColor, color.a); + }); + }, { p5 }); + + case constants.THRESHOLD: + return renderer.baseFilterShader().modify(() => { + const filterParameter = p5.uniformFloat(); + p5.getColor((inputs, canvasContent) => { + const color = p5.getTexture(canvasContent, inputs.texCoord); + // weighted grayscale with luminance values + const gray = p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); + const threshold = p5.floor(filterParameter * 255.0) / 255; + const blackOrWhite = p5.step(threshold, gray); + return p5.vec4(p5.vec3(blackOrWhite), color.a); + }); + }, { p5 }); + + case constants.POSTERIZE: + return renderer.baseFilterShader().modify(() => { + const filterParameter = p5.uniformFloat(); + const quantize = (color, n) => { + // restrict values to N options/bins + // and floor each channel to nearest value + // + // eg. when N = 5, values = 0.0, 0.25, 0.50, 0.75, 1.0 + // then quantize (0.1, 0.7, 0.9) -> (0.0, 0.5, 1.0) + + color = color * n; + color = p5.floor(color); + color = color / (n - 1.0); + return color; + }; + p5.getColor((inputs, canvasContent) => { + const color = p5.getTexture(canvasContent, inputs.texCoord); + const restrictedColor = quantize(color.rgb, filterParameter); + return p5.vec4(restrictedColor, color.a); + }); + }, { p5 }); + + case constants.BLUR: + return renderer.baseFilterShader().modify(() => { + const radius = p5.uniformFloat(); + const direction = p5.uniformVec2(); + + // This isn't a real Gaussian weight, it's a quadratic weight + const quadWeight = (x, e) => { + return p5.pow(e - p5.abs(x), 2.0); + }; + + p5.getColor((inputs, canvasContent) => { + const uv = inputs.texCoord; + + // A reasonable maximum number of samples + const maxSamples = 64.0; + + let numSamples = p5.floor(radius * 7.0); + if (p5.mod(numSamples, 2) == 0.0) { + numSamples++; + } + + let avg = p5.vec4(0.0); + let total = 0.0; + + // Calculate the spacing to avoid skewing if numSamples > maxSamples + let spacing = 1.0; + if (numSamples > maxSamples) { + spacing = numSamples / maxSamples; + numSamples = maxSamples; + } + + for (let i = 0; i < numSamples; i++) { + const sample = i * spacing - (numSamples - 1.0) * 0.5 * spacing; + const sampleCoord = uv + p5.vec2(sample, sample) / inputs.canvasSize * direction; + const weight = quadWeight(sample, (numSamples - 1.0) * 0.5 * spacing); + + avg += weight * p5.getTexture(canvasContent, sampleCoord); + total += weight; + } + + return avg / total; + }); + }, { p5 }); + + case constants.ERODE: + return renderer.baseFilterShader().modify(() => { + const luma = (color) => { + return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); + }; + + p5.getColor((inputs, canvasContent) => { + const uv = inputs.texCoord; + let minColor = p5.getTexture(canvasContent, uv); + let minLuma = luma(minColor); + + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + if (x != 0 && y != 0) { + const offset = p5.vec2(x, y) * inputs.texelSize; + const neighborColor = p5.getTexture(canvasContent, uv + offset); + const neighborLuma = luma(neighborColor); + + if (neighborLuma < minLuma) { + minLuma = neighborLuma; + minColor = neighborColor; + } + } + } + } + + return minColor; + }); + }, { p5 }); + + case constants.DILATE: + return renderer.baseFilterShader().modify(() => { + const luma = (color) => { + return p5.dot(color.rgb, p5.vec3(0.2126, 0.7152, 0.0722)); + }; + + p5.getColor((inputs, canvasContent) => { + const uv = inputs.texCoord; + let maxColor = p5.getTexture(canvasContent, uv); + let maxLuma = luma(maxColor); + + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + if (x != 0 && y != 0) { + const offset = p5.vec2(x, y) * inputs.texelSize; + const neighborColor = p5.getTexture(canvasContent, uv + offset); + const neighborLuma = luma(neighborColor); + + if (neighborLuma > maxLuma) { + maxLuma = neighborLuma; + maxColor = neighborColor; + } + } + } + } + + return maxColor; + }); + }, { p5 }); + + case constants.OPAQUE: + return renderer.baseFilterShader().modify(() => { + p5.getColor((inputs, canvasContent) => { + const color = p5.getTexture(canvasContent, inputs.texCoord); + return p5.vec4(color.rgb, 1.0); + }); + }, { p5 }); + + default: + throw new Error(`Unknown filter: ${operation}`); + } +} diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 29d3cb6a29..1dca9dc607 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -17,6 +17,7 @@ import { textCoreConstants } from "../type/textCore"; import { RenderBuffer } from "../webgl/p5.RenderBuffer"; import { Image } from "../image/p5.Image"; import { Texture } from "../webgl/p5.Texture"; +import { makeFilterShader } from "../core/filterShaders"; export function getStrokeDefs(shaderConstant) { const STROKE_CAP_ENUM = {}; @@ -1819,6 +1820,12 @@ export class Renderer3D extends Renderer { return dataArr; } + _makeFilterShader(renderer, operation) { + const p5 = this._pInst; + + return makeFilterShader(this, operation, p5); + } + remove() { if (this._textCanvas) { this._textCanvas.parentElement.removeChild(this._textCanvas); diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index fe47646851..d6bf72eed3 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -10,33 +10,27 @@ import { } from "../webgl/utils"; import * as constants from '../core/constants'; -import filterGrayFrag from '../webgl/shaders/filters/gray.frag'; -import filterErodeFrag from '../webgl/shaders/filters/erode.frag'; -import filterDilateFrag from '../webgl/shaders/filters/dilate.frag'; -import filterBlurFrag from '../webgl/shaders/filters/blur.frag'; -import filterPosterizeFrag from '../webgl/shaders/filters/posterize.frag'; -import filterOpaqueFrag from '../webgl/shaders/filters/opaque.frag'; -import filterInvertFrag from '../webgl/shaders/filters/invert.frag'; -import filterThresholdFrag from '../webgl/shaders/filters/threshold.frag'; -import filterShaderVert from '../webgl/shaders/filters/default.vert'; import { filterParamDefaults } from './const'; - import filterBaseFrag from '../webgl/shaders/filters/base.frag'; import filterBaseVert from '../webgl/shaders/filters/base.vert'; import webgl2CompatibilityShader from '../webgl/shaders/webgl2Compatibility.glsl'; +import { glslBackend } from '../webgl/strands_glslBackend'; +import { getShaderHookTypes } from '../webgl/shaderHookUtils'; +import noiseGLSL from '../webgl/shaders/functions/noise3DGLSL.glsl'; +import { makeFilterShader } from '../core/filterShaders'; class FilterRenderer2D { /** * Creates a new FilterRenderer2D instance. - * @param {p5} pInst - The p5.js instance. + * @param {p5} parentRenderer - The p5.js instance. */ - constructor(pInst) { - this.pInst = pInst; + constructor(parentRenderer) { + this.parentRenderer = parentRenderer; // Create a canvas for applying WebGL-based filters this.canvas = document.createElement('canvas'); - this.canvas.width = pInst.width; - this.canvas.height = pInst.height; + this.canvas.width = parentRenderer.width; + this.canvas.height = parentRenderer.height; // Initialize the WebGL context let webglVersion = constants.WEBGL2; @@ -232,23 +226,15 @@ class FilterRenderer2D { tex.bindTexture(); tex.update(); gl.uniform1i(uniform.location, uniform.samplerIndex); - } + }, + baseFilterShader: () => this.baseFilterShader(), + strandsBackend: glslBackend, + getShaderHookTypes: (shader, hookName) => getShaderHookTypes(shader, hookName), + uniformNameFromHookKey: (key) => key.slice(key.indexOf(' ') + 1), }; this._baseFilterShader = undefined; - // Store the fragment shader sources - this.filterShaderSources = { - [constants.BLUR]: filterBlurFrag, - [constants.INVERT]: filterInvertFrag, - [constants.THRESHOLD]: filterThresholdFrag, - [constants.ERODE]: filterErodeFrag, - [constants.GRAY]: filterGrayFrag, - [constants.DILATE]: filterDilateFrag, - [constants.POSTERIZE]: filterPosterizeFrag, - [constants.OPAQUE]: filterOpaqueFrag - }; - // Store initialized shaders for each operation this.filterShaders = {}; @@ -320,6 +306,10 @@ class FilterRenderer2D { return this._baseFilterShader; } + getNoiseShaderSnippet() { + return noiseGLSL; + } + /** * Set the current filter operation and parameter. If a customShader is provided, * that overrides the operation-based shader. @@ -363,18 +353,8 @@ class FilterRenderer2D { return; } - const fragShaderSrc = this.filterShaderSources[this.operation]; - if (!fragShaderSrc) { - console.error('No shader available for this operation:', this.operation); - return; - } - - // Create and store the new shader - const newShader = new Shader( - this._renderer, - filterShaderVert, - fragShaderSrc - ); + // Use the shared makeFilterShader function from filterShaders.js + const newShader = makeFilterShader(this._renderer, this.operation, this.parentRenderer._pInst); this.filterShaders[this.operation] = newShader; this._shader = newShader; } @@ -392,7 +372,7 @@ class FilterRenderer2D { get canvasTexture() { if (!this._canvasTexture) { - this._canvasTexture = new Texture(this._renderer, this.pInst.wrappedElt); + this._canvasTexture = new Texture(this._renderer, this.parentRenderer.wrappedElt); } return this._canvasTexture; } @@ -403,12 +383,12 @@ class FilterRenderer2D { _renderPass() { const gl = this.gl; this._shader.bindShader('fill'); - const pixelDensity = this.pInst.pixelDensity ? - this.pInst.pixelDensity() : 1; + const pixelDensity = this.parentRenderer.pixelDensity ? + this.parentRenderer.pixelDensity() : 1; const texelSize = [ - 1 / (this.pInst.width * pixelDensity), - 1 / (this.pInst.height * pixelDensity) + 1 / (this.parentRenderer.width * pixelDensity), + 1 / (this.parentRenderer.height * pixelDensity) ]; const canvasTexture = this.canvasTexture; @@ -416,15 +396,15 @@ class FilterRenderer2D { // Set uniforms for the shader this._shader.setUniform('tex0', canvasTexture); this._shader.setUniform('texelSize', texelSize); - this._shader.setUniform('canvasSize', [this.pInst.width, this.pInst.height]); + this._shader.setUniform('canvasSize', [this.parentRenderer.width, this.parentRenderer.height]); this._shader.setUniform('radius', Math.max(1, this.filterParameter)); this._shader.setUniform('filterParameter', this.filterParameter); this._shader.setDefaultUniforms(); - this.pInst.states.setValue('rectMode', constants.CORNER); - this.pInst.states.setValue('imageMode', constants.CORNER); - this.pInst.blendMode(constants.BLEND); - this.pInst.resetMatrix(); + this.parentRenderer.states.setValue('rectMode', constants.CORNER); + this.parentRenderer.states.setValue('imageMode', constants.CORNER); + this.parentRenderer.blendMode(constants.BLEND); + this.parentRenderer.resetMatrix(); const identityMatrix = [1, 0, 0, 0, @@ -459,8 +439,8 @@ class FilterRenderer2D { console.error('Cannot apply filter: shader not initialized.'); return; } - this.pInst.push(); - this.pInst.resetMatrix(); + this.parentRenderer.push(); + this.parentRenderer.resetMatrix(); // For blur, we typically do two passes: one horizontal, one vertical. if (this.operation === constants.BLUR && !this.customShader) { // Horizontal pass @@ -468,39 +448,39 @@ class FilterRenderer2D { this._renderPass(); // Draw the result onto itself - this.pInst.clear(); - this.pInst.drawingContext.drawImage( + this.parentRenderer.clear(); + this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, - this.pInst.width, this.pInst.height + this.parentRenderer.width, this.parentRenderer.height ); // Vertical pass this._shader.setUniform('direction', [0, 1]); this._renderPass(); - this.pInst.clear(); - this.pInst.drawingContext.drawImage( + this.parentRenderer.clear(); + this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, - this.pInst.width, this.pInst.height + this.parentRenderer.width, this.parentRenderer.height ); } else { // Single-pass filters this._renderPass(); - this.pInst.clear(); + this.parentRenderer.clear(); // con - this.pInst.blendMode(constants.BLEND); + this.parentRenderer.blendMode(constants.BLEND); - this.pInst.drawingContext.drawImage( + this.parentRenderer.drawingContext.drawImage( this.canvas, 0, 0, - this.pInst.width, this.pInst.height + this.parentRenderer.width, this.parentRenderer.height ); } - this.pInst.pop(); + this.parentRenderer.pop(); } } diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index b55429dc72..2537ab552c 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -41,6 +41,7 @@ function strands(p5, fn) { if (active) { p5.disableFriendlyErrors = true; } + ctx.p5 = p5; } function deinitStrandsContext(ctx) { diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 1c6ddd0a4c..ada09776dc 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -228,6 +228,12 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { const originalp5Fn = fn[typeInfo.fnName]; fn[typeInfo.fnName] = function(...args) { if (strandsContext.active) { + // For vector types with a single argument, repeat it for each component + if (typeInfo.dimension > 1 && args.length === 1 && !Array.isArray(args[0]) && + !(args[0] instanceof StrandsNode && args[0].dimension > 1) && + (typeInfo.baseType === BaseType.FLOAT || typeInfo.baseType === BaseType.INT || typeInfo.baseType === BaseType.BOOL)) { + args = Array(typeInfo.dimension).fill(args[0]); + } const { id, dimension } = build.primitiveConstructorNode(strandsContext, typeInfo, args); return createStrandsNode(id, dimension, strandsContext); } else if (originalp5Fn) { diff --git a/src/strands/strands_conditionals.js b/src/strands/strands_conditionals.js index cd40e8cd91..e9e6aca08e 100644 --- a/src/strands/strands_conditionals.js +++ b/src/strands/strands_conditionals.js @@ -70,6 +70,9 @@ function buildConditional(strandsContext, conditional) { branchBlocks.push(branchContentBlock); CFG.pushBlock(cfg, branchContentBlock); const branchResults = branchCallback(); + for (const key in branchResults) { + branchResults[key] = strandsContext.p5.strandsNode(branchResults[key]); + } for (const key in branchResults) { if (!phiBlockDependencies[key]) { phiBlockDependencies[key] = [{ value: branchResults[key], blockId: branchContentBlock }]; diff --git a/src/strands/strands_for.js b/src/strands/strands_for.js index 76eb703cbf..9d4092f39d 100644 --- a/src/strands/strands_for.js +++ b/src/strands/strands_for.js @@ -360,6 +360,9 @@ export class StrandsFor { const loopVarNode = createStrandsNode(phiNode.id, phiNode.dimension, this.strandsContext); this.bodyResults = this.bodyCb(loopVarNode, phiVars); + for (const key in this.bodyResults) { + this.bodyResults[key] = this.strandsContext.p5.strandsNode(this.bodyResults[key]); + } this.phiNodesForBody = phiNodesForBody; // Capture the final block after body execution before popping this.finalBodyBlock = cfg.currentBlock; diff --git a/src/strands/strands_phi_utils.js b/src/strands/strands_phi_utils.js index e73c4c34cd..1437e75b8d 100644 --- a/src/strands/strands_phi_utils.js +++ b/src/strands/strands_phi_utils.js @@ -9,7 +9,10 @@ export function createPhiNode(strandsContext, phiInputs, varName) { throw new Error(`No valid inputs for phi node for variable ${varName}`); } // Get dimension and baseType from first valid input - const firstInput = DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); + let firstInput = validInputs + .map((input) => DAG.getNodeDataFromID(strandsContext.dag, input.value.id)) + .find((input) => input.dimension) ?? + DAG.getNodeDataFromID(strandsContext.dag, validInputs[0].value.id); const dimension = firstInput.dimension; const baseType = firstInput.baseType; const nodeData = { @@ -27,4 +30,4 @@ export function createPhiNode(strandsContext, phiInputs, varName) { dimension, baseType }; -} \ No newline at end of file +} diff --git a/src/webgl/index.js b/src/webgl/index.js index 2f84a9ec19..b8f165f458 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -10,11 +10,9 @@ import matrix from '../math/p5.Matrix'; import geometry from './p5.Geometry'; import framebuffer from './p5.Framebuffer'; import dataArray from './p5.DataArray'; -import shader from './p5.Shader'; import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; -import strands from '../strands/p5.strands'; export default function(p5){ p5.registerAddon(rendererGL); @@ -31,7 +29,5 @@ export default function(p5){ p5.registerAddon(camera); p5.registerAddon(framebuffer); p5.registerAddon(dataArray); - p5.registerAddon(shader); p5.registerAddon(texture); - p5.registerAddon(strands); } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index ad4110b2de..1aeeaa3040 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -17,6 +17,7 @@ import { RGB, RGBA } from '../color/creating_reading'; import { Image } from '../image/p5.Image'; import { glslBackend } from './strands_glslBackend'; import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; +import { getShaderHookTypes } from './shaderHookUtils'; import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; import filterBaseVert from "./shaders/filters/base.vert"; @@ -37,17 +38,7 @@ import lineFrag from "./shaders/line.frag"; import imageLightVert from "./shaders/imageLight.vert"; import imageLightDiffusedFrag from "./shaders/imageLightDiffused.frag"; import imageLightSpecularFrag from "./shaders/imageLightSpecular.frag"; - import filterBaseFrag from "./shaders/filters/base.frag"; -import filterGrayFrag from "./shaders/filters/gray.frag"; -import filterErodeFrag from "./shaders/filters/erode.frag"; -import filterDilateFrag from "./shaders/filters/dilate.frag"; -import filterBlurFrag from "./shaders/filters/blur.frag"; -import filterPosterizeFrag from "./shaders/filters/posterize.frag"; -import filterOpaqueFrag from "./shaders/filters/opaque.frag"; -import filterInvertFrag from "./shaders/filters/invert.frag"; -import filterThresholdFrag from "./shaders/filters/threshold.frag"; -import filterShaderVert from "./shaders/filters/default.vert"; const { lineDefs } = getStrokeDefs((n, v) => `#define ${n} ${v}\n`); @@ -75,17 +66,6 @@ for (const key in defaultShaders) { defaultShaders[key] = webgl2CompatibilityShader + defaultShaders[key]; } -const filterShaderFrags = { - [constants.GRAY]: filterGrayFrag, - [constants.ERODE]: filterErodeFrag, - [constants.DILATE]: filterDilateFrag, - [constants.BLUR]: filterBlurFrag, - [constants.POSTERIZE]: filterPosterizeFrag, - [constants.OPAQUE]: filterOpaqueFrag, - [constants.INVERT]: filterInvertFrag, - [constants.THRESHOLD]: filterThresholdFrag, -}; - /** * 3D graphics class * @private @@ -1021,14 +1001,6 @@ class RendererGL extends Renderer3D { } } - _makeFilterShader(renderer, operation) { - return new Shader( - renderer, - filterShaderVert, - filterShaderFrags[operation] - ); - } - _prepareBuffer(renderBuffer, geometry, shader) { const attributes = shader.attributes; const gl = this.GL; @@ -1329,77 +1301,7 @@ class RendererGL extends Renderer3D { } getShaderHookTypes(shader, hookName) { - let fullSrc = shader._vertSrc; - let body = shader.hooks.vertex[hookName]; - if (!body) { - body = shader.hooks.fragment[hookName]; - fullSrc = shader._fragSrc; - } - 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 => { - const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); - if (!structDefMatch) return undefined; - const properties = []; - for (const defSrc of structDefMatch[1].split(';')) { - // E.g. `int var1, var2;` or `MyStruct prop;` - const parts = defSrc.trim().split(/\s+|,/g); - const typeName = parts.shift(); - const names = [...parts]; - const typeProperties = structProperties(typeName); - for (const name of names) { - const dataType = TypeInfoFromGLSLName[typeName] || null; - properties.push({ - name, - type: { - typeName, - qualifiers: [], - properties: typeProperties, - dataType, - } - }); - } - } - return properties; - }; - const parameters = parameterMatch[1].split(',').map(paramString => { - // e.g. `int prop` or `in sampler2D prop` or `const float prop` - const parts = paramString.trim().split(/\s+/g); - const name = parts.pop(); - const typeName = parts.pop(); - const qualifiers = [...parts]; - const properties = structProperties(typeName); - const dataType = TypeInfoFromGLSLName[typeName] || null; - return { - name, - type: { - typeName, - qualifiers, - properties, - dataType, - } - }; - }); - const dataType = TypeInfoFromGLSLName[returnType] || null; - return { - name: functionName, - returnType: { - typeName: returnType, - qualifiers: returnQualifiers, - properties: structProperties(returnType), - dataType, - }, - parameters - }; + return getShaderHookTypes(shader, hookName); } ////////////////////////////////////////////// diff --git a/src/webgl/shaderHookUtils.js b/src/webgl/shaderHookUtils.js new file mode 100644 index 0000000000..57b5a03b26 --- /dev/null +++ b/src/webgl/shaderHookUtils.js @@ -0,0 +1,78 @@ +import { TypeInfoFromGLSLName } from '../strands/ir_types'; + +/** + * Shared utility function for parsing shader hook types from GLSL shader source + */ +export function getShaderHookTypes(shader, hookName) { + let fullSrc = shader._vertSrc; + let body = shader.hooks.vertex[hookName]; + if (!body) { + body = shader.hooks.fragment[hookName]; + fullSrc = shader._fragSrc; + } + 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 => { + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); + if (!structDefMatch) return undefined; + const properties = []; + for (const defSrc of structDefMatch[1].split(';')) { + // E.g. `int var1, var2;` or `MyStruct prop;` + const parts = defSrc.trim().split(/\s+|,/g); + const typeName = parts.shift(); + const names = [...parts]; + const typeProperties = structProperties(typeName); + for (const name of names) { + const dataType = TypeInfoFromGLSLName[typeName] || null; + properties.push({ + name, + type: { + typeName, + qualifiers: [], + properties: typeProperties, + dataType, + } + }); + } + } + return properties; + }; + const parameters = parameterMatch[1].split(',').map(paramString => { + // e.g. `int prop` or `in sampler2D prop` or `const float prop` + const parts = paramString.trim().split(/\s+/g); + const name = parts.pop(); + const typeName = parts.pop(); + const qualifiers = [...parts]; + const properties = structProperties(typeName); + const dataType = TypeInfoFromGLSLName[typeName] || null; + return { + name, + type: { + typeName, + qualifiers, + properties, + dataType, + } + }; + }); + const dataType = TypeInfoFromGLSLName[returnType] || null; + return { + name: functionName, + returnType: { + typeName: returnType, + qualifiers: returnQualifiers, + properties: structProperties(returnType), + dataType, + }, + parameters + }; +} \ No newline at end of file diff --git a/src/webgpu/shaders/filters/base.js b/src/webgpu/shaders/filters/base.js index cd9ee59319..ec9c474731 100644 --- a/src/webgpu/shaders/filters/base.js +++ b/src/webgpu/shaders/filters/base.js @@ -1,12 +1,12 @@ const filterUniforms = ` -struct FilterUniforms { +struct Uniforms { uModelViewMatrix: mat4x4, uProjectionMatrix: mat4x4, canvasSize: vec2, texelSize: vec2, } -@group(0) @binding(0) var uniforms: FilterUniforms; +@group(0) @binding(0) var uniforms: Uniforms; @group(0) @binding(1) var tex0: texture_2d; @group(0) @binding(2) var tex0_sampler: sampler; `; @@ -25,7 +25,7 @@ struct VertexOutput { @vertex fn main(input: VertexInput) -> VertexOutput { var output: VertexOutput; - + // transferring texcoords for the frag shader output.vTexCoord = input.aTexCoord; @@ -34,7 +34,7 @@ fn main(input: VertexInput) -> VertexOutput { // project to 3D space output.position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4; - + return output; } `; @@ -61,11 +61,11 @@ fn main(input: FragmentInput) -> FragmentOutput { inputs.texCoord = input.vTexCoord; inputs.canvasSize = uniforms.canvasSize; inputs.texelSize = uniforms.texelSize; - + var outColor = HOOK_getColor(inputs, tex0, tex0_sampler); outColor = vec4(outColor.rgb * outColor.a, outColor.a); output.color = outColor; - + return output; } -`; \ No newline at end of file +`; diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 6912a134e0..0942da7b64 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -71,7 +71,15 @@ const cfgHandlers = { const T = extractNodeTypeInfo(dag, nodeID); const typeName = wgslBackend.getTypeName(T.baseType, T.dimension); // Initialize with default value - WGSL requires initialization - const defaultValue = T.baseType === 'float' ? '0.0' : '0'; + let defaultValue; + if (T.dimension === 1) { + defaultValue = T.baseType === 'float' ? '0.0' : '0'; + } else { + // For vector types, use constructor with repeated scalar values + const scalarDefault = T.baseType === 'float' ? '0.0' : '0'; + const components = Array(T.dimension).fill(scalarDefault).join(', '); + defaultValue = `${typeName}(${components})`; + } generationContext.write(`var ${tmp}: ${typeName} = ${defaultValue};`); } } @@ -346,6 +354,19 @@ export const wgslBackend = { return `${T}(${deps.join(', ')})`; } if (node.opCode === OpCode.Nary.FUNCTION_CALL) { + // Convert mod() function calls to % operator in WGSL + if (node.identifier === 'mod' && node.dependsOn.length === 2) { + const [leftID, rightID] = node.dependsOn; + const left = this.generateExpression(generationContext, dag, leftID); + const right = this.generateExpression(generationContext, dag, rightID); + const useParantheses = node.usedBy.length > 0; + if (useParantheses) { + return `(${left} % ${right})`; + } else { + return `${left} % ${right}`; + } + } + const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); return `${node.identifier}(${functionArgs.join(', ')})`; } From 11e7a1e33e2dd14771d81333605d6a03537934d7 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 14:57:05 -0500 Subject: [PATCH 92/98] Remove glsl-specific implementations --- src/webgl/shaders/filters/blur.frag | 60 ------------------------ src/webgl/shaders/filters/default.vert | 18 ------- src/webgl/shaders/filters/dilate.frag | 39 --------------- src/webgl/shaders/filters/erode.frag | 39 --------------- src/webgl/shaders/filters/gray.frag | 16 ------- src/webgl/shaders/filters/invert.frag | 15 ------ src/webgl/shaders/filters/opaque.frag | 12 ----- src/webgl/shaders/filters/posterize.frag | 29 ------------ src/webgl/shaders/filters/threshold.frag | 23 --------- 9 files changed, 251 deletions(-) delete mode 100644 src/webgl/shaders/filters/blur.frag delete mode 100644 src/webgl/shaders/filters/default.vert delete mode 100644 src/webgl/shaders/filters/dilate.frag delete mode 100644 src/webgl/shaders/filters/erode.frag delete mode 100644 src/webgl/shaders/filters/gray.frag delete mode 100644 src/webgl/shaders/filters/invert.frag delete mode 100644 src/webgl/shaders/filters/opaque.frag delete mode 100644 src/webgl/shaders/filters/posterize.frag delete mode 100644 src/webgl/shaders/filters/threshold.frag diff --git a/src/webgl/shaders/filters/blur.frag b/src/webgl/shaders/filters/blur.frag deleted file mode 100644 index 2823043b1f..0000000000 --- a/src/webgl/shaders/filters/blur.frag +++ /dev/null @@ -1,60 +0,0 @@ -precision highp float; - -// Two-pass blur filter, unweighted kernel. -// See also a similar blur at Adam Ferriss' repo of shader examples: -// https://github.com/aferriss/p5jsShaderExamples/blob/gh-pages/4_image-effects/4-9_single-pass-blur/effect.frag - - -uniform sampler2D tex0; -varying vec2 vTexCoord; -uniform vec2 direction; -uniform vec2 canvasSize; -uniform float radius; - -float random(vec2 p) { - vec3 p3 = fract(vec3(p.xyx) * .1031); - p3 += dot(p3, p3.yzx + 33.33); - return fract((p3.x + p3.y) * p3.z); -} - -// This isn't a real Gaussian weight, it's a quadratic weight. It's what the -// CPU mode's blur uses though, so we also use it here to match. -float quadWeight(float x, float e) { - return pow(e-abs(x), 2.); -} - -void main(){ - vec2 uv = vTexCoord; - - // A reasonable maximum number of samples - const float maxSamples = 64.0; - - float numSamples = floor(7. * radius); - if (fract(numSamples / 2.) == 0.) { - numSamples++; - } - vec4 avg = vec4(0.0); - float total = 0.0; - - // Calculate the spacing to avoid skewing if numSamples > maxSamples - float spacing = 1.0; - if (numSamples > maxSamples) { - spacing = numSamples / maxSamples; - numSamples = maxSamples; - } - - float randomOffset = (spacing - 1.0) * mix(-0.5, 0.5, random(gl_FragCoord.xy)); - for (float i = 0.0; i < maxSamples; i++) { - if (i >= numSamples) break; - - float sample = i * spacing - (numSamples - 1.0) * 0.5 * spacing + randomOffset; - vec2 sampleCoord = uv + vec2(sample, sample) / canvasSize * direction; - float weight = quadWeight(sample, (numSamples - 1.0) * 0.5 * spacing); - - avg += weight * texture2D(tex0, sampleCoord); - total += weight; - } - - avg /= total; - gl_FragColor = avg; -} diff --git a/src/webgl/shaders/filters/default.vert b/src/webgl/shaders/filters/default.vert deleted file mode 100644 index ee73804cec..0000000000 --- a/src/webgl/shaders/filters/default.vert +++ /dev/null @@ -1,18 +0,0 @@ -uniform mat4 uModelViewMatrix; -uniform mat4 uProjectionMatrix; - -attribute vec3 aPosition; -// texcoords only come from p5 to vertex shader -// so pass texcoords on to the fragment shader in a varying variable -attribute vec2 aTexCoord; -varying vec2 vTexCoord; - -void main() { - // transferring texcoords for the frag shader - vTexCoord = aTexCoord; - - // copy position with a fourth coordinate for projection (1.0 is normal) - vec4 positionVec4 = vec4(aPosition, 1.0); - - gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; -} diff --git a/src/webgl/shaders/filters/dilate.frag b/src/webgl/shaders/filters/dilate.frag deleted file mode 100644 index 3209c4a7ef..0000000000 --- a/src/webgl/shaders/filters/dilate.frag +++ /dev/null @@ -1,39 +0,0 @@ -// Increase the bright areas in an image - -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; -uniform vec2 texelSize; - -float luma(vec3 color) { - // weighted grayscale with luminance values - // weights 77, 151, 28 taken from src/image/filters.js - return dot(color, vec3(0.300781, 0.589844, 0.109375)); -} - -void main() { - vec4 color = texture2D(tex0, vTexCoord); - float lum = luma(color.rgb); - - // set current color as the brightest neighbor color - - vec4 neighbors[4]; - neighbors[0] = texture2D(tex0, vTexCoord + vec2( texelSize.x, 0.0)); - neighbors[1] = texture2D(tex0, vTexCoord + vec2(-texelSize.x, 0.0)); - neighbors[2] = texture2D(tex0, vTexCoord + vec2(0.0, texelSize.y)); - neighbors[3] = texture2D(tex0, vTexCoord + vec2(0.0, -texelSize.y)); - - for (int i = 0; i < 4; i++) { - vec4 neighborColor = neighbors[i]; - float neighborLum = luma(neighborColor.rgb); - - if (neighborLum > lum) { - color = neighborColor; - lum = neighborLum; - } - } - - gl_FragColor = color; -} diff --git a/src/webgl/shaders/filters/erode.frag b/src/webgl/shaders/filters/erode.frag deleted file mode 100644 index 6be0e78525..0000000000 --- a/src/webgl/shaders/filters/erode.frag +++ /dev/null @@ -1,39 +0,0 @@ -// Reduces the bright areas in an image - -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; -uniform vec2 texelSize; - -float luma(vec3 color) { - // weighted grayscale with luminance values - // weights 77, 151, 28 taken from src/image/filters.js - return dot(color, vec3(0.300781, 0.589844, 0.109375)); -} - -void main() { - vec4 color = texture2D(tex0, vTexCoord); - float lum = luma(color.rgb); - - // set current color as the darkest neighbor color - - vec4 neighbors[4]; - neighbors[0] = texture2D(tex0, vTexCoord + vec2( texelSize.x, 0.0)); - neighbors[1] = texture2D(tex0, vTexCoord + vec2(-texelSize.x, 0.0)); - neighbors[2] = texture2D(tex0, vTexCoord + vec2(0.0, texelSize.y)); - neighbors[3] = texture2D(tex0, vTexCoord + vec2(0.0, -texelSize.y)); - - for (int i = 0; i < 4; i++) { - vec4 neighborColor = neighbors[i]; - float neighborLum = luma(neighborColor.rgb); - - if (neighborLum < lum) { - color = neighborColor; - lum = neighborLum; - } - } - - gl_FragColor = color; -} diff --git a/src/webgl/shaders/filters/gray.frag b/src/webgl/shaders/filters/gray.frag deleted file mode 100644 index 815388c746..0000000000 --- a/src/webgl/shaders/filters/gray.frag +++ /dev/null @@ -1,16 +0,0 @@ -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; - -float luma(vec3 color) { - // weighted grayscale with luminance values - return dot(color, vec3(0.2126, 0.7152, 0.0722)); -} - -void main() { - vec4 tex = texture2D(tex0, vTexCoord); - float gray = luma(tex.rgb); - gl_FragColor = vec4(gray, gray, gray, tex.a); -} diff --git a/src/webgl/shaders/filters/invert.frag b/src/webgl/shaders/filters/invert.frag deleted file mode 100644 index f260c21a77..0000000000 --- a/src/webgl/shaders/filters/invert.frag +++ /dev/null @@ -1,15 +0,0 @@ -// Set each pixel to inverse value -// Note that original INVERT does not change the opacity, so this follows suit - -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; - -void main() { -vec4 color = texture2D(tex0, vTexCoord); -vec3 origColor = color.rgb / color.a; -vec3 invertedColor = vec3(1.0) - origColor; -gl_FragColor = vec4(invertedColor * color.a, color.a); -} diff --git a/src/webgl/shaders/filters/opaque.frag b/src/webgl/shaders/filters/opaque.frag deleted file mode 100644 index 23e5c0f851..0000000000 --- a/src/webgl/shaders/filters/opaque.frag +++ /dev/null @@ -1,12 +0,0 @@ -// Set alpha channel to entirely opaque - -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; - -void main() { - vec4 color = texture2D(tex0, vTexCoord); - gl_FragColor = vec4(color.rgb / color.a, 1.0); -} diff --git a/src/webgl/shaders/filters/posterize.frag b/src/webgl/shaders/filters/posterize.frag deleted file mode 100644 index a232737547..0000000000 --- a/src/webgl/shaders/filters/posterize.frag +++ /dev/null @@ -1,29 +0,0 @@ -// Limit color space for a stylized cartoon / poster effect - -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; -uniform float filterParameter; - -vec3 quantize(vec3 color, float n) { - // restrict values to N options/bins - // and floor each channel to nearest value - // - // eg. when N = 5, values = 0.0, 0.25, 0.50, 0.75, 1.0 - // then quantize (0.1, 0.7, 0.9) -> (0.0, 0.5, 1.0) - - color = color * n; - color = floor(color); - color = color / (n - 1.0); - return color; -} - -void main() { - vec4 color = texture2D(tex0, vTexCoord); - - vec3 restrictedColor = quantize(color.rgb / color.a, filterParameter); - - gl_FragColor = vec4(restrictedColor.rgb * color.a, color.a); -} diff --git a/src/webgl/shaders/filters/threshold.frag b/src/webgl/shaders/filters/threshold.frag deleted file mode 100644 index 36234cfcbf..0000000000 --- a/src/webgl/shaders/filters/threshold.frag +++ /dev/null @@ -1,23 +0,0 @@ -// Convert pixels to either white or black, -// depending on if their luma is above or below filterParameter - -precision highp float; - -varying vec2 vTexCoord; - -uniform sampler2D tex0; -uniform float filterParameter; - -float luma(vec3 color) { - // weighted grayscale with luminance values - return dot(color, vec3(0.2126, 0.7152, 0.0722)); -} - -void main() { - vec4 color = texture2D(tex0, vTexCoord); - float gray = luma(color.rgb / color.a); - // floor() used to match src/image/filters.js - float threshold = floor(filterParameter * 255.0) / 255.0; - float blackOrWhite = step(threshold, gray); - gl_FragColor = vec4(vec3(blackOrWhite) * color.a, color.a); -} From 6ed13666eb78d27eba2736c3dbb78d127495a8c9 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 15:28:52 -0500 Subject: [PATCH 93/98] Add visual tests for all filters --- src/core/filterShaders.js | 4 +- src/strands/strands_transpiler.js | 24 ++++-- test/unit/visual/cases/webgl.js | 81 ++++++++++++++++++ test/unit/visual/cases/webgpu.js | 69 +++++++++++++++ .../It can apply BLUR with a value/000.png | Bin 0 -> 3120 bytes .../metadata.json | 3 + .../In 2d mode/It can apply BLUR/000.png | Bin 0 -> 3117 bytes .../It can apply BLUR/metadata.json | 3 + .../It can apply DILATE (4x)/000.png | Bin 0 -> 506 bytes .../It can apply DILATE (4x)/metadata.json | 3 + .../It can apply ERODE (4x)/000.png | Bin 0 -> 604 bytes .../It can apply ERODE (4x)/metadata.json | 3 + .../In 2d mode/It can apply GRAY/000.png | Bin 0 -> 640 bytes .../It can apply GRAY/metadata.json | 3 + .../In 2d mode/It can apply INVERT/000.png | Bin 0 -> 611 bytes .../It can apply INVERT/metadata.json | 3 + .../000.png | Bin 0 -> 462 bytes .../metadata.json | 3 + .../In 2d mode/It can apply POSTERIZE/000.png | Bin 0 -> 484 bytes .../It can apply POSTERIZE/metadata.json | 3 + .../000.png | Bin 0 -> 462 bytes .../metadata.json | 3 + .../In 2d mode/It can apply THRESHOLD/000.png | Bin 0 -> 484 bytes .../It can apply THRESHOLD/metadata.json | 3 + .../It can apply BLUR with a value/000.png | Bin 0 -> 1608 bytes .../metadata.json | 3 + .../In webgl mode/It can apply BLUR/000.png | Bin 0 -> 2578 bytes .../It can apply BLUR/metadata.json | 3 + .../It can apply DILATE (4x)/000.png | Bin 0 -> 475 bytes .../It can apply DILATE (4x)/metadata.json | 3 + .../It can apply ERODE (4x)/000.png | Bin 0 -> 630 bytes .../It can apply ERODE (4x)/metadata.json | 3 + .../In webgl mode/It can apply GRAY/000.png | Bin 0 -> 629 bytes .../It can apply GRAY/metadata.json | 3 + .../In webgl mode/It can apply INVERT/000.png | Bin 0 -> 614 bytes .../It can apply INVERT/metadata.json | 3 + .../000.png | Bin 0 -> 461 bytes .../metadata.json | 3 + .../It can apply POSTERIZE/000.png | Bin 0 -> 482 bytes .../It can apply POSTERIZE/metadata.json | 3 + .../000.png | Bin 0 -> 461 bytes .../metadata.json | 3 + .../It can apply THRESHOLD/000.png | Bin 0 -> 482 bytes .../It can apply THRESHOLD/metadata.json | 3 + .../It can use filter shader hooks/000.png | Bin 489 -> 633 bytes .../It can apply BLUR with a value/000.png | Bin 0 -> 1453 bytes .../metadata.json | 3 + .../WebGPU/filters/It can apply BLUR/000.png | Bin 0 -> 2407 bytes .../filters/It can apply BLUR/metadata.json | 3 + .../filters/It can apply DILATE (4x)/000.png | Bin 0 -> 484 bytes .../It can apply DILATE (4x)/metadata.json | 3 + .../filters/It can apply ERODE (4x)/000.png | Bin 0 -> 632 bytes .../It can apply ERODE (4x)/metadata.json | 3 + .../WebGPU/filters/It can apply GRAY/000.png | Bin 0 -> 637 bytes .../filters/It can apply GRAY/metadata.json | 3 + .../filters/It can apply INVERT/000.png | Bin 0 -> 644 bytes .../filters/It can apply INVERT/metadata.json | 3 + .../000.png | Bin 0 -> 457 bytes .../metadata.json | 3 + .../filters/It can apply POSTERIZE/000.png | Bin 0 -> 482 bytes .../It can apply POSTERIZE/metadata.json | 3 + .../000.png | Bin 0 -> 457 bytes .../metadata.json | 3 + .../filters/It can apply THRESHOLD/000.png | Bin 0 -> 482 bytes .../It can apply THRESHOLD/metadata.json | 3 + 65 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply ERODE (4x)/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply ERODE (4x)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply GRAY/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply GRAY/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply INVERT/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply INVERT/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply DILATE (4x)/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply DILATE (4x)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply INVERT/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply INVERT/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/000.png create mode 100644 test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply BLUR with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply BLUR with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply BLUR/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply BLUR/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply DILATE (4x)/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply DILATE (4x)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply ERODE (4x)/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply ERODE (4x)/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply GRAY/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply GRAY/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply INVERT/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply INVERT/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/metadata.json create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/metadata.json diff --git a/src/core/filterShaders.js b/src/core/filterShaders.js index 4ba28d3e53..a19d9bc8d7 100644 --- a/src/core/filterShaders.js +++ b/src/core/filterShaders.js @@ -116,7 +116,7 @@ export function makeFilterShader(renderer, operation, p5) { for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { - if (x != 0 && y != 0) { + if (x != 0 || y != 0) { const offset = p5.vec2(x, y) * inputs.texelSize; const neighborColor = p5.getTexture(canvasContent, uv + offset); const neighborLuma = luma(neighborColor); @@ -146,7 +146,7 @@ export function makeFilterShader(renderer, operation, p5) { for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { - if (x != 0 && y != 0) { + if (x != 0 || y != 0) { const offset = p5.vec2(x, y) * inputs.texelSize; const neighborColor = p5.getTexture(canvasContent, uv + offset); const neighborLuma = luma(neighborColor); diff --git a/src/strands/strands_transpiler.js b/src/strands/strands_transpiler.js index f6d6167c4e..ec7e0137e0 100644 --- a/src/strands/strands_transpiler.js +++ b/src/strands/strands_transpiler.js @@ -3,6 +3,7 @@ import { ancestor, recursive } from 'acorn-walk'; import escodegen from 'escodegen'; import { UnarySymbolToName } from './ir_types'; let blockVarCounter = 0; +let loopVarCounter = 0; function replaceBinaryOperator(codeSource) { switch (codeSource) { case '+': return 'add'; @@ -563,6 +564,9 @@ const ASTCallbacks = { // Transform for statement into strandsFor() call // for (init; test; update) body -> strandsFor(initCb, conditionCb, updateCb, bodyCb, initialVars) + + // Generate unique loop variable name + const uniqueLoopVar = `loopVar${loopVarCounter++}`; // Create the initial callback from the for loop's init let initialFunction; @@ -608,14 +612,14 @@ const ASTCallbacks = { // Replace loop variable references with the parameter if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; - conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, 'loopVar'); + conditionBody = this.replaceIdentifierReferences(conditionBody, loopVarName, uniqueLoopVar); } const conditionAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: conditionBody }] }; conditionBody = conditionAst.body[0].expression; const conditionFunction = { type: 'ArrowFunctionExpression', - params: [{ type: 'Identifier', name: 'loopVar' }], + params: [{ type: 'Identifier', name: uniqueLoopVar }], body: conditionBody }; @@ -626,14 +630,14 @@ const ASTCallbacks = { // Replace loop variable references with the parameter if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; - updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, 'loopVar'); + updateExpr = this.replaceIdentifierReferences(updateExpr, loopVarName, uniqueLoopVar); } const updateAst = { type: 'Program', body: [{ type: 'ExpressionStatement', expression: updateExpr }] }; updateExpr = updateAst.body[0].expression; updateFunction = { type: 'ArrowFunctionExpression', - params: [{ type: 'Identifier', name: 'loopVar' }], + params: [{ type: 'Identifier', name: uniqueLoopVar }], body: { type: 'BlockStatement', body: [{ @@ -645,12 +649,12 @@ const ASTCallbacks = { } else { updateFunction = { type: 'ArrowFunctionExpression', - params: [{ type: 'Identifier', name: 'loopVar' }], + params: [{ type: 'Identifier', name: uniqueLoopVar }], body: { type: 'BlockStatement', body: [{ type: 'ReturnStatement', - argument: { type: 'Identifier', name: 'loopVar' } + argument: { type: 'Identifier', name: uniqueLoopVar } }] } }; @@ -665,13 +669,13 @@ const ASTCallbacks = { // Replace loop variable references in the body if (node.init?.type === 'VariableDeclaration') { const loopVarName = node.init.declarations[0].id.name; - bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, 'loopVar'); + bodyBlock = this.replaceIdentifierReferences(bodyBlock, loopVarName, uniqueLoopVar); } const bodyFunction = { type: 'ArrowFunctionExpression', params: [ - { type: 'Identifier', name: 'loopVar' }, + { type: 'Identifier', name: uniqueLoopVar }, { type: 'Identifier', name: 'vars' } ], body: bodyBlock @@ -921,6 +925,10 @@ const ASTCallbacks = { } } export function transpileStrandsToJS(p5, sourceString, srcLocations, scope) { + // Reset counters at the start of each transpilation + blockVarCounter = 0; + loopVarCounter = 0; + const ast = parse(sourceString, { ecmaVersion: 2021, locations: srcLocations diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 66a04198a5..d59af7a684 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -86,6 +86,87 @@ visualSuite('WebGL', function() { } ); + for (const mode of ['webgl', '2d']) { + visualSuite(`In ${mode} mode`, function() { + const setupSketch = (p5) => { + p5.createCanvas(50, 50, mode === 'webgl' ? p5.WEBGL : p5.P2D); + if (mode === 'webgl') p5.translate(-p5.width/2, -p5.height/2); + p5.clear(); + p5.fill('red'); + p5.circle(20, 20, 15); + if (mode === 'webgl') { + p5.beginShape(p5.QUAD_STRIP); + p5.fill('cyan'); + p5.vertex(35, 35); + p5.vertex(45, 35); + p5.fill('blue'); + p5.vertex(35, 45); + p5.vertex(45, 45); + p5.endShape(); + } else { + p5.push(); + const grad = p5.drawingContext.createLinearGradient(35, 35, 35, 45); + grad.addColorStop(0, 'cyan'); + grad.addColorStop(1, 'blue'); + p5.drawingContext.fillStyle = grad; + p5.rect(35, 35, 10, 10); + p5.pop(); + } + }; + + visualTest('It can apply GRAY', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.GRAY); + screenshot(); + }); + visualTest('It can apply INVERT', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.INVERT); + screenshot(); + }); + visualTest('It can apply THRESHOLD', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.THRESHOLD); + screenshot(); + }); + visualTest('It can apply THRESHOLD with a value', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.THRESHOLD, 0.8); + screenshot(); + }); + visualTest('It can apply POSTERIZE', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.THRESHOLD); + screenshot(); + }); + visualTest('It can apply POSTERIZE with a value', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.THRESHOLD, 2); + screenshot(); + }); + visualTest('It can apply BLUR', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.BLUR, 5); + screenshot(); + }); + visualTest('It can apply BLUR with a value', function(p5, screenshot) { + setupSketch(p5); + p5.filter(p5.BLUR, 10); + screenshot(); + }); + visualTest('It can apply ERODE (4x)', function(p5, screenshot) { + setupSketch(p5); + for (let i = 0; i < 4; i++) p5.filter(p5.ERODE); + screenshot(); + }); + visualTest('It can apply DILATE (4x)', function(p5, screenshot) { + setupSketch(p5); + for (let i = 0; i < 4; i++) p5.filter(p5.DILATE); + screenshot(); + }); + }); + } + for (const mode of ['webgl', '2d']) { visualSuite(`In ${mode} mode`, function() { visualTest('It can use filter shader hooks', function(p5, screenshot) { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 8eef145710..a03a6765ca 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -116,6 +116,75 @@ visualSuite("WebGPU", function () { ); }); + visualSuite('filters', function() { + const setupSketch = async (p5) => { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.translate(-p5.width/2, -p5.height/2); + p5.clear(); + p5.fill('red'); + p5.circle(20, 20, 15); + p5.beginShape(p5.QUAD_STRIP); + p5.fill('cyan'); + p5.vertex(35, 35); + p5.vertex(45, 35); + p5.fill('blue'); + p5.vertex(35, 45); + p5.vertex(45, 45); + p5.endShape(); + }; + + visualTest('It can apply GRAY', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.GRAY); + await screenshot(); + }); + visualTest('It can apply INVERT', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.INVERT); + await screenshot(); + }); + visualTest('It can apply THRESHOLD', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.THRESHOLD); + await screenshot(); + }); + visualTest('It can apply THRESHOLD with a value', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.THRESHOLD, 0.8); + await screenshot(); + }); + visualTest('It can apply POSTERIZE', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.THRESHOLD); + await screenshot(); + }); + visualTest('It can apply POSTERIZE with a value', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.THRESHOLD, 2); + await screenshot(); + }); + visualTest('It can apply BLUR', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.BLUR, 5); + await screenshot(); + }); + visualTest('It can apply BLUR with a value', async function(p5, screenshot) { + await setupSketch(p5); + p5.filter(p5.BLUR, 10); + await screenshot(); + }); + visualTest('It can apply ERODE (4x)', async function(p5, screenshot) { + await setupSketch(p5); + for (let i = 0; i < 4; i++) p5.filter(p5.ERODE); + await screenshot(); + }); + visualTest('It can apply DILATE (4x)', async function(p5, screenshot) { + await setupSketch(p5); + for (let i = 0; i < 4; i++) p5.filter(p5.DILATE); + await screenshot(); + }); + }); + visualSuite("Canvas Resizing", function () { visualTest( "Main canvas drawing after resize", diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/000.png new file mode 100644 index 0000000000000000000000000000000000000000..fb81ee07e721249e4b74a79fbf568d97786bebdc GIT binary patch literal 3120 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 KMNUMnLSTZ0BIDcu literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/000.png new file mode 100644 index 0000000000000000000000000000000000000000..cdbbbff03c6a6eb003be7bca2a17b524370569f1 GIT binary patch literal 3117 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 Hu0mjfD9PJ0 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/000.png new file mode 100644 index 0000000000000000000000000000000000000000..f480f88aa0d28fcc68cede3b65f42f0191395009 GIT binary patch literal 506 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2o;fFs}D>aSW+od^5w*?}!19tE?W| zoR;DPZAC^8)Yd#;*wErM>r$y#(UYZ(M_$YlIuXn~;rfGFKwaSi z3!@*4mv-uCG!`882v^Duly+uoR6eTBVERUEx7+TsB(|>=N!+v|xqC!L7R)_%G*t=XlSN*zPbvy!`&d*o(reC;d}cEzrF6bn@FB zDv@`~Q_e~jNgNkUuNFFT^T3BMB`SNCo@=U={b>`p|8L_j^I0n&M=iEL!Jc6Av9;#T zU&nh34+%S49LlS@lkiSJLF8d!^UgJl!N28o%dQHSlyV3FDJ1ESZQ>dJ8{EDx{%R}w u+p)sk_%|y9!~g$UTDMq$F~|mrI%bB={*onp+%dO+A`G6celF{r5}E*}Vb~%7 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply ERODE (4x)/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply ERODE (4x)/000.png new file mode 100644 index 0000000000000000000000000000000000000000..677db991572190bf35ba2afcaa0efe087f0ff961 GIT binary patch literal 604 zcmV-i0;BzjP)Mh zdonYGQKN0_S|bEN>?p)@V#pQcsgsLatwI2+4d7Y~#KaJR_gdm1)+z)*hjR}wV#u*~ z?|IHVa@`04>`pX*v9ApQN*Ou!p5y$YL{~!i5Ciy6j3^5U0iP4WwMuWlS{^O&8bC1r ziqWC?bESeJRD}%hli(VqlnGcZLcjnBFkm^ClNo+6lG+p*izBso&LP$!r2egNSX1!G z^F;flj;)O^YFvcM#^}bbMM(X_=Qsm60!C8chnN@0lj2%!w_hzn0P+q)R0AWxW9;i3 z*GMbr?X6Xa1Ggwf8ta#9D*^LT3SGj{%M+#s`}{G0yI}hIojz3IWE!0Obrc zB6x&2^9Yh7?zO~2Y>g0r0OYp>*uNt9V2@v7l*ZaxAx?)Npb>?!C&pB~Ce&{wBouPa z%~QynF6~*96WO!dbKa|`iSRAA#3uTB5;6f>dPpHlI~HA^6N&EdaR0|cu?N|3xcsNr ziTqH%%VBcYR^LKC4oCZxT~nca-$Dd9ccqZIo!YA|Clb)V+45QV4R!v(kiT!1a~0B{Cwl5jG@mJ7f#8 z&(BHWliuVff`n#dbR;_B=>+1{@^rB3WuqhNqw_Q|I^yYI)yuxZkumR{ax6H$CD6tg zcmJo<1%cQ_x7$qygTZ(>91;TUUayxh0>F^NO~0iMNGbr-@As+G>D-l~%^+7Ot^wp7 zMhlWc;3L4DIzzwH;aMjoq%n1r)B%&b9 zI>eQc^#sR6jU!{6u;V_rmqAdDlrc0qu5lzg9*@qVWlrWi(Q<+YWVzjLEtmgII~)$^ zEb+@nXh1TEk`uPpB=Fw;m#9Ek{al1`^Ldfn`SVPDh6V(?rR>U67`V3CY=}Vs;mr*Q z(t==a3W2ND3PK5@F^E7L0PFRd0L)`vA$WbN1A_Sw_yHgmodF7gwg7x_5^}imTk3+` z)IlsnfA9bTZH#$)-SR$0c@>DUXcL_X(!{A(?HL`ZI-Mr|7Lp@)%ejPcULk?dY&J`5 z!XS`NP$1{?nI@A7O{de+`gA&pbh8Q$q$moiip%9<5=bvNklk)aRRPyTyHN!PvfuBo zj+E7{xms|eD2l5qxMGj}RycywhWM&z1qejKfvP}`@o-2dsAmLkq_St@Hr@&lT&+CM zDX)lDfZ$uDO~`FBs*tXh%jYcQ1`A>cbM1uWh^uxZ|7}2i00030|CSUXjQ{`u21!Ig aR09BjVxDxU!qBP!0000)TJ|;SUO@VV7#YE9caCvH2R1 z+E+JrjUms^sfPoDod)EdjDJ0^0qHbl^>aEaS`EmpQK<1RhvxN)T1^AevAXoA6?_u^ zh*VX@!JpZ7NCttJ4iegwuJ~O4!-d%fmJC7-fN|)^A`EimiV04nob-g$U9Vq{16>swL{(DwL^eVfgdUo5mE35mk1fvG)>0N%(ztad-;%037M^t)sgB*k^^;X zB^m23TODy*my}o?Nix=5_7xrZmECs|2Z{30T7fgog`@dE?)87{Q$6#_8`YBvW{LR2R2KC? zk=%a8b=K37h^vv@m2&^&5z?=;EN`9n2ss8AM{ z{jh3((Ua4!)S2~kgfcj5AqJKla#O-91TqkVAmdSh0XxEk*CImL9D-jmfb9KmhcR=1 zvIa&@xkpsUjU8kmjVOXKa|pU&QqQjvl1frjyo6|6+7eS8+0t#zUNd!sr<^4keKQGx z7_Y;c0NccDLKw@OVs2%SlY<$o5~ zM$Ak?8e-Oq5@K~M8-D=+0RR6kiY=-D000I_L_t&o00vz)Wkacr^8f$<07*qoM6N<$ Eg2U#+%>V!Z literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE with a value/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply POSTERIZE/000.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d17e6b23a79c11e0a477b4d476af32832cb893 GIT binary patch literal 484 zcmVGT>yzZ|byGBw1q5ZG=44i|2eh|Jz-qzNCNh2_dr$GA0reaq2+a zT275smyL<2Tj!J*6LD&+y6iJ1@+PNW#J^5j2yKi?ACpf#0v0gxpyvybz*jnrq5000030|3L)Vj{pDw21!Ig aR09C4T~1|&nTKou0000fgdUo5mE35mk1fvG)>0N%(ztad-;%037M^t)sgB*k^^;X zB^m23TODy*my}o?Nix=5_7xrZmECs|2Z{30T7fgog`@dE?)87{Q$6#_8`YBvW{LR2R2KC? zk=%a8b=K37h^vv@m2&^&5z?=;EN`9n2ss8AM{ z{jh3((Ua4!)S2~kgfcj5AqJKla#O-91TqkVAmdSh0XxEk*CImL9D-jmfb9KmhcR=1 zvIa&@xkpsUjU8kmjVOXKa|pU&QqQjvl1frjyo6|6+7eS8+0t#zUNd!sr<^4keKQGx z7_Y;c0NccDLKw@OVs2%SlY<$o5~ zM$Ak?8e-Oq5@K~M8-D=+0RR6kiY=-D000I_L_t&o00vz)Wkacr^8f$<07*qoM6N<$ Eg2U#+%>V!Z literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD/000.png new file mode 100644 index 0000000000000000000000000000000000000000..f1d17e6b23a79c11e0a477b4d476af32832cb893 GIT binary patch literal 484 zcmVGT>yzZ|byGBw1q5ZG=44i|2eh|Jz-qzNCNh2_dr$GA0reaq2+a zT275smyL<2Tj!J*6LD&+y6iJ1@+PNW#J^5j2yKi?ACpf#0v0gxpyvybz*jnrq5000030|3L)Vj{pDw21!Ig aR09C4T~1|&nTKou0000W}Br@KpXlQc$@|*V&7q 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply DILATE (4x)/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply DILATE (4x)/000.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b9a71fda886644122caffba9e7b875c8231005 GIT binary patch literal 475 zcmV<10VMv3P)W$mz{ZJ!@XaZyxRwIgOGBiljf*x3S+PNk zVkQz(3c{zNfVJP)Z(X4aTSp3QZ1}dhI&zeQcj@~g=IEgHCg)lZ05H$^A;%|)(*b}X zN35vUfVjzC4yRzqb;?t#q1+h-VVB=z3xmutoWdw&#DJhn)KW*hh$0|7HWNS|sR_us?;FWH;sop0V9e|k66+Zu_ zq$BgJihy7Mqv^=pjkEL2sbM6C=nOgL*%bw$D;RY{Za6hcj2cGjxQ4Naq9B-YV{X(T z{IO$A4j8ZHx*}Qwf*}m@ip4bR6oN;I95v*MYAuMHz_BNH35Te!B4@TQxi(T3Fo;s7I)cLLPjglhu97zPpZMXC)1^MD}cvsEJq<^e&> zXQ@^Y%pdLL*qz1d57i1X*#6Z2qG(t>{Hz<~>(m>1h%hAU1{plg`fMPxwrRtnnmV%N zbvvr9355P(aE_?GQEebM=9VlP$dcXKqV@*>0RR7;mnyvg000I_L_t&o0PKErWdbkx QkpKVy07*qoM6N<$g7onmKmY&$ literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/000.png new file mode 100644 index 0000000000000000000000000000000000000000..991e412ea03f8ee866da1179ea13d706e4ba7979 GIT binary patch literal 629 zcmV-*0*d{KP)OvIx1So23P|-nPFv+mJL8jLkU74@u|o=BNK!&80;A{PGsbVKXYy0 z+3UL)^Et$Cvm0ml6$aS$cqG7yc#Ra6VOdBP0+JRmHTJ-eE$ITn#IX&`VzE$?MIeH@T7Zz1F$U?F0$>f{?QaPCyJm#B zDF$b=nF2_^%#17A0Psv|GT{}1O^bjqAA>akn$G9*DaHxf8lWa3G*bC3MM19aKBYlm zoS^*{0NFPyWr1XiRrwV`syzL?y<Q+JmQLR=>4TnQxpiw!Wpg@ktqgt=m#z2$9p@IY1Y&Oaie*)xiI2a)SL=${QPN&lq zKB;U1#9 P00000NkvXXu0mjfIvEk1 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply INVERT/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply INVERT/000.png new file mode 100644 index 0000000000000000000000000000000000000000..342f41f8a0058e7d4a114b9f63934ce74a14521a GIT binary patch literal 614 zcmV-s0-61ZP)fNnyuV7#*HSdZG{1=Y1C#CQp?CVWT)8e z1OTg$TEl29Scq;+sU%l(M0nOnXg&1NDp|e7Qg^cXF>QbLJWdOO%PP7 z_l+?ESVJ&azZR=E3xN;>3z^fMspJsH86!YT1sJfgeUT7PZg(<><4kWQz;n2Y5Wm@) zeHOB2_J_{S6&cFPn$MD|2)}f5d?;bb2th#gu1^(@`lK=D!hr3INk&LV?7MaDOYfg! z4yFU2|4B>8lgPbJI2b|tEj=MGqB!RNM#x=MWb<1M=Pp%cBg{Zc?`qveMY#W$D&e1l z$rTyMK$J(%rE_n3w;XdY1BWN`KT^w@54}8p>fLh8g%Olb$!Bts5dtd+&T+7PPfAFi zT*y2N3CY$ius;9*0RR6!7^NWq000I_L_t&o02@j7WhfNy5&!@I07*qoM6N<$f#9c3Z1h5BYeVBQR z3ORB65(k4ghQEmbsk>g!60#;6c`G3!yML>8P2^TrMn0QuBE0D|cGa_)gdl*=VU2-p z#LOh5A!c2a5NpS>@imd;4x7{0M9eKq4ycJFH`tthrit)x%~z$yz&2uM5&|&@Y7A^+ zvk7U)Sr#S4(ycA92LJ#7|Nqv4L`DDr00v1!K~w_(8#Xj$mu^Jz00000NkvXXu0mjf D8U(~9 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/000.png new file mode 100644 index 0000000000000000000000000000000000000000..4945855c91d7ac666a6aa4623a703aeeb56d17c3 GIT binary patch literal 482 zcmV<80UiE{P)GT>yzF9JbFlR5U%M95QIxyiTvza35LL;5Gb5VG1JVjnkJL!*8EJ7Zy$o0H> zVw-g=LbzJZq>V#QY$I(-79q8SQ}-=G-W`lRvCXm;As@|*lMXFHJpVkeqjXw?z{Ns{ zn5C{3LYNA~@HGaO7!B5hu}*|y5Q89o%fNtbVccgPAryyT7KbZ?iCY#v0yqOxHb*|Z zLcX~@i-SQN!#_oU)Gg~?LQ1lkj}S7m`$x57B1c`BxwqOx_@~p@pPsEG1Oa>wYYgm8 z%t}HEv2EK@%L?e=v7C?tj;gH?qH2=?lygow*R7Uv)rr*jR`)|lwKbCtVj`0oUoE@R zL@vUTuS$)975L9uNeHYUs4=iRTTRH0T+d4g>FL%P*iQfe0RR6HIjn8~000I_L_t&o Y0Kqm)WjSH)@c;k-07*qoM6N<$f+@hsUH||9 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/000.png new file mode 100644 index 0000000000000000000000000000000000000000..2bdf346c69fb7297e21a68505c526913e8b0b7df GIT binary patch literal 461 zcmV;;0W$uHP)#9c3Z1h5BYeVBQR z3ORB65(k4ghQEmbsk>g!60#;6c`G3!yML>8P2^TrMn0QuBE0D|cGa_)gdl*=VU2-p z#LOh5A!c2a5NpS>@imd;4x7{0M9eKq4ycJFH`tthrit)x%~z$yz&2uM5&|&@Y7A^+ zvk7U)Sr#S4(ycA92LJ#7|Nqv4L`DDr00v1!K~w_(8#Xj$mu^Jz00000NkvXXu0mjf D8U(~9 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/000.png new file mode 100644 index 0000000000000000000000000000000000000000..4945855c91d7ac666a6aa4623a703aeeb56d17c3 GIT binary patch literal 482 zcmV<80UiE{P)GT>yzF9JbFlR5U%M95QIxyiTvza35LL;5Gb5VG1JVjnkJL!*8EJ7Zy$o0H> zVw-g=LbzJZq>V#QY$I(-79q8SQ}-=G-W`lRvCXm;As@|*lMXFHJpVkeqjXw?z{Ns{ zn5C{3LYNA~@HGaO7!B5hu}*|y5Q89o%fNtbVccgPAryyT7KbZ?iCY#v0yqOxHb*|Z zLcX~@i-SQN!#_oU)Gg~?LQ1lkj}S7m`$x57B1c`BxwqOx_@~p@pPsEG1Oa>wYYgm8 z%t}HEv2EK@%L?e=v7C?tj;gH?qH2=?lygow*R7Uv)rr*jR`)|lwKbCtVj`0oUoE@R zL@vUTuS$)975L9uNeHYUs4=iRTTRH0T+d4g>FL%P*iQfe0RR6HIjn8~000I_L_t&o Y0Kqm)WjSH)@c;k-07*qoM6N<$f+@hsUH||9 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/metadata.json b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply THRESHOLD/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can use filter shader hooks/000.png index 8ecd5c2bc4e0f3f131251bcf8676fe3ea663446d..f3e7b7eafe44aab8f48324317091e091a54185dd 100644 GIT binary patch delta 609 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 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE with a value/metadata.json b/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/000.png b/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/000.png new file mode 100644 index 0000000000000000000000000000000000000000..b621fb76d6263ef815977005096b56234adb15ea GIT binary patch literal 482 zcmV<80UiE{P)#NRR{h zXayN-E}I=OTNjj=9SJhlT=rQV`4I-hDtChUMdrhXR|sSw{vnJBxov~_m(17$Bh%R7 z5duNTK4J(4>?53O-zo%R5Y!$HmK-g<8)p>)vEIYT;gRdjQ{gQ_JkBCUMXVQO5%S)9 z$Oybei0AR}Rz2Rd zc4Va6nzLr>2yZ`4+-aLh2m<(m)CAa_ zn3;rZ#MX6PTCsr+F4GA);HX+kAu5{;pi;_GNc~Y{@};Q>umZo0nS{U!f|>xki`j(i$o2e5Aw6!5!2SaO0RR8=mP+{m000I_L_t&o Y0C9g#Wod47rT_o{07*qoM6N<$f={T{umAu6 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/metadata.json b/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/filters/It can apply POSTERIZE/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/000.png b/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/000.png new file mode 100644 index 0000000000000000000000000000000000000000..3fed12acb70503bd89668d19d4e7307b16b80f30 GIT binary patch literal 457 zcmV;)0XF`LP)sXQpGtg0l;8&r5i*LYX~)jYxGL)A??*l*WVS)pL~0^Q4%Drc zWURYwO~h?oQejOb$yj&WPfg@342YW?3G$=l!&X!XWFfv2#)KS}MShTsYhWbJT@fJ= zgzP(pV8Fh^g|2&rKn#M~&B0Qm#hYyx)z$_;-zoJ4O z+#m~SL=of!Vjh^(A+{HezNH z(h#%$C?QtIvhg*MWQWb|Ya(XLk_&1g$p)L-&omMKt$EYb1lUIGOhOdVm2WS zIm?d{VsUE(_6`65|Nmf)IL-h700v1!K~w_(%WyVj_Zx1T00000NkvXXu0mjf2vfkf literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/metadata.json b/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD with a value/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/000.png b/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/000.png new file mode 100644 index 0000000000000000000000000000000000000000..b621fb76d6263ef815977005096b56234adb15ea GIT binary patch literal 482 zcmV<80UiE{P)#NRR{h zXayN-E}I=OTNjj=9SJhlT=rQV`4I-hDtChUMdrhXR|sSw{vnJBxov~_m(17$Bh%R7 z5duNTK4J(4>?53O-zo%R5Y!$HmK-g<8)p>)vEIYT;gRdjQ{gQ_JkBCUMXVQO5%S)9 z$Oybei0AR}Rz2Rd zc4Va6nzLr>2yZ`4+-aLh2m<(m)CAa_ zn3;rZ#MX6PTCsr+F4GA);HX+kAu5{;pi;_GNc~Y{@};Q>umZo0nS{U!f|>xki`j(i$o2e5Aw6!5!2SaO0RR8=mP+{m000I_L_t&o Y0C9g#Wod47rT_o{07*qoM6N<$f={T{umAu6 literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/metadata.json b/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/filters/It can apply THRESHOLD/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file From 9073e9e77fe045281c1fd797acc2a14b9d2192e4 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 15:37:06 -0500 Subject: [PATCH 94/98] Add back dithering --- src/core/filterShaders.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/core/filterShaders.js b/src/core/filterShaders.js index a19d9bc8d7..20baa8f000 100644 --- a/src/core/filterShaders.js +++ b/src/core/filterShaders.js @@ -69,6 +69,12 @@ export function makeFilterShader(renderer, operation, p5) { return p5.pow(e - p5.abs(x), 2.0); }; + const random = (p) => { + let p3 = p5.fract(p.xyx * .1031); + p3 += p5.dot(p3, p3.yzx + 33.33); + return p5.fract((p3.x + p3.y) * p3.z); + }; + p5.getColor((inputs, canvasContent) => { const uv = inputs.texCoord; @@ -90,8 +96,9 @@ export function makeFilterShader(renderer, operation, p5) { numSamples = maxSamples; } + const randomOffset = (spacing - 1.0) * p5.mix(-0.5, 0.5, random(uv * inputs.canvasSize)); for (let i = 0; i < numSamples; i++) { - const sample = i * spacing - (numSamples - 1.0) * 0.5 * spacing; + const sample = i * spacing - (numSamples - 1.0) * 0.5 * spacing + randomOffset; const sampleCoord = uv + p5.vec2(sample, sample) / inputs.canvasSize * direction; const weight = quadWeight(sample, (numSamples - 1.0) * 0.5 * spacing); From 16de851d4635895f97eea1f28823528fcc2bd36b Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 15:45:29 -0500 Subject: [PATCH 95/98] Remove extra regex escape Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/webgl/shaderHookUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webgl/shaderHookUtils.js b/src/webgl/shaderHookUtils.js index 57b5a03b26..c816a70a62 100644 --- a/src/webgl/shaderHookUtils.js +++ b/src/webgl/shaderHookUtils.js @@ -22,7 +22,7 @@ export function getShaderHookTypes(shader, hookName) { throw new Error(`Couldn't find function parameters in hook body:\n${body}`); } const structProperties = structName => { - const structDefMatch = new RegExp(`struct\\s+${structName}\\s*\{([^\}]*)\}`).exec(fullSrc); + const structDefMatch = new RegExp(`struct\\s+${structName}\\s*{([^}]*)}`).exec(fullSrc); if (!structDefMatch) return undefined; const properties = []; for (const defSrc of structDefMatch[1].split(';')) { From b3e2007fdee3503884e85b9439db98b5b149623e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 15:58:23 -0500 Subject: [PATCH 96/98] Add getTexture() to ts generation --- docs/parameterData.json | 3 +-- src/webgl/shaderHookUtils.js | 4 ++-- utils/typescript.mjs | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/parameterData.json b/docs/parameterData.json index ab5f651595..897f52b8fa 100644 --- a/docs/parameterData.json +++ b/docs/parameterData.json @@ -1938,8 +1938,7 @@ "overloads": [ [ "Object" - ], - [] + ] ] }, "vertex": { diff --git a/src/webgl/shaderHookUtils.js b/src/webgl/shaderHookUtils.js index c816a70a62..8199746130 100644 --- a/src/webgl/shaderHookUtils.js +++ b/src/webgl/shaderHookUtils.js @@ -1,6 +1,6 @@ import { TypeInfoFromGLSLName } from '../strands/ir_types'; -/** +/* * Shared utility function for parsing shader hook types from GLSL shader source */ export function getShaderHookTypes(shader, hookName) { @@ -75,4 +75,4 @@ export function getShaderHookTypes(shader, hookName) { }, parameters }; -} \ No newline at end of file +} diff --git a/utils/typescript.mjs b/utils/typescript.mjs index d36a59ba99..258067d70b 100644 --- a/utils/typescript.mjs +++ b/utils/typescript.mjs @@ -56,6 +56,26 @@ function processStrandsFunctions() { description: `Discards the current pixel`, static: false }, + { + name: 'getTexture', + overloads: [{ + params: [ + { + name: 'tex', + type: { type: 'NameExpression', name: 'any' }, + optional: false, + }, + { + name: 'coord', + type: { type: 'NameExpression', name: 'any' }, + optional: false, + }, + ], + return: { + type: { type: 'NameExpression', name: 'any' } // Return 'any' for strands nodes + } + }], + } ]; // Add ALL GLSL builtin functions (both isp5Function: true and false) From 2c75c2561d9dde1cda4e508f380b9fa2a0e781dd Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 30 Nov 2025 16:14:01 -0500 Subject: [PATCH 97/98] Use no stroke in tests --- test/unit/visual/cases/webgl.js | 1 + test/unit/visual/cases/webgpu.js | 1 + .../It can apply BLUR with a value/000.png | Bin 3120 -> 3173 bytes .../In 2d mode/It can apply BLUR/000.png | Bin 3117 -> 3157 bytes .../It can apply DILATE (4x)/000.png | Bin 506 -> 773 bytes .../It can apply ERODE (4x)/000.png | Bin 604 -> 296 bytes .../In 2d mode/It can apply GRAY/000.png | Bin 640 -> 529 bytes .../In 2d mode/It can apply INVERT/000.png | Bin 611 -> 542 bytes .../000.png | Bin 462 -> 439 bytes .../In 2d mode/It can apply POSTERIZE/000.png | Bin 484 -> 447 bytes .../000.png | Bin 462 -> 439 bytes .../In 2d mode/It can apply THRESHOLD/000.png | Bin 484 -> 447 bytes .../000.png | Bin 335 -> 425 bytes .../It can use filter shader hooks/000.png | Bin 482 -> 658 bytes .../It can apply BLUR with a value/000.png | Bin 1608 -> 1582 bytes .../In webgl mode/It can apply BLUR/000.png | Bin 2578 -> 2489 bytes .../It can apply DILATE (4x)/000.png | Bin 475 -> 540 bytes .../It can apply ERODE (4x)/000.png | Bin 630 -> 294 bytes .../In webgl mode/It can apply GRAY/000.png | Bin 629 -> 517 bytes .../In webgl mode/It can apply INVERT/000.png | Bin 614 -> 512 bytes .../000.png | Bin 461 -> 425 bytes .../It can apply POSTERIZE/000.png | Bin 482 -> 432 bytes .../000.png | Bin 461 -> 425 bytes .../It can apply THRESHOLD/000.png | Bin 482 -> 432 bytes .../000.png | Bin 339 -> 417 bytes .../000.png | Bin 347 -> 424 bytes .../WebGL/filter/On a framebuffer/000.png | Bin 347 -> 424 bytes .../WebGL/filter/On the main canvas/000.png | Bin 413 -> 487 bytes .../It can apply BLUR with a value/000.png | Bin 1453 -> 1415 bytes .../WebGPU/filters/It can apply BLUR/000.png | Bin 2407 -> 2290 bytes .../filters/It can apply DILATE (4x)/000.png | Bin 484 -> 479 bytes .../filters/It can apply ERODE (4x)/000.png | Bin 632 -> 388 bytes .../WebGPU/filters/It can apply GRAY/000.png | Bin 637 -> 505 bytes .../filters/It can apply INVERT/000.png | Bin 644 -> 570 bytes .../000.png | Bin 457 -> 415 bytes .../filters/It can apply POSTERIZE/000.png | Bin 482 -> 421 bytes .../000.png | Bin 457 -> 415 bytes .../filters/It can apply THRESHOLD/000.png | Bin 482 -> 421 bytes 38 files changed, 2 insertions(+) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index d59af7a684..33ca8cf02c 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -92,6 +92,7 @@ visualSuite('WebGL', function() { p5.createCanvas(50, 50, mode === 'webgl' ? p5.WEBGL : p5.P2D); if (mode === 'webgl') p5.translate(-p5.width/2, -p5.height/2); p5.clear(); + p5.noStroke(); p5.fill('red'); p5.circle(20, 20, 15); if (mode === 'webgl') { diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index a03a6765ca..e581e4c8fa 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -121,6 +121,7 @@ visualSuite("WebGPU", function () { await p5.createCanvas(50, 50, p5.WEBGPU); p5.translate(-p5.width/2, -p5.height/2); p5.clear(); + p5.noStroke(); p5.fill('red'); p5.circle(20, 20, 15); p5.beginShape(p5.QUAD_STRIP); diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply BLUR with a value/000.png index fb81ee07e721249e4b74a79fbf568d97786bebdc..da159f872ed964c9cfbbd41bc2e08f59f3600ef9 100644 GIT binary patch delta 3169 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 Hu0mjfO1vJO delta 3116 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 delta 3113 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 DGPmTs diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply DILATE (4x)/000.png index f480f88aa0d28fcc68cede3b65f42f0191395009..113293017c7480e0bcd412d84b3f887751ccb2d7 100644 GIT binary patch delta 750 zcmVZmoN<>pLi4*u5{tj@Jh~Y~`k46=$RU??#zc^F z^4x`}5jM#;Mt=}sJ_2%q=jL2K*A;7wu#pS!4l@^p*EmPCE|gbm1kxTMJ+V(nu(*2k7{uNK!#?ixvC zh(G{Bd`!$mJ}8$tBG98z!(5&RhtHEU1R*2p9N$L}fG8JnE{B4 zQ`AJ%On;&s6T(K+!54up({Vo)7hQ^|q=vZEz#)*&+%SZV_>K~B5_B74Ftry=*Mkaf z1YS`+ru#!bth@`oQFPTtRI}U?#R*|55;42f{rV%KWv(?Y#b(V$QvKD0!^qVfMG|tH zNNU8@gyTep9ff|STew=|QatndrG-q>Ptv4wc0nyAgQq%RV-04O}oCX}z zjcj$fW^K9=x;X;Vjns5~D|fn)5vKvibR%0`u34LIc*m zp&MI5#0W!?=c44)uBigqiW7*Upr|OpwYvZh(`@$$?+9Us zK#&)A-{D=+6RVQ`Agd6Awaa7exFlKKcVx{Oau~-EXL}OF*}(AhbR@i`&Vqd%`8-j- z{ek*7nyKSjqd&+8U_9}KeBAv(-l4)1LpZ{2dfZ^{5LzG<;cc{lG`gaN z(~PWU>%NN4d%5kjAeR$FFJ=Z29r=6=L5^G&vt?ffDtk9a2!TL zMB)aFh=GUWFcKmXH(*2zJRFCS5Rtf{w~?2B&v<$bK delta 580 zcmV-K0=xaF0^9_UBYy%BNklMhdonYGQKN0_S|bEN?0+c4b7II9<*Ad4TdhI> zs}0~<48+6`f%jVCA=WAcK!?nzgiwG@ftud|BBI}_;aO#B2m1ieE9vd6Rfq$+hYWB$G4{3hI6lN$gaATk z0cDQ?jwi+kn>{hk?zx6|h_wm<#=!vP3^XEmggEmEl7A!awZubgjSzqU zrI5Lu+C8f-Clb)8#x(`X`?t#5W^@Bf$|z9SB#;QDep121mrLb5s}{anxAxwzoNw=lKUn z)3ihsGZGeF76Re~wXSOo5RU>8bA*zf9~ojH=z5lAHL)xRdw-)q4s!Pr?wS`1QA!m` zsSig4IfNsz+!j{rA|bdkM2;1q86X!m`q@Pk2_Yhq4ohn2D7$f5$d&tkrIwR(S_r+7 ztgjSz_?_#gg(OMR>=0n5T{a`{8RC=BTo_K&Iv-e4n4gd9C^Cq+ delta 616 zcmV-u0+;=f1b_vQBYy%lNklV+45QV4R!v(kiT!1a~0B{Cwl5jG@mJ7f# z8&(BHWliuVff`n#dbR;_B=>+1{@^rB3WuqhN zqw_Q|I^yYI)yuxZkumR{ax6H$CD6tgcmJo<1%cQ_x7$qygMYzzI2;lJ?Ov~!Fap4k z!%e@X4oE5h)bIDH)9KumqRk*zD6RqI9Yzb1Lf|97o#ao0^9aB^l0YDfvnrAc zXYf2(&2dfAfWQr>eQ?#-70d|=WF(>>%sRxCk@W<}M2#b3oUr3Qx0gXsj+8MpI<9df zJ06eDqGe9zJb%%0f(B%{-EJ+H|4lm_4(KfL%SUKHGKi8Bw$>!@-u{=UKv?};gmLqE zk=*(7Onrt11iPi|%2OD)w%KfmK>*>+4G7YLU~US5tJMlZ38FEGKpOz-^_l?8V_qS6 zeX0Y3`4IR4AQqhg3W2r&d~p(Txbj=-g516AdpT_Am{U$CX)$Gr_<8~I%jIGcNG~{$-EK!!0oO#kQ3VIG-|w%El+~@dT5zK%imNNQVvqe+ID*rL_^N0H z2t>kxs$oEm@o-2dsAmLkq_St@Hr@&lT&+CMDX)lDfZ$uDO~`FBs*tXh%jYcQ1`A>c zbM1uWh^uxZ|7}2i00030|CSUXjQ{`u21!IgR09BjVxDxU!qBP!0000q1fB$tBYy$YNkl`w_xDw!i+R}O4Eac>a%zjo)UWl%zqtmrN);Ygopto0y&3s zo1*AI_&5=Z8;i{ZL#8}rU=>X%0Zmdi>X|dDVP(i delta 587 zcmV-R0<`^}1mgsdBYy%INkl)TJ|;SUO@aW%YA9 zD_RZ6tx>4)FMo&T^@>_e1Jbd&^r#hl690%)RmH)d*>*?ry`*#?#j zLJfd%=*S`ra^#8$PNd}=gl47!p&AOKz#a#zc#Uy1#sr`vHizK4Y7kB)44e;s3(*~+ z=!^+~oC~gl7m=~80vWYYES#b-493_Y;`=nFnU0%)n13paymHaO6rb{C12PEvbd7w$m&PIa?=CNp6U2SvU$T%h2WeBw59AFJL;i~EGnOC7J5+}J z5FKH)L*3xDLx51^Z#P8ljVE+$#`1f#SoM&zYli^2*NO4`Av(fphq}RQhXA3-4}5Qi zoH^M)T0}3dm1p?Um@^2zCS8h8S@QbTnyO1UE+7fT+9mY|00960ocs0M00006Nkl5n1GfW^B!6s4L_t(|0qmG<4#FS|hSihl$<&kS&6LNCA40lyURbstB}7mk@Bo*PvPTWIQcab72@ybQ08&G36@S3eB~~IlG60nBP4m=p zCe2q{TZvc{wwzHDwGcsFb%Ug^b>ud^j3VU-)xTXt5Opg zui1!X0JRAxqE)I_-C-R00ssL2|KDvq+yDRo21!IgR2BmO7qKd3?SsjX00000NkvXX Hu0mjfXt=lH delta 436 zcmV;l0Zab31I`1GB!7cRL_t(|0qmCxj>0euM7w*j-96dD$$*oAA1V?NQSb+s2pQEh zO~%g5xK#9e`H)WunXQr4k?Kg219fX98S5@v9dTQilvo`}GS*%86&?AN-FFfPiSrta znUl+Hj;IjGLi%6mB1lfxgE4am19P3s{E7&HAY`BM0|WLME`PM{6#_8`YBvW{LR2R2KC?k=%a8b=K37h^vv@m2&^&5z?=;EN`9n2ss8A zM{{jh3((Ua4!)S2~kgfcj5AqJKla#O-91TqkV zAmdSh0XxEk*MA~H*c^giGJx#;Z-+5+f3gNfPPs=^$c-IjA&n@4F>?sIVN%bp5|T<% zQ@n&|T-p*-9of=t&0aHggr}S(8htYfff%pDngH9xY(g4xmWvW%acc`~btKtkbNT9s z*|Ow>>PWJ|=JK<3gttmfJP4ghh~CH5c)Bb3uK^h1!PC7(N z2;}|fj6atzd4kAnMusEdh?4^GX*mgNzBU{&OXqYj9B~rXd~Hidegy?_U1pj3jTS@z z{uIdfCH|M09DB0#A2mc!AMgOzkg`V&wOUP+d<_vmmH?!N+J7d1wM(o-_+$X6-Mi0I z%b9(?+1g6PqOj#mF;NQ<)GcnXE37%YyK1}aO=R1>;mShfq4Y0FPvuAnV6CP`1ntgu zzPBS&5kY|)KPSU-rnrU05iaLH;f%V}a)z@mVI=|>6b2ygxuAyHTut>}4G{qT34$C* z_Nbv&O8rI+k$)8xpG=YkfLGwWlVeY|{6`BSD@s%cI5V;4yUZ<#bOEe22_l*@qw?X% zDCM=j;fSV8P5xQ<9Q6oCj#L?$3r9TEPbpne`W#2SXLaNVYc>}|vhobr3`Yito^3gs zBT3Mfgp_T^Y(%!`POO5+39-7AevKn900030|5pB?cOC!$00v1!K~w_(UkgZOEFsIG P00000NkvXXu0mjf!0ft} delta 458 zcmV;*0X6==1LOmcB!8JnL_t(|0qmCnPQx$^MH4OtoQ!cY;AF*b>aOkCDPK{NUjftpR=ad)|acZo(>@z0vCZ}J-zQmT* z@QKOKV=k)@$U^!<=psm#>xEAYVPLL7&o7G*2txJ|KQLe);eSl~S|JdFVDI5z$?3%p zKq>|OT2$l`lhh1}Rd7Sf2q@QESlhH+h=CB&6v zPw^1a)1@OZF_9y?-E-DV6X7X0Vmo~^34s`|!#xIeXR`@O%d#|W$A0r;Iw2cwLv9Ee zYS#cb=bUn`TdmZr6RGj7#zRQ8H79MvL{4ga_1M`a!b4kSkNpuelaPzB8!>_3ubG70 zSt+~_qG+0H$}a!_0RR6%1lf-O000I_L_t&o0IXe3WrmrDYybcN07*qoM6N<$f_@s( AQ~&?~ diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can apply THRESHOLD with a value/000.png index b2878659a68ac37251baeb32a90fac62d74979d7..84120bba92b287dfc05c29efb2731c6fda15f895 100644 GIT binary patch delta 413 zcmV;O0b>5n1GfW^B!6s4L_t(|0qmG<4#FS|hSihl$<&kS&6LNCA40lyURbstB}7mk@Bo*PvPTWIQcab72@ybQ08&G36@S3eB~~IlG60nBP4m=p zCe2q{TZvc{wwzHDwGcsFb%Ug^b>ud^j3VU-)xTXt5Opg zui1!X0JRAxqE)I_-C-R00ssL2|KDvq+yDRo21!IgR2BmO7qKd3?SsjX00000NkvXX Hu0mjfXt=lH delta 436 zcmV;l0Zab31I`1GB!7cRL_t(|0qmCxj>0euM7w*j-96dD$$*oAA1V?NQSb+s2pQEh zO~%g5xK#9e`H)WunXQr4k?Kg219fX98S5@v9dTQilvo`}GS*%86&?AN-FFfPiSrta znUl+Hj;IjGLi%6mB1lfxgE4am19P3s{E7&HAY`BM0|WLME`PM{6#_8`YBvW{LR2R2KC?k=%a8b=K37h^vv@m2&^&5z?=;EN`9n2ss8A zM{{jh3((Ua4!)S2~kgfcj5AqJKla#O-91TqkV zAmdSh0XxEk*MA~H*c^giGJx#;Z-+5+f3gNfPPs=^$c-IjA&n@4F>?sIVN%bp5|T<% zQ@n&|T-p*-9of=t&0aHggr}S(8htYfff%pDngH9xY(g4xmWvW%acc`~btKtkbNT9s z*|Ow>>PWJ|=JK<3gttmfJP4ghh~CH5c)Bb3uK^h1!PC7(N z2;}|fj6atzd4kAnMusEdh?4^GX*mgNzBU{&OXqYj9B~rXd~Hidegy?_U1pj3jTS@z z{uIdfCH|M09DB0#A2mc!AMgOzkg`V&wOUP+d<_vmmH?!N+J7d1wM(o-_+$X6-Mi0I z%b9(?+1g6PqOj#mF;NQ<)GcnXE37%YyK1}aO=R1>;mShfq4Y0FPvuAnV6CP`1ntgu zzPBS&5kY|)KPSU-rnrU05iaLH;f%V}a)z@mVI=|>6b2ygxuAyHTut>}4G{qT34$C* z_Nbv&O8rI+k$)8xpG=YkfLGwWlVeY|{6`BSD@s%cI5V;4yUZ<#bOEe22_l*@qw?X% zDCM=j;fSV8P5xQ<9Q6oCj#L?$3r9TEPbpne`W#2SXLaNVYc>}|vhobr3`Yito^3gs zBT3Mfgp_T^Y(%!`POO5+39-7AevKn900030|5pB?cOC!$00v1!K~w_(UkgZOEFsIG P00000NkvXXu0mjf!0ft} delta 458 zcmV;*0X6==1LOmcB!8JnL_t(|0qmCnPQx$^MH4OtoQ!cY;AF*b>aOkCDPK{NUjftpR=ad)|acZo(>@z0vCZ}J-zQmT* z@QKOKV=k)@$U^!<=psm#>xEAYVPLL7&o7G*2txJ|KQLe);eSl~S|JdFVDI5z$?3%p zKq>|OT2$l`lhh1}Rd7Sf2q@QESlhH+h=CB&6v zPw^1a)1@OZF_9y?-E-DV6X7X0Vmo~^34s`|!#xIeXR`@O%d#|W$A0r;Iw2cwLv9Ee zYS#cb=bUn`TdmZr6RGj7#zRQ8H79MvL{4ga_1M`a!b4kSkNpuelaPzB8!>_3ubG70 zSt+~_qG+0H$}a!_0RR6%1lf-O000I_L_t&o0IXe3WrmrDYybcN07*qoM6N<$f_@s( AQ~&?~ diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can combine multiple filter passes/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can combine multiple filter passes/000.png index 9679aa86ddef9de37307dba4aebf912d7d1cdcd0..f084149dd81cdfd3a14a128314226bb42c4b5ba7 100644 GIT binary patch delta 400 zcmV;B0dM}#0;vO#BYy#2NklFXLC;^e zRYAaS4E%e(x^sLifSqB_V= zWmqDR3sVGn{0r8Bb`#{m6hTDbfKho>!1IS^wa{5m1yR4=FzN{}AVAAznLsop14ex% zXooR_XyD8+E|52jfF!MZ3gXGQL5{`+BOSlBzC#SA3FK{h2AD1&0_qK;o*s1r(a0BDu0#kAj>4O6v!3VEA48vz(n?@f#`}w3`O>6Rw{@Fc#L^!)%&P)5Dl{! zwA85AFAV|Fa2YL0H7yzj(qhuGcUBOK_fyRVIUUCYi=D^>A)GAiL{7&s!D1&eK?o-c u%Ovs-00960r-5`100006Nkl00{s|MNUMnLSTYa+^ly1 delta 309 zcmZ36Sb>N6!l(c8XRmlpExU2+ z$Pv%BJH{b4a~f9lSw6Qht7ua=8x*2+mdSF;iFkFRV>@E&pL*u$tyMpwy5`Ufg)kS@ zXM&GhLlW~8H5M^Bx6WCxW}e`Ys{WkG9$}Riy|cAF*wj=f_B^hiQKtCMiYMaie-2CW zEmekf&aTZq0%B^Lo}8KRPC|T%@iE??YrgyK-OREx;G)741&&4ouXEj;%`VAaiW4Oq z*W4GMRw#Hjex652C1;YRz>my{~kXzo2SX6v0|3I>k>zu8KN#) zM~VX!l3&_3>2S?d=Gc^dM$gi#W#{e_yQhLUC~SS{E|se7swrm}fWXt$&t;ucLK6T% C;)cQi diff --git a/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/000.png b/test/unit/visual/screenshots/WebGL/filter/In 2d mode/It can use filter shader hooks/000.png index 78e625579cef5f63b2afbe93844d8bf2c55312ef..f20254897854e04fce21c4f08ed67c8439413994 100644 GIT binary patch delta 634 zcmV-=0)_qJ1Cj-hBYy%%NklRN#!ILsWZ1HB?*oOn?4E5JsrAx!Texw%%i0 zHw!XOY>BVeYfD*EO%}v%Hg%g6nFviTcR9_1MBeFDQ^|td@ApWSm-NblL_Q9Yz2Z_zl%ECJ?RHJg-dU6d@iAzgM5Z)sb;#ahNjn-&KE&6VC;N8e;X7t z>^g}F0ErnP>n&1SI>pq#2qYzgAcldFm;j_aMpE2*tbcvktO6-96#?NsCPLy8g@csb zv^1w?D&ZjS`U?;aQu8=0O{S(-ILMN@UhCl?<8QBLP1p1a2YJ@~xW48uKsZRm%jOuS zG!6&xI8dp9xja9Hq)$07*qoM6N<$g3qBR2mk;8 delta 457 zcmV;)0XF`U1>yscBYyw^b5ch_0Itp)=>Px$oJmAMRA@u(n1QK;Fbsx!>*zY_I^sI& zI=YUJ2z_`*)&6OciZpN_T-AK}(>eiY&|G=6T*3=$fpq zYgbj(e%-chHohnj58$1@@|Iw70NsL2)8y!fSPqy=5DtU~wtt2>1xbPMFn!em_AFjD-b3B-F50MIFRSl_1D44F7c^H7&A&NbHYe0LF1ljmPc%K}B8z@|x6= z1hFcN!PKIjieMxxD+mEdsbaCw$=}n;E}_a%4eb#?y=k->0aV*~POHP+Q@0?Lf3Xfx z6>pkm?|5VH34a|Z9GEB&|1v*dt?T+V>eBxn00F-`710p@0*O72B{~DNM-vB0NvYpJ zA*wtY1W`pCM5>VbPff8CrZ|Yi{@{!N1?21mdrfLdf*^%aJhdd6P0mL?agbaifC`9& zDo9#g&Lgqy$l1D-3Xejlf^eHh4um=g0&^SzIN6}GM?yl5#Et+Q2opuPj-mINDS|qN zt}h^H#I)BJMYJP~KEEh}MofEsQA9h!==1*om2|gyIF?q-00000NkvXXu0mjfPj$-4 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR with a value/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply BLUR with a value/000.png index 56af4a17bdc392eeb5350b284822fd28121be306..9a3c23627110949004e962528f2df652904c6102 100644 GIT binary patch delta 1565 zcmV+&2IBe146Y22B!A;cL_t(|0j$@FavMnu15nx7`}+TH-KxalYYS5p-6JW-)<6Kk zquCxg$-Z7LmtPuwEe*dF|9-_E#eZJ$SMlG4f3Enq82&5XK@L){*Nn_FJLfpl+0b|T zRI}l=ICv?21Bvq1bCX!klJsVgP9Yb;Zpz2Tv_Yk6Mx~U#TSPXilA$8huR>1 z;zzuzk2iVlAP&^-^ty;7D8SIQc;eCGXZbnjEYA5D)ZtS&FiFKA#%Iv;qu9-!=k_?d z^NqGpADU$MXm!Di<3GcCq1TFod47+hJJSmW8nS}=peItpr=e?c{#3!xJ$eT-py$LB zwbpw0ON@`@8-KzV>>$s_(6zWD7vOa*p2!^Bp{~bW_dTOAyxeO{t}_i;6bJDWcb1#5 zWIXyASQ^xJzWGy2GH?LhneMRYjCl}0Ivl@qKZl;MFmylh!3GMT7j~^CG2LOoj^yHr z>2S2T!+bG5gB>Ax`oO*Bo})V~1U&o<)J|9`-qd1{$A3YV@A39`q&LzNmX1d+)IXFy zb<}Cg-*9@v7V0`NzK0rp{1a;Zj>HISHW&lF!&bTWYtY?RpYYu}N3|)&S)*E5gjN>(lJ2C_xsO4U- z7dX5cVt?6S`D)5PE=}Kob5~#0MB|A{AJ58Z;;f*=BUW!Ib#$1XaG~YZWfs@`9h(8k z(cgj^d%1Zd%V-}jrO)7PMbsPoqOaej+PI0o?MlG{Prl`CYXf}jn`%P zA5hD_1l4PHhk7x7gkIP6?5ktmA4Tu#{4>qFihtzR1*%}iWv!py2JySQ4{ z?3L5R_!ea8*|Uos!pFT&eE22AKZIv5Qs0<5?)`bI6MOvbKK`6NHD>)p)7j(bXL$AJ zA`Y2QFuk|`FP9Hq73OkBt~NNdeBs_xRUwK*j|B5x!JY;4)uoI^Ool zRe!yw=fy#lEV<+EO}#Fnvk!i#>#bPW1yW0c7LzSZiI$%VH56?ve27ia?9D0vGyG+4l4%Cpjb zJ2-TQn~kRhOQIG!(bM4z1e@gm1DUocv_!b#GxI10lK3vAAjfdd0p4O z$CJew(RKuJn7)C8rapLT%keODEk1*~4t0s0Ibks#4(zwn@eWpKxB)=#(3gnDXD}nV zXx!nv;8>^(+Fp2qKe1|UtK9*8|?G}y}Z=paVI0(A#OUMB) z5JTe5@$p2?z<~v6yn|hL{_-Sqh#@;24_g!mVo2Qi%ukK;Ce~8T6bn1kAK)Si`|#*9 zw%{|+pE^3|=kdh%U}^cz(foy6L}2Jd{M|ymHu=e^;qP?jht<$$_;7&~x_?M-j>eB* zxnNhD98SLLcy^&N(0B(q_>glErX)m%c@vg_?u#dO`R>u_p`U@hKrKGtBHfJc+_i(n zKtBuhL3$G7csMbB8ZP2i8Zgj51D4t$v*n^Y$HzhP;^@xn|3@qjNzfhf&p5+zN3ZYx zk7zUXcc$OG5#rwBa=nJ0$bazo;*R(@e%JB7h10*-MdqU74uxVo;o$CxAs#((FE?v< zvHtkc~-&InVKE%9%r#$6YqPt&$-cG0RRC1|E4Be`v3p{21!IgR2BmOn$X*^H@v`5 P00000NkvXXu0mjf4b~}R delta 1591 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 delta 449 zcmV;y0Y3hm1lt3UB!7@eL_t(|0qj>hPQ)-2eBne~0I}SJYfuqR#&Q%IXpx|yKw+kc zwsA3u$Fh}uk)IM-e&hLgrS(l5W$m zz{ZJ!@XaZyxPO)c*h@pCrj3g>30bj0jAAAdQwqYTqJX>85>Ino5d*|_2|=G{+~Rum zi2G+v5d&=c3IPGvNFgpKT{xMD%SjhbCgO6^g)?d*oAvb-@(sQpl|>9d^cV687Bw}D zkV7Q$Q<(;nBnDvmAFV?q!t+g6Yl;||2pq7CFI>^yaXNRbDPn+3-`~jJd5Yh;u80v4 r{{R30|Npyu^$q|400v1!K~w_(bjJ{M+jV;s00000NkvXXu0mjf*37^$ diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply ERODE (4x)/000.png index 6d0ff90e7bbb2e32fafe51d83496456ad4637845..cd7c1257613a49fcd57a77a7775013ad43d7e6da 100644 GIT binary patch delta 268 zcmV+n0rUR$1f~LzBYyzfNklc{lG`gaN z%Z%)1`>~47d%NwjAh#1lFIEN-9r=0;LC#z@v+YfDtk9a2!TL zMB)aFh=GUWFcKmXH(*2zJRFCS5Rtf{w~@Dh&loiUrE8G?(Leqe<&Ws@9pvl(o<~I~ z{C)2re!$tAAhV=YeHc-Zo}Gh{SyHOL#~Aqm00960*k0ri00006Nkl%uBYy%bNklZ(X4aTSp3QZ1}dhI&zeQ zcj@~g=IEgHCg)lZ05H$^A;%|)(*b}XN35vUfVjzC4yRzqb$`lJtD)Q(1!0%pWDA4L zF`U9EWyFA>OVm1%Z`NdQxIIMbpA5}f>#VNUSR}r=;ZJ6oRL$=2ze*`8dc>#BOtiIXle|@ z#LuD~nh@IrLVw?75s-^d0|v9kq3!Q!4}aw`4s_4fB4)HrcEw;oSWGd zP;=@vn9^44h=AagZn&x5CmJ1qn9mhH|E8oP^Q?-1U;v}($lQ&y^USGXB!}n>Ip*0F z1)(b#bwh4AHA;*cM(VhRv52A|m~mrn)FJ$_V@(biuYcvbB3c82Aq?`0#Wd>_f=7uQ zHROtFEr^@r&5JOOm~R5$26BP;{dOB0$ZlId@x`qp6C1NzxVDb)rkkVDhSdb(03(xk z0@U7wYXZR-1`+c`stpA5fFS0xRU-)I0YS`Xsa6ooAMNGXoyF=8)e17${?z}XXjnb` ztQ+L()K?pNh%hAU1{plg`fMPxwrRtnnmV%Nbvvr9355P(aE_?GQEebM=9VlP$dcXK sqV@*>0RR7;mnyvg000I_L_t&o0PKErWdbkxkpKVy07*qoM6N<$f>)#*0{{R3 diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply GRAY/000.png index 991e412ea03f8ee866da1179ea13d706e4ba7979..2e4f3d070989301196646b02849a9cf45f755c50 100644 GIT binary patch delta 492 zcmVjx_cO(Zih8YHEIGR`F5Z@yUQmv@qn1F_9W=ZJH}&;;DkGBjBBvd$6q=nOSF zM+^;?z3gU=gkkua<9@$C*ov5q+8_|0B*{Ang6AAj6s5?;aetgLb&=cHAecJDLcDC( z+wJxM3~FUHTjkjxzVAO(L;wa3snqJ!ut0E}Ads>#dMNeOS{BIKagqt(^vKo{<;fsA z=Ut~)Bz~DAgIupy_D`!$?^3kro(v*dOvIx1So23P|-nPFv+mJL8j zLkU74@u|o=BNK!&80;A{PGsbVKXYy0nVb^i~#`*8t*Vg zKqg`~!MN0_Ua$X{rOg=68jVIG4*-(NUvULV#ppodsk3&wtwcw5yk^D)1XGEadkS$C zXf2t!4wYUzfyi~SHlks?WyJ-guT5FOX!yR>Bnz<%$bgsGVvkI!=Ws2ve%W6xAdfs$ zI_?58-S79-rhoOEsrAGcC%Axo?RL8tm%qtYtJSUJq2&US7BDsTz>qEJ0>Z?x4a{P( zP?AL;g1TCOkd-k8>6ijw4dLx?2>ZKcgt;jOXS10CNWaXCE7}0?OlmUW6@pERfG{6} zH36E==kqDX3ECQe6MRNar_&YWVT~qu5KjBqJAx*75bi(M#}sHHNZtwf z7~5{QYF4>i8Usz<-XZ<`rswl{EdOm91C4;B5_cse5R75)l|XwJp@H0)v*|<-o6~xS r_8R~I|NqoZ5j+3@00v1!K~w_(FV>KB;U1#900000NkvXXu0mjfGmH^F diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply INVERT/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply INVERT/000.png index 342f41f8a0058e7d4a114b9f63934ce74a14521a..93b705b399d092571fd7df0b53a66cab938f62a0 100644 GIT binary patch delta 487 zcmVDa!%UUgX920aex3! zxYuB<1M&P_fIx{L)JXD$xjK+iH$pL*ny~(lwH!q1L@1!9Hhj*(`X7TFbmOsg>m+xi zM((<R^O6m$A#&JYxtI&Z!O{p+IXdJ{D zg3yCNAQ8EoNPpY|FmeT=G^u)!{WQF0MC2eC!Vy>#k;A<`ukl_(4Q^$0WRA=tm?~dHn`P8}HkyK}D_P2Nb@1-D)&amakCrc~|PK$>vkS~^35}Xzf zTOcKFASb5aYg-^CZXkz+$HNu~y8RQ*<>r`zuWf;#+h8TZ!sB5JBML^x>!^SohN~1@fd$u+c#T&AKG@WgPhi00960+K%ER00006 dNklt7+6`5>m^^IAo{T?F0a;kXplNEm(+dOsOPSb3}O7V2Sbb7`;hI zYhXe-YMCK)R!Fdrt&9xM+Wimcps_-?>Y}t@AzZE2K!q4$4WY9_f`yFiy6RG}kXBY2 zC=;Vl3S)?#4u1(2a*$ardNs{u5h7SfjTw(@WX90RuwWtVC_YU{hW83OC@86`B~Ff_ z6e0#e=3|avL=ZEETe@bd2!}!pf=5jdRI2xlF#=daFj&79t2YaQ5CjXE)19g05XTuK zKuZM}u(5rS5KnG*GKk|$ZzaHUxQY&eC%s@-;YTZRexc`?b;h%%a6&c7tlt<5{b8mXL9CI)OhbQwtQp=hT zy*z*F-A8iFg%Olb$!Bts5dtd+&T+7PPfAFiT*y2N3CY$ius;9*0RR6!7^NWq000I_ cL_t&o02@j7WhfNy5&!@I07*qoM6N<$f)3jPB>(^b diff --git a/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/000.png b/test/unit/visual/screenshots/WebGL/filter/In webgl mode/It can apply POSTERIZE with a value/000.png index 2bdf346c69fb7297e21a68505c526913e8b0b7df..def98ee34e60b2cd771446ca0e0e8d17c7223142 100644 GIT binary patch delta 399 zcmV;A0dW4!1E~X$B!6B>L_t(|0qmF!%EB-VhSihl$<&kS&9o0#D3oq((zIpfWBD4g zWO;vvpqJA@{+!5aM!F;Jh>-&RY8eSDeylqpmdA0vr-Ym}_+f5;q2|LP`WYwZI@DagZI=p&3`XSNCb%L04K;}eaP99$PmD#B~GNI%&fXQGE4c=UU#IVjIz#;88wlu zaYr&qCKue1$p&QWS9K%s-_dkT%d?~aUCd~e@sj_|FrCNf^D5y=477Mw_}Qlt6~ tD%PDHLkV1jAcuY>>q delta 435 zcmV;k0Zjg>1I+`FB!7ZQL_t(|0qmCzZo)7Og}YsB+sU?_Y~y6$uL6ni6a0gl2pQFt z(>&Yn*-=s7mlye!kl6-V6RC+Lb)b$`Qe)j|Ya;H}B?Z<*k{aty`>cul2!lVd{n7BT zPg!qrMTH;$^54StoHRa$7%;M(dqspm5&@4Gf&u#o7uxpiwZe$`w|C(IEKH80I9oP&l0jG8+j`sBfEdA zc1`3~S4KXYZ6dtsGyK&tZ*$ZN$tZq#-B!6W|L_t(|0qm3uZo?o9hRaU2oow65wwrbSq#;o?P_S`8ts`7q z#}5A^%G>2ht{}3Tk>N-<;-o;lT28{M9~+LSrE?k#N1TLJKX#@gUxFgXyX2>?8AJeo z71W1{fPXE+tyv8b%hzWsA_()% z&tSNb0EfmA=0;tC#0P_3p(O&IUSN>WILH~@VIcy*Bqy~%au34vN~_U>Zg=0DLsy(vv+mm2y3$zM4Iv}xEYQt5WRWiZjK~D+Y{1uyLKaTi*Ddw z5E&5bl-8$l;M1&07*qoM6N<$g2ZIB A-2eap delta 456 zcmV;(0XP1z1L6aaB!8DlL_t(|0qmCnPQx$^MH4OtoQ!cY;AF)w0zpTUIrh>-$WvUo z$+!K#9Zl*(`X|2-vf3bHA~BIc9f+$nsIlg>F%fg?1_j1M1~t~4_FfaY2!l7V?aT17 zCwV_*d4(VV@-M>nH)(tfF<|6)?D7bKBm!!*8EJ7Zy$o0H>Vw-g=LbzJZq>V#QY$I(-79q8SQ}-=G-W`lR zvCXm;As@|*lMXFHJpVkeqjXw?z{Ns{n5C{3LYNA~@HGaO7!B5hu}*|y5Q89o%fNtb zVccgPAryyT7Jr8;gNa)fJ_0xcQ#MCFyh6UYJ&S`u9K%0FfYdGPUP4N;nU4@Mv-?N2 zVhTPRvR|3bAe5Qp*bH;IW*L1CFY#5Ta_60hDu2 zIoGY0a@C2{_*VBrNVPSS4q_sc8ec8D(nK!8lCMgQfk74c&ss?ctRSc{usd5#$c|jk yO9<)d)*0AO00030{}DN?ZU6uP21!IgR09CPHcVwXVeat&0000L_t(|0qmF!%EB-VhSihl$<&kS&9o0#D3oq((zIpfWBD4g zWO;vvpqJA@{+!5aM!F;Jh>-&RY8eSDeylqpmdA0vr-Ym}_+f5;q2|LP`WYwZI@DagZI=p&3`XSNCb%L04K;}eaP99$PmD#B~GNI%&fXQGE4c=UU#IVjIz#;88wlu zaYr&qCKue1$p&QWS9K%s-_dkT%d?~aUCd~e@sj_|FrCNf^D5y=477Mw_}Qlt6~ tD%PDHLkV1jAcuY>>q delta 435 zcmV;k0Zjg>1I+`FB!7ZQL_t(|0qmCzZo)7Og}YsB+sU?_Y~y6$uL6ni6a0gl2pQFt z(>&Yn*-=s7mlye!kl6-V6RC+Lb)b$`Qe)j|Ya;H}B?Z<*k{aty`>cul2!lVd{n7BT zPg!qrMTH;$^54StoHRa$7%;M(dqspm5&@4Gf&u#o7uxpiwZe$`w|C(IEKH80I9oP&l0jG8+j`sBfEdA zc1`3~S4KXYZ6dtsGyK&tZ*$ZN$tZq#-B!6W|L_t(|0qm3uZo?o9hRaU2oow65wwrbSq#;o?P_S`8ts`7q z#}5A^%G>2ht{}3Tk>N-<;-o;lT28{M9~+LSrE?k#N1TLJKX#@gUxFgXyX2>?8AJeo z71W1{fPXE+tyv8b%hzWsA_()% z&tSNb0EfmA=0;tC#0P_3p(O&IUSN>WILH~@VIcy*Bqy~%au34vN~_U>Zg=0DLsy(vv+mm2y3$zM4Iv}xEYQt5WRWiZjK~D+Y{1uyLKaTi*Ddw z5E&5bl-8$l;M1&07*qoM6N<$g2ZIB A-2eap delta 456 zcmV;(0XP1z1L6aaB!8DlL_t(|0qmCnPQx$^MH4OtoQ!cY;AF)w0zpTUIrh>-$WvUo z$+!K#9Zl*(`X|2-vf3bHA~BIc9f+$nsIlg>F%fg?1_j1M1~t~4_FfaY2!l7V?aT17 zCwV_*d4(VV@-M>nH)(tfF<|6)?D7bKBm!!*8EJ7Zy$o0H>Vw-g=LbzJZq>V#QY$I(-79q8SQ}-=G-W`lR zvCXm;As@|*lMXFHJpVkeqjXw?z{Ns{n5C{3LYNA~@HGaO7!B5hu}*|y5Q89o%fNtb zVccgPAryyT7Jr8;gNa)fJ_0xcQ#MCFyh6UYJ&S`u9K%0FfYdGPUP4N;nU4@Mv-?N2 zVhTPRvR|3bAe5Qp*bH;IW*L1CFY#5Ta_60hDu2 zIoGY0a@C2{_*VBrNVPSS4q_sc8ec8D(nK!8lCMgQfk74c&ss?ctRSc{usd5#$c|jk yO9<)d)*0AO00030{}DN?ZU6uP21!IgR09CPHcVwXVeat&00000-*ztBYy!_Nkl-Rg8vod(bU!q0yG;!yQbO1SzrPIyFuVPHK!l< zwk8PRZoYD36$Jbc;NR;xfGUWm;e8LRg9!z>Lq)$Qi1mbF&42aEM68LZ4&qZ;mI%bc zR6)M}1?xbk3F2U?AR_31QF&89$WNFih@8!aQBU{*0<;{K2}DyOVANNFaTqg*2JQ^g z1#-a%NYZ+yz@JPv$lbVLr2BWH@8E+O0=Z1@05b(dK(k@gvq#fFH1tWBsUU)RO4C7} zN@Y68F^L=n@_)qj3cFe(FrK|(Al@tHfzYf_5SfxR$fBOw-~B>Cu3;8~mYS>IU!O}r zG+0J!Qq72#fsB~6?A;Z_;=QOzkm5KtSY#p_giu(>M2h3sV3CPz5JF+$m_$AS00960 mU%ev?00006NklPx$4M{{nRA@u(+2IPpFbo9X_5B~ZGGQ;sv zxUxZ0NCa_$N>7ZzN-#72((-c^_!f!Zh4&k={aiFPn&sojE?w(Z)bVQ ze)LJ{rPRM?-hNWA&wP^Onx=fyBY>%LYg3cyPa;BDl_^NM&h!-Q-@D<#&DO_m;Se?U~Ta!hQ#Z{olNI r4gTe~DWM4fM9r-} delta 321 zcmV-H0lxmI1KR?SBYyw^b5ch_0Itp)=>Px$6-h)vRA@u(mq8ALFc1U7r}Dj)@8wfe zROCXTDDkX_khuaTV|&wdN)NfFhj1i=jFzMmaUwwuxLQHRcFQ`EZtH?xbRt2EFlD>6QV*QFfxb; z@r5C3HUtO(BY_ZU)PAX{3_5lOZq=p<)OG3es4!c{?a#{}z!MIoeN84wN`R z{dO2g%NdJ!);b`Bi=>7VA>AajoCx6}so_LOHwi5#Lbyn3I1$oKLd%H|E|MA_3piUd TqFF*Y00000NkvXXu0mjfCyj?n diff --git a/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/000.png b/test/unit/visual/screenshots/WebGL/filter/On a framebuffer/000.png index 197629c93fc4078cdd31de9bfe9882cdf2bc59dc..e810f3c78d3a72c2a5085bf4a43cbf5774c92945 100644 GIT binary patch delta 399 zcmcc3w1Rnpay?_Lr;B4q1>>7ZzN-#72((-c^_!f!Zh4&k={aiFPn&sojE?w(Z)bVQ ze)LJ{rPRM?-hNWA&wP^Onx=fyBY>%LYg3cyPa;BDl_^NM&h!-Q-@D<#&DO_m;Se?U~Ta!hQ#Z{olNI r4gTe~DWM4fM9r-} delta 321 zcmV-H0lxmI1KR?SBYyw^b5ch_0Itp)=>Px$6-h)vRA@u(mq8ALFc1U7r}Dj)@8wfe zROCXTDDkX_khuaTV|&wdN)NfFhj1i=jFzMmaUwwuxLQHRcFQ`EZtH?xbRt2EFlD>6QV*QFfxb; z@r5C3HUtO(BY_ZU)PAX{3_5lOZq=p<)OG3es4!c{?a#{}z!MIoeN84wN`R z{dO2g%NdJ!);b`Bi=>7VA>AajoCx6}so_LOHwi5#Lbyn3I1$oKLd%H|E|MA_3piUd TqFF*Y00000NkvXXu0mjfCyj?n diff --git a/test/unit/visual/screenshots/WebGL/filter/On the main canvas/000.png b/test/unit/visual/screenshots/WebGL/filter/On the main canvas/000.png index 176f683c628ab19faf547846bee2a13d8d9f197a..7690b9e28b1e705daefd30316b5d0c6443e09532 100644 GIT binary patch delta 462 zcmV;<0Wtoa1Lp&fBYy#$NklC5OdSq~yJ^l; zp6AyXKxQC7V1CU+XCO3d?ytFH0)nBn5KB{N&@MjIlJ=9br5Q{V<`h{2mS@e;Qw`AXyCKAZ)<@etS#ROWlmjr@=z0__% z#&P^oh9ZGk>2;OA|LNivRuItkbw1nb}d8(?aSHRI& zl_|(Px$S4l)cRA@u(mq8AKFc3u@4prDGCqquA z?x2TaOi7y%W0cJQjfM;x(Kw~=zbO>tfnum2G=IAOF zx{*r3num2G=IAOFRxuJnxTg7v$XQh{`TU+g$7!0@bLK$>$$tRDw+V{KT|}rrG?2Xz zFOXx5akYmY6c8gsnL7mr2I;!)vMkFDOcaKJLE5&xl?9E!#2BHuK*b~*loo?PST-^V z)LwrBl5WZKJonc2gg`OI;&{d@=9XRSk{HYgz33bIz92Vn^rHc|#MnxR0@j1-u(_fpki=O&T{K?ivS zO5eAyx~lp}zp-AXwriKxTIfv!t#=SKL3X_xVJC`?=SI*3+4XLOohUk<8$lCf*SisR hqUd;T1Wk}#{{$-?g){hrl{)|c002ovPDHLkV1hyYtJVMj diff --git a/test/unit/visual/screenshots/WebGPU/filters/It can apply BLUR with a value/000.png b/test/unit/visual/screenshots/WebGPU/filters/It can apply BLUR with a value/000.png index 93511d5cb868e9a67295c0653dc1716897b15361..4b1a365ddb2b5b1ae96e01fd6764d09f9f9fb99a 100644 GIT binary patch delta 1396 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 delta 458 zcmV;*0X6>L1LOmcB!8JnL_t(|0qj>xZo)7So%BS#KvjB^UZbm~Co4TlH?T#54GR?B zLxK%iOgzsx3KMyh$nreDkCE_;E#yfda|<#P$wVqSkVmVMvBj{@oyf{;_(cuQ;@Tc- zi|uwaPD+FVNFKxt-{yU|_8W`YTT75z2nkRC_=E-pK^QcJV1MqK-9kvtaILh|m@Xkw1(m>3NF`&7VKb2t>zYnwB2Dcd**p^& zF{SC0E0J{tifT-kke>=d{=}FrA#kq~?`!x+EyZ*RF+9ptlvtp48EzqTP(k1W+Y$>e z>2M3-t|Wl-(0@@7jvJ?3#kK5VFTU||wM|+yNyrtMg5YciMy}#o_FVgK@3+-pUwy2I z0l*?5=+lf>T(3THSBw-fz^1Jb5KxU2qHGrmK-6Mnfh72P9*M`{FS-Z|`^V#?= zg`&;L{~6QzOtqh{=TLI-Jn)x+r}{xwedjWjH#RPUjT03ZZ5r;Km}9vowMEd9p(OQ) zpV4FQ!_g8G)e98gF&DgU5SEG$bg6n|#xz%OmPe1}vuGEViJTw4UpIPi`FF_Xkat}Q zP8WLOmz*)I`>c6i&?Ts$vc0P#eB+xs%SmfDl<$-Is5R}bzWXN8Z|9~f#~t1D`>Dsa zL-&$@79Cj~5&FMS;(*4DJz?&47v6tMxRIQ>cJ_(YuMb2dC4MaxHf&hx?cy~x>#DkY zlxA#IpNwYcs?hssGL_5>4FCV9JM$l5U|?Va1tT-V<#%$aC(^Wb14S4-UHx3vIVCg! E0CJS2T>t<8 delta 608 zcmV-m0-yba1Na1xBYy%dNkl~;(P7G!e zBP@?5BM%R)v}^NztudK=FJI&sAiXtmbYyg-(GHATtI@_*Uv_k)dg~gUI6Bg3W2-N_ zRY(4W1A2{-#D|D?dp!%nEaSf~j$bb0uNZ%Z z!0{B6!uTaDd6+vl=bMf-APg~@M$E-bW`~ZkpeA!ME#|>mnjEztj53r)Zy^AwVLofj zozp}$AdJ}AGV$zkYX*zBN($wUdiw}@ByR>J`=*XVCz2>ue>xie{8ZFp*0!e^{ z$vX>bua_Oap!yLFxt#Su@9 zWgokxBW02#Hw;iyLhL%6K~Q*66f=a%^Zbf|zaeH^+l$5pq<=)QD11#UuBB-u~BNc{>YM}LxjmIVS= zPyRNfx1%a!ox?qmaV1t0DI|z(TR|X*f7$HdR!P(^D0`e$^ zS7rf1C=B?%K*Udo$3EZxoIw_W2($#ud3OM6yo}92UQRvWLqHB_y0N4V+45QW|D;Q|~0*s)?;dI8SBO-N4Gu;l`< zWWy4KK;mab=ZgXe6`Xj$)UAv>$>2OE@0)R``fmQoCxV35$k>tC5l!R50?%)-&_`GKzh8)ves!7Yl7NZ_W*u%Uel3m zv)MSCF4knu3x6vv(148Q^LfSFUu5g`nlB>`K0*VM6{ej06!6~umZ(64AT5kZT0RPG zTCUV9G$1@J5p$UYxizNKsSzFThPQ5DKnud!PcfK|$776|;$>z&&*hronruTl@tI%n zcz6T^TU7Z?OsA8{B*QpCE^~AN05oX5#-ku7bC|{Sn}19Y<$pX2#J|&$Cq$5v z0=@il?8wVYUh=@uI>N7CmH2@b5=heNbcTb$z#3?T0$4d+Mo1v0*=(AAzi$mR=Ef%| zklk))7K?>7(A?lK!GSE7OJj>a0kYk0tq=gB3BDuy{r&`U_lzcZ5UO4Fj-Ux1g#FL< zu?3n4a$c(fKIT@dm6^?E)CWpfzRI0?en79oFkxG@q7;`zDs(-qw3N)PKo>cHnOw-sP zw(7q{I=ZfD(l1Vt-m@(0dol=d+-!2|8@yl{c@#_Ac@e{(5d z9T$j?8t4OwyFD(DcGdglE(NTk0?FP)=3|ieO^*uXCOg7L0wEM@64$3uBLL_t&o0E+&TW$b7#7P2K2rNDjGqs4W=%#<)7H`{mI>bainmc-AGG9Vly&1k{k(Cf%IqvDy;Wq zlOx`x3zV1~2~=3`%U;Bh`Fw8Xip{n$&hvbhU9p`d1hQ~{oqtU7h2l=c3C1I1%8eQr zoeti;g+Q=0ri6GQnvG)D8CROHh9Iz-2JK!#AO=AMoMML{1e0?Ld(OI-5F;lbXnSJ> zBW5rdi-UV<)rYMTa#TBsR=zhVvvk;h2*+E@55MPT?qK&Ed)+f`A!W%V>twIf-x=(hkwHY4A{_dmXIAe$WnD_YCB_s z{j|e`K3+sfXp%P9PeN>}bWKch6%&n+ zme{JvvM+s0_63+0{QD;^A&(;Wd?LUI((UmHc@o9G_%}js!jXp`H?r)QD-XgP=%jqB=2bs-Ccf=hrs(@cDqr$4Ubw|{tGg|157!_8%ZA(Y4LB(SU`k~wJX#s-x zPmufyabJQyW6Xsi`==)a1EUftS)>6ebz`l=3`PU87FOE-36N5#ruiC2-W3XJ)@G0WYY-~)^p{Fz`nzUu6u<*41a>!&B0Qm#hYCUMGryuj9^4=cX+#m^1Y#bT)Z?dwq>|J$FCiM2Mq+9r zBi+{QHQPjZ`f1`x+e|_bz#F6{z&2uL64DT}{wN_<$FlJ?kz|L>?Q0@t%aRLfBFP4u z+s`x+{;he_)CAZ@>`X!+20=}LZDKYd4LQq?5@K;{1Ss|n00960V2(J>00006NklA z(Nq{1mB^e;8jw;q<~qz^G$3KmFo?Jm5II25OR8O~0g0-3G7$(U0`bhz$2`-% z1xOSKPz-Yi;(w*FZwV65?9w|(m$D}=bw?7#>9XXGSd@Lt{H)#Wj;vLs=bSrYQT-6Y z5W@FZyZfk)tZ7ZoIY_cR9Twe@4nz?UH&LM?L@m p0RR6XT)6ZA000I_L_t&o0E$0GWq?ZJ$N&HU07*qoLn9N$S{jh$>ex zq{+AQ|2IP7ZF!Mj3Yo2u*^%r>kOTQ>1sQ8Dn;kJ*7nGPC2{P7P_E{bI5eCF6cY^sv z=EH_p2xKAtA&d#RZG-uj%-91X)7aq=0zt?=Vh9H8Bb;mBDt`oG5Y!$HmK-g<8)p>) zvEIYT;gRdjQ{gQ_JkBCUMXVQO5%S)9$Oybei0AR}Rz2Rd4YH6%6v3Q8%oP)Qf0mF?lA7WvMB~y( zOm<|X+nTdx>j-Z@P26dlNeBY?g46`qotT+~Y{b@eU0Sh$4ldIPIpC;TN+Bwn44_iV zQc68)C1>qOjc;{5g;cFMX(KywlJV7JXX?mBSn{Q*2|=&|zm1uMzzTwz0K1FXgzU)m y{7E4_ZjHeH0{{U3|MZqh`2YX_21!IgR09BUe@%jqB=2bs-Ccf=hrs(@cDqr$4Ubw|{tGg|157!_8%ZA(Y4LB(SU`k~wJX#s-x zPmufyabJQyW6Xsi`==)a1EUftS)>6ebz`l=3`PU87FOE-36N5#ruiC2-W3XJ)@G0WYY-~)^p{Fz`nzUu6u<*41a>!&B0Qm#hYCUMGryuj9^4=cX+#m^1Y#bT)Z?dwq>|J$FCiM2Mq+9r zBi+{QHQPjZ`f1`x+e|_bz#F6{z&2uL64DT}{wN_<$FlJ?kz|L>?Q0@t%aRLfBFP4u z+s`x+{;he_)CAZ@>`X!+20=}LZDKYd4LQq?5@K;{1Ss|n00960V2(J>00006NklA z(Nq{1mB^e;8jw;q<~qz^G$3KmFo?Jm5II25OR8O~0g0-3G7$(U0`bhz$2`-% z1xOSKPz-Yi;(w*FZwV65?9w|(m$D}=bw?7#>9XXGSd@Lt{H)#Wj;vLs=bSrYQT-6Y z5W@FZyZfk)tZ7ZoIY_cR9Twe@4nz?UH&LM?L@m p0RR6XT)6ZA000I_L_t&o0E$0GWq?ZJ$N&HU07*qoLn9N$S{jh$>ex zq{+AQ|2IP7ZF!Mj3Yo2u*^%r>kOTQ>1sQ8Dn;kJ*7nGPC2{P7P_E{bI5eCF6cY^sv z=EH_p2xKAtA&d#RZG-uj%-91X)7aq=0zt?=Vh9H8Bb;mBDt`oG5Y!$HmK-g<8)p>) zvEIYT;gRdjQ{gQ_JkBCUMXVQO5%S)9$Oybei0AR}Rz2Rd4YH6%6v3Q8%oP)Qf0mF?lA7WvMB~y( zOm<|X+nTdx>j-Z@P26dlNeBY?g46`qotT+~Y{b@eU0Sh$4ldIPIpC;TN+Bwn44_iV zQc68)C1>qOjc;{5g;cFMX(KywlJV7JXX?mBSn{Q*2|=&|zm1uMzzTwz0K1FXgzU)m y{7E4_ZjHeH0{{U3|MZqh`2YX_21!IgR09BUe@ Date: Sun, 30 Nov 2025 16:23:10 -0500 Subject: [PATCH 98/98] Fix clipping for WebGPU --- preview/index.html | 3 + src/core/p5.Renderer3D.js | 6 -- src/webgl/p5.RendererGL.js | 3 + src/webgpu/p5.RendererWebGPU.js | 96 ++++++++++++++++-- test/unit/visual/cases/webgpu.js | 39 +++++++ .../Basic clipping with circles/000.png | Bin 0 -> 1108 bytes .../Basic clipping with circles/metadata.json | 3 + 7 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 test/unit/visual/screenshots/WebGPU/Clipping/Basic clipping with circles/000.png create mode 100644 test/unit/visual/screenshots/WebGPU/Clipping/Basic clipping with circles/metadata.json diff --git a/preview/index.html b/preview/index.html index 53fa4df90c..02bb545f12 100644 --- a/preview/index.html +++ b/preview/index.html @@ -96,6 +96,8 @@ p.draw = function () { p.clear(); + p.push(); + p.clip(() => p.rect(-50, -50, 200, 200)); /*p.orbitControl(); p.push(); p.textAlign(p.CENTER, p.CENTER); @@ -173,6 +175,7 @@ p.pop(); p.filter(p.BLUR, 10) + p.pop(); }; }; diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 1dca9dc607..09ceae4af0 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1341,12 +1341,6 @@ export class Renderer3D extends Renderer { this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1] ) { this._clearClip(); - if (!this._userEnabledStencil) { - this._internalDisable.call(this.GL, this.GL.STENCIL_TEST); - } - - // Reset saved state - // this._userEnabledStencil = this._savedStencilTestState; } super.pop(...args); this._applyStencilTestIfClipping(); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 1aeeaa3040..d5adb6afdc 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -498,6 +498,9 @@ class RendererGL extends Renderer3D { _clearClipBuffer() { this.GL.clearStencil(1); this.GL.clear(this.GL.STENCIL_BUFFER_BIT); + if (!this._userEnabledStencil) { + this._internalDisable.call(this.GL, this.GL.STENCIL_TEST); + } } // x,y are canvas-relative (pre-scaled by _pixelDensity) diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 39337f9bb9..c5d67193fd 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -414,24 +414,22 @@ class RendererWebGPU extends Renderer3D { multisample: { count: sampleCount }, depthStencil: { format: depthFormat, - depthWriteEnabled: true, + depthWriteEnabled: !clipping, depthCompare: 'less-equal', stencilFront: { - compare: clipping ? 'always' : (clipApplied ? 'equal' : 'always'), + compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'), failOp: 'keep', depthFailOp: 'keep', passOp: clipping ? 'replace' : 'keep', }, stencilBack: { - compare: clipping ? 'always' : (clipApplied ? 'equal' : 'always'), + compare: clipping ? 'always' : (clipApplied ? 'not-equal' : 'always'), failOp: 'keep', depthFailOp: 'keep', passOp: clipping ? 'replace' : 'keep', }, - stencilReadMask: clipApplied ? 0xFFFFFFFF : 0x00000000, - stencilWriteMask: clipping ? 0xFFFFFFFF : 0x00000000, - stencilLoadOp: "load", - stencilStoreOp: "store", + stencilReadMask: 0xFF, + stencilWriteMask: clipping ? 0xFF : 0x00, }, }); shader._pipelineCache.set(key, pipeline); @@ -1070,6 +1068,18 @@ class RendererWebGPU extends Renderer3D { const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); const currentShader = this._curShader; passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode }))); + + // 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); + } // Bind vertex buffers for (const buffer of this._getVertexBuffers(currentShader)) { const location = currentShader.attributes[buffer.attr].location; @@ -1569,6 +1579,78 @@ class RendererWebGPU extends Renderer3D { return { adjustedWidth: width, adjustedHeight: height }; } + _applyClip() { + const commandEncoder = this.device.createCommandEncoder(); + + const activeFramebuffer = this.activeFramebuffer(); + const depthTexture = activeFramebuffer ? + (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : + this.depthTexture; + + if (!depthTexture) { + return; + } + + const depthStencilAttachment = { + view: depthTexture.createView(), + 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() { + const commandEncoder = this.device.createCommandEncoder(); + + const activeFramebuffer = this.activeFramebuffer(); + const depthTexture = activeFramebuffer ? + (activeFramebuffer.aaDepthTexture || activeFramebuffer.depthTexture) : + this.depthTexture; + + if (!depthTexture) { + return; + } + + const depthStencilAttachment = { + view: depthTexture.createView(), + 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() { // TODO } diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index e581e4c8fa..8fac79a9db 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -377,6 +377,45 @@ visualSuite("WebGPU", function () { ); }); + visualSuite("Clipping", function () { + visualTest( + "Basic clipping with circles", + async function (p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + p5.background("white"); + + // Draw some circles that extend beyond the clipping area + p5.fill("red"); + p5.noStroke(); + p5.circle(-15, -15, 25); + p5.fill("green"); + p5.circle(15, -15, 25); + p5.fill("blue"); + p5.circle(-15, 15, 25); + p5.fill("yellow"); + p5.circle(15, 15, 25); + + // Apply clipping to a smaller rectangle in the center + p5.push(); + p5.clip(() => { + p5.rect(-12.5, -12.5, 25, 25); + }); + + // Draw more circles that should be clipped to the rectangle + p5.fill("purple"); + p5.circle(-8, -8, 16); + p5.fill("orange"); + p5.circle(8, 8, 16); + p5.fill("cyan"); + p5.circle(0, 0, 12); + + p5.pop(); + + await screenshot(); + }, + ); + }); + visualSuite('Typography', function () { visualSuite('textFont', function () { visualTest('with a font file in WebGPU', async function (p5, screenshot) { diff --git a/test/unit/visual/screenshots/WebGPU/Clipping/Basic clipping with circles/000.png b/test/unit/visual/screenshots/WebGPU/Clipping/Basic clipping with circles/000.png new file mode 100644 index 0000000000000000000000000000000000000000..b33b9f5961ab159ed0ab553531be8b6e61571e79 GIT binary patch literal 1108 zcmV-a1grarP)SqO?+@T!eo$nblp_);vis3Rw0TtJ zh;-$Oj!`d1B$ln9t{jo`c~WT9WEtHHy5`Squgye$-~@wU5o$2U z)vgGaoCM+s^PJ;mB3qOHsjd_!G9_N0>!~Wsyf~5Z)?&rnS}IC0D=LU&4Y(#4yFx;1 zY}eQ@R)xAut3v@NIR)`yB$)@rfeB;XI?}cW!o*##PT*U6!C#ISH}UVF7=P~NU2HGM z@`e~3Z)ZdV;J0;EPr3W`=|}i( z3nfU5piE5d#`83hvE^Mp$q98|L;UVjzN!&ES{IDh;iI4ano(!IJ0A&KnYevKj!!M}(6?F_%KoxlLoj}A$2>=2M7rRmO?mxBj zBH-I@fF}jvi9y6kJrH4WEZz(-L-JAuVubvGjT9M@X#ZuA*m-$k%n1!d%rJC8b$e{p zo3Gh#Uy^Y(#)Jj}exQ$$!02KI-jG}3%4}Z9$aNU7L5}A@uf;6zOY9t0;IbUqOvP)9HxgyLA5UN zx=h<*k<7Zd%T_gpj