Bullet positions are written to a float texture and updated every frame with a compute shader, then that texture is read by the vertex shader of the bullet material, to set the vericies of every mesh instance to where it’s supposed to be. Collisions are calculated with another computer shader using the result of the position shader as it’s input then the collision results are read back using async texture reading via a Pixel Buffer Object once per frame.
Let me know if that explanation was hard to follow, and I can elaborate on any part.
Also, the slowdowns I find happen when there’s straight up too many vertices on screen, because the bullets aren’t particles, they’re like 150 vertex meshes on the “high” graphics setting. For example, on Lunatic difficulty, the last boss attack is 362,880 bullets at once, multiplied by 150ish is 54,432,000 vertices. (I may have the math wrong on that one, that’s a larger number than I thought)