MacExternalBrowserSurface.kt
src/jvmMain/kotlin/afield/media/browser/MacExternalBrowserSurface.kt · 190 lines
package afield.media.browser
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.texture.IOSurfaceHostBuffer
import afield.graphics.texture.Image
import afield.graphics.texture.MetalIOSurface
import afield.graphics.texture.image_layout_transition
import afield.graphics.vulkan.util.OS
import afield.graphics.vulkan.util.OSType
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.VkImageSubresourceRange
The macOS browser-frame receiver, which imports Electron's IOSurfaces into Vulkan.
class MacExternalBrowserSurface
macOS implementation of ExternalBrowserSurface. A native producer transfers a Mach send right for Electron's process-local IOSurface; this receiver resolves and imports it, while CopiedExternalBrowserSurface copies into a stable image and acknowledges only after the fence.
Two ways in, by driver. MoltenVK backs a VkImage with the IOSurface itself
(VK_EXT_metal_objects), and the base class copies straight out of it. KosmicKrisp cannot back an
image with an IOSurface at all (see IOSurfaceHostBuffer), so there the surface's pages become a
buffer and recordPreCopy copies them into a per-slot staging image first — one more GPU copy,
no CPU copy, and every other part of the frame lifecycle unchanged.
class MacExternalBrowserSurface(
renderer: Renderer,
spec: ExternalBrowserSurfaceSpec,
releaseListener: BrowserFrameReleaseListener = BrowserFrameReleaseListener { }
) : CopiedExternalBrowserSurface(renderer, spec, releaseListener) {
private val importedSlots = mutableMapOf<Int, ImportedImage>()
private var importedStreamId: Long? = null
KosmicKrisp's route: no image-level IOSurface import, but host-pointer buffers.
private val viaHostBuffer = !renderer.device.supportsMetalObjects && renderer.device.supportsExternalMemoryHost
One slot's surface as a buffer, and the device-local image the resident copy reads from.
private inner class HostSlot(val ioSurfaceRef: Long, slot: Int, identity: Any) {
val host = IOSurfaceHostBuffer(renderer.device, ioSurfaceRef, readOnly = true)
val image = try {
Image(
renderer.device,
Image.ImageData()
.width(spec.width)
.height(spec.height)
.format(importVkFormat)
.mipLevels(1)
.usage(VK_IMAGE_USAGE_TRANSFER_DST_BIT or VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
.debugName("external-browser-iosurface-staging slot=$slot handle=$identity")
)
} catch (t: Throwable) {
host.close()
throw t
}
fun cleanup() {
host.close()
image.cleanup()
MetalIOSurface.release(ioSurfaceRef)
}
}
private val hostSlots = mutableMapOf<Int, HostSlot>()
Set by importFrame and consumed by the very next recordPreCopy; prepare is not reentrant.
private var pendingUpload: HostSlot? = null
private val bytesPerPixel = when (spec.pixelFormat) {
BrowserSurfacePixelFormat.RGBA16_FLOAT -> 8
else -> 4
}
init
init {
require(OS == OSType.MACOS) { "MacExternalBrowserSurface requires macOS" }
if (!viaHostBuffer) renderer.device.requireMetalObjects("MacExternalBrowserSurface")
}
override fun importFrame(frame: ExternalBrowserFrame): ImportedImage?
body — 72 lines, from line 84
override fun importFrame(frame: ExternalBrowserFrame): ImportedImage? {
if (frame.streamId != importedStreamId) {
importedSlots.values.forEach(::retire)
importedSlots.clear()
hostSlots.clear() // their cleanup is inside the retired ImportedImages
importedStreamId = frame.streamId
}
if (frame.handle == null) return importedSlots[frame.slot]?.also { markUpload(frame.slot) }
val (identity, ioSurfaceRef) = try {
when (val handle = frame.handle) {
is ExternalBrowserImageHandle.MacIOSurfaceMachPort -> {
val ref = MetalIOSurface.lookupFromMachPort(handle.value)
MetalIOSurface.ioSurfaceId(ref) to ref
}
is ExternalBrowserImageHandle.MacIOSurfaceId ->
handle.value to MetalIOSurface.lookup(handle.value)
else -> return null
}
} finally {
// Lookup retained the IOSurface; the transport right is no longer needed here.
runCatching { frame.handle.close() }
.onFailure {
lprintln("warning,graphics", "could not release browser transport handle: ${it.message}")
}
}
importedSlots[frame.slot]?.takeIf { it.identity == identity }?.let {
// A repeated Mach port still transferred a fresh send right and lookup retain.
if (ioSurfaceRef != 0L) MetalIOSurface.release(ioSurfaceRef)
markUpload(frame.slot)
return it
}
if (ioSurfaceRef == 0L) return null
val imported = if (viaHostBuffer) {
val slot = try {
HostSlot(ioSurfaceRef, frame.slot, identity)
} catch (t: Throwable) {
MetalIOSurface.release(ioSurfaceRef)
throw t
}
hostSlots[frame.slot] = slot
ImportedImage(identity, slot.image.vkImage, { slot.cleanup() })
} else {
val image = try {
Image(
renderer.device,
Image.ImageData()
.width(spec.width)
.height(spec.height)
.format(importVkFormat)
.mipLevels(1)
.usage(VK_IMAGE_USAGE_TRANSFER_SRC_BIT or VK_IMAGE_USAGE_SAMPLED_BIT)
.debugName("external-browser-iosurface slot=${frame.slot} handle=$identity")
.also { it.importMetalIOSurface = ioSurfaceRef }
)
} catch (t: Throwable) {
MetalIOSurface.release(ioSurfaceRef)
throw t
}
ImportedImage(identity, image.vkImage, {
image.cleanup()
MetalIOSurface.release(ioSurfaceRef)
})
}
importedSlots.put(frame.slot, imported)?.let(::retire)
markUpload(frame.slot)
return imported
}
Electron redraws into the same IOSurface, so a host-buffer slot re-copies on every frame, cached or not; an imported image needs nothing, its memory is the surface.
private fun markUpload(slot: Int) {
if (viaHostBuffer) pendingUpload = hostSlots[slot]
}
override fun recordPreCopy(command: CommandBuffer, source: ImportedImage)
override fun recordPreCopy(command: CommandBuffer, source: ImportedImage) {
val slot = pendingUpload ?: return
pendingUpload = null
MemoryStack.stackPush().use { stack ->
val range = VkImageSubresourceRange.calloc(stack)
.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).baseMipLevel(0).levelCount(1).baseArrayLayer(0).layerCount(1)
// Entirely overwritten below, so discarding the previous contents is correct.
image_layout_transition(
command.vkCommandBuffer, slot.image.vkImage,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
0, VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, range
)
slot.host.copyToImage(command.vkCommandBuffer, slot.image.vkImage, bytesPerPixel)
}
// recordCopy transitions from here to TRANSFER_SRC_OPTIMAL for the copy into the resident
// texture, so tell it where the image actually is.
source.layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
}
override fun cleanupImports()
override fun cleanupImports() {
pendingUpload = null
importedSlots.values.forEach { it.cleanup() }
importedSlots.clear()
hostSlots.clear()
}
}