Skip to content

PingPongRestoreSmoke.kt

src/jvmMain/kotlin/afield/examples/four/PingPongRestoreSmoke.kt · 202 lines

package afield.examples.four

import afield.graphics.texture.image_layout_transition
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.vulkan.targets.FrameCadencedPingPongRenderToTexture
import afield.graphics.vulkan.targets.PingPongRenderToTexture
import afield.graphics.vulkan.targets.RTTParameters
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.Window
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.VK13.*
import org.lwjgl.vulkan.VkBufferImageCopy
import org.lwjgl.vulkan.VkImageSubresourceRange
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess

FIELD_TEST id: four.pingpong-restore.smoke requiresDisplay: true runner: jvm-main entrypoint: afield.examples.four.PingPongRestoreSmoke tags: [smoke, four, gpu, feedback, lifecycle] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "ping-pong history restore PASSED" stdoutNotContains: - "VUID-" - "Validation Error" stderrNotContains: - "VUID-" - "Validation Error"

object PingPongRestoreSmoke

Real transfers through every slot of ordinary and cadenced, two-attachment, two-layer rings.

declaration — 63 lines, from line 41
object PingPongRestoreSmoke {
    private const val SIDE = 8
    private const val LAYERS = 2
    private const val BYTES = SIDE * SIDE * LAYERS * 4

    private val setup: RTTParameters.() -> Unit = {
        width = SIDE
        height = SIDE
        colorAttachments = 2
        colorLayerCount = LAYERS
        multiviewViewMask = 3
        format = VK_FORMAT_R8G8B8A8_UNORM
        useDepth = false
        clearFirst = false
        colorTransferDst = true
    }

    private class WatchedRing(renderer: Renderer) : PingPongRenderToTexture(renderer, 3, setup) {
        var renderHooks = 0
        override fun beforeRender(index: Int) {
            renderHooks++
            super.beforeRender(index)
        }
        fun boundImage() = colorImageHandles[0]
    }

    private fun region(stack: MemoryStack, offset: Long = 0) = VkBufferImageCopy.calloc(1, stack).also {
        it[0].bufferOffset(offset)
        it[0].imageSubresource().aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(LAYERS)
        it[0].imageExtent().set(SIDE, SIDE, 1)
    }

    private fun pattern(seed: Int) = ByteArray(BYTES) { i -> ((i * 17 + seed) % 251).toByte() }

    private fun views(target: PingPongRenderToTexture): List<Long> = buildList {
        add(target.latestImageView.imageView.image)
        add(target.previousImageView.imageView.image)
        add(target.thisImageView.imageView.image)
        add(target.otherImageView.imageView.image)
        for (a in 0 until target.colorAttachmentCount) for (i in 0 until target.frameCount) {
            add(target.frameImageView(i, a).imageView.image)
        }
    }

    private fun readRing(command: CommandBuffer, target: PingPongRenderToTexture, output: VulkanBuffer) {
        MemoryStack.stackPush().use { stack ->
            val range = VkImageSubresourceRange.calloc(stack).aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
                .baseMipLevel(0).levelCount(1).baseArrayLayer(0).layerCount(LAYERS)
            for (a in 0 until target.colorAttachmentCount) for (i in 0 until target.frameCount) {
                val image = target.frameImageView(i, a).imageView.image
                image_layout_transition(command.vkCommandBuffer, image,
                    VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
                    VK_ACCESS_2_MEMORY_READ_BIT or VK_ACCESS_2_MEMORY_WRITE_BIT, VK_ACCESS_2_TRANSFER_READ_BIT,
                    VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, range)
                vkCmdCopyImageToBuffer(command.vkCommandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                    output.buffer, region(stack, (a * target.frameCount + i) * BYTES.toLong()))
                image_layout_transition(command.vkCommandBuffer, image,
                    VK_PIPELINE_STAGE_2_COPY_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
                    VK_ACCESS_2_TRANSFER_READ_BIT, VK_ACCESS_2_MEMORY_READ_BIT or VK_ACCESS_2_MEMORY_WRITE_BIT,
                    VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, range)
            }
        }
    }

fun main(args: Array<String>)

body — 97 lines, from line 105
    @JvmStatic
    fun main(args: Array<String>) {
        Options.doVulkanSDK()
        Options.enableVR = false
        Options.validate = true
        val window = Window("Ping-pong history restore")
        val renderer = Renderer(window)
        try {
            check(renderer.instance.validationActive) { "This regression requires Vulkan validation" }
            check(runCatching { renderer.whenCurrentGraphicsFrameComplete {} }.exceptionOrNull() is IllegalStateException)
            val ordinary = WatchedRing(renderer)
            val cadenced = FrameCadencedPingPongRenderToTexture(renderer, 4, 3, setup)
            val patterns = listOf(pattern(3), pattern(73), pattern(149))
            val uploads = patterns.map { bytes ->
                VulkanBuffer(renderer, BYTES.toLong(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
                    VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_COHERENT_BIT).also {
                    it.mapAndWrite_bytes { put(bytes) }
                }
            }
            val readback = VulkanBuffer(renderer, BYTES * 6L, VK_BUFFER_USAGE_TRANSFER_DST_BIT,
                VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_CACHED_BIT)
            var action: ((CommandBuffer, Int) -> Unit)? = null
            renderer.renderTargetFor().preTargets["restore-test"] = Target { command, index -> action?.invoke(command, index) }
            fun frame() { window.pollEvents(); renderer.render() }
            fun await(done: CompletableFuture<Unit>) {
                val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(20)
                while (!done.isDone && System.nanoTime() < deadline) frame()
                done.get(1, TimeUnit.SECONDS)
            }
            fun upload(command: CommandBuffer, image: Long, which: Int) {
                MemoryStack.stackPush().use { stack ->
                    vkCmdCopyBufferToImage(command.vkCommandBuffer, uploads[which].buffer, image,
                        VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, region(stack))
                }
            }
            for (target in listOf(ordinary, cadenced)) {
                action = { command, index -> target.render(command, index) }
                repeat(5) { frame() }
                val identities = views(target)
                val bound = ordinary.boundImage()
                val hooks = ordinary.renderHooks
                val cadence = cadenced.pingPongCount to cadenced.framesSincePingPong
                var done = CompletableFuture<Unit>()
                action = { command, _ ->
                    action = null
                    target.recordReplaceHistory(command, 0) { upload(command, it, 0) }
                    target.recordReplaceHistory(command, 1) { upload(command, it, 1) }
                    target.recordReplaceHistory(command, 0) { upload(command, it, 2) }
                    check(views(target) == identities) { "restore rotated a public view" }
                    check(ordinary.boundImage() == bound && ordinary.renderHooks == hooks) { "restore ran render hooks or changed the bound image" }
                    check(cadenced.pingPongCount to cadenced.framesSincePingPong == cadence) { "restore advanced cadence" }
                    check(runCatching { target.recordReplaceHistory(command, 2) {} }.exceptionOrNull() is IllegalArgumentException)
                    readRing(command, target, readback)
                    renderer.whenCurrentGraphicsFrameComplete { done.complete(Unit) }
                    check(!done.isDone) { "completion ran while commands were still recording" }
                }
                await(done)
                fun verify() = readback.mapAndRead_bytes {
                    for (a in 0..1) for (i in 0..2) for (b in 0 until BYTES) {
                        val expected = patterns[if (a == 0) 2 else 1][b]
                        check(get((a * 3 + i) * BYTES + b) == expected) { "wrong byte at attachment $a slot $i byte $b" }
                    }
                }
                verify()
                // Normal renders must retain the replacement and use the correct tracked layouts.
                action = { command, index -> target.render(command, index) }
                repeat(13) { frame() }
                done = CompletableFuture()
                action = { command, _ ->
                    action = null
                    readRing(command, target, readback)
                    renderer.whenCurrentGraphicsFrameComplete { done.complete(Unit) }
                }
                await(done)
                verify()
                println("${target.javaClass.simpleName}: all slots, attachments and layers restored; cadence retained")
            }
            // Cleanup retires submitted callbacks even if there will be no next render.
            val finalCompletion = CompletableFuture<Unit>()
            var finalRecorded = false
            action = { _, _ ->
                action = null
                renderer.whenCurrentGraphicsFrameComplete { finalCompletion.complete(Unit) }
                finalRecorded = true
            }
            val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(20)
            while (!finalRecorded && System.nanoTime() < deadline) frame()
            check(finalRecorded) { "The final frame was never recorded" }
            renderer.cleanup()
            check(finalCompletion.isDone) { "The final submitted capture was not completed at shutdown" }
            println("ping-pong history restore PASSED")
        } finally {
            try { renderer.cleanup() } finally { window.cleanup() }
        }
        exitProcess(0)
    }
}