superpc.kt
src/jvmMain/kotlin/afield/graphics/vulkan/util/superpc.kt · 839 lines
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package afield.graphics.vulkan.util
Push constants as a Kotlin class, and the GLSL that reads them back. A subclass of
PushConstants declares its members as ordinary var properties; reflection turns those into a
byte layout with explicit offsets, glsl() emits the push-constant block and accessors that match
it, and writeTo serialises the current values into a buffer by those same offsets.
The reflection runs once per class and the layout is cached, so the per-frame cost is the write.
Members are laid out in @PcOrder order where that annotation is given and alphabetically where
it is not, which is what makes a layout stable from one run to the next.
What the generated GLSL looks like
One push-constant block, a layout(offset = …) on every member, a float register file loaded out
of that block by loadRegs(), and then accessors under the names the Kotlin class used:
{{ pc.dec }}
void main() {
loadRegs();
uFragColor = baseColor();
}
A scalar member becomes a macro, so time in Kotlin reads as time in shader code. A vector
member becomes a function of no arguments under its own name, so baseColor reads as
baseColor(). To keep those macros clear of the block's own members, a member inside the block
carries a leading underscore: Kotlin's time is declared as _time and the bare name is the
macro that reads it out of the register file.
Instruction payloads
A property whose name already begins with an underscore is an instruction payload: it is declared in the push-constant block under exactly that name and nothing else happens to it. It is not loaded into the register file and gets no accessor, so a shader reads it from the block itself and a frame pays nothing for payloads it is not reading.
Member types
Float, Vector2f, Vector3f and Vector4f become a float, vec2, vec3 and vec4. The
half-precision types travel packed into unsigned integers, to be unpacked in the shader: Vec2h
as a uint, Vec3h and Vec4h as a uvec2, and the eight-lane Vector8h as a uvec4. Every
member has to be a var, since the layout reads and writes through the property itself, and any
other type fails when the layout is built rather than at draw time. otherDeclarations is the way
around all of this: raw GLSL inserted into the block that reflection never sees, which you place
yourself at nextFreeOffset(…).
class MaterialPC : PushConstants<MaterialPC>() {
@PcOrder(0) var time: Float = 0f
@PcOrder(1) var baseColor: Vector4f = Vector4f(1f, 1f, 1f, 1f)
@PcOrder(2) var uvScale: Vector2f = Vector2f(1f, 1f)
@PcOrder(100) var _patch0: Vec4h = Vector4f().half
}
val pc = MaterialPC()
pc.time = 1.0f
pc["uvScale"] = Vector2f(2f, 2f)
shaderTemplate["pc"] = mapOf("dec" to Resolver { pc.glsl() })
mesh.push.bindAll { pc }
The block reaches the shader through the template engine, as above, and the instance is bound by
handing it to a mesh's push-constant structure, which writes it out each time that mesh's push
constants are assembled for a draw. The buffer it is written into has to be little-endian, and a
ByteBuffer is big-endian until it is told otherwise.
import afield.graphics.vulkan.buffers.WritesToBuffer
import afield.math.linalg.Vector2f
import afield.math.linalg.Vector3f
import afield.math.linalg.Vector4f
import afield.math.linalg.Vector8h
import java.nio.ByteBuffer
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
Ordering annotation (optional)
annotation class PcOrder(val value: Int)
Fixes where a push-constant member sits in the block, lowest value first.
Members carrying this annotation are laid out in its order and come before every member that does
not, which are laid out alphabetically after them. Numbering the whole class buys two things.
Packing: each member is aligned to its own size, so a vec3 followed by a float fills sixteen
bytes exactly where the reverse order wastes twelve. And stability: under the alphabetical
fallback a member named early in the alphabet shifts every offset after it, which silently moves
where nextFreeOffset points and invalidates any layout(offset = …) written by hand into
otherDeclarations. Values need not be contiguous; leaving a gap is how payload members take the
high numbers.
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class PcOrder(val value: Int)
Layout / schema model
internal enum class PcKind(val regFloats: Int)
internal enum class PcKind(val regFloats: Int) {
F1(1),
V2(2),
V3(3),
V4(4),
H2(2), // Vec2h -> uint
H3(3), // Vec3h -> uvec2
H4(4), // Vec4h -> uvec2
H8(8) // Vector8h -> uvec4
}
internal data class FieldInfo
internal data class FieldInfo(
val name: String,
val kind: PcKind,
val pcOffsetBytes: Int,
val pcSizeBytes: Int,
val regIndex: Int?, // null if excluded from regs (underscore)
val property: KProperty1<Any, *>
)
internal class Schema
internal class Schema(
val fields: List<FieldInfo>,
val regFloatCount: Int,
val pcTotalBytes: Int,
val regIndexByName: Map<String, Int>,
val pcOffsetByName: Map<String, Int>
)
Base class
abstract class PushConstants<T : Any> : WritesToBuffer
Base class for a push-constant block written as Kotlin properties, which generates its own matching GLSL declaration.
Subclass it, declare each member as a var of a supported type, and the layout is derived by
reflection on first use: glsl() emits the block and its accessors, writeTo serialises the
current values at the same offsets, and pcOffsetOf/regIndexOf answer where a member landed. The
two sides cannot disagree, because both read one cached schema.
The type parameter is the subclass's own type and is not used by anything here; write
class MaterialPC : PushConstants<MaterialPC>().
abstract class PushConstants<T : Any> : WritesToBuffer {
companion object
companion object {
private val cache = ConcurrentHashMap<KClass<*>, Schema>()
}
private val schema: Schema by lazy {
cache.computeIfAbsent(this::class) { buildSchema(it) }
}
open var otherDeclarations: String = ""
Optional raw GLSL declarations inserted into the push-constant block.
This is NOT reflected into the schema: - no automatic offset calculation - no register loading - no accessors
Use this for hand-authored members you want visible in GLSL only.
fun pcSizeBytes(): Int = schema.pcTotalBytes
How many bytes this block occupies, rounded up to a multiple of 16.
This is the size to allocate for a buffer that writeTo will fill, and the size to declare as the push-constant range. The rounding means it is normally a little larger than the members add up to.
fun regFloatCount(): Int = schema.regFloatCount
How many floats the generated register file holds: the REG_COUNT the emitted GLSL declares.
One per scalar member and one per component of a vector member, counting only the members that get accessors; an underscore payload contributes nothing.
fun nextFreeOffset(alignment: Int = 4): Int
First free byte offset after all reflected fields, optionally aligned.
Use this to place extra manual declarations in otherDeclarations. Example: val offset = nextFreeOffset(16) otherDeclarations = "layout(offset = $offset) uint _manualFlags;"
fun nextFreeOffset(alignment: Int = 4): Int {
require(alignment > 0 && (alignment and (alignment - 1)) == 0) {
"Alignment must be a positive power of two, was $alignment"
}
val used = schema.fields.maxOfOrNull { it.pcOffsetBytes + it.pcSizeBytes } ?: 0
if (alignment == 1) return used
return ((used + alignment - 1) / alignment) * alignment
}
fun regIndexOf(name: String): Int
Where name starts in the generated register file: the value of the REG_<name> constant
the emitted GLSL declares.
Throws for an unknown member and for an underscore payload, since a payload is never loaded into the register file and so has no index there. Use pcOffsetOf for those.
fun regIndexOf(name: String): Int =
schema.regIndexByName[name]
?: error("No register-backed field named '$name'")
fun pcOffsetOf(name: String): Int
The byte offset of name within the push-constant block, which is the offset the generated
layout(offset = …) carries.
Answers for every member including underscore payloads, and throws for a name the class does not declare.
fun pcOffsetOf(name: String): Int =
schema.pcOffsetByName[name]
?: error("No field named '$name'")
operator fun get(name: String): Any?
Dynamic getter by field name.
operator fun get(name: String): Any? {
val f = schema.fields.firstOrNull { it.name == name }
?: error("No field named '$name'")
return f.property.get(this as Any)
}
operator fun set(name: String, value: Any?)
Dynamic setter by field name.
Requires underlying property to be declared as var.
operator fun set(name: String, value: Any?) {
val f = schema.fields.firstOrNull { it.name == name }
?: error("No field named '$name'")
if (value == null) {
error("Null not supported for push-constant field '$name'")
}
validateValueOrThrow(f, value)
@Suppress("UNCHECKED_CAST")
val mutable = f.property as? KMutableProperty1<Any, Any?>
?: error("Field '$name' must be declared as 'var' to support runtime mutation.")
mutable.set(this as Any, value)
}
override fun writeTo(b: ByteBuffer, baseOffset: Int)
Writes every member into b at baseOffset plus its own offset, leaving the buffer's position
where it found it.
Each member is written with absolute puts, so this is safe to call on a shared buffer. The
buffer has to be little-endian, and a ByteBuffer is big-endian until
order(ByteOrder.LITTLE_ENDIAN) is called on it — get that wrong and every value arrives
byte-reversed with no error anywhere.
override fun writeTo(b: ByteBuffer, baseOffset: Int) {
val oldPos = b.position()
for (f in schema.fields) {
val abs = baseOffset + f.pcOffsetBytes
val v = f.property.get(this as Any)
writeField(b, abs, f.kind, v)
}
b.position(oldPos)
}
fun glsl
Emits the GLSL side of this block: the push-constant declaration, a float register file, a
loadRegs() that fills it, and an accessor per member under the member's own Kotlin name.
The declaration gives every member an explicit layout(offset = …) and a leading underscore,
which keeps the block's own names clear of the accessors. A scalar member's accessor is a
#define, so time in Kotlin reads as time; a vector member's is a function of no
arguments, so baseColor reads as baseColor(). Half-precision members get unpack helpers
ahead of the block. Call loadRegs() once at the top of main before reading anything.
emitScalarLayoutQualifier adds the scalar layout qualifier to the block, which a shader
compiled with scalar block layout needs. regsName and pcInstanceName rename the register
array and the block instance, for a shader that already has something called regs or pc.
fun glsl(
emitScalarLayoutQualifier: Boolean = false,
regsName: String = "regs",
pcInstanceName: String = "pc"
): String = GlslGen.generate(
kClass = this::class,
schema = schema,
emitScalarLayoutQualifier = emitScalarLayoutQualifier,
regsName = regsName,
pcInstanceName = pcInstanceName,
otherDeclarations = otherDeclarations
)
Validation + writing
source — 77 lines, from line 315
private fun validateValueOrThrow(f: FieldInfo, value: Any) {
val ok = when (f.kind) {
PcKind.F1 -> value is Float
PcKind.V2 -> value is Vector2f
PcKind.V3 -> value is Vector3f
PcKind.V4 -> value is Vector4f
PcKind.H2 -> value is Vec2h
PcKind.H3 -> value is Vec3h
PcKind.H4 -> value is Vec4h
PcKind.H8 -> value is Vector8h
}
if (!ok) {
error(
"Type mismatch for '${f.name}'. " +
"Expected ${expectedTypeName(f.kind)}, got ${value::class.simpleName}"
)
}
}
private fun expectedTypeName(kind: PcKind): String = when (kind) {
PcKind.F1 -> "Float"
PcKind.V2 -> "Vector2f"
PcKind.V3 -> "Vector3f"
PcKind.V4 -> "Vector4f"
PcKind.H2 -> "Vec2h"
PcKind.H3 -> "Vec3h"
PcKind.H4 -> "Vec4h"
PcKind.H8 -> "Vector8h"
}
private fun writeField(bb: ByteBuffer, absOffset: Int, kind: PcKind, v: Any?) {
val oldPos = bb.position()
bb.position(absOffset)
when (kind) {
PcKind.F1 -> bb.putFloat(v as Float)
PcKind.V2 -> {
val t = v as Vector2f
bb.putFloat(t.x); bb.putFloat(t.y)
}
PcKind.V3 -> {
val t = v as Vector3f
bb.putFloat(t.x); bb.putFloat(t.y); bb.putFloat(t.z)
}
PcKind.V4 -> {
val t = v as Vector4f
bb.putFloat(t.x); bb.putFloat(t.y); bb.putFloat(t.z); bb.putFloat(t.w)
}
PcKind.H2 -> {
val t = (v as Vec2h).v
bb.putInt(packHalf2x16(t.x, t.y))
}
PcKind.H3 -> {
val t = (v as Vec3h).v
val xy = packHalf2x16(t.x, t.y)
val z0 = packHalf1x16(t.z)
bb.putInt(xy)
bb.putInt(z0)
}
PcKind.H4 -> {
val t = (v as Vec4h).v
val xy = packHalf2x16(t.x, t.y)
val zw = packHalf2x16(t.z, t.w)
bb.putInt(xy)
bb.putInt(zw)
}
// Vector8h stores fp16; its own writer emits the uvec4 bytes (two halfs per uint
// lane) with absolute puts, so it is independent of the buffer's position.
PcKind.H8 -> (v as Vector8h).get(absOffset, bb)
}
bb.position(oldPos)
}
}
Schema builder
source — 96 lines, from line 397
private fun buildSchema(k: KClass<*>): Schema {
// Deterministic ordering:
// 1) PcOrder if present
// 2) name
val props = k.memberProperties
.sortedWith(compareBy<KProperty1<out Any, *>>(
{ it.findAnnotation<PcOrder>()?.value ?: Int.MAX_VALUE },
{ it.name }
))
var pcOffset = 0
var regIndex = 0
val fields = mutableListOf<FieldInfo>()
val regMap = mutableMapOf<String, Int>()
val pcMap = mutableMapOf<String, Int>()
fun align(off: Int, a: Int): Int {
if (a <= 1) return off
val r = off % a
return if (r == 0) off else off + (a - r)
}
for (pAny in props) {
val name = pAny.name
// Skip framework / helper properties that are not part of the PC payload.
// Currently this is just otherDeclarations:String from the base class.
if (name == "otherDeclarations") continue
val kind = inferKind(pAny.returnType.classifier as? KClass<*>)
// Enforce mutability at schema build time for clearer errors.
val isMutable = (pAny as? KMutableProperty1<*, *>) != null
if (!isMutable) {
error("Push-constant field '$name' must be declared as 'var'.")
}
val (alignBytes, sizeBytes) = pcStorageLayout(kind)
pcOffset = align(pcOffset, alignBytes)
val excluded = name.startsWith("_")
val thisRegIndex =
if (excluded) null
else regIndex.also { regMap[name] = it }
pcMap[name] = pcOffset
@Suppress("UNCHECKED_CAST")
val p = pAny as KProperty1<Any, *>
fields += FieldInfo(
name = name,
kind = kind,
pcOffsetBytes = pcOffset,
pcSizeBytes = sizeBytes,
regIndex = thisRegIndex,
property = p
)
pcOffset += sizeBytes
if (!excluded) regIndex += kind.regFloats
}
// Round total bytes to 16 for convenience/safety.
fun align16(off: Int): Int {
val a = 16
val r = off % a
return if (r == 0) off else off + (a - r)
}
val total = align16(pcOffset)
return Schema(
fields = fields,
regFloatCount = regIndex,
pcTotalBytes = total,
regIndexByName = regMap,
pcOffsetByName = pcMap
)
}
private fun inferKind(k: KClass<*>?): PcKind = when (k) {
Float::class -> PcKind.F1
Vector2f::class -> PcKind.V2
Vector3f::class -> PcKind.V3
Vector4f::class -> PcKind.V4
Vec2h::class -> PcKind.H2
Vec3h::class -> PcKind.H3
Vec4h::class -> PcKind.H4
Vector8h::class -> PcKind.H8
else -> error("Unsupported push-constant field type: $k")
}
Storage layout for the GLSL types we emit in the push constant block.
We assume std430-ish alignment rules for: - float - vec2/3/4 - uint - uvec2
Half wrappers map to uint/uvec2 storage types in GLSL.
private fun pcStorageLayout(kind: PcKind): Pair<Int, Int> = when (kind) {
PcKind.F1 -> 4 to 4
PcKind.V2 -> 8 to 8
PcKind.V3 -> 16 to 12
PcKind.V4 -> 16 to 16
PcKind.H2 -> 4 to 4 // uint
PcKind.H3 -> 8 to 8 // uvec2
PcKind.H4 -> 8 to 8 // uvec2
PcKind.H8 -> 16 to 16 // uvec4
}
GLSL generator
object GlslGen
Builds the GLSL text for a push-constant block from a layout that reflection has already worked out.
Reached through PushConstants.glsl rather than directly, since the layout it needs is internal, so
there is no way to call it with a schema of your own. It is public because the generated text is
worth reading when a shader disagrees with its Kotlin: print pc.glsl() and compare the offsets.
object GlslGen {
internal fun generate
body — 225 lines, from line 531
internal fun generate(
kClass: KClass<*>,
schema: Schema,
emitScalarLayoutQualifier: Boolean,
regsName: String,
pcInstanceName: String,
otherDeclarations: String = ""
): String {
val blockTypeName = kClass.simpleName ?: "PC"
// Push-constant member naming to avoid macro collisions.
fun pcMemberName(fieldName: String): String =
if (fieldName.startsWith("_")) fieldName else "_$fieldName"
val sb = StringBuilder()
// sb.appendLine("#version 450")
// sb.appendLine()
// Half transit decode helpers
sb.appendLine("// ---- Half transit decode helpers ----")
sb.appendLine("vec2 half2(uint p) { return unpackHalf2x16(p); }")
sb.appendLine(
"vec3 half3(uvec2 p) { " +
"vec2 a = unpackHalf2x16(p.x); " +
"vec2 b = unpackHalf2x16(p.y); " +
"return vec3(a.x, a.y, b.x); " +
"}"
)
sb.appendLine(
"vec4 half4(uvec2 p) { " +
"vec2 a = unpackHalf2x16(p.x); " +
"vec2 b = unpackHalf2x16(p.y); " +
"return vec4(a, b); " +
"}"
)
sb.appendLine(
"void half8(uvec4 p, out vec4 lo, out vec4 hi) { " +
"lo = vec4(unpackHalf2x16(p.x), unpackHalf2x16(p.y)); " +
"hi = vec4(unpackHalf2x16(p.z), unpackHalf2x16(p.w)); " +
"}"
)
sb.appendLine()
// Push constant block header
val layoutHead = if (emitScalarLayoutQualifier)
"layout(scalar, push_constant)"
else
"layout(push_constant)"
sb.appendLine("$layoutHead uniform $blockTypeName {")
for (f in schema.fields) {
val glslType = glslStorageType(f.kind)
val m = pcMemberName(f.name)
sb.appendLine(" layout(offset = ${f.pcOffsetBytes}) $glslType $m;")
}
// Extra user-provided declarations, inserted inside the block.
val extra = otherDeclarations.trim()
if (extra.isNotEmpty()) {
extra.lineSequence().forEach { line ->
val l = line.trimEnd()
if (l.isNotBlank()) sb.appendLine(" $l")
else sb.appendLine()
}
}
sb.appendLine("} $pcInstanceName;")
sb.appendLine()
// Register indices
sb.appendLine("// ---- Register file layout ----")
for (f in schema.fields) {
if (f.regIndex != null) {
sb.appendLine("const uint REG_${f.name} = ${f.regIndex}u;")
}
}
sb.appendLine("const uint REG_COUNT = ${schema.regFloatCount}u;")
sb.appendLine("float $regsName[REG_COUNT];")
sb.appendLine()
// Loader
sb.appendLine("// ---- Load push constants into register file ----")
sb.appendLine("void loadRegs() {")
for (f in schema.fields) {
val n = f.name
val r = f.regIndex ?: continue
val m = pcMemberName(n)
when (f.kind) {
PcKind.F1 -> {
sb.appendLine(" $regsName[REG_$n] = $pcInstanceName.$m;")
}
PcKind.V2 -> {
sb.appendLine(" $regsName[REG_$n + 0] = $pcInstanceName.$m.x;")
sb.appendLine(" $regsName[REG_$n + 1] = $pcInstanceName.$m.y;")
}
PcKind.V3 -> {
sb.appendLine(" $regsName[REG_$n + 0] = $pcInstanceName.$m.x;")
sb.appendLine(" $regsName[REG_$n + 1] = $pcInstanceName.$m.y;")
sb.appendLine(" $regsName[REG_$n + 2] = $pcInstanceName.$m.z;")
}
PcKind.V4 -> {
sb.appendLine(" $regsName[REG_$n + 0] = $pcInstanceName.$m.x;")
sb.appendLine(" $regsName[REG_$n + 1] = $pcInstanceName.$m.y;")
sb.appendLine(" $regsName[REG_$n + 2] = $pcInstanceName.$m.z;")
sb.appendLine(" $regsName[REG_$n + 3] = $pcInstanceName.$m.w;")
}
PcKind.H2 -> {
sb.appendLine(" vec2 _${n}_v = half2($pcInstanceName.$m);")
sb.appendLine(" $regsName[REG_$n + 0] = _${n}_v.x;")
sb.appendLine(" $regsName[REG_$n + 1] = _${n}_v.y;")
}
PcKind.H3 -> {
sb.appendLine(" vec3 _${n}_v = half3($pcInstanceName.$m);")
sb.appendLine(" $regsName[REG_$n + 0] = _${n}_v.x;")
sb.appendLine(" $regsName[REG_$n + 1] = _${n}_v.y;")
sb.appendLine(" $regsName[REG_$n + 2] = _${n}_v.z;")
}
PcKind.H4 -> {
sb.appendLine(" vec4 _${n}_v = half4($pcInstanceName.$m);")
sb.appendLine(" $regsName[REG_$n + 0] = _${n}_v.x;")
sb.appendLine(" $regsName[REG_$n + 1] = _${n}_v.y;")
sb.appendLine(" $regsName[REG_$n + 2] = _${n}_v.z;")
sb.appendLine(" $regsName[REG_$n + 3] = _${n}_v.w;")
}
PcKind.H8 -> {
sb.appendLine(" vec4 _${n}_lo;")
sb.appendLine(" vec4 _${n}_hi;")
sb.appendLine(" half8($pcInstanceName.$m, _${n}_lo, _${n}_hi);")
sb.appendLine(" $regsName[REG_$n + 0] = _${n}_lo.x;")
sb.appendLine(" $regsName[REG_$n + 1] = _${n}_lo.y;")
sb.appendLine(" $regsName[REG_$n + 2] = _${n}_lo.z;")
sb.appendLine(" $regsName[REG_$n + 3] = _${n}_lo.w;")
sb.appendLine(" $regsName[REG_$n + 4] = _${n}_hi.x;")
sb.appendLine(" $regsName[REG_$n + 5] = _${n}_hi.y;")
sb.appendLine(" $regsName[REG_$n + 6] = _${n}_hi.z;")
sb.appendLine(" $regsName[REG_$n + 7] = _${n}_hi.w;")
}
}
}
sb.appendLine("}")
sb.appendLine()
// Accessors for non-underscore fields
sb.appendLine("// ---- Typed accessors over the register file ----")
for (f in schema.fields) {
if (f.regIndex == null) continue
val n = f.name
when (f.kind) {
PcKind.F1 -> {
// macro below
}
PcKind.V2, PcKind.H2 -> {
sb.appendLine("vec2 $n() {")
sb.appendLine(" return vec2($regsName[REG_$n + 0], $regsName[REG_$n + 1]);")
sb.appendLine("}")
}
PcKind.V3, PcKind.H3 -> {
sb.appendLine("vec3 $n() {")
sb.appendLine(" return vec3(")
sb.appendLine(" $regsName[REG_$n + 0],")
sb.appendLine(" $regsName[REG_$n + 1],")
sb.appendLine(" $regsName[REG_$n + 2]")
sb.appendLine(" );")
sb.appendLine("}")
}
PcKind.V4, PcKind.H4 -> {
sb.appendLine("vec4 $n() {")
sb.appendLine(" return vec4(")
sb.appendLine(" $regsName[REG_$n + 0],")
sb.appendLine(" $regsName[REG_$n + 1],")
sb.appendLine(" $regsName[REG_$n + 2],")
sb.appendLine(" $regsName[REG_$n + 3]")
sb.appendLine(" );")
sb.appendLine("}")
}
PcKind.H8 -> {
sb.appendLine("vec4 ${n}Lo() {")
sb.appendLine(" return vec4(")
sb.appendLine(" $regsName[REG_$n + 0],")
sb.appendLine(" $regsName[REG_$n + 1],")
sb.appendLine(" $regsName[REG_$n + 2],")
sb.appendLine(" $regsName[REG_$n + 3]")
sb.appendLine(" );")
sb.appendLine("}")
sb.appendLine("vec4 ${n}Hi() {")
sb.appendLine(" return vec4(")
sb.appendLine(" $regsName[REG_$n + 4],")
sb.appendLine(" $regsName[REG_$n + 5],")
sb.appendLine(" $regsName[REG_$n + 6],")
sb.appendLine(" $regsName[REG_$n + 7]")
sb.appendLine(" );")
sb.appendLine("}")
}
}
sb.appendLine()
}
// Scalar macros (safe due to PC member underscore prefix).
sb.appendLine("// ---- Scalar macros ----")
for (f in schema.fields) {
if (f.regIndex == null) continue
if (f.kind == PcKind.F1) {
sb.appendLine("#define ${f.name} ($regsName[REG_${f.name}])")
}
}
sb.appendLine("//#<line>")
return sb.toString()
}
private fun glslStorageType(kind: PcKind): String = when (kind) {
PcKind.F1 -> "float"
PcKind.V2 -> "vec2"
PcKind.V3 -> "vec3"
PcKind.V4 -> "vec4"
PcKind.H2 -> "uint"
PcKind.H3 -> "uvec2"
PcKind.H4 -> "uvec2"
PcKind.H8 -> "uvec4"
}
}
Smoke test
class SmokePC : PushConstants<SmokePC>()
A worked example of a push-constant class, covering each supported member type and both payload conventions.
Five ordered members that get accessors, then two underscore payloads given high PcOrder numbers
so later members can be added in front of them. Nothing outside this file uses it; what reads it is
the main below, which prints the layout, the generated GLSL and a word dump of the written bytes.
class SmokePC : PushConstants<SmokePC>() {
@PcOrder(0) var time: Float = 0f
@PcOrder(1) var baseColor: Vector4f = Vector4f()
@PcOrder(2) var roughMetal: Vec2h = Vector2f().half
@PcOrder(3) var uvScale: Vector2f = Vector2f(1f, 1f)
@PcOrder(4) var emissive: Vector3f = Vector3f()
@PcOrder(100) var _patch0: Vec4h = Vector4f().half
instruction payloads (excluded from regs)
@PcOrder(101) var _patch1: Vec4h = Vector4f().half
private fun dumpU32(bb: ByteBuffer, bytes: Int) {
val words = bytes / 4
println("Raw u32 dump ($words words):")
for (i in 0 until words) {
val w = bb.getInt(i * 4).toUInt()
val hex = w.toString(16).padStart(8, '0')
println(" [$i] 0x$hex")
}
}
fun main()
Prints SmokePC's layout, its generated GLSL and a word dump of the bytes it writes.
Nothing in Field calls this; it is here to be run on its own when a block's offsets need checking against a shader, which it does without a device, a window or a renderer.
body — 41 lines, from line 798
fun main() {
val pc = SmokePC().apply {
time = 1.25f
baseColor = Vector4f(1f, 0.5f, 0.25f, 1f)
roughMetal = Vector2f(0.4f, 0.9f).half
uvScale = Vector2f(2f, 3f)
emissive = Vector3f(0.1f, 0.2f, 0.3f)
// "instruction words as floats" for smoke only.
_patch0 = Vector4f(12f, 1f, 13f, 1f).half
_patch1 = Vector4f(0f, 0f, 0f, 0f).half
// Example extra declaration using nextFreeOffset:
val extraOffset = nextFreeOffset(16)
otherDeclarations = "layout(offset = $extraOffset) uint _extraDebug;"
}
// Demonstrate dynamic mutation hitting real fields.
pc["time"] = 2.0f
pc["uvScale"] = Vector2f(4f, 5f)
println("---- Introspection ----")
println("pcSizeBytes = ${pc.pcSizeBytes()}")
println("regFloatCount = ${pc.regFloatCount()}")
listOf("time", "baseColor", "roughMetal", "uvScale", "emissive").forEach { n ->
println("$n: pcOffset=${pc.pcOffsetOf(n)} bytes, regIndex=${pc.regIndexOf(n)}")
}
println()
println("---- Generated GLSL ----")
println(pc.glsl())
val bytes = pc.pcSizeBytes()
val bb = ByteBuffer.allocateDirect(bytes)
.order(java.nio.ByteOrder.LITTLE_ENDIAN)
pc.writeTo(bb, 0)
println("---- ByteBuffer dump ----")
dumpU32(bb, bytes)
}