RenderTarget.kt
src/jvmMain/kotlin/afield/graphics/vulkan/targets/RenderTarget.kt · 377 lines
package afield.graphics.vulkan.targets
import afield.graphics.vulkan.device.vkLayout
import afield.app.protocol.Rect
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.buffers.MissingRefException
import afield.graphics.vulkan.device.CommandBuffer
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.texture.image_layout_transition
import afield.graphics.vulkan.util.Options
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.KHRSwapchain.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK13.*
The render pass that draws a window's swapchain image: an ordered collection of modules, mesh modules and other targets recorded into one frame.
Add passes with += on a RenderTarget; a RendersAfter pass is moved behind the target it
depends on before the list is recorded.
class RenderTarget
Records a window's frame from its named modules, mesh modules and other Targets, in insertion order.
class RenderTarget(
/** The renderer that owns the window this target draws, and that submits the command buffer the passes record into. */
val renderer: Renderer,
private val contextProvider: () -> Renderer.WindowContext = { renderer.windowContext(0) },
/** Which of the renderer's windows this target draws, matching the context it reads its swapchain and depth attachments from. `renderer.renderTargetFor(windowIndex)` is how a box reaches the one it wants, and the main window's is [Renderer.renderList]. */
val windowIndex: Int = 0
) : Target {
private val ctx get() = contextProvider()
var modules = mutableMapOf<String, ModulePointer>()
The modules drawn into this window, by name, recorded in insertion order before meshModules. Add with +=; passing a name-to-ModulePointer pair takes any entry already under that name out first, so re-adding one moves it to the end of the order.
var meshModules = mutableMapOf<String, MeshModulePointer>()
The mesh-shader modules drawn into this window, by name, recorded after modules and in their own insertion order.
var others = mutableMapOf<String, Target>()
Targets rendered after this one's pass has ended, by name — the render-to-textures and compute targets chained behind the window. A RendersAfter entry is moved behind the target it names before the list runs.
var preRecordedInsideRendering: List<CommandBuffer>? = null
Secondary command buffers to execute inside the rendering instance in place of recording modules and meshModules this frame. It is consumed: the field is set back to null once the frame has used it.
var preTargets = mutableMapOf<String, Target>()
Targets rendered before any of this one's render-pass work, by name — where an upload or streaming copy goes, since a transfer cannot be recorded inside dynamic rendering.
operator fun plusAssign(m : Pair<String, ModulePointer>)
@JvmName("plusAssign")
operator fun plusAssign(m : Pair<String, ModulePointer>) {
if (m.first in modules) modules.remove(m.first) // allow reordering
modules.putOwned(m.first, m.second, renderer)
}
operator fun plusAssign(m : ModulePointer)
@JvmName("plusAssign_mp")
operator fun plusAssign(m : ModulePointer) {
modules.putOwned(m.current.parameters.name, m, renderer)
}
operator fun plusAssign(m : Pair<String, Module>)
@JvmName("plusAssign_module")
operator fun plusAssign(m : Pair<String, Module>) {
modules.putOwned(m.first, m.second.pointer(), renderer)
}
operator fun plusAssign(m : Pair<String, MeshModulePointer>)
@JvmName("plusAssign_mesh")
operator fun plusAssign(m : Pair<String, MeshModulePointer>) {
meshModules.putOwned(m.first, m.second, renderer)
}
operator fun plusAssign(m : MeshModulePointer)
@JvmName("plusAssign_mmp")
operator fun plusAssign(m : MeshModulePointer) {
meshModules.putOwned(m.current.parameters.name, m, renderer)
}
operator fun plusAssign(m : Pair<String, MeshModule>)
@JvmName("plusAssign_mmodule")
operator fun plusAssign(m : Pair<String, MeshModule>) {
meshModules.putOwned(m.first, m.second.pointer(), renderer)
}
operator fun plusAssign(m : Pair<String, Target>)
@JvmName("plusAssign_target")
operator fun plusAssign(m : Pair<String, Target>) {
others.putOwned(m.first, m.second, renderer)
}
var seenBefore = mutableSetOf<Int>()
The swapchain image indices this target has already presented, which decide whether a frame's first barrier declares the old layout as present-source or undefined. It is cleared when the swapchain is recreated, since a resize hands back brand-new images.
Identity of the swapchain seenBefore was populated against. When the swapchain is recreated
(resize, or the Wayland fullscreen configure landing after startup) its images are brand-new
and start in UNDEFINED layout, making the "seen -> oldLayout = PRESENT_SRC" assumption stale.
If we don't reset, the first frame per image submits a barrier with the wrong oldLayout and
validation flags SwapchainImagei as UNDEFINED-vs-PRESENT_SRC_KHR (once per image).
private var seenBeforeSwapChain: Long = 0L
override fun render(command: CommandBuffer, index: Int)
body — 244 lines, from line 108
override fun render(command: CommandBuffer, index: Int) {
val swapChainExtent = ctx.swapChain.swapChainExtent
val width = swapChainExtent.width()
val height = swapChainExtent.height()
if (ctx.swapChain.vkSwapChain != seenBeforeSwapChain) {
seenBefore.clear()
seenBeforeSwapChain = ctx.swapChain.vkSwapChain
}
MemoryStack.stackPush().use { stack ->
// Run pre-render targets (e.g., streaming uploads) before any render pass work.
preTargets.forEach { (_, tgt) -> tgt.render(command, index) }
// Subresource ranges (stack-allocated, freed at end of scope)
val colorRange = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(VK_REMAINING_MIP_LEVELS)
.baseArrayLayer(0)
.layerCount(VK_REMAINING_ARRAY_LAYERS)
val depthRange = VkImageSubresourceRange.calloc(stack)
.aspectMask(Options.depthAspectMask(ctx.depthAttachments[index].image.format))
.baseMipLevel(0)
.levelCount(VK_REMAINING_MIP_LEVELS)
.baseArrayLayer(0)
.layerCount(VK_REMAINING_ARRAY_LAYERS)
val depthLayout =
if (Options.depthHasStencil(ctx.depthAttachments[index].image.format))
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
else
VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL
// Initial layout transition for the swapchain image to COLOR_ATTACHMENT_OPTIMAL.
// If we've presented this image before, oldLayout is PRESENT; otherwise UNDEFINED.
run {
val (srcStage, srcAccess, oldLayout) =
if (seenBefore.contains(index))
Triple(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, 0L, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR)
else
// Note: Use COLOR_ATTACHMENT_OUTPUT so this transition is synchronized by the
// swapchain acquire semaphore wait (sync validation on MoltenVK).
Triple(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, 0L, VK_IMAGE_LAYOUT_UNDEFINED)
image_layout_transition(
command.vkCommandBuffer,
ctx.swapChain.imageViews[index].image,
srcStage,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
srcAccess,
VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
oldLayout,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
colorRange
)
}
// Depth initial transition (UNDEFINED -> DEPTH_STENCIL_ATTACHMENT_OPTIMAL)
image_layout_transition(
command.vkCommandBuffer,
ctx.depthAttachments[index].image,
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT or VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT,
0,
VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT or VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
depthLayout,
depthRange
)
seenBefore.add(index)
// Rendering attachments (KHR)
val colorAttachment =
VkRenderingAttachmentInfoKHR.calloc(1, stack)
.sType(VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR)
.imageView(ctx.swapChain.imageViews[index].vkImageView)
.imageLayout(vkLayout(VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL))
.resolveMode(VK_RESOLVE_MODE_NONE)
.loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR)
.storeOp(VK_ATTACHMENT_STORE_OP_STORE)
.clearValue(
VkClearValue.calloc(stack).color(
VkClearColorValue.calloc(stack)
.float32(0, 0.0f)
.float32(1, 0.0f)
.float32(2, 0.0f)
.float32(3, 1f)
)
)
val depthAttachment =
VkRenderingAttachmentInfoKHR.calloc(1, stack)
.sType(VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR)
.imageView(ctx.depthAttachments[index].imageView.vkImageView)
.imageLayout(vkLayout(depthLayout))
.resolveMode(VK_RESOLVE_MODE_NONE)
.loadOp(VK_ATTACHMENT_LOAD_OP_CLEAR)
.storeOp(VK_ATTACHMENT_STORE_OP_DONT_CARE)
.clearValue(
VkClearValue.calloc(stack).depthStencil(
VkClearDepthStencilValue.calloc(stack)
.depth(1f)
.stencil(0)
)
)
val recorded = preRecordedInsideRendering
val useSecondaryInsideRendering = !recorded.isNullOrEmpty()
val renderingFlags = if (useSecondaryInsideRendering) {
VK_RENDERING_CONTENTS_SECONDARY_COMMAND_BUFFERS_BIT or
KHRMaintenance7.VK_RENDERING_CONTENTS_INLINE_BIT_KHR
} else 0
// Rendering info (KHR)
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(renderingFlags)
.layerCount(1)
.pColorAttachments(colorAttachment)
.pDepthAttachment(depthAttachment.get(0))
// Upload dirty vertex/index buffers before beginning rendering (transfer ops are not allowed inside).
// This keeps Mesh/VertexBuffer updates low-noise and spec-correct.
modules.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))
}
}
}
meshModules.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.forEach { (_, mp) ->
mp.current.before.forEach { (_, target) ->
target.render(command, index)
}
}
meshModules.forEach { (_, mp) ->
mp.current.before.forEach { (_, target) ->
target.render(command, index)
}
}
// Texture providers may need layout transitions before descriptors are pushed.
// Dynamic rendering does not allow image barriers inside the rendering instance.
modules.forEach { (_, mp) -> mp.prepareSampledImagesFor(command) }
meshModules.forEach { (_, mp) -> mp.prepareSampledImagesFor(command) }
command.inRenderPassInstance = true
KHRDynamicRendering.vkCmdBeginRenderingKHR(command.vkCommandBuffer, renderInfo)
val fullRect = Rect(0.0, 0.0, width.toDouble(), height.toDouble())
if (recorded.isNullOrEmpty()) {
modules.forEach { (_, mp) ->
setViewportAndScissor(command.vkCommandBuffer, mp.current.parameters.viewport ?: fullRect, stack)
mp.render(command, Rectanglef(0f, 0f, width.toFloat(), height.toFloat()))
}
meshModules.forEach { (_, mp) ->
setViewportAndScissor(command.vkCommandBuffer, mp.current.parameters.viewport ?: fullRect, stack)
mp.render(command, Rectanglef(0f, 0f, width.toFloat(), height.toFloat()))
}
} else {
// Execute pre-recorded modules/mesh modules in order
val pb = stack.mallocPointer(recorded.size)
recorded.forEach { cb ->
pb.put(cb.vkCommandBuffer.address())
}
pb.flip()
VK10.vkCmdExecuteCommands(command.vkCommandBuffer, pb)
}
preRecordedInsideRendering = null
KHRDynamicRendering.vkCmdEndRenderingKHR(command.vkCommandBuffer)
command.inRenderPassInstance = false
command.flushDeferredGraphicsHostReadBarriers()
// Transition swapchain image to PRESENT for presentation engine.
// With sync2 you can use NONE for dst stage/access.
image_layout_transition(
command.vkCommandBuffer,
ctx.swapChain.imageViews[index].image,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_2_NONE,
VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT,
0,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
colorRange
)
}
// Render any chained targets after we've finished this one, each RendersAfter pass after its target.
orderRendersAfter(others)
others.forEach { it.value.render(command, index) }
}
private fun setViewportAndScissor(cmd: VkCommandBuffer, rect: Rect, stack: MemoryStack) {
val viewport = VkViewport.calloc(1, stack)
.x(rect.x.toFloat())
.y((rect.y + rect.h).toFloat()) // flip Y
.height((-rect.h).toFloat())
.width(rect.w.toFloat())
.minDepth(0f).maxDepth(1f)
VK10.vkCmdSetViewport(cmd, 0, viewport)
val scissor = VkRect2D.calloc(1, stack)
.extent(
VkExtent2D.calloc(stack)
.width(rect.w.toInt())
.height(rect.h.toInt())
)
.offset(VkOffset2D.calloc(stack).x(rect.x.toInt()).y(rect.y.toInt()))
VK10.vkCmdSetScissor(cmd, 0, scissor)
}
}
interface RendersAfter
A pass that has to render after another Target in the same list: a publisher after the texture it publishes. Wherever it was added, RenderTarget moves it behind rendersAfter before it iterates.
interface RendersAfter {
val rendersAfter: Target
The target this pass has to follow. RenderTarget looks for it in the same others map and moves this entry behind it; a target that is not in that map leaves the order alone.
internal fun orderRendersAfter(list: MutableMap<String, Target>)
Moves each RendersAfter entry of list behind its target, when the target comes later.
internal fun orderRendersAfter(list: MutableMap<String, Target>) {
if (list.values.none { it is RendersAfter }) return
repeat(list.size) {
val keys = list.keys.toList()
val values = list.values.toList()
val late = keys.indices.firstOrNull { i ->
val after = (values[i] as? RendersAfter)?.rendersAfter ?: return@firstOrNull false
values.indexOfFirst { it === after } > i
} ?: return
val key = keys[late]
val value = list.remove(key)!!
list[key] = value
}
}