Skip to content

FieldStatsSmokeTest.kt

src/jvmMain/kotlin/afield/examples/four/FieldStatsSmokeTest.kt · 153 lines

package afield.examples.four

import afield.document.graph.Box
import afield.graphics.vulkan.Renderer
import afield.graphics.shader.Introspection
import afield.graphics.shader.ShaderKind
import afield.graphics.vulkan.util.FIELD_STATS_PARTIAL_BUFFER_NAME
import afield.graphics.vulkan.util.FIELD_STATS_RESULT_BUFFER_NAME
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.ShaderFieldStatsBinding
import afield.graphics.vulkan.util.GpuProbe
import afield.graphics.vulkan.util.Window
import afield.graphics.shader.fieldStatsHere
import afield.graphics.shader.preprocess
import afield.graphics.shader.useFieldStats
import kotlin.math.abs

class FieldStatsSmokeTest

companion object

fun main(args: Array<String>)

body — 133 lines, from line 20
        @JvmStatic
        fun main(args: Array<String>) {
            Options.enableVR = false
            val window = Window("FieldStats Smoke Test")
            val renderer = Renderer(window)
            val bench = GpuProbe(renderer, "FieldStatsSmokeTest", pump = {
                window.pollEvents(); renderer.render()
            })
            try {
                val box = Box("field-stats-smoke").apply { name = "field-stats-smoke" }
                val stats = box.fieldStatsHere(
                    channels = 2,
                    sketchSize = 64,
                    maxPartials = 2048,
                    stateSlots = 2
                )

                val shader = preprocess(
                    box,
                    """
                        #version 450
                        layout(local_size_x = 64) in;
                        {% property "fieldStats" %}

                        void main() {
                            float smoothedMedian =
                                fieldStatsSmoothedQuantile(0u, 0u, 0.5, 2.0);
                            float smoothedMean =
                                fieldStatsSmoothedMean(1u, 1u, 2.0);
                            fieldStatsRecord(0u, float(gl_GlobalInvocationID.x));
                            fieldStatsRecord(1u, float(fieldStatsResult.generation) * 100.0);
                            if (smoothedMedian < -1e30 || smoothedMean < -1e30) {
                                fieldStatsResult.generation = 0u;
                            }
                        }
                    """.trimIndent(),
                    ShaderKind.COMPUTE_SHADER
                )

                val stage = bench.stage("producer", shader) {
                    fieldStats = ShaderFieldStatsBinding(stats, producer = true)
                    ssboResolver = { requested ->
                        when (requested.name) {
                            FIELD_STATS_PARTIAL_BUFFER_NAME -> stats.partialBufferParameters(renderer)
                            FIELD_STATS_RESULT_BUFFER_NAME -> stats.resultBufferParameters(renderer)
                            else -> null
                        }
                    }
                }.dispatch(groups = 16) {}

                val consumerBox = Box("field-stats-consumer").apply { name = "field-stats-consumer" }
                consumerBox.useFieldStats(stats)
                val consumerShader = preprocess(
                    consumerBox,
                    """
                        #version 450
                        layout(local_size_x = 1) in;
                        {% property "fieldStats" %}

                        void main() {
                            float median = fieldStatsQuantile(0u, 0.5);
                            float smoothedMedian = fieldStatsState(0u);
                            if (median < -1e30 || smoothedMedian < -1e30) {
                                fieldStatsResult.generation = 0u;
                            }
                        }
                    """.trimIndent(),
                    ShaderKind.COMPUTE_SHADER
                )
                require(Introspection(consumerShader, ShaderKind.COMPUTE_SHADER).valid) {
                    "FieldStats consumer shader did not compile"
                }

                run {
                    // Six producer generations are the fixture: channel 1 records generation * 100.
                    // This is a sampled sequence, not an arbitrary wait for a shader to become ready.
                    require(stage.valid) { "FieldStats producer shader did not compile" }
                    bench.pumpFrames(6)
                    bench.idle()
                    check(bench.verdict() == 0) { "FieldStats producer shader failed" }

                    val scalar = stats.scalar(0)
                    val deadline = System.nanoTime() + 2_000_000_000L
                    while (scalar.latest() == null && System.nanoTime() < deadline) {
                        Thread.sleep(5)
                    }
                    val snapshot = requireNotNull(scalar.latest()) {
                        "FieldStats asynchronous snapshot was not published"
                    }
                    require(snapshot.calls == 1024L) { "Expected 1024 calls, got ${snapshot.calls}" }
                    require(snapshot.finiteCount == 1024L) { "Expected 1024 finite values, got ${snapshot.finiteCount}" }
                    require(abs(snapshot.min - 0.0) < 0.01) { "Expected min 0, got ${snapshot.min}" }
                    require(abs(snapshot.max - 1023.0) < 0.01) { "Expected max 1023, got ${snapshot.max}" }
                    require(abs(snapshot.mean - 511.5) < 0.1) { "Expected mean 511.5, got ${snapshot.mean}" }
                    require(snapshot.quantile(0.5) in 400.0..624.0) {
                        "Expected approximate median near the center, got ${snapshot.quantile(0.5)}"
                    }
                    val state = requireNotNull(stats.state(0).latest()) {
                        "FieldStats asynchronous state snapshot was not published"
                    }
                    require(state.initialized) { "Expected smoothing state to be initialized" }
                    require(state.mode == 2) { "Expected quantile smoothing mode, got ${state.mode}" }
                    require(state.channel == 0) { "Expected smoothing channel 0, got ${state.channel}" }
                    require(state.value in 400.0..624.0) {
                        "Expected smoothed median near the center, got ${state.value}"
                    }
                    val changingSnapshot = requireNotNull(stats.scalar(1).latest()) {
                        "FieldStats changing-channel snapshot was not published"
                    }
                    val changingState = requireNotNull(stats.state(1).latest()) {
                        "FieldStats mean smoothing state was not published"
                    }
                    require(abs(changingSnapshot.mean - 500.0) < 0.1) {
                        "Expected current changing mean 500, got ${changingSnapshot.mean}"
                    }
                    require(changingState.value in 250.0..400.0) {
                        "Expected half-life smoothing to lag current mean, got ${changingState.value}"
                    }

                    println(
                        "FieldStatsSmokeTest ok: generation=${snapshot.generation} count=${snapshot.finiteCount} " +
                            "mean=${snapshot.mean} q50=${snapshot.quantile(0.5)} smoothedQ50=${state.value} " +
                            "changingMean=${changingSnapshot.mean} smoothedMean=${changingState.value}"
                    )
                }
            } finally {
                bench.close()
                renderer.cleanup()
                window.cleanup()
            }
        }
    }
}