Skip to content

feat: particle system component#7350

Merged
pravusjif merged 31 commits intodevfrom
feat/sdk-particle-system
Apr 10, 2026
Merged

feat: particle system component#7350
pravusjif merged 31 commits intodevfrom
feat/sdk-particle-system

Conversation

@pravusjif
Copy link
Copy Markdown
Member

@pravusjif pravusjif commented Mar 6, 2026

WHY

New ParticleSystem SDK component is being introduced to expand the set of tools for creators.

Notion doc: https://www.notion.so/decentraland/SDK-Particles-2c65f41146a580969aaceb9f4ee01ea8

WHAT

  • Implemented support for new ParticleSystem SDK component
  • Implemented test coverage
  • Implemented Particles Sytem Widget for the Debug Panel container showing the current amount of particles in the current scene + the maximum amount. The ParticleSystemBudgetSystem downgrades the emission rate of ALL of the playing particle systems in the scene proportionally, to keep the amount below the max.

Related PRs:

TEST INSTRUCTIONS

  1. Download the build from this PR and open it connected to the SEPOLIA sdk7testscenes.dcl.eth world and to position 0,7:

Windows
"C:\Users\[YOUR-USER]\Downloads\Decentraland_windows64\Decentraland.exe" --realm sdk7testscenes.dcl.eth --dclenv zone --position 0,7 --debug --skip-version-check true

macOS
open Decentraland.app --args --realm sdk7testscenes.dcl.eth --dclenv zone --position 0,7 --debug --skip-version-check true

(Generic instructions to connect a custom build with app params at: https://github.com/decentraland/unity-explorer/blob/dev/docs/how-to-connect-to-a-local-scene.md)

  1. Once you enter the scene confirm that you see the particle systems working, similar to what you see in the demo video (there are MANY parameters for each particle system, so just play a bit with some parameters in some particle systems and confirm that they have an effecton the particle system).

  2. Confirm that in the Debug Panel you see a widget for Particles and you see that it stays AROUND 1000

Demo Video

Screen.Recording.2026-04-02.at.1.24.24.AM-L.mp4

@pravusjif pravusjif self-assigned this Mar 6, 2026
@pravusjif pravusjif added sdk shape-up It has been shaped and is awaiting the cycle. labels Mar 6, 2026
@pravusjif pravusjif moved this from Todo to In Progress in Creators Tools Mar 6, 2026
@pravusjif pravusjif moved this from Todo to In Progress in Explorer Alpha Mar 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 6, 2026

@pravusjif pravusjif added the force-build Used to trigger a build on draft PR label Mar 9, 2026
@pravusjif pravusjif added force-build Used to trigger a build on draft PR and removed force-build Used to trigger a build on draft PR labels Mar 13, 2026
@pravusjif pravusjif added force-build Used to trigger a build on draft PR and removed force-build Used to trigger a build on draft PR labels Mar 18, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 6, 2026

Claude finished @pravusjif's task in 4m 0s —— View job


Code Review

  • Gather context and read changed files
  • Verify previous issues were fixed
  • Review new/changed code for bugs and standards violations
  • Post inline comments and summary

Previously reported issues — confirmed fixed ✅

Issue Status
Ref written after structural change (LifecycleSystem.cs:43-44) ✅ Fixed
Missing World.Remove<ParticleSystemComponent> in cleanup (CleanupSystem.cs:44) ✅ Fixed
GC allocation in ApplyEmissionCachedBursts pattern (ApplyPropertiesSystem.cs:118) ✅ Fixed

New issue — must fix before merge

TextureData never dereferenced → memory leak ParticleSystemComponent.cs:53

CleanUpTexture correctly cancels in-flight promises via ForgetLoading, but it never calls Dereference() on a resolved TextureData. The streaming system uses reference counting (StreamableRefCountData) to decide when a texture can be unloaded. Without a matching Dereference() the reference count never reaches zero, so every unique particle texture leaks until the world is destroyed.

Compare with the established pattern in LightSourceComponent.CleanUp (LightSourceComponent.cs:71):

SourceTextureData?.Dereference();
SourceTextureData = null;

A secondary symptom: ParticleMaterial.mainTexture is never cleared, so removing the texture field from a PBParticleSystem leaves the old texture rendering.

Fix — add LoadedTextureData to ParticleSystemComponent and update CleanUpTexture to dereference it and null out mainTexture (see inline comment).


REVIEW_RESULT: FAIL ❌

@pravusjif pravusjif removed the request for review from fcolarich April 7, 2026 11:20
@pravusjif
Copy link
Copy Markdown
Member Author

@claude review

@pravusjif pravusjif moved this from In Progress to With QA / Awaiting Review in Explorer Alpha Apr 7, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 7, 2026

Claude finished @pravusjif's task in 3m 48s —— View job


Code Review

  • Gather context and read changed files
  • Verify all previously reported issues are fixed
  • Review new/changed code for bugs and standards violations
  • Post inline comments and summary

Previously reported issues — status

Issue Status
Ref written after structural change (LifecycleSystem) ✅ Fixed
Missing World.Remove<ParticleSystemComponent> in cleanup ✅ Fixed
GC allocation in ApplyEmission (CachedBursts) ✅ Fixed
alejandro-jimenez-dcl review comments (initial size, utils extraction, etc.) ✅ Addressed

Remaining issue — must fix before merge

Resolved TextureData still not dereferenced → memory leak ParticleSystemComponent.cs:54

The fix in 4ba7b701 calls promise.TryDereference(world) inside CleanUpTexture, but only when TexturePromise != null. The problem is that ResolveTexturePromise sets component.TexturePromise = null after consuming the promise. So when CleanUpTexture runs after a texture has already resolved (e.g., texture URL changes, or texture field is removed), TexturePromise is null, the block is skipped, and TextureData.Dereference() is never called.

The test DereferenceResolvedTextureOnCleanup does NOT catch this: it inserts the result directly onto the promise entity without calling TryConsume, so TexturePromise is still non-null during cleanup. The actual production code path (consume → null out → later cleanup) is untested and leaks.

Fix — follow the LightSourceComponent.SourceTextureData pattern (see inline comment):

  1. Add public TextureData? LoadedTextureData to ParticleSystemComponent
  2. In ResolveTexturePromise: component.LoadedTextureData = result.Asset
  3. In CleanUpTexture: LoadedTextureData?.Dereference(); LoadedTextureData = null; if (ParticleMaterial != null) ParticleMaterial.mainTexture = null;
  4. Add a test that consumes the promise first, then calls cleanup, and asserts referenceCount == 0

REVIEW_RESULT: FAIL ❌

@pravusjif
Copy link
Copy Markdown
Member Author

@claude review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 7, 2026

Claude finished @pravusjif's task in 3m 34s —— View job


Code Review

  • Gather context and read changed files
  • Verify previously reported texture dereference fix (1d2f9a38)
  • Review remaining systems and components for new issues
  • Post summary

Previously reported issues — status

Issue Status
Ref written after structural change (LifecycleSystem) ✅ Fixed
Missing World.Remove<ParticleSystemComponent> in cleanup ✅ Fixed
GC allocation in ApplyEmission (CachedBursts) ✅ Fixed
TextureData never dereferenced → memory leak ✅ Fixed — SourceTextureData pattern now matches LightSourceComponent, test covers post-consume path

Remaining issue — must fix before merge

ParticleMaterial.mainTexture not cleared on texture removal ParticleSystemComponent.cs:66

SourceTextureData?.Dereference() correctly decrements the streaming system's reference count, which can trigger the texture being unloaded. However ParticleMaterial.mainTexture is never set to null in CleanUpTexture. Once the streaming system unloads the underlying Texture2D, the material holds a reference to a destroyed Unity object, causing rendering artifacts.

This was flagged in the round-2 review as a secondary symptom. It remains unaddressed.

Fix — add ParticleMaterial.mainTexture = null after nulling SourceTextureData (see inline comment):

SourceTextureData?.Dereference();
SourceTextureData = null;

if (ParticleMaterial != null)
    ParticleMaterial.mainTexture = null;

REVIEW_RESULT: FAIL ❌

@pravusjif
Copy link
Copy Markdown
Member Author

@claude review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 7, 2026

Claude finished @pravusjif's task in 3m 7s —— View job


Code Review

  • Gather context and read changed files
  • Verify previously reported mainTexture fix (1135d4b3)
  • Review remaining systems and components for new issues
  • Post inline comments and summary

Previously reported issues — status

Issue Status
Ref written after structural change (LifecycleSystem) ✅ Fixed
Missing World.Remove<ParticleSystemComponent> in cleanup ✅ Fixed
GC allocation in ApplyEmission (CachedBursts) ✅ Fixed
TextureData never dereferenced → memory leak (SourceTextureData pattern) ✅ Fixed
ParticleMaterial.mainTexture not cleared on texture removal ✅ Fixed — CleanUpTexture now sets mainTexture = null, test ClearMaterialMainTextureOnTextureCleanup covers it

New issue — must fix before merge

Negative emission multiplier when far over budget ParticleSystemBudgetSystem.cs:40

When totalParticles > maxSceneParticles / 0.2 (i.e. more than 5× the budget), the expression ((float)maxSceneParticles / totalParticles) - EMISION_MULTIPLIER_EXTRA_THRESHOLD goes negative. Setting rateOverTimeMultiplier to a negative value is undefined — Unity does not guarantee it clamps internally, and the behaviour can change across versions.

Fix — clamp to zero (see inline comment):

float multiplier = totalParticles <= maxSceneParticles
    ? 1f
    : Mathf.Max(0f, ((float)maxSceneParticles / totalParticles) - EMISION_MULTIPLIER_EXTRA_THRESHOLD);

REVIEW_RESULT: FAIL ❌

@pravusjif
Copy link
Copy Markdown
Member Author

@claude review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 7, 2026

Claude finished @pravusjif's task in 4m 4s —— View job


Code Review

  • Gather context
  • Verify all previously reported fixes
  • Review current code for remaining issues
  • Post summary

Previously reported issues — all confirmed fixed ✅

Issue Status
Ref written after structural change (LifecycleSystem.cs:43-44) ✅ Fixed
Missing World.Remove<ParticleSystemComponent> in cleanup ✅ Fixed
GC allocation in ApplyEmission (CachedBursts pattern) ✅ Fixed
TextureData never dereferenced → memory leak (SourceTextureData pattern) ✅ Fixed
ParticleMaterial.mainTexture not cleared on texture removal ✅ Fixed
Negative emission multiplier when far over budget (Mathf.Max(0f, …)) ✅ Fixed — f019d2ce

No new blocking issues found

The current implementation is consistent with established codebase patterns:

  • CleanUpTexture uses ForgetLoading (in-flight) + SourceTextureData?.Dereference() (resolved) — matches LightSourceComponent.CleanUp exactly.
  • ResolveTexturePromise correctly stores result.Asset into SourceTextureData and applies mainTexture via the TextureData → Texture? implicit conversion.
  • ReleaseParticleSystem correctly releases material back to pool after CleanUpTexture clears mainTexture, avoiding dangling material references.
  • Budget system executes last in the group (after PlaybackSystem) and the Mathf.Max(0f, …) clamp prevents negative multipliers at any particle count.

REVIEW_RESULT: PASS ✅

@github-project-automation github-project-automation Bot moved this from With QA / Awaiting Review to In Progress in Explorer Alpha Apr 8, 2026
@pravusjif pravusjif moved this from In Progress to With QA / Awaiting Review in Explorer Alpha Apr 8, 2026
@balducciv
Copy link
Copy Markdown

balducciv commented Apr 9, 2026

✅ PR reviewed and tested by QA on both platforms following the test scene at scenes/0,7-particle-system.
✅ Smoke test performed on Windows and macOS to verify the normal flow is working as expected.

Build: v0.138.0-alpha-feat/sdk-particle-system-f019d2c
OS: macOS 15.7.4 — Apple M3 Pro / Windows 11
Environment: Tested in zone realm sdk7testscenes.dcl.eth but also tested previous versions on pravus.dcl.eth in org.

Test scene: sdk7testscenes.dcl.eth (Sepolia) — 17 particle system examples covering all PBParticleSystem features

Test results:

  • All 17 emitters render and run correctly on scene entry
  • Playback controls (Play / Pause / Stop) work as expected across emitters
  • Flags (Active, Loop, Prewarm, FaceDir) toggle correctly and affect emission behavior
  • Billboard toggle correctly controls whether particles face the camera
  • Simulation Space (Local / World) works correctly — validated on Moving Trail with Tween YOYO
  • Blend modes (Alpha, Additive, Multiply) render correctly and switch in real time
  • All 4 emitter shapes (Point, Sphere, Cone, Box) produce correct spawn volumes
  • Shape visualizer shows/hides correctly, updates in real time when shape is changed, and casts no shadows
  • Emission fields (Rate, Lifetime, Max Particles) respond to stepper buttons and text input
  • Per-property Reset (R) buttons restore original values correctly
  • Gravity behaves correctly across positive, negative, and zero values
  • Velocity range produces varied speeds within start/end bounds
  • Size Over Time (shrink, grow, constant) works correctly per emitter
  • Rotation (Initial Rotation + Rotation Over Time) as quaternion values applied correctly — validated on Vortex Spiral and Tumbling Leaves with billboard: false
  • FaceDir validated on Asteroid Trail — particles orient to travel direction
  • Color hex input and separate alpha control work correctly; particles fade out smoothly at end of lifetime
  • Limit Velocity (speed cap + dampen) works correctly — validated on Lightning Sparks, Heavy Rain, Asteroid Trail, One-Shot Burst
  • Additional Force validated on Purple Swirl and Vortex Spiral — lateral drift applies correctly and toggles off cleanly
  • All 4 spritesheet emitters (Bat Swarm 4×4, Bee Swarm 1×20, Campfire 4×3, Flame Wisps 4×3) animate correctly at expected frame rates
  • One-Shot Burst fires once on entry and stops; does not re-fire on scene re-entry
  • Fireworks Loop fires 3 staggered bursts per cycle with correct probability behavior on Burst 2
  • Burst panel controls (add, remove, time/count/cycles/interval/probability) work correctly; max 8 bursts enforced
  • Copy button outputs a valid SDK snippet reflecting the current panel state; "Copied!" feedback appears
  • Regression: box shape size no longer bleeds into other shapes after switching
  • Regression: simulation space correctly affects force-over-lifetime on Purple Swirl
  • Regression: cone shape visualizer renders correctly on Snowfall and Asteroid Trail
  • Regression: Bat Swarm billboard defaults to ON correctly
  • Console Particles: Scene particles do not surpass 1000
  • Can leave the scene and particles stop playing. When returning to the scene, particles resume.
  • Tested the scene with 2-3 players and particles behaved as expected
  • No errors on the Scene console (triggered witth args --scene-console)

Unrelated errors noted (do not affect verdict):

  • GPUInstancerPro NullReferenceException on GPUIOcclusionCullingData — GPU instancing occlusion culling issue, unrelated to particle system
  • HTTP 404 on globalpx.dcl.eth/about — global portable experience not deployed in Sepolia zone, expected in this environment
  • Curl error 42: Callback aborted — recurring non-blocking network noise on scene dispose/reload cycles, known issue
  • Missing script references (EditorNote, Chat Section) — editor-only components, not relevant at runtime
  • LensFlareDataDriven shader not supported — render pipeline warning, unrelated

Verdict: PASS ✅

Player.log

Mac:
https://github.com/user-attachments/assets/345d4471-7732-4dd0-956b-888829c81dfc

Windows:

09.04.2026_17.19.03_REC.mp4

@github-project-automation github-project-automation Bot moved this from With QA / Awaiting Review to In Progress in Explorer Alpha Apr 9, 2026
@pravusjif pravusjif moved this from In Progress to With QA / Awaiting Review in Explorer Alpha Apr 10, 2026
@pravusjif pravusjif merged commit d4c1fa9 into dev Apr 10, 2026
16 of 19 checks passed
@pravusjif pravusjif deleted the feat/sdk-particle-system branch April 10, 2026 11:32
@github-project-automation github-project-automation Bot moved this from With QA / Awaiting Review to Done in Explorer Alpha Apr 10, 2026
@github-project-automation github-project-automation Bot moved this from QA to To Release in Creators Tools Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk shape-up It has been shaped and is awaiting the cycle.

Projects

Status: To Release
Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants