Skip to content

NanoOutputBoundsSmokeTest.kt

src/jvmMain/kotlin/afield/examples/four/NanoOutputBoundsSmokeTest.kt · 136 lines

package afield.examples.four

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.util.GpuProbe
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.Window
import afield.lang.nano.NanoComputeShader
import afield.lang.nano.NanoVM
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.system.exitProcess

FIELD_TEST id: four.nano.output-bounds requiresDisplay: true runner: jvm-main entrypoint: afield.examples.four.NanoOutputBoundsSmokeTest tags: [smoke, four, gpu, compute, nano] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "Nano GPU output bounds PASSED"

object NanoOutputBoundsSmokeTest

Runs the production shader on adjacent byte slices, including shared packed words.

fun main(args: Array<String>)

body — 108 lines, from line 28
    @JvmStatic
    fun main(args: Array<String>) {
        check(
            runCatching { NanoComputeShader.buildMappings(2, outSize = -1) }.exceptionOrNull()
                is IllegalArgumentException,
        )
        check(
            runCatching { NanoComputeShader.buildMappings(2, maxSteps = -1) }.exceptionOrNull()
                is IllegalArgumentException,
        )
        check(
            runCatching { NanoComputeShader.buildMappings(2, outSize = Int.MAX_VALUE) }.exceptionOrNull()
                is ArithmeticException,
        )
        Options.enableVR = false
        val window = Window("Nano output bounds")
        val renderer = Renderer(window)
        try {
            for (capacity in listOf(0, 1, 3, 4)) {
                GpuProbe(
                        renderer,
                        "nano-out-$capacity",
                        pump = {
                            window.pollEvents()
                            renderer.render()
                        },
                    )
                    .use { probe ->
                        val count = 32
                        val programs =
                            (0 until count).map { i ->
                                // LDI r0,i+1; OUT r0; INC r0; JMP back to OUT.
                                byteArrayOf(0xc0.toByte(), (i + 1).toByte(), 0x18, 0x04, 0xe0.toByte(), 0xfc.toByte())
                            }
                        val mapping =
                            NanoComputeShader.buildMappings(count, codeSize = 8, outSize = capacity, maxSteps = 100)
                        // Keep guard bytes before and after the entire allocation as well as per-VM bounds.
                        for (i in 0 until count) mapping.putInt(i * 20 + 8, 4 + i * capacity)
                        val code = NanoComputeShader.packPrograms(programs, 8)
                        val mappings = probe.fixture("mappings", mapping.remaining() / 4)
                        mappings.mapAndWrite_bytes { put(mapping.duplicate()) }
                        val ram = probe.fixture("ram", count * 32 / 4)
                        ram.mapAndWrite_bytes { while (hasRemaining()) put(0) }
                        val codes = probe.fixture("codes", code.remaining() / 4)
                        codes.mapAndWrite_bytes { put(code.duplicate()) }
                        val outputBytes = 4 + count * capacity + 16
                        val output = probe.fixture("out", outputBytes / 4)
                        output.mapAndWrite_bytes { while (hasRemaining()) put(0x7e.toByte()) }
                        val sizes = mapOf(
                            "MappingBuffer" to mapping.capacity(),
                            "RamBuffer" to count * 32,
                            "CodeBuffer" to code.capacity(),
                            "OutBuffer" to outputBytes,
                        )
                        val buffers = mapOf(
                            "MappingBuffer" to mappings,
                            "RamBuffer" to ram,
                            "CodeBuffer" to codes,
                            "OutBuffer" to output,
                        )
                        val stage =
                            probe
                                .stage("bounded-out", NanoComputeShader.SOURCE) {
                                    ssboResolver = { requested ->
                                        requested.apply {
                                            size = sizes.getValue(name).toLong()
                                            addressable()
                                            streamReads()
                                            result = buffers.getValue(name)
                                        }
                                    }
                                }
                                .dispatch(groups = 1) {
                                    bind("numInstances") { count }
                                    bind("defaultMaxSteps") { 200 }
                                    bind("ramSize") { 32 }
                                    bind("codeSize") { 8 }
                                }
                        check(stage.valid) { probe.verdictLines("Nano compilation").joinToString("\n") }
                        // The untouched guard bytes are the assertion: sentinel-based settle would
                        // deliberately overwrite them and wait for writes that must never happen.
                        probe.pumpFrames(4)
                        probe.idle()
                        val actual = ByteBuffer.allocate(outputBytes).order(ByteOrder.LITTLE_ENDIAN)
                        probe.readInts(output).forEach(actual::putInt)
                        val expected = ByteArray(outputBytes) { 0x7e.toByte() }
                        for (i in programs.indices) {
                            val cpuOutput = mutableListOf<Int>()
                            NanoVM(maxSteps = 100).apply { onOutput = { cpuOutput += it } }.run(programs[i])
                            check(cpuOutput.size > capacity)
                            cpuOutput.take(capacity).forEachIndexed { j, value ->
                                expected[4 + i * capacity + j] = value.toByte()
                            }
                        }
                        check(actual.array().contentEquals(expected)) {
                            "Output escaped or corrupted a $capacity-byte slice"
                        }
                        println("Nano capacity=$capacity: 32 CPU prefixes match; guard bytes preserved")
                    }
            }
            println("Nano GPU output bounds PASSED")
        } finally {
            renderer.cleanup()
            window.cleanup()
        }
        exitProcess(0)
    }
}