#12181·pixijs

`PrepareUpload` causes race condition in batcher

Author: UlyssesZhCreated Sep 3, 2026Updated Sep 3, 2026

PixiJS version

8.20.1

Link to minimal reproduction

https://jsfiddle.net/UlyssesZhan/s9r0qfvc/1/

Steps to reproduce

See reproduction link.

//   TypeError: Cannot read properties of undefined (reading '_source')
//     at _DefaultBatcher.break (.../pixi.js/lib/rendering/batcher/shared/Batcher.mjs)
//
// The trick is to call renderer.prepare.upload(text) and render the text in the
// SAME tick.  prepare.upload is asynchronous, so the deferred upload later calls
// CanvasTextPipe.initGpuText() again, replacing the already-rendered text's GPU
// batchable with one that has texture === undefined.
import { Application, Container, Graphics, Text, extensions, PrepareSystem } from '@pixi/node';

extensions.add(PrepareSystem);

const app = new Application();
await app.init();

// A normal text object.
const text = new Text({ text: 'race' });

// A masked Graphics causes Pixi to break the batch mid-render
const mask = new Graphics().rect(0, 0, 50, 50).fill(0xffffff);
const masked = new Graphics().circle(25, 25, 20).fill(0xff0000);
masked.mask = mask;

const stage = new Container();
// Text must be collected before the masked object so the undefined-texture
// batch element is present when StencilMaskPipe asks the batcher to break.
stage.addChild(text);
stage.addChild(mask);
stage.addChild(masked);

// 1. Queue the async prepare upload.
app.renderer.prepare.upload(text);

// 2. Render before the async prepare callback runs.
//    This makes Text._didTextUpdate = false after a valid texture is created.
console.log('first render');
app.renderer.render(stage);
console.log('first render done');

// 3. Let PixiJS's Ticker.system run the queued prepare.upload().
//    CanvasTextPipe.initGpuText() replaces the valid GPU data with a fresh
//    BatchableText whose texture is undefined.
await new Promise((resolve) => setTimeout(resolve, 100));

// 4. Force Pixi to rebuild the render group.  Without this, the second render
//    can reuse the first render's already-built instructions, which still point
//    to the old valid GPU batchable.  Adding/removing a child makes
//    structureDidChange true, so Pixi rebuilds the instruction set and now
//    picks up the fresh BatchableText that has texture === undefined.
stage.addChild(new Container());

// 5. Render again.  Because _didTextUpdate is already false, CanvasTextPipe
//    does not regenerate the texture, and the undefined-texture batchable is
//    sent to the batcher.  The next batch break throws the CI error.
console.log('second render');
app.renderer.render(stage);
console.log('second render done (unexpected)');

await app.destroy();
process.exit(0);

What is expected?

The line second render done (unexpected) is printed.

What is actually happening?

The line second render done (unexpected) is not printed.

System Info

System:
    OS: Linux 6.12 cpe:/o:nixos:nixos:26.05 26.05 (Yarara)
    CPU: (16) x64 AMD Ryzen 7 5800H with Radeon Graphics
    Memory: 7.60 GB / 27.26 GB
    Container: Yes
    Shell: 5.9 - /nix/store/358sk8371mkz87iq17kw4qcyyi6sps0k-zsh-5.9/bin/zsh
  Browsers:
    Brave Browser: 148.1.90.124
    Firefox: 151.0.2
    Firefox Developer Edition: 151.0.2

Any additional comments?

Using @pixi/node can get you additional error after "second render":

file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/rendering/batcher/shared/Batcher.mjs:137
    let blendMode = getAdjustedBlendModeBlend(firstElement.blendMode, firstElement.texture._source);
                                                                                           ^

TypeError: Cannot read properties of undefined (reading '_source')
    at _DefaultBatcher.break (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/rendering/batcher/shared/Batcher.mjs:137:92)
    at _BatcherPipe.break (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/rendering/batcher/shared/BatcherPipe.mjs:46:28)
    at StencilMaskPipe.push (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/rendering/mask/stencil/StencilMaskPipe.mjs:17:37)
    at Graphics.collectRenderablesWithEffects (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/container-mixins/collectRenderablesMixin.mjs:28:12)
    at Graphics.collectRenderables (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/container-mixins/collectRenderablesMixin.mjs:13:12)
    at Container.collectRenderablesSimple (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/container-mixins/collectRenderablesMixin.mjs:20:19)
    at Container.collectRenderablesWithEffects (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/container-mixins/collectRenderablesMixin.mjs:30:10)
    at RenderGroupSystem._buildInstructions (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/RenderGroupSystem.mjs:137:10)
    at RenderGroupSystem._updateRenderGroups (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/RenderGroupSystem.mjs:104:12)
    at RenderGroupSystem.render (file:///home/ulysses/projects/test-text-prepare/node_modules/pixi.js/lib/scene/container/RenderGroupSystem.mjs:30:10)