Skip to content

[p5.js 2.0+ Bug Report]: stale sharedVariables leak into later p5.strands shaders #8875

@eupthere

Description

@eupthere

Most appropriate sub-area of p5.js?

  • Accessibility
  • Color
  • Core/Environment/Rendering
  • Data
  • DOM
  • Events
  • Image
  • IO
  • Math
  • Typography
  • Utilities
  • WebGL
  • WebGPU
  • p5.strands
  • Build process
  • Unit testing
  • Internationalization
  • Friendly errors
  • Other (specify if possible)

p5.js version

2.3.0

Web browser and version

Firefox 151.0.2

Operating system

MacOSX 26.5.1

Steps to reproduce this

Steps:

  1. Create a p5.strands shader using shared*() or varying*() with a name containing __, for example let __worldPos = varyingVec3();.
  2. Build another unrelated shader afterward in the same p5 instance, without using __worldPos at all.
  3. The later shader can fail compilation because the old shared/varying declaration leaks into the generated shader source.

Snippet:

function setup() {
  createCanvas(400, 400, WEBGL);

  const firstShader = baseMaterialShader().modify(() => {
    let __worldPos = varyingVec3();
    getWorldInputs((inputs) => {
      __worldPos = inputs.position.xyz;
      return inputs;
    });
    getFinalColor(() => {
      return [abs(__worldPos / 25), 1];
    });
  });

  console.log('firstShader varyings:', firstShader?.hooks?.varyingVariables);

  const secondShader = baseFilterShader().modify(() => {
    filterColor.begin();
    filterColor.set([1, 0, 0, 1]);
    filterColor.end();
  });

  console.log('secondShader varyings:', secondShader?.hooks?.varyingVariables);

  background(255);
  filter(secondShader);

  noLoop();
}

In the generated shader source, the later shader unexpectedly contains:

OUT vec3 __worldPos;

even though the later shader callback never mentions worldPos or __worldPos.

Additionally, logging the shader hook metadata shows that the second shader already contains the stale varying before it is ever compiled:

console.log(firstShader?.hooks?.varyingVariables);
// ["vec3 __worldPos"]

console.log(secondShader?.hooks?.varyingVariables);
// ["vec3 __worldPos"]

Suspected cause

sharedVariables in p5.strands appears to be lazily created in src/strands/strands_api.js, but it does not seem to be reset in initStrandsContext() / deinitStrandsContext() in src/strands/p5.strands.js.

// Initialize shared variables tracking if not present
if (!strandsContext.sharedVariables) {
strandsContext.sharedVariables = new Map();
}

function initStrandsContext(
ctx,
backend,
{ active = false, renderer = null, baseShader = null } = {},
) {
ctx.dag = createDirectedAcyclicGraph();
ctx.cfg = createControlFlowGraph();
ctx.uniforms = [];
ctx.vertexDeclarations = new Set();
ctx.fragmentDeclarations = new Set();
ctx.computeDeclarations = new Set();
ctx.hooks = [];
ctx.backend = backend;
ctx.active = active;
ctx.renderer = renderer;
ctx.baseShader = baseShader;
ctx.previousFES = p5.disableFriendlyErrors;
ctx.windowOverrides = {};
ctx.fnOverrides = {};
ctx.graphicsOverrides = {};
ctx._randomSeed = null;
if (active) {
p5.disableFriendlyErrors = true;
}
ctx.p5 = p5;
}
function deinitStrandsContext(ctx) {
ctx.dag = createDirectedAcyclicGraph();
ctx.cfg = createControlFlowGraph();
ctx.uniforms = [];
ctx.vertexDeclarations = new Set();
ctx.fragmentDeclarations = new Set();
ctx.computeDeclarations = new Set();
ctx.hooks = [];
ctx.active = false;
ctx._randomSeed = null;
p5.disableFriendlyErrors = ctx.previousFES;
for (const key in ctx.windowOverrides) {
window[key] = ctx.windowOverrides[key];
}
for (const key in ctx.fnOverrides) {
fn[key] = ctx.fnOverrides[key];
}
// Clean up the hooks temporarily installed on p5.Graphics.prototype (#8549)
const GraphicsProto = p5.Graphics?.prototype;
if (GraphicsProto) {
for (const key in ctx.graphicsOverrides) {
if (ctx.graphicsOverrides[key] === undefined) {
delete GraphicsProto[key];
} else {
GraphicsProto[key] = ctx.graphicsOverrides[key];
}
}
}
}

That suggests stale shared*() / varying*() bookkeeping may leak across modify() calls on the reused strands context object, which can affect later tests/shader builds.

Expected behavior

Each modify() call should start with a fresh per-build sharedVariables state, so declarations from earlier shader builds do not leak into later unrelated shaders.

Additional context

This surfaced while working on a separate fix for inferred uniform names containing __ (#8794). As new regression tests were added for varying*(), shared*(), and related shader-side identifiers, unrelated later tests started failing because stale sharedVariables state appeared to leak into subsequent modify() calls. That makes debugging and writing regression coverage harder, because a test can fail with declarations originating from a previous shader build rather than from its own callback.

Unrelated tests failing:

Image

Unexpected shared variable(__worldPos) added to the shader source:

Image

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions