Skip to content

refactor(particles): use lossless RGBA32U fallback instead of pack8#8926

Merged
mvaligursky merged 2 commits into
mainfrom
mv-particle-u32-textures
Jun 18, 2026
Merged

refactor(particles): use lossless RGBA32U fallback instead of pack8#8926
mvaligursky merged 2 commits into
mainfrom
mv-particle-u32-textures

Conversation

@mvaligursky

Copy link
Copy Markdown
Contributor

Replaces the GPU particle system's lossy pack8 (RGBA8 bit-packing) fallback — used when float textures aren't renderable on some WebGL2 devices lacking EXT_color_buffer_float — with lossless RGBA32U integer textures that store exact float bits. The storage format is selected in-shader via the global CAPS_TEXTURE_FLOAT_RENDERABLE define, mirroring the depth prepass. WebGPU is always float-renderable, so its shaders are unaffected.

Changes:

  • Add GLSL particleInputU32 / particleOutputU32 chunks (read via usampler2D + uintBitsToFloat, write via floatBitsToUint with fragmentOutputTypes: 'uvec4'); simulation and render shaders branch on CAPS_TEXTURE_FLOAT_RENDERABLE.
  • Particle state texture format is now RGBA32F when renderable, else RGBA32U; CPU uploads the same float bytes reinterpreted as Uint32 (lossless, no bounds remapping).
  • Remove the pack8 path entirely: the pack8 flag, calculateBoundsMad, and the maxVel / inBounds / outBounds remapping machinery; the fallback state-texture height drops from 4 rows back to 2.
  • Remove the two dead WebGL1-era useCpu guards (maxVertexTextures <= 1, fragmentUniformsCount < 64) — both are unreachable under WebGL2 spec minimums. CPU sorting remains the genuine CPU-mode trigger.
  • Delete the now-unused particleInputRgba8 / particleOutputRgba8 chunks (GLSL and WGSL) and the dead WGSL PACK8 branches.

Performance:

  • On the fallback path, particle state textures are half the size (2 rows vs 4) and the per-frame bounds-remap uniform work is gone. The common float-renderable path is unchanged.

Notes:

  • The fallback is now bit-exact, removing the previous 16-bit position / 8-bit velocity precision loss.
  • Verified on WebGL2 and WebGPU across the particle examples, including a forced textureFloatRenderable = false run to exercise the RGBA32U path.

When float textures are not renderable (some WebGL2 devices lacking
EXT_color_buffer_float), GPU particle state was stored in RGBA8 textures via
a lossy bit-packing (pack8) with per-frame bounds remapping. Replace this
with lossless RGBA32U integer textures that store exact float bits, selected
in-shader via the global CAPS_TEXTURE_FLOAT_RENDERABLE define. WebGPU is
always float-renderable, so its shaders are unaffected (dead PACK8 branches
removed).

This removes the pack8 flag, calculateBoundsMad, and the maxVel / inBounds /
outBounds remapping machinery, and drops the integer state-texture height
from 4 rows back to 2.

Also remove two dead WebGL1-era useCpu guards (maxVertexTextures <= 1 and
fragmentUniformsCount < 64) that cannot trigger under WebGL2 spec minimums.
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2275.6 KB (−8.6 KB, −0.38%) 584.4 KB (−1.7 KB, −0.29%) 454.2 KB (−1.4 KB, −0.32%)
playcanvas.min.mjs 2273.1 KB (−8.6 KB, −0.38%) 583.5 KB (−1.6 KB, −0.28%) 453.8 KB (−1.4 KB, −0.30%)

The float and u32 readInput/writeOutput chunks differed only in the texel
cast, so fold them into single particleInput / particleOutput chunks. The
GLSL chunk handles the difference internally with a small #ifdef
CAPS_TEXTURE_FLOAT_RENDERABLE around just the cast; the WGSL chunk is the
float logic (WebGPU is always float-renderable). Include sites in both
backends are now identical and the per-format branch is gone from them.

Copilot AI 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.

Pull request overview

This PR refactors the GPU particle system fallback path by removing the legacy lossy pack8 (RGBA8 packing) approach and introducing a lossless “store float bits” path that uses RGBA32U integer state textures when float render targets are unavailable, with shader selection controlled via CAPS_TEXTURE_FLOAT_RENDERABLE.

Changes:

  • Consolidates particle state read/write shader chunks into unified particleInput / particleOutput implementations (GLSL + WGSL) and removes the old PACK8 branches/chunks.
  • Switches GPU particle state textures to RGBA32F when float-renderable, otherwise RGBA32U, and updates CPU uploads to reinterpret float bytes as Uint32 bit patterns.
  • Removes pack8-related bounds/velocity remapping machinery and deletes unreachable legacy CPU-forcing guards.

