Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions src/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,22 +286,29 @@ function rendererWebGPU(p5, fn) {
}
}

const raw = map ? map(srcData) : srcData;
const typed = this._normalizeBufferData(raw, Float32Array);
// Check if we already have a buffer for this data
let existingBuffer = buffers[dst];
const needsNewBuffer = !existingBuffer;

// Only create new buffer and write data if buffer doesn't exist or data is dirty
if (needsNewBuffer || geometry.dirtyFlags[src] !== false) {
const raw = map ? map(srcData) : srcData;
const typed = this._normalizeBufferData(raw, Float32Array);

// Always use pooled buffers - let the pool system handle sizing and reuse
const pooledBufferInfo = this._getVertexBufferFromPool(geometry, dst, typed.byteLength);
// Get pooled buffer (may reuse existing or create new)
const pooledBufferInfo = this._getVertexBufferFromPool(geometry, dst, typed.byteLength);

// Create a copy of the data to avoid conflicts when geometry arrays are reset
const dataCopy = new typed.constructor(typed);
pooledBufferInfo.dataCopy = dataCopy;
// 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);
// 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;
// Update the buffers cache to use the pooled buffer
buffers[dst] = pooledBufferInfo.buffer;
geometry.dirtyFlags[src] = false;
}

shader.enableAttrib(attr, size);
}
Expand Down Expand Up @@ -354,9 +361,9 @@ function rendererWebGPU(p5, fn) {
}
};

freeDefs(this.renderer.buffers.stroke);
freeDefs(this.renderer.buffers.fill);
freeDefs(this.renderer.buffers.user);
freeDefs(this.buffers.stroke);
freeDefs(this.buffers.fill);
freeDefs(this.buffers.user);
}

_getValidSampleCount(requestedCount) {
Expand Down Expand Up @@ -1014,6 +1021,11 @@ function rendererWebGPU(p5, fn) {
// Return all uniform buffers to their pools
this._returnUniformBuffersToPool();

// Mark all geometry buffers for return after frame is complete
for (const geometry of this._geometriesWithPools) {
this._markGeometryBuffersForReturn(geometry);
}

// Return all vertex buffers to their pools
this._returnVertexBuffersToPool();

Expand Down
38 changes: 38 additions & 0 deletions test/unit/visual/cases/webgpu.js
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,44 @@ visualSuite("WebGPU", function () {
);
});

visualSuite("Immediate Mode Buffer Reuse", function () {
visualTest(
"beginShape/endShape reuses buffers across frames",
async function (p5, screenshot) {
await p5.createCanvas(50, 50, p5.WEBGPU);

// Draw the same triangle in different positions across 3 frames
// Each frame draws the same number of vertices (3) so that it
// isn't FORCED to allocate new buffers, and should be trying
// to reuse them.
const positions = [
{ x: -15, y: -10 }, // Frame 1: left
{ x: 0, y: -10 }, // Frame 2: center
{ x: 15, y: -10 } // Frame 3: right
];

for (let frame = 0; frame < 3; frame++) {
const pos = positions[frame];

p5.background(200);
p5.fill('red');
p5.noStroke();

// Draw triangle using immediate mode. This means it's using the
// same geometry every frame. We expect to see different results
// if it's correctly updating the buffers.
p5.beginShape();
p5.vertex(pos.x - 5, pos.y + 10); // bottom-left
p5.vertex(pos.x + 5, pos.y + 10); // bottom-right
p5.vertex(pos.x, pos.y); // top
p5.endShape(p5.CLOSE);

await screenshot();
}
}
);
});

visualSuite("Image Based Lighting", function () {
const shinesses = [50, 150];
for (const shininess of shinesses) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 3
}
129 changes: 129 additions & 0 deletions test/unit/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import p5 from '../../../src/app.js';
import rendererWebGPU from "../../../src/webgpu/p5.RendererWebGPU";

p5.registerAddon(rendererWebGPU);

suite('WebGPU p5.RendererWebGPU', function() {
let myp5;
let prevPixelRatio;

beforeAll(async function() {
prevPixelRatio = window.devicePixelRatio;
window.devicePixelRatio = 1;
myp5 = new p5(function(p) {
p.setup = function() {};
});
});

beforeEach(async function() {
await myp5.createCanvas(50, 50, 'webgpu');
});

afterEach(function() {
myp5.remove();
});

afterAll(function() {
window.devicePixelRatio = prevPixelRatio;
});

suite('Buffer Pooling', function() {
test('drawing geometry twice reuses vertex buffers', async function() {
// Create a simple geometry
const geom = myp5.buildGeometry(() => {
myp5.triangle(0, 0, 10, 0, 5, 10);
});

// Draw the geometry once
myp5.background(255);
myp5.model(geom);

// Check the vertex buffer pool size for position attribute
const poolForVertexBuffer = geom._vertexBufferPools?.vertexBuffer;
expect(poolForVertexBuffer).to.exist;
const initialPoolSize = poolForVertexBuffer.length;
const initialInUseSize = geom._vertexBuffersInUse?.vertexBuffer?.length || 0;

// Draw the geometry again
myp5.background(255);
myp5.model(geom);

// Verify the pool hasn't grown - buffers should be reused
const finalPoolSize = poolForVertexBuffer.length;
const finalInUseSize = geom._vertexBuffersInUse?.vertexBuffer?.length || 0;

// Pool size should stay the same or be smaller (buffers moved from pool to in-use)
// The total number of buffers (pool + in-use) should remain constant
expect(initialPoolSize + initialInUseSize).to.equal(finalPoolSize + finalInUseSize);
});

test('freeGeometry causes new buffer allocation on next draw', async function() {
// Create a simple geometry
const geom = myp5.buildGeometry(() => {
myp5.triangle(0, 0, 10, 0, 5, 10);
});

// Draw the geometry once
myp5.background(255);
myp5.model(geom);

// Get initial buffer count
const poolForVertexBuffer = geom._vertexBufferPools?.vertexBuffer;
expect(poolForVertexBuffer).to.exist;
const initialTotalBuffers = poolForVertexBuffer.length +
(geom._vertexBuffersInUse?.vertexBuffer?.length || 0);

// Free the geometry
myp5.freeGeometry(geom);

// Draw the geometry again
myp5.background(255);
myp5.model(geom);

// After freeGeometry, new buffers should be allocated
const finalTotalBuffers = poolForVertexBuffer.length +
(geom._vertexBuffersInUse?.vertexBuffer?.length || 0);

// We should have more buffers now since freeGeometry marks geometry as dirty
// and new buffers need to be created
expect(finalTotalBuffers).to.be.greaterThan(initialTotalBuffers);
});

test('immediate mode geometry reuses buffers across frames', async function() {
// Function to draw the same shape using immediate mode
const drawSameShape = () => {
myp5.background(255);
myp5.beginShape();
myp5.vertex(0, 0);
myp5.vertex(10, 0);
myp5.vertex(5, 10);
myp5.endShape();
};

// Draw the shape for the first frame
drawSameShape();
await myp5._renderer.finishDraw();

// Get the immediate mode geometry (shapeBuilder geometry)
const immediateGeom = myp5._renderer.shapeBuilder.geometry;
const poolForVertexBuffer = immediateGeom._vertexBufferPools?.vertexBuffer;
expect(poolForVertexBuffer).to.exist;

const initialTotalBuffers = poolForVertexBuffer.length +
(immediateGeom._vertexBuffersInUse?.vertexBuffer?.length || 0);

// Draw the same shape for several more frames
for (let frame = 0; frame < 5; frame++) {
drawSameShape();
await myp5._renderer.finishDraw();

// Check that total buffer count hasn't increased
const currentTotalBuffers = poolForVertexBuffer.length +
(immediateGeom._vertexBuffersInUse?.vertexBuffer?.length || 0);

expect(currentTotalBuffers).to.equal(initialTotalBuffers,
`Buffer count should stay constant across frames (frame ${frame})`);
}
});
});
});
Loading