Skip to content

SlugRenderSmoke.kt

src/jvmMain/kotlin/afield/examples/four/SlugRenderSmoke.kt · 125 lines

package afield.examples.four

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.bindPushConstant
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.vulkan.util.Options
import afield.graphics.vulkan.util.Window
import afield.geometry.path.FLine
import afield.graphics.text.slug.*
import afield.math.linalg.*
import org.lwjgl.vulkan.VK10.*
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.abs
import kotlin.system.exitProcess

FIELD_TEST id: four.slug.readback requiresDisplay: true runner: jvm-main entrypoint: afield.examples.four.SlugRenderSmoke tags: [smoke, four, gpu, slug] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "Slug rendering PASSED" stdoutNotContains: - "VUID-" - "Validation Error" stderrNotContains: - "VUID-" - "Validation Error"

object SlugRenderSmoke

Real analytic coverage, alpha and immutable atlas replacement on the GPU.

@JvmStatic fun main(args: Array<String>)

body — 82 lines, from line 43
    @JvmStatic fun main(args: Array<String>) {
        val executable = File("native/field-slug/build/field-slug")
        fun rectangle(left: Double, right: Double) = FLine().moveTo(left, -0.8)
            .lineTo(right, -0.8).lineTo(right, 0.8).lineTo(left, 0.8).close()
        val shape = rectangle(-0.8, 0.8).moveTo(-0.25, -0.25)
            .lineTo(-0.25, 0.25).lineTo(0.25, 0.25).lineTo(0.25, -0.25).close()
        val prepared = SlugPath.compile(shape, executable)
        val replacement = SlugPath.compile(rectangle(0.4, 0.8), executable)
        Options.enableVR = false
        Options.validate = true
        val window = Window("Slug coverage regression")
        val renderer = Renderer(window)
        try {
            val target = RenderToTexture(renderer) {
                width = 128; height = 128; useDepth = false
                format = VK_FORMAT_R32G32B32A32_SFLOAT
                clearColor = Vector4f(0f, 0f, 0f, 0f)
            }
            renderer.renderList.preTargets["slug"] = target
            val module = Module(parameters(renderer) {
                vertex = SlugShaders.vertex; fragment = SlugShaders.fragment
                +target; depthTest = false; depthWrite = false
                srcAlpha = VK_BLEND_FACTOR_ONE
            }).pointer()
            check(module.current.valid)
            val mesh = Mesh(renderer)
            module += "slug" to mesh
            target += "slug" to module
            mesh.resize(6)
            val data = SlugBuffer(renderer, prepared)
            var opacity = 1f
            var scale = 1f
            mesh.push.bindPushConstant("slugData", { data }) { error("slugData") }
            mesh.push.bindPushConstant("mvp", { Matrix4f().scaling(scale, scale, 1f) }) { error("mvp") }
            mesh.push.bindPushConstant("viewport", { Vector2f(128f, 128f) }) { error("viewport") }
            mesh.push.bindPushConstant("tint", { Vector4f(1f, 1f, 1f, opacity) }) { error("tint") }
            fun frame() { window.pollEvents(); renderer.render() }
            fun read(): FloatArray {
                repeat(4) { frame() }
                val result = target.readPixelsAsync()
                val deadline = System.nanoTime() + 15_000_000_000L
                while (!result.isDone && System.nanoTime() < deadline) frame()
                check(result.isDone) { "Readback timeout" }
                val b = ByteBuffer.wrap(result.get()).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer()
                return FloatArray(b.remaining()).also { b.get(it) }
            }
            fun alpha(p: FloatArray, x: Int, y: Int) = p[(y * 128 + x) * 4 + 3]
            fun near(actual: Float, expected: Float) = check(abs(actual - expected) < 0.02f) {
                "Expected $expected, got $actual"
            }
            val opaque = read()
            near(alpha(opaque, 64, 64), 0f)
            near(alpha(opaque, 32, 64), 1f)
            near(alpha(opaque, 2, 64), 0f)
            check(opaque.indices.any { it % 4 == 3 && opaque[it] in 0.05f..0.95f }) { "No antialiased edge pixels" }
            opacity = 0.5f
            val translucent = read()
            near(alpha(translucent, 32, 64), 0.5f)
            for (c in 0..2) near(translucent[(64 * 128 + 32) * 4 + c], opaque[(64 * 128 + 32) * 4 + c] * 0.5f)
            scale = 0.5f
            val smaller = read()
            near(alpha(smaller, 32, 64), 0f)
            near(alpha(smaller, 44, 64), 0.5f)
            scale = 1f
            val previous = data.getBuffer()
            data.replace(replacement)
            check(data.revision == 1L && data.getBuffer() != previous)
            val replaced = read()
            near(alpha(replaced, 32, 64), 0f)
            near(alpha(replaced, 104, 64), 0.5f)
            // Closed while its mesh is still bound: the mesh draws nothing, and nothing throws.
            data.close(); data.close()
            val closed = read()
            check(closed.indices.none { it % 4 == 3 && closed[it] > 0.001f }) { "A closed SlugBuffer still drew coverage" }
            check(runCatching { data.replace(replacement) }.isFailure) { "a closed SlugBuffer must refuse replace" }
            module.current.meshes.clear()
            repeat(40) { frame() }
            println("Slug rendering PASSED: hole, edges, alpha, scale, replacement, close before or after the mesh")
        } finally { renderer.cleanup(); window.cleanup() }
        exitProcess(0)
    }
}