RtxdiLightingSmoke.kt
src/jvmMain/kotlin/afield/graphics/render/lighting/RtxdiLightingSmoke.kt · 153 lines
package afield.graphics.render.lighting
import afield.document.graph.Box
import afield.graphics.vulkan.device.lightingCapabilityReport
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.Window
import afield.math.linalg.Matrix4f
import afield.math.linalg.Vector3f
import org.json.JSONObject
import java.io.File
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
import kotlin.math.*
import kotlin.system.exitProcess
object RtxdiLightingSmoke
Optional GPU acceptance check: build the pinned shaders before run-jvm-main.
@JvmStatic fun main(args: Array<String>)
body — 134 lines, from line 19
@JvmStatic fun main(args: Array<String>) {
val program=RtxdiProgram.load()
Options.enableVR=false;Options.doVulkanSDK()
val dir=File("build/lighting-experiment/rtxdi-gpu").also { it.mkdirs() }
val window=Window("Field · RTXDI laboratory");val renderer=Renderer(window)
val box=Box("rtxdi-check");box.setPropertyRaw("renderer",renderer,doResync=false);box.setPropertyRaw("window",window,doResync=false)
try {
Box.withBoxContextNow(box) {
DirectLightingDemo(renderer,256,192).use { demo ->
val light=demo.lighting
val settings=RtxdiSettings({ demo.bundle.scene.camera.modelView() },{ demo.bundle.scene.projection.evaluateUniform() })
fun emitters(n: Int)=List(n) { i ->
val a=i*2*PI/n
IdentifiedAreaLight(100+i,AreaEmitter(Vector3f((cos(a)*3).toFloat(),4.5f,(sin(a)*2).toFloat()),
Vector3f(0.6f,0f,0f),Vector3f(0f,0f,0.6f),Vector3f(150f/n,100f/n,70f/n))) }
val original=emitters(16);settings.commitLights(original)
light.enableRtxdi(program,settings).getOrThrow();light.samples=4
fun pump(n: Int=1) { repeat(n) { window.pollEvents();renderer.render() } }
fun read(): FloatArray { pump();return light.readLinear().also { data ->
check(data.all { it.isFinite() }) { "Non-finite RTXDI output" }
check(data.indices.filter { it%4!=3 }.all { data[it]>=0 && data[it]<10000 }) { "Negative or explosive lighting" }
} }
fun mean(data: FloatArray): Double = (data.indices step 4).filter { data[it+3]>=0 }.map { i ->
0.2126*data[i]+0.7152*data[i+1]+0.0722*data[i+2] }.average()
fun variance(data: List<Double>)=data.sumOf { (it-data.average()).pow(2) }/(data.size-1)
fun ensemble(mode: DirectLighting.Mode,temporal: Boolean,spatial: Int,budget: Int): List<Double> {
light.mode=mode;settings.temporal=temporal;settings.spatialSamples=spatial;light.samples=budget
return List(16) { trial -> settings.resetHistory("independent ensemble $trial");pump(if(temporal)24 else 1);mean(read()) }
}
val reference=ensemble(DirectLighting.Mode.RTXDI_REFERENCE,false,0,256)
save(dir,"reference",read(),light.width,light.height)
val report=JSONObject().put("gpu",renderer.device.lightingCapabilityReport()).put("program",program.manifest)
.put("width",light.width).put("height",light.height).put("lights",original.size).put("workingBytes",light.workingBytes)
val ensembles=JSONObject().put("reference",JSONObject().put("mean",reference.average()).put("variance",variance(reference)))
for((label,t,s) in listOf(Triple("initial",false,0),Triple("temporal",true,0),Triple("spatial",false,4),Triple("full",true,4))) {
val values=ensemble(DirectLighting.Mode.RTXDI,t,s,4)
val se=sqrt(variance(values)/values.size+variance(reference)/reference.size)
val delta=abs(values.average()-reference.average())
println("RTXDI $label mean=${values.average()} reference=${reference.average()} SE=$se delta=$delta")
ensembles.put(label,JSONObject().put("mean",values.average()).put("variance",variance(values)).put("standardError",se).put("difference",delta))
// Independent scene-mean ensembles; 6 SE plus 0.1% covers FP/UV quantization.
check(delta<=6*se+0.001*reference.average()) { "$label estimator differs from reference: delta=$delta SE=$se" }
save(dir,label,read(),light.width,light.height)
}
report.put("ensembles",ensembles)
light.view=DirectLighting.View.HISTORY_REJECTION
val history=read();save(dir,"history",history,light.width,light.height)
check(mean(history)<0.5) { "Static scene rejects most history: ${mean(history)}" }
demo.bundle.scene.camera.at.x+=0.02f
val movingHistory=read();check(mean(movingHistory)<0.98) { "Smooth camera has no valid reprojection" }
demo.bundle.scene.camera.at.x-=3f
read();check(!light.rtxdiHistoryValid) { "Camera cut retained history" }
demo.bundle.scene.camera.at.set(5f,4f,6f);settings.resetHistory("restore camera");pump(24)
light.view=DirectLighting.View.RADIANCE
// Stable IDs must survive ordering edits without global history invalidation.
val resets=light.rtxdiResetCount
settings.commitLights(original.reversed());read()
check(light.rtxdiResetCount==resets && light.rtxdiHistoryValid)
settings.commitLights(original.drop(8));pump(16)
light.view=DirectLighting.View.LIGHT_ID
val ids=read()
val allowed=(108..115).map { id -> floatArrayOf((id*71%251)/250f,(id*137%251)/250f,(id*197%251)/250f) }+listOf(floatArrayOf(0f,0f,0f))
check((ids.indices step 4).filter { ids[it+3]>=0 }.all { offset -> allowed.any { rgb -> (0..2).all { abs(ids[offset+it]-rgb[it])<1e-6f } } }) { "Deleted light ID reused" }
light.view=DirectLighting.View.RADIANCE
settings.commitLights(emptyList());val dark=read()
check(mean(dark)<1e-7) { "Deleted lights left stale illumination" }
settings.commitLights(original)
val before=demo.visibility.blasBuilds
demo.moveObject(1,Matrix4f().translation(-2.2f,0.75f,1f));read()
check(!light.rtxdiHistoryValid && demo.visibility.blasBuilds==before)
demo.moveObject(1,Matrix4f().translation(-1.3f,0.75f,0f));pump(16)
for(view in listOf(DirectLighting.View.LIGHT_ID,DirectLighting.View.RESERVOIR_M,DirectLighting.View.RESERVOIR_WEIGHT,DirectLighting.View.MOTION)) {
light.view=view;save(dir,view.name.lowercase(),read(),light.width,light.height)
}
light.view=DirectLighting.View.RADIANCE
light.mode=DirectLighting.Mode.SOURCE;val source=read()
light.mode=DirectLighting.Mode.RTXDI;light.effectWeight=0f;val bypass=read()
check(source.indices.filter { it%4!=3 }.all { abs(source[it]-bypass[it])<1e-6 }) { "Bypass changed source" }
light.effectWeight=1f;light.reloadRtxdi(program).getOrThrow();read();check(!light.rtxdiHistoryValid)
val sweeps=mutableListOf<JSONObject>()
for(n in listOf(1,16,128))for(budget in listOf(1,8)) {
settings.commitLights(emitters(n));light.samples=budget;pump(24)
light.clearRtxdiTimings();pump(96)
val timing=JSONObject()
light.rtxdiTimings.forEach { (stage,all) -> val values=all.sorted()
if(values.isNotEmpty())timing.put(stage,JSONObject().put("count",values.size).put("p50",values[values.size/2])
.put("p95",values[(values.size*0.95).toInt()]).put("min",values.first()).put("max",values.last())) }
sweeps+=JSONObject().put("lights",n).put("candidates",budget).put("gpuMs",timing)
}
report.put("sweeps",sweeps).put("historyRejectionStatic",mean(history)).put("historyRejectionMovingCamera",mean(movingHistory))
.put("historyResets",light.rtxdiResetCount)
File(dir,"measurements.json").writeText(report.toString(2))
if("--clip" in args) {
val clip=File(dir,"clip").also { it.mkdirs() };light.samples=4
repeat(120) { frame ->
settings.commitLights(emitters(16).map { l -> l.copy(emitter=l.emitter.copy(center=Vector3f(l.emitter.center).add(sin(frame/20.0).toFloat(),0f,0f))) })
save(clip,"%04d".format(frame),read(),light.width,light.height)
}
}
// Pathological emitters and black materials must remain finite and dark when appropriate.
settings.commitLights(listOf(original.first().copy(emitter=original.first().emitter.copy(
edgeU=Vector3f(0.006f,0f,0f),edgeV=Vector3f(0f,0f,0.006f),radiance=Vector3f(50000f)))))
pump(8);read()
demo.meshes.forEach { it.gBuffer.mesh.push["baseColor"]=afield.math.linalg.Vector4f(0f,0f,0f,1f) }
settings.resetHistory("black materials");check(mean(read())<1e-7)
println(report.toString(2))
}
}
// Recreate at a non-tiled size; old GPU allocations retire after their last use.
Box.withBoxContextNow(box) {
DirectLightingDemo(renderer,173,119).use { resized ->
val settings=RtxdiSettings({ resized.bundle.scene.camera.modelView() },{ resized.bundle.scene.projection.evaluateUniform() })
settings.commitLights(listOf(IdentifiedAreaLight(1,resized.lighting.emitter)))
resized.lighting.enableRtxdi(program,settings).getOrThrow();resized.lighting.mode=DirectLighting.Mode.RTXDI
repeat(16) { window.pollEvents();renderer.render() }
check(resized.lighting.readLinear().all { it.isFinite() })
org.lwjgl.glfw.GLFW.glfwSetWindowSize(window.windowHandle,800,600)
repeat(8) { window.pollEvents();renderer.render() }
check(resized.lighting.readLinear().any { it>0.1f })
}
}
println("RTXDI LIGHTING CHECK DONE")
} finally { renderer.cleanup();window.cleanup() }
exitProcess(0)
}
private fun save(dir: File,name: String,data: FloatArray,width: Int,height: Int) {
val image=BufferedImage(width,height,BufferedImage.TYPE_INT_RGB)
for(y in 0 until height)for(x in 0 until width) {
fun code(c: Int): Int { val v=data[(y*width+x)*4+c].toDouble().coerceAtLeast(0.0);return (255*(v/(1+v)).pow(1/2.2)).roundToInt().coerceIn(0,255) }
image.setRGB(x,y,(code(0) shl 16) or (code(1) shl 8) or code(2))
}
ImageIO.write(image,"png",File(dir,"$name.png"))
}
}