Skip to content

FourVisualizers.kt

src/jvmMain/kotlin/afield/graphics/render/FourVisualizers.kt · 947 lines

package and imports — 45 lines, from line 1
package afield.graphics.render

import afield.graphics.vulkan.CaptureOrientation
import afield.graphics.vulkan.Target

import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.target

import afield.graphics.vulkan.device.vkLayout
import afield.runtime.lifecycle.RunsAt
import afield.ui.visualization.H2ValueSnapshot
import afield.runtime.lifecycle.newRoot
import afield.ui.visualization.registerH2Value
import afield.ui.visualization.facts
import afield.ui.visualization.registerValuePrinter
import afield.graphics.vulkan.buffers.*
import afield.graphics.vulkan.device.*
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.vulkan.targets.MultiPingPongRenderToTexture
import afield.graphics.vulkan.targets.RTTParameters
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.texture.Image
import afield.graphics.texture.ImageView
import afield.graphics.texture.LoadedTexture
import afield.graphics.texture.StorageImage
import afield.graphics.texture.image_layout_transition
import afield.graphics.vulkan.util.Camera
import afield.graphics.vulkan.util.SuperProjection
import afield.graphics.vulkan.util.Window
import afield.media.image.util.PngEncoder
import afield.ui.visualization.h
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.VK10
import org.lwjgl.vulkan.VK13.*
import org.lwjgl.vulkan.VkBufferImageCopy
import org.lwjgl.vulkan.VkImageBlit
import org.lwjgl.vulkan.VkImageSubresourceRange
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import afield.ui.visualization.Table
import afield.ui.visualization.CellRole

Inline value printers for the Vulkan graphics types, so a renderer, buffer or texture shows as a readable table when a box prints it.

registerFourVisualizers runs at each new root and registers a printer for Renderer, Camera, Window, the device and swapchain types, images and buffers, uniform Structures, the module and module-pointer types, Mesh, render-to-texture targets, LoadedTexture and StorageImage. Each printer renders a small coloured table of the object's state: frame counter and upload bytes for a renderer, extents and formats for images, field values for a structure. TexturePreviewCache holds the downscaled previews shown beside texture values.

private const val H2_FRAMEBUFFERGROUP_MAX_FLOATS = 65_536

tiny helpers

source — 145 lines, from line 63
private fun esc(s: String) = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

private fun formatBytes(bytes: Long): String = when {
    bytes < 0 -> "?"
    bytes < 1024 -> "$bytes B"
    bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0)
    bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024))
    else -> "%.2f GB".format(bytes / (1024.0 * 1024 * 1024))
}

private fun frameBufferGroupH2Snapshot(fb: FrameBufferGroup): H2ValueSnapshot {
    if (fb.buffers.isEmpty()) {
        return H2ValueSnapshot(
            value = FloatArray(0),
            summary = "FrameBufferGroup (empty)",
            title = "FrameBufferGroup floats"
        )
    }

    val buffer = fb.getLastWritten()
    val totalFloats = (buffer.requestedSize / Float.SIZE_BYTES)
        .coerceAtMost(Int.MAX_VALUE.toLong())
        .toInt()
    val snapshotFloats = minOf(totalFloats, H2_FRAMEBUFFERGROUP_MAX_FLOATS)
    val snapshot = FloatArray(snapshotFloats)

    val summary = buildString {
        append("FrameBufferGroup read ")
        append(snapshotFloats)
        append("/")
        append(totalFloats)
        append(" floats")
        if (snapshotFloats < totalFloats) append(" (truncated)")
        append(" | ")
        append(formatBytes(buffer.requestedSize))
        append(" | n=")
        append(fb.n)
        append(" | lastWritten=")
        append(if (fb.lastWritten < 0) "none" else fb.lastWritten.toString())
    }

    return try {
        if (snapshotFloats > 0) {
            buffer.mapAndRead {
                position(0)
                get(snapshot, 0, minOf(snapshot.size, remaining()))
            }
        }
        H2ValueSnapshot(
            value = snapshot,
            summary = summary,
            title = "FrameBufferGroup floats"
        )
    } catch (e: Throwable) {
        H2ValueSnapshot(
            value = listOf("error" to (e.message ?: e::class.java.simpleName)),
            summary = "FrameBufferGroup read failed: ${e.message ?: e::class.java.simpleName}",
            title = "FrameBufferGroup read error"
        )
    }
}

private fun hex(v: Long) = "0x${java.lang.Long.toHexString(v)}"

private fun vkFormatName(fmt: Int): String = when (fmt) {
    VK_FORMAT_UNDEFINED -> "UNDEFINED"
    VK_FORMAT_R8_UNORM -> "R8"
    VK_FORMAT_R8G8_UNORM -> "RG8"
    VK_FORMAT_R8G8B8_UNORM -> "RGB8"
    VK_FORMAT_R8G8B8A8_UNORM -> "RGBA8"
    VK_FORMAT_R8G8B8A8_SRGB -> "RGBA8_SRGB"
    VK_FORMAT_B8G8R8A8_UNORM -> "BGRA8"
    VK_FORMAT_B8G8R8A8_SRGB -> "BGRA8_SRGB"
    VK_FORMAT_R16_SFLOAT -> "R16F"
    VK_FORMAT_R16G16_SFLOAT -> "RG16F"
    VK_FORMAT_R16G16B16A16_SFLOAT -> "RGBA16F"
    VK_FORMAT_R32_SFLOAT -> "R32F"
    VK_FORMAT_R32G32_SFLOAT -> "RG32F"
    VK_FORMAT_R32G32B32_SFLOAT -> "RGB32F"
    VK_FORMAT_R32G32B32A32_SFLOAT -> "RGBA32F"
    VK_FORMAT_R32_SINT -> "R32I"
    VK_FORMAT_R32G32_SINT -> "RG32I"
    VK_FORMAT_R32G32B32A32_SINT -> "RGBA32I"
    VK_FORMAT_R32_UINT -> "R32U"
    VK_FORMAT_R32G32_UINT -> "RG32U"
    VK_FORMAT_R32G32B32A32_UINT -> "RGBA32U"
    VK_FORMAT_R16_UNORM -> "R16"
    VK_FORMAT_R16G16_UNORM -> "RG16"
    VK_FORMAT_R16G16B16A16_UNORM -> "RGBA16"
    VK_FORMAT_D16_UNORM -> "D16"
    VK_FORMAT_D32_SFLOAT -> "D32F"
    VK_FORMAT_D24_UNORM_S8_UINT -> "D24S8"
    VK_FORMAT_D32_SFLOAT_S8_UINT -> "D32FS8"
    VK_FORMAT_R8_UINT -> "R8U"
    VK_FORMAT_R16_UINT -> "R16U"
    VK_FORMAT_R16_SINT -> "R16I"
    VK_FORMAT_R8G8B8A8_UINT -> "RGBA8U"
    VK_FORMAT_A2B10G10R10_UNORM_PACK32 -> "A2BGR10"
    VK_FORMAT_B10G11R11_UFLOAT_PACK32 -> "B10G11R11F"
    VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 -> "E5BGR9F"
    else -> "fmt#$fmt"
}

private fun bufferUsageFlags(usage: Int): String {
    val flags = mutableListOf<String>()
    if (usage and VK_BUFFER_USAGE_TRANSFER_SRC_BIT != 0) flags += "xferSrc"
    if (usage and VK_BUFFER_USAGE_TRANSFER_DST_BIT != 0) flags += "xferDst"
    if (usage and VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT != 0) flags += "UBO"
    if (usage and VK_BUFFER_USAGE_STORAGE_BUFFER_BIT != 0) flags += "SSBO"
    if (usage and VK_BUFFER_USAGE_INDEX_BUFFER_BIT != 0) flags += "index"
    if (usage and VK_BUFFER_USAGE_VERTEX_BUFFER_BIT != 0) flags += "vertex"
    if (usage and VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT != 0) flags += "indirect"
    if (usage and VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT != 0) flags += "devAddr"
    return if (flags.isEmpty()) hex(usage.toLong()) else flags.joinToString("+")
}

private fun memoryPropertyFlags(flags: Int, cpuOnly: Boolean = false): String {
    if (cpuOnly) return badge("#af6", "CPU_ONLY")
    if (flags == 0) return dim("auto")
    val names = mutableListOf<String>()
    if (flags and VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT != 0) names += "device"
    if (flags and VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT != 0) names += "host"
    if (flags and VK_MEMORY_PROPERTY_HOST_COHERENT_BIT != 0) names += "coherent"
    if (flags and VK_MEMORY_PROPERTY_HOST_CACHED_BIT != 0) names += "cached"
    if (flags and VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT != 0) names += "lazy"
    if (flags and VK_MEMORY_PROPERTY_PROTECTED_BIT != 0) names += "protected"
    return if (names.isEmpty()) hex(flags.toLong()) else names.joinToString("+")
}

private fun primitiveTopologyName(topo: Int): String = when (topo) {
    VK_PRIMITIVE_TOPOLOGY_POINT_LIST -> "points"
    VK_PRIMITIVE_TOPOLOGY_LINE_LIST -> "lines"
    VK_PRIMITIVE_TOPOLOGY_LINE_STRIP -> "lineStrip"
    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST -> "triangles"
    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP -> "triStrip"
    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN -> "triFan"
    else -> "topo#$topo"
}

private fun fillModeName(mode: Int): String = when (mode) {
    VK_POLYGON_MODE_FILL -> "fill"
    VK_POLYGON_MODE_LINE -> "wireframe"
    VK_POLYGON_MODE_POINT -> "point"
    else -> "fill#$mode"
}

html micro-DSL

private const val S = "style='font-family:\"Iosevka Web\",monospace;font-size:12px;color:#ccc;line-height:1.4'"
private const val CLR_CLASS = "#cc8"
private const val CLR_NUM = "#8cf"
private const val CLR_STR = "#8f8"
private const val CLR_BOOL = "#f88"
private const val CLR_DIM = "#888"
private const val CLR_PUNCT = "#666"
private const val CLR_ORANGE = "#fc8"

private fun span(color: String, text: String) = "<span style='color:$color'>$text</span>"
private fun cls(name: String) = span(CLR_CLASS, name)
private fun num(v: Any) = span(CLR_NUM, "$v")
private fun str(v: String) = span(CLR_STR, "\"${esc(v)}\"")
private fun bool(v: Boolean) = span(CLR_BOOL, "$v")
private fun dim(v: String) = span(CLR_DIM, v)
private fun punct(v: String) = span(CLR_PUNCT, v)

User-typed name (uniform, buffer, box, mesh slot) — single-quoted, green

private fun name(v: String) = span(CLR_STR, "'${esc(v)}'")

Format a vec3 compactly

private fun vec3(v: afield.math.linalg.Vector3f) = "${span(CLR_NUM, "%.2f".format(v.x))}, ${span(CLR_NUM, "%.2f".format(v.y))}, ${span(CLR_NUM, "%.2f".format(v.z))}"

row and table now build a facts Table rather than <tr><td style=…> by hand. Their signatures changed from String to Pair, which is what lets all ~120 row("label", value) call sites in this file stay exactly as they were: a conditional row that used to collapse to "" is now null, which facts drops.

private fun row(label: String, value: String): Pair<String, String> = label to value

The values in this file are already HTML — num(), badge(), cls(), vec3() all return coloured spans — so the value column is formatted rather than escaped. format output is trusted, which is the documented way to say "this cell is markup". Labels stay escaped: they are plain text.

private fun table(vararg rows: Pair<String, String>?) =
    facts(rows.asList()).format("value") { it?.toString() ?: "" }.toHTML()

private fun badge(color: String, text: String) =
    "<span style='background:${color}22;color:$color;padding:0 4px;border-radius:3px;font-size:11px'>$text</span>"

Format a list of user-typed keys (truncated if > maxItems)

private fun nameList(keys: Collection<String>, maxItems: Int = 8): String {
    if (keys.isEmpty()) return dim("none")
    val shown = keys.take(maxItems)
    val items = shown.joinToString(punct(", ")) { name(it) }
    return if (keys.size > maxItems) "$items " + dim("…${keys.size - maxItems} more")
    else items
}

private fun shaderBadges(
    vertex: String = "", fragment: String = "", compute: String = "",
    mesh: String = "", task: String = ""
): String {
    val b = mutableListOf<String>()
    if (vertex.isNotBlank()) b += badge("#6cf", "VS")
    if (fragment.isNotBlank()) b += badge("#f8a", "FS")
    if (compute.isNotBlank()) b += badge("#af6", "CS")
    if (mesh.isNotBlank()) b += badge("#fc6", "MS")
    if (task.isNotBlank()) b += badge("#c8f", "TS")
    return b.joinToString(" ")
}

texture preview cache & readback

private data class CachedPreview(val base64: String, val width: Int, val height: Int, val frameStamp: Long)

object TexturePreviewCache

Thread-safe cache of downsampled texture previews keyed by VkImage handle. Call invalidate when an image is destroyed or recreated.

object TexturePreviewCache {
    private val cache = ConcurrentHashMap<Long, CachedPreview>()
    private const val MAX_DIM = 128

fun invalidate(vkImage: Long) { cache.remove(vkImage) }

fun clear() { cache.clear() }

fun getCachedImgTag(vkImage: Long): String?

Return cached <img> tag HTML, or null.

    fun getCachedImgTag(vkImage: Long): String? {
        val c = cache[vkImage] ?: return null
        return imgTag(c)
    }

    private fun imgTag(c: CachedPreview) =
        "<img src='data:image/png;base64,${c.base64}' width='${c.width}' height='${c.height}' " +
        "style='image-rendering:pixelated;border:1px solid #444;margin:2px 0;display:block'/>"

fun capture

Synchronous one-shot GPU readback of vkImage into a small PNG preview.

Requirements: - The image's usage flags MUST include VK_IMAGE_USAGE_TRANSFER_SRC_BIT. - currentLayout must be the image's current layout (e.g. SHADER_READ_ONLY, COLOR_ATTACHMENT_OPTIMAL). - Blocks the calling thread until GPU work completes (fence wait).

Returns an <img> HTML tag or null on failure.

body — 198 lines, from line 306
    fun capture(
        renderer: Renderer,
        vkImage: Long,
        format: Int,
        srcWidth: Int,
        srcHeight: Int,
        currentLayout: Int,
        maxDim: Int = MAX_DIM
    ): String? {
        if (srcWidth <= 0 || srcHeight <= 0) return null
        // Compute thumbnail dimensions maintaining aspect ratio
        val scale = maxDim.toFloat() / maxOf(srcWidth, srcHeight).coerceAtLeast(1)
        val thumbW: Int
        val thumbH: Int
        if (scale >= 1f) {
            thumbW = srcWidth; thumbH = srcHeight
        } else {
            thumbW = (srcWidth * scale).toInt().coerceAtLeast(1)
            thumbH = (srcHeight * scale).toInt().coerceAtLeast(1)
        }

        val needsResize = thumbW != srcWidth || thumbH != srcHeight
        val directCopyFormat = when (format) {
            VK_FORMAT_R8G8B8A8_UNORM,
            VK_FORMAT_R8G8B8A8_SRGB,
            VK_FORMAT_B8G8R8A8_UNORM,
            VK_FORMAT_B8G8R8A8_SRGB -> true
            else -> false
        }
        val displayEncodedSource = when (format) {
            VK_FORMAT_R8G8B8A8_SRGB,
            VK_FORMAT_B8G8R8A8_SRGB,
            VK_FORMAT_R16G16B16A16_SFLOAT,
            VK_FORMAT_R32G32B32A32_SFLOAT -> true
            else -> false
        }
        val needsBlit = needsResize || !directCopyFormat
        // Target format for readback. HDR/float and sRGB sources become display-encoded
        // RGBA8 so PNG previews match the swapchain path instead of storing linear bytes.
        val readbackFormat = if (displayEncodedSource) VK_FORMAT_R8G8B8A8_SRGB else VK_FORMAT_R8G8B8A8_UNORM
        val bpp = 4
        val stagingSize = thumbW.toLong() * thumbH.toLong() * bpp

        // Use a private command pool so we're thread-safe
        val pool = CommandPool(renderer.device, renderer.graphQueue.queueFamilyIndex)
        val cmd = CommandBuffer(pool, primary = true, oneTimeSubmit = true)
        val staging = VulkanBuffer(
            renderer = renderer,
            requestedSize = stagingSize,
            usage = VK10.VK_BUFFER_USAGE_TRANSFER_DST_BIT,
            reqMask = VK10.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK10.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
        )
        val fence = Fence(renderer.device, signaled = false)

        // Optional intermediate blit image when downsampling
        var blitImage: Image? = null
        var blitImageView: ImageView? = null

        try {
            cmd.beginRecording()
            MemoryStack.stackPush().use { stack ->
                val range = VkImageSubresourceRange.calloc(stack)
                    .aspectMask(VK10.VK_IMAGE_ASPECT_COLOR_BIT)
                    .baseMipLevel(0).levelCount(1).baseArrayLayer(0).layerCount(1)

                // Transition source image to TRANSFER_SRC
                image_layout_transition(
                    cmd.vkCommandBuffer, vkImage,
                    VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
                    VK_ACCESS_2_MEMORY_READ_BIT or VK_ACCESS_2_MEMORY_WRITE_BIT,
                    VK_ACCESS_2_TRANSFER_READ_BIT,
                    currentLayout, VK10.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, range
                )

                val copySourceImage: Long
                val copySourceLayout: Int

                if (needsBlit) {
                    // Create a temporary RGBA8 image for resizing and/or display-format conversion.
                    val blitData = Image.ImageData()
                        .width(thumbW).height(thumbH)
                        .format(readbackFormat)
                        .mipLevels(1)
                        .usage(VK10.VK_IMAGE_USAGE_TRANSFER_SRC_BIT or VK10.VK_IMAGE_USAGE_TRANSFER_DST_BIT)
                    blitImage = Image(renderer.device, blitData)

                    // Transition blit image to TRANSFER_DST
                    image_layout_transition(
                        cmd.vkCommandBuffer, blitImage.vkImage,
                        VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
                        0, VK_ACCESS_2_TRANSFER_WRITE_BIT,
                        VK10.VK_IMAGE_LAYOUT_UNDEFINED, VK10.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, range
                    )

                    // Blit (downsample)
                    val blit = VkImageBlit.calloc(1, stack)
                    blit[0]
                        .srcSubresource { it.aspectMask(VK10.VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(1) }
                        .dstSubresource { it.aspectMask(VK10.VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(1) }
                    blit[0].srcOffsets(0).set(0, 0, 0)
                    blit[0].srcOffsets(1).set(srcWidth, srcHeight, 1)
                    blit[0].dstOffsets(0).set(0, 0, 0)
                    blit[0].dstOffsets(1).set(thumbW, thumbH, 1)

                    VK10.vkCmdBlitImage(
                        cmd.vkCommandBuffer,
                        vkImage, vkLayout(VK10.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),
                        blitImage.vkImage, vkLayout(VK10.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),
                        blit, VK10.VK_FILTER_LINEAR
                    )

                    // Transition blit image to TRANSFER_SRC for the copy-to-buffer
                    image_layout_transition(
                        cmd.vkCommandBuffer, blitImage.vkImage,
                        VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
                        VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_TRANSFER_READ_BIT,
                        VK10.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK10.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, range
                    )

                    copySourceImage = blitImage.vkImage
                    copySourceLayout = VK10.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
                } else {
                    copySourceImage = vkImage
                    copySourceLayout = VK10.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL
                }

                // Copy image to staging buffer
                val region = VkBufferImageCopy.calloc(1, stack)
                region[0]
                    .bufferOffset(0).bufferRowLength(0).bufferImageHeight(0)
                    .imageSubresource { it.aspectMask(VK10.VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(1) }
                    .imageOffset { it.set(0, 0, 0) }
                    .imageExtent { it.set(thumbW, thumbH, 1) }

                VK10.vkCmdCopyImageToBuffer(
                    cmd.vkCommandBuffer,
                    copySourceImage, vkLayout(copySourceLayout),
                    staging.buffer, region
                )

                // Transition source image back to its original layout
                image_layout_transition(
                    cmd.vkCommandBuffer, vkImage,
                    VK_PIPELINE_STAGE_2_TRANSFER_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,
                    VK10.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, currentLayout, range
                )
            }
            cmd.endRecording()

            // Submit and wait
            MemoryStack.stackPush().use { stack ->
                val pCmd = stack.mallocPointer(1).put(0, cmd.vkCommandBuffer)
                renderer.graphQueue.submit(pCmd, null, null, null, fence)
            }
            fence.fenceWait()

            // Read pixels from staging buffer
            var result: String? = null
            staging.mapAndRead_bytes {
                // Handle BGR→RGB swap for BGRA source formats
                val isBGR = format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB
                if (isBGR && !needsBlit) {
                    // When we blit to RGBA8, the GPU already converts. But when copying directly
                    // from a BGRA source without blit, we need to swap B and R channels.
                    val pixelCount = thumbW * thumbH
                    for (i in 0 until pixelCount) {
                        val off = i * 4
                        val b = this[off]
                        this.put(off, this[off + 2])
                        this.put(off + 2, b)
                    }
                }
                // vkCmdCopyImageToBuffer hands rows over top-to-bottom, and the optional
                // downsampling blit above has srcOffsets(0) == dstOffsets(0) == (0,0,0), so it
                // does not invert Y either. Preserve the order — see CaptureOrientation.
                val png = PngEncoder.encodeFromBuffer(this, thumbW, thumbH, flipY = false)
                result = Base64.getEncoder().encodeToString(png)
            }

            if (result != null) {
                val cached = CachedPreview(result, thumbW, thumbH, System.nanoTime())
                cache[vkImage] = cached
                return imgTag(cached)
            }
            return null
        } catch (e: Exception) {
            return null
        } finally {
            fence.cleanup()
            staging.cleanup()
            cmd.cleanup()
            pool.cleanup()
            blitImage?.cleanup()
        }
    }
}

Attempt to read a LoadedTexture preview by re-reading from the file on disk. No GPU access needed — just STB decode + downsample + PNG encode.

source — 44 lines, from line 509
private fun loadedTextureFilePreview(lt: LoadedTexture, maxDim: Int = 128): String? {
    return try {
        val path = java.io.File(lt.fileName)
        if (!path.exists() || !path.isFile) return null
        val bytes = path.readBytes()
        val buf = org.lwjgl.system.MemoryUtil.memAlloc(bytes.size)
        buf.put(bytes).flip()
        org.lwjgl.system.MemoryStack.stackPush().use { stack ->
            val w = stack.mallocInt(1)
            val h = stack.mallocInt(1)
            val ch = stack.mallocInt(1)
            val decoded = org.lwjgl.stb.STBImage.stbi_load_from_memory(buf, w, h, ch, 4) ?: return null
            try {
                val srcW = w[0]; val srcH = h[0]
                val scale = maxDim.toFloat() / maxOf(srcW, srcH).coerceAtLeast(1)
                val thumbW: Int; val thumbH: Int
                if (scale >= 1f) { thumbW = srcW; thumbH = srcH }
                else { thumbW = (srcW * scale).toInt().coerceAtLeast(1); thumbH = (srcH * scale).toInt().coerceAtLeast(1) }

                // Simple bilinear downsample
                val pixels = IntArray(thumbW * thumbH)
                for (ty in 0 until thumbH) {
                    for (tx in 0 until thumbW) {
                        val sx = (tx.toFloat() / thumbW * srcW).toInt().coerceIn(0, srcW - 1)
                        val sy = (ty.toFloat() / thumbH * srcH).toInt().coerceIn(0, srcH - 1)
                        val off = (sy * srcW + sx) * 4
                        val r = decoded[off].toInt() and 0xFF
                        val g = decoded[off + 1].toInt() and 0xFF
                        val b = decoded[off + 2].toInt() and 0xFF
                        val a = decoded[off + 3].toInt() and 0xFF
                        pixels[tx + ty * thumbW] = (a shl 24) or (r shl 16) or (g shl 8) or b
                    }
                }
                val png = PngEncoder.encode(pixels, thumbW, thumbH)
                val b64 = Base64.getEncoder().encodeToString(png)
                "<img src='data:image/png;base64,$b64' width='$thumbW' height='$thumbH' " +
                "style='image-rendering:pixelated;border:1px solid #444;margin:2px 0;display:block'/>"
            } finally {
                org.lwjgl.stb.STBImage.stbi_image_free(decoded)
            }
        }
    } catch (_: Exception) { null }
    finally {}
}

registrations

fun registerFourVisualizers()

body — 391 lines, from line 556
@RunsAt(newRoot)
fun registerFourVisualizers() {

    // ── Renderer ─────────────────────────────────────────────────────
    registerValuePrinter<Renderer> { r ->
        val deviceName = try { r.physicalDevice.deviceName } catch (_: Exception) { "?" }
        val windowCount = r.windows.size
        val stats = r.lastVertexUploadFrameStats
        val sc = try { r.swapChain } catch (_: Exception) { null }
        table(
            row("Renderer", cls("Renderer") + " " + dim("frame ${r.frameCounter}")),
            row("device", str(deviceName)),
            row("windows", num(windowCount)),
            row("swapchain", "${num(r.numImages)} images, current=${num(r.currentImage)}"),
            sc?.let { row("extent", "${num(it.swapChainExtent.width())}×${num(it.swapChainExtent.height())}") },
            row("upload", "${formatBytes(stats.totalBytes)} " + dim("(rec ${formatBytes(stats.recordedBytes)} + imm ${formatBytes(stats.immediateBytes)})"))
        )
    }

    // ── Camera ───────────────────────────────────────────────────────
    registerValuePrinter<Camera> { cam ->
        val dist = "%.2f".format(cam.distanceToTarget())
        val proj = cam.p
        table(
            row("Camera", cls("Camera")),
            row("at", vec3(cam.at)),
            row("target", vec3(cam.target)),
            row("up", vec3(cam.up)),
            row("distance", num(dist)),
            row("fov", num("%.1f°".format(Math.toDegrees(proj.fov)))),
            row("near / far", "${num("%.3f".format(proj.zNear))} / ${num("%.1f".format(proj.zFar))}"),
            if (proj.orthoness > 0.0) row("orthoness", num("%.2f".format(proj.orthoness))) else null,
            if (proj.ox != 0.0 || proj.oy != 0.0) row("offset", "${num("%.2f".format(proj.ox))}, ${num("%.2f".format(proj.oy))}") else null
        )
    }

    // ── Window ───────────────────────────────────────────────────────
    registerValuePrinter<Window> { w ->
        table(
            row("Window", cls("Window") + " " + str(w.title)),
            row("size", "${num(w.width)}×${num(w.height)}" + if (w.isResized) " " + badge("#f88", "resized") else ""),
            row("cursor", span(CLR_ORANGE, "%.1f, %.1f".format(w.cursor.x, w.cursor.y)))
        )
    }

    // ── Device ───────────────────────────────────────────────────────
    registerValuePrinter<Device> { d ->
        val flags = mutableListOf<String>()
        if (d.supportsPushDescriptors) flags += "pushDesc"
        if (d.supportsExtendedDynamicState) flags += "dynState"
        if (d.supportsExtendedDynamicState3) flags += "dynState3"
        if (d.enableMeshShading) flags += "meshShading"
        table(
            row("Device", cls("Device")),
            row("features", if (flags.isEmpty()) dim("none") else flags.joinToString(" ") { badge("#6cf", it) }),
            row("pushDescMax", num(d.pushDescriptorMax))
        )
    }

    // ── PhysicalDevice ───────────────────────────────────────────────
    registerValuePrinter<PhysicalDevice> { pd ->
        table(
            row("PhysicalDevice", cls("PhysicalDevice")),
            row("name", str(pd.deviceName)),
            row("memoryTypes", num(pd.vkMemoryProperties.memoryTypeCount())),
            row("memoryHeaps", num(pd.vkMemoryProperties.memoryHeapCount())),
            row("queueFamilies", num(pd.vkQueueFamilyProps.remaining()))
        )
    }

    // ── SwapChain ────────────────────────────────────────────────────
    registerValuePrinter<SwapChain> { sc ->
        table(
            row("SwapChain", cls("SwapChain")),
            row("format", badge("#fc8", vkFormatName(sc.surfaceFormat.imageFormat))),
            row("extent", "${num(sc.swapChainExtent.width())}×${num(sc.swapChainExtent.height())}"),
            row("images", num(sc.imageViews.size)),
            row("transferSrc", bool(sc.supportsTransferSrc))
        )
    }

    // ── Image ────────────────────────────────────────────────────────
    registerValuePrinter<Image> { img ->
        val d = img.imageData
        table(
            row("Image", cls("Image") + " " + badge("#fc8", vkFormatName(d.format))),
            row("size", "${num(d.width)}×${num(d.height)}"),
            row("mips", num(d.mipLevels)),
            row("layers", num(d.arrayLayers)),
            row("samples", num(d.sampleCount))
        )
    }

    // ── ImageView ────────────────────────────────────────────────────
    registerValuePrinter<ImageView> { iv ->
        val d = iv.imageViewData
        table(
            row("ImageView", cls("ImageView") + " " + badge("#fc8", vkFormatName(d.format))),
            row("mips", num(d.mipLevels)),
            row("layers", "${num(d.layerCount)} " + dim("base=${d.baseArrayLayer}"))
        )
    }

    // ── VulkanBuffer ─────────────────────────────────────────────────
    registerValuePrinter<VulkanBuffer> { vb ->
        val usageStr = bufferUsageFlags(vb.usage)
        val requestedMemory = memoryPropertyFlags(vb.reqMask, vb.cpuOnly)
        val actualMemory = memoryPropertyFlags(vb.allocationMemoryFlags, vb.cpuOnly)
        val memoryType = if (vb.allocationMemoryType >= 0) " " + dim("#${vb.allocationMemoryType}") else ""
        table(
            row("VulkanBuffer", cls("VulkanBuffer") + " " + badge("#6cf", formatBytes(vb.requestedSize))),
            row("usage", usageStr),
            row("memory", "$requestedMemory ${punct("→")} $actualMemory$memoryType"),
            row("watermark", "${formatBytes(vb.watermark)} " + dim("/ ${formatBytes(vb.requestedSize)}")),
            if (vb.readWatermark >= 0 || vb.writeWatermark >= 0)
                row("rw marks", "r=${formatBytes(vb.readWatermark)}  w=${formatBytes(vb.writeWatermark)}")
            else null,
            if (vb.gpuAddress > 0) row("devAddr", bool(true)) else null,
            if (vb.cpuOnly) row("cpuOnly", bool(true)) else null,
            if (vb.zeroBeforeComputation) row("zeroBefore", bool(true)) else null
        )
    }

    // ── FrameBufferGroup ─────────────────────────────────────────────
    registerH2Value<FrameBufferGroup> { fb ->
        frameBufferGroupH2Snapshot(fb)
    }

    registerValuePrinter<FrameBufferGroup> { fb ->
        val sizes = fb.buffers.map { formatBytes(it.requestedSize) }
        val memories = fb.buffers.map { memoryPropertyFlags(it.allocationMemoryFlags, it.cpuOnly) }.distinct()
        val mode = fb.accessPattern.name.lowercase() +
                (if (fb.usesStagedUploads) " " + badge("#6cf", "staged write") else "") +
                (if (fb.usesReadbackStaging) " " + badge("#fc8", "readback") else "")
        table(
            row("FrameBufferGroup", cls("FrameBufferGroup") + " " + dim("n=${fb.n}")),
            row("mode", mode),
            row("lastWritten", if (fb.lastWritten < 0) dim("none") else num(fb.lastWritten)),
            row("buffers", if (fb.buffers.isEmpty()) dim("empty")
                    else sizes.joinToString(punct(", "))),
            row("memory", if (memories.isEmpty()) dim("none") else memories.joinToString(punct(", ")))
        )
    }

    // ── VertexBuffer ─────────────────────────────────────────────────
    registerValuePrinter<VertexBuffer> { vb ->
        val dirty = vb.watermark != vb.transferWatermark
        table(
            row("VertexBuffer", cls("VertexBuffer") + " " + badge("#6cf", formatBytes(vb.size))),
            row("usage", bufferUsageFlags(vb.usage)),
            row("watermark", "${num(vb.watermark)} " + if (dirty) badge("#f88", "dirty") else dim("clean")),
            if (vb.cpu != null) row("cpu", badge("#af6", "mapped")) else null
        )
    }

    // ── Structure ────────────────────────────────────────────────────
    registerValuePrinter<Structure> { s ->
        // maxRows replaces the hand-written "…N more" row, and the two markup columns are formatted
        // rather than escaped — the same opt-in `table()` above uses.
        val members = Table(listOf("offset", "member", "size", "bound"))
            .rows(
                s.members.map { m ->
                    listOf(
                        "+${s.offsets[m.name] ?: m.offset}",
                        name(m.name),
                        "${s.sizes[m.name] ?: m.memberSize().toInt()}B",
                        if (m.name in s.bindings) badge("#af6", "bound")
                        else if (m.name in s.values) badge("#fc8", "set")
                        else "",
                    )
                }
            )
            .role("offset", CellRole.DIM)
            .format("member") { it?.toString() ?: "" }
            .format("bound") { it?.toString() ?: "" }
        members.maxRows = 12
        val proxyNote = if (s.proxying != null) " → " + dim("proxied") else ""
        "<div $S>${cls("Structure")} ${dim("${s.members.size} members")}$proxyNote" +
        (if (s.dirty) " " + badge("#f88", "dirty") else "") +
        members.toHTML() +
        "</div>"
    }

    // ── Parameters ───────────────────────────────────────────────────
    registerValuePrinter<Parameters> { p ->
        table(
            row("Parameters", cls("Parameters") + " " + name(p.name)),
            row("shaders", shaderBadges(vertex = p.vertex, fragment = p.fragment, compute = p.compute, mesh = p.mesh, task = p.task)),
            row("topology", primitiveTopologyName(p.primitiveType)),
            row("fill", fillModeName(p.fillMode)),
            row("blend", bool(p.blend) + if (p.blend) " " + dim("src=${p.srcColor} dst=${p.dstColor}") else ""),
            row("depthTest", bool(p.depthTest)),
            row("colors", "${num(p.color)} " + if (p.colorFormats.isNotEmpty()) dim(p.colorFormats.joinToString(",") { vkFormatName(it) }) else ""),
            row("imageFormat", badge("#fc8", vkFormatName(p.imageFormat))),
            p.viewport?.let { row("viewport", "${num(it.x.toInt())},${num(it.y.toInt())} ${num(it.w.toInt())}×${num(it.h.toInt())}") }
        )
    }

    // ── Module ───────────────────────────────────────────────────────
    registerValuePrinter<Module> { m ->
        table(
            row("Module", cls("Module") + " " + name(m.parameters.name) +
                    (if (!m.valid) " " + badge("#f88", "INVALID") else "") +
                    (if (!m.enabled) " " + badge("#888", "disabled") else "")),
            row("shaders", shaderBadges(vertex = m.parameters.vertex, fragment = m.parameters.fragment)),
            row("meshes", nameList(m.meshes.keys)),
            row("uniforms", nameList(m.liveUniformStructures.keys)),
            if (m.pushConstantSize > 0) row("pushConst", "${num(m.pushConstantSize)}B") else null
        )
    }

    // ── ModulePointer ────────────────────────────────────────────────
    registerValuePrinter<ModulePointer> { mp -> mp.current.h }

    // ── MeshModule ───────────────────────────────────────────────────
    registerValuePrinter<MeshModule> { m ->
        table(
            row("MeshModule", cls("MeshModule") + " " + name(m.parameters.name) +
                    (if (!m.valid) " " + badge("#f88", "INVALID") else "") +
                    (if (!m.enabled) " " + badge("#888", "disabled") else "")),
            row("shaders", shaderBadges(fragment = m.parameters.fragment, mesh = m.parameters.mesh, task = m.parameters.task)),
            row("meshes", nameList(m.meshes.keys)),
            row("uniforms", nameList(m.liveUniformStructures.keys)),
            if (m.pushConstantSize > 0) row("pushConst", "${num(m.pushConstantSize)}B") else null
        )
    }

    // ── MeshModulePointer ────────────────────────────────────────────
    registerValuePrinter<MeshModulePointer> { mp -> mp.current.h }

    // ── ComputeModule ────────────────────────────────────────────────
    registerValuePrinter<ComputeModule> { m ->
        table(
            row("ComputeModule", cls("ComputeModule") + " " + name(m.parameters.name) +
                    (if (!m.valid) " " + badge("#f88", "INVALID") else "") +
                    (if (!m.enabled) " " + badge("#888", "disabled") else "")),
            row("shaders", shaderBadges(compute = m.parameters.compute)),
            row("tasks", nameList(m.meshes.keys)),
            row("uniforms", nameList(m.liveUniformStructures.keys)),
            if (m.pushConstantSize > 0) row("pushConst", "${num(m.pushConstantSize)}B") else null,
            row("zeroBefore", if (m.zeroBefore.isEmpty()) dim("none") else nameList(m.zeroBefore.keys))
        )
    }

    // ── ComputeModulePointer ─────────────────────────────────────────
    registerValuePrinter<ComputeModulePointer> { mp -> mp.current.h }

    // ── Mesh ─────────────────────────────────────────────────────────
    registerValuePrinter<Mesh> { m ->
        table(
            row("Mesh", cls("Mesh") + if (m.notInitialized) " " + badge("#f88", "uninit") else ""),
            row("vertices", num(m.numVert)),
            row("triangles", num(m.numTriangle)),
            row("instances", num(m.instanceCount)),
            row("inputs", nameList(m.vn.keys)),
            if (m.t != null) row("indexed", bool(true)) else null,
            if (m.push.members.isNotEmpty()) row("pushConst", "${num(m.push.members.size)} members") else null
        )
    }

    // ── Introspection ────────────────────────────────────────────────
    registerValuePrinter<Introspection> { intro ->
        if (!intro.valid) {
            cls("Introspection") + " " + badge("#f88", "INVALID")
        } else {
            val i = intro as Introspected
            val parts = mutableListOf<String>()
            if (i.inputs.isNotEmpty()) parts += "${i.inputs.size} inputs"
            if (i.outputs.isNotEmpty()) parts += "${i.outputs.size} outputs"
            if (i.ubos.isNotEmpty()) parts += "${i.ubos.size} UBOs"
            if (i.ssbo.isNotEmpty()) parts += "${i.ssbo.size} SSBOs"
            if (i.textures.isNotEmpty()) parts += "${i.textures.size} textures"
            if (i.images.isNotEmpty()) parts += "${i.images.size} images"
            if (i.pushConstants.isNotEmpty()) parts += "${i.pushConstants.size} push"
            cls("Introspection") + " " + dim(parts.joinToString(" · "))
        }
    }

    // ── RTTParameters ────────────────────────────────────────────────
    registerValuePrinter<RTTParameters> { p ->
        val fmtList = if (p.colorFormats.isNotEmpty()) p.colorFormats.joinToString(", ") { vkFormatName(it) }
                      else vkFormatName(p.format)
        table(
            row("RTTParameters", cls("RTTParameters")),
            row("size", "${num(p.width)}×${num(p.height)}"),
            row("mipLevels", num(p.mipLevels)),
            row("colorAttachments", num(p.colorAttachments) + " " + dim(fmtList)),
            row("layers", num(p.colorLayerCount)),
            row("depth", if (p.useDepth) bool(true) + " " + dim(vkFormatName(p.depthFormat)) else bool(false)),
            row("clear", if (p.clearFirst) bool(true) + " " + dim("(${p.clearColor})") else bool(false)),
            row("sampled", bool(p.colorSampled)),
            row("transferSrc", bool(p.colorTransferSrc)),
            row("transferDst", bool(p.colorTransferDst)),
            if (p.mipLevels > 1) row("genMipmaps", bool(p.generateColorMipmaps)) else null
        )
    }

    // ── RenderToTexture ──────────────────────────────────────────────
    registerValuePrinter<RenderToTexture> { rtt ->
        val p = rtt.parameters
        val fmts = rtt.colorFormats.joinToString(", ") { vkFormatName(it) }
        val hasTransferSrc = p.colorTransferSrc

        val preview = if (hasTransferSrc && rtt.colorImageRefs.isNotEmpty()) {
            val img = rtt.colorImageRefs[0]
            if (img != null) {
                // color attachment 0 is typically in SHADER_READ_ONLY after a frame, or COLOR_ATTACHMENT_OPTIMAL
                val layout = if (p.colorSampled) VK10.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
                             else VK10.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
                TexturePreviewCache.getCachedImgTag(img.vkImage)
                    ?: TexturePreviewCache.capture(rtt.renderer, img.vkImage, rtt.colorFormats[0], rtt.width, rtt.height, layout)
            } else null
        } else null

        val previewRow = if (preview != null) row("preview", preview)
                         else if (!hasTransferSrc) row("preview", dim("set colorTransferSrc=true to preview"))
                         else null

        table(
            row("RenderToTexture", cls("RenderToTexture") + " " + badge("#fc8", fmts)),
            row("size", "${num(rtt.width)}×${num(rtt.height)}" + if (p.mipLevels > 1) " " + dim("mip×${p.mipLevels}") else ""),
            row("attachments", num(p.colorAttachments) + " " + dim("layers=${rtt.colorLayerCount}")),
            row("depth", if (p.useDepth) bool(true) + " " + dim(vkFormatName(p.depthFormat)) else bool(false)),
            row("modules", nameList(rtt.modules.keys)),
            row("flags", listOfNotNull(
                if (p.colorSampled) "sampled" else null,
                if (hasTransferSrc) "xferSrc" else null,
                if (p.colorTransferDst) "xferDst" else null,
                if (p.clearFirst) "clear" else null
            ).joinToString(" ") { badge("#6cf", it) }.ifEmpty { dim("none") }),
            previewRow
        )
    }

    // ── MultiPingPongRenderToTexture ─────────────────────────────────
    registerValuePrinter<MultiPingPongRenderToTexture> { mpp ->
        val p = mpp.parameters
        val fmts = p.colorFormats.ifEmpty { listOf(p.format) }.joinToString(", ") { vkFormatName(it) }
        val hasTransferSrc = p.colorTransferSrc

        val preview = if (hasTransferSrc) {
            val iv = try { mpp.imageView } catch (_: Exception) { null }
            val img = iv?.underlyingImage
            if (img != null) {
                val layout = if (p.colorSampled) VK10.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
                             else VK10.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
                TexturePreviewCache.getCachedImgTag(img.vkImage)
                    ?: TexturePreviewCache.capture(mpp.renderer, img.vkImage, img.format, p.width, p.height, layout)
            } else null
        } else null

        val previewRow = if (preview != null) row("preview", preview)
                         else if (!hasTransferSrc) row("preview", dim("set colorTransferSrc=true to preview"))
                         else null

        table(
            row("MultiPingPongRTT", cls("MultiPingPongRTT") + " " + badge("#fc8", fmts)),
            row("size", "${num(p.width)}×${num(p.height)}" + if (p.mipLevels > 1) " " + dim("mip×${p.mipLevels}") else ""),
            row("passes", "${num(mpp.otherPassCount)} other " + dim("(init=${mpp.runInitPass}, final=${mpp.runFinalPass})")),
            row("flags", listOfNotNull(
                if (p.colorSampled) "sampled" else null,
                if (hasTransferSrc) "xferSrc" else null,
                if (p.colorTransferDst) "xferDst" else null
            ).joinToString(" ") { badge("#6cf", it) }.ifEmpty { dim("none") }),
            previewRow
        )
    }

    // ── LoadedTexture ────────────────────────────────────────────────
    registerValuePrinter<LoadedTexture> { lt ->
        val preview = loadedTextureFilePreview(lt)

        table(
            row("LoadedTexture", cls("LoadedTexture") + " " + str(lt.fileName.substringAfterLast('/'))),
            row("size", "${num(lt.width)}×${num(lt.height)}"),
            row("mips", num(lt.mipLevels)),
            row("format", badge("#fc8", vkFormatName(lt.image.format))),
            if (preview != null) row("preview", preview)
            else row("preview", dim("file not found"))
        )
    }

    // ── StorageImage ─────────────────────────────────────────────────
    registerValuePrinter<StorageImage> { si ->
        table(
            row("StorageImage", cls("StorageImage") + " " + badge("#fc8", vkFormatName(si.imageFormat))),
            row("size", "${num(si.width)}×${num(si.height)}"),
            row("preview", dim("storage-only (no TRANSFER_SRC)"))
        )
    }
}