RenderToTexture.kt
src/jvmMain/kotlin/afield/graphics/vulkan/targets/RenderToTexture.kt · 2007 lines
package afield.graphics.vulkan.targets
import afield.graphics.vulkan.device.vkLayout
import afield.app.protocol.Rect
import afield.runtime.lifecycle.Runloop
import afield.graphics.vulkan.GpuImageCapture
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.buffers.MissingRefException
import afield.graphics.render.Module
import afield.graphics.render.ModulePointer
import afield.graphics.render.MeshModulePointer
import afield.document.context.fromContext
import afield.graphics.render.pointer
import afield.graphics.texture.*
import afield.graphics.texture.Image.ImageData
import afield.graphics.texture.ImageView.ImageViewData
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.Window
import afield.math.linalg.Vec4
import afield.math.linalg.f
import afield.math.linalg.vec
import afield.math.linalg.Rectanglef
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.KHRDynamicRendering.VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR
import org.lwjgl.vulkan.KHRDynamicRendering.VK_STRUCTURE_TYPE_RENDERING_INFO_KHR
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK13.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
Offscreen rendering: RenderToTexture draws modules into its own color (and optional depth) images, which other modules then sample as textures.
A box beneath the renderer builds one with a configuration block over RTTParameters (size,
formats, attachment count, mip levels, clearing, depth), registers modules to draw into it, and
hands its image views to whatever samples the result. The same target can be blitted to the
window, read back to the CPU, or saved as a JPEG. PingPongRenderToTexture and
MultiPingPongRenderToTexture build feedback and multi-pass chains from it.
data class RTTParameters
Configuration for a RenderToTexture: size, formats, attachments, clearing and depth, set in its constructor block.
body — 100 lines, from line 47
data class RTTParameters(
/** Width of every attachment, in pixels. */
var width: Int = 1024,
/** Height of every attachment, in pixels. */
var height: Int = 1024,
/** Mip levels on the color attachments; more than one asks for [generateColorMipmaps]. */
var mipLevels: Int = 1,
/** The `VK_FORMAT_*` used for any color attachment [colorFormats] does not name. */
var format: Int = VK_FORMAT_B8G8R8A8_SRGB,
/** How many color attachments to create. */
var colorAttachments: Int = 1,
/** Per-attachment formats; empty uses [format] throughout, and a list must be [colorAttachments] long. */
var colorFormats: List<Int> = emptyList(),
/** Single layer count for all color attachments; 0 infers it from the external views. */
var colorLayerCount: Int = 1,
/** Expose color attachments as `sampler2DArray` even for a single layer. */
var forceColorArrayView: Boolean = false,
/** Optional viewport array override. */
var viewports: List<afield.app.protocol.Rect> = emptyList(),
/** Non-zero enables `VK_KHR_multiview` / dynamic-rendering multiview. */
var multiviewViewMask: Int = 0,
/** Optional externally-owned attachments, by slot. */
var externalColorViews: Map<Int, HasImageView> = emptyMap(),
/** The RGBA the color attachments are cleared to when [clearFirst]. */
var clearColor: Vec4 = vec(0, 0, 0, 1),
// --- Color QoL ---
/** Clear the color attachments at the start of the pass; false loads the previous contents instead. */
var clearFirst: Boolean = true,
/** The color attachments' `VK_ATTACHMENT_STORE_OP_*`. */
var colorStoreOp: Int = VK_ATTACHMENT_STORE_OP_STORE,
/** Whether the result will be sampled, which controls usage flags and the final transition. */
var colorSampled: Boolean = true,
/**
* Allow the produced color image to be used as a transfer source — mirroring, or copying into
* an OpenXR swapchain.
*/
var colorTransferSrc: Boolean = true,
/** Allow the produced color image to be used as a transfer destination. */
var colorTransferDst: Boolean = false,
/** Opt in when an external compute pass writes the color attachment as a storage image. */
var colorStorage: Boolean = false,
/** Generate the color mip chain automatically, when [mipLevels] is more than one. */
var generateColorMipmaps: Boolean = true,
/** Filter for the mip-generating blits; downgraded to `VK_FILTER_NEAREST` where linear is unsupported. */
var mipBlitFilter: Int = VK_FILTER_LINEAR,
// --- Depth/Stencil options ---
/** Create a depth (by default depth+stencil) attachment and depth-test against it. */
var useDepth: Boolean = true,
/** The depth attachment's `VK_FORMAT_*`; depth+stencil by default. */
var depthFormat: Int = Options.defaultDepthFormat,
/** The depth value cleared to when [depthLoadOp] clears. */
var clearDepth: Float = 1f,
/** The stencil value cleared to when [depthLoadOp] clears. */
var clearStencil: Int = 0,
/** The depth attachment's `VK_ATTACHMENT_LOAD_OP_*`. */
var depthLoadOp: Int = VK_ATTACHMENT_LOAD_OP_CLEAR,
/** The depth attachment's `VK_ATTACHMENT_STORE_OP_*`. */
var depthStoreOp: Int = VK_ATTACHMENT_STORE_OP_STORE,
/** Set true only if you intend to sample the depth later. */
var depthSampled: Boolean = false,
/**
* Name for this target in Vulkan debug labels; empty synthesizes one from a counter and the
* allocation site.
*/
var debugName: String = "",
/**
* Create color attachments with IOSurface-exportable backing (MoltenVK) so the target can be
* handed zero-copy to a hardware video encoder (field-encode). Opt-in; macOS/MoltenVK only.
*/
var exportMetalIOSurface: Boolean = false,
/**
* Back color attachment 0 with an externally-created IOSurface, given as its `IOSurfaceRef`
* pointer, instead of exporting one. Preferred for streaming: an `IOSurfaceCreate`-born
* surface carries a real `pixelFormat` that CoreVideo/VideoToolbox can wrap, where MoltenVK's
* export leaves it 0. macOS/MoltenVK only; the surface's format and size must match this
* target's.
*/
var importMetalIOSurface: Long = 0,
/**
* Create color attachments with opaque-fd-exportable backing (`VK_KHR_external_memory_fd`) so
* the target can be handed zero-copy to NVENC through CUDA external memory import
* (field-encode-linux). Opt-in; Linux/NVIDIA only; see [VulkanExternalMemory].
*/
var exportOpaqueFd: Boolean = false,
/**
* Create color attachments with dma-buf-exportable backing and a driver-chosen DRM format
* modifier, so the target can be handed to any dma-buf importer — including Field's own
* external-browser receiver, which is what makes that receiver testable without a browser.
* Opt-in; Linux only; see [LinuxDmaBufExport].
*/
var exportDmaBuf: Boolean = false
)
open class RenderToTexture
An offscreen target holding one set of color attachments, any number of them, which its modules draw into and other modules sample as textures. Optionally a depth+stencil attachment as well, to depth-test against while rendering. The attachments are single-buffered, so reading last frame's output while writing this one is PingPongRenderToTexture's job rather than this class's.
open class RenderToTexture(
/** The renderer whose device allocates the attachments and whose swapchain depth sets [numImages]. */
val renderer: Renderer = fromContext(),
parameters: RTTParameters.() -> Unit = {}
) : Target, HasImageView, HasImageViewArray, HasDefaultTextureSampler, PreparesSampledImages {
var parameters = RTTParameters().apply(parameters)
The RTTParameters this target was built with. The images were created from them at
construction, so changing a size or format here does not reallocate; the per-frame fields
(RTTParameters.clearFirst, RTTParameters.clearColor, RTTParameters.colorStoreOp,
RTTParameters.depthLoadOp, RTTParameters.viewports) are re-read each time the pass records.
override var defaultSampler: TextureSampler? = null
private val debugName = parametersDebugName()
fun setDefaultSampler
fun setDefaultSampler(
anisotropyEnable: Boolean = true,
filter: Int = VK_FILTER_LINEAR,
mipmapMode: Int = if (filter == VK_FILTER_LINEAR) VK_SAMPLER_MIPMAP_MODE_LINEAR else VK_SAMPLER_MIPMAP_MODE_NEAREST,
addressModeU: Int = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeV: Int = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeW: Int = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE
): TextureSampler {
val sampler = TextureSampler(
renderer = renderer,
mipLevels = mipLevels,
anisotropyEnable = anisotropyEnable,
filter = filter,
mipmapMode = mipmapMode,
addressModeU = addressModeU,
addressModeV = addressModeV,
addressModeW = addressModeW
)
defaultSampler = sampler
return sampler
}
fun repeatU
fun repeatU(
anisotropyEnable: Boolean = true,
filter: Int = VK_FILTER_LINEAR,
mipmapMode: Int = if (filter == VK_FILTER_LINEAR) VK_SAMPLER_MIPMAP_MODE_LINEAR else VK_SAMPLER_MIPMAP_MODE_NEAREST
): TextureSampler = setDefaultSampler(
anisotropyEnable = anisotropyEnable,
filter = filter,
mipmapMode = mipmapMode,
addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT,
addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE
)
override val imageView: ImageView
override val imageView: ImageView
get() = imageViewArray.first().imageView
val colorImageRefs: MutableList<Image?>
The Image behind each color attachment, in slot order. An entry is null where the slot
came from RTTParameters.externalColorViews and that view has no Image of its own, so
read colorImageHandles when all you need is the Vulkan handle.
val colorImageHandles: MutableList<Long>
The VkImage handle of each color attachment, external slots included; its size is the number of attachments, which every module registered here has to declare.
protected val mutableImageViewArray: MutableList<HasImageView>
private val sampledMip0ImageViewArray: MutableList<HasImageView>
protected val attachmentImageViews: MutableList<ImageView>
override val imageViewArray: List<HasImageView>
override val imageViewArray: List<HasImageView>
get() = if (usesFullSampledMipChain()) mutableImageViewArray else sampledMip0ImageViewArray
val colorFormats: List<Int>
The resolved VK_FORMAT_* of each color attachment: RTTParameters.colorFormats when it was given, otherwise RTTParameters.format repeated. A module's declared formats have to match this list exactly.
val colorLayerCount: Int
Array layers in each color attachment. RTTParameters.colorLayerCount when positive; 0 takes the count from the external views, which all have to agree.
depth (optional)
var depthImageView: ImageView? = null
The depth(+stencil) attachment's view, or null when RTTParameters.useDepth is off. Its aspect mask covers stencil too for a combined format, which is why sampling uses depthSampledImageView.
var depthSampledImageView: ImageView? = null
A depth-aspect-only view of the same image, for sampling the depth result. It is a second view only when the format carries stencil and RTTParameters.depthSampled is set; otherwise it is depthImageView itself, and null when there is no depth attachment.
var depthImage: Image? = null
The depth(+stencil) image, or null when RTTParameters.useDepth is off. It carries colorLayerCount layers so a multiview or layered pass can depth-test.
protected var colorLayout: IntArray = intArrayOf() // tracks mip level 0 per attachment
Track current layouts across frames (base color level and depth).
private var depthLayout: Int = VK_IMAGE_LAYOUT_UNDEFINED
protected val sampledReadStageMask: Long
protected val sampledReadStageMask: Long =
VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT or
VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT or
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT or
(if (renderer.device.enableMeshShading)
EXTMeshShader.VK_PIPELINE_STAGE_TASK_SHADER_BIT_EXT.toLong() or EXTMeshShader.VK_PIPELINE_STAGE_MESH_SHADER_BIT_EXT.toLong()
else 0L)
protected var colorMipsInitialized: BooleanArray = booleanArrayOf()
Track whether mips (levels 1..N-1) have ever been initialized (affects oldLayout for barriers)
protected var colorFormatCanBlit: BooleanArray = booleanArrayOf()
Cached format capabilities & chosen filter
protected var colorFormatLinearBlit: BooleanArray = booleanArrayOf()
protected var chosenMipFilter: IntArray = intArrayOf()
val width: Int get() = parameters.width
The attachment width the images were created at, read back from parameters.
val height: Int get() = parameters.height
The attachment height the images were created at, read back from parameters.
val mipLevels: Int get() = parameters.mipLevels
Mip levels on the color attachments. Above one, the sampled views cover the whole chain once the mips have been generated, and only mip 0 before that.
private fun usesFullSampledMipChain(): Boolean {
if (mipLevels <= 1) return true
if (!parameters.generateColorMipmaps) return false
return colorImageHandles.indices.all { idx ->
colorMipsInitialized.getOrElse(idx) { false } &&
colorFormatCanBlit.getOrElse(idx) { false }
}
}
var modules = mutableMapOf<String, ModulePointer>()
The modules drawn into this target, by name, recorded in insertion order. Add with
rtt += "name" to module, which checks the module's attachment count, formats, view mask
and viewport count against this target first; a module written for the window fails that
check until +target appears in its own parameters. Adding a module under a name already
here replaces it without moving it in the order.
var meshModules = mutableMapOf<String, MeshModulePointer>()
Procedural geometry, drawn after conventional modules in this same rendering instance.
source — 50 lines, from line 287
private data class FileSaveRequest(
val outputPath: String,
val attachment: Int,
val quality: Int,
val completion: CompletableFuture<String>,
/** Set by [readPixelsAsync]: deliver the attachment's bytes as stored instead of writing a JPEG. */
val raw: CompletableFuture<ByteArray>? = null
)
private data class RecordedFileSave(
val request: FileSaveRequest,
val stagingBuffer: VulkanBuffer,
val width: Int,
val height: Int,
val format: Int
)
private val fileSaveLock = Any()
private val pendingFileSaves = ArrayDeque<FileSaveRequest>()
private fun parametersDebugName(): String {
val explicit = parameters.debugName.trim()
if (explicit.isNotEmpty()) return explicit
return "RenderToTexture#${nextRttDebugId.getAndIncrement()}@${allocationSite()}"
}
private fun allocationSite(): String {
val ignoredPrefixes = listOf(
"java.",
"jdk.",
"kotlin.",
"afield.graphics.vulkan.targets.RenderToTexture"
)
val frame = Throwable().stackTrace.firstOrNull { st ->
ignoredPrefixes.none { st.className.startsWith(it) }
} ?: return "unknown"
return "${frame.fileName ?: frame.className}:${frame.lineNumber}"
}
private fun imageDebugName(kind: String, attachment: Int? = null): String =
buildString {
append(debugName)
append(" ")
append(kind)
if (attachment != null) append("[").append(attachment).append("]")
append(" ")
append(width)
append("x")
append(height)
}
fun saveJpgAsync(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String>
Queue a non-blocking save of a color attachment to JPEG.
The GPU copy is recorded the next time this target renders. The returned future completes after readback, JPEG encoding, and file write finish on a background thread.
@JvmOverloads
fun saveJpgAsync(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String> {
val completion = CompletableFuture<String>()
if (attachment !in colorImageHandles.indices) {
completion.completeExceptionally(
IllegalArgumentException("attachment $attachment is out of range for $this")
)
return completion
}
synchronized(fileSaveLock) {
pendingFileSaves.addLast(
FileSaveRequest(
outputPath = outputPath,
attachment = attachment,
quality = quality.coerceIn(1, 100),
completion = completion
)
)
}
return completion
}
fun saveJpegAsync(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String>
@JvmOverloads
fun saveJpegAsync(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String> =
saveJpgAsync(outputPath, attachment, quality)
fun readPixelsAsync(attachment: Int = 0): CompletableFuture<ByteArray>
Queue a copy of a color attachment's pixels exactly as the attachment stores them — for a float target, the float values the shader wrote, unclamped, NaN included — which a JPEG or an 8-bit preview cannot carry.
Like saveJpgAsync, the copy is recorded the next time this target renders, after its draw,
so it holds that frame's image; the future completes once that frame's GPU work has finished.
The bytes are row-major with row 0 first, width * height * bytesPerPixel(format) long, in the
device's (little-endian) byte order. Needs colorTransferSrc = true.
@JvmOverloads
fun readPixelsAsync(attachment: Int = 0): CompletableFuture<ByteArray> {
val raw = CompletableFuture<ByteArray>()
if (attachment !in colorImageHandles.indices) {
raw.completeExceptionally(IllegalArgumentException("attachment $attachment is out of range for $this"))
return raw
}
val completion = CompletableFuture<String>()
completion.whenComplete { _, t -> if (t != null) raw.completeExceptionally(t) }
synchronized(fileSaveLock) {
pendingFileSaves.addLast(FileSaveRequest("", attachment, 0, completion, raw))
}
return raw
}
fun launchJpgSave(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String>
@JvmOverloads
fun launchJpgSave(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String> =
saveJpgAsync(outputPath, attachment, quality)
private fun dequeueFileSaves(): List<FileSaveRequest> =
synchronized(fileSaveLock) {
if (pendingFileSaves.isEmpty()) emptyList()
else buildList(pendingFileSaves.size) {
while (pendingFileSaves.isNotEmpty()) add(pendingFileSaves.removeFirst())
}
}
fun recordColorTransferSource(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) {
Record copy with color attachment in TRANSFER_SRC_OPTIMAL, then restore the layout it
had — the same bracket the file saves below use, for a consumer outside this class that reads
the attachment on the same command buffer. FieldRTTStreamer
uses it to copy each rendered frame into its IOSurface on KosmicKrisp. Outside a render pass.
fun recordColorTransferSource(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) {
check(parameters.colorTransferSrc) { "RenderToTexture attachment $attachment was not created with colorTransferSrc = true" }
check(!command.inRenderPassInstance) { "recordColorTransferSource must be recorded outside a render pass" }
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).baseMipLevel(0).levelCount(1).baseArrayLayer(0)
.layerCount(colorLayerCount.coerceAtLeast(1))
val oldLayout = colorLayout[attachment]
val (srcStage, srcAccess) = captureSourceStageAndAccess(oldLayout)
transitionColorAttachment(command, attachment, srcStage, VK_PIPELINE_STAGE_2_COPY_BIT, srcAccess,
VK_ACCESS_2_TRANSFER_READ_BIT, oldLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, range)
copy(colorImageHandles[attachment])
transitionColorAttachment(command, attachment, VK_PIPELINE_STAGE_2_COPY_BIT, srcStage, VK_ACCESS_2_TRANSFER_READ_BIT,
srcAccess, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, oldLayout, range)
colorLayout[attachment] = oldLayout
}
}
fun recordColorTransferDestination(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) {
GPU producer upload into a sampled attachment. Outside a render pass; no host staging.
fun recordColorTransferDestination(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) {
check(parameters.colorTransferDst && parameters.colorSampled)
check(!command.inRenderPassInstance && parameters.mipLevels == 1)
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack).aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0).levelCount(1).baseArrayLayer(0).layerCount(colorLayerCount.coerceAtLeast(1))
val oldLayout = colorLayout[attachment]
val (srcStage, srcAccess) = captureSourceStageAndAccess(oldLayout)
transitionColorAttachment(command, attachment, srcStage, VK_PIPELINE_STAGE_2_COPY_BIT, srcAccess,
VK_ACCESS_2_TRANSFER_WRITE_BIT, oldLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, range)
copy(colorImageHandles[attachment])
transitionColorAttachment(command, attachment, VK_PIPELINE_STAGE_2_COPY_BIT, sampledReadStageMask,
VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, range)
colorLayout[attachment] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
}
}
protected fun recordPendingFileSaves(command: CommandBuffer)
body — 114 lines, from line 450
protected fun recordPendingFileSaves(command: CommandBuffer) {
val requests = dequeueFileSaves()
if (requests.isEmpty()) return
val recorded = mutableListOf<RecordedFileSave>()
MemoryStack.stackPush().use { stack ->
requests.forEach { request ->
try {
require(request.attachment in colorImageHandles.indices) {
"attachment ${request.attachment} is out of range for $this"
}
val canTransfer =
parameters.colorTransferSrc ||
(parameters.generateColorMipmaps &&
parameters.mipLevels > 1 &&
colorFormatCanBlit.getOrElse(request.attachment) { false })
require(canTransfer) {
"RenderToTexture attachment ${request.attachment} was not created with transfer-source usage; set colorTransferSrc = true"
}
val format = colorFormats[request.attachment]
val bytesPerPixel = GpuImageCapture.bytesPerPixel(format, "RenderToTexture")
val staging = VulkanBuffer(
renderer = renderer,
requestedSize = width.toLong() * height.toLong() * bytesPerPixel.toLong(),
usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT,
reqMask = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or VK_MEMORY_PROPERTY_HOST_CACHED_BIT
)
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(colorLayerCount.coerceAtLeast(1))
val oldLayout = colorLayout[request.attachment]
val (srcStage, srcAccess) = captureSourceStageAndAccess(oldLayout)
transitionColorAttachment(
command = command,
attachment = request.attachment,
srcStage = srcStage,
dstStage = VK_PIPELINE_STAGE_2_COPY_BIT,
srcAccess = srcAccess,
dstAccess = VK_ACCESS_2_TRANSFER_READ_BIT,
oldLayout = oldLayout,
newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
range = range
)
val copyRegion = VkBufferImageCopy.calloc(1, stack)
copyRegion[0]
.bufferOffset(0L)
.bufferRowLength(0)
.bufferImageHeight(0)
.imageSubresource(
VkImageSubresourceLayers.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.mipLevel(0)
.baseArrayLayer(0)
.layerCount(1)
)
.imageOffset(VkOffset3D.calloc(stack).x(0).y(0).z(0))
.imageExtent(VkExtent3D.calloc(stack).width(width).height(height).depth(1))
vkCmdCopyImageToBuffer(
command.vkCommandBuffer,
colorImageHandles[request.attachment],
vkLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),
staging.buffer,
copyRegion
)
transitionColorAttachment(
command = command,
attachment = request.attachment,
srcStage = VK_PIPELINE_STAGE_2_COPY_BIT,
dstStage = srcStage,
srcAccess = VK_ACCESS_2_TRANSFER_READ_BIT,
dstAccess = srcAccess,
oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
newLayout = oldLayout,
range = range
)
colorLayout[request.attachment] = oldLayout
recorded += RecordedFileSave(request, staging, width, height, format)
} catch (t: Throwable) {
request.completion.completeExceptionally(t)
}
}
}
if (recorded.isNotEmpty()) {
renderer.afterCurrentGraphicsFrameComplete {
recorded.forEach { capture ->
Runloop.miscPool.execute { completeFileSave(capture) }
}
}
}
}
private fun captureSourceStageAndAccess(layout: Int): Pair<Long, Long> =
when (layout) {
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ->
sampledReadStageMask to VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL ->
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT to VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ->
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT to VK_ACCESS_2_TRANSFER_READ_BIT
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ->
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT to VK_ACCESS_2_TRANSFER_WRITE_BIT
else -> VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT to 0L
}
override fun prepareSampledImagesFor(command: CommandBuffer)
body — 107 lines, from line 565
override fun prepareSampledImagesFor(command: CommandBuffer) {
if (!parameters.colorSampled) return
if (colorImageHandles.isEmpty()) return
MemoryStack.stackPush().use { stack ->
colorImageHandles.forEachIndexed { idx, _ ->
if (colorLayout.getOrElse(idx) { VK_IMAGE_LAYOUT_UNDEFINED } != VK_IMAGE_LAYOUT_UNDEFINED) {
return@forEachIndexed
}
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(colorLayerCount.coerceAtLeast(1))
transitionColorAttachment(
command = command,
attachment = idx,
srcStage = VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
dstStage = sampledReadStageMask,
srcAccess = 0L,
dstAccess = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
range = range
)
colorLayout[idx] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
}
}
}
private fun transitionColorAttachment(
command: CommandBuffer,
attachment: Int,
srcStage: Long,
dstStage: Long,
srcAccess: Long,
dstAccess: Long,
oldLayout: Int,
newLayout: Int,
range: VkImageSubresourceRange
) {
val imageRef = colorImageRefs.getOrElse(attachment) { null }
if (imageRef != null) {
image_layout_transition(
command.vkCommandBuffer,
imageRef,
srcStage,
dstStage,
srcAccess,
dstAccess,
oldLayout,
newLayout,
range
)
} else {
image_layout_transition(
command.vkCommandBuffer,
colorImageHandles[attachment],
srcStage,
dstStage,
srcAccess,
dstAccess,
oldLayout,
newLayout,
range
)
}
}
private fun completeFileSave(capture: RecordedFileSave) {
try {
capture.request.raw?.let { raw ->
var bytes: ByteArray? = null
capture.stagingBuffer.mapAndRead_bytes {
rewind()
bytes = ByteArray(remaining()).also { get(it) }
}
raw.complete(requireNotNull(bytes))
capture.request.completion.complete("")
return
}
val rgba = readCaptureRgba(capture)
GpuImageCapture.writeJpeg(
capture.request.outputPath,
rgba,
capture.width,
capture.height,
capture.request.quality
)
capture.request.completion.complete(capture.request.outputPath)
} catch (t: Throwable) {
capture.request.completion.completeExceptionally(t)
} finally {
capture.stagingBuffer.cleanup()
}
}
private fun readCaptureRgba(capture: RecordedFileSave): ByteArray {
var rgba: ByteArray? = null
capture.stagingBuffer.mapAndRead_bytes {
rgba = GpuImageCapture.readRgba(this, capture.format, capture.width, capture.height, "RenderToTexture")
}
return requireNotNull(rgba)
}
var commandBuffers: Array<CommandBuffer>
One primary command buffer per swapchain image, allocated at construction. The pass itself
records into the command buffer handed to render, so these are for code that submits a
target on its own; nothing in this class submits them, and a caller that allocated a target
outside the renderer's frame loop frees them along with the target.
var fences: Array<afield.graphics.vulkan.device.Fence>
One fence per swapchain image, created already signalled, to pair with commandBuffers in the same off-frame submission.
val numImages = renderer.swapChain.imageViews.size
How many command buffers and fences this target holds: the renderer's swapchain image count, read once at construction. The attachments themselves are single-buffered; PingPongRenderToTexture is the variant that rings them.
init
body — 251 lines, from line 687
init {
val resolvedColorFormats =
if (this.parameters.colorFormats.isNotEmpty()) this.parameters.colorFormats
else List(this.parameters.colorAttachments) { this@RenderToTexture.parameters.format }
require(resolvedColorFormats.isNotEmpty()) { "RenderToTexture needs at least one color format" }
require(resolvedColorFormats.size == this.parameters.colorAttachments) {
"colorFormats size (${resolvedColorFormats.size}) must match colorAttachments (${this.parameters.colorAttachments})"
}
colorFormats = resolvedColorFormats
val explicitLayerCount = this.parameters.colorLayerCount
val externalViews = this.parameters.externalColorViews
val inferredLayerCount =
if (explicitLayerCount > 0) explicitLayerCount
else if (externalViews.isNotEmpty()) {
val counts = externalViews.values.map { it.imageView.imageViewData.layerCount }
require(counts.distinct().size == 1) {
"All external color attachments must share the same layerCount; got $counts"
}
counts.first()
} else 1
colorLayerCount = inferredLayerCount
val multiviewLayerCount = Integer.bitCount(this.parameters.multiviewViewMask)
if (this.parameters.multiviewViewMask != 0) {
require(renderer.device.supportsMultiview) {
"RenderToTexture requested multiviewViewMask=0x${this.parameters.multiviewViewMask.toString(16)}, but VK_KHR_multiview is not available"
}
require(colorLayerCount >= multiviewLayerCount) {
"RenderToTexture multiviewViewMask=0x${this.parameters.multiviewViewMask.toString(16)} needs at least $multiviewLayerCount layers, but colorLayerCount=$colorLayerCount"
}
require(this.parameters.multiviewViewMask > 0) {
"RenderToTexture multiviewViewMask must be positive when multiview is enabled"
}
}
// --- Query blit/filter support for each color format and pick a safe filter ---
MemoryStack.stackPush().use { stack ->
colorFormatCanBlit = BooleanArray(resolvedColorFormats.size)
colorFormatLinearBlit = BooleanArray(resolvedColorFormats.size)
chosenMipFilter = IntArray(resolvedColorFormats.size)
resolvedColorFormats.forEachIndexed { idx, fmt ->
val props = VkFormatProperties.calloc(stack)
vkGetPhysicalDeviceFormatProperties(renderer.device.physicalDevice.vkPhysicalDevice, fmt, props)
val feats = props.optimalTilingFeatures()
val canSrc = (feats and VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0
val canDst = (feats and VK_FORMAT_FEATURE_BLIT_DST_BIT) != 0
val canBlit = canSrc && canDst
colorFormatCanBlit[idx] = canBlit
colorFormatLinearBlit[idx] = (feats and VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0
chosenMipFilter[idx] = when (this.parameters.mipBlitFilter) {
VK_FILTER_LINEAR -> if (colorFormatLinearBlit[idx]) VK_FILTER_LINEAR else VK_FILTER_NEAREST
else -> VK_FILTER_NEAREST
}
}
}
val wantsMips = this.parameters.mipLevels > 1 && this.parameters.generateColorMipmaps
require(externalViews.keys.all { it in colorFormats.indices }) {
"externalColorViews keys must be within [0, ${colorFormats.lastIndex}]"
}
val tmpViews = MutableList<HasImageView?>(colorFormats.size) { null }
val tmpSampledMip0Views = MutableList<HasImageView?>(colorFormats.size) { null }
val tmpAttachmentViews = MutableList<ImageView?>(colorFormats.size) { null }
val tmpImageRefs = MutableList<Image?>(colorFormats.size) { null }
val tmpImageHandles = MutableList<Long>(colorFormats.size) { 0L }
val forceColorArrayView = this.parameters.forceColorArrayView
colorFormats.forEachIndexed { idx, fmt ->
val external = externalViews[idx]
if (external != null) {
require(external.imageView.imageViewData.layerCount == colorLayerCount) {
"External color attachment $idx has layerCount=${external.imageView.imageViewData.layerCount} but expected $colorLayerCount"
}
tmpImageRefs[idx] = external.imageView.underlyingImage
tmpImageHandles[idx] = external.imageView.image
val sampledView =
if (forceColorArrayView && external.imageView.imageViewData.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) {
val ivd = ImageViewData()
.format(fmt)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseArrayLayer(external.imageView.imageViewData.baseArrayLayer)
.layerCount(colorLayerCount.coerceAtLeast(1))
.mipLevels(external.imageView.imageViewData.mipLevels)
.debugName(imageDebugName("sampled-array-view", idx))
.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
ImageView(renderer.device, external.imageView.image, ivd)
} else {
external.imageView
}
tmpViews[idx] = sampledView
tmpSampledMip0Views[idx] =
if (sampledView.imageView.imageViewData.mipLevels == 1) sampledView
else {
val ivd = ImageViewData()
.format(fmt)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseArrayLayer(sampledView.imageView.imageViewData.baseArrayLayer)
.layerCount(colorLayerCount.coerceAtLeast(1))
.mipLevels(1)
.debugName(imageDebugName("sampled-mip0-view", idx))
.viewType(sampledView.imageView.imageViewData.viewType)
ImageView(renderer.device, external.imageView.image, ivd)
}
val attachmentView =
if (mipLevels == 1 && !forceColorArrayView) external.imageView
else {
val ivd = ImageViewData()
.format(fmt)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseArrayLayer(external.imageView.imageViewData.baseArrayLayer)
.layerCount(colorLayerCount.coerceAtLeast(1))
.mipLevels(1)
.debugName(imageDebugName("attachment-view", idx))
if (ivd.layerCount > 1 || forceColorArrayView) ivd.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
ImageView(renderer.device, external.imageView.image, ivd)
}
tmpAttachmentViews[idx] = attachmentView
} else {
val needsMipTransfers = wantsMips && colorFormatCanBlit[idx]
val data = ImageData()
.width(width)
.height(height)
.arrayLayers(colorLayerCount.coerceAtLeast(1))
.usage(
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT or
(if (this.parameters.colorStorage) VK_IMAGE_USAGE_STORAGE_BIT else 0) or
(if (this.parameters.colorSampled) VK_IMAGE_USAGE_SAMPLED_BIT else 0) or
(if (this.parameters.colorTransferSrc || needsMipTransfers) VK_IMAGE_USAGE_TRANSFER_SRC_BIT else 0) or
(if (this.parameters.colorTransferDst || needsMipTransfers) VK_IMAGE_USAGE_TRANSFER_DST_BIT else 0)
)
.format(fmt)
.mipLevels(mipLevels)
.debugName(imageDebugName("color", idx))
.aliasSingleLayerDescriptors(forceColorArrayView)
data.exportMetalIOSurface = this.parameters.exportMetalIOSurface
data.exportOpaqueFd = this.parameters.exportOpaqueFd
data.exportDmaBuf = this.parameters.exportDmaBuf
// Import applies to color attachment 0 only (a streamed RTT is single-attachment;
// a ring is N separate RTTs, each importing its own surface).
if (idx == 0) data.importMetalIOSurface = this.parameters.importMetalIOSurface
val img = Image(renderer.device, data)
val ivd = ImageViewData()
.format(data.format)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.layerCount(data.arrayLayers)
.mipLevels(mipLevels)
.debugName(imageDebugName("sampled-view", idx))
if (ivd.layerCount > 1 || forceColorArrayView) ivd.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
val view = ImageView(renderer.device, img, ivd)
tmpViews[idx] = view
tmpSampledMip0Views[idx] =
if (mipLevels == 1) view
else {
val svd = ImageViewData()
.format(data.format)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.layerCount(data.arrayLayers)
.mipLevels(1)
.debugName(imageDebugName("sampled-mip0-view", idx))
if (svd.layerCount > 1 || forceColorArrayView) svd.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
ImageView(renderer.device, img, svd)
}
tmpImageRefs[idx] = img
tmpImageHandles[idx] = img.vkImage
val attachmentView =
if (mipLevels == 1) view
else {
val avd = ImageViewData()
.format(data.format)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.layerCount(data.arrayLayers)
.mipLevels(1)
.debugName(imageDebugName("attachment-view", idx))
if (avd.layerCount > 1 || forceColorArrayView) avd.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
ImageView(renderer.device, img, avd)
}
tmpAttachmentViews[idx] = attachmentView
}
}
mutableImageViewArray = tmpViews.map { requireNotNull(it) }.toMutableList()
sampledMip0ImageViewArray = tmpSampledMip0Views.map { requireNotNull(it) }.toMutableList()
attachmentImageViews = tmpAttachmentViews.map { requireNotNull(it) }.toMutableList()
colorImageRefs = tmpImageRefs
colorImageHandles = tmpImageHandles
colorLayout = IntArray(colorImageHandles.size) { VK_IMAGE_LAYOUT_UNDEFINED }
colorMipsInitialized = BooleanArray(colorImageHandles.size) { false }
// DEPTH+STENCIL (optional)
if (this.parameters.useDepth) {
val depthUsage =
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT or
(if (this.parameters.depthSampled) VK_IMAGE_USAGE_SAMPLED_BIT else 0)
val depthImageData = ImageData()
.width(width)
.height(height)
.arrayLayers(colorLayerCount.coerceAtLeast(1))
.usage(depthUsage)
.format(this.parameters.depthFormat)
.mipLevels(1)
.debugName(imageDebugName("depth"))
.aliasSingleLayerDescriptors(this.parameters.depthSampled && colorLayerCount > 1)
depthImage = Image(renderer.device, depthImageData)
val depthAspect = aspectMaskForDepth(this.parameters.depthFormat)
val depthViewData = ImageViewData()
.format(this.parameters.depthFormat)
.aspectMask(depthAspect)
.layerCount(depthImageData.arrayLayers)
.mipLevels(1)
.debugName(imageDebugName("depth-view"))
if (depthViewData.layerCount > 1) depthViewData.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
depthImageView = ImageView(renderer.device, depthImage!!, depthViewData)
depthSampledImageView =
if (this.parameters.depthSampled && depthAspect != VK_IMAGE_ASPECT_DEPTH_BIT) {
val depthOnlyViewData = ImageViewData()
.format(this.parameters.depthFormat)
.aspectMask(VK_IMAGE_ASPECT_DEPTH_BIT)
.layerCount(depthImageData.arrayLayers)
.mipLevels(1)
.debugName(imageDebugName("depth-sampled-view"))
if (depthOnlyViewData.layerCount > 1) depthOnlyViewData.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
ImageView(
renderer.device,
depthImage!!,
depthOnlyViewData
)
} else {
depthImageView
}
}
// per-swapchain command buffers & fences
commandBuffers = buildList(numImages) {
for (i in 0 until numImages) add(CommandBuffer(renderer.commandPool, true, false))
}.toTypedArray()
fences = buildList(numImages) {
for (i in 0 until numImages) add(afield.graphics.vulkan.device.Fence(renderer.device, true))
}.toTypedArray()
}
protected open fun beforeRender(index: Int) {}
protected open fun afterRender(index: Int) {}
protected open fun prepareAdditionalResources(command: CommandBuffer, index: Int) {}
Hook for subclasses that need to record transfer/upload commands before dynamic rendering begins.
Called after any module/mesh buffer preparation, while command.inRenderPassInstance == false.
protected open fun renderAdditional(command: CommandBuffer, index: Int, stack: MemoryStack) {}
Hook for subclasses that want to emit additional draw commands inside the dynamic rendering scope.
Called after modules have rendered and viewport/scissor have been set to this RTT's configured viewports.
data class ColorGrade
data class ColorGrade(
var exposure: Vec4 = vec(1f, 1f, 1f, 1f),
var gamma: Vec4 = vec(1f, 1f, 1f, 1f),
var offset: Vec4 = vec(0f, 0f, 0f, 0f),
var sampleWeight: Float = 1f
)
open fun blitToScreen
Simple on-screen blit of a color attachment from this RTT. Generates a fullscreen triangle mesh internally (no vertex inputs) and wires a small grading push-constant block.
viewport clips the destination on the screen target, sourceRect crops the sampled source
texture, and the returned ModulePointer identifies the generated blit module.
sourceRect is in normalized texel space with y measured downwards from the top row,
matching blitVertexShader. It is not a place to flip an image: Rect(0.0, 1.0, 1.0, -1.0)
used to be how callers cancelled a mirror in that shader, and the mirror is now gone.
If warper is supplied, it should be the GLSL body of vec2 warp(vec2 x, float t). x is
the unwarped NDC vertex position, t is time.x, and the return value is added as an NDC
offset. Warped blits use a viewport-local grid sized by numSegmentsW and numSegmentsH,
so normal viewport/scissor clipping crops displaced vertices.
body — 43 lines, from line 980
open fun blitToScreen(
renderer: Renderer = this.renderer,
target: RenderTarget = renderer.renderList,
attachment: Int = 0,
name: String = "rtt-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer {
require(attachment in imageViewArray.indices) {
"attachment $attachment is out of range for RenderToTexture with ${imageViewArray.size} color attachments"
}
val sourceProvider = object : HasImageViewArray, PreparesSampledImages {
override val imageViewArray: List<HasImageView>
get() = listOf(this@RenderToTexture.imageViewArray[attachment])
override fun prepareSampledImagesFor(command: CommandBuffer) {
this@RenderToTexture.prepareSampledImagesFor(command)
}
}
return blitImageToScreen(
sourceProvider = sourceProvider,
sourceView = imageViewArray[attachment].imageView,
renderer = renderer,
target = target,
name = name,
viewport = viewport,
sourceRect = sourceRect,
blend = blend,
additive = additive,
sourceLayer = sourceLayer,
warper = warper,
numSegmentsW = numSegmentsW,
numSegmentsH = numSegmentsH,
configure = configure
)
}
protected fun blitImageToScreen
body — 85 lines, from line 1024
protected fun blitImageToScreen(
sourceProvider: HasImageViewArray,
sourceView: ImageView,
renderer: Renderer = this.renderer,
target: RenderTarget = renderer.renderList,
name: String = "rtt-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer {
require(numSegmentsW >= 1) { "numSegmentsW must be at least 1" }
require(numSegmentsH >= 1) { "numSegmentsH must be at least 1" }
val sourceIsArray = sourceView.imageViewData.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY
if (sourceIsArray) {
require(sourceLayer in 0 until sourceView.imageViewData.layerCount) {
"sourceLayer $sourceLayer is out of range for source image with ${sourceView.imageViewData.layerCount} layers"
}
} else {
require(sourceLayer == 0) {
"sourceLayer can only be non-zero when the source attachment is a 2D array image view"
}
}
val grade = ColorGrade().apply(configure)
val sampler = afield.graphics.texture.TextureSampler(
renderer,
sourceView.imageViewData.mipLevels,
true,
VK_FILTER_NEAREST,
VK_SAMPLER_MIPMAP_MODE_NEAREST
)
val module = Module(afield.graphics.render.parameters(renderer) {
this.name = name
color = 1
depth = 1
depthFormat = Options.defaultDepthFormat
depthTest = false
imageFormat = renderer.windowContext(target.windowIndex).swapChain.surfaceFormat.imageFormat
this.blend = blend || additive
if (additive) {
colorBlendOp = VK_BLEND_OP_ADD
alphaBlendOp = VK_BLEND_OP_ADD
}
this.viewport = viewport
vertex = if (usesSegmentedBlit(warper, numSegmentsW, numSegmentsH)) {
segmentedBlitVertexShader(warper)
} else {
blitVertexShader
}
fragment = if (sourceIsArray) blitArrayFragmentShader else blitFragmentShader
textureResolver = { tex ->
if (tex.name == "sourceTex") sourceProvider to sampler else null
}
}).pointer()
val mesh = afield.graphics.render.Mesh(renderer, module)
if (usesSegmentedBlit(warper, numSegmentsW, numSegmentsH)) {
buildSegmentedBlitMesh(mesh, module, numSegmentsW, numSegmentsH)
} else {
// Fullscreen triangle mesh (no attributes).
mesh.resize(3, 0, module)
}
mesh.push["exposure"] = grade.exposure
mesh.push["gamma"] = grade.gamma
mesh.push["offset"] = grade.offset
val src = sourceRect ?: Rect(0.0, 0.0, 1.0, 1.0)
mesh.push["srcRect"] = vec(src.x.f, src.y.f, src.w.f, src.h.f)
mesh.push["sampleWeight"] = grade.sampleWeight
if (sourceIsArray) mesh.push["sourceLayer"] = sourceLayer.toFloat()
var tick = 0L
mesh.push.bind("time") {
vec((tick++ / 120.0).toFloat(), 0f, 0f, 0f)
}
module.current.meshes[name] = mesh
target += name to module
return module
}
open fun debugBlitToScreen
open fun debugBlitToScreen(
renderer: Renderer = this.renderer,
target: RenderTarget = renderer.renderList,
attachment: Int = 0,
name: String = "rtt-debug-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer = blitToScreen(
renderer = renderer,
target = target,
attachment = attachment,
name = name,
viewport = viewport,
sourceRect = sourceRect,
blend = blend,
additive = additive,
sourceLayer = sourceLayer,
warper = warper,
numSegmentsW = numSegmentsW,
numSegmentsH = numSegmentsH,
configure = configure
)
fun debugBlitToScreen
fun debugBlitToScreen(
windowIndex: Int,
renderer: Renderer = this.renderer,
attachment: Int = 0,
name: String = "rtt-debug-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer = debugBlitToScreen(
renderer = renderer,
target = renderer.renderTargetFor(windowIndex),
attachment = attachment,
name = name,
viewport = viewport,
sourceRect = sourceRect,
blend = blend,
additive = additive,
sourceLayer = sourceLayer,
warper = warper,
numSegmentsW = numSegmentsW,
numSegmentsH = numSegmentsH,
configure = configure
)
fun debugBlitToScreen
fun debugBlitToScreen(
window: Window,
renderer: Renderer = this.renderer,
attachment: Int = 0,
name: String = "rtt-debug-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer = debugBlitToScreen(
windowIndex = renderer.windowIndex(window),
renderer = renderer,
attachment = attachment,
name = name,
viewport = viewport,
sourceRect = sourceRect,
blend = blend,
additive = additive,
sourceLayer = sourceLayer,
warper = warper,
numSegmentsW = numSegmentsW,
numSegmentsH = numSegmentsH,
configure = configure
)
fun blitToScreen
Blits a color attachment to the renderer window identified by windowIndex.
This resolves the window's RenderTarget and delegates to the main
blitToScreen overload, preserving optional cropping, blending, warping,
and color grading.
fun blitToScreen(
windowIndex: Int,
renderer: Renderer = this.renderer,
attachment: Int = 0,
name: String = "rtt-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer = blitToScreen(
renderer = renderer,
target = renderer.renderTargetFor(windowIndex),
attachment = attachment,
name = name,
viewport = viewport,
sourceRect = sourceRect,
blend = blend,
additive = additive,
sourceLayer = sourceLayer,
warper = warper,
numSegmentsW = numSegmentsW,
numSegmentsH = numSegmentsH,
configure = configure
)
fun blitToScreen
Blits a color attachment to the screen for window.
The window is resolved through renderer, then forwarded to the
window-index overload.
fun blitToScreen(
window: Window,
renderer: Renderer = this.renderer,
attachment: Int = 0,
name: String = "rtt-blit",
viewport: Rect? = null,
sourceRect: Rect? = null,
blend: Boolean = false,
additive: Boolean = false,
sourceLayer: Int = 0,
warper: String? = null,
numSegmentsW: Int = 1,
numSegmentsH: Int = 1,
configure: ColorGrade.() -> Unit = {}
): ModulePointer = blitToScreen(
windowIndex = renderer.windowIndex(window),
renderer = renderer,
attachment = attachment,
name = name,
viewport = viewport,
sourceRect = sourceRect,
blend = blend,
additive = additive,
sourceLayer = sourceLayer,
warper = warper,
numSegmentsW = numSegmentsW,
numSegmentsH = numSegmentsH,
configure = configure
)
companion object
declaration — 65 lines, from line 1273
companion object {
private fun usesSegmentedBlit(warper: String?, numSegmentsW: Int, numSegmentsH: Int): Boolean =
!warper.isNullOrBlank() || numSegmentsW > 1 || numSegmentsH > 1
private fun buildSegmentedBlitMesh(
mesh: afield.graphics.render.Mesh,
module: ModulePointer,
numSegmentsW: Int,
numSegmentsH: Int
) {
val verticesW = numSegmentsW + 1
val verticesH = numSegmentsH + 1
mesh.resize(verticesW * verticesH, numSegmentsW * numSegmentsH * 2, module)
mesh.write("inPosition") {
for (y in 0 until verticesH) {
val v = y.toFloat() / numSegmentsH.toFloat()
for (x in 0 until verticesW) {
val u = x.toFloat() / numSegmentsW.toFloat()
put(u).put(v)
}
}
}
mesh.t?.mapAndWrite_ints {
for (y in 0 until numSegmentsH) {
for (x in 0 until numSegmentsW) {
val a = y * verticesW + x
val b = a + 1
val c = a + verticesW
val d = c + 1
put(a).put(b).put(c)
put(b).put(d).put(c)
}
}
}
}
private fun segmentedBlitVertexShader(warper: String?): String {
val warpBody = warper?.takeIf { it.isNotBlank() } ?: "return vec2(0.0);"
return """
#version 460
layout(location = 0) in vec2 inPosition;
layout(location = 0) out vec2 fragUV;
layout(push_constant) uniform Push {
vec4 exposure;
vec4 gamma;
vec4 offset;
vec4 srcRect;
float sampleWeight;
float sourceLayer;
vec4 time;
} push;
vec2 warp(vec2 x, float t) {
$warpBody
}
void main() {
vec2 ndc = inPosition * 2.0 - 1.0;
gl_Position = vec4(ndc + warp(ndc, push.time.x), 0.0, 1.0);
// Texel-aligned, matching blitVertexShader — see its KDoc.
fragUV = vec2(inPosition.x, 1.0 - inPosition.y);
}
""".trimIndent()
}
Fullscreen triangle for blitImageToScreen.
fragUV is texel-aligned: fragUV.y = 0 is the first row in memory, the top of the
source image. The source was rendered through the negative-height viewport, so clip
y = +1 is its row 0, and this shader's own output goes to a swapchain rendered through
the same convention — so the identity mapping is fragUV.y = 1 - (clip.y + 1) / 2.
This used to be a bare fragUV = pos, GL-style, which mirrored every RTT vertically on
its way to the window. It went unnoticed for a long time because the two things that
would have shown it up both cancelled it: the standard renderer's own post chain carried
an odd number of matching mirrors, and ImGuiRenderToTexture passed
Rect(0.0, 1.0, 1.0, -1.0) as sourceRect to undo it locally.
source — 75 lines, from line 1353
private val blitVertexShader = """
#version 460
layout(location = 0) out vec2 fragUV;
void main() {
// Fullscreen triangle: vertices 0,1,2 -> (-1,-1), (3,-1), (-1,3)
vec2 pos = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
fragUV = vec2(pos.x, 1.0 - pos.y);
}
""".trimIndent()
private val blitFragmentShader = """
#version 460
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D sourceTex;
layout(push_constant) uniform Push {
vec4 exposure;
vec4 gamma;
vec4 offset;
vec4 srcRect; // xy = offset, zw = size in UV space
float sampleWeight;
vec4 time;
} push;
void main() {
if (push.time.x < -1e30) discard;
vec2 uv = push.srcRect.xy + fragUV * push.srcRect.zw;
vec4 sampled = push.sampleWeight == 0.0 ? vec4(0.0, 0.0, 0.0, 1.0) : texture(sourceTex, uv);
if (any(isnan(sampled)) || any(isinf(sampled))) {
sampled = vec4(0.0, 0.0, 0.0, 1.0);
}
vec4 color = sampled * push.sampleWeight;
vec3 graded = color.rgb * push.exposure.rgb + push.offset.rgb;
color.rgb = pow(max(graded, vec3(0.0)), push.gamma.rgb);
outColor = color;
}
""".trimIndent()
private val blitArrayFragmentShader = """
#version 460
layout(location = 0) in vec2 fragUV;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2DArray sourceTex;
layout(push_constant) uniform Push {
vec4 exposure;
vec4 gamma;
vec4 offset;
vec4 srcRect; // xy = offset, zw = size in UV space
float sampleWeight;
float sourceLayer;
vec4 time;
} push;
void main() {
if (push.time.x < -1e30) discard;
vec2 uv = push.srcRect.xy + fragUV * push.srcRect.zw;
int layerCount = textureSize(sourceTex, 0).z;
float layer = clamp(push.sourceLayer, 0.0, float(max(layerCount - 1, 0)));
vec4 sampled = push.sampleWeight == 0.0 ? vec4(0.0, 0.0, 0.0, 1.0) : texture(sourceTex, vec3(uv, layer));
if (any(isnan(sampled)) || any(isinf(sampled))) {
sampled = vec4(0.0, 0.0, 0.0, 1.0);
}
vec4 color = sampled * push.sampleWeight;
vec3 graded = color.rgb * push.exposure.rgb + push.offset.rgb;
color.rgb = pow(max(graded, vec3(0.0)), push.gamma.rgb);
outColor = color;
}
""".trimIndent()
}
override fun render(command: CommandBuffer, index: Int)
body — 291 lines, from line 1429
override fun render(command: CommandBuffer, index: Int) {
beforeRender(index)
MemoryStack.stackPush().use { stack ->
// COLOR subresource range for mip 0 ONLY (we don't touch other mips during the attachment phase)
// Initial COLOR transition for mip 0 based on tracked layout + ops
colorImageHandles.forEachIndexed { idx, img ->
val imgRef = colorImageRefs.getOrNull(idx)
val colorBaseRange = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(colorLayerCount.coerceAtLeast(1))
val layout = colorLayout[idx]
val (srcStage, srcAccess) = when (layout) {
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ->
// Include CA/depth test stages to chain with broad read barriers on strict validators (MoltenVK/macOS).
(sampledReadStageMask or
VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT or
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT) to VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL ->
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT to VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ->
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT to VK_ACCESS_2_TRANSFER_READ_BIT
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ->
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT to VK_ACCESS_2_TRANSFER_WRITE_BIT
else ->
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT to 0L
}
if (imgRef != null) {
image_layout_transition(
command.vkCommandBuffer,
imgRef,
srcStage,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
srcAccess,
VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
layout,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
colorBaseRange
)
} else {
image_layout_transition(
command.vkCommandBuffer,
img,
srcStage,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
srcAccess,
VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
layout,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
colorBaseRange
)
}
colorLayout[idx] = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
}
// DEPTH (optional) subresource range + transition
var depthRange: VkImageSubresourceRange? = null
if (parameters.useDepth && depthImage != null) {
val depthAspect = aspectMaskForDepth(parameters.depthFormat)
depthRange = VkImageSubresourceRange.calloc(stack)
.aspectMask(depthAspect)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(depthImageView!!.imageViewData.layerCount)
val (srcStage, srcAccess) = when (depthLayout) {
VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL ->
sampledReadStageMask to VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL ->
(VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT or VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT) to
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT
else ->
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT to 0L
}
image_layout_transition(
command.vkCommandBuffer,
depthImage!!,
srcStage,
VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT,
srcAccess,
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
depthLayout,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
depthRange
)
depthLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
}
// COLOR attachment info (KHR)
val colorAttachmentInfo = VkRenderingAttachmentInfoKHR.calloc(colorImageHandles.size, stack)
colorImageHandles.indices.forEach { i ->
colorAttachmentInfo[i]
.sType(VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR)
.imageView(attachmentImageViews[i].vkImageView)
.imageLayout(vkLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL))
.resolveMode(VK_RESOLVE_MODE_NONE)
.loadOp(if (parameters.clearFirst) VK_ATTACHMENT_LOAD_OP_CLEAR else VK_ATTACHMENT_LOAD_OP_LOAD)
.storeOp(parameters.colorStoreOp)
.clearValue(
VkClearValue.calloc(stack).color(
VkClearColorValue.calloc(stack)
.float32(0, this.parameters.clearColor.x.f)
.float32(1, this.parameters.clearColor.y.f)
.float32(2, this.parameters.clearColor.z.f)
.float32(3, this.parameters.clearColor.w.f)
)
)
}
// DEPTH attachment info (KHR) (optional)
val depthAttachmentInfo =
VkRenderingAttachmentInfoKHR.calloc(1, stack)
.sType(VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR)
if (parameters.useDepth && depthImageView != null) {
depthAttachmentInfo.imageView(depthImageView!!.vkImageView)
depthAttachmentInfo.imageLayout(vkLayout(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL))
depthAttachmentInfo.resolveMode(VK_RESOLVE_MODE_NONE)
depthAttachmentInfo.loadOp(parameters.depthLoadOp)
depthAttachmentInfo.storeOp(parameters.depthStoreOp)
depthAttachmentInfo.clearValue(
VkClearValue.calloc(stack).depthStencil(
VkClearDepthStencilValue.calloc(stack)
.depth(parameters.clearDepth)
.stencil(parameters.clearStencil)
)
)
} else {
depthAttachmentInfo.imageView(0)
depthAttachmentInfo.imageLayout(VK_IMAGE_LAYOUT_UNDEFINED)
}
// Rendering info (KHR)
val renderViewMask = parameters.multiviewViewMask
val renderLayers = if (renderViewMask != 0) 1 else colorLayerCount.coerceAtLeast(1)
val renderInfo = VkRenderingInfoKHR.calloc(stack)
.sType(VK_STRUCTURE_TYPE_RENDERING_INFO_KHR)
.renderArea(
VkRect2D.calloc(stack)
.offset(VkOffset2D.calloc(stack).x(0).y(0))
.extent(VkExtent2D.calloc(stack).width(width).height(height))
)
.flags(0)
.layerCount(renderLayers)
.viewMask(renderViewMask)
.pColorAttachments(colorAttachmentInfo)
.pDepthAttachment(depthAttachmentInfo.get(0))
// KHR dynamic rendering
// Upload dirty vertex/index buffers before beginning rendering.
modules.values.forEach { mp ->
mp.current.meshes.values.forEach { mesh ->
mesh.prepareForDraw(command)
mesh.push.primeBufferReferences(command) {
throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
}
}
mp.current.liveUniformStructures.values.forEach { structure ->
structure.primeBufferReferences(command) {
throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
}
}
}
modules.values.forEach { mp ->
mp.current.before.values.forEach { target ->
target.render(command, index)
}
}
meshModules.values.forEach { mp ->
mp.current.meshes.values.forEach { mesh ->
mesh.prepareForDraw(command)
mesh.push.primeBufferReferences(command) {
throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
}
}
mp.current.liveUniformStructures.values.forEach { structure ->
structure.primeBufferReferences(command) {
throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
}
}
mp.current.before.values.forEach { it.render(command, index) }
}
// Allow subclasses to enqueue additional transfers/uploads before rendering begins.
prepareAdditionalResources(command, index)
// Texture providers may need layout transitions before descriptors are pushed.
// Dynamic rendering does not allow image barriers inside the rendering instance.
modules.values.forEach { it.prepareSampledImagesFor(command) }
meshModules.values.forEach { it.prepareSampledImagesFor(command) }
command.inRenderPassInstance = true
KHRDynamicRendering.vkCmdBeginRenderingKHR(command.vkCommandBuffer, renderInfo)
val targetViewports = if (parameters.viewports.isNotEmpty()) parameters.viewports
else listOf(Rect(0.0, 0.0, width.toDouble(), height.toDouble()))
modules.values.forEach {
setViewportAndScissorArray(command.vkCommandBuffer, targetViewports, stack)
it.render(command, Rectanglef(0f, 0f, width.toFloat(), height.toFloat()))
}
meshModules.values.forEach {
setViewportAndScissorArray(command.vkCommandBuffer, targetViewports, stack)
it.render(command, Rectanglef(0f, 0f, width.toFloat(), height.toFloat()))
}
// Allow subclasses to emit additional drawing inside the dynamic rendering region.
setViewportAndScissorArray(command.vkCommandBuffer, targetViewports, stack)
renderAdditional(command, index, stack)
KHRDynamicRendering.vkCmdEndRenderingKHR(command.vkCommandBuffer)
command.inRenderPassInstance = false
command.flushDeferredGraphicsHostReadBarriers()
// --- Automatic color mipmap recompute (if requested & supported) ---
colorImageHandles.forEachIndexed { idx, img ->
val doMips = parameters.generateColorMipmaps &&
parameters.mipLevels > 1 &&
parameters.colorSampled &&
colorFormatCanBlit.getOrElse(idx) { false }
if (doMips) {
generateColorMipChain(
commandBuffer = command.vkCommandBuffer,
image = img,
imageRef = colorImageRefs.getOrElse(idx) { null },
width = width,
height = height,
mipLevels = mipLevels,
filter = chosenMipFilter.getOrElse(idx) { VK_FILTER_LINEAR },
mipsWereInitialized = colorMipsInitialized.getOrElse(idx) { false },
arrayLayers = colorLayerCount.coerceAtLeast(1)
)
colorMipsInitialized[idx] = true
colorLayout[idx] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL // after mipgen all mips are SRV
} else if (parameters.colorSampled) {
// No mip generation: only mip 0 was rendered. Keep descriptors on a mip-0 view
// and transition exactly that subresource to the sampled layout.
val baseLevelRange = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(colorLayerCount.coerceAtLeast(1))
transitionColorAttachment(
command = command,
attachment = idx,
srcStage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
dstStage = sampledReadStageMask,
srcAccess = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
dstAccess = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
range = baseLevelRange
)
colorLayout[idx] = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
} else {
// Not sampling: keep mip 0 as color attachment
colorLayout[idx] = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
}
}
// Transition DEPTH after render ONLY if we intend to sample it
if (parameters.useDepth && parameters.depthSampled && depthImage != null && depthRange != null) {
image_layout_transition(
command.vkCommandBuffer,
depthImage!!,
VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT,
sampledReadStageMask,
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL,
depthRange
)
depthLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
} else if (parameters.useDepth) {
depthLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
}
recordPendingFileSaves(command)
} // MemoryStack.use
afterRender(index)
}
operator fun plusAssign(m: Pair<String, ModulePointer>)
@JvmName("plusAssign")
operator fun plusAssign(m: Pair<String, ModulePointer>) {
validateModule(m.second.current)
modules[m.first] = m.second
}
operator fun plusAssign(m: Pair<String, Module>)
@JvmName("plusAssign_module")
operator fun plusAssign(m: Pair<String, Module>) {
validateModule(m.second)
modules[m.first] = m.second.pointer()
}
operator fun plusAssign(m: Pair<String, MeshModulePointer>)
body — 47 lines, from line 1733
@JvmName("plusAssign_meshModule")
operator fun plusAssign(m: Pair<String, MeshModulePointer>) {
validateModuleParameters(m.second.current.parameters)
meshModules.putOwned(m.first, m.second, renderer)
}
private fun validateModule(module: Module) = validateModuleParameters(module.parameters)
private fun validateModuleParameters(p: afield.graphics.render.Parameters) {
val moduleFormats = if (p.colorFormats.isNotEmpty())
p.colorFormats else List(p.color) { p.imageFormat }
val moduleColorCount = moduleFormats.size
require(moduleColorCount == colorImageHandles.size) {
"RenderToTexture expects ${colorImageHandles.size} color attachments, but module '${p.name}' declares ${moduleColorCount}"
}
require(moduleFormats == colorFormats) {
"RenderToTexture color format mismatch for module '${p.name}': module=$moduleFormats target=$colorFormats. Add +target inside the module's parameters."
}
require(p.viewMask == parameters.multiviewViewMask) {
"RenderToTexture viewMask mismatch for module '${p.name}': module=0x${p.viewMask.toString(16)} target=0x${parameters.multiviewViewMask.toString(16)}. Add +target inside the module's parameters."
}
val expectedViewportCount = parameters.viewports.size.coerceAtLeast(1)
require(p.viewportCount >= expectedViewportCount) {
"RenderToTexture viewport count mismatch for module '${p.name}': module=${p.viewportCount} target=$expectedViewportCount. Add +target inside the module's parameters."
}
}
private fun setViewportAndScissorArray(cmd: VkCommandBuffer, rects: List<Rect>, stack: MemoryStack) {
val viewports = VkViewport.calloc(rects.size, stack)
rects.forEachIndexed { i, r ->
viewports[i]
.x(r.x.toFloat())
.y((r.y + r.h).toFloat()) // flip Y
.width(r.w.toFloat())
.height((-r.h).toFloat())
.minDepth(0f).maxDepth(1f)
}
VK10.vkCmdSetViewport(cmd, 0, viewports)
val scissors = VkRect2D.calloc(rects.size, stack)
rects.forEachIndexed { i, r ->
scissors[i]
.offset(VkOffset2D.calloc(stack).x(r.x.toInt()).y(r.y.toInt()))
.extent(VkExtent2D.calloc(stack).width(r.w.toInt()).height(r.h.toInt()))
}
VK10.vkCmdSetScissor(cmd, 0, scissors)
}
override fun toString(): String
override fun toString(): String {
val ds = if (parameters.useDepth) " +Depth" + (if (hasStencil(parameters.depthFormat)) "+Stencil" else "") else ""
return "RenderToTexture[${width}x${height}, colors=${colorImageHandles.size}$ds, mips=${mipLevels}]"
}
Mip generation (color)
Recomputes the full mip chain from mip 0 → mip N-1 using vkCmdBlitImage. Ends with ALL mips in SHADER_READ_ONLY_OPTIMAL.
Assumes: mip 0 is in COLOR_ATTACHMENT_OPTIMAL at call time (we just finished rendering).
source — 203 lines, from line 1796
private fun generateColorMipChain(
commandBuffer: VkCommandBuffer,
image: Long,
imageRef: Image?,
width: Int,
height: Int,
mipLevels: Int,
filter: Int,
mipsWereInitialized: Boolean,
arrayLayers: Int
) {
MemoryStack.stackPush().use { stack ->
val layerCount = arrayLayers.coerceAtLeast(1)
for (layer in 0 until layerCount) {
// 1) Transition mip 0: COLOR_ATTACHMENT_OPTIMAL -> TRANSFER_SRC_OPTIMAL (this layer only)
run {
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0).levelCount(1)
.baseArrayLayer(layer).layerCount(1)
if (imageRef != null) {
image_layout_transition(
commandBuffer,
imageRef,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
range
)
} else {
image_layout_transition(
commandBuffer,
image,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
range
)
}
}
// 2) Transition mip 1..N-1 to TRANSFER_DST_OPTIMAL for this layer
run {
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(1).levelCount(mipLevels - 1)
.baseArrayLayer(layer).layerCount(1)
val (srcStage, srcAccess, oldLayout) =
if (mipsWereInitialized)
Triple(
sampledReadStageMask,
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
)
else
Triple(
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
0L,
VK_IMAGE_LAYOUT_UNDEFINED
)
if (imageRef != null) {
image_layout_transition(
commandBuffer,
imageRef,
srcStage,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
srcAccess,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
oldLayout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
range
)
} else {
image_layout_transition(
commandBuffer,
image,
srcStage,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
srcAccess,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
oldLayout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
range
)
}
}
// 3) Blit mip chain: level i-1 -> i (single layer)
var srcW = width
var srcH = height
for (i in 1 until mipLevels) {
val dstW = if (srcW > 1) srcW / 2 else 1
val dstH = if (srcH > 1) srcH / 2 else 1
val blit = VkImageBlit.calloc(1, stack)
blit.srcSubresource()
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.mipLevel(i - 1)
.baseArrayLayer(layer)
.layerCount(1)
blit.srcOffsets(0).set(0, 0, 0)
blit.srcOffsets(1).set(srcW, srcH, 1)
blit.dstSubresource()
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.mipLevel(i)
.baseArrayLayer(layer)
.layerCount(1)
blit.dstOffsets(0).set(0, 0, 0)
blit.dstOffsets(1).set(dstW, dstH, 1)
vkCmdBlitImage(
commandBuffer,
image, vkLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),
image, vkLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),
blit,
filter
)
// Make mip i a source for the next iteration: DST -> SRC
val dstToSrc = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(i).levelCount(1)
.baseArrayLayer(layer).layerCount(1)
if (imageRef != null) {
image_layout_transition(
commandBuffer,
imageRef,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstToSrc
)
} else {
image_layout_transition(
commandBuffer,
image,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dstToSrc
)
}
srcW = dstW
srcH = dstH
}
// 4) Transition ALL mips of this layer to SHADER_READ_ONLY_OPTIMAL
run {
val allLevels = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0).levelCount(mipLevels)
.baseArrayLayer(layer).layerCount(1)
if (imageRef != null) {
image_layout_transition(
commandBuffer,
imageRef,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
sampledReadStageMask,
VK_ACCESS_2_TRANSFER_READ_BIT or VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, // valid for mip 0 and last src; others are also SRC after loop
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
allLevels
)
} else {
image_layout_transition(
commandBuffer,
image,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
sampledReadStageMask,
VK_ACCESS_2_TRANSFER_READ_BIT or VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, // valid for mip 0 and last src; others are also SRC after loop
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
allLevels
)
}
}
}
}
}
}
private val nextRttDebugId = AtomicInteger(1)
helpers
private fun hasStencil(format: Int): Boolean =
format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT
private fun aspectMaskForDepth(format: Int): Int =
VK_IMAGE_ASPECT_DEPTH_BIT or (if (hasStencil(format)) VK_IMAGE_ASPECT_STENCIL_BIT else 0)