Reviewed changes

Copilot reviewed 21 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/scene/shader-lib/wgsl/collections/particle-chunks-wgsl.js Rewires WGSL particle chunk registry to use unified input/output chunks.
src/scene/shader-lib/wgsl/chunks/particle/vert/particle-shader.js Removes PACK8 include branching; always includes unified input chunk.
src/scene/shader-lib/wgsl/chunks/particle/frag/particleOutputRgba8.js Deletes the old RGBA8 packing output path.
src/scene/shader-lib/wgsl/chunks/particle/frag/particleOutput.js Adds unified WGSL particle state output (2-row layout).
src/scene/shader-lib/wgsl/chunks/particle/frag/particleInputRgba8.js Deletes the old RGBA8 packing input path.
src/scene/shader-lib/wgsl/chunks/particle/frag/particleInput.js Adds unified WGSL particle state input (2-row layout).
src/scene/shader-lib/wgsl/chunks/particle/frag/particle-simulation.js Removes PACK8 branching; uses unified input/output chunks.
src/scene/shader-lib/programs/particle.js Removes PACK8 define generation from particle render shader program.
src/scene/shader-lib/glsl/collections/particle-chunks-glsl.js Rewires GLSL particle chunk registry to use unified input/output chunks.
src/scene/shader-lib/glsl/chunks/particle/vert/particle-shader.js Removes PACK8 include branching; always includes unified input chunk.
src/scene/shader-lib/glsl/chunks/particle/vert/particle_init.js Switches particle state samplers between sampler2D / usampler2D based on CAPS_TEXTURE_FLOAT_RENDERABLE.
src/scene/shader-lib/glsl/chunks/particle/frag/particleUpdaterInit.js Switches particleTexIN between sampler2D / usampler2D based on CAPS_TEXTURE_FLOAT_RENDERABLE.
src/scene/shader-lib/glsl/chunks/particle/frag/particleOutputRgba8.js Deletes the old RGBA8 packing output path.
src/scene/shader-lib/glsl/chunks/particle/frag/particleOutputFloat.js Deletes the old float output chunk (replaced by unified output).
src/scene/shader-lib/glsl/chunks/particle/frag/particleOutput.js Adds unified GLSL particle state output, writing float or float-bit uvec4 based on capability define.
src/scene/shader-lib/glsl/chunks/particle/frag/particleInputRgba8.js Deletes the old RGBA8 packing input path.
src/scene/shader-lib/glsl/chunks/particle/frag/particleInputFloat.js Deletes the old float input chunk (replaced by unified input).
src/scene/shader-lib/glsl/chunks/particle/frag/particleInput.js Adds unified GLSL particle state input, reading float or uint-bit floats based on capability define.
src/scene/shader-lib/glsl/chunks/particle/frag/particle-simulation.js Removes PACK8 branching; uses unified input/output chunks.
src/scene/particle-system/particle-material.js Removes pack8 option from particle material shader option set.
src/scene/particle-system/particle-emitter.js Selects RGBA32F vs RGBA32U for GPU state textures; removes pack8 sizing/remap logic; sets integer fragment output type.
src/scene/particle-system/gpu-updater.js Removes pack8-only uniforms/bounds remap uploads.
src/scene/particle-system/cpu-updater.js Removes pack8 encoding logic and simplifies spawn-state initialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 624 to +630
if (!this.useCpu) {
if (this.pack8) {
this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, PIXELFORMAT_RGBA8, 1, false);
this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, PIXELFORMAT_RGBA8, 1, false);
this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart, PIXELFORMAT_RGBA8, 1, false);
} else {
this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex);
this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex);
this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart);
}
// use float textures when renderable, otherwise fall back to integer textures storing
// the exact float bits (RGBA32U is always renderable on WebGL2 and WebGPU)
const particleFormat = gd.textureFloatRenderable ? PIXELFORMAT_RGBA32F : PIXELFORMAT_RGBA32U;
this.particleTexIN = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, particleFormat);
this.particleTexOUT = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTex, particleFormat);
this.particleTexStart = _createTexture(gd, this.numParticlesPot, particleTexHeight, this.particleTexStart, particleFormat);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

rubbish, those are in code webgl 2.0 spec as renderable.

@mvaligursky mvaligursky merged commit 5f07961 into main Jun 18, 2026
11 checks passed
@mvaligursky mvaligursky deleted the mv-particle-u32-textures branch June 18, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants