Skip to content

Commit da1c0d6

Browse files
authored
Merge branch 'dev-2.0' into nityam/tessellation-freeze-fix-2.x
2 parents 41a0e50 + 2eee170 commit da1c0d6

12 files changed

Lines changed: 329 additions & 80 deletions

File tree

src/core/p5.Renderer3D.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export class Renderer3D extends Renderer {
160160

161161
// clipping
162162
this._clipDepths = [];
163+
this._textContextSavedStack = [];
163164
this._isClipApplied = false;
164165
this._stencilTestOn = false;
165166

@@ -1329,13 +1330,25 @@ export class Renderer3D extends Renderer {
13291330
return this;
13301331
}
13311332

1333+
push() {
1334+
super.push()
1335+
const saved = !!(this.states.textFont?.font);
1336+
if (saved) {
1337+
this.textDrawingContext().save()
1338+
}
1339+
this._textContextSavedStack.push(saved);
1340+
}
1341+
13321342
pop(...args) {
13331343
if (
13341344
this._clipDepths.length > 0 &&
13351345
this._pushPopDepth === this._clipDepths[this._clipDepths.length - 1]
13361346
) {
13371347
this._clearClip();
13381348
}
1349+
if (this._textContextSavedStack.pop()) {
1350+
this.textDrawingContext().restore()
1351+
}
13391352
super.pop(...args);
13401353
this._applyStencilTestIfClipping();
13411354
}

src/strands/strands_transpiler.js

Lines changed: 33 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,35 @@ function replaceReferences(node, tempVarMap) {
235235
internalReplaceReferences(node);
236236
}
237237

238+
// Shared handler for both BinaryExpression and LogicalExpression —
239+
// both follow the same operator-to-method-call transformation pattern.
240+
function transformBinaryOrLogical(node, state, ancestors) {
241+
if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
242+
return;
243+
}
244+
const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
245+
if (unsafeTypes.includes(node.left.type)) {
246+
node.left = {
247+
type: 'CallExpression',
248+
callee: {
249+
type: 'Identifier',
250+
name: '__p5.strandsNode',
251+
},
252+
arguments: [node.left]
253+
};
254+
}
255+
node.type = 'CallExpression';
256+
node.callee = {
257+
type: 'MemberExpression',
258+
object: node.left,
259+
property: {
260+
type: 'Identifier',
261+
name: replaceBinaryOperator(node.operator),
262+
},
263+
};
264+
node.arguments = [node.right];
265+
}
266+
238267
const ASTCallbacks = {
239268
UnaryExpression(node, state, ancestors) {
240269
if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
@@ -509,72 +538,10 @@ const ASTCallbacks = {
509538
}
510539
}
511540
},
512-
BinaryExpression(node, state, ancestors) {
513-
// Don't convert uniform default values to node methods, as
514-
// they should be evaluated at runtime, not compiled.
515-
if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
516-
return;
517-
}
518-
// If the left hand side of an expression is one of these types,
519-
// we should construct a node from it.
520-
const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
521-
if (unsafeTypes.includes(node.left.type)) {
522-
const leftReplacementNode = {
523-
type: 'CallExpression',
524-
callee: {
525-
type: 'Identifier',
526-
name: '__p5.strandsNode',
527-
},
528-
arguments: [node.left]
529-
}
530-
node.left = leftReplacementNode;
531-
}
532-
// Replace the binary operator with a call expression
533-
// in other words a call to BaseNode.mult(), .div() etc.
534-
node.type = 'CallExpression';
535-
node.callee = {
536-
type: 'MemberExpression',
537-
object: node.left,
538-
property: {
539-
type: 'Identifier',
540-
name: replaceBinaryOperator(node.operator),
541-
},
542-
};
543-
node.arguments = [node.right];
544-
},
545-
LogicalExpression(node, state, ancestors) {
546-
// Don't convert uniform default values to node methods, as
547-
// they should be evaluated at runtime, not compiled.
548-
if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
549-
return;
550-
}
551-
// If the left hand side of an expression is one of these types,
552-
// we should construct a node from it.
553-
const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
554-
if (unsafeTypes.includes(node.left.type)) {
555-
const leftReplacementNode = {
556-
type: 'CallExpression',
557-
callee: {
558-
type: 'Identifier',
559-
name: '__p5.strandsNode',
560-
},
561-
arguments: [node.left]
562-
}
563-
node.left = leftReplacementNode;
564-
}
565-
// Replace the logical operator with a call expression
566-
// in other words a call to BaseNode.or(), .and() etc.
567-
node.type = 'CallExpression';
568-
node.callee = {
569-
type: 'MemberExpression',
570-
object: node.left,
571-
property: {
572-
type: 'Identifier',
573-
name: replaceBinaryOperator(node.operator),
574-
},
575-
};
576-
node.arguments = [node.right];
577-
},
541+
BinaryExpression: transformBinaryOrLogical,
542+
LogicalExpression: transformBinaryOrLogical,
543+
544+
578545
ConditionalExpression(node, state, ancestors) {
579546
if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
580547
return;

src/type/textCore.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,13 +1587,6 @@ function textCore(p5, fn) {
15871587
if (typeof weight === 'number') {
15881588
this.states.setValue('fontWeight', weight);
15891589
this._applyTextProperties();
1590-
1591-
// Safari works without weight set in the canvas style attribute, and actually
1592-
// has buggy behavior if it is present, using the wrong weight when drawing
1593-
// multiple times with different weights
1594-
if (!p5.prototype._isSafari()) {
1595-
this._setCanvasStyleProperty('font-variation-settings', `"wght" ${weight}`);
1596-
}
15971590
return;
15981591
}
15991592
// the getter

src/webgl/3d_primitives.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,33 +1792,38 @@ function primitives3D(p5, fn){
17921792
const prevMode = this.states.textureMode;
17931793
this.states.setValue('textureMode', constants.NORMAL);
17941794
const prevOrder = this.bezierOrder();
1795-
this.bezierOrder(2);
1795+
this.bezierOrder(3);
17961796
this.beginShape();
17971797
const addUVs = (x, y) => [x, y, 0, (x - x1)/width, (y - y1)/height];
1798+
const rr = 0.5523; // kappa: 4*(sqrt(2)-1)/3, handle ratio for cubic bezier circle approximation
17981799
if (tr !== 0) {
17991800
this.vertex(...addUVs(x2 - tr, y1));
1800-
this.bezierVertex(...addUVs(x2, y1));
1801+
this.bezierVertex(...addUVs(x2 - tr + tr * rr, y1));
1802+
this.bezierVertex(...addUVs(x2, y1 + tr - tr * rr));
18011803
this.bezierVertex(...addUVs(x2, y1 + tr));
18021804
} else {
18031805
this.vertex(...addUVs(x2, y1));
18041806
}
18051807
if (br !== 0) {
18061808
this.vertex(...addUVs(x2, y2 - br));
1807-
this.bezierVertex(...addUVs(x2, y2));
1809+
this.bezierVertex(...addUVs(x2, y2 - br + br * rr));
1810+
this.bezierVertex(...addUVs(x2 - br + rr * br, y2));
18081811
this.bezierVertex(...addUVs(x2 - br, y2));
18091812
} else {
18101813
this.vertex(...addUVs(x2, y2));
18111814
}
18121815
if (bl !== 0) {
18131816
this.vertex(...addUVs(x1 + bl, y2));
1814-
this.bezierVertex(...addUVs(x1, y2));
1817+
this.bezierVertex(...addUVs(x1 + bl - bl * rr, y2));
1818+
this.bezierVertex(...addUVs(x1, y2 - bl + bl * rr));
18151819
this.bezierVertex(...addUVs(x1, y2 - bl));
18161820
} else {
18171821
this.vertex(...addUVs(x1, y2));
18181822
}
18191823
if (tl !== 0) {
18201824
this.vertex(...addUVs(x1, y1 + tl));
1821-
this.bezierVertex(...addUVs(x1, y1));
1825+
this.bezierVertex(...addUVs(x1, y1 + tl - tl * rr));
1826+
this.bezierVertex(...addUVs(x1 + tl - tl * rr, y1));
18221827
this.bezierVertex(...addUVs(x1 + tl, y1));
18231828
} else {
18241829
this.vertex(...addUVs(x1, y1));

src/webgl/p5.RendererGL.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ class RendererGL extends Renderer3D {
529529

530530
/**
531531
* Loads the pixels data for this canvas into the pixels[] attribute.
532-
* Note that updatePixels() and set() do not work.
532+
* Note that set() does not work.
533533
* Any pixel manipulation must be done directly to the pixels[] array.
534534
*
535535
* @private

src/webgpu/p5.RendererWebGPU.js

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,78 @@ function rendererWebGPU(p5, fn) {
173173
device.queue.writeBuffer(this.buffer, 0, floatData);
174174
}
175175
}
176+
177+
/**
178+
* Reads data from a storage buffer back into JavaScript.
179+
*
180+
* Copies data from the GPU to the CPU using a temporary buffer,
181+
* so it must be awaited. Returns a `Float32Array` for number
182+
* buffers, or an array of plain objects for struct buffers.
183+
*
184+
* Note: This is a GPU -> CPU read, so calling it often (like every frame)
185+
* can be slow.
186+
*
187+
* ```js example
188+
* let data;
189+
* let computeShader;
190+
*
191+
* async function setup() {
192+
* await createCanvas(100, 100, WEBGPU);
193+
*
194+
* data = createStorage(new Float32Array([1, 2, 3, 4]));
195+
* computeShader = buildComputeShader(doubleValues);
196+
* compute(computeShader, 4);
197+
*
198+
* let result = await data.read();
199+
* // result is Float32Array [2, 4, 6, 8]
200+
* for (let i = 0; i < result.length; i++) {
201+
* print(result[i]);
202+
* }
203+
* describe('Prints the values 2, 4, 6, 8 to the console.');
204+
* }
205+
*
206+
* function doubleValues() {
207+
* let d = uniformStorage(data);
208+
* let idx = index.x;
209+
* d[idx] = d[idx] * 2;
210+
* }
211+
* ```
212+
*
213+
* @method read
214+
* @for p5.StorageBuffer
215+
* @beta
216+
* @webgpu
217+
* @webgpuOnly
218+
* @returns {Promise<Float32Array|Object[]>}
219+
*/
220+
async read() {
221+
const device = this._renderer.device;
222+
this._renderer.flushDraw();
223+
224+
const stagingBuffer = device.createBuffer({
225+
size: this.size,
226+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
227+
});
228+
229+
const commandEncoder = device.createCommandEncoder();
230+
commandEncoder.copyBufferToBuffer(this.buffer, 0, stagingBuffer, 0, this.size);
231+
device.queue.submit([commandEncoder.finish()]);
232+
233+
await stagingBuffer.mapAsync(GPUMapMode.READ, 0, this.size);
234+
const mappedRange = stagingBuffer.getMappedRange(0, this.size);
235+
236+
// Copy before unmapping because mapped memory becomes invalid after unmap
237+
const rawCopy = new Float32Array(mappedRange.byteLength / 4);
238+
rawCopy.set(new Float32Array(mappedRange));
239+
240+
stagingBuffer.unmap();
241+
stagingBuffer.destroy();
242+
243+
if (this._schema !== null) {
244+
return this._renderer._unpackStructArray(rawCopy, this._schema);
245+
}
246+
return rawCopy;
247+
}
176248
}
177249

178250
/**
@@ -3243,19 +3315,24 @@ ${hookUniformFields}}
32433315

32443316
let maxEnd = 0;
32453317
let maxAlign = 1;
3246-
const fields = entries.map(([name]) => {
3318+
const fields = entries.map(([name, value]) => {
32473319
const el = elements[name];
32483320
maxEnd = Math.max(maxEnd, el.offsetEnd);
32493321
// Alignment for scalars/vectors: <=4 -> 4, <=8 -> 8, else 16
32503322
const align = el.size <= 4 ? 4 : el.size <= 8 ? 8 : 16;
32513323
maxAlign = Math.max(maxAlign, align);
3324+
// Track original JS type for reconstruction during readback
3325+
const kind = value?.isVector ? 'vector'
3326+
: value?.isColor ? 'color'
3327+
: undefined;
32523328
return {
32533329
name,
32543330
baseType: el.baseType,
32553331
size: el.size,
32563332
offset: el.offset,
32573333
packInPlace: el.packInPlace ?? false,
32583334
dim: el.size / 4,
3335+
kind,
32593336
};
32603337
});
32613338

@@ -3282,6 +3359,65 @@ ${hookUniformFields}}
32823359
return floatView;
32833360
}
32843361

3362+
// Inverse of _packStructArray reads packed buffer back into plain JS objects
3363+
// using the same schema layout - fields, stride and offsets
3364+
_unpackStructArray(floatView, schema) {
3365+
const { fields, stride } = schema;
3366+
const dataView = new DataView(floatView.buffer);
3367+
const count = Math.floor(floatView.byteLength / stride);
3368+
const result = [];
3369+
3370+
for (let i = 0; i < count; i++) {
3371+
const item = {};
3372+
const baseOffset = i * stride;
3373+
for (const field of fields) {
3374+
const byteOffset = baseOffset + field.offset;
3375+
const n = field.size / 4;
3376+
3377+
if (field.baseType === 'u32') {
3378+
if (n === 1) {
3379+
item[field.name] = dataView.getUint32(byteOffset, true);
3380+
} else {
3381+
item[field.name] = Array.from({ length: n }, (_, j) =>
3382+
dataView.getUint32(byteOffset + j * 4, true)
3383+
);
3384+
}
3385+
} else if (field.baseType === 'i32') {
3386+
if (n === 1) {
3387+
item[field.name] = dataView.getInt32(byteOffset, true);
3388+
} else {
3389+
item[field.name] = Array.from({ length: n }, (_, j) =>
3390+
dataView.getInt32(byteOffset + j * 4, true)
3391+
);
3392+
}
3393+
} else {
3394+
const idx = byteOffset / 4;
3395+
if (n === 1) {
3396+
item[field.name] = floatView[idx];
3397+
} else {
3398+
const values = Array.from(floatView.slice(idx, idx + n));
3399+
if (field.kind === 'vector') {
3400+
item[field.name] = this._pInst.createVector(...values);
3401+
} else if (field.kind === 'color') {
3402+
// Color was packed as normalized RGBA [0-1] via _getRGBA([1,1,1,1])
3403+
// Scale back to the current colorMode range
3404+
const maxes = this.states.colorMaxes[this.states.colorMode];
3405+
item[field.name] = this._pInst.color(
3406+
values[0] * maxes[0], values[1] * maxes[1],
3407+
values[2] * maxes[2], values[3] * maxes[3]
3408+
);
3409+
} else {
3410+
item[field.name] = values;
3411+
}
3412+
}
3413+
}
3414+
}
3415+
result.push(item);
3416+
}
3417+
3418+
return result;
3419+
}
3420+
32853421
createStorage(dataOrCount) {
32863422
const device = this.device;
32873423

0 commit comments

Comments
 (0)