From 0571ced981a1f6eaf64baa651e872d7f3539021b Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Wed, 17 Dec 2025 00:55:41 -0800 Subject: [PATCH 1/2] Implement auto-return for shader hooks when types match and no value is returned --- src/strands/strands_api.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 7e649d0731..914e951d2b 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -368,23 +368,38 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { CFG.pushBlock(cfg, entryBlockID); const args = createHookArguments(strandsContext, hookType.parameters); const userReturned = hookUserCallback(...args); + + // Auto-return input if types match and user didn't return anything + let effectiveReturn = userReturned; + if (userReturned === undefined) { + const expected = hookType.returnType; + if (isStructType(expected.typeName)) { + if (hookType.parameters.length === 1) { + const paramType = hookType.parameters[0].type.typeName; + if (paramType === expected.typeName) { + effectiveReturn = args[0]; + } + } + } + } + const expectedReturnType = hookType.returnType; let rootNodeID = null; if(isStructType(expectedReturnType.typeName)) { const expectedStructType = structType(expectedReturnType); - if (userReturned instanceof StrandsNode) { - const returnedNode = getNodeDataFromID(strandsContext.dag, userReturned.id); + if (effectiveReturn instanceof StrandsNode) { + const returnedNode = getNodeDataFromID(strandsContext.dag, effectiveReturn.id); if (returnedNode.baseType !== expectedStructType.typeName) { - FES.userError("type error", `You have returned a ${userReturned.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); + FES.userError("type error", `You have returned a ${effectiveReturn.baseType} from ${hookType.name} when a ${expectedStructType.typeName} was expected.`); } const newDeps = returnedNode.dependsOn.slice(); for (let i = 0; i < expectedStructType.properties.length; i++) { const expectedType = expectedStructType.properties[i].dataType; - const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[userReturned.id], strandsContext); + const receivedNode = createStrandsNode(returnedNode.dependsOn[i], dag.dependsOn[effectiveReturn.id], strandsContext); newDeps[i] = enforceReturnTypeMatch(strandsContext, expectedType, receivedNode, hookType.name); } - dag.dependsOn[userReturned.id] = newDeps; - rootNodeID = userReturned.id; + dag.dependsOn[effectiveReturn.id] = newDeps; + rootNodeID = effectiveReturn.id; } else { const expectedProperties = expectedStructType.properties; @@ -392,11 +407,11 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { for (let i = 0; i < expectedProperties.length; i++) { const expectedProp = expectedProperties[i]; const propName = expectedProp.name; - const receivedValue = userReturned[propName]; + const receivedValue = effectiveReturn[propName]; if (receivedValue === undefined) { FES.userError('type error', `You've returned an incomplete struct from ${hookType.name}.\n` + `Expected: { ${expectedReturnType.properties.map(p => p.name).join(', ')} }\n` + - `Received: { ${Object.keys(userReturned).join(', ')} }\n` + + `Received: { ${Object.keys(effectiveReturn).join(', ')} }\n` + `All of the properties are required!`); } const expectedTypeInfo = expectedProp.dataType; @@ -409,7 +424,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) { } else /*if(isNativeType(expectedReturnType.typeName))*/ { const expectedTypeInfo = TypeInfoFromGLSLName[expectedReturnType.typeName]; - rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, userReturned, hookType.name); + rootNodeID = enforceReturnTypeMatch(strandsContext, expectedTypeInfo, effectiveReturn, hookType.name); } const fullHookName = `${hookType.returnType.typeName} ${hookType.name}`; const hookInfo = availableHooks[fullHookName]; From 6b8e46b44445bb0528391272fc2a9a78f6c43b92 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Date: Sun, 4 Jan 2026 16:20:03 -0800 Subject: [PATCH 2/2] Add visual tests for auto-return shader hooks --- test/unit/visual/cases/webgl.js | 105 ++++++++++++++++++ .../000.png | Bin 0 -> 1136 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 760 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 946 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 619 bytes .../metadata.json | 3 + .../000.png | Bin 0 -> 232 bytes .../metadata.json | 3 + .../explicit return still works/000.png | Bin 0 -> 232 bytes .../explicit return still works/metadata.json | 3 + 13 files changed, 123 insertions(+) create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return preserves multiple property modifications/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return preserves multiple property modifications/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return works with getCameraInputs/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return works with getCameraInputs/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return works with getObjectInputs/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return works with getObjectInputs/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return works with getPixelInputs/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return works with getPixelInputs/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-returns input struct when return is omitted/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-returns input struct when return is omitted/metadata.json create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/000.png create mode 100644 test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/metadata.json diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 9dc8bb36a7..5ef52bba54 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -807,5 +807,110 @@ visualSuite('WebGL', function() { p5.circle(p5.noise(0), p5.noise(0), 20); screenshot(); }); + + visualSuite('auto-return for shader hooks', () => { + visualTest('auto-returns input struct when return is omitted', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseMaterialShader().modify(() => { + p5.getWorldInputs((inputs) => { + inputs.position.x += 10; + // No explicit return - should auto-return inputs + }); + }, { p5 }); + p5.background(255); + p5.noStroke(); + p5.shader(shader); + p5.sphere(20); + screenshot(); + }); + + visualTest('explicit return still works', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseMaterialShader().modify(() => { + p5.getWorldInputs((inputs) => { + inputs.position.x += 10; + return inputs; // Explicit return should still work + }); + }, { p5 }); + p5.background(255); + p5.noStroke(); + p5.shader(shader); + p5.sphere(20); + screenshot(); + }); + + visualTest('auto-return works with getObjectInputs', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseMaterialShader().modify(() => { + p5.getObjectInputs((inputs) => { + inputs.position.x += 0.25; + // No explicit return + }); + }, { p5 }); + p5.background(255); + p5.lights(); + p5.fill('red'); + p5.noStroke(); + p5.rotateY(p5.PI / 2); + p5.camera(-800, 0, 0, 0, 0, 0); + p5.shader(shader); + p5.sphere(20); + screenshot(); + }); + + visualTest('auto-return works with getCameraInputs', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseMaterialShader().modify(() => { + p5.getCameraInputs((inputs) => { + inputs.position.x += 10; + // No explicit return + }); + }, { p5 }); + p5.background(255); + p5.lights(); + p5.fill('red'); + p5.noStroke(); + p5.rotateY(p5.PI / 2); + p5.camera(-800, 0, 0, 0, 0, 0); + p5.shader(shader); + p5.sphere(20); + screenshot(); + }); + + visualTest('auto-return preserves multiple property modifications', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseMaterialShader().modify(() => { + p5.getWorldInputs((inputs) => { + inputs.position.x += 5; + inputs.position.y += 5; + inputs.normal.x += 0.5; + inputs.normal = p5.normalize(inputs.normal); + // No explicit return - all modifications should be preserved + }); + }, { p5 }); + p5.background(255); + p5.lights(); + p5.fill('red'); + p5.noStroke(); + p5.shader(shader); + p5.sphere(20); + screenshot(); + }); + + visualTest('auto-return works with getPixelInputs', (p5, screenshot) => { + p5.createCanvas(50, 50, p5.WEBGL); + const shader = p5.baseMaterialShader().modify(() => { + p5.getPixelInputs((inputs) => { + inputs.color = p5.vec4(1.0, 0.0, 0.0, 1.0); // Red + // No explicit return + }); + }, { p5 }); + p5.background(255); + p5.noStroke(); + p5.shader(shader); + p5.circle(0, 0, 40); + screenshot(); + }); + }); }); }); diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return preserves multiple property modifications/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-return preserves multiple property modifications/000.png new file mode 100644 index 0000000000000000000000000000000000000000..ef6d92a6bd32a2f7643cba1168b061d9490f8b71 GIT binary patch literal 1136 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 VK6h?QC=*bG!PC{xWt~$(69CiFR?h$c literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-returns input struct when return is omitted/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-returns input struct when return is omitted/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/auto-returns input struct when return is omitted/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/000.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb873e934d0427fd2e3f3510b1c45b99e593fd1 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nETxt=bLAr*{oCLQEF;2^+s`0M@R zOAYgkRt;MP`&jTc+_4277ouO}UG6Sq3z z!LcT`TTnXog@7_sj_Dx}(fty~I~sy|4T6s9A2U>15Te`@^0B+HheP8khjQqjBpGEU zFSxQ#=_cb@9nhm>C%U|8H1)Y!=X=Y#@g+GvrM< VK6h?QC=*bG!PC{xWt~$(69CiFR?h$c literal 0 HcmV?d00001 diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/auto-return for shader hooks/explicit return still works/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file