Skip to content

ShaderLearningChecks.kt

src/jvmMain/kotlin/afield/graphics/shader/learning/ShaderLearningChecks.kt · 92 lines

package afield.graphics.shader.learning

import afield.media.reconstruction.ReconstructionCapture
import org.json.JSONObject
import java.io.File
import kotlin.math.*

Acceptance checks for a learned shader, run from a box beneath the moduleHere starting point once training finishes.

ShaderLearningChecks.run measures a ShaderLearningView at a held-out parameter: full-image error, timing, and whether a rejected model leaves the previous one attached. ShaderLearningChecks.stripeMetrics measures error at the bundled test pattern's hard stripe edge separately, because a whole-image mean can hide edge smoothing.

object ShaderLearningChecks

Called from a child of the actual moduleHere Starting Point, after its training process completes.

object ShaderLearningChecks {

fun stripeMetrics(original: FloatArray,learned: FloatArray,width: Int,height: Int): JSONObject

Measure the bundled hard stripe separately: a whole-image mean can hide edge smoothing.

    fun stripeMetrics(original: FloatArray,learned: FloatArray,width: Int,height: Int): JSONObject {
        require(original.size==width*height*4 && learned.size==original.size)
        check(original.all { it.isFinite() } && learned.all { it.isFinite() })
        val stripe=floatArrayOf(.95f,.04f,.8f)
        val center=((height/2)*width+width/2)*4
        check((0..2).all { abs(original[center+it]-stripe[it])<.001f }) { "Missing source stripe" }
        val inside=((height/2)*width+((-.11f+1)*.5f*width).toInt())*4
        val outside=((height/2)*width+((-.13f+1)*.5f*width).toInt())*4
        val jump=(0..2).sumOf { abs(original[inside+it]-original[outside+it]).toDouble() }
        check(jump>.4) { "Source stripe edge is not discontinuous: $jump" }
        var edgeError=0.0;var otherError=0.0;var edgeCount=0;var otherCount=0
        var maximum=0f
        for(y in 0 until height)for(x in 0 until width) {
            val edge=abs(abs((x+.5f)/width*2-1)-.12f)<.02f
            for(c in 0..2) {
                val i=(y*width+x)*4+c;val d=abs(original[i]-learned[i]);maximum=max(maximum,d)
                if(edge){edgeError+=d*d;edgeCount++}else{otherError+=d*d;otherCount++}
            }
        }
        return JSONObject().put("xRange",listOf(-.12,.12)).put("rgb",listOf(.95,.04,.8))
            .put("edgeBandHalfWidthNormalized",.02).put("sourceEdgeRgbL1",jump)
            .put("edgeBandPixelFraction",edgeCount.toDouble()/(edgeCount+otherCount))
            .put("edgeBandLinearRgbMse",edgeError/edgeCount).put("awayFromEdgesLinearRgbMse",otherError/otherCount)
            .put("fullImageLinearRgbMse",(edgeError+otherError)/(edgeCount+otherCount))
            .put("maxAbsoluteRgbError",maximum)
    }

fun run(lab: ShaderLearningView,directory: File=File("build/neural-tools/from-module"))

body — 48 lines, from line 44
    fun run(lab: ShaderLearningView,directory: File=File("build/neural-tools/from-module")) {
        directory.mkdirs();val renderer=lab.renderer;val model=requireNotNull(lab.model)
        val oldView=lab.view;val oldParameter=lab.parameter
        fun pump(n: Int=1){val end=renderer.frameCounter+n;while(renderer.frameCounter<end){renderer.windows.forEach { it.pollEvents() };renderer.render()}}
        try {
            val report=JSONObject().put("cooperative",model.cooperative);lab.parameter=.173f
            lab.view=2;pump(16)
            val original=ReconstructionCapture.read(renderer,lab.source);val learned=ReconstructionCapture.read(renderer,model.output)
            check(original.all { it.isFinite() } && learned.all { it.isFinite() })
            var error=0.0;var count=0;var maximum=0f
            for(i in original.indices)if(i%4<3){val d=abs(original[i]-learned[i]);maximum=max(maximum,d);error+=d*d;count++}
            val mse=error/count;report.put("heldOutParameter",lab.parameter).put("fullImageLinearRgbMse",mse).put("maxAbsoluteRgbError",maximum)
            report.put("stripe",stripeMetrics(original,learned,lab.source.width,lab.source.height))
            check(mse<.003) { "Bundled editable teacher did not fit: $mse" }
            ReconstructionCapture.save(File(directory,"source.png"),original,lab.source.width,lab.source.height)
            ReconstructionCapture.save(File(directory,"learned.png"),learned,lab.source.width,lab.source.height)
            for(view in listOf(0,1)) {
                lab.view=view;pump(32);val first=lab.viewGpuMilliseconds.size
                val computeFirst=model.gpuMilliseconds.size;val renders=lab.sourceRenderCount;pump(100)
                if(view==1)check(renders==lab.sourceRenderCount) { "Learned replacement still rendered the teacher" }
                val times=lab.viewGpuMilliseconds.drop(first).sorted();check(times.size>=95)
                report.put(if(view==0)"sourceViewGpuMs" else "learnedViewGpuMs",JSONObject().put("count",times.size)
                    .put("p50",times[times.size/2]).put("p95",times[(times.size*.95).toInt()]))
                if(view==1) {
                    val compute=model.gpuMilliseconds.drop(computeFirst).sorted();check(compute.size>=95)
                    report.put("learnedComputeGpuMs",JSONObject().put("count",compute.size)
                        .put("p50",compute[compute.size/2]).put("p95",compute[(compute.size*.95).toInt()]))
                }
            }
            check(runCatching { model.setParameter(2f) }.isFailure)
            val rejected=File(directory,"rejected-model").also { it.mkdirs() }
            File(rejected,"weights.json").writeText(JSONObject().put("sourceKind","captured-module")
                .put("sourceShaderDigest","a different shader").toString())
            check(runCatching { lab.load(rejected) }.isFailure && lab.model===model)
            report.put("incompatibleModelPreservedWorkingView",true)
            lab.view=2
            repeat(120){i->lab.parameter=sin(i/120f*2*PI).toFloat();pump()
                val image=ReconstructionCapture.read(renderer,lab.output)
                ReconstructionCapture.save(File(directory,"clip/%04d.png".format(i)),image,lab.source.width,lab.source.height)
                if(i==24)ReconstructionCapture.save(File(directory,"comparison.png"),image,lab.source.width,lab.source.height)
            }
            report.put("output",listOf(lab.source.width,lab.source.height)).put("sourcePassSkippedInReplacement",true)
                .put("sourceShaderDigest",ShaderTraining.sourceDigest(lab.source)).put("frameCount",120)
            File(directory,"measurements.json").writeText(report.toString(2)+"\n")
            println("MODULE SHADER LEARNING CHECK DONE")
        } finally {lab.view=oldView;lab.parameter=oldParameter}
    }
}