CopiedExternalBrowserSurface.kt
src/jvmMain/kotlin/afield/media/browser/CopiedExternalBrowserSurface.kt · 328 lines
package afield.media.browser
import afield.graphics.vulkan.device.vkLayout
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.texture.HasImageView
import afield.graphics.texture.Image
import afield.graphics.texture.ImageView
import afield.graphics.texture.image_layout_transition
import afield.app.diagnostics.logging.lprintln
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK13.*
import org.lwjgl.vulkan.VkClearColorValue
import org.lwjgl.vulkan.VkImageCopy
import org.lwjgl.vulkan.VkImageSubresourceRange
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference
The shared base of the native browser-frame receivers, which copies each imported frame into one stable texture for rendering.
sealed class CopiedExternalBrowserSurface
The platform-independent half of all native browser receivers. A backend only imports a frame's native image; this class owns conflation, the stable resident texture, copies, fence-delayed release acknowledgements, and close ordering.
sealed class CopiedExternalBrowserSurface(
protected val renderer: Renderer,
final override val spec: ExternalBrowserSurfaceSpec,
releaseListener: BrowserFrameReleaseListener
) : ExternalBrowserSurface {
protected class ImportedImage
protected class ImportedImage(
val identity: Any,
val image: Long,
private val cleanupAction: () -> Unit,
var layout: Int = VK_IMAGE_LAYOUT_UNDEFINED,
val externalQueueFamily: Int = VK_QUEUE_FAMILY_IGNORED
) {
fun cleanup() = cleanupAction()
private data class FrameKey(val streamId: Long, val frameId: Long, val releaseToken: Long)
private val inFlight = ConcurrentHashMap<FrameKey, ExternalBrowserFrame>()
protected val onRelease
protected val onRelease = BrowserFrameReleaseListener { release ->
runCatching { releaseListener.released(release) }
.onFailure { lprintln("warning,graphics", "browser frame release listener failed: ${it.message}") }
}
private val mailbox = ExternalBrowserFrameMailbox(spec, onRelease)
private val currentFrame = AtomicReference<ExternalBrowserFrame?>(null)
protected val importVkFormat = spec.pixelFormat.storageVkFormat()
External APIs describe the allocation's channel storage separately from its color space. Import the raw bytes, then copy them into the view format Field should sample.
private val residentVkFormat = spec.pixelFormat.sampledVkFormat()
private val residentImage = Image(
renderer.device,
Image.ImageData()
.width(spec.width)
.height(spec.height)
.format(residentVkFormat)
.mipLevels(1)
.usage(VK_IMAGE_USAGE_TRANSFER_SRC_BIT or VK_IMAGE_USAGE_TRANSFER_DST_BIT or VK_IMAGE_USAGE_SAMPLED_BIT)
.debugName("external-browser-resident ${spec.width}x${spec.height}")
)
private val residentView = ImageView(
renderer.device,
residentImage,
ImageView.ImageViewData()
.format(residentVkFormat)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.mipLevels(1)
.debugName("external-browser-resident-view ${spec.width}x${spec.height}")
)
@Volatile
private var residentLayout = VK_IMAGE_LAYOUT_UNDEFINED
@Volatile
private var closed = false
final override val imageView: ImageView
final override val imageView: ImageView
get() = residentView
final override val imageViewArray: List<HasImageView>
final override val imageViewArray: List<HasImageView>
get() = listOf(this)
final override val presentedFrame: ExternalBrowserFrame?
final override val presentedFrame: ExternalBrowserFrame?
get() = currentFrame.get()
final override fun offerFrame(frame: ExternalBrowserFrame): BrowserFrameOfferResult
final override fun offerFrame(frame: ExternalBrowserFrame): BrowserFrameOfferResult = mailbox.offer(frame)
final override fun prepareSampledImagesFor(command: CommandBuffer)
final override fun prepareSampledImagesFor(command: CommandBuffer) {
if (closed) return
check(!command.inRenderPassInstance) {
"External browser texture copies must be recorded before dynamic rendering begins"
}
val frame = mailbox.takeLatest()
if (frame == null) {
ensureResidentInitialized(command)
return
}
val imported = runCatching { importFrame(frame) }
.onFailure {
lprintln(
"warning,graphics",
"could not import browser frame ${frame.streamId}:${frame.frameId}: ${it.message ?: it::class.simpleName}"
)
}
.getOrNull()
if (imported == null) {
// A backend may reject the handle type before its normal import/consume path.
runCatching { frame.handle?.close() }
release(frame, BrowserFrameReleaseReason.IMPORT_FAILED)
ensureResidentInitialized(command)
return
}
recordCopy(command, imported)
val key = frame.key()
inFlight[key] = frame
renderer.afterCurrentGraphicsFrameComplete {
if (inFlight.remove(key, frame)) {
currentFrame.set(frame.copy(handle = null))
release(frame, BrowserFrameReleaseReason.PRESENTED)
}
}
}
protected abstract fun importFrame(frame: ExternalBrowserFrame): ImportedImage?
Return the imported image for frame, using a slot cache when its handle is omitted.
protected abstract fun cleanupImports()
Destroy currently cached imports. The device is idle when this is called.
protected fun retire(imported: ImportedImage)
protected fun retire(imported: ImportedImage) {
// Replacement can occur while a previous graphics frame still references this image.
renderer.cleanup.add(Runnable { imported.cleanup() })
}
protected open fun recordPreCopy(command: CommandBuffer, source: ImportedImage) = Unit
Recorded immediately before the copy into the resident texture, on the same command buffer. A backend that has to get pixels onto the GPU itself — rather than importing an allocation that already holds them — stages that work here, so it still inherits the conflation, fence and release ordering rather than reimplementing them.
source — 133 lines, from line 153
private fun recordCopy(command: CommandBuffer, source: ImportedImage) {
recordPreCopy(command, source)
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(1)
image_layout_transition(
command.vkCommandBuffer,
source.image,
if (source.externalQueueFamily != VK_QUEUE_FAMILY_IGNORED) VK_PIPELINE_STAGE_2_NONE
else if (source.layout == VK_IMAGE_LAYOUT_UNDEFINED) VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT
else VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_2_COPY_BIT,
0,
VK_ACCESS_2_TRANSFER_READ_BIT,
source.layout,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
range,
source.externalQueueFamily,
if (source.externalQueueFamily == VK_QUEUE_FAMILY_IGNORED) VK_QUEUE_FAMILY_IGNORED
else renderer.graphQueue.queueFamilyIndex
)
image_layout_transition(
command.vkCommandBuffer,
residentImage,
if (residentLayout == VK_IMAGE_LAYOUT_UNDEFINED) VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT else sampledStages,
VK_PIPELINE_STAGE_2_COPY_BIT,
if (residentLayout == VK_IMAGE_LAYOUT_UNDEFINED) 0 else VK_ACCESS_2_SHADER_SAMPLED_READ_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
residentLayout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
range
)
val region = VkImageCopy.calloc(1, stack)
region[0]
.srcSubresource {
it.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(1)
}
.srcOffset { it.set(0, 0, 0) }
.dstSubresource {
it.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(1)
}
.dstOffset { it.set(0, 0, 0) }
.extent { it.set(spec.width, spec.height, 1) }
vkCmdCopyImage(
command.vkCommandBuffer,
source.image,
vkLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),
residentImage.vkImage,
vkLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),
region
)
image_layout_transition(
command.vkCommandBuffer,
source.image,
VK_PIPELINE_STAGE_2_COPY_BIT,
if (source.externalQueueFamily == VK_QUEUE_FAMILY_IGNORED) VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT
else VK_PIPELINE_STAGE_2_NONE,
VK_ACCESS_2_TRANSFER_READ_BIT,
0,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_GENERAL,
range,
if (source.externalQueueFamily == VK_QUEUE_FAMILY_IGNORED) VK_QUEUE_FAMILY_IGNORED
else renderer.graphQueue.queueFamilyIndex,
source.externalQueueFamily
)
image_layout_transition(
command.vkCommandBuffer,
residentImage,
VK_PIPELINE_STAGE_2_COPY_BIT,
sampledStages,
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
)
}
source.layout = VK_IMAGE_LAYOUT_GENERAL
residentLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
}
private fun ensureResidentInitialized(command: CommandBuffer) {
if (residentLayout != VK_IMAGE_LAYOUT_UNDEFINED) return
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)
.baseMipLevel(0)
.levelCount(1)
.baseArrayLayer(0)
.layerCount(1)
image_layout_transition(
command.vkCommandBuffer,
residentImage,
VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_2_CLEAR_BIT,
0,
VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
range
)
val black = VkClearColorValue.calloc(stack)
.float32(0, 0f).float32(1, 0f).float32(2, 0f).float32(3, 1f)
vkCmdClearColorImage(
command.vkCommandBuffer,
residentImage.vkImage,
vkLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),
black,
range
)
image_layout_transition(
command.vkCommandBuffer,
residentImage,
VK_PIPELINE_STAGE_2_CLEAR_BIT,
sampledStages,
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
)
}
residentLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
}
final override fun close()
body — 41 lines, from line 287
final override fun close() {
if (closed) return
closed = true
mailbox.close()
renderer.device.waitIdle()
inFlight.entries.toList().forEach { (key, frame) ->
if (inFlight.remove(key, frame)) release(frame, BrowserFrameReleaseReason.CLOSED)
}
residentView.cleanup()
residentImage.cleanup()
cleanupImports()
}
private fun release(frame: ExternalBrowserFrame, reason: BrowserFrameReleaseReason) {
onRelease.released(
BrowserFrameRelease(frame.streamId, frame.frameId, frame.releaseToken, frame.slot, reason)
)
}
private fun ExternalBrowserFrame.key() = FrameKey(streamId, frameId, releaseToken)
private fun BrowserSurfacePixelFormat.sampledVkFormat(): Int = when (this) {
BrowserSurfacePixelFormat.BGRA8_SRGB -> VK_FORMAT_B8G8R8A8_SRGB
BrowserSurfacePixelFormat.RGBA8_SRGB -> VK_FORMAT_R8G8B8A8_SRGB
BrowserSurfacePixelFormat.BGRA8_UNORM -> VK_FORMAT_B8G8R8A8_UNORM
BrowserSurfacePixelFormat.RGBA8_UNORM -> VK_FORMAT_R8G8B8A8_UNORM
BrowserSurfacePixelFormat.RGBA16_FLOAT -> VK_FORMAT_R16G16B16A16_SFLOAT
}
private fun BrowserSurfacePixelFormat.storageVkFormat(): Int = when (this) {
BrowserSurfacePixelFormat.BGRA8_SRGB,
BrowserSurfacePixelFormat.BGRA8_UNORM -> VK_FORMAT_B8G8R8A8_UNORM
BrowserSurfacePixelFormat.RGBA8_SRGB,
BrowserSurfacePixelFormat.RGBA8_UNORM -> VK_FORMAT_R8G8B8A8_UNORM
BrowserSurfacePixelFormat.RGBA16_FLOAT -> VK_FORMAT_R16G16B16A16_SFLOAT
}
private companion object {
const val sampledStages = VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT or VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT
}
}