Skip to content

fix(particles): upload CPU vertex buffer on rebuild/reset so paused emitters don't render garbage#8945

Merged
willeastcott merged 2 commits into
mainfrom
fix-particles-paused-rebuild-vb-upload
Jun 23, 2026
Merged

fix(particles): upload CPU vertex buffer on rebuild/reset so paused emitters don't render garbage#8945
willeastcott merged 2 commits into
mainfrom
fix-particles-paused-rebuild-vb-upload

Conversation

@willeastcott

Copy link
Copy Markdown
Contributor

Description

CPU particle emitters (sort != PARTICLESORT_NONE) fill their vertex buffer in addTime() but defer the GPU upload to ParticleEmitter.finishFrame(), which ParticleSystemComponentSystem.onUpdate only calls while !component._paused.

When a colorMap texture with preload: false finishes loading after the system has been paused, the resulting colorMap setter → _setComplexPropertyrebuild() + reset() leaves the freshly-allocated vertex buffer unflushed. The GPU keeps rendering the stride-4 quad template uploaded by _allocate(), reinterpreted as the 15-float CPU vertex format → garbage scale/positions → billboards stretched fullscreen on the first frame.

Fix: flush the buffer with finishFrame() after the initial addTime(0) fill in rebuild() and reset(). It's a no-op for GPU particles and safe to call without a matching lock() (the non-paused update loop already does so every frame).

Verified in-browser against the issue's repro (autoPlay + sort:2 + non-preloaded colorMap + pause() on setTimeout(0)): the fullscreen stretch is gone and the playing path is unaffected. Existing particle-system tests pass. (The headless test harness uses NullGraphicsDevice, which disables particle systems, so the emitter vertex-buffer path can't be unit-tested there.)

Fixes #5993

Checklist

  • I have read the contributing guidelines
  • My code follows the project's coding standards
  • This PR focuses on a single change

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2283.1 KB (+0.0 KB, +0.00%) 585.6 KB (+0.0 KB, +0.00%) 455.4 KB (−0.1 KB, −0.03%)
playcanvas.min.mjs 2280.5 KB (+0.0 KB, +0.00%) 584.9 KB (+0.0 KB, +0.00%) 454.9 KB (+0.1 KB, +0.02%)

CPU particle emitters (sort != NONE) fill their vertex buffer in addTime()
but defer the GPU upload to finishFrame(), which the component system only
calls while not paused. When a non-preloaded colorMap loads after the system
is paused, the resulting rebuild()+reset() leave the freshly-allocated buffer
unflushed, so the GPU keeps rendering the _allocate template data reinterpreted
as the CPU vertex format - producing fullscreen-stretched billboards.

Flush the buffer via finishFrame() after the initial fill in rebuild()/reset().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated PR review — posted on my behalf by Claude Code (Opus 4.8). Not a human review; flagging for awareness.


Verdict: correct and minimal. No blocking issues.

I traced the lock/upload lifecycle to confirm the fix is safe:

  • In CPU mode, addTime() (particle-emitter.js:1082) calls this.vertexBuffer.lock() and deliberately leaves it unlocked (line 1084 is commented out) — the GPU upload is deferred to finishFrame().
  • finishFrame() (line 1013) only unlocks when useCpu, so it's a genuine no-op for GPU particles. ✓
  • VertexBuffer.unlock() (vertex-buffer.js:147) is stateless — lock() just returns storage, there's no lock flag — so calling it after the addTime(0) fill simply uploads the current contents. Safe with no matching explicit lock(), exactly as the PR notes. ✓

So adding finishFrame() after the initial fill in both rebuild() and reset() correctly flushes the freshly-allocated CPU buffer instead of leaving the stale stride-4 quad template on the GPU for a paused emitter.

Other angles:

  • Breaking changes: none.
  • Performance: in the non-paused path this causes one redundant upload per rebuild()/reset() (the frame's regular finishFrame() uploads again). These are infrequent (asset load / property change), so it's negligible — worth being aware of but not worth guarding.
  • Tests: understandable that the NullGraphicsDevice headless harness can't exercise this; in-browser verification against the #5993 repro is the right call. A line in CHANGELOG/PR noting the manual-verification-only status is already present.

@willeastcott willeastcott merged commit 05d8f45 into main Jun 23, 2026
10 checks passed
@willeastcott willeastcott deleted the fix-particles-paused-rebuild-vb-upload branch June 23, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Particle System Issue - Texture stretched to fullscreen

2 participants