ElectronBrowserTextureSession.kt
src/jvmMain/kotlin/afield/media/browser/ElectronBrowserTextureSession.kt · 705 lines
package afield.media.browser
import afield.graphics.vulkan.util.OS
import afield.graphics.vulkan.util.OSType
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.json.put
import org.lwjgl.system.APIUtil.apiCreateLibrary
import org.lwjgl.system.JNI
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import org.lwjgl.system.SharedLibrary
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.InetAddress
import java.net.ServerSocket
import java.nio.channels.ServerSocketChannel
import java.nio.channels.SocketChannel
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference
The Electron process that paints a web page for Field, and the ways a frame of it crosses into this process.
ElectronBrowserTextureSession starts electron/browser-texture-producer.js under the Electron
that ./gradlew electronInstall put in the checkout, passes it the page, the surface size and
the rest as FIELD_BROWSER_TEXTURE_* environment variables, and talks to it in JSON lines over a
loopback socket the session opens for the purpose. All of that is platform-neutral; only the GPU
handle transfer is not, and each BrowserHandleTransport here is one way to do it: a Mach send
right on macOS, a dma-buf descriptor over SCM_RIGHTS on Linux, and an opt-in fallback that
carries the pixels themselves over a second socket for when no handle can cross. The receiver
side of the contract is ExternalBrowserSurface, described in
dev_docs/external_browser_surfaces.md.
internal fun locateFieldProjectRoot(): File
Walks up from the working directory to the Field checkout that owns the Electron producer.
internal fun locateFieldProjectRoot(): File {
var cursor = File(System.getProperty("user.dir")).absoluteFile
while (true) {
if (File(cursor, "electron/browser-texture-producer.js").isFile &&
File(cursor, "build.gradle.kts").isFile
) return cursor
cursor = cursor.parentFile ?: error(
"Could not locate the Field checkout from ${System.getProperty("user.dir")}"
)
}
}
internal fun fieldElectronBinary(root: File): File
The Electron the checkout installed, under whichever app name electronInstall gave it.
internal fun fieldElectronBinary(root: File): File {
val dist = File(root, "electron/node_modules/electron/dist")
if (OS != OSType.MACOS) return File(dist, "electron")
return listOf(
File(dist, "Field4.app/Contents/MacOS/Field4"),
File(dist, "Electron.app/Contents/MacOS/Field4"),
File(dist, "Electron.app/Contents/MacOS/Electron"),
).firstOrNull { it.isFile } ?: File(dist, "Electron.app/Contents/MacOS/Electron")
}
internal class ElectronBrowserTextureSession
Everything about driving the Electron producer that does not depend on how a GPU handle crosses
the process boundary: launching it, the loopback JSON control channel, readiness, and turning a
frame message into an offered ExternalBrowserFrame.
Only the handle transfer is per-platform, so only that is behind BrowserHandleTransport. The transport is a constructor parameter rather than an overridable method because it has to exist before the producer process starts — the process needs the transport's address in its environment.
surface and transport are both optional: with neither, the producer runs in its no-frames
mode and this class is the whole browser. See HeadlessPuppetBrowser.
internal class ElectronBrowserTextureSession(
projectRoot: File,
private val surface: ExternalBrowserSurface?,
url: String,
spec: ExternalBrowserSurfaceSpec,
private val writer: AtomicReference<PrintWriter?>,
private val loadedAfterFrame: AtomicLong,
readyBoxUid: String?,
private val transport: BrowserHandleTransport?,
private val stateListener: (PuppetBrowserState) -> Unit,
private val eventListener: (PuppetBrowserEvent) -> Unit,
private val pageRequest: (id: String, name: String, value: JsonElement) -> Unit,
/** Called on every `did-finish-load`, with the state the producer observed at that moment. */
private val loadedListener: (PuppetBrowserState) -> Unit = {},
// Overrides the producer's per-mode offscreen frame rate; null keeps that mode's default.
framesPerSecond: Int? = null,
) : AutoCloseable {
private val closed = AtomicBoolean(false)
private val ready = CompletableFuture<Unit>()
private val requestSequence = AtomicLong()
private val pending = ConcurrentHashMap<String, CompletableFuture<JsonObject>>()
private val server = ServerSocket(0, 1, InetAddress.getLoopbackAddress())
private val process: Process
private val worker: Thread
init
body — 89 lines, from line 114
init {
val electron = fieldElectronBinary(projectRoot)
val producer = File(projectRoot, "electron/browser-texture-producer.js")
listOfNotNull(electron, producer, transport?.nodeBridge).forEach { file ->
require(file.isFile) {
"Missing browser texture runtime ${file.absolutePath}; run ./gradlew electronInstall"
}
}
val streamId = (System.nanoTime() and Long.MAX_VALUE).coerceAtLeast(1)
process = ProcessBuilder(electron.absolutePath, producer.absolutePath)
.directory(projectRoot)
.redirectErrorStream(true)
.apply {
environment()["FIELD_BROWSER_TEXTURE_URL"] = url
environment()["FIELD_BROWSER_TEXTURE_WIDTH"] = spec.width.toString()
environment()["FIELD_BROWSER_TEXTURE_HEIGHT"] = spec.height.toString()
environment()["FIELD_BROWSER_TEXTURE_DEVICE_SCALE_FACTOR"] = spec.deviceScaleFactor.toString()
environment()["FIELD_BROWSER_TEXTURE_SLOTS"] = spec.slotCount.toString()
environment()["FIELD_BROWSER_TEXTURE_CONTROL_PORT"] = server.localPort.toString()
transport?.nodeBridge?.let { environment()["FIELD_BROWSER_TEXTURE_BRIDGE"] = it.absolutePath }
environment()["FIELD_BROWSER_TEXTURE_STREAM_ID"] = streamId.toString()
if (transport == null) environment()["FIELD_BROWSER_TEXTURE_NO_FRAMES"] = "1"
framesPerSecond?.let { environment()["FIELD_BROWSER_TEXTURE_FRAME_RATE"] = it.toString() }
environment().putAll(transport?.producerEnvironment ?: emptyMap())
readyBoxUid?.takeIf { it.isNotBlank() }?.let {
environment()["FIELD_BROWSER_TEXTURE_READY_BOX_UID"] = it
}
}
.start()
Thread({
process.inputStream.bufferedReader().useLines { lines ->
lines.forEach { println("[field-browser-texture] $it") }
}
}, "field-browser-texture-output").apply {
isDaemon = true
start()
}
worker = Thread({ runControlLoop() }, "field-browser-texture-control").apply {
isDaemon = true
start()
}
}
private fun runControlLoop() {
try {
server.accept().use { socket ->
val out = PrintWriter(socket.getOutputStream(), true)
writer.set(out)
BufferedReader(InputStreamReader(socket.getInputStream())).useLines { lines ->
lines.takeWhile { !closed.get() }.forEach { line ->
if (line.isNotBlank()) handle(Json.parseToJsonElement(line).jsonObject)
}
}
}
} catch (t: Throwable) {
if (!closed.get()) System.err.println("[field-browser-texture] control failed: ${t.message}")
} finally {
writer.set(null)
}
}
private fun handle(message: JsonObject) {
when (message.string("type")) {
"ready" -> {
message.objectOrNull("state")?.let { stateListener(puppetBrowserState(it)) }
ready.complete(Unit)
println("[field-browser-texture] producer ready pid=${message.string("pid")}")
}
"loaded" -> {
loadedAfterFrame.set(message.long("afterFrameId"))
val loaded = message.objectOrNull("state")?.let(::puppetBrowserState)
loaded?.let(stateListener)
loadedListener(loaded ?: PuppetBrowserState(url = message.string("url")))
println("[field-browser-texture] page loaded ${message.string("url")}")
}
"error" -> System.err.println("[field-browser-texture] ${message.string("message")}")
"frame" -> offer(message)
"response" -> receiveResponse(message)
"event" -> receiveEvent(message)
"page-request" -> pageRequest(
message.string("id"),
message.string("name"),
message["value"] ?: JsonNull,
)
}
}
fun command
fun command(
name: String,
params: JsonObject = JsonObject(emptyMap()),
timeoutMillis: Long = 12_000,
): CompletableFuture<JsonObject> {
val id = requestSequence.incrementAndGet().toString()
val result = CompletableFuture<JsonObject>()
ready.whenComplete { _, startupError ->
if (startupError != null) {
result.completeExceptionally(startupError)
return@whenComplete
}
if (closed.get()) {
result.completeExceptionally(PuppetBrowserCommandException("Puppet browser is closed", name))
return@whenComplete
}
pending[id] = result
runCatching {
send(buildJsonObject {
put("type", "command")
put("id", id)
put("command", name)
put("params", params)
})
}.onFailure { error ->
pending.remove(id, result)
result.completeExceptionally(error)
}
}
result.orTimeout(timeoutMillis, TimeUnit.MILLISECONDS)
result.whenComplete { _, _ -> pending.remove(id, result) }
return result
}
fun post(name: String, params: JsonObject)
fun post(name: String, params: JsonObject) {
ready.thenRun {
if (!closed.get()) send(buildJsonObject {
put("type", "post")
put("command", name)
put("params", params)
})
}
}
fun pageResponse(id: String, result: JsonElement?, error: Throwable?)
body — 83 lines, from line 248
fun pageResponse(id: String, result: JsonElement?, error: Throwable?) {
if (closed.get()) return
send(buildJsonObject {
put("type", "page-response")
put("id", id)
put("ok", error == null)
if (error == null) put("result", result ?: JsonNull)
else put("error", buildJsonObject {
put("name", error::class.simpleName ?: "Error")
put("message", error.message ?: error.toString())
})
})
}
private fun send(message: JsonObject) {
val out = writer.get() ?: throw PuppetBrowserCommandException("Puppet browser control channel is unavailable")
synchronized(out) {
out.println(message.toString())
out.flush()
if (out.checkError()) throw PuppetBrowserCommandException("Puppet browser control channel write failed")
}
}
private fun receiveResponse(message: JsonObject) {
val id = message.string("id")
val future = pending.remove(id) ?: return
if (message.boolean("ok")) {
future.complete(message.objectOrNull("result") ?: JsonObject(emptyMap()))
} else {
val error = message.objectOrNull("error")
future.completeExceptionally(
PuppetBrowserCommandException(
error?.stringOrNull("message") ?: "Puppet browser command failed",
details = error,
)
)
}
}
private fun receiveEvent(message: JsonObject) {
val event = PuppetBrowserEvent(
sequence = message["sequence"]?.jsonPrimitive?.longOrNull ?: 0,
name = message.string("name"),
payload = message.objectOrNull("payload") ?: JsonObject(emptyMap()),
)
if (event.name == "state") {
event.payload.objectOrNull("state")?.let { stateListener(puppetBrowserState(it)) }
}
eventListener(event)
}
private fun offer(message: JsonObject) {
// The producer only announces frames when it was given somewhere to put them, so this is a
// protocol violation rather than a mode: fail loudly instead of dropping a leased texture.
val transport = transport ?: error("The producer announced a frame in no-frames mode: $message")
val surface = surface ?: error("The producer announced a frame with no receiving surface")
val token = message.long("token")
val handle = transport.receive(message, token, 5_000)
val colorSpace = message.colorSpace("colorSpace")
val isSrgb = colorSpace?.transfer.equals("srgb", ignoreCase = true)
val pixelFormat = when (message.string("pixelFormat")) {
"bgra" -> if (isSrgb) BrowserSurfacePixelFormat.BGRA8_SRGB else BrowserSurfacePixelFormat.BGRA8_UNORM
"rgba" -> if (isSrgb) BrowserSurfacePixelFormat.RGBA8_SRGB else BrowserSurfacePixelFormat.RGBA8_UNORM
"rgbaf16" -> BrowserSurfacePixelFormat.RGBA16_FLOAT
else -> error("Unsupported Electron browser texture format ${message.string("pixelFormat")}")
}
surface.offerFrame(
ExternalBrowserFrame(
streamId = message.long("streamId"),
frameId = message.long("frameId"),
releaseToken = token,
slot = message.int("slot"),
width = message.int("width"),
height = message.int("height"),
pixelFormat = pixelFormat,
handle = handle,
colorSpace = colorSpace,
visibleRect = message.rect("visibleRect"),
contentRect = message.rect("contentRect"),
timestampMicros = message["timestamp"]?.jsonPrimitive?.longOrNull,
)
)
}
override fun close()
override fun close() {
if (!closed.compareAndSet(false, true)) return
val closedError = PuppetBrowserCommandException("Puppet browser closed")
ready.completeExceptionally(closedError)
pending.values.forEach { it.completeExceptionally(closedError) }
pending.clear()
writer.getAndSet(null)?.let { out ->
synchronized(out) {
out.println("{\"type\":\"close\"}")
out.flush()
}
}
runCatching { server.close() }
process.destroy()
if (!process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS)) process.destroyForcibly()
worker.join(6_000)
runCatching { transport?.close() }
}
}
internal fun <T> CompletableFuture<T>.discardResult(): CompletableFuture<Unit>
internal fun <T> CompletableFuture<T>.discardResult(): CompletableFuture<Unit> {
val discarded = CompletableFuture<Unit>()
whenComplete { _, error ->
if (error == null) discarded.complete(Unit)
else discarded.completeExceptionally(error)
}
return discarded
}
internal interface BrowserHandleTransport : AutoCloseable
How one platform moves a Chromium-owned GPU allocation into this process. Implementations own a receiver-side endpoint whose address they publish into the producer's environment, and turn each announced frame into the ExternalBrowserImageHandle its receiver understands.
internal interface BrowserHandleTransport : AutoCloseable {
val nodeBridge: File?
The Node-API addon the producer loads to send on this transport; null if it needs none.
val producerEnvironment: Map<String, String>
Endpoint address and anything else the producer needs to reach this receiver.
fun receive(message: JsonObject, token: Long, timeoutMs: Int): ExternalBrowserImageHandle
Blocks for the handle the producer transferred for token out of band.
companion object
fun forThisPlatform(projectRoot: File): BrowserHandleTransport
fun forThisPlatform(projectRoot: File): BrowserHandleTransport = when (OS) {
OSType.MACOS -> MacMachSurfaceTransport(projectRoot)
OSType.LINUX -> LinuxDmaBufTransport(projectRoot)
else -> error(
"FieldUiBrowserTexture has no Electron handle transport on ${OS.name.lowercase()}; " +
"the receiver exists but its producer bridge does not"
)
}
}
}
macOS: Chromium's IOSurface arrives as a Mach send right on a per-session bootstrap service.
private class MacMachSurfaceTransport(projectRoot: File) : BrowserHandleTransport {
private val service = "com.field4.browser-texture.${ProcessHandle.current().pid()}.${System.nanoTime()}"
private val bridgeDir = File(projectRoot, "build/field-browser-bridge/macos")
private val inbox = MacMachSurfaceInbox(File(bridgeDir, "libfield_browser_bridge.dylib"), service)
override val nodeBridge = File(bridgeDir, "field_browser_bridge.node")
override val producerEnvironment = mapOf("FIELD_BROWSER_TEXTURE_MACH_SERVICE" to service)
override fun receive(message: JsonObject, token: Long, timeoutMs: Int): ExternalBrowserImageHandle {
val received = inbox.receive(timeoutMs)
require(received.token == token) {
"Mach surface token ${received.token} did not match frame token $token"
}
return ExternalBrowserImageHandle.MacIOSurfaceMachPort(received.port)
}
override fun close() = inbox.close()
}
Linux: Chromium's nativePixmap dma-buf arrives over SCM_RIGHTS, because a descriptor number is
meaningless in another process and a dma-buf cannot be re-opened through /proc/<pid>/fd.
The control message carries only the descriptors and the frame token; each plane's stride, offset, size and the allocation's DRM modifier come alongside on the JSON channel, which is where they belong — they are metadata, not kernel objects. Planes name their descriptor by index so that planes sharing one allocation stay one allocation after the transfer.
source — 65 lines, from line 417
private class LinuxDmaBufTransport(projectRoot: File) : BrowserHandleTransport {
private val socket = "field4.browser-texture.${ProcessHandle.current().pid()}.${System.nanoTime()}"
private val bridgeDir = File(projectRoot, "build/field-browser-bridge/linux")
private val inbox = LinuxDmaBufInbox(File(bridgeDir, "libfield_browser_bridge.so"), socket)
override val nodeBridge = File(bridgeDir, "field_browser_bridge.node")
override val producerEnvironment = mapOf("FIELD_BROWSER_TEXTURE_DMABUF_SOCKET" to socket)
override fun receive(message: JsonObject, token: Long, timeoutMs: Int): ExternalBrowserImageHandle {
val received = inbox.receive(timeoutMs)
if (received.token != token) {
received.closeFds()
error("dma-buf token ${received.token} did not match frame token $token")
}
return try {
val planes = message["planes"]?.jsonArray
?: error("Linux browser frame carries no plane layout: $message")
ExternalBrowserImageHandle.LinuxDmaBuf(
planes = planes.map { element ->
val plane = element.jsonObject
val index = plane.int("fdIndex")
require(index in received.fds.indices) {
"plane names descriptor $index but only ${received.fds.size} arrived"
}
ExternalBrowserImageHandle.LinuxDmaBuf.Plane(
fd = received.fds[index],
stride = plane.int("stride"),
offset = plane.int("offset"),
size = plane.long("size"),
)
},
// Electron sends the modifier as a decimal string; DRM_FORMAT_MOD_INVALID and the
// vendor-high-bit modifiers do not fit a signed parse.
modifier = java.lang.Long.parseUnsignedLong(message.string("modifier")),
)
} catch (t: Throwable) {
received.closeFds()
throw t
}
}
override fun close() = inbox.close()
}
private fun JsonObject.string(name: String): String =
this[name]?.jsonPrimitive?.contentOrNull ?: error("Browser texture message has no '$name': $this")
private fun JsonObject.long(name: String): Long =
this[name]?.jsonPrimitive?.longOrNull ?: string(name).toLong()
private fun JsonObject.int(name: String): Int =
this[name]?.jsonPrimitive?.intOrNull ?: string(name).toInt()
private fun JsonObject.rect(name: String): BrowserSurfaceRect? =
this[name]?.jsonObject?.let { BrowserSurfaceRect(it.int("x"), it.int("y"), it.int("width"), it.int("height")) }
private fun JsonObject.colorSpace(name: String): BrowserSurfaceColorSpace? =
(this[name] as? JsonObject)?.let {
BrowserSurfaceColorSpace(
primaries = it.string("primaries"),
transfer = it.string("transfer"),
matrix = it.string("matrix"),
range = it.string("range"),
)
}
internal class CpuBitmapTransport
The opt-in fallback's transport: no GPU handle crosses at all, so instead of answering a frame
announcement on the control channel this one is the frame source, reading header-plus-pixels off
a second loopback socket and offering each frame to the surface directly.
internal class CpuBitmapTransport(
private val surface: ExternalBrowserSurface,
private val spec: ExternalBrowserSurfaceSpec,
) : BrowserHandleTransport {
private val server = ServerSocketChannel.open().apply {
bind(java.net.InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 1)
}
private val closed = AtomicBoolean(false)
private val worker: Thread
override val nodeBridge: File? = null
override val producerEnvironment
override val producerEnvironment = mapOf(
"FIELD_BROWSER_TEXTURE_CPU_BITMAP" to "1",
"FIELD_BROWSER_TEXTURE_PIXEL_PORT" to
(server.localAddress as java.net.InetSocketAddress).port.toString(),
)
init
body — 68 lines, from line 506
init {
worker = Thread({ readFrames() }, "field-browser-texture-pixels").apply {
isDaemon = true
start()
}
}
private fun readFrames() {
try {
server.accept().use { channel ->
// The producer writes the header in network order; LWJGL hands back native-order
// buffers, which is little-endian everywhere Field runs, so say so explicitly
// rather than reading a byte-swapped magic and calling the channel desynchronised.
val header = MemoryUtil.memAlloc(HEADER_BYTES).order(java.nio.ByteOrder.BIG_ENDIAN)
try {
while (!closed.get()) {
header.clear()
if (!readFully(channel, header)) return
header.flip()
val magic = header.int
check(magic == MAGIC) { "browser pixel channel desynchronised (magic 0x${magic.toString(16)})" }
val streamId = header.long
val frameId = header.long
val slot = header.int
val width = header.int
val height = header.int
val stride = header.int
val length = header.int
header.int // reserved
require(length in 1..MAX_FRAME_BYTES) { "browser pixel frame claims $length bytes" }
// Read straight into the buffer the importer will memcpy from; the handle
// owns it from here and frees it on close, including when the mailbox
// conflates this frame away before it is ever imported.
val pixels = MemoryUtil.memAlloc(length)
if (!readFully(channel, pixels)) {
MemoryUtil.memFree(pixels)
return
}
pixels.flip()
surface.offerFrame(
ExternalBrowserFrame(
streamId = streamId,
frameId = frameId,
releaseToken = frameId,
slot = slot,
width = width,
height = height,
pixelFormat = spec.pixelFormat,
handle = ExternalBrowserImageHandle.CpuBitmap(pixels, stride),
)
)
}
} finally {
MemoryUtil.memFree(header)
}
}
} catch (t: Throwable) {
if (!closed.get()) System.err.println("[field-browser-texture] pixel channel failed: ${t.message}")
}
}
private fun readFully(channel: SocketChannel, into: java.nio.ByteBuffer): Boolean {
while (into.hasRemaining()) {
if (channel.read(into) < 0) return false
}
return true
}
override fun receive(message: JsonObject, token: Long, timeoutMs: Int): ExternalBrowserImageHandle
override fun receive(message: JsonObject, token: Long, timeoutMs: Int): ExternalBrowserImageHandle =
error("the CPU bitmap transport delivers frames on its own channel, not by announcement")
override fun close()
override fun close() {
closed.set(true)
runCatching { server.close() }
}
private companion object {
const val MAGIC = 0x46504958 // "FPIX"
const val HEADER_BYTES = 44
const val MAX_FRAME_BYTES = 256 * 1024 * 1024
}
}
internal data class ReceivedMachSurface(val token: Long, val port: Int)
internal class MacMachSurfaceInbox(libraryFile: File, service: String) : AutoCloseable
internal class MacMachSurfaceInbox(libraryFile: File, service: String) : AutoCloseable {
private val library: SharedLibrary
private val receiveAddress: Long
private val destroyAddress: Long
private var receiver: Long
init
init {
require(libraryFile.isFile) {
"Missing ${libraryFile.absolutePath}; run ./gradlew buildFieldBrowserBridge"
}
library = apiCreateLibrary(libraryFile.absolutePath)
val openAddress = library.getFunctionAddress("field_browser_mach_receiver_open")
receiveAddress = library.getFunctionAddress("field_browser_mach_receiver_receive")
destroyAddress = library.getFunctionAddress("field_browser_mach_receiver_destroy")
require(openAddress != 0L && receiveAddress != 0L && destroyAddress != 0L) {
"Native browser bridge is missing its Mach receiver exports"
}
MemoryStack.stackPush().use { stack ->
receiver = JNI.callPP(MemoryUtil.memAddress(stack.UTF8(service)), openAddress)
}
require(receiver != 0L) { "Could not register browser texture Mach service $service" }
}
fun receive(timeoutMs: Int): ReceivedMachSurface
fun receive(timeoutMs: Int): ReceivedMachSurface {
val output = MemoryUtil.memCalloc(16)
try {
val address = MemoryUtil.memAddress(output)
val result = JNI.invokePPI(receiver, address, timeoutMs, receiveAddress)
require(result == 0) { "Mach surface receive failed with code $result" }
return ReceivedMachSurface(
token = MemoryUtil.memGetLong(address),
port = MemoryUtil.memGetLong(address + 8).toInt(),
)
} finally {
MemoryUtil.memFree(output)
}
}
override fun close()
override fun close() {
val value = receiver
receiver = 0L
if (value != 0L) JNI.callPV(value, destroyAddress)
library.free()
}
}
One frame's worth of descriptors, already installed in this process by the kernel. They are owned
here from this moment: whatever happens next must either hand them to a
ExternalBrowserImageHandle.LinuxDmaBuf or closeFds them.
source — 52 lines, from line 643
private class ReceivedDmaBuf(val token: Long, val fds: List<Int>) {
fun closeFds() = fds.forEach { fd -> runCatching { ExternalBrowserFdClose.close(fd) } }
}
private class LinuxDmaBufInbox(libraryFile: File, socketName: String) : AutoCloseable {
private val library: SharedLibrary
private val receiveAddress: Long
private val destroyAddress: Long
private var receiver: Long
init {
require(libraryFile.isFile) {
"Missing ${libraryFile.absolutePath}; run ./gradlew buildFieldBrowserBridge"
}
library = apiCreateLibrary(libraryFile.absolutePath)
val openAddress = library.getFunctionAddress("field_browser_dmabuf_receiver_open")
receiveAddress = library.getFunctionAddress("field_browser_dmabuf_receiver_receive")
destroyAddress = library.getFunctionAddress("field_browser_dmabuf_receiver_destroy")
require(openAddress != 0L && receiveAddress != 0L && destroyAddress != 0L) {
"Native browser bridge is missing its dma-buf receiver exports"
}
MemoryStack.stackPush().use { stack ->
receiver = JNI.callPP(MemoryUtil.memAddress(stack.UTF8(socketName)), openAddress)
}
require(receiver != 0L) { "Could not bind browser texture dma-buf socket @$socketName" }
}
fun receive(timeoutMs: Int): ReceivedDmaBuf {
// token, descriptor count, then up to four descriptors.
val output = MemoryUtil.memCalloc(8 * 8)
try {
val address = MemoryUtil.memAddress(output)
val result = JNI.invokePPI(receiver, address, timeoutMs, receiveAddress)
require(result == 0) { "dma-buf receive failed with code $result" }
val count = MemoryUtil.memGetLong(address + 8).toInt()
require(count in 1..4) { "dma-buf receive reported $count descriptors" }
return ReceivedDmaBuf(
token = MemoryUtil.memGetLong(address),
fds = (0 until count).map { MemoryUtil.memGetLong(address + 16 + it * 8L).toInt() },
)
} finally {
MemoryUtil.memFree(output)
}
}
override fun close() {
val value = receiver
receiver = 0L
if (value != 0L) JNI.callPV(value, destroyAddress)
library.free()
}
}
close(2), for descriptors this file received but never handed to a Vulkan import.
private object ExternalBrowserFdClose {
private val libc: SharedLibrary by lazy { apiCreateLibrary("libc.so.6") }
private val closeAddress: Long by lazy { libc.getFunctionAddress("close") }
fun close(fd: Int) {
check(JNI.callI(fd, closeAddress) == 0) { "close($fd) failed" }
}
}