Skip to content

PathTraceTemporalGpuSmoke.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTemporalGpuSmoke.kt · 339 lines

package afield.graphics.scene.fabricated

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.util.Camera
import afield.graphics.vulkan.util.KeyCamera
import afield.math.linalg.Vector3f
import afield.math.linalg.Vector4f
import kotlinx.coroutines.delay
import kotlin.math.abs
import kotlin.math.tan
import kotlin.math.exp

object PathTraceTemporalGpuSmoke

Real GPU history/visibility checks, including a measured moving-camera noise comparison.

object PathTraceTemporalGpuSmoke {
    private suspend fun step(tracer: PathTracingRenderer) {
        tracer.paused=false
        val deadline=System.nanoTime()+30_000_000_000L
        while (!tracer.paused) { check(System.nanoTime()<deadline) { "Temporal dispatch timed out" }; delay(1) }
    }
    private fun latch(tracer: PathTracingRenderer) {
        tracer.paused=true
        tracer.materialModule.current.after["temporal-test-step"] = Target { _, _ -> tracer.paused=true }
        tracer.install()
    }
    private fun centre(p: FloatArray, stride: Int=1): List<Double> =
        (8 until 24).flatMap { y -> (8 until 40).map { x -> p[(y*48+x)*stride].toDouble() } }
    private fun variance(p: FloatArray): Double {
        val v=centre(p,4);val mean=v.average();return v.sumOf { (it-mean)*(it-mean) }/v.size
    }

suspend fun runFog(renderer: Renderer)

body — 80 lines, from line 31
    suspend fun runFog(renderer: Renderer) {
        val backends=listOf(PathTracingRenderer.Backend.COMPUTE_BVH) +
            if (renderer.device.supportsRayQuery) listOf(PathTracingRenderer.Backend.RAY_QUERY) else emptyList()
        for (backend in backends) {
            val camera=Camera(renderer).also { it.at.set(0f,0f,3f);it.target.set(0f,0f,0f);it.p.fov=0.7 }
            val scene=PathTraceScene().apply {
                val m=material(PathTraceMaterial.emissive(2f,0.25f,0.5f))
                quad(Vector3f(-200f,-200f,0f),Vector3f(200f,-200f,0f),Vector3f(200f,200f,0f),Vector3f(-200f,200f,0f),m)
            }
            PathTracingRenderer(renderer,scene,PathTraceEnvironment.constant(2f,0.5f,0.25f),9,9,camera,backend).use { t ->
                // Isolate the analytic fog curve from the emitter's dielectric reflection of the environment.
                t.pixelJitter=0f;t.maxBounces=1;t.environmentIntensity=0f;latch(t)
                val base=floatArrayOf(2f,0.25f,0.5f)
                fun expect(rgb: FloatArray, alpha: Float=1f) {
                    val pixels=t.readLinear()
                    check(pixels.all { it.isFinite() })
                    val actual=pixels.copyOfRange(160,164)
                    check((0..2).all { abs(actual[it]-rgb[it])<2e-4f } && abs(actual[3]-alpha)<1e-6f) {
                        "Fog RGB/alpha: ${actual.toList()}, expected ${rgb.toList()}, $alpha"
                    }
                }
                fun fogged(depth: Float): FloatArray {
                    val transmission=exp(-t.fogDensity*(depth-t.fogStartDistance).coerceAtLeast(0f))
                    val c=t.fogColor;val color=floatArrayOf(c.x,c.y,c.z)
                    return FloatArray(3) { base[it]*transmission+color[it]*(1f-transmission) }
                }
                step(t);expect(base)
                val fog=Vector3f(0.1f,0.3f,0.7f);t.fogColor=fog;fog.x=10f
                check(t.fogColor.x==0.1f);t.fogColor.x=20f;check(t.fogColor.x==0.1f)
                t.fogDensity=0.4f;step(t);expect(fogged(3f));check(t.samples==1)
                repeat(4) { step(t) };expect(fogged(3f));check(t.samples==5)
                t.fogStartDistance=3f;step(t);expect(base);check(t.samples==1)
                t.fogStartDistance=1f;step(t);expect(fogged(3f))
                t.fogDensity=0f;step(t);expect(base)
                // Raw history must survive dense fog, then be fogged at the new camera distance.
                t.temporalReuse=true;t.temporalMotionSensitivity=0f;t.temporalRoughnessSensitivity=0f
                t.fogDensity=1f;t.fogStartDistance=0f;camera.at.z=100f;t.samplesPerFrame=16;step(t)
                expect(fogged(100f));camera.at.z=1f;t.samplesPerFrame=1;step(t)
                check(t.readHistorySampleCounts()[40]>15f) { "Fog test did not retain history" }
                expect(fogged(1f));repeat(4) { step(t) };expect(fogged(1f))
                t.fogColor=Vector3f(0.4f,0.1f,0.2f);step(t);expect(fogged(1f))
                check(t.readHistorySampleCounts().all { it==1f }) { "Fog colour edit retained stale history" }
                camera.lookAlong(Vector3f(0f,0f,3f),Vector3f(0f,0f,1f));t.environmentIntensity=1f;step(t)
                expect(floatArrayOf(0.4f,0.1f,0.2f))
                t.fogBackground=false;step(t);expect(floatArrayOf(2f,0.5f,0.25f))
                t.showEnvironment=false;t.background=Vector4f(0.2f,0.4f,0.8f,0.25f);step(t)
                expect(floatArrayOf(0.05f,0.1f,0.2f),0.25f)
                t.fogBackground=true;step(t);expect(floatArrayOf(0.1f,0.025f,0.05f),0.25f)
                t.background=Vector4f(0f);step(t);expect(FloatArray(3),0f)
                t.fogDensity=0f;step(t);expect(FloatArray(3),0f)
                for (bad in listOf(-1f,Float.NaN,Float.POSITIVE_INFINITY)) {
                    check(runCatching { t.fogDensity=bad }.isFailure)
                    check(runCatching { t.fogStartDistance=bad }.isFailure)
                    check(runCatching { t.fogColor=Vector3f(bad,0f,0f) }.isFailure)
                }
                println("PASS GPU $backend depth fog: analytic distance/start, disabled equivalence, dense-to-clear reprojection, stationary accumulation, resets, HDR background and premultiplied alpha")
            }
            camera.lookAt(Vector3f(0f,0f,3f),Vector3f())
            val edgeScene=PathTraceScene().apply {
                val m=material(PathTraceMaterial.diffuse(1f,1f,1f))
                triangle(Vector3f(-0.8f,-0.8f,0f),Vector3f(0.8f,-0.8f,0f),Vector3f(0f,0.8f,0f),m)
            }
            PathTracingRenderer(renderer,edgeScene,PathTraceEnvironment.constant(),9,9,camera,backend).use { t ->
                t.samplesPerFrame=64;t.maxBounces=1;t.fogDensity=100f;t.fogColor=Vector3f(0.2f,0.4f,0.6f)
                t.showEnvironment=false;t.background=Vector4f(0f);t.focusDistance=6f;latch(t)
                for (reuse in listOf(false,true)) for (aperture in listOf(0f,0.25f)) {
                    t.temporalReuse=reuse;t.apertureRadius=aperture;repeat(2) { step(t) }
                    val pixels=t.readLinear()
                    check(pixels.indices.step(4).any { pixels[it+3] in 0.01f..0.99f }) { "Fog coverage test had no antialiased edge" }
                    for (i in pixels.indices step 4) {
                        val a=pixels[i+3]
                        check(abs(pixels[i]-0.2f*a)<1e-5f && abs(pixels[i+1]-0.4f*a)<1e-5f && abs(pixels[i+2]-0.6f*a)<1e-5f) {
                            "Fog broke premultiplied silhouette coverage: reuse=$reuse, aperture=$aperture"
                        }
                    }
                }
                println("PASS GPU $backend depth fog: jittered and defocused transparent silhouettes, history on/off")
            }
        }
    }

suspend fun run(renderer: Renderer, verifyKeyboard: Boolean = true)

body — 228 lines, from line 111
    suspend fun run(renderer: Renderer, verifyKeyboard: Boolean = true) {
        val backends=listOf(PathTracingRenderer.Backend.COMPUTE_BVH) +
            if (renderer.device.supportsRayQuery) listOf(PathTracingRenderer.Backend.RAY_QUERY) else emptyList()
        for (backend in backends) {
            // The GPU audit runs on a shared desktop. Keep the controller active, but isolate its
            // idle/decay check from real keystrokes sent to the test window by another user.
            if (verifyKeyboard) {
            // Captured from a live keyboard camera that alternated by two ULPs forever when idle.
            val vertical=Camera(renderer).also {
                it.at.set(0.9632708f,8.943225f,-1.362556f);it.target.set(0f,0.65f,0f)
                it.up.set(0.4941822f,-0.19653562f,-0.84685177f)
            }
            val keys=KeyCamera(renderer,vertical,"temporal-idle-camera-check",acceptPhysicalKeys=false)
            try {
                PathTracingRenderer(renderer,PathTraceScene().apply {
                    val m=material(PathTraceMaterial.diffuse(1f,1f,1f))
                    quad(Vector3f(-20f,0f,-20f),Vector3f(-20f,0f,20f),Vector3f(20f,0f,20f),Vector3f(20f,0f,-20f),m)
                },PathTraceEnvironment.constant(),48,32,vertical,backend).use { t ->
                    t.samplesPerFrame=1;latch(t)
                    val up=Vector3f(vertical.up)
                    for (reuse in listOf(false,true)) {
                        t.temporalReuse=reuse;t.reset()
                        repeat(32) { step(t) }
                        check(t.samples==32 && vertical.up==up) { "Idle keyboard camera kept resetting convergence: samples=${t.samples}, up=${vertical.up}, at=${vertical.at}, actions=${keys.actions.keys}" }
                    }
                    // Exercise one real controller action and its release, then require convergence again.
                    keys.decay=0.0
                    keys.actions["test-pitch"] = 1.0 to { _: Double -> vertical.pitch(0.01) }
                    delay(100)
                    while (keys.actions.isNotEmpty()) delay(1)
                    step(t);val start=t.samples
                    repeat(32) { step(t) }
                    check(t.samples==start+32) { "Released keyboard camera failed to settle" }
                    println("PASS GPU $backend idle keyboard camera: live near-vertical pose, reuse on/off and released rotation converge")
                }
            } finally { renderer.window.onPoll.remove(keys.name) }
            }
            val camera=Camera(renderer).also { it.at.set(0f,0f,3f);it.target.set(0f,0f,0f);it.p.fov=0.7 }
            fun plane(material: PathTraceMaterial)=PathTraceScene().apply {
                val m=material(material)
                quad(Vector3f(-20f,-20f,0f),Vector3f(20f,-20f,0f),Vector3f(20f,20f,0f),Vector3f(-20f,20f,0f),m)
            }
            PathTracingRenderer(renderer,plane(PathTraceMaterial.diffuse(1f,1f,1f)),PathTraceEnvironment.constant(),48,32,camera,backend).use { t ->
                t.temporalReuse=true;t.maxBounces=2;t.samplesPerFrame=16;latch(t)
                repeat(32) { step(t) }
                check(t.readHistorySampleCounts().all { it==512f })
                t.samplesPerFrame=1;camera.moveWorld(0.02,0.0,0.0);step(t)
                check(t.samples==1)
                val reused=t.readLinear();val counts=t.readHistorySampleCounts()
                check(centre(counts).average()>10) { "Camera motion discarded all useful history: ${centre(counts).average()}" }
                check(counts.all { it in 1f..17.001f }) { "Motion history exceeded the cap" }
                step(t)
                val stationary=t.readHistorySampleCounts()
                check(stationary.indices.all { abs(stationary[it]-counts[it]-1f)<1e-4 }) { "Stationary history didn't resume exact accumulation" }
                t.temporalReuse=false;step(t)
                val cold=t.readLinear()
                val ratio=variance(reused)/variance(cold)
                check(ratio<0.45) { "Moving-camera variance did not improve: ratio=$ratio" }
                check(abs(centre(reused,4).average()-centre(cold,4).average())<0.06) { "Temporal reuse biased the white furnace excessively" }
                t.temporalReuse=true;step(t)
                check(t.readHistorySampleCounts().all { it==1f })
                t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.yaw(0.005);step(t)
                check(centre(t.readHistorySampleCounts()).average()>8) { "Rotation failed to reproject" }
                camera.p.fov=0.71;step(t)
                check(centre(t.readHistorySampleCounts()).average()>8) { "FOV change failed to reproject" }
                repeat(12) {
                    camera.moveWorld(0.01,0.0,0.0);step(t)
                    check(t.samples==1 && centre(t.readHistorySampleCounts()).average()>7) {
                        "Continuous motion stopped retaining useful history"
                    }
                }
                t.environmentIntensity=0.5f;step(t)
                check(t.readHistorySampleCounts().all { it==1f }) { "Lighting edit retained stale history" }
                t.reset();step(t);check(t.readHistorySampleCounts().all { it==1f })
                t.pixelJitter=0f;step(t);check(t.readHistorySampleCounts().all { it==1f })
                t.apertureRadius=0.3f;t.focusDistance=6f;camera.lookAt(Vector3f(0f,0f,3f),Vector3f());step(t)
                t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(t.readHistorySampleCounts().all { it==1f }) { "Strong DOF blur incorrectly reused a point correspondence" }
                t.focusDistance=3f;step(t);t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(centre(t.readHistorySampleCounts()).average()>8) { "In-focus DOF surface lost valid history" }
                t.focusDistance=null;step(t);repeat(3) { step(t) }
                camera.target.z=-3f;step(t)
                check(t.readHistorySampleCounts().all { it==1f }) { "Focus-only edit retained the old blur" }
                t.apertureRadius=0f;t.temporalMaxSamples=4;step(t);t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(t.readHistorySampleCounts().all { it in 1f..5.001f })
                for (bad in listOf(0,257)) check(runCatching { t.temporalMaxSamples=bad }.isFailure)
                println("PASS GPU $backend temporal: motion variance ratio=$ratio, mean retained weight=${centre(counts).average()-1}, " +
                    "continuous motion/rotation/FOV, stationary growth, reset/toggle/lens invalidation, DOF rejection and bounded counts")
            }
            camera.lookAt(Vector3f(0f,0f,3f),Vector3f());camera.p.fov=0.7
            PathTracingRenderer(renderer,plane(PathTraceMaterial.diffuse(1f,1f,1f)),PathTraceEnvironment.constant(),48,32,camera,backend).use { t ->
                t.temporalReuse=true;t.pixelJitter=0f;latch(t)
                for (sensitivity in listOf(0f,0.5f,1f)) {
                    t.temporalMotionSensitivity=sensitivity;t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                    camera.moveWorld(if (camera.at.x<0.4f) 0.8 else -0.8,0.0,0.0);step(t)
                    val weights=t.readHistorySampleCounts()
                    // Only compare pixels inside both views; the larger move exposes the right edge.
                    val count=(12 until 20).flatMap { y -> (16 until 32).map { x -> weights[y*48+x].toDouble() } }.average()
                    check(abs(count-(1+16*(1-sensitivity)))<0.02) {
                        "Motion sensitivity $sensitivity retained unexpected weight: $count; " +
                            (12 until 20).flatMap { y -> (16 until 32).mapNotNull { x ->
                                weights[y*48+x].takeIf { abs(it-(1+16*(1-sensitivity)))>0.02 }
                                    ?.let { "($x,$y)=$it" }
                            } }.joinToString()
                    }
                }
                t.temporalMotionSensitivity=0f;step(t)
                check(t.readHistorySampleCounts().all { it==1f }) { "Motion policy edit failed to reset history" }
                t.apertureRadius=0.1f;t.focusDistance=6f;t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(t.readHistorySampleCounts().all { it==1f })
                t.temporalMaxBlurRadius=2f;step(t)
                check(t.readHistorySampleCounts().all { it==1f }) { "Blur policy edit failed to reset history" }
                t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(centre(t.readHistorySampleCounts()).average()>8) { "Relaxed defocus policy failed to retain history" }
                for (bad in listOf(-0.1f,1.1f,Float.NaN,Float.POSITIVE_INFINITY)) {
                    check(runCatching { t.temporalMotionSensitivity=bad }.isFailure)
                    check(runCatching { t.temporalRoughnessSensitivity=bad }.isFailure)
                }
                for (bad in listOf(-1f,Float.NaN,Float.POSITIVE_INFINITY))
                    check(runCatching { t.temporalMaxBlurRadius=bad }.isFailure)
                println("PASS GPU $backend history policy: continuous motion weights, relaxed defocus, reset and input validation")
            }
            camera.lookAt(Vector3f(0f,0f,3f),Vector3f())
            PathTracingRenderer(renderer,plane(PathTraceMaterial.metal(1f,1f,1f,0f)),PathTraceEnvironment.constant(),48,32,camera,backend).use { t ->
                t.temporalReuse=true;latch(t);t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(t.readHistorySampleCounts().all { it==1f }) { "Default policy reused a mirror" }
                t.temporalRoughnessSensitivity=0f;step(t)
                check(t.readHistorySampleCounts().all { it==1f })
                t.samplesPerFrame=16;repeat(4) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                check(centre(t.readHistorySampleCounts()).average()>15) { "Artistic policy failed to retain mirror history" }
                t.temporalRoughnessSensitivity=1f;step(t)
                check(t.readHistorySampleCounts().all { it==1f })
                println("PASS GPU $backend history policy: optional full-weight mirror reuse and conservative restoration")
            }
            // Dense tessellation must not erase history merely because jitter crosses triangle edges.
            camera.lookAt(Vector3f(0f,0f,3f),Vector3f())
            val grid=PathTraceScene().apply {
                val m=material(PathTraceMaterial.diffuse(1f,1f,1f))
                for (y in -12 until 12) for (x in -12 until 12) {
                    val px=x*0.2f;val py=y*0.2f
                    quad(Vector3f(px,py,0f),Vector3f(px+0.2f,py,0f),Vector3f(px+0.2f,py+0.2f,0f),Vector3f(px,py+0.2f,0f),m)
                }
            }
            PathTracingRenderer(renderer,grid,PathTraceEnvironment.constant(),48,32,camera,backend).use { t ->
                t.temporalReuse=true;t.samplesPerFrame=16;latch(t);repeat(32) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.02,0.0,0.0);step(t)
                val retained=centre(t.readHistorySampleCounts()).count { it>8.0 }.toDouble()/512.0
                check(retained>0.95) { "Triangle seams discarded valid surface history: retained fraction=$retained" }
                println("PASS GPU $backend tessellated surface: retained fraction=$retained after jittered convergence")
                camera.lookAt(Vector3f(0f,-4f,1f),Vector3f())
                t.reset();t.samplesPerFrame=16;repeat(32) { step(t) };t.samplesPerFrame=1
                camera.moveWorld(0.01,0.0,0.0);step(t)
                val grazing=centre(t.readHistorySampleCounts()).count { it>8.0 }.toDouble()/512.0
                check(grazing>0.5) { "Grazing surface discarded its stretched footprint: retained fraction=$grazing" }
                println("PASS GPU $backend grazing tessellated surface: retained fraction=$grazing")
            }
            for ((label,material) in listOf("polished" to PathTraceMaterial.metal(0.8f,0.8f,0.8f,0.05f),
                "glass" to PathTraceMaterial.thinGlass(), "alpha" to PathTraceMaterial(opacity=0.5f,alphaMode=PathTraceMaterial.AlphaMode.BLEND))) {
                camera.lookAt(Vector3f(0f,0f,3f),Vector3f())
                PathTracingRenderer(renderer,plane(material),PathTraceEnvironment.constant(),48,32,camera,backend).use { t ->
                    t.temporalReuse=true;t.samplesPerFrame=16;latch(t);repeat(4) { step(t) };t.samplesPerFrame=1
                    camera.moveWorld(0.02,0.0,0.0);step(t)
                    check(t.readHistorySampleCounts().all { it==1f }) { "$label reused invalid primary-surface history" }
                    println("PASS GPU $backend temporal rejection: $label")
                }
            }
            val occlusion=PathTraceScene().apply {
                val red=material(PathTraceMaterial.emissive(1f,0f,0f))
                val blue=material(PathTraceMaterial.emissive(0f,0f,1f))
                quad(Vector3f(-20f,-20f,0f),Vector3f(20f,-20f,0f),Vector3f(20f,20f,0f),Vector3f(-20f,20f,0f),blue)
                quad(Vector3f(-20f,-20f,1f),Vector3f(0f,-20f,1f),Vector3f(0f,20f,1f),Vector3f(-20f,20f,1f),red)
            }
            val sameMaterialLayers=PathTraceScene().apply {
                val m=material(PathTraceMaterial.diffuse(1f,1f,1f))
                quad(Vector3f(-20f,-20f,0f),Vector3f(20f,-20f,0f),Vector3f(20f,20f,0f),Vector3f(-20f,20f,0f),m)
                quad(Vector3f(-0.4f,-20f,0.75f),Vector3f(0.4f,-20f,0.75f),Vector3f(0.4f,20f,0.75f),Vector3f(-0.4f,20f,0.75f),m)
            }
            camera.lookAt(Vector3f(0f,0f,3f),Vector3f());camera.p.fov=0.7
            PathTracingRenderer(renderer,sameMaterialLayers,PathTraceEnvironment.constant(),48,32,camera,backend).use { t ->
                t.temporalReuse=true;t.pixelJitter=0f;t.samplesPerFrame=16;latch(t);repeat(8) { step(t) }
                camera.moveWorld(0.6,0.0,0.0);t.samplesPerFrame=1;step(t)
                val counts=t.readHistorySampleCounts();var revealed=0
                for (x in 0 until 48) {
                    val dx=((x+0.5)/48.0*2.0-1.0)*tan(0.35)*1.5
                    val backgroundX=0.6+3.0*dx
                    val currentForegroundX=0.6+2.25*dx
                    val oldForegroundX=backgroundX*0.75
                    if (abs(currentForegroundX)>0.43 && abs(oldForegroundX)<0.37) {
                        revealed++
                        check(counts[16*48+x]==1f) { "Same-material depth layers leaked history at x=$x: ${counts[16*48+x]}" }
                    }
                }
                check(revealed>0)
                println("PASS GPU $backend same-material depth rejection: $revealed revealed columns restart")
            }
            camera.lookAt(Vector3f(0f,0f,3f),Vector3f())
            PathTracingRenderer(renderer,occlusion,PathTraceEnvironment.constant(0f),48,32,camera,backend).use { t ->
                // The two emissive quads are colour TAGS for history here, not lights, and the
                // assertion below needs each pixel to be exactly pure red or exactly pure blue.
                // Direct emitter sampling would make the red panel at z=1 light the blue plane at
                // z=0 through its 4% dielectric lobe -- correctly, and at maxBounces=1, where a BSDF
                // continuation gathers nothing because a shadow ray reaches one segment further.
                t.emitterSampling=false
                t.temporalReuse=true;t.pixelJitter=0f;t.maxBounces=1;t.samplesPerFrame=16;latch(t);repeat(8) { step(t) }
                camera.moveWorld(0.2,0.0,0.0);t.samplesPerFrame=1;step(t)
                val image=t.readLinear();val counts=t.readHistorySampleCounts()
                check(image.indices.step(4).all { i ->
                    (image[i]<1e-5 && abs(image[i+2]-1f)<1e-5) || (abs(image[i]-1f)<1e-5 && image[i+2]<1e-5)
                }) { "Disocclusion carried red history onto a newly revealed blue surface" }
                val blue=image.indices.step(4).filter { image[it+2]>0.99f }.map { counts[it/4] }
                check(blue.any { it==1f } && blue.any { it>2f }) { "Expected both newly exposed and retained blue pixels: ${blue.minOrNull()}..${blue.maxOrNull()}" }
                t.showEnvironment=false;t.background=Vector4f(0f);step(t)
                camera.lookAlong(Vector3f(0f,0f,3f),Vector3f(0f,0f,1f));step(t)
                check(t.readLinear().all { it==0f }) { "Camera cut leaked history into transparent sky" }
                check(t.readHistorySampleCounts().all { it==1f })
                println("PASS GPU $backend temporal: disocclusion, occlusion, camera cut and transparent background")
            }
        }
    }
}