Structure.kt
src/jvmMain/kotlin/afield/graphics/vulkan/buffers/Structure.kt · 677 lines · cited by Keyframes: capturing, persisting and replaying live state
package afield.graphics.vulkan.buffers
import afield.math.linalg.Vector2f
import afield.math.linalg.Vector3f
import afield.math.linalg.Vector4f
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.shader.Introspected
import afield.graphics.shader.layoutSizeInBytes
import afield.graphics.shader.memberTypeKey
import afield.graphics.shader.memberTypeName
import afield.app.diagnostics.logging.lprintln
import afield.graphics.vulkan.util.Vec2h
import afield.graphics.vulkan.util.Vec3h
import afield.graphics.vulkan.util.Vec4h
import afield.graphics.vulkan.util.packHalf1x16
import afield.graphics.vulkan.util.packHalf2x16
import afield.animation.interpolation.NeedsStartingValue
import afield.math.linalg.*
import java.nio.ByteBuffer
import java.nio.FloatBuffer
import java.nio.IntBuffer
import kotlin.reflect.KProperty
Structure maps a shader's introspected uniform block or push-constant block onto named Kotlin members, so a box can set, read and bind values by member name.
A Structure is built from the Introspected.Member list that shader reflection produces, and
writes each value at its reflected offset when the block is uploaded. Values can be set
directly, or bound to a function that is re-evaluated each time the block is written
(bindPushConstant, HasUniform); a ProgressingUniform keeps being written until it reports
completion. A member that nothing has been assigned to fails with unassignedMemberMessage.
interface HasUniform<T> : () -> T {
A uniform value computed on demand: evaluateUniform is called each time the owning Structure is written.
fun evaluateUniform(): T
override fun invoke() = evaluateUniform()
interface ProgressingUniform
Marker for uniforms that progress over time and need to keep being written until they report completion.
val isComplete: Boolean
interface WritesToBuffer
fun writeTo(b : ByteBuffer, baseOffset : Int)
class MissingRefException(override val message: String) : Exception(message)
fun unassignedMemberMessage(member: Any?): String
The message for a shader member that has nothing bound when a dispatch or draw needs it. It is
not a missing buffer: nothing has been assigned to the member yet, and the usual reason is a
binding written inside loop {}, which takes effect a frame after the first dispatch.
fun unassignedMemberMessage(member: Any?): String =
"shader member `$member` has not been assigned yet, so this dispatch would read a null GPU reference. " +
"Bindings set inside loop {} take effect a frame late; assign it once at the top level of the box."
fun <T : Structure> HasBuffer.asStructure(structure: T, members: List<Introspected.Member>): T
fun <T : Structure> HasBuffer.asStructure(structure: T, members: List<Introspected.Member>): T {
structure.setMembers(members)
structure.proxyTo(this)
return structure
}
fun <T : Structure> BufferParameters.asStructure(structure: T, members: List<Introspected.Member>): T
fun <T : Structure> BufferParameters.asStructure(structure: T, members: List<Introspected.Member>): T =
buffer.asStructure(structure, members)
fun <T> Structure.setPushConstant(name: String, value: T, notFound: () -> Nothing) {
Sets a push-constant value by name, or invokes notFound if name isn't a known offset.
Shared by the FromStructure/BoundFromStructure delegates on Structure's various hosts
(Mesh, MeshMesh, ComputeMesh, RayTracingDispatch, and their groups), which otherwise
each hand-rolled this same offset-check-then-write shape.
fun <T> Structure.setPushConstant(name: String, value: T, notFound: () -> Nothing) {
if (offsets.containsKey(name)) set(name, value as Any) else notFound()
}
fun <T> Structure.getPushConstant(name: String, notFound: () -> Nothing): T =
Reads a push-constant value by name, or invokes notFound if name isn't a known offset.
fun <T> Structure.getPushConstant(name: String, notFound: () -> Nothing): T =
if (offsets.containsKey(name)) get(name) else notFound()
fun Structure.bindPushConstant(name: String, value: Function0<*>, notFound: () -> Nothing) {
Binds a live push-constant supplier by name, or invokes notFound if name isn't a known offset.
fun Structure.bindPushConstant(name: String, value: Function0<*>, notFound: () -> Nothing) {
if (!offsets.containsKey(name)) notFound()
val unsigned = members.firstOrNull { it.name == name }?.type?.fold(
{ it == afield.graphics.shader.PrimitiveType.uint }, { false }
) == true
bind(name, if (unsigned) {
{
val next = value()
require(next is Int || next is UInt) { "uint push member '$name' needs an Int or UInt, got ${next?.javaClass?.simpleName}" }
next
}
} else value)
}
fun <T> Structure.getBoundPushConstant(name: String, notFound: () -> Nothing): () -> T =
Reads a bound push-constant supplier by name, or invokes notFound if name isn't a known offset.
@Suppress("UNCHECKED_CAST")
fun <T> Structure.getBoundPushConstant(name: String, notFound: () -> Nothing): () -> T =
if (offsets.containsKey(name)) bindings[name] as () -> T else notFound()
fun <E> Iterable<E>.bindPushConstantAcrossGroup
Binds value as a push constant on every element of a group, initializing each element's
offsets on first use via ensureInitialized. Throws (via notFound, given the offending
element) if some element in the group doesn't have name.
fun <E> Iterable<E>.bindPushConstantAcrossGroup(
name: String,
value: Function0<*>,
push: (E) -> Structure,
ensureInitialized: (E) -> Unit,
notFound: (E) -> Nothing,
) {
forEach { element ->
ensureInitialized(element)
push(element).bindPushConstant(name, value) { notFound(element) }
}
}
fun <E, T> Iterable<E>.firstBoundPushConstant
Finds the first element of a group that has name bound, initializing each element's offsets
on first use via ensureInitialized. Throws notFound if no element has it.
@Suppress("UNCHECKED_CAST")
fun <E, T> Iterable<E>.firstBoundPushConstant(
name: String,
push: (E) -> Structure,
ensureInitialized: (E) -> Unit,
notFound: () -> Nothing,
): () -> T {
forEach { element ->
ensureInitialized(element)
val s = push(element)
if (s.offsets.containsKey(name)) return s.bindings[name] as () -> T
}
notFound()
}
open class Structure
@Suppress("UNCHECKED_CAST")
open class Structure(
var members: List<Introspected.Member> = emptyList(),
private val baseOffset: Int = 0,
private val pathPrefix: String = "",
private val root: Structure? = null
) : BufferWriteable, BufferReadable, HasBuffer {
class FromStructure<T>
operator fun setValue(inside: Structure, property: KProperty<*>, any: T)
operator fun setValue(inside: Structure, property: KProperty<*>, any: T) {
inside.set(property.name, any as Any)
}
operator fun getValue(inside: Structure, property: KProperty<*>): T
operator fun getValue(inside: Structure, property: KProperty<*>): T {
return inside.get(property.name) as T
}
}
var nullPointerChecks: MutableMap<String, Boolean> = mutableMapOf()
var offsets = mutableMapOf<String, Int>()
var sizes = mutableMapOf<String, Int>()
var pointers = mutableMapOf<String, Boolean>()
val bindings: MutableMap<String, Function0<*>> = root?.bindings ?: mutableMapOf()
structures elements either have live bindings (evaluated on demand) or values
val values: MutableMap<String, Any> = root?.values ?: mutableMapOf()
var allBinding : (() -> WritesToBuffer)? = null
Bumped by every change a write has to carry (set, bind, bindAll, a carry-over). A per-frame
uniform is a FrameBufferGroup with one buffer per swapchain image, and a change has to reach
each of them: writtenGeneration records, per buffer slot, the generation that slot last got, so
needsWrite is per slot. A single dirty flag cleared after writing the current image's buffer
left the others holding the old value, and frames alternated between the two.
@Volatile
private var generation = 0L
private val writtenGeneration = java.util.concurrent.ConcurrentHashMap<Int, Long>()
How many slots the buffer this structure is written into has — the swapchain image count for a per-frame block; 1 until a write says otherwise.
@Volatile
private var slotCount = 1
var dirty: Boolean
Whether some buffer slot may still hold older values than this structure — setting it true is
markDirty; setting it false records every slot as current.
var dirty: Boolean
get() {
val r = rootStructure()
return (0 until r.slotCount).any { r.writtenGeneration.getOrDefault(it, 0L) != r.generation }
}
set(value) {
val r = rootStructure()
if (value) markDirty() else (0 until r.slotCount).forEach { r.writtenGeneration[it] = r.generation }
}
fun needsWrite(slot: Int): Boolean
Whether buffer slot needs this structure written into it: it has live bindings (evaluated per
write), a whole-block binding, or a change newer than the last write to that slot.
fun needsWrite(slot: Int): Boolean {
val r = rootStructure()
return r.bindings.isNotEmpty() || r.allBinding != null ||
r.writtenGeneration.getOrDefault(slot, 0L) != r.generation
}
fun writeIfStale(target: FrameBufferGroup, cmd: CommandBuffer?, autowireReferences: (String) -> () -> HasBuffer) {
Write this structure into target's buffer for the current swapchain image, if that buffer is
stale (needsWrite). The one path a module's per-frame uniform blocks are written by.
fun writeIfStale(target: FrameBufferGroup, cmd: CommandBuffer?, autowireReferences: (String) -> () -> HasBuffer) {
val slots = maxOf(1, target.buffers.size)
writeSlotIfStale(target.renderer.currentImage % slots, slots, cmd, autowireReferences) { body -> target.mapAndWrite_bytes(body) }
}
internal fun writeSlotIfStale
writeIfStale for an explicit slot of slots and writer — what writeIfStale and a test share.
internal fun writeSlotIfStale(
slot: Int,
slots: Int,
cmd: CommandBuffer?,
autowireReferences: (String) -> () -> HasBuffer,
mapAndWrite: (ByteBuffer.() -> Unit) -> Unit,
): Boolean {
val r = rootStructure()
r.slotCount = slots
if (!needsWrite(slot)) return false
// Record the generation this write carries, read before writing: a set() that lands while the
// write runs bumps it again and the slot stays stale.
val carried = r.generation
mapAndWrite { r.writeToBuffer(this, cmd, autowireReferences) }
r.writtenGeneration.merge(slot, carried) { a, b -> maxOf(a, b) }
return true
}
var proxying: FrameBufferGroup?
Legacy group binding, sharing the backing buffer used by mapping and nested structures.
var proxying: FrameBufferGroup?
get() = rootStructure().proxyBuffer as? FrameBufferGroup
set(value) {
rootStructure().proxyBuffer = value
}
private var proxyBuffer: HasBuffer? = null
private fun rootStructure(): Structure = root?.rootStructure() ?: this
private fun storageKey(property: String): String =
if (pathPrefix.isBlank()) property else "$pathPrefix.$property"
private fun markDirty() {
generation++
root?.markDirty()
}
fun setMembers(members: List<Introspected.Member>): Structure
fun setMembers(members: List<Introspected.Member>): Structure {
this.members = members
offsets = members.associate { it.name to baseOffset + it.offset }.toMutableMap()
sizes = members.associate { it.name to it.layoutSizeInBytes() }.toMutableMap()
pointers = members.associate {
it.name to it.physicalPointer
}.toMutableMap()
return this
}
operator fun set(property: String, value: Any)
Set property to value, held until the next write to the buffer.
Once this structure has a layout, property must be one of its members and value must fit the
member's reflected type — the same shape, and a number the member can hold without loss, converted
when it is written (coerce: an Int into a uint64_t or a float, a Double into a float,
a whole Float into an int); otherwise nothing is stored, and the executing box is
warned on the lines of its code that name property. Both used to be stored anyway — an unknown
member with a stdout println, a mistyped one with nothing at all — and the mistyped one was then
written by its Kotlin type over whatever came next in the block. A structure with no layout yet
(a mesh's push constants before its module is known) stores what it is given, as before.
operator fun set(property: String, value: Any) {
val key = storageKey(property)
if (members.isNotEmpty()) {
val member = members.firstOrNull { it.name == property }
val refusal = if (member == null)
"`$property` is not in this block — the shader declares ${members.joinToString { "`${it.name}`" }.ifEmpty { "nothing" }}"
else member.refusalFor(value)
if (refusal != null) {
refuseSet(property, "$refusal; the value was not stored")
return
}
}
markDirty()
(value as? NeedsStartingValue<*>)?.startAt(bindings[key] ?: values[key])
values[key] = value
bindings.remove(key)
}
var reportTo: ((String) -> Unit)? = null
Where a write this structure refuses is reported (see refusalFor). A module points its uniform structures at its error handler, which puts the message on the box; unset, it goes to the log.
One report per distinct refusal: a value set every frame would otherwise report every frame.
private val reported: MutableSet<String> = java.util.Collections.synchronizedSet(mutableSetOf())
private fun refuseSet(property: String, message: String) {
val box = afield.runtime.lifecycle.RunScope.executingBox()
if (!rootStructure().reported.add("set:${box?.id}:$message")) return
lprintln("warning,graphics", message)
if (box != null) afield.ui.output.markShaderReferenceLines(box, mapOf(property to message))
}
private fun refuseWrite(key: String, why: String) {
val root = rootStructure()
if (!root.reported.add("write:$key:$why")) return
val message = "Not writing `$key` to the shader: $why. The value was set for a different interface; " +
"set one of the type the shader now declares."
root.reportTo?.let { runCatching { it(message) } } ?: lprintln("warning,graphics", message)
}
internal fun carryValuesFrom(old: Structure, blockName: String): List<String>
Take over old's values and bindings wherever they still fit this structure's layout — the
value-carrying half of a full shader reload. A plain value is kept when its member still exists and
the value fits the member's new type; a binding (a lambda, whose result cannot be judged without
running it) when its member exists with the same type as before. Returns one line per thing
dropped, naming it as [blockName](../../../../../../../reference/source/jvmMain/afield/lang/stacksl/stackAutoNumbers.md#L87).member.
internal fun carryValuesFrom(old: Structure, blockName: String): List<String> {
val dropped = mutableListOf<String>()
for ((key, value) in old.values.toMap()) {
val member = memberAtPath(members, key)
val why = if (member == null) "no longer in the shader" else member.refusalFor(value)
if (why == null) values[key] = value else dropped += "`$blockName.$key` dropped: $why"
}
for ((key, binding) in old.bindings.toMap()) {
val member = memberAtPath(members, key)
val was = memberAtPath(old.members, key)
when {
member == null -> dropped += "`$blockName.$key` (bound) dropped: no longer in the shader"
was != null && memberTypeKey(member) != memberTypeKey(was) ->
dropped += "`$blockName.$key` (bound) dropped: now ${memberTypeName(member)}, was ${memberTypeName(was)}"
else -> bindings[key] = binding
}
}
if (old.allBinding != null) {
if (old.members == members) allBinding = old.allBinding
else dropped += "`$blockName` whole-block binding dropped: the layout changed"
}
if (values.isNotEmpty() || bindings.isNotEmpty() || allBinding != null) markDirty()
return dropped
}
fun bind(property: String, value: Function0<*>)
fun bind(property: String, value: Function0<*>) {
markDirty()
val key = storageKey(property)
(value as? NeedsStartingValue<*>)?.startAt(bindings[key] ?: values[key])
bindings[key] = value
}
fun bindAll(value : () -> WritesToBuffer){
fun bindAll(value : () -> WritesToBuffer){
markDirty()
allBinding = value
}
operator fun <T> get(property: String)
operator fun <T> get(property: String) = (bindings[storageKey(property)]?.invoke() ?: values[storageKey(property)]) as T
fun bindingFor(property: String): Function0<*>? = bindings[storageKey(property)]
The live supplier for property if it is bound, or null if it is held as a plain value or is
not held at all — get without invoking the binding, for a caller that wants the supplier itself.
Honours this structure's nested path, which reading bindings directly does not.
fun valueFor(property: String): Any? = values[storageKey(property)]
The plain value for property if it is held as one, or null if it is bound or absent — the other
half of bindingFor. A bound property reads null here even though get returns something.
fun setProxyFor(p: FrameBufferGroup)
fun setProxyFor(p: FrameBufferGroup) {
proxyTo(p)
}
fun proxyTo(p: HasBuffer): Structure
Forwards this structure's buffer access to p and returns this structure
for chaining.
When p is a FrameBufferGroup, it is also recorded as the active proxy
target for direct member writes.
fun proxyTo(p: HasBuffer): Structure {
rootStructure().proxyBuffer = p
return this
}
fun <T : Structure> childStructure
fun <T : Structure> childStructure(
property: String,
builder: (List<Introspected.Member>, Int, String, Structure) -> T
): T {
val member = members.firstOrNull { it.name == property }
?: throw IllegalArgumentException("Can't find '$property' in this Structure, options are ${members.map { it.name }}")
val nested = member.type.fold({ null }, { it })
?: throw IllegalArgumentException("'$property' is not a nested structure")
return builder(nested, baseOffset + member.offset, storageKey(property), rootStructure()).also {
it.setMembers(nested)
}
}
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): Structure {
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): Structure {
val target = rootStructure().proxyBuffer
when (target) {
is FrameBufferGroup -> target.mapAndWrite(builder)
is VulkanBuffer -> target.mapAndWrite(builder)
is BufferWriteable -> target.mapAndWrite(builder)
else -> null
}?.let {
return this
} ?: throw IllegalArgumentException("This Structure is not bound to a writable Buffer")
return this
}
fun write(name: String, builder: FloatBuffer.() -> Unit) {
fun write(name: String, builder: FloatBuffer.() -> Unit) {
rootStructure().proxying?.let {
val m = offsets[name]
?: throw IllegalArgumentException("Can't find '$name' in this Structure, options are ${offsets.keys}")
it.mapAndWrite {
this.position(m)
builder()
}
} ?: throw IllegalArgumentException("This Structure is not bound to any Buffer")
}
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): Structure {
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): Structure {
val target = rootStructure().proxyBuffer
when (target) {
is FrameBufferGroup -> target.mapAndWrite_bytes(builder)
is VulkanBuffer -> target.mapAndWrite_bytes(builder)
is BufferWriteable -> target.mapAndWrite_bytes(builder)
else -> null
} ?: throw IllegalArgumentException("This Structure is not bound to a writable Buffer")
return this
}
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): Structure {
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): Structure {
val target = rootStructure().proxyBuffer
when (target) {
is FrameBufferGroup -> target.mapAndWrite_ints(builder)
is VulkanBuffer -> target.mapAndWrite_ints(builder)
is BufferWriteable -> target.mapAndWrite_ints(builder)
else -> null
} ?: throw IllegalArgumentException("This Structure is not bound to a writable Buffer")
return this
}
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): Structure
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): Structure {
flushIfNeeded(cmd)
return this
}
fun primeBufferReferences(cmd: CommandBuffer, autowireReferences: (String) -> () -> HasBuffer) {
body — 44 lines, from line 467
fun primeBufferReferences(cmd: CommandBuffer, autowireReferences: (String) -> () -> HasBuffer) {
nullPointerChecks = pointers.filter { it.value }.toMutableMap()
members.forEach { primeMember(it, cmd, pathPrefix, autowireReferences) }
if (nullPointerChecks.isNotEmpty()) {
nullPointerChecks.keys.toList().forEach { name ->
val buffer = autowireReferences(name)
bindings[name] = { buffer }
primeValue(buffer, cmd, name)
}
}
}
private fun primeMember(
member: Introspected.Member,
cmd: CommandBuffer,
prefix: String,
autowireReferences: (String) -> () -> HasBuffer
) {
val key = if (prefix.isBlank()) member.name else "$prefix.${member.name}"
val raw = bindings[key]?.invoke() ?: values[key]
raw?.let { primeValue(it, cmd, key) }
if (!member.physicalPointer) {
member.type.fold(
{},
{ nested -> nested.forEach { primeMember(it, cmd, key, autowireReferences) } }
)
}
}
private fun primeValue(v: Any, cmd: CommandBuffer, name: String) {
when (v) {
is HasUniform<*> -> {
val resolved = v.evaluateUniform() ?: return
primeValue(resolved, cmd, name)
}
is HasBuffer -> {
val address = v.getGPUAddress(cmd)
if (address <= 0) {
throw IllegalArgumentException(" buffer for member ${name} does not have a gpu address. Make sure you call .addressable() on it's buffer parameters")
}
nullPointerChecks.remove(name)
}
}
}
fun writeToBuffer(f: ByteBuffer, cmd : CommandBuffer?, autowireReferences: (String) -> () -> HasBuffer) {
body — 52 lines, from line 513
fun writeToBuffer(f: ByteBuffer, cmd : CommandBuffer?, autowireReferences: (String) -> () -> HasBuffer) {
nullPointerChecks = pointers.filter { it.value }.toMutableMap()
var keepDirty = false
allBinding?.let {
it().writeTo(f, 0)
}
keepDirty = writeMembersToBuffer(members, baseOffset, pathPrefix, f, cmd) || keepDirty
if (nullPointerChecks.isNotEmpty()) {
nullPointerChecks.keys.forEach { name ->
println("WARNING: trying to autowire $name (and ultimately ${nullPointerChecks.keys}) into push constants")
val buffer = autowireReferences(name)
bindings[name] = { buffer }
writeToBuffer(offsets[name]!!, buffer, f, cmd, name = name)
}
}
// A progressing uniform still animating: every slot has to be written again.
if (keepDirty) markDirty()
}
private fun writeMembersToBuffer(
members: List<Introspected.Member>,
baseOffset: Int,
prefix: String,
f: ByteBuffer,
cmd: CommandBuffer?
): Boolean {
var keepDirty = false
members.forEach { member ->
val key = if (prefix.isBlank()) member.name else "$prefix.${member.name}"
val raw = bindings[key]?.invoke() ?: values[key]
if (raw != null) {
val resolved = if (raw is HasUniform<*>) {
if (raw is ProgressingUniform && !raw.isComplete) keepDirty = true
raw.evaluateUniform()
} else raw
when (val coerced = resolved?.let { member.coerce(it) }) {
is Coerced.Refused -> refuseWrite(key, coerced.why)
is Coerced.Written -> writeToBuffer(baseOffset + member.offset, coerced.value, f, cmd, name = key)
null -> {}
}
} else if (!member.physicalPointer) {
member.type.fold(
{},
{ nested ->
keepDirty = writeMembersToBuffer(nested, baseOffset + member.offset, key, f, cmd) || keepDirty
}
)
}
}
return keepDirty
}
internal fun writeToBuffer(offset: Int, v: Any, f: ByteBuffer, cmd : CommandBuffer?, name: String = "unnamed")
body — 56 lines, from line 566
internal fun writeToBuffer(offset: Int, v: Any, f: ByteBuffer, cmd : CommandBuffer?, name: String = "unnamed") {
when (v) {
is Float -> f.putFloat(offset, v)
is Int -> f.putInt(offset, v)
is Long -> f.putLong(offset, v)
is Vector2f -> v.get(offset, f)
is Vector3f -> v.get(offset, f)
is Vector4f -> v.get(offset, f)
is Matrix2f -> v.get(offset, f)
is Matrix3f -> v.get(offset, f)
is Matrix4f -> v.get(offset, f)
is Quaternionf -> {
f.putFloat(offset, v.x)
f.putFloat(offset + 4, v.y)
f.putFloat(offset + 8, v.z)
f.putFloat(offset + 12, v.w)
}
is Vec2h -> {
val t = v.v
f.putInt(offset, packHalf2x16(t.x, t.y))
}
is Vec3h -> {
val t = v.v
f.putInt(offset, packHalf2x16(t.x, t.y))
f.putInt(offset + 4, packHalf1x16(t.z))
}
is Vec4h -> {
val t = v.v
f.putInt(offset, packHalf2x16(t.x, t.y))
f.putInt(offset + 4, packHalf2x16(t.z, t.w))
}
// 8-lane fp16 vector: its own writer emits the 16-byte uvec4 layout (two halfs per lane).
is Vector8h -> v.get(offset, f)
is UInt -> f.putInt(offset, v.toInt())
is HalfBits -> f.putShort(offset, v.bits)
is ByteBuffer -> {
var was = f.position()
f.position(offset)
f.put(v)
f.position(was)
}
is WritesToBuffer -> v.writeTo(f, offset)
is HasUniform<*> -> writeToBuffer(offset, v.evaluateUniform()!!, f, cmd, name)
is HasBuffer -> {
val address = v.getGPUAddress(cmd ?: throw IllegalStateException("writing buffer reference `$name` needs a command buffer"))
if (address <= 0) {
throw IllegalArgumentException(" buffer for member ${name} does not have a gpu address. Make sure you call .addressable() on it's buffer parameters")
}
nullPointerChecks.remove(name)
writeToBuffer(offset, address, f, cmd, name)
}
else -> println("Warning: unknown type: $v")
}
}
override fun mapAndRead(builder: FloatBuffer.() -> Unit): Structure {
override fun mapAndRead(builder: FloatBuffer.() -> Unit): Structure {
val target = rootStructure().proxyBuffer
when (target) {
is FrameBufferGroup -> target.mapAndRead(builder)
is VulkanBuffer -> target.mapAndRead(builder)
is BufferReadable -> target.mapAndRead(builder)
else -> null
} ?: throw IllegalArgumentException("This Structure is not bound to a readable Buffer")
return this
}
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): Structure {
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): Structure {
val target = rootStructure().proxyBuffer
when (target) {
is FrameBufferGroup -> target.mapAndRead_bytes(builder)
is VulkanBuffer -> target.mapAndRead_bytes(builder)
is BufferReadable -> target.mapAndRead_bytes(builder)
else -> null
} ?: throw IllegalArgumentException("This Structure is not bound to a readable Buffer")
return this
}
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): Structure {
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): Structure {
val target = rootStructure().proxyBuffer
when (target) {
is FrameBufferGroup -> target.mapAndRead_ints(builder)
is VulkanBuffer -> target.mapAndRead_ints(builder)
is BufferReadable -> target.mapAndRead_ints(builder)
else -> null
} ?: throw IllegalArgumentException("This Structure is not bound to a readable Buffer")
return this
}
override fun getBuffer(): Long
override fun getBuffer(): Long =
rootStructure().proxyBuffer?.getBuffer()
?: throw IllegalArgumentException("This Structure is not bound to any Buffer")
override fun getGPUAddress(cmd: CommandBuffer): Long
override fun getGPUAddress(cmd: CommandBuffer): Long {
flushIfNeeded(cmd)
return rootStructure().proxyBuffer?.getGPUAddress(cmd)
?: throw IllegalArgumentException("This Structure is not bound to any Buffer")
}
private fun flushIfNeeded(cmd: CommandBuffer?) {
val r = rootStructure()
if (cmd == null) return
val target = r.proxyBuffer
val slots = if (target is FrameBufferGroup) maxOf(1, target.buffers.size) else 1
val slot = if (target is FrameBufferGroup) target.renderer.currentImage % slots else 0
r.writeSlotIfStale(slot, slots, cmd, { name ->
throw MissingRefException("No autowire resolver is available for '$name' while writing a Structure-backed buffer")
}) { body -> r.mapAndWrite_bytes(body) }
}
}