I'm trying to do a particle sim, where particle lo...
# compose-desktop
u
I'm trying to do a particle sim, where particle looks like
Particle(val position: Offset, val velocity: Offset, val alpha: Float)
when I'm updating a particle, I'm doing it via
Copy code
var particle by remember { mutableStateOf(Particle(..))
..
particle = particle.copy(position = ... )
Works, but since this is a sim, that's recalculated every frame, I don't want to create instances on every frame So to avoid it, I want to have a mutable model This is what I came up with
Copy code
class Particle(
    initialPosition: Offset,
    initialVelocity: Offset,
    val alpha: Float
) {
    var position: Offset by mutableStateOf(initialPosition)
    var velocity: Offset by mutableStateOf(initialVelocity)
}

...

particle.position = ...
How does this look? Is it idiomatic compose? Do I need the
@Stable
annotation or is this fine?
r
FWIW I wouldn't have particles carry mutable state
Ideally your particles would be stored in SoA (structure of arrays) form, for efficiency
(by mutable state I meant
mutableStateOf()
)
u
Fair, but that means something like this
data class Particles(positions: Array<Offset>, .. )
but then if I just
particles.positions[i] = ..
, how will compose recompose?
r
You will update all particles on every frame, so recomposition shouldn't be driven by a single particle updating
Similarly what you want is not recomposition but redraw
There's no great way to drive drawing in Compose like with Views, but you could for instance have a counter that increments on every simulated frame
You just need to read that counter at draw time to trigger a redraw
u
okay so compose doesnt track
Particles
, gotcha btw why would my approach not be great? Obviously there's more overhead, but aren't the recompositions batched somehow to vsync? i.e. if I update bunch of things in a loop, I'll only get one recomposition/redraw, no?
r
They are batched but you are going to pay a lot of overhead for no good reason
(memory, CPU, etc.)
Of course it depends on how many particles you want to be able to handle but there's really no reason to take such an OOP approach in this case
u
gotcha also, I didn't think SoA would be relevant on jvm here (I'm a beginner).. will it somehow use vectorized instructions internally? or is it simply about compactness & data locality (no pointer chasing) end game is compute on gpu, and yes that has to be SoA, I'm just building out the math so far
r
It's even more relevant on JVM
It's not about vectorization, it's about cache coherency
(which btw would be even worse when using
mutableStateOf
as you are introducing at least another pointer chase)
u
> It's even more relevant on JVM why? isnt pointer dereference the same in any langauge?
r
Yes but not exactly either
Since JVM does only heap allocations, an
Array<Particle>
always require 2 pointer jumps to get to the particle
Whereas in C/C++ you can have a
Particle[]
where all the data is stored directly in the array, so only one dereference
u
but particle has to be a struct, right? not a class
r
No, it works the same
In C++ the only difference is the default visibility of the members
Also note that while
Offset
is overall pretty efficient, in your case you might want to just store the positions as float arrays so you don't have to pack/unpack the data on every read/write
I optimized
Offset
the most it could be, but it's still a tiny bit of overhead
u
okay so on jvm SoA only makes sense where each component is primitive type?
r
Yes indeed
(
Offset
is technically a primitive btw)
u
gotcha thanks if I were to go on gpu for the compute, what would I use with compose? vulkan? but them, vulkan is android specific right? I can't expect vulkan to be there when doing compose multiplatform on desktop, right?
r
Vulkan is not Android specific, not it exists everywhere (including macOS with MoltenVK)
But Vulkan is a pain to use
Google just released a WebGPU library for Android, it would be a lot easier
And it's a Kotlin API
u
hmm does that need a browser?
r
Nope
It's related to web just like JavaScript is related to Java
😂 1
u
😄
I was talking to Mojo people the other day, probing them for mobile support; aaand I dont hold my breath, since they'd need to have backends for all of the mobile gpus, and thats a lot more than just 2 on desktop really
so I floated vulkan, but they complained about vulking being so behind the curve, apparently they added support for tensor-like cores only recently
but with video games.. people just write HLSL, right?
r
What's Mojo?
Yeah pretty much
u
oh..mojo is a new pythonic language that is like rust in perf but also does gpu chris lattner being behind it, creator of llvm, swift etc and they match/outperform cuda on nvidia; and the same source can compile for amd as well so no vendor lock in
r
Ah yeah that language, yeah I see what it is
Yeah if you want full performance you need to support each architecture separately, a pain on mobile
But you don't need this level of performance
So WebGPU/Vulkan is good enough for you
u
if I take the hit and write the compute shader in vulkan, will the molten vk thingy work on ios as well? or is it just for macs
btw how does vulkan support all the mobile gpus? do they flip the relationship and have gpu vendors write drivers for them? or is it just sheer will power 😄
r
Yeah MoltenVK works on iOS
Vulkan is like GL, it's the vendors that write drivers
(which of course leads to a lot of device specific bugs, etc.)
WebGPU btw is just a layer on top of Vulkan/Metal/DirectX
Instead of using MoltenVK you could write Metal code on iOS/macOS. You won't need much
And for shaders you can do what we did in Filament and write them in GLSL (or HLSL) and use spirv-cross to transpile them to MetalSL
u
gotcha, great info! just a final one, I've not touched vulkan/gpu compute on android, but cuda/mojo etc all have this async model of api; so I'd assume its the same with vulkan so -- on android/compose, naturally async = coroutines,... but I probably should not use coroutines, for perf overhead, right? or are coroutines fine? (update every frame)
r
Coroutines are fine, but I wouldn't fire one coroutine per particle
I would launch a batch of N-1 coroutines in the default dispatcher to use ~N-1 cores (but only if there are enough particles to feed that many cores)
u
hmm are you talking doing the particle compute on cpu?
r
If you are doing it on the CPU yes
Otherwise not sure why you need coroutines?
I mean, aside from the setup for GPU compute but…
u
I only meant as a programming model, since the loop is coroutines anyways, atleast thats what I have
Copy code
LaunchedEffect(Unit) {
    var lastFrameTimestamp = 0L
    while (isActive) {
        withFrameMillis { frameTimestamp ->
            val dt = if (lastFrameTimestamp == 0L) {
                0f
            } else {
                (frameTimestamp - lastFrameTimestamp) / 1000f
            }

            updateParticles(particles, dt)

            lastFrameTimestamp = frameTimestamp
        }
    }
}
and I'd have updateParticles run on gpu; or rather, to send the results to host via a coroutine..somehow or would you rather block on a cpu thread?
I might be completelly off, it's just what I'm thinking about the future gpu
r
If you do the rendering with WebGPU/Vulkan you don't even have to worry about that
u
nn I meant gpu just for compute, not rendering
r
you'd have to synchronize the data somehow then
you could just do a copy back to the CPU
and that would be your mutable state that triggers a redraw
u
yes I meant to copy, but obviously to keep the buffer pre allocated on host-- and then do your counter to trigger redraw, trick, right?
r
btw for your simulation you could use
withFrameNanos
to sync with vsync
yeah a counter would work
but tbh I don't know how useful it will be to do the simulation on the GPU if you do the rendering with Compose
your bottleneck will become Compose's rendering APIs
(or you will have so few particles that the overhead of dispatching work to the GPU won't be worth it)
u
do you mean that
Copy code
Canvas(Modifier.fillMaxSize()) {
    for (particle in particles) {
        val absolutePosition = Offset(
            x = particle.position.x * size.width,
            y = particle.position.y * size.height
        )
        drawCircle(
            color = androidx.compose.ui.graphics.Color.Magenta.copy(alpha = particle.alpha),
            center = absolutePosition,
            radius = 2f
        )
    }
}
compose will not manage say 1M of the
drawCircle
calls on each frame?
r
I'd be surprised if it did
u
yea im just making sure if this is what you meant by compose rendering apis
https://developer.android.com/jetpack/androidx/releases/webgp pls, is this the google's webgpu for android you mentioned?
r
Indeed
u
gotcha, thank you for all the insights!
The JNI is a prebuilt that I assume is built in the Chromium project