PingPongRenderToTexture.kt
src/jvmMain/kotlin/afield/graphics/vulkan/targets/PingPongRenderToTexture.kt · 795 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.device.CommandBuffer
import afield.graphics.vulkan.device.Fence
import afield.graphics.render.ModulePointer
import afield.document.context.fromContext
import afield.graphics.texture.HasDefaultTextureSampler
import afield.graphics.texture.HasImageView
import afield.graphics.texture.HasImageViewArray
import afield.graphics.texture.Image
import afield.graphics.texture.Image.ImageData
import afield.graphics.texture.ImageView
import afield.graphics.texture.ImageView.ImageViewData
import afield.graphics.texture.TextureSampler
import afield.graphics.texture.image_layout_transition
import afield.math.linalg.f
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.VK10.VK_IMAGE_ASPECT_COLOR_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
import org.lwjgl.vulkan.VK10.VK_IMAGE_LAYOUT_UNDEFINED
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_SAMPLED_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_TRANSFER_DST_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_USAGE_TRANSFER_SRC_BIT
import org.lwjgl.vulkan.VK10.VK_IMAGE_VIEW_TYPE_2D_ARRAY
import org.lwjgl.vulkan.VK10.vkCmdClearColorImage
import org.lwjgl.vulkan.VK13.VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT
import org.lwjgl.vulkan.VK13.VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
import org.lwjgl.vulkan.VK13.VK_ACCESS_2_TRANSFER_WRITE_BIT
import org.lwjgl.vulkan.VK13.VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT
import org.lwjgl.vulkan.VK13.VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT
import org.lwjgl.vulkan.VK13.VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT
import org.lwjgl.vulkan.VkClearColorValue
import org.lwjgl.vulkan.VkImageSubresourceRange
Feedback rendering: PingPongRenderToTexture keeps a ring of images per color attachment, so a module can sample the previous frame while drawing the next.
Each frame renders into one slot of the ring and exposes the earlier, completed slots as textures, which is the shape of trails, reaction-diffusion and other effects that read their own last output. FrameCadencedPingPongRenderToTexture advances the ring only every few frames, for a slow sequence that downstream code crossfades between.
open class PingPongRenderToTexture
RenderToTexture variant that ring-buffers every color attachment. Each frame it renders into one image set while exposing previous completed frames via otherImageView and previousFrameImageViews, so modules can sample feedback history without managing several RTT instances.
open class PingPongRenderToTexture(
renderer: Renderer = fromContext(),
frameCount: Int = 2,
parameters: RTTParameters.() -> Unit = {}
) : RenderToTexture(renderer, parameters) {
val frameCount: Int
How many images the ring holds per color attachment, and so how far back previousFrameImageViews reaches: a frame writes one slot and the other frameCount - 1 stay readable. Two or more, and two is the plain feedback case.
val frameCount: Int = frameCount.also {
require(it >= 2) { "PingPongRenderToTexture requires frameCount >= 2" }
}
private val pingAttachmentCount = colorImageHandles.size
private val pingImages: List<List<Image>>
protected val pingImageViews: List<List<HasImageView>>
private val pingAttachmentViews: List<List<ImageView>>
private val pingImageHandles: List<LongArray>
private val pingLayouts = Array(pingAttachmentCount) { IntArray(this.frameCount) { VK_IMAGE_LAYOUT_UNDEFINED } }
private val pingMipsInitialized = Array(pingAttachmentCount) { BooleanArray(this.frameCount) { false } }
protected var writeIndex = 0
private var boundFrameIndex = 0
protected var latestIndex
protected var latestIndex = 0
private set
protected var previousIndex
protected var previousIndex = this.frameCount - 1
private set
open val otherImageView: HasImageView = otherImageViewFor(0)
open val thisImageView: HasImageView = thisImageViewFor(0)
val latestImageView: HasImageView = latestImageViewFor(0)
Color attachment 0 of the slot the last completed frame wrote. The object is fixed and its view follows the ring, so a binding made once keeps reading the latest frame; while a pass records, that is the same image otherImageView gives.
val previousImageView: HasImageView = previousImageViewFor(0)
Color attachment 0 of the slot written one frame before latestImageView's, which is two frames back from the one being written.
val colorAttachmentCount: Int
How many color attachments the ring carries, each with its own frameCount images. latestImageViewFor and the other per-attachment accessors take a slot in 0 until colorAttachmentCount.
val colorAttachmentCount: Int
get() = pingAttachmentCount
fun recordReplaceHistory
Replace every history slot with one coherent external state, without rotating the ring.
Intended for prepared playback cuts: copy records a GPU transfer into each image. The
ordinary RTT transition helper owns the layouts, so subsequent feedback cannot read stale
pre-cut history. Requires colorTransferDst = true, colorSampled = true and a single
mip level. Call outside a render pass on the graphics recording thread. The callback
records a copy of mip 0, including every array layer; it must not alias any ring image.
Ring indices, cadence and the currently bound attachment remain unchanged. This records
commands only; Renderer.whenCurrentGraphicsFrameComplete can report their completion.
fun recordReplaceHistory(
command: CommandBuffer,
attachment: Int = 0,
copy: (vkImage: Long) -> Unit,
) {
checkedAttachment(attachment)
check(!command.inRenderPassInstance) { "recordReplaceHistory must be recorded outside a render pass" }
check(parameters.colorTransferDst && parameters.colorSampled && mipLevels == 1) {
"recordReplaceHistory requires colorTransferDst, colorSampled and one mip level"
}
val savedBound = boundFrameIndex
try {
for (slot in 0 until frameCount) {
bindFrame(slot)
recordColorTransferDestination(command, attachment, copy)
pingLayouts[attachment][slot] = colorLayout[attachment]
}
} finally {
bindFrame(savedBound)
}
}
protected fun checkedAttachment(attachment: Int): Int
protected fun checkedAttachment(attachment: Int): Int {
require(attachment in 0 until pingAttachmentCount) {
"color attachment $attachment out of range; expected 0..${pingAttachmentCount - 1}"
}
return attachment
}
protected fun pingImageView(frame: Int, attachment: Int): ImageView
protected fun pingImageView(frame: Int, attachment: Int): ImageView =
pingImageViews[checkedAttachment(attachment)][wrapFrameIndex(frame)].imageView
open fun otherImageViewFor(attachment: Int): HasImageView
open fun otherImageViewFor(attachment: Int): HasImageView {
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(otherIndex, attachment)
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
open fun thisImageViewFor(attachment: Int): HasImageView
open fun thisImageViewFor(attachment: Int): HasImageView {
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(writeIndex, attachment)
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
fun latestImageViewFor(attachment: Int): HasImageView
fun latestImageViewFor(attachment: Int): HasImageView {
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(latestIndex, attachment)
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
fun previousImageViewFor(attachment: Int): HasImageView
fun previousImageViewFor(attachment: Int): HasImageView {
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(previousIndex, attachment)
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
val previousFrameImageViews: HasImageViewArray
Previous completed frames in render-time order: index 0 is the most recent completed frame, index 1 is the frame before that, and so on.
Bind this to sampler2D history[N](../../../../../../../reference/source/jvmMain/afield/audio/synth/DrawnMorphDocs.md#L60) where N == frameCount - 1.
val previousFrameImageViews: HasImageViewArray = object : HasImageViewArray, HasDefaultTextureSampler {
override val imageViewArray: List<HasImageView>
get() = (1 until this@PingPongRenderToTexture.frameCount).map { previousFrameImageView(it) }
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
fun previousFrameImageViewsFor(attachment: Int): HasImageViewArray
fun previousFrameImageViewsFor(attachment: Int): HasImageViewArray {
checkedAttachment(attachment)
return object : HasImageViewArray, HasDefaultTextureSampler {
override val imageViewArray: List<HasImageView>
get() = (1 until this@PingPongRenderToTexture.frameCount).map {
previousFrameImageView(it, attachment)
}
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
val frameImageViews: HasImageViewArray
All frame slots in write order, starting with writeIndex. Outside the update render this is oldest-to-newest for a warmed-up ring, with thisImageView substituted at its physical slot.
For a frame-cadenced target, adjacent elements are therefore adjacent in
history and the direct otherImageView -> thisImageView cadence blend is
the final adjacent pair in the array.
val frameImageViews: HasImageViewArray = object : HasImageViewArray, HasDefaultTextureSampler {
override val imageViewArray: List<HasImageView>
get() = (0 until this@PingPongRenderToTexture.frameCount).map { frameImageView(it) }
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
fun frameImageViewsFor(attachment: Int): HasImageViewArray
fun frameImageViewsFor(attachment: Int): HasImageViewArray {
checkedAttachment(attachment)
return object : HasImageViewArray, HasDefaultTextureSampler {
override val imageViewArray: List<HasImageView>
get() = (0 until this@PingPongRenderToTexture.frameCount).map {
frameImageView(it, attachment)
}
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
val writeFrameImageViews: HasImageViewArray
Explicit alias for frameImageViews, named for code that wants to state that it depends on the write-order rotation.
val writeFrameImageViews: HasImageViewArray = object : HasImageViewArray, HasDefaultTextureSampler {
override val imageViewArray: List<HasImageView>
get() = (0 until this@PingPongRenderToTexture.frameCount).map { writeFrameImageView(it) }
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
fun writeFrameImageViewsFor(attachment: Int): HasImageViewArray
fun writeFrameImageViewsFor(attachment: Int): HasImageViewArray {
checkedAttachment(attachment)
return object : HasImageViewArray, HasDefaultTextureSampler {
override val imageViewArray: List<HasImageView>
get() = (0 until this@PingPongRenderToTexture.frameCount).map {
writeFrameImageView(it, attachment)
}
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
protected open val frameImageViewWriteStartIndex: Int
protected open val frameImageViewWriteStartIndex: Int
get() = writeIndex
protected open val frameImageViewThisIndex: Int
protected open val frameImageViewThisIndex: Int
get() = writeIndex
open fun frameImageView(offset: Int): HasImageView = frameImageView(offset, 0)
Returns a frame by this target's default binding order.
open fun frameImageView(offset: Int, attachment: Int): HasImageView
open fun frameImageView(offset: Int, attachment: Int): HasImageView = writeFrameImageView(offset, attachment)
fun writeFrameImageView(offset: Int): HasImageView = writeFrameImageView(offset, 0)
Returns a frame by write-order offset. Offset 0 is writeIndex, offset 1 is the next physical ring slot, and so on. When the selected physical slot is the slot represented by thisImageView, it resolves through thisImageView so subclass dynamic semantics are preserved.
fun writeFrameImageView(offset: Int, attachment: Int): HasImageView
fun writeFrameImageView(offset: Int, attachment: Int): HasImageView {
require(offset in 0 until frameCount) {
"frame offset $offset out of range; expected 0..${frameCount - 1}"
}
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() {
val physicalIndex = wrapFrameIndex(frameImageViewWriteStartIndex + offset)
return if (physicalIndex == frameImageViewThisIndex) {
thisImageViewFor(attachment).imageView
} else {
pingImageView(physicalIndex, attachment)
}
}
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
fun previousFrameImageView(age: Int): HasImageView = previousFrameImageView(age, 0)
Returns the Nth previous completed frame relative to the next/current
render. age == 1 is the most recent completed frame.
fun previousFrameImageView(age: Int, attachment: Int): HasImageView
fun previousFrameImageView(age: Int, attachment: Int): HasImageView {
require(age in 1 until frameCount) {
"previous frame age $age out of range; expected 1..${frameCount - 1}"
}
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(previousFrameIndex(age), attachment)
override var defaultSampler: TextureSampler?
get() = this@PingPongRenderToTexture.defaultSampler
set(value) {
this@PingPongRenderToTexture.defaultSampler = value
}
}
}
val previousFrameCount: Int
How many completed frames are readable while one is being written: frameCount less the slot in use, and the length of previousFrameImageViews — the array size a sampler2D history binding declares.
val previousFrameCount: Int
get() = frameCount - 1
protected val otherIndex: Int
protected val otherIndex: Int
get() = previousFrameIndex(1)
protected fun previousFrameIndex(age: Int): Int
protected fun previousFrameIndex(age: Int): Int {
require(age in 1 until frameCount) {
"previous frame age $age out of range; expected 1..${frameCount - 1}"
}
return wrapFrameIndex(writeIndex - age)
}
protected fun wrapFrameIndex(index: Int): Int
protected fun wrapFrameIndex(index: Int): Int {
val wrapped = index % frameCount
return if (wrapped < 0) wrapped + frameCount else wrapped
}
private data class PingFrameResources(
val image: Image,
val sampledView: ImageView,
val attachmentView: ImageView
)
private var additionalFrameBuildIndex: Int = 0
init
body — 207 lines, from line 376
init {
require(colorImageHandles.isNotEmpty()) { "PingPongRenderToTexture requires at least one color attachment" }
require(this.parameters.externalColorViews.isEmpty()) {
"PingPongRenderToTexture does not support external color views; every color attachment is ring-buffered"
}
val baseImages = List(pingAttachmentCount) { attachment ->
requireNotNull(colorImageRefs[attachment]) {
"PingPongRenderToTexture attachment $attachment must be internally owned"
}
}
val idleLayout =
if (this.parameters.colorSampled) VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
else VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
fun imageDataFor(attachment: Int): ImageData {
val wantsMips = this.parameters.mipLevels > 1 && this.parameters.generateColorMipmaps
val needsTransfer = wantsMips && colorFormatCanBlit.getOrElse(attachment) { false }
return ImageData()
.width(width)
.height(height)
.arrayLayers(colorLayerCount.coerceAtLeast(1))
.usage(
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT or
(if (this.parameters.colorSampled) VK_IMAGE_USAGE_SAMPLED_BIT else 0) or
(if (this.parameters.colorTransferSrc) VK_IMAGE_USAGE_TRANSFER_SRC_BIT else 0) or
(if (this.parameters.colorTransferDst || this.parameters.colorSampled) VK_IMAGE_USAGE_TRANSFER_DST_BIT else 0) or
(if (needsTransfer) VK_IMAGE_USAGE_TRANSFER_SRC_BIT or VK_IMAGE_USAGE_TRANSFER_DST_BIT else 0)
)
.format(colorFormats[attachment])
.mipLevels(mipLevels)
.debugName(pingDebugName("color", attachment))
.aliasSingleLayerDescriptors(this.parameters.forceColorArrayView)
}
fun createAdditionalFrame(attachment: Int): PingFrameResources {
val frame = additionalFrameBuildIndex
val imageData = imageDataFor(attachment)
val image = Image(renderer.device, imageData)
val viewData = ImageViewData()
.format(imageData.format)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.layerCount(imageData.arrayLayers)
.mipLevels(mipLevels)
.debugName(pingDebugName("sampled-view", attachment, frame))
if (viewData.layerCount > 1 || this.parameters.forceColorArrayView) {
viewData.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
}
val sampledView = ImageView(renderer.device, image, viewData)
val attachmentView =
if (mipLevels == 1) sampledView
else {
val avd = ImageViewData()
.format(imageData.format)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.layerCount(imageData.arrayLayers)
.mipLevels(1)
.debugName(pingDebugName("attachment-view", attachment, frame))
if (avd.layerCount > 1 || this.parameters.forceColorArrayView) {
avd.viewType(VK_IMAGE_VIEW_TYPE_2D_ARRAY)
}
ImageView(renderer.device, image, avd)
}
return PingFrameResources(image, sampledView, attachmentView)
}
val additionalFrames = (0 until pingAttachmentCount).map { attachment ->
(1 until this.frameCount).map { frame ->
additionalFrameBuildIndex = frame
createAdditionalFrame(attachment)
}
}
pingImages = (0 until pingAttachmentCount).map { attachment ->
listOf(baseImages[attachment]) + additionalFrames[attachment].map { it.image }
}
pingImageViews = (0 until pingAttachmentCount).map { attachment ->
listOf(mutableImageViewArray[attachment]) + additionalFrames[attachment].map { it.sampledView }
}
pingAttachmentViews = (0 until pingAttachmentCount).map { attachment ->
listOf(attachmentImageViews[attachment]) + additionalFrames[attachment].map { it.attachmentView }
}
pingImageHandles = (0 until pingAttachmentCount).map { attachment ->
LongArray(this.frameCount) { frame ->
if (frame == 0) colorImageHandles[attachment] else additionalFrames[attachment][frame - 1].image.vkImage
}
}
initializePingImagesNow(idleLayout)
}
private fun pingDebugName(kind: String, attachment: Int, frame: Int = additionalFrameBuildIndex): String {
val prefix = parameters.debugName.trim().ifEmpty { "PingPongRenderToTexture" }
return "$prefix ping[$frame] $kind[$attachment] ${width}x${height}"
}
private fun recordIdleImageInitialization(command: CommandBuffer, image: Image, finalLayout: Int) {
MemoryStack.stackPush().use { stack ->
val rangeBuffer = VkImageSubresourceRange.calloc(1, stack)
val fullRange = rangeBuffer[0]
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(mipLevels)
.baseArrayLayer(0)
.layerCount(colorLayerCount.coerceAtLeast(1))
val canTransferClear = (image.imageData.usage and VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0
if (canTransferClear) {
image_layout_transition(
command.vkCommandBuffer,
image,
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
0L,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
fullRange
)
val clearColor = VkClearColorValue.calloc(stack)
.float32(0, parameters.clearColor.x.f)
.float32(1, parameters.clearColor.y.f)
.float32(2, parameters.clearColor.z.f)
.float32(3, parameters.clearColor.w.f)
vkCmdClearColorImage(
command.vkCommandBuffer,
image.vkImage,
vkLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),
clearColor,
rangeBuffer
)
val (dstStage, dstAccess) =
if (finalLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
sampledReadStageMask to VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
else
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT to VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT
image_layout_transition(
command.vkCommandBuffer,
image,
VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT,
dstStage,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
dstAccess,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
finalLayout,
fullRange
)
} else {
val (dstStage, dstAccess) =
if (finalLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
sampledReadStageMask to VK_ACCESS_2_SHADER_SAMPLED_READ_BIT
else
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT to VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT
image_layout_transition(
command.vkCommandBuffer,
image,
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
dstStage,
0L,
dstAccess,
VK_IMAGE_LAYOUT_UNDEFINED,
finalLayout,
fullRange
)
}
}
}
private fun initializePingImagesNow(finalLayout: Int) {
val command = CommandBuffer(renderer.commandPool, true, true)
val fence = Fence(renderer.device, true)
try {
command.beginRecording()
pingImages.forEach { attachmentImages ->
attachmentImages.forEach { image ->
recordIdleImageInitialization(command, image, finalLayout)
}
}
command.endRecording()
fence.reset()
MemoryStack.stackPush().use { stack ->
renderer.graphQueue.submit(stack.pointers(command.vkCommandBuffer), null, null, null, fence)
}
fence.fenceWait()
for (attachment in 0 until pingAttachmentCount) {
for (frame in 0 until frameCount) {
pingLayouts[attachment][frame] = finalLayout
pingMipsInitialized[attachment][frame] = true
}
colorLayout[attachment] = finalLayout
colorMipsInitialized[attachment] = true
}
} finally {
fence.cleanup()
command.cleanup()
}
}
override fun blitToScreen
Blits the latest ping-pong output to a screen target.
Registers this object as the named source pre-target and removes duplicate
others entries before delegating to RenderToTexture.blitToScreen.
override fun blitToScreen(
renderer: Renderer,
target: RenderTarget,
attachment: Int,
name: String,
viewport: Rect?,
sourceRect: Rect?,
blend: Boolean,
additive: Boolean,
sourceLayer: Int,
warper: String?,
numSegmentsW: Int,
numSegmentsH: Int,
configure: ColorGrade.() -> Unit
): ModulePointer {
target.preTargets["$name-source"] = this
target.others.entries.removeIf { it.value === this }
return super.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
)
}
override fun debugBlitToScreen
override fun debugBlitToScreen(
renderer: Renderer,
target: RenderTarget,
attachment: Int,
name: String,
viewport: Rect?,
sourceRect: Rect?,
blend: Boolean,
additive: Boolean,
sourceLayer: Int,
warper: String?,
numSegmentsW: Int,
numSegmentsH: Int,
configure: ColorGrade.() -> Unit
): ModulePointer {
checkedAttachment(attachment)
val latestView = latestImageViewFor(attachment)
val sourceProvider = object : HasImageViewArray {
override val imageViewArray: List<HasImageView>
get() = listOf(latestView)
}
return blitImageToScreen(
sourceProvider = sourceProvider,
sourceView = latestView.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
)
}
override fun beforeRender(index: Int)
override fun beforeRender(index: Int) {
bindFrame(writeIndex)
}
private fun bindFrame(write: Int) {
boundFrameIndex = write
for (attachment in 0 until pingAttachmentCount) {
colorImageRefs[attachment] = pingImages[attachment][write]
colorImageHandles[attachment] = pingImageHandles[attachment][write]
mutableImageViewArray[attachment] = pingImageViews[attachment][write]
attachmentImageViews[attachment] = pingAttachmentViews[attachment][write]
colorLayout[attachment] = pingLayouts[attachment][write]
colorMipsInitialized[attachment] = pingMipsInitialized[attachment][write]
}
}
override fun afterRender(index: Int)
override fun afterRender(index: Int) {
val write = writeIndex
val previous = wrapFrameIndex(write - 1)
for (attachment in 0 until pingAttachmentCount) {
pingLayouts[attachment][write] = colorLayout[attachment]
pingMipsInitialized[attachment][write] = colorMipsInitialized[attachment]
}
latestIndex = write
previousIndex = previous
writeIndex = wrapFrameIndex(write + 1)
}
}
class FrameCadencedPingPongRenderToTexture
A PingPongRenderToTexture that only advances the frame ring once every
framesPerPingPong render calls. Between advances, thisImageView remains
the latest completed texture and otherImageView remains the previous one,
so downstream modules can crossfade from other to this with crossfade.
class FrameCadencedPingPongRenderToTexture(
renderer: Renderer = fromContext(),
framesPerPingPong: Int = 1,
frameCount: Int = 2,
parameters: RTTParameters.() -> Unit = {}
) : PingPongRenderToTexture(renderer, frameCount, parameters) {
private var insidePingPongRender = false
var framesPerPingPong: Int
How many render calls one ring slot is held for: 1 advances every frame, 4 renders once and shows that result for four. Writing a value below 1 stores 1, and shortening the period also pulls framesSincePingPong back inside it, so the next advance is not stranded.
var framesPerPingPong: Int = framesPerPingPong.coerceAtLeast(1)
set(value) {
val safeValue = value.coerceAtLeast(1)
field = safeValue
framesSincePingPong = framesSincePingPong.coerceAtMost(safeValue - 1)
}
var framesSincePingPong: Int
Render calls since the last advance, counting 0 on the frame that advanced. It drives crossfade and framePhase, and reaching framesPerPingPong less one is what makes the next call render.
var framesSincePingPong: Int = 0
private set
var pingPongCount: Long
How many times the ring has advanced, which is how many frames this target has actually rendered as against the render calls it was given. Zero until the first, which is why the first call always renders whatever the cadence says.
var pingPongCount: Long = 0
private set
val framesUntilPingPong: Int
Render calls still to be held before the ring advances again: 0 means the next call renders. It is also 0 before the first advance, which is not waiting on the cadence.
val framesUntilPingPong: Int
get() = if (pingPongCount == 0L) 0 else (framesPerPingPong - 1 - framesSincePingPong).coerceAtLeast(0)
val crossfade: Float
0 immediately after a ping-pong render, 1 on the last held frame before
the next one. Intended for mix(other, this, crossfade).
val crossfade: Float
get() = if (framesPerPingPong <= 1) 1f
else framesSincePingPong.toFloat() / (framesPerPingPong - 1).toFloat()
val framePhase: Float
Similar to crossfade, but normalized by the whole cadence period and therefore stays below 1 before the next ping-pong.
val framePhase: Float
get() = framesSincePingPong.toFloat() / framesPerPingPong.toFloat()
override val frameImageViewThisIndex: Int
override val frameImageViewThisIndex: Int
get() = if (insidePingPongRender) writeIndex else latestIndex
override val thisImageView: HasImageView = thisImageViewFor(0)
override val otherImageView: HasImageView = otherImageViewFor(0)
override fun thisImageViewFor(attachment: Int): HasImageView
override fun thisImageViewFor(attachment: Int): HasImageView {
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(if (insidePingPongRender) writeIndex else latestIndex, attachment)
override var defaultSampler: TextureSampler?
get() = this@FrameCadencedPingPongRenderToTexture.defaultSampler
set(value) {
this@FrameCadencedPingPongRenderToTexture.defaultSampler = value
}
}
}
override fun otherImageViewFor(attachment: Int): HasImageView
override fun otherImageViewFor(attachment: Int): HasImageView {
checkedAttachment(attachment)
return object : HasImageView, HasDefaultTextureSampler {
override val imageView: ImageView
get() = pingImageView(if (insidePingPongRender) otherIndex else previousIndex, attachment)
override var defaultSampler: TextureSampler?
get() = this@FrameCadencedPingPongRenderToTexture.defaultSampler
set(value) {
this@FrameCadencedPingPongRenderToTexture.defaultSampler = value
}
}
}
override fun render(command: CommandBuffer, index: Int)
override fun render(command: CommandBuffer, index: Int) {
val shouldPingPong = pingPongCount == 0L || framesSincePingPong >= framesPerPingPong - 1
if (shouldPingPong) {
insidePingPongRender = true
try {
super.render(command, index)
} finally {
insidePingPongRender = false
}
pingPongCount++
framesSincePingPong = 0
} else {
framesSincePingPong++
}
}
override fun toString(): String
override fun toString(): String {
return "FrameCadencedPingPongRenderToTexture[${width}x${height}, frames=$frameCount, every=$framesPerPingPong, crossfade=$crossfade]"
}
}