Skip to content

Commit 5555f13

Browse files
committed
Make sure swizzle assignment of storage properties works
1 parent 938cb8b commit 5555f13

6 files changed

Lines changed: 157 additions & 0 deletions

File tree

src/strands/strands_transpiler.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,75 @@ const ASTCallbacks = {
12471247
delete node.update;
12481248
},
12491249

1250+
// Swizzle assignments on storage fields like data[i].velocity.y *= -1 don't work
1251+
// as plain assignments because data.get(i).velocity returns a StrandsNode without
1252+
// an onRebind callback, so setting .y on it has no effect on the buffer.
1253+
//
1254+
// We detect this case here (ancestor walk is post-order, so by the time this runs
1255+
// data[i] has already been converted to data.get(i) by the MemberExpression visitor).
1256+
// buildPropertyPath returns null when it hits a non-Identifier object (like a .get()
1257+
// call), which distinguishes this from struct-field swizzles like inputs.position.x
1258+
// where buildPropertyPath returns a non-null path and the phi-node mechanism handles it.
1259+
//
1260+
// We rewrite fieldExpr.swizzle = rhs into a read-modify-write:
1261+
// let __tmp = fieldExpr
1262+
// __tmp.swizzle = rhs swizzleTrap.set mutates __tmp.id
1263+
// fieldExpr = __tmp proxy setter writes back to the buffer
1264+
ExpressionStatement(node, state, ancestors) {
1265+
if (ancestors.some(a => nodeIsUniform(a) || nodeIsUniformCallbackFn(a, state.uniformCallbackNames))) {
1266+
return;
1267+
}
1268+
const assign = node.expression;
1269+
if (assign?.type !== 'AssignmentExpression') return;
1270+
const left = assign.left;
1271+
if (left.type !== 'MemberExpression' || left.computed) return;
1272+
const propName = left.property.name;
1273+
if (!propName || !isSwizzle(propName)) return;
1274+
const fieldExpr = left.object;
1275+
// A plain identifier (e.g. myVec.y = 5) is handled directly by swizzleTrap.set.
1276+
if (fieldExpr.type === 'Identifier') return;
1277+
// A simple dotted path (e.g. inputs.position.x) is handled by the phi-node mechanism.
1278+
if (buildPropertyPath(fieldExpr) !== null) return;
1279+
1280+
const tmpName = `__swizzle_tmp_${blockVarCounter++}`;
1281+
node.type = 'BlockStatement';
1282+
node.body = [
1283+
{
1284+
type: 'VariableDeclaration',
1285+
declarations: [{
1286+
type: 'VariableDeclarator',
1287+
id: { type: 'Identifier', name: tmpName },
1288+
init: JSON.parse(JSON.stringify(fieldExpr)),
1289+
}],
1290+
kind: 'let',
1291+
},
1292+
{
1293+
type: 'ExpressionStatement',
1294+
expression: {
1295+
type: 'AssignmentExpression',
1296+
operator: '=',
1297+
left: {
1298+
type: 'MemberExpression',
1299+
object: { type: 'Identifier', name: tmpName },
1300+
property: { type: 'Identifier', name: propName },
1301+
computed: false,
1302+
},
1303+
right: assign.right,
1304+
},
1305+
},
1306+
{
1307+
type: 'ExpressionStatement',
1308+
expression: {
1309+
type: 'AssignmentExpression',
1310+
operator: '=',
1311+
left: JSON.parse(JSON.stringify(fieldExpr)),
1312+
right: { type: 'Identifier', name: tmpName },
1313+
},
1314+
},
1315+
];
1316+
delete node.expression;
1317+
},
1318+
12501319
// Helper method to replace identifier references in AST nodes
12511320
replaceIdentifierReferences(node, oldName, newName) {
12521321
if (!node || typeof node !== 'object') return node;

test/unit/visual/cases/webgpu.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,88 @@ visualSuite("WebGPU", function () {
14221422
await screenshot();
14231423
}
14241424
);
1425+
1426+
visualTest(
1427+
'Compute shader assigns to a swizzle of a struct vector field',
1428+
async function(p5, screenshot) {
1429+
await p5.createCanvas(50, 50, p5.WEBGPU);
1430+
1431+
const particles = p5.createStorage([
1432+
{ position: [15, 10] },
1433+
]);
1434+
1435+
// Negate position.y via swizzle assignment
1436+
const computeShader = p5.buildComputeShader(() => {
1437+
const buf = p5.uniformStorage('buf', particles);
1438+
const idx = p5.index.x;
1439+
buf[idx].position.y *= -1;
1440+
}, { p5, particles });
1441+
p5.compute(computeShader, 1);
1442+
1443+
const sphereShader = p5.baseMaterialShader().modify(() => {
1444+
const buf = p5.uniformStorage('buf', particles);
1445+
p5.getWorldInputs((inputs) => {
1446+
const pos = buf[0].position;
1447+
inputs.position.x += pos.x;
1448+
inputs.position.y += pos.y;
1449+
return inputs;
1450+
});
1451+
}, { p5, particles });
1452+
1453+
const geo = p5.buildGeometry(() => p5.sphere(5));
1454+
p5.background(200);
1455+
p5.noStroke();
1456+
p5.fill(255, 0, 0);
1457+
p5.shader(sphereShader);
1458+
p5.model(geo, 1);
1459+
1460+
await screenshot();
1461+
}
1462+
);
1463+
1464+
visualTest(
1465+
'Compute shader assigns to a swizzle of a struct vector field inside an if statement',
1466+
async function(p5, screenshot) {
1467+
await p5.createCanvas(50, 50, p5.WEBGPU);
1468+
1469+
const particles = p5.createStorage([
1470+
{ position: [0, 0], velocity: [5, 5] },
1471+
]);
1472+
1473+
// Move by velocity, then negate velocity.y if position.y > 0.
1474+
// After 1st run: position=[5,5], velocity=[5,-5].
1475+
// After 2nd run: position=[10,0], velocity=[5,-5].
1476+
const computeShader = p5.buildComputeShader(() => {
1477+
const buf = p5.uniformStorage('buf', particles);
1478+
const idx = p5.index.x;
1479+
buf[idx].position += buf[idx].velocity;
1480+
if (buf[idx].position.y > 0) {
1481+
buf[idx].velocity.y *= -1;
1482+
}
1483+
}, { p5, particles });
1484+
p5.compute(computeShader, 1);
1485+
p5.compute(computeShader, 1);
1486+
1487+
const sphereShader = p5.baseMaterialShader().modify(() => {
1488+
const buf = p5.uniformStorage('buf', particles);
1489+
p5.getWorldInputs((inputs) => {
1490+
const pos = buf[0].position;
1491+
inputs.position.x += pos.x;
1492+
inputs.position.y += pos.y;
1493+
return inputs;
1494+
});
1495+
}, { p5, particles });
1496+
1497+
const geo = p5.buildGeometry(() => p5.sphere(5));
1498+
p5.background(200);
1499+
p5.noStroke();
1500+
p5.fill(255, 0, 0);
1501+
p5.shader(sphereShader);
1502+
p5.model(geo, 1);
1503+
1504+
await screenshot();
1505+
}
1506+
);
14251507
});
14261508

14271509
visualSuite('Feedback', function() {
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"numScreenshots": 1
3+
}
390 Bytes
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"numScreenshots": 1
3+
}

0 commit comments

Comments
 (0)