VulkanBuffer.kt
src/jvmMain/kotlin/afield/graphics/vulkan/buffers/VulkanBuffer.kt · 701 lines
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package afield.graphics.vulkan.buffers
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.render.computeMemoryBarrier
import afield.graphics.render.graphicsMemoryBarrier
import afield.document.context.fromContext
import afield.graphics.vulkan.util.AutoDisposer
import afield.graphics.vulkan.util.vkCheck
import afield.math.linalg.i
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import org.lwjgl.util.vma.Vma
import org.lwjgl.util.vma.Vma.vmaDestroyBuffer
import org.lwjgl.util.vma.VmaAllocationCreateInfo
import org.lwjgl.util.vma.VmaAllocationInfo
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK12.*
import org.lwjgl.vulkan.VK13.*
import org.lwjgl.vulkan.VkBufferCreateInfo
import org.lwjgl.vulkan.VkBufferDeviceAddressInfo
import org.lwjgl.vulkan.VkBufferMemoryBarrier2
import org.lwjgl.vulkan.VkDependencyInfo
import java.nio.ByteBuffer
import java.nio.FloatBuffer
import java.nio.IntBuffer
import kotlin.math.max
A block of GPU memory, and the ways to hand one to a shader. VulkanBuffer is the allocation
itself: VMA-suballocated, kept mapped on the CPU side for its whole life, resizable while frames
are in flight, and released through the renderer's frame-retirement queue rather than on the spot.
Vertex data, uniforms, storage buffers, acceleration-structure scratch and compute input and
output are all this one class under different usage flags.
Writing and reading go through three small interfaces, and which you reach for is a question of how
much you want to think about layout. BufferWriteable and BufferReadable map the allocation as
a plain FloatBuffer, ByteBuffer or IntBuffer and let you address it yourself.
DynamicallyWriteable gives you a DynamicBuffer instead, which pads to std430 as it goes and
grows the allocation under a write that would overflow it; write { … } starts at the beginning
and append { … } continues from the cursor.
HasBuffer is the handle a shader is bound to, and the useful thing about it is that it composes.
Adding a Long to one offsets it. zeroBefore on a buffer, and barrier or graphicsBarrier on
any handle, return a handle that records that work on the way to giving the address up. "This
buffer, cleared first" and "this buffer, once the compute writes to it are visible" then travel
with the buffer, so nothing has to remember where in the frame to put the clear or the barrier.
Render workers read the handle, address and descriptor range while recording a frame, on other
threads than the one that writes them. Every one of those fields is therefore @Volatile, and
allocation builds its replacements in locals and publishes them at the end, handle before range. A
worker sees either the old allocation, which is still alive, or the new one, and never a null
handle mid-resize.
interface HasBuffer
A GPU buffer handle and the device address a shader reads it at.
fun getBuffer(): Long
fun getGPUAddress(cmd: CommandBuffer): Long
operator fun HasBuffer.plus(offset: Long): HasBuffer
operator fun HasBuffer.plus(offset: Long): HasBuffer {
val inside = this
return object : HasBuffer {
override fun getBuffer(): Long = inside.getBuffer()
override fun getGPUAddress(cmd: CommandBuffer): Long = inside.getGPUAddress(cmd) + offset
}
}
val VulkanBuffer.zeroBefore: HasBuffer
val VulkanBuffer.zeroBefore: HasBuffer
get() {
val inside = this
return object : HasBuffer {
override fun getBuffer(): Long = inside.getBuffer()
override fun getGPUAddress(cmd: CommandBuffer): Long {
inside.zero(cmd)
return inside.getGPUAddress(cmd)
}
}
}
val HasBuffer.barrier: HasBuffer
val HasBuffer.barrier: HasBuffer
get() {
val inside = this
return object : HasBuffer {
override fun getBuffer(): Long = inside.getBuffer()
override fun getGPUAddress(cmd: CommandBuffer): Long {
computeMemoryBarrier(cmd, inside.getBuffer())
return inside.getGPUAddress(cmd)
}
}
}
val HasBuffer.graphicsBarrier: HasBuffer
val HasBuffer.graphicsBarrier: HasBuffer
get() {
val inside = this
return object : HasBuffer {
override fun getBuffer(): Long = inside.getBuffer()
override fun getGPUAddress(cmd: CommandBuffer): Long {
if (!cmd.inRenderPassInstance) {
graphicsMemoryBarrier(cmd, inside.getBuffer())
}
return inside.getGPUAddress(cmd)
}
}
}
interface BufferWriteable
fun mapAndWrite(builder: FloatBuffer.() -> Unit): BufferWriteable
fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): BufferWriteable
fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): BufferWriteable
fun clean(hostReading: Boolean, cmd: CommandBuffer? = null): BufferWriteable
interface DynamicallyWriteable
fun write(builder: DynamicBuffer.() -> Unit): DynamicallyWriteable
fun append(builder: DynamicBuffer.() -> Unit): DynamicallyWriteable
val cursor : Int
interface BufferReadable
fun mapAndRead(builder: FloatBuffer.() -> Unit): BufferReadable
fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): BufferReadable
fun mapAndRead_ints(builder: IntBuffer.() -> Unit): BufferReadable
fun clean(hostReading: Boolean, cmd: CommandBuffer?): BufferReadable
class VulkanBuffer
class VulkanBuffer(
val renderer: Renderer = fromContext(),
var requestedSize: Long,
val usage: Int,
val reqMask: Int,
val needsDeviceAddress: Boolean = false,
val cpuOnly: Boolean = false,
) : HasBuffer, BufferWriteable, BufferReadable, DynamicallyWriteable {
var gpuAddress: Long
Volatile, like the handle fields below: the render workers read these while recording, on other threads than the one that reallocates. See realloc.
@Volatile
var gpuAddress: Long = -1
private val writeLock = Any()
VMA-managed
@Volatile
private var allocationSize: Long = 0
var allocationMemoryType: Int
var allocationMemoryType: Int = -1
private set
var allocationMemoryFlags: Int
var allocationMemoryFlags: Int = 0
private set
var buffer: Long
@Volatile
var buffer: Long = 0L
@JvmName("_getBuffer")
get() = field
VmaAllocation handle (NOT a VkDeviceMemory)
@Volatile
private var allocation: Long = 0L
CPU pointer; stays valid while mapped (we keep it persistently mapped)
@Volatile
private var mappedMemory: Long = 0L
private var hostBuffer: ByteBuffer? = null
class DescriptorBinding internal constructor(val buffer: Long, val range: Long)
The handle and range a descriptor write needs, published as one object so a render worker
never pairs one allocation's handle with another's size. Replaced (never mutated) by
allocate, realloc, cleanup and pushCleanup.
@Volatile
private var binding = DescriptorBinding(VK_NULL_HANDLE, VK_WHOLE_SIZE)
var hint_read: Boolean = false
var hint_write: Boolean = false
var watermark: Long = 0
var readWatermark: Long = -1
var writeWatermark: Long = -1
var zeroBeforeComputation = false
var source: BufferParameters? = null
private val autoDisposeToken = AutoDisposer.register(this, "VulkanBuffer(size=$requestedSize)", owner = renderer.device) { /* set after allocate */ }
init
init {
allocate(requestedSize)
autoDisposeToken.updateCleanup(snapshotCleanup())
}
Allocate a buffer + suballocation of requested bytes via VMA (kept persistently mapped), then
publish it.
Everything is built into locals first and the fields are only written at the end, handle
last and binding after it. A render worker recording a frame on another thread therefore
reads either the previous allocation (still alive: realloc retires it through the
renderer's frame-retirement queue) or this one, and never VK_NULL_HANDLE. Clearing the
handle before allocating, as this used to, let Module.Uniform.pushDescriptorSet push a
null buffer mid-realloc, which MoltenVK dereferences (MVKBuffer::getMTLBuffer, address
0x68). See dev_docs/renderer_teardown_order.md, "Live-frame crash".
source — 108 lines, from line 205
private fun allocate(requested: Long) {
if (cpuOnly) {
val size = requested.coerceAtLeast(1L).toInt()
val host = MemoryUtil.memAlloc(size)
allocationSize = size.toLong()
allocationMemoryType = -1
allocationMemoryFlags = 0
hostBuffer = host
mappedMemory = MemoryUtil.memAddress(host)
gpuAddress = 0L
allocation = 0L
buffer = 0L
requestedSize = requested
binding = DescriptorBinding(VK_NULL_HANDLE, rangeFor(requested))
return
}
MemoryStack.stackPush().use { stack ->
val usageBits = if (needsDeviceAddress) usage or VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT else usage
val size = requested.coerceAtLeast(1L)
val bufCI = VkBufferCreateInfo.calloc(stack).apply {
sType(VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO)
size(size)
usage(usageBits)
sharingMode(VK_SHARING_MODE_EXCLUSIVE)
}
val allocCI = VmaAllocationCreateInfo.calloc(stack).apply {
usage(Vma.VMA_MEMORY_USAGE_AUTO)
// Choose RANDOM host access for anything the CPU will *read* back, so VMA
// lands the allocation in HOST_CACHED memory. The two read signals are:
// - an explicit HOST_CACHED request (readback buffers, e.g. streamReads()), and
// - a transfer-destination-only staging buffer (uploaded, then read).
// Everything else is a sequential CPU->GPU write, where write-combined memory is
// ideal. Getting this wrong is expensive: SEQUENTIAL_WRITE steers VMA toward
// uncached/write-combined memory, and CPU reads from that are pathologically slow
// on discrete GPUs (no caching or prefetch) — the classic slow-readback trap.
val wantsHostReads =
reqMask and VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT != 0 && (
reqMask and VK_MEMORY_PROPERTY_HOST_CACHED_BIT != 0 ||
(usageBits and VK_BUFFER_USAGE_TRANSFER_DST_BIT != 0 &&
usageBits and VK_BUFFER_USAGE_TRANSFER_SRC_BIT == 0)
)
val hostAccessFlag =
if (wantsHostReads) {
Vma.VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT
} else {
Vma.VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT
}
flags(
hostAccessFlag or Vma.VMA_ALLOCATION_CREATE_MAPPED_BIT
)
if (reqMask != 0) {
requiredFlags(reqMask and VK_MEMORY_PROPERTY_HOST_CACHED_BIT.inv())
if (reqMask and VK_MEMORY_PROPERTY_HOST_CACHED_BIT != 0) {
preferredFlags(VK_MEMORY_PROPERTY_HOST_CACHED_BIT)
}
}
}
val pBuffer = stack.mallocLong(1)
val pAlloc = stack.mallocPointer(1)
val allocator = allocatorForRenderer(renderer)
val err = Vma.vmaCreateBuffer(allocator, bufCI, allocCI, pBuffer, pAlloc, null)
require(err == VK_SUCCESS) { "vmaCreateBuffer failed: $err" }
val newBuffer = pBuffer[0]
val newAllocation = pAlloc[0]
// Query size + mapped pointer
val info = VmaAllocationInfo.calloc(stack)
Vma.vmaGetAllocationInfo(allocator, newAllocation, info)
val newMemoryType = info.memoryType()
val newMemoryFlags = if (newMemoryType >= 0) {
renderer.physicalDevice.vkMemoryProperties.memoryTypes()[newMemoryType].propertyFlags()
} else {
0
}
// Optional: device address. Eager, so a lazy getGPUAddress() can never cache the
// previous allocation's address after a realloc.
var newAddress = 0L
if (needsDeviceAddress) {
val addrInfo = VkBufferDeviceAddressInfo.calloc(stack).apply {
sType(VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO)
buffer(newBuffer)
}
newAddress = vkGetBufferDeviceAddress(renderer.device.vkDevice, addrInfo)
require(newAddress != 0L) { "vkGetBufferDeviceAddress returned 0; check features/flags/usage." }
}
// Publish: everything else first, the handle and the descriptor binding last.
allocation = newAllocation
allocationSize = info.size()
allocationMemoryType = newMemoryType
allocationMemoryFlags = newMemoryFlags
hostBuffer = null
mappedMemory = info.pMappedData()
gpuAddress = newAddress
buffer = newBuffer
requestedSize = requested
binding = DescriptorBinding(newBuffer, rangeFor(requested))
}
}
private fun rangeFor(requested: Long): Long = if (requested > 0L) requested else VK_WHOLE_SIZE
fun realloc(bytes: Long)
Grow-resize with deferred destruction of the old buffer/allocation.
Safe against a frame being recorded on another thread: the old allocation stays valid until
the renderer's retirement queue releases it, 8 × numImages submitted frames later, and the
handle a recorder reads is never zero in between (see allocate).
fun realloc(bytes: Long) {
synchronized(writeLock) {
// After teardown began the allocator is about to go, and a retirement added now could
// miss the final drain.
requireLiveRenderer()
check(!released) { "$this was released with cleanup(); it cannot be reallocated" }
val oldBuffer = buffer
val oldAlloc = allocation
val oldHost = hostBuffer
allocate(bytes)
autoDisposeToken.updateCleanup(snapshotCleanup())
renderer.cleanup.add {
if (oldHost != null) MemoryUtil.memFree(oldHost)
if (oldBuffer != VK_NULL_HANDLE && oldAlloc != 0L) {
vmaDestroyBuffer(allocatorForRenderer(renderer), oldBuffer, oldAlloc)
}
}
}
}
fun dynamicResizer()
Optional helper you already used for dynamic growth.
fun dynamicResizer() = { was: FloatBuffer, hint: Int ->
val nextSize = 2 + max(hint, (was.capacity() * 1.5).i)
println("Realloc ${was.capacity()}")
val allocBefore = allocation
realloc(nextSize * 4L)
val mapped = map()
val data = MemoryUtil.memFloatBuffer(mapped, requestedSize.toInt() / 4)
was.clear()
data.put(was)
data
}
fun zero(cmd: CommandBuffer)
Fill with zeros then make the writes visible to subsequent compute shader reads/writes using synchronization2. If you need this to precede graphics-stage use instead, change dstStageMask to the appropriate stage(s).
fun zero(cmd: CommandBuffer) {
if (cpuOnly) {
hostBuffer?.let { MemoryUtil.memSet(it, 0) }
return
}
MemoryStack.stackPush().use { stack ->
vkCmdFillBuffer(cmd.vkCommandBuffer, buffer, 0L, allocationSize, 0)
val bufBarrier = VkBufferMemoryBarrier2.calloc(1, stack)
bufBarrier[0]
.sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
.srcStageMask(VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT)
.srcAccessMask(VK_ACCESS_2_TRANSFER_WRITE_BIT)
.dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
.dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)
.srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.buffer(buffer)
.offset(0L)
.size(allocationSize)
val dep = VkDependencyInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
.pBufferMemoryBarriers(bufBarrier)
vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
}
}
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): VulkanBuffer
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): VulkanBuffer {
if (hostReading) {
// mapAndRead* already does this inline before reading, but clean() must keep
// this promise when called on its own, ahead of some other read of the mapped memory.
invalidate()
}
// hostReading == false: no-op. Unlike Structure/VertexBuffer/FrameBufferGroup, this class
// has no deferred-write tracking -- mapAndWrite/write/append already flush eagerly on every
// call, so there is nothing pending here to commit.
return this
}
After Renderer.cleanup, AutoDisposer has destroyed this buffer's allocation (or freed its
host copy) through a snapshot that cannot clear mappedMemory, so that pointer dangles and
the allocator behind flush/invalidate is gone. Writing through it was a SIGSEGV, not an
error; this makes it an exception that names the cause. See
dev_docs/renderer_teardown_order.md.
private fun requireLiveRenderer() {
check(!renderer.isClosed) {
"$this used after its Renderer was disposed; stop the box writing it before the renderer " +
"(or its window, or its document) goes away"
}
check(!destroyed) {
"$this used after cleanup() destroyed it; drop every use of a buffer before releasing it"
}
}
fun descriptorBinding(): DescriptorBinding
The handle and range to write into a descriptor for this buffer, read once, as a pair.
Refuses, with an IllegalStateException that names the cause, rather than hand the driver a
handle it will fault on: after Renderer.cleanup has begun, and once the retirement queue
has destroyed a buffer that cleanup released while a module still binds it (between the
two the handle is still valid, and binding it is allowed). MoltenVK does not
validate a pushed buffer handle; it dereferences it while recording, so either case used to
be a SIGSEGV in MVKBuffer::getMTLBuffer on a render worker. A CPU-only buffer has no
VkBuffer and is refused too.
fun descriptorBinding(): DescriptorBinding {
requireLiveRenderer()
val bound = binding
check(bound.buffer != VK_NULL_HANDLE) {
if (cpuOnly) "$this is CPU-only and has no VkBuffer to bind to a descriptor"
else "$this was released (cleanup() or pushCleanup()) while a module still binds it; " +
"drop the binding before releasing it"
}
return bound
}
fun map(): Long
Map via VMA (or host-only); keep persistently mapped (recommended).
fun map(): Long {
requireLiveRenderer()
if (cpuOnly) return mappedMemory
if (mappedMemory == MemoryUtil.NULL) {
MemoryStack.stackPush().use { stack ->
val pp = stack.mallocPointer(1)
val err = Vma.vmaMapMemory(allocatorForRenderer(renderer), allocation, pp)
vkCheck(err, "vmaMapMemory failed")
mappedMemory = pp[0]
}
}
return mappedMemory
}
fun unmap(offset: Long = 0, size: Long = requestedSize)
Flush writes for non-coherent memory. We keep it mapped.
fun unmap(offset: Long = 0, size: Long = requestedSize) {
requireLiveRenderer()
if (!cpuOnly) {
Vma.vmaFlushAllocation(allocatorForRenderer(renderer), allocation, offset, size)
}
}
private fun invalidate(offset: Long = 0, size: Long = requestedSize) {
requireLiveRenderer()
if (!cpuOnly) {
Vma.vmaInvalidateAllocation(allocatorForRenderer(renderer), allocation, offset, size)
}
}
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): VulkanBuffer {
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): VulkanBuffer {
synchronized(writeLock) {
hint_write = true
writeWatermark++
val m = map()
val data = MemoryUtil.memFloatBuffer(m, requestedSize.toInt() / 4)
builder.invoke(data)
unmap(0, requestedSize)
return this
}
}
var writePosition = 0
dynamic write helpers
override val cursor: Int get() = writePosition
override fun write(builder: DynamicBuffer.() -> Unit): VulkanBuffer {
override fun write(builder: DynamicBuffer.() -> Unit): VulkanBuffer {
synchronized(writeLock) {
hint_write = true
writeWatermark++
val m = map()
val data = MemoryUtil.memFloatBuffer(m, requestedSize.toInt() / 4)
val db = DynamicBuffer(data, dynamicResizer())
builder.invoke(db)
writePosition = db.target.position()
unmap(0, (writePosition * 4L).coerceAtMost(requestedSize))
return this
}
}
override fun append(builder: DynamicBuffer.() -> Unit): VulkanBuffer {
override fun append(builder: DynamicBuffer.() -> Unit): VulkanBuffer {
synchronized(writeLock) {
hint_write = true
writeWatermark++
val m = map()
val data = MemoryUtil.memFloatBuffer(m, requestedSize.toInt() / 4)
val db = DynamicBuffer(data, dynamicResizer())
db.target.position(writePosition)
builder.invoke(db)
writePosition = db.target.position()
unmap(0, (writePosition * 4L).coerceAtMost(requestedSize))
return this
}
}
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): VulkanBuffer {
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): VulkanBuffer {
synchronized(writeLock) {
hint_write = true
writeWatermark++
val m = map()
val data = MemoryUtil.memByteBuffer(m, requestedSize.toInt())
builder.invoke(data)
unmap(0, requestedSize)
return this
}
}
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): VulkanBuffer {
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): VulkanBuffer {
synchronized(writeLock) {
hint_write = true
writeWatermark++
val m = map()
val data = MemoryUtil.memIntBuffer(m, requestedSize.toInt() / 4)
builder.invoke(data)
unmap(0, requestedSize)
return this
}
}
override fun mapAndRead(builder: FloatBuffer.() -> Unit): VulkanBuffer {
override fun mapAndRead(builder: FloatBuffer.() -> Unit): VulkanBuffer {
hint_read = true
readWatermark++
invalidate(0, requestedSize)
val m = map()
val data = MemoryUtil.memFloatBuffer(m, requestedSize.toInt() / 4)
builder.invoke(data)
return this
}
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): VulkanBuffer {
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): VulkanBuffer {
return mapAndReadBytes(requestedSize, builder)
}
fun mapAndReadBytes(size: Long, builder: ByteBuffer.() -> Unit): VulkanBuffer {
fun mapAndReadBytes(size: Long, builder: ByteBuffer.() -> Unit): VulkanBuffer {
hint_read = true
readWatermark++
val readSize = size.coerceIn(0L, requestedSize)
invalidate(0, readSize)
val m = map()
val data = MemoryUtil.memByteBuffer(m, readSize.toInt())
builder.invoke(data)
return this
}
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): VulkanBuffer {
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): VulkanBuffer {
hint_read = true
readWatermark++
invalidate(0, requestedSize)
val m = map()
val data = MemoryUtil.memIntBuffer(m, requestedSize.toInt() / 4)
builder.invoke(data)
return this
}
fun pushCleanup() = cleanup()
Same as cleanup, which now always retires through the renderer's queue while the renderer is alive. Kept for the callers that used it to ask for exactly that.
fun cleanup()
Release this buffer.
While its renderer is alive the VkBuffer and its memory are destroyed through the renderer's
retirement queue (Renderer.cleanup, released 8 × numImages submitted frames later), so a
frame that recorded or is executing the buffer, or a render worker that read it a moment
ago, still finds it valid. Until then it behaves as before; afterwards map, unmap,
descriptorBinding and realloc refuse by name. After the renderer has closed (device
idle) it is destroyed at once.
Destroying immediately, as this used to, was reproducible in a live Field: swapping a
module's per-frame uniform buffers for new ones and calling cleanup() on the old from a
thread crashed the JVM in MVKBuffer::getMTLBuffer (a destroyed handle pushed by a render
worker); doing it from a box's own loop {} logged VUID-vkDestroyBuffer-buffer-00922
(destroyed while a submitted frame used it) 40 times in 15 s. See
dev_docs/renderer_teardown_order.md, "Immediate cleanup mid-frame".
fun cleanup() {
if (!autoDisposeToken.disable()) return
released = true
if (renderer.isClosed) destroyNow()
else renderer.cleanup.add(Runnable { destroyNow() })
}
Set by cleanup: the handles are on their way to the retirement queue.
@Volatile
private var released = false
val isReleased: Boolean get() = released
True once cleanup has released this buffer (its handles may not be destroyed yet).
Set once the handles are actually destroyed; everything that would touch them refuses.
@Volatile
private var destroyed = false
private fun destroyNow() {
destroyed = true
binding = DescriptorBinding(VK_NULL_HANDLE, binding.range)
val host = hostBuffer
val buf = buffer
val alloc = allocation
buffer = VK_NULL_HANDLE
allocation = 0L
mappedMemory = 0L
allocationSize = 0L
allocationMemoryType = -1
allocationMemoryFlags = 0
gpuAddress = 0L
hostBuffer = null
if (host != null) MemoryUtil.memFree(host)
if (buf != VK_NULL_HANDLE && alloc != 0L) {
vmaDestroyBuffer(allocatorForRenderer(renderer), buf, alloc)
}
}
class Cleaner
class Cleaner(
val renderer: Renderer,
val cbuffer: Long,
val callocation: Long,
val chostBuffer: ByteBuffer?
) : Runnable {
override fun run()
override fun run() {
if (chostBuffer != null) MemoryUtil.memFree(chostBuffer)
if (cbuffer != VK_NULL_HANDLE && callocation != 0L) {
vmaDestroyBuffer(allocatorForRenderer(renderer), cbuffer, callocation)
}
}
}
private fun snapshotCleanup(): () -> Unit {
val renderer = renderer
val buf = buffer
val alloc = allocation
val host = hostBuffer
return {
if (host != null) MemoryUtil.memFree(host)
if (buf != VK_NULL_HANDLE && alloc != 0L) {
vmaDestroyBuffer(allocatorForRenderer(renderer), buf, alloc)
}
}
}
override fun toString(): String
override fun toString(): String {
return "VulkanBuffer(requestedSize=$requestedSize, memory=${memoryFlagsForString(allocationMemoryFlags)}${if (mappedMemory != MemoryUtil.NULL) " MAPPED" else ""})"
}
val descriptorRange: Long
val descriptorRange: Long
get() = if (requestedSize > 0L) requestedSize else VK_WHOLE_SIZE
private fun memoryFlagsForString(flags: Int): String {
if (cpuOnly) return "CPU_ONLY"
if (flags == 0) return "auto"
val names = mutableListOf<String>()
if (flags and VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT != 0) names += "DEVICE_LOCAL"
if (flags and VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT != 0) names += "HOST_VISIBLE"
if (flags and VK_MEMORY_PROPERTY_HOST_COHERENT_BIT != 0) names += "HOST_COHERENT"
if (flags and VK_MEMORY_PROPERTY_HOST_CACHED_BIT != 0) names += "HOST_CACHED"
if (flags and VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT != 0) names += "LAZY"
if (flags and VK_MEMORY_PROPERTY_PROTECTED_BIT != 0) names += "PROTECTED"
return names.joinToString("|").ifEmpty { "0x${flags.toString(16)}" }
}
override fun getBuffer(): Long = buffer
override fun getGPUAddress(cmd: CommandBuffer): Long
override fun getGPUAddress(cmd: CommandBuffer): Long {
if (cpuOnly) return 0L
if (gpuAddress > 0) return gpuAddress
MemoryStack.stackPush().use { stack ->
val info = VkBufferDeviceAddressInfo.calloc(stack).apply {
sType(VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO)
buffer(buffer)
}
gpuAddress = vkGetBufferDeviceAddress(renderer.device.vkDevice, info)
return gpuAddress
}
}
}