Skip to content

OpenDlssNrStudy.kt

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrStudy.kt · 207 lines

An editable source image fed through the native neural-rendering (NR) pass, with a graded comparison.

OpenDlssNrStudy renders a source, either a fragment shader, a loaded image or a small geometry scene, into a texture, runs it through OpenDlssNrPass when OpenDlssNrSupport reports the native library ready, and displays a graded comparison. Without the library it still shows the source. OpenDlssNrStudy.reload swaps shaders live and OpenDlssNrStudy.saveFrame captures a frame.

package afield.graphics.render.neural

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.device.CommandBuffer
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.texture.LoadedTexture
import afield.graphics.texture.TextureSampler
import afield.graphics.texture.HasImageViewArray
import afield.graphics.texture.PreparesSampledImages
import afield.runtime.lifecycle.RunScope
import afield.math.linalg.Vector4f
import org.lwjgl.vulkan.VK13.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.file.Path
import java.util.concurrent.CompletableFuture

class OpenDlssNrStudy

Editable source -> native NR -> graded comparison. Source mode works without the optional library. The current box run owns the study: stopping or rerunning it closes the old resources. The renderer also closes the study before destroying its device. The comparison and capture use the very same targets as the live Starting Point.

class OpenDlssNrStudy(
    val renderer: Renderer,
    val size: Int = 768,
    val name: String = "nr-study",
    imagePath: String? = null,
    val geometry: Boolean = false,
    vertex: String = if(geometry)resource("cubes.vert") else defaultVertex,
    fragment: String = if(geometry)resource("cubes.frag") else defaultFragment,
    attach: Boolean = true
) : Target, AutoCloseable {

init

    init { require(!geometry || imagePath==null) { "The geometry study needs shaders rather than an input image" } }

val availability = OpenDlssNrSupport.availability(renderer)

    private val image = imagePath?.let { LoadedTexture.fromFile(it, renderer, VK_FORMAT_R8G8B8A8_UNORM) }

val source

    val source = RenderToTexture(renderer) {
        width=size; height=size; useDepth=geometry; format=VK_FORMAT_R16G16B16A16_SFLOAT
        colorTransferSrc=true; debugName="$name-source"
    }
    private val imageProvider=image?.let { texture -> object : HasImageViewArray,PreparesSampledImages {
        override val imageViewArray=listOf(texture)
        override fun prepareSampledImagesFor(command: CommandBuffer)=texture.prepareSampledImagesFor(command)
    } }
    private val sampler=TextureSampler(renderer,1,false,VK_FILTER_LINEAR)
    private fun buildSourceModule(vertex: String,fragment: String) = Module(parameters(renderer) {
        this.name="$name-source"; +source; depthTest=geometry; depthWrite=geometry; blend=false
        textureResolver={ if(it.name=="image" && imageProvider!=null)imageProvider to sampler else null }
        this.vertex=vertex
        this.fragment=if(image==null)fragment else """
            #version 460
            layout(location=0) in vec2 uv;
            layout(location=0) out vec4 color;
            layout(binding=0) uniform sampler2D image;
            layout(push_constant) uniform Study { vec4 frame; } pc;
            void main() {
                vec3 c=texture(image,uv).rgb;
                c=mix(c/12.92,pow((c+.055)/1.055,vec3(2.4)),step(vec3(.04045),c));
                color=vec4(c+pc.frame.w,1);
            }
        """.trimIndent()
    })
    private var sourceModule = buildSourceModule(vertex,fragment)
    private var sourceMesh = triangle(sourceModule, if(geometry)3+39*36 else 3)

A rebuilt source (new stage interface) waiting for the next frame boundary.

    @Volatile
    private var pendingSource: Pair<Module,Mesh>?=null

val pass = if(availability.ready)OpenDlssNrPass(renderer,size,size) else null

val output

    val output = RenderToTexture(renderer) {
        width=size*3; height=size+64; useDepth=false; format=VK_FORMAT_R16G16B16A16_SFLOAT
        colorTransferSrc=true; debugName="$name-comparison"
    }
    private val compareModule = Module(parameters(renderer) {
        this.name="$name-comparison"; +output; depthTest=false; depthWrite=false; blend=false
        this.vertex=defaultVertex; this.fragment=resource("compare.frag")
        textureResolver={ when(it.name) { "source" -> source to sampler; "neural" -> (pass?.output ?: source) to sampler; else -> null } }
    })
    private val compareMesh = triangle(compareModule)
    private var screen: ModulePointer?=null
    private var frame=0L
    private var closed=false
    private val ownerScope=RunScope.current()

Closes this study when its renderer tears its device down, so the boxes using it need not stop first.

    private val closeWithRenderer=Runnable { close() }

val isClosed: Boolean get()=closed

Whether close has run, by the box or by the renderer going away.

var time=0f

var animate=true

var recording=false

The editable capture box reserves scene time while recording.

var controls=OpenDlssNrControls(style=1,tone=0f,structure=10f)

var grade=Vector4f(.65f,1.1f,1.4f,1f)

init

    init {
        check(sourceModule.valid && compareModule.valid) { "NR study shader compilation failed" }
        source.modules["source"]=sourceModule.pointer()
        output.modules["compare"]=compareModule.pointer()
        renderer.beforeDeviceCleanup+=closeWithRenderer
        if(attach)attach()
        ownerScope?.own(this)
    }
    private fun triangle(module: Module, vertices: Int=3): Mesh = Mesh(renderer,module.pointer()).also {
        it.resize(vertices,0,module.pointer()); it.push.setMembers(module.fi.pushConstants.single().members)
        module.meshes["triangle"]=it
    }

fun attach(): OpenDlssNrStudy

    fun attach(): OpenDlssNrStudy {
        check(!closed)
        renderer.renderList.preTargets[name]=this
        screen=output.blitToScreen(name=name)
        return this
    }

fun reload(vertex: String=if(geometry)resource("cubes.vert") else defaultVertex,

A shader edit replaces the source at a frame boundary. An edit that keeps the stage interface swaps the pipeline in place; one that changes it (adding or removing a varying, a push-constant member, a binding) rebuilds the source module and mesh. Invalid edits retain the current source.

    fun reload(vertex: String=if(geometry)resource("cubes.vert") else defaultVertex,
        fragment: String=if(geometry)resource("cubes.frag") else defaultFragment): Result<Unit> = runCatching {
        check(!closed && image==null) { "Shader reload requires the procedural source" }
        val errors=mutableListOf<String>()
        val v=Introspection(vertex,ShaderKind.VERTEX_SHADER,errorHandler={message,_ -> errors+=message})
        val f=Introspection(fragment,ShaderKind.FRAGMENT_SHADER,errorHandler={message,_ -> errors+=message})
        check(v.valid && f.valid) {errors.joinToString("\n")}
        val current=pendingSource?.first ?: sourceModule
        if(graphicsShaderAbiCheck(current.vi,current.fi,v,f).compatible) {
            current.installPipelineUpdate(current.buildPipelineUpdate(v,f))
        } else {
            val module=buildSourceModule(vertex,fragment)
            if(!module.valid) { renderer.cleanup+=Runnable { module.cleanup() }; error("NR study shader compilation failed") }
            val mesh=triangle(module,if(geometry)3+39*36 else 3)
            pendingSource?.let { (old,_) -> renderer.cleanup+=Runnable { old.cleanup() } }
            pendingSource=module to mesh
        }
        pass?.reset("source shader edit")
    }

val reloadPending: Boolean get()=pendingSource!=null

Whether the last interface-changing reload has reached the GPU.

fun saveFrame(path: Path): CompletableFuture<Path>

Queue the next comparison frame for a lossless PNG after its GPU fence completes.

    fun saveFrame(path: Path): CompletableFuture<Path> {
        check(!closed)
        return output.readPixelsAsync().thenApplyAsync { bytes ->
            val buffer=ByteBuffer.wrap(bytes).order(ByteOrder.nativeOrder())
            val pixels=FloatArray(output.width*output.height*4) { java.lang.Float.float16ToFloat(buffer.getShort(it*2)) }
            OpenDlssNrDiagnostics.png(pixels,output.width,output.height,path)
            path
        }
    }

override fun render(command: CommandBuffer,index: Int)

    override fun render(command: CommandBuffer,index: Int) {
        if(closed)return
        pendingSource?.let { (module,mesh) ->
            pendingSource=null
            val old=sourceModule
            sourceModule=module; sourceMesh=mesh
            source.modules["source"]=module.pointer()
            renderer.cleanup+=Runnable { old.cleanup() }
        }
        if(geometry) {
            sourceMesh.push["sizeJitter"]=Vector4f(size.toFloat(),size.toFloat(),0f,0f)
            sourceMesh.push["times"]=Vector4f(time,time,0f,0f)
            sourceMesh.push["settings"]=Vector4f(0f,1f,0f,0f)
        } else sourceMesh.push["frame"]=Vector4f(time,size.toFloat(),size.toFloat(),0f)
        source.render(command,index)
        pass?.let {
            it.controls=controls
            it.evaluate(command,OpenDlssNrInputs(source,OpenDlssNrColorEncoding.LINEAR_LDR),OpenDlssNrFrame(frame))
        }
        compareMesh.push["grade"]=grade
        compareMesh.push["dimensions"]=Vector4f(output.width.toFloat(),output.height.toFloat(),64f,if(pass==null)0f else 1f)
        output.render(command,index)
        if(animate)time+=1f/30f
        frame++
    }

override fun close()

    override fun close() {
        if(closed)return
        closed=true
        ownerScope?.disown(this)
        renderer.beforeDeviceCleanup.remove(closeWithRenderer)
        renderer.renderList.preTargets.remove(name,this)
        screen?.let { renderer.renderList.modules.remove(name,it) }
        pass?.close()
        renderer.cleanup+=Runnable {
            screen?.current?.cleanup(); sourceModule.cleanup(); pendingSource?.first?.cleanup(); compareModule.cleanup()
            for(target in listOf(source,output)) {
                target.imageViewArray.forEach {it.imageView.cleanup()}
                target.colorImageRefs.forEach {it?.cleanup()}
                target.depthImageView?.cleanup(); target.depthImage?.cleanup()
                target.defaultSampler?.cleanup()
            }
            image?.cleanup(); sampler.cleanup()
        }
    }

companion object

    companion object {
        private fun resource(file: String)=OpenDlssNrStudy::class.java.getResource("/afield/four/neuralrendering/$file")!!.readText()

val defaultVertex: String get()=resource("study.vert")

val defaultFragment: String get()=resource("study.frag")