Skip to content

Commit ae2eeff

Browse files
committed
feat: Basic mean square velocity calculation
feat: Basic graph options in gui (clear/disable) fix: Multiple graphs not working at once
1 parent 888e597 commit ae2eeff

10 files changed

Lines changed: 232 additions & 43 deletions

File tree

particles-kool/src/commonMain/kotlin/me/dvyy/particles/Modules.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import de.fabmax.kool.KoolContext
44
import me.dvyy.particles.clustering.ParticleClustering
55
import me.dvyy.particles.compute.ConvertParticlesShader
66
import me.dvyy.particles.compute.ParticleBuffers
7+
import me.dvyy.particles.compute.data.MeanSquareVelocities
78
import me.dvyy.particles.compute.data.VelocitiesDataShader
89
import me.dvyy.particles.compute.partitioning.GPUSort
910
import me.dvyy.particles.compute.partitioning.OffsetsShader
11+
import me.dvyy.particles.compute.partitioning.ResetBuffers
1012
import me.dvyy.particles.compute.simulation.FieldsMultiPasses
1113
import me.dvyy.particles.compute.simulation.FieldsShader
1214
import me.dvyy.particles.compute.simulation.VerletHalfStepShader
@@ -43,10 +45,12 @@ fun shadersModule() = module {
4345
singleOf(::ConvertParticlesShader)
4446
singleOf(::OffsetsShader)
4547
singleOf(::GPUSort)
48+
singleOf(::ResetBuffers)
4649
singleOf(::FieldsShader)
4750
singleOf(::VerletHalfStepShader)
4851
singleOf(::FieldsMultiPasses)
4952
singleOf(::VelocitiesDataShader)
53+
singleOf(::MeanSquareVelocities)
5054
}
5155

5256
fun sceneModule() = module {

particles-kool/src/commonMain/kotlin/me/dvyy/particles/ParticlesScene.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import kotlinx.coroutines.flow.update
1010
import me.dvyy.particles.clustering.ParticleClustering
1111
import me.dvyy.particles.compute.ConvertParticlesShader
1212
import me.dvyy.particles.compute.ParticleBuffers
13+
import me.dvyy.particles.compute.data.MeanSquareVelocities
1314
import me.dvyy.particles.compute.data.VelocitiesDataShader
1415
import me.dvyy.particles.compute.partitioning.GPUSort
1516
import me.dvyy.particles.compute.partitioning.OffsetsShader
1617
import me.dvyy.particles.compute.partitioning.ReorderBuffersShader
18+
import me.dvyy.particles.compute.partitioning.ResetBuffers
1719
import me.dvyy.particles.compute.simulation.FieldsMultiPasses
1820
import me.dvyy.particles.config.AppSettings
1921
import me.dvyy.particles.config.ConfigRepository
@@ -27,10 +29,12 @@ class ParticlesScene(
2729
val configRepo: ConfigRepository,
2830
val clustering: ParticleClustering,
2931
val gpuSort: GPUSort,
32+
val resetBuffers: ResetBuffers,
3033
val cameraManager: CameraManager,
3134
val particlesMesh: ParticlesMesh,
3235
val offsetsShader: OffsetsShader,
3336
val velocitiesDataShader: VelocitiesDataShader,
37+
val meanSquareDataShader: MeanSquareVelocities,
3438
// val reorderBuffersShader: ReorderBuffersShader,
3539
val convertShader: ConvertParticlesShader,
3640
val fieldsShader: FieldsMultiPasses,
@@ -46,7 +50,7 @@ class ParticlesScene(
4650
val computePass = ComputePass("Particles Compute")
4751
//TODO placing this lower seems to set velocity to zero at the start. Is any kind of velocity read at certain times causing it to zero out?
4852
computePass.addTask(particlesMesh.colorShader, configRepo.numGroups) // Recolor particles
49-
gpuSort.addResetShader(computePass) // Reset keys and indices based on grid cell particle is in
53+
resetBuffers.addResetShader(computePass) // Reset keys and indices based on grid cell particle is in
5054
gpuSort.addSortingShader(configRepo.count, buffers = buffers, computePass = computePass) // Sort by grid cells
5155

5256
// Web has a limit of 8 storage buffers per shader stage, accommodate this by running multiple reorder shaders
@@ -75,6 +79,7 @@ class ParticlesScene(
7579

7680
// == DATA COLLECTION ==
7781
velocitiesDataShader.addTo(computePass)
82+
meanSquareDataShader.addTo(computePass)
7883

7984
addComputePass(computePass)
8085

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package me.dvyy.particles.compute.data
2+
3+
import de.fabmax.kool.math.Vec3i
4+
import de.fabmax.kool.modules.ksl.KslComputeShader
5+
import de.fabmax.kool.modules.ksl.lang.*
6+
import de.fabmax.kool.pipeline.ComputePass
7+
import me.dvyy.particles.compute.ParticleBuffers
8+
import me.dvyy.particles.compute.partitioning.WORK_GROUP_SIZE
9+
import me.dvyy.particles.helpers.Buffers
10+
11+
class MeanSquareVelocities(
12+
private val buffers: ParticleBuffers,
13+
) {
14+
private val meanSquareVelocities = KslComputeShader("MeanSquareVelocities") {
15+
computeStage(WORK_GROUP_SIZE) {
16+
val inputs = storage<KslFloat4>("velocities")
17+
val squareVelocities = storage<KslFloat1>("squareVelocities")
18+
19+
main {
20+
val id = int1Var(inGlobalInvocationId.x.toInt1())
21+
val velocity = float3Var(inputs[id].xyz)
22+
val length = float1Var(length(velocity))
23+
squareVelocities[id] = length * length
24+
}
25+
}
26+
}
27+
28+
private val reduce = KslComputeShader("MeanSquareVelocities_reduce") {
29+
computeStage(WORK_GROUP_SIZE) {
30+
val inputs = storage<KslFloat1>("inputs")
31+
val outputs = storage<KslFloat1>("outputs")
32+
val total = uniformInt1("total")
33+
34+
main {
35+
val id = int1Var(inGlobalInvocationId.x.toInt1())
36+
// average two neighbouring values, pass to output
37+
`if`(id lt total) {
38+
outputs[id] = inputs[id * 2.const] + inputs[(id * 2.const) + 1.const]
39+
}
40+
}
41+
}
42+
}
43+
private var inputs by reduce.storage("inputs")
44+
private var outputs by reduce.storage("outputs")
45+
private var total by reduce.uniform1i("total")
46+
47+
val inputBuffer = Buffers.floats(buffers.count)
48+
val outputBuffer = Buffers.floats(buffers.count)
49+
50+
private val roundedUp = 1 shl (32 - (buffers.count - 1).countLeadingZeroBits())
51+
private val iterations = roundedUp.countTrailingZeroBits()
52+
val readBack = if (iterations % 2 == 0) inputBuffer else outputBuffer
53+
54+
fun addTo(
55+
pass: ComputePass,
56+
) {
57+
pass.apply {
58+
addTask(meanSquareVelocities.apply {
59+
storage("velocities", buffers.velocitiesBuffer)
60+
storage("squareVelocities", inputBuffer)
61+
}, numGroups = buffers.configRepo.numGroups)
62+
repeat(iterations) { iteration ->
63+
addTask(
64+
reduce,
65+
numGroups = Vec3i(((roundedUp shr iteration) + WORK_GROUP_SIZE - 1) / WORK_GROUP_SIZE, 1, 1)
66+
).apply {
67+
pipeline.swapPipelineData("iteration $iteration")
68+
inputs = if (iteration % 2 == 0) inputBuffer else outputBuffer
69+
outputs = if (iteration % 2 == 0) outputBuffer else inputBuffer
70+
total = buffers.count shr iteration
71+
onBeforeDispatch {
72+
pipeline.swapPipelineData("iteration $iteration")
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}

particles-kool/src/commonMain/kotlin/me/dvyy/particles/compute/partitioning/GPUSort.kt

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import me.dvyy.particles.config.ConfigRepository
1010

1111
const val WORK_GROUP_SIZE = 64
1212

13-
class GPUSort(
13+
class ResetBuffers(
1414
val configRepo: ConfigRepository,
1515
val buffers: ParticleBuffers,
1616
) {
17+
1718
/**
1819
* Given particles positions and grid info, resets keys and indices buffers such that:
1920
* - Keys point to the grid cell of particle at index
@@ -46,7 +47,7 @@ class GPUSort(
4647
}
4748

4849
fun addResetShader(
49-
computePass: ComputePass
50+
computePass: ComputePass,
5051
) {
5152
val reset = resetBuffersShader.apply {
5253
uniform1f("gridSize", configRepo.gridSize)
@@ -57,7 +58,9 @@ class GPUSort(
5758
}
5859
computePass.addTask(reset, numGroups = configRepo.numGroups)
5960
}
61+
}
6062

63+
class GPUSort {
6164
val sorter = KslComputeShader("GPUSort") {
6265
computeStage(WORK_GROUP_SIZE) {
6366
val numValues = uniformInt1("numValues")
@@ -67,11 +70,6 @@ class GPUSort(
6770

6871
val cellIdKeys = storage<KslInt1>("keys")
6972
val indices = storage<KslInt1>("indices")
70-
// val positions = storage<KslFloat4>("currPositions")
71-
// val velocities = storage<KslFloat4>("currVelocities")
72-
// val forces = storage<KslFloat4>("prevForces")
73-
// val types = storage<KslInt1>("types")
74-
// val clusters = storage<KslInt1>("clusters")
7573

7674
main {
7775
val i = int1Var(inGlobalInvocationId.x.toInt1())
@@ -95,19 +93,15 @@ class GPUSort(
9593
buffer[indexLow] = currHigh
9694
buffer[indexHigh] = currLow
9795
}
96+
9897
fun swapInts(buffer: KslPrimitiveStorage<KslPrimitiveStorageType<KslInt1>>) {
9998
val currLow = int1Var(buffer[indexLow])
10099
val currHigh = int1Var(buffer[indexHigh])
101100
buffer[indexLow] = currHigh
102101
buffer[indexHigh] = currLow
103102
}
104-
// swapFloats(positions)
105-
// swapFloats(velocities)
106-
// swapFloats(forces)
107-
// swapInts(types)
108103
swapInts(cellIdKeys)
109104
swapInts(indices)
110-
// swapInts(clusters)
111105
}
112106
}
113107
}
@@ -120,26 +114,15 @@ class GPUSort(
120114
var groupWidthU by sorter.uniform1i("groupWidth")
121115
var groupHeightU by sorter.uniform1i("groupHeight")
122116
var stepIndexU by sorter.uniform1i("stepIndex")
123-
// var positions1 by sorter.storage("currPositions")
124-
// var velocities1 by sorter.storage("currVelocities")
125-
// var prevForces by sorter.storage("prevForces")
126-
// var types by sorter.storage("types")
127-
// var clusters by sorter.storage("clusters")
128117

129118
fun addSortingShader(
130119
count: Int,
131120
buffers: ParticleBuffers,
132121
computePass: ComputePass,
133122
) {
134-
135123
numValues = count
136124
keys = buffers.particleGridCellKeys
137125
indices = buffers.sortIndices
138-
// positions1 = buffers.positionBuffer
139-
// velocities1 = buffers.velocitiesBuffer
140-
// prevForces = buffers.forcesBuffer
141-
// types = buffers.particleTypesBuffer
142-
// clusters = buffers.clustersBuffer
143126

144127
computePass.apply {
145128
val numPairs = count.takeHighestOneBit() * 2

particles-kool/src/commonMain/kotlin/me/dvyy/particles/ui/nodes/GraphNode.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlinx.coroutines.Deferred
1919
import me.dvyy.particles.compute.forces.ForceWithParameters
2020
import me.dvyy.particles.compute.forces.PairwiseForce
2121
import me.dvyy.particles.compute.partitioning.WORK_GROUP_SIZE
22+
import kotlin.math.min
2223

2324
class GraphNode : UiRenderer<UiNode> {
2425
private val graphMesh: Mesh
@@ -92,17 +93,37 @@ class GraphNode : UiRenderer<UiNode> {
9293
fun render(valuesX: FloatArray, valuesY: FloatArray) {
9394
this.valuesX = valuesX
9495
this.valuesY = valuesY
96+
updateViewPort()
97+
updated = true
98+
}
99+
100+
fun updateViewPort() {
95101
viewport.set(
96102
valuesX.min(),
97-
valuesY.min(),
103+
min(0f, valuesY.min()),
98104
valuesX.max(),
99105
valuesY.max(),
100106
)
107+
}
108+
109+
fun pushNewValueRight(value: Float) {
110+
for (i in 0..<valuesY.lastIndex) {
111+
valuesY[i] = valuesY[i + 1]
112+
}
113+
valuesY[valuesY.lastIndex] = value
114+
updateViewPort()
115+
updated = true
116+
}
117+
118+
fun clearYAxis(value: Float = 0f) {
119+
for(i in valuesY.indices) {
120+
valuesY[i] = value
121+
}
101122
updated = true
102123
}
103124

104125
init {
105-
graphMesh = Mesh(graphGeom, name = "DebugOverlay/DeltaTGraph")
126+
graphMesh = Mesh(graphGeom, name = "GraphNode")
106127
graphMesh.geometry.usage = Usage.DYNAMIC
107128
graphMesh.shader = Ui2Shader()
108129
}
@@ -116,11 +137,10 @@ class GraphNode : UiRenderer<UiNode> {
116137
// })
117138
// }
118139
// }
119-
node.surface.getMeshLayer(node.modifier.zLayer - 1).addCustomLayer("dt-graph") { graphMesh }
140+
node.surface.getMeshLayer(node.modifier.zLayer - 1).addCustomLayer("dt-graph-${node}") { graphMesh }
120141
if (node.clipBoundsPx == clipBounds && !updated) return
121142
clipBounds.set(node.clipBoundsPx)
122143
updated = false
123-
println("Ran render ui!")
124144
node.apply {
125145
width = widthPx.toInt()
126146
height = heightPx.toInt()

particles-kool/src/commonMain/kotlin/me/dvyy/particles/ui/viewmodels/ParticlesViewModel.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import de.fabmax.kool.modules.ui2.MutableStateValue
55
import de.fabmax.kool.pipeline.MipMapping
66
import de.fabmax.kool.pipeline.SamplerSettings
77
import de.fabmax.kool.pipeline.Texture2d
8+
import de.fabmax.kool.util.Float32Buffer
89
import de.fabmax.kool.util.Int32Buffer
910
import de.fabmax.kool.util.launchOnMainThread
1011
import io.github.vinceglb.filekit.FileKit
@@ -21,6 +22,7 @@ import kotlinx.io.files.Path
2122
import kotlinx.serialization.KSerializer
2223
import me.dvyy.particles.SceneManager
2324
import me.dvyy.particles.compute.ParticleBuffers
25+
import me.dvyy.particles.compute.data.MeanSquareVelocities
2426
import me.dvyy.particles.compute.data.VelocitiesDataShader
2527
import me.dvyy.particles.config.AppSettings
2628
import me.dvyy.particles.config.ConfigRepository
@@ -44,6 +46,7 @@ class ParticlesViewModel(
4446
private val paramOverrides: ParameterOverrides,
4547
private val scope: CoroutineScope,
4648
private val velocitiesData: VelocitiesDataShader,
49+
private val meanSquareData: MeanSquareVelocities,
4750
) {
4851
val passesPerFrame = MutableStateFlow(1)
4952
val uiState: MutableStateValue<List<UiConfigurable>> = configRepo.config.map { it.simulation }
@@ -72,6 +75,12 @@ class ParticlesViewModel(
7275
val velocitiesHistogram = GraphNode().apply {
7376
style = GraphStyle.Bar(width = 5.0)
7477
}
78+
val msqvOverTime = GraphNode().apply {
79+
render(FloatArray(1024) { it.toFloat() }, FloatArray(1024) { 0f })
80+
style = GraphStyle.Bar(width = 5.0)
81+
}
82+
83+
val meanSquareVelocity = MutableStateFlow(0f)
7584

7685
suspend fun updateVelocityHistogram() {
7786
val buckets = Int32Buffer(velocitiesData.numBuckets)
@@ -83,6 +92,14 @@ class ParticlesViewModel(
8392
)
8493
}
8594

95+
suspend fun readbackMeanSquareVelocity() {
96+
val result = Float32Buffer(buffers.count)
97+
meanSquareData.readBack.downloadData(result)
98+
val msqV = result[0] / buffers.count
99+
msqvOverTime.pushNewValueRight(msqV)
100+
meanSquareVelocity.update { msqV }
101+
}
102+
86103
fun updateState(simulation: Simulation.() -> Simulation) = scope.launch {
87104
val config = configRepo.config.value
88105
val newSimulation = simulation(config.simulation)

particles-kool/src/commonMain/kotlin/me/dvyy/particles/ui/windows/SimulationStatisticsWindow.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package me.dvyy.particles.ui.windows
22

33
import de.fabmax.kool.modules.ui2.*
44
import de.fabmax.kool.toString
5-
import de.fabmax.kool.util.launchOnMainThread
65
import kotlinx.coroutines.CoroutineScope
76
import kotlinx.coroutines.flow.update
87
import me.dvyy.particles.clustering.ParticleClustering
@@ -33,7 +32,7 @@ class SimulationStatisticsWindow(
3332
val simsPs = mutableStateOf(0.0)
3433
val fps = mutableStateOf(0.0)
3534
val clusterOptions = settings.clusterOptions.asMutableState(scope)
36-
35+
val meanSquareVelocity = viewModel.meanSquareVelocity.asMutableState(scope)
3736

3837
override fun UiScope.windowContent() = ScrollArea(
3938
withHorizontalScrollbar = false,
@@ -51,16 +50,16 @@ class SimulationStatisticsWindow(
5150
}
5251

5352
Category("Graphs") {
54-
Subcategory("Velocity Histogram") {
55-
var counter by remember(0)
56-
// Update velocity histogram every 25 frames
57-
surface.onEachFrame {
58-
counter++
59-
if (counter % 50 == 0) launchOnMainThread {
60-
viewModel.updateVelocityHistogram()
61-
}
53+
Subcategory("Mean Square Velocity") {
54+
ParameterGraph(viewModel.msqvOverTime, redraw = { viewModel.readbackMeanSquareVelocity() }) {
55+
Text("Mean Square Velocity: " + meanSquareVelocity.use().toString(2)) { }
6256
}
63-
ParameterGraph(viewModel.velocitiesHistogram)
57+
}
58+
Subcategory("Velocity Histogram") {
59+
ParameterGraph(
60+
viewModel.velocitiesHistogram,
61+
redrawFreq = 50,
62+
redraw = { viewModel.updateVelocityHistogram() })
6463
}
6564
Subcategory("Cluster size distribution") {
6665
val opts by clusterOptions

0 commit comments

Comments
 (0)