godotengine / godotengine/godot

GPU particles won't emit subparticles at end when parent FPS is sufficiently higher than child FPS

Open
#98,072 2 comments 0 reactions 0 assignees View on GitHub
bug topic:particles
Dominant language
C++
Stars
117k
Forks
26.8k
PR merge metrics
PR metrics pending

Description

### Tested versions

Godot v4.4.dev (db66bd35a)
Godot v4.3.stable.mono

### System information

Godot v4.4.dev (76a135926) - Windows 10.0.19045 - Multi-window, 1 monitor - Vulkan (Forward+) - dedicated AMD Radeon RX 6650 XT (Advanced Micro Devices, Inc.; 31.0.24033.1003) - AMD Ryzen 7 3800X 8-Core Processor (16 threads)

### Issue description

If a parent GPU particle node's FPS is significantly greater than its sub emitter, it will not emit particles. Doing a deep dive into the matter I believe has exposed the main culprit; `vkCmdCopyBuffer` calls will write to the sub-emitter buffer while a read on the next frame is required to reset and activate particles in the child.

As you can see here, the source/dest buffer is written to as 2 particles were set to be emitted by `emit_subparticle` within the compute shader.
![image](https://github.com/user-attachments/assets/31d355ce-582d-4f41-ad43-41aaacfa948d)
In addition you can tell that at this step in the frame, another `vkCmdCopyBuffer` is queued working over the same buffers as in EID 56 and 57...and as you can see on the next copy to buffer, the value is reset
![image](https://github.com/user-attachments/assets/5e3745ef-b9a5-4788-a4b1-d13937a73f97)

The compute shader being dispatched at this point are `particles.glsl` shaders of the parent, as the child runs its particles compute and copy particles compute before the parent dispatches for additional context. This is important because the child shader runs at the beginning of the compute step in the frame, and relies on the buffer not having a 0 value in order to reset particles.
![image](https://github.com/user-attachments/assets/d67a16ab-3a44-462d-8cf8-281ee110878f)
As demonstrated above, the "flushing" of particles is done on the first part of the compute pass with the amount to reset set on the previous frame.

It is also possible that accumulation of fractional delta values when using non-integer lifetimes can allow particles to sometimes be emitted, as the write to the buffer saying new particles were created can happen at the end, it just isn't likely while using integer lifetime values for the parent...although using fractional lifetime values and changing the lifetime to an integer amount can retain enough fractional accumulation that the emission trigger will occur at the end of the compute stage, making it appear as if the bug sometimes is present and sometimes not.

As far as a fix goes, my thought is that in particles storage that you'd need to read the particle count value from the emission buffer and only write to that buffer if the particles count is less than 0, although I am not sure if that is just a band-aid solution. As such I think a fix may warrant a discussion (although I have tested locally that it does indeed work for the test case).

### Steps to reproduce

Create a GPU particles parent node
set its FPS to 60
Create a GPU particles child node
Set as the child
retain its FPS of 30.

See how no particles are generated

### Minimal reproduction project (MRP)

[particlebugreport.zip](https://github.com/user-attachments/files/17334214/particlebugreport.zip)

Contributor guide

Open the contributing guide

Research direction

Start with the provided particlebugreport.zip reproduction and inspect the particles.glsl compute shaders and particle storage mentioned in the report. Trace the emission-buffer copy, reset, and read ordering across parent and child compute passes; done means the child emits particles reliably when the parent runs at 60 FPS and the child at 30 FPS.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
computer-graphics, game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.