`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)
Source: pixijs/pixijs