GlslCompletionSupport.kt
src/jvmMain/kotlin/afield/runtime/completion/GlslCompletionSupport.kt · 1074 lines
package and imports — 42 lines, from line 1
package afield.runtime.completion
import afield.app.protocol.Output
import afield.data.table.div
import afield.data.table.get
import afield.data.table.minus
import afield.data.table.plus
import afield.data.table.rem
import afield.data.table.times
import afield.document.graph.preamble
import afield.runtime.execution.plus
import afield.runtime.lifecycle.plus
import afield.runtime.lifecycle.rem
import afield.ui.notation.bool
import afield.ui.notation.double
import afield.ai.conversation.images
import afield.app.protocol.Completion
import afield.app.protocol.CompletionRequest
import afield.app.protocol.Metadata
import afield.app.protocol.SymbolContextRequest
import afield.app.protocol.SymbolContextResponse
import afield.audio.synth.*
import afield.document.graph.Box
import afield.document.graph.box
import afield.document.workspace.metadata
import afield.graphics.vulkan.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.lifecycle.end
import afield.runtime.lifecycle.map
import afield.ui.output.templates.filter
import afield.ai.compat.glslSpecForProperty
import afield.graphics.shader.Introspection
import afield.graphics.shader.Introspected
import afield.graphics.shader.PrimitiveType
import afield.graphics.shader.ShaderKind
import afield.graphics.vulkan.util.listGlobalShaderTemplateNames
import afield.graphics.shader._computeShader
import afield.graphics.shader._meshShader
import afield.graphics.shader._rayTracingShader
import afield.graphics.shader._shader
Completion and hover context for GLSL shader properties (fragment, vertex, compute and the
rest), including Field's {% ... %} and {{ ... }} shader template syntax.
GlslCompletionSupport.complete builds an inventory of the symbols visible in the shader (built-ins,
declarations and struct members from the box's own source and its shader template) and offers root
names, receiver.member completions, or template tags and expressions depending on the caret.
GlslCompletionSupport.describeContext answers the matching symbol-context request.
A token's span in the shader source.
private data class GlslBounds(val start: Int, val end: Int, val text: String)
private data class GlslMemberInfo(
val name: String,
val type: String,
val tail: String = "",
val documentation: String = ""
)
private data class GlslSymbolInfo(
val name: String,
val type: String,
val kind: String,
val tail: String,
val documentation: String = "",
val members: List<GlslMemberInfo> = emptyList()
)
private data class GlslCompletionInventory(
val symbols: List<GlslSymbolInfo>,
val structures: Map<String, List<GlslMemberInfo>>,
val introspectionCount: Int
)
private data class GlslReceiverAccess(
val receiver: String,
val prefix: String,
val bounds: GlslBounds
)
object GlslCompletionSupport
fun isGlslProperty(property: String): Boolean
fun isGlslProperty(property: String): Boolean =
glslSpecForProperty(property) != null
fun complete(box: Box, request: CompletionRequest): Completion
fun complete(box: Box, request: CompletionRequest): Completion {
val code = request.allCode
val position = request.position.coerceIn(0, code.length)
val templateContext = templateContextAt(code, position)
val tokenBounds = findIdentifierBoundsAt(code, position) ?: GlslBounds(position, position, "")
val inventory = buildInventory(box, request.property, code)
val activePrefix = findReceiverAccess(code, tokenBounds)?.prefix ?: tokenBounds.text
val candidates = when (templateContext) {
GlslTemplateContext.TAG -> templateTagCompletions()
GlslTemplateContext.EXPRESSION -> {
val receiver = findReceiverAccess(code, tokenBounds)
if (receiver?.receiver == "shadertoy") templateShadertoyMemberCompletions()
else templateExpressionCompletions(inventory)
}
null -> {
val receiver = findReceiverAccess(code, tokenBounds)
if (receiver != null) {
memberCompletions(receiver, inventory)
} else {
rootCompletions(request.property, inventory)
}
}
}
val metadata = filterCompletionCandidates(candidates, activePrefix)
.distinctBy { it.text }
.sortedWith(compareBy<Metadata>({ completionSortGroup(it) }, { it.displayText.lowercase() }))
.take(360)
val bounds = findReceiverAccess(code, tokenBounds)?.bounds ?: tokenBounds
return Completion(
boundsStart = bounds.start,
boundsEnd = bounds.end,
metadata = metadata
)
}
fun describeContext(box: Box, request: SymbolContextRequest): SymbolContextResponse
body — 947 lines, from line 127
fun describeContext(box: Box, request: SymbolContextRequest): SymbolContextResponse {
val code = request.allCode
val position = request.position.coerceIn(0, code.length)
val inventory = buildInventory(box, request.property, code)
val templateContext = templateContextAt(code, position)
if (templateContext != null) {
val tag = if (templateContext == GlslTemplateContext.TAG) "{% ... %}" else "{{ ... }}"
val detail = if (templateContext == GlslTemplateContext.TAG) {
"Field shader template tag: if / else / endif, include, copyPC"
} else {
"Field shader interpolation: resolves against shaderTemplate and chained box context"
}
return SymbolContextResponse(
symbol = tag,
html = glslContextHtml(
heading = "Field GLSL template",
signature = tag,
detail = detail
)
)
}
val bounds = findIdentifierBoundsAt(code, position)
if (bounds == null || bounds.text.isBlank()) {
return stageSummaryResponse(request.property, inventory)
}
findReceiverAccess(code, bounds)?.let { receiver ->
findMember(receiver, inventory)?.let { member ->
return SymbolContextResponse(
symbol = member.name,
html = glslContextHtml(
heading = "GLSL member",
signature = "${member.type} ${receiver.receiver}.${member.name}",
detail = member.tail.ifBlank { member.documentation }
)
)
}
}
val symbol = inventory.symbols.firstOrNull { it.name == bounds.text }
?: builtinSymbolsForProperty(request.property).firstOrNull { it.name == bounds.text }
if (symbol != null) {
return SymbolContextResponse(
symbol = symbol.name,
html = glslContextHtml(
heading = symbol.kind,
signature = "${symbol.type} ${symbol.name}".trim(),
detail = symbol.tail.ifBlank { symbol.documentation },
members = symbol.members
)
)
}
return stageSummaryResponse(request.property, inventory)
}
private fun completionSortGroup(item: Metadata): Int = when {
item.tail.startsWith("runtime") || item.tail.contains("push constant") || item.tail.contains("binding") -> 0
item.tail.startsWith("local") -> 1
item.tail.startsWith("Field") -> 2
item.icon == "keyword" -> 4
else -> 3
}
}
private fun filterCompletionCandidates(candidates: List<Metadata>, prefix: String): List<Metadata> {
val normalized = prefix.trim()
if (normalized.isBlank()) return candidates
val prefixMatches = candidates.filter {
it.text.startsWith(normalized, ignoreCase = true) ||
it.displayText.startsWith(normalized, ignoreCase = true)
}
if (prefixMatches.isNotEmpty()) return prefixMatches
val containsMatches = candidates.filter {
it.text.contains(normalized, ignoreCase = true) ||
it.displayText.contains(normalized, ignoreCase = true)
}
return containsMatches.ifEmpty { candidates }
}
private enum class GlslTemplateContext {
EXPRESSION,
TAG
}
private fun templateContextAt(code: String, position: Int): GlslTemplateContext? {
val safe = position.coerceIn(0, code.length)
val exprOpen = code.lastIndexOf("{{", safe)
val exprClose = code.lastIndexOf("}}", safe)
if (exprOpen >= 0 && exprOpen > exprClose) return GlslTemplateContext.EXPRESSION
val tagOpen = code.lastIndexOf("{%", safe)
val tagClose = code.lastIndexOf("%}", safe)
if (tagOpen >= 0 && tagOpen > tagClose) return GlslTemplateContext.TAG
return null
}
private fun findIdentifierBoundsAt(code: String, position: Int): GlslBounds? {
if (code.isEmpty()) return null
val safe = position.coerceIn(0, code.length)
fun isIdent(ch: Char) = ch == '_' || ch.isLetterOrDigit()
val probe = when {
safe < code.length && isIdent(code[safe]) -> safe
safe > 0 && isIdent(code[safe - 1]) -> safe - 1
else -> return null
}
var start = probe
while (start > 0 && isIdent(code[start - 1])) start--
var end = probe + 1
while (end < code.length && isIdent(code[end])) end++
return GlslBounds(start, end, code.substring(start, end))
}
private fun findReceiverAccess(code: String, tokenBounds: GlslBounds): GlslReceiverAccess? {
var dot = tokenBounds.start - 1
while (dot >= 0 && code[dot].isWhitespace()) dot--
if (dot < 0 || code[dot] != '.') return null
var receiverEnd = dot
while (receiverEnd > 0 && code[receiverEnd - 1].isWhitespace()) receiverEnd--
var receiverStart = receiverEnd
while (receiverStart > 0) {
val ch = code[receiverStart - 1]
if (ch != '_' && !ch.isLetterOrDigit()) break
receiverStart--
}
if (receiverStart >= receiverEnd) return null
return GlslReceiverAccess(
receiver = code.substring(receiverStart, receiverEnd),
prefix = tokenBounds.text,
bounds = tokenBounds
)
}
private fun buildInventory(box: Box, property: String, code: String): GlslCompletionInventory {
val symbols = linkedMapOf<String, GlslSymbolInfo>()
val structures = linkedMapOf<String, List<GlslMemberInfo>>()
var introspectionCount = 0
fun addSymbol(symbol: GlslSymbolInfo) {
symbols.putIfAbsent(symbol.name, symbol)
if (symbol.members.isNotEmpty()) {
structures.putIfAbsent(symbol.name, symbol.members)
structures.putIfAbsent(symbol.type, symbol.members)
}
}
for ((stage, introspection) in currentIntrospections(box, property)) {
if (!introspection.valid) continue
introspectionCount++
addIntrospectionSymbols(stage, introspection, ::addSymbol, structures)
}
for (symbol in parseLocalGlslSymbols(code)) {
addSymbol(symbol)
}
return GlslCompletionInventory(
symbols = symbols.values.toList(),
structures = structures,
introspectionCount = introspectionCount
)
}
private fun currentIntrospections(box: Box, property: String): List<Pair<String, Introspection>> {
val out = mutableListOf<Pair<String, Introspection>>()
when (property.lowercase()) {
"vertex" -> box._shader?.current?.vi?.let { out += "vertex" to it }
"fragment" -> {
box._shader?.current?.fi?.let { out += "fragment" to it }
box._meshShader?.current?.fi?.let { out += "fragment" to it }
}
"compute" -> box._computeShader?.current?.ci?.let { out += "compute" to it }
"task" -> box._meshShader?.current?.ti?.let { out += "task" to it }
"mesh" -> box._meshShader?.current?.mi?.let { out += "mesh" to it }
"raygen" -> addRayStage(box, ShaderKind.RAYGEN_SHADER, out)
"miss" -> addRayStage(box, ShaderKind.MISS_SHADER, out)
"closesthit" -> addRayStage(box, ShaderKind.CLOSEST_HIT_SHADER, out)
"anyhit" -> addRayStage(box, ShaderKind.ANY_HIT_SHADER, out)
"intersection" -> addRayStage(box, ShaderKind.INTERSECTION_SHADER, out)
"callable" -> addRayStage(box, ShaderKind.CALLABLE_SHADER, out)
}
return out.distinctBy { System.identityHashCode(it.second) }
}
private fun addRayStage(box: Box, kind: ShaderKind, out: MutableList<Pair<String, Introspection>>) {
box._rayTracingShader?.current?.stageRecords
?.filter { it.stage.kind == kind }
?.forEach { out += it.stage.name to it.introspection }
}
private fun addIntrospectionSymbols(
stage: String,
introspection: Introspection,
addSymbol: (GlslSymbolInfo) -> Unit,
structures: MutableMap<String, List<GlslMemberInfo>>
) {
introspection.inputs.forEach { input ->
addSymbol(
GlslSymbolInfo(
name = input.name,
type = glslTypeName(input.type),
kind = "GLSL input",
tail = "runtime $stage in location ${input.location}",
documentation = "Input reflected from the latest successful shader introspection."
)
)
}
introspection.outputs.forEach { output ->
addSymbol(
GlslSymbolInfo(
name = output.name,
type = glslTypeName(output.type),
kind = "GLSL output",
tail = "runtime $stage out location ${output.location}",
documentation = "Output reflected from the latest successful shader introspection."
)
)
}
introspection.pushConstants.forEach { push ->
val members = push.members.toGlslMembers("push constant")
addSymbol(
GlslSymbolInfo(
name = push.name,
type = "push_constant",
kind = "GLSL push constant",
tail = "runtime $stage push constant ${push.size} bytes",
members = members
)
)
members.forEach { member ->
addSymbol(
GlslSymbolInfo(
name = member.name,
type = member.type,
kind = "GLSL push constant member",
tail = "runtime $stage push constant",
documentation = member.documentation
)
)
}
}
introspection.ubos.forEach { ubo ->
val members = ubo.members.toGlslMembers("uniform block")
addSymbol(
GlslSymbolInfo(
name = ubo.name,
type = "uniform",
kind = "GLSL uniform block",
tail = "runtime set ${ubo.set} binding ${ubo.binding}, ${ubo.size} bytes",
members = members
)
)
}
introspection.ssbo.forEach { ssbo ->
val members = ssbo.members.toGlslMembers("storage block")
addSymbol(
GlslSymbolInfo(
name = ssbo.name,
type = "buffer",
kind = "GLSL storage block",
tail = "runtime set ${ssbo.set} binding ${ssbo.binding}, ${ssbo.size} bytes",
members = members
)
)
}
introspection.textures.forEach { texture ->
addSymbol(
GlslSymbolInfo(
name = texture.name,
type = texture.type,
kind = "GLSL texture",
tail = "runtime set ${texture.set} binding ${texture.binding}" +
if (texture.arrayCount > 0) ", array ${texture.arrayCount}" else ""
)
)
}
introspection.images.forEach { image ->
addSymbol(
GlslSymbolInfo(
name = image.name,
type = image.type,
kind = "GLSL image",
tail = "runtime set ${image.set} binding ${image.binding}"
)
)
}
introspection.accelerationStructures.forEach { accel ->
addSymbol(
GlslSymbolInfo(
name = accel.name,
type = accel.type,
kind = "GLSL acceleration structure",
tail = "runtime set ${accel.set} binding ${accel.binding}"
)
)
}
introspection.structureTypes.forEach { structure ->
val members = structure.members.toGlslMembers("struct")
structures.putIfAbsent(structure.name, members)
addSymbol(
GlslSymbolInfo(
name = structure.name,
type = if (structure.bufferReference) "buffer_reference" else "struct",
kind = "GLSL structure",
tail = "runtime ${structure.size} bytes",
members = members
)
)
}
}
private fun List<Introspected.Member>.toGlslMembers(source: String): List<GlslMemberInfo> =
map { member ->
GlslMemberInfo(
name = member.name,
type = glslTypeName(member.type),
tail = source,
documentation = buildString {
append(source)
append(", offset ")
append(member.offset)
if (member.arrayStride > 0) append(", array stride ").append(member.arrayStride)
}
)
}
private fun glslTypeName(type: arrow.core.Either<PrimitiveType, List<Introspected.Member>>): String =
type.fold(::primitiveTypeName) { "struct" }
private fun primitiveTypeName(type: PrimitiveType): String = when (type) {
PrimitiveType.float -> "float"
PrimitiveType.vec2 -> "vec2"
PrimitiveType.vec3 -> "vec3"
PrimitiveType.vec4 -> "vec4"
PrimitiveType.int -> "int"
PrimitiveType.uint -> "uint"
PrimitiveType.mat2 -> "mat2"
PrimitiveType.mat3 -> "mat3"
PrimitiveType.mat4 -> "mat4"
PrimitiveType.float16_t -> "float16_t"
PrimitiveType.f16vec2 -> "f16vec2"
PrimitiveType.f16vec3 -> "f16vec3"
PrimitiveType.f16vec4 -> "f16vec4"
PrimitiveType.uint64_t -> "uint64_t"
PrimitiveType.int64_t -> "int64_t"
PrimitiveType.deviceptr -> "uint64_t"
}
private val glslBlockCommentRegex = Regex("""/\*.*?\*/""", RegexOption.DOT_MATCHES_ALL)
private val glslLineCommentRegex = Regex("""//.*?$""", setOf(RegexOption.MULTILINE))
private val fieldTemplateIslandRegex = Regex("""\{\{.*?}}|\{%.*?%}""", RegexOption.DOT_MATCHES_ALL)
private val localStructRegex = Regex(
"""\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{(.*?)\}\s*([A-Za-z_][A-Za-z0-9_]*)?\s*;""",
setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE)
)
private val localBlockRegex = Regex(
"""(?:layout\s*\([^)]*\)\s*)?\b(uniform|buffer)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{(.*?)\}\s*([A-Za-z_][A-Za-z0-9_]*)?\s*;""",
setOf(RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE)
)
private val localFieldRegex = Regex(
"""(?:layout\s*\([^)]*\)\s*)?(?:(?:readonly|writeonly|coherent|volatile|restrict|const|in|out|inout|flat|smooth|centroid|sample|patch|highp|mediump|lowp)\s+)*([A-Za-z_][A-Za-z0-9_]*(?:\s*<[^;>]+>)?)\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*\[[^]]*])?\s*$"""
)
private val localVariableRegex = Regex(
"""(?m)(?:^|[;{(,])\s*(?:layout\s*\([^)]*\)\s*)?(?:(?:const|in|out|inout|uniform|buffer|shared|flat|smooth|centroid|sample|patch|readonly|writeonly|coherent|volatile|restrict|highp|mediump|lowp)\s+)*([A-Za-z_][A-Za-z0-9_]*(?:\s*<[^;>()]+>)?)\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*\[[^]]*])?\s*(?=[=;,)])"""
)
private val localFunctionRegex = Regex(
"""(?m)(?:^|[;}])\s*(?:(?:const|precise|highp|mediump|lowp)\s+)*([A-Za-z_][A-Za-z0-9_]*(?:\s*<[^;>()]+>)?)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^;{}()]*)\)\s*(?=[{;])"""
)
private fun parseLocalGlslSymbols(code: String): List<GlslSymbolInfo> {
val clean = code
.replace(glslBlockCommentRegex, "")
.replace(glslLineCommentRegex, "")
.replace(fieldTemplateIslandRegex, " ")
val symbols = linkedMapOf<String, GlslSymbolInfo>()
val structures = linkedMapOf<String, List<GlslMemberInfo>>()
fun parseMembers(body: String, source: String): List<GlslMemberInfo> =
body.split(";").mapNotNull { raw ->
val line = raw.trim().takeIf { it.isNotBlank() } ?: return@mapNotNull null
val match = localFieldRegex.matchEntire(line) ?: return@mapNotNull null
GlslMemberInfo(
name = match.groupValues[2],
type = match.groupValues[1].trim(),
tail = source,
documentation = source
)
}
localStructRegex.findAll(clean).forEach { match ->
val typeName = match.groupValues[1]
val members = parseMembers(match.groupValues[2], "local struct")
structures[typeName] = members
symbols[typeName] = GlslSymbolInfo(
name = typeName,
type = "struct",
kind = "GLSL structure",
tail = "local struct",
members = members
)
val instance = match.groupValues.getOrNull(3).orEmpty()
if (instance.isNotBlank()) {
symbols[instance] = GlslSymbolInfo(
name = instance,
type = typeName,
kind = "GLSL variable",
tail = "local struct instance",
members = members
)
}
}
localBlockRegex.findAll(clean).forEach { match ->
val blockKind = match.groupValues[1]
val blockName = match.groupValues[2]
val members = parseMembers(match.groupValues[3], "local $blockKind block")
val instance = match.groupValues.getOrNull(4).orEmpty().ifBlank { blockName }
structures[blockName] = members
symbols[instance] = GlslSymbolInfo(
name = instance,
type = blockName,
kind = "GLSL $blockKind block",
tail = "local $blockKind block",
members = members
)
}
localFunctionRegex.findAll(clean).forEach { match ->
val returnType = compactGlslWhitespace(match.groupValues[1])
val name = match.groupValues[2].trim()
val parameters = compactGlslWhitespace(match.groupValues[3])
if (returnType in glslControlWords || name in glslControlWords || name == "main") return@forEach
symbols.putIfAbsent(
name,
GlslSymbolInfo(
name = name,
type = returnType,
kind = "GLSL function",
tail = "local function(${glslParameterTypeSummary(parameters)})",
documentation = "$returnType $name($parameters)"
)
)
}
localVariableRegex.findAll(clean).forEach { match ->
val type = match.groupValues[1].trim()
val name = match.groupValues[2].trim()
if (name in glslControlWords || name == "main") return@forEach
symbols.putIfAbsent(
name,
GlslSymbolInfo(
name = name,
type = type,
kind = "GLSL variable",
tail = "local $type",
members = structures[type].orEmpty()
)
)
}
return symbols.values.toList()
}
private fun compactGlslWhitespace(value: String): String =
value.trim().replace(Regex("""\s+"""), " ")
private fun glslParameterTypeSummary(parameters: String): String {
if (parameters.isBlank()) return ""
val qualifiers = setOf(
"const", "in", "out", "inout", "highp", "mediump", "lowp", "precise",
"readonly", "writeonly", "coherent", "volatile", "restrict"
)
return parameters
.split(",")
.map { parameter ->
parameter
.replace(Regex("""layout\s*\([^)]*\)"""), " ")
.replace(Regex("""\[[^]]*]"""), "[]")
.trim()
.split(Regex("""\s+"""))
.filter { it.isNotBlank() && it !in qualifiers }
.let { words ->
when {
words.isEmpty() -> ""
words.size == 1 -> words.single()
else -> words.dropLast(1).joinToString(" ")
}
}
}
.filter { it.isNotBlank() }
.joinToString(", ")
}
private fun rootCompletions(property: String, inventory: GlslCompletionInventory): List<Metadata> =
inventory.symbols.map { it.toMetadata() } +
builtinSymbolsForProperty(property).map { it.toMetadata() } +
glslSnippetCompletions(property)
private fun memberCompletions(receiver: GlslReceiverAccess, inventory: GlslCompletionInventory): List<Metadata> {
val members = membersForReceiver(receiver.receiver, inventory)
if (members.isEmpty() && vectorLike(receiver.receiver)) return swizzleCompletions(receiver.receiver)
return members.map {
Metadata(
icon = "property",
tail = it.tail,
text = it.name,
displayText = it.name,
documentation = glslDocumentation("GLSL member", "${it.type} ${it.name}", it.documentation.ifBlank { it.tail })
)
} + swizzleCompletions(receiver.receiver).takeIf { receiver.receiver in setOf("vec2", "vec3", "vec4") }.orEmpty()
}
private fun findMember(receiver: GlslReceiverAccess, inventory: GlslCompletionInventory): GlslMemberInfo? =
membersForReceiver(receiver.receiver, inventory).firstOrNull { it.name == receiver.prefix }
private fun membersForReceiver(receiver: String, inventory: GlslCompletionInventory): List<GlslMemberInfo> {
val symbol = inventory.symbols.firstOrNull { it.name == receiver }
if (symbol != null) {
if (symbol.members.isNotEmpty()) return symbol.members
inventory.structures[symbol.type]?.let { return it }
}
inventory.structures[receiver]?.let { return it }
val type = symbol?.type ?: receiver
return when (type) {
"vec2", "ivec2", "uvec2", "bvec2" -> vectorMembers(2)
"vec3", "ivec3", "uvec3", "bvec3" -> vectorMembers(3)
"vec4", "ivec4", "uvec4", "bvec4" -> vectorMembers(4)
else -> emptyList()
}
}
private fun vectorLike(receiver: String): Boolean =
receiver in setOf("vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "uvec2", "uvec3", "uvec4")
private fun vectorMembers(size: Int): List<GlslMemberInfo> {
val names = listOf("x", "y", "z", "w").take(size) + listOf("r", "g", "b", "a").take(size)
return names.map { GlslMemberInfo(it, "float", "vector component", "Vector component alias.") }
}
private fun swizzleCompletions(receiver: String): List<Metadata> {
val size = when {
receiver.endsWith("2") -> 2
receiver.endsWith("3") -> 3
else -> 4
}
val axes = listOf("x", "y", "z", "w").take(size)
val common = axes + listOf(
axes.take(2).joinToString(""),
axes.take(3).joinToString(""),
axes.take(4).joinToString(""),
axes.reversed().take(size.coerceAtMost(3)).joinToString("")
).filter { it.isNotBlank() }
return common.distinct().map {
Metadata(
icon = "property",
tail = "swizzle",
text = it,
displayText = it,
documentation = glslDocumentation("GLSL swizzle", it, "Vector swizzle.")
)
}
}
private fun GlslSymbolInfo.toMetadata(): Metadata =
Metadata(
icon = when {
kind.contains("function", ignoreCase = true) -> "method"
kind.contains("structure", ignoreCase = true) -> "class"
kind.contains("keyword", ignoreCase = true) -> "keyword"
kind.contains("type", ignoreCase = true) -> "class"
else -> "property"
},
tail = tail,
text = name,
displayText = name,
documentation = glslDocumentation(kind, "${type} ${name}".trim(), documentation.ifBlank { tail }, members)
)
private val glslControlWords = setOf(
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", "return", "discard"
)
private val glslKeywordSymbols = (
glslControlWords +
setOf(
"layout", "uniform", "buffer", "push_constant", "in", "out", "inout", "const", "shared",
"flat", "smooth", "centroid", "sample", "patch", "readonly", "writeonly", "coherent",
"volatile", "restrict", "precision", "highp", "mediump", "lowp", "struct", "precise",
"nonuniformEXT", "pervertexEXT", "perprimitiveEXT", "taskPayloadSharedEXT", "rayPayloadEXT",
"rayPayloadInEXT", "hitAttributeEXT", "callableDataEXT", "callableDataInEXT"
)
).map {
GlslSymbolInfo(it, "", "GLSL keyword", "keyword")
}
private val glslTypeSymbols = listOf(
"void", "bool", "int", "uint", "float", "double",
"vec2", "vec3", "vec4", "ivec2", "ivec3", "ivec4", "uvec2", "uvec3", "uvec4", "bvec2", "bvec3", "bvec4",
"dvec2", "dvec3", "dvec4",
"mat2", "mat3", "mat4", "mat2x2", "mat2x3", "mat2x4", "mat3x2", "mat3x3", "mat3x4",
"mat4x2", "mat4x3", "mat4x4", "dmat2", "dmat3", "dmat4", "dmat2x2", "dmat2x3", "dmat2x4",
"dmat3x2", "dmat3x3", "dmat3x4", "dmat4x2", "dmat4x3", "dmat4x4",
"float16_t", "f16vec2", "f16vec3", "f16vec4", "int8_t", "i8vec2", "i8vec3", "i8vec4",
"uint8_t", "u8vec2", "u8vec3", "u8vec4", "int16_t", "i16vec2", "i16vec3", "i16vec4",
"uint16_t", "u16vec2", "u16vec3", "u16vec4", "int64_t", "i64vec2", "i64vec3", "i64vec4",
"uint64_t", "u64vec2", "u64vec3", "u64vec4",
"sampler1D", "sampler1DArray", "sampler1DArrayShadow", "sampler1DShadow",
"sampler2D", "sampler2DArray", "sampler2DArrayShadow", "sampler2DMS", "sampler2DMSArray",
"sampler2DRect", "sampler2DRectShadow", "sampler2DShadow", "sampler3D", "samplerBuffer",
"samplerCube", "samplerCubeArray", "samplerCubeArrayShadow", "samplerCubeShadow",
"isampler1D", "isampler1DArray", "isampler2D", "isampler2DArray", "isampler2DMS",
"isampler2DMSArray", "isampler2DRect", "isampler3D", "isamplerBuffer", "isamplerCube",
"isamplerCubeArray", "usampler1D", "usampler1DArray", "usampler2D", "usampler2DArray",
"usampler2DMS", "usampler2DMSArray", "usampler2DRect", "usampler3D", "usamplerBuffer",
"usamplerCube", "usamplerCubeArray",
"texture1D", "texture1DArray", "texture2D", "texture2DArray", "texture2DMS", "texture2DMSArray",
"texture2DRect", "texture3D", "textureBuffer", "textureCube", "textureCubeArray",
"itexture1D", "itexture1DArray", "itexture2D", "itexture2DArray", "itexture2DMS",
"itexture2DMSArray", "itexture2DRect", "itexture3D", "itextureBuffer", "itextureCube",
"itextureCubeArray", "utexture1D", "utexture1DArray", "utexture2D", "utexture2DArray",
"utexture2DMS", "utexture2DMSArray", "utexture2DRect", "utexture3D", "utextureBuffer",
"utextureCube", "utextureCubeArray",
"image1D", "image1DArray", "image2D", "image2DArray", "image2DMS", "image2DMSArray",
"image2DRect", "image3D", "imageBuffer", "imageCube", "imageCubeArray",
"iimage1D", "iimage1DArray", "iimage2D", "iimage2DArray", "iimage2DMS", "iimage2DMSArray",
"iimage2DRect", "iimage3D", "iimageBuffer", "iimageCube", "iimageCubeArray",
"uimage1D", "uimage1DArray", "uimage2D", "uimage2DArray", "uimage2DMS", "uimage2DMSArray",
"uimage2DRect", "uimage3D", "uimageBuffer", "uimageCube", "uimageCubeArray",
"subpassInput", "subpassInputMS", "isubpassInput", "isubpassInputMS", "usubpassInput", "usubpassInputMS",
"sampler", "samplerShadow", "atomic_uint", "accelerationStructureEXT", "rayQueryEXT"
).map {
GlslSymbolInfo(it, "type", "GLSL type", "type")
}
private val glslFunctionSymbols = listOf(
"abs", "sign", "floor", "ceil", "fract", "mod", "min", "max", "clamp", "mix", "step", "smoothstep",
"sin", "cos", "tan", "asin", "acos", "atan", "pow", "exp", "log", "sqrt", "inversesqrt",
"length", "distance", "dot", "cross", "normalize", "reflect", "refract", "faceforward",
"radians", "degrees", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
"isnan", "isinf", "fma", "frexp", "ldexp",
"matrixCompMult", "outerProduct", "transpose", "determinant", "inverse",
"lessThan", "lessThanEqual", "greaterThan", "greaterThanEqual", "equal", "notEqual", "any", "all", "not",
"uaddCarry", "usubBorrow", "umulExtended", "imulExtended", "bitfieldExtract", "bitfieldInsert",
"bitfieldReverse", "bitCount", "findLSB", "findMSB", "floatBitsToInt", "floatBitsToUint",
"intBitsToFloat", "uintBitsToFloat", "packSnorm2x16", "unpackSnorm2x16", "packUnorm2x16",
"unpackUnorm2x16", "packHalf2x16", "unpackHalf2x16", "packDouble2x32", "unpackDouble2x32",
"texture", "textureProj", "textureLod", "textureOffset", "texelFetch", "texelFetchOffset",
"textureProjOffset", "textureLodOffset", "textureProjLod", "textureProjLodOffset",
"textureGrad", "textureGradOffset", "textureProjGrad", "textureProjGradOffset",
"textureGather", "textureGatherOffset", "textureGatherOffsets", "textureSize",
"textureQueryLod", "textureQueryLevels", "textureSamples", "textureQuerySamples",
"dFdx", "dFdy", "fwidth", "dFdxFine", "dFdyFine", "fwidthFine", "dFdxCoarse", "dFdyCoarse", "fwidthCoarse",
"imageLoad", "imageStore", "imageSize", "imageSamples", "imageAtomicAdd", "imageAtomicMin",
"imageAtomicMax", "imageAtomicAnd", "imageAtomicOr", "imageAtomicXor", "imageAtomicExchange",
"imageAtomicCompSwap",
"atomicAdd", "atomicMin", "atomicMax", "atomicAnd", "atomicOr", "atomicXor", "atomicExchange",
"atomicCompSwap", "atomicCounter", "atomicCounterIncrement", "atomicCounterDecrement",
"barrier", "memoryBarrier", "memoryBarrierAtomicCounter", "memoryBarrierBuffer", "memoryBarrierImage",
"memoryBarrierShared", "groupMemoryBarrier", "subgroupBarrier", "subgroupMemoryBarrier",
"subgroupMemoryBarrierBuffer", "subgroupMemoryBarrierShared", "subgroupMemoryBarrierImage",
"subgroupElect", "subgroupAll", "subgroupAny", "subgroupAllEqual", "subgroupBroadcast",
"subgroupBroadcastFirst", "subgroupBallot", "subgroupInverseBallot", "subgroupBallotBitExtract",
"subgroupBallotBitCount", "subgroupBallotInclusiveBitCount", "subgroupBallotExclusiveBitCount",
"subgroupBallotFindLSB", "subgroupBallotFindMSB", "subgroupShuffle", "subgroupShuffleXor",
"subgroupShuffleUp", "subgroupShuffleDown", "subgroupAdd", "subgroupMul", "subgroupMin",
"subgroupMax", "subgroupAnd", "subgroupOr", "subgroupXor", "subgroupInclusiveAdd",
"subgroupInclusiveMul", "subgroupInclusiveMin", "subgroupInclusiveMax", "subgroupInclusiveAnd",
"subgroupInclusiveOr", "subgroupInclusiveXor", "subgroupExclusiveAdd", "subgroupExclusiveMul",
"subgroupExclusiveMin", "subgroupExclusiveMax", "subgroupExclusiveAnd", "subgroupExclusiveOr",
"subgroupExclusiveXor",
"EmitMeshTasksEXT", "SetMeshOutputsEXT", "traceRayEXT", "reportIntersectionEXT",
"ignoreIntersectionEXT", "terminateRayEXT", "executeCallableEXT", "rayQueryInitializeEXT",
"rayQueryProceedEXT", "rayQueryTerminateEXT", "rayQueryGenerateIntersectionEXT",
"rayQueryConfirmIntersectionEXT", "rayQueryGetIntersectionTypeEXT", "rayQueryGetRayTMinEXT",
"rayQueryGetRayFlagsEXT", "rayQueryGetWorldRayOriginEXT", "rayQueryGetWorldRayDirectionEXT",
"rayQueryGetIntersectionTEXT", "rayQueryGetIntersectionInstanceCustomIndexEXT",
"rayQueryGetIntersectionInstanceIdEXT", "rayQueryGetIntersectionInstanceShaderBindingTableRecordOffsetEXT",
"rayQueryGetIntersectionGeometryIndexEXT", "rayQueryGetIntersectionPrimitiveIndexEXT",
"rayQueryGetIntersectionBarycentricsEXT", "rayQueryGetIntersectionFrontFaceEXT",
"rayQueryGetIntersectionCandidateAABBOpaqueEXT", "rayQueryGetIntersectionObjectRayOriginEXT",
"rayQueryGetIntersectionObjectRayDirectionEXT", "rayQueryGetIntersectionObjectToWorldEXT",
"rayQueryGetIntersectionWorldToObjectEXT"
).map {
GlslSymbolInfo(it, "fn", "GLSL function", "builtin function")
}
private fun builtinSymbolsForProperty(property: String): List<GlslSymbolInfo> {
val stageBuiltins = when (property.lowercase()) {
"vertex" -> listOf(
GlslSymbolInfo("gl_Position", "vec4", "GLSL vertex builtin", "builtin output"),
GlslSymbolInfo("gl_PointSize", "float", "GLSL vertex builtin", "builtin output"),
GlslSymbolInfo("gl_VertexIndex", "int", "GLSL vertex builtin", "builtin input"),
GlslSymbolInfo("gl_InstanceIndex", "int", "GLSL vertex builtin", "builtin input"),
GlslSymbolInfo("gl_DrawID", "int", "GLSL vertex builtin", "builtin input"),
GlslSymbolInfo("gl_BaseVertex", "int", "GLSL vertex builtin", "builtin input"),
GlslSymbolInfo("gl_BaseInstance", "int", "GLSL vertex builtin", "builtin input")
)
"fragment" -> listOf(
GlslSymbolInfo("gl_FragCoord", "vec4", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_FrontFacing", "bool", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_PointCoord", "vec2", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_FragDepth", "float", "GLSL fragment builtin", "builtin output"),
GlslSymbolInfo("gl_HelperInvocation", "bool", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_SampleID", "int", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_SamplePosition", "vec2", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_SampleMask", "array", "GLSL fragment builtin", "builtin output"),
GlslSymbolInfo("gl_SampleMaskIn", "array", "GLSL fragment builtin", "builtin input"),
GlslSymbolInfo("gl_PrimitiveID", "int", "GLSL fragment builtin", "builtin input")
)
"compute" -> listOf(
GlslSymbolInfo("gl_GlobalInvocationID", "uvec3", "GLSL compute builtin", "builtin input"),
GlslSymbolInfo("gl_LocalInvocationID", "uvec3", "GLSL compute builtin", "builtin input"),
GlslSymbolInfo("gl_WorkGroupID", "uvec3", "GLSL compute builtin", "builtin input"),
GlslSymbolInfo("gl_NumWorkGroups", "uvec3", "GLSL compute builtin", "builtin input"),
GlslSymbolInfo("gl_WorkGroupSize", "uvec3", "GLSL compute builtin", "builtin input"),
GlslSymbolInfo("gl_LocalInvocationIndex", "uint", "GLSL compute builtin", "builtin input")
)
"mesh" -> listOf(
GlslSymbolInfo("gl_WorkGroupID", "uvec3", "GLSL mesh builtin", "builtin input"),
GlslSymbolInfo("gl_LocalInvocationID", "uvec3", "GLSL mesh builtin", "builtin input"),
GlslSymbolInfo("gl_LocalInvocationIndex", "uint", "GLSL mesh builtin", "builtin input"),
GlslSymbolInfo("gl_MeshVerticesEXT", "array", "GLSL mesh builtin", "builtin output"),
GlslSymbolInfo("gl_PrimitiveTriangleIndicesEXT", "array", "GLSL mesh builtin", "builtin output"),
GlslSymbolInfo("gl_PrimitiveLineIndicesEXT", "array", "GLSL mesh builtin", "builtin output"),
GlslSymbolInfo("gl_PrimitivePointIndicesEXT", "array", "GLSL mesh builtin", "builtin output"),
GlslSymbolInfo("gl_PrimitiveCountEXT", "uint", "GLSL mesh builtin", "builtin output"),
GlslSymbolInfo("SetMeshOutputsEXT", "fn", "GLSL mesh function", "builtin function")
)
"task" -> listOf(
GlslSymbolInfo("EmitMeshTasksEXT", "fn", "GLSL task function", "builtin function"),
GlslSymbolInfo("gl_WorkGroupID", "uvec3", "GLSL task builtin", "builtin input"),
GlslSymbolInfo("gl_LocalInvocationID", "uvec3", "GLSL task builtin", "builtin input"),
GlslSymbolInfo("gl_LocalInvocationIndex", "uint", "GLSL task builtin", "builtin input")
)
"raygen", "miss", "closesthit", "anyhit", "intersection", "callable" -> listOf(
GlslSymbolInfo("gl_LaunchIDEXT", "uvec3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_LaunchSizeEXT", "uvec3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_WorldRayOriginEXT", "vec3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_WorldRayDirectionEXT", "vec3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_ObjectRayOriginEXT", "vec3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_ObjectRayDirectionEXT", "vec3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_RayTminEXT", "float", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_RayTmaxEXT", "float", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_InstanceCustomIndexEXT", "int", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_InstanceID", "int", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_PrimitiveID", "int", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_GeometryIndexEXT", "int", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_HitKindEXT", "int", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_ObjectToWorldEXT", "mat4x3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("gl_WorldToObjectEXT", "mat4x3", "GLSL ray tracing builtin", "builtin input"),
GlslSymbolInfo("traceRayEXT", "fn", "GLSL ray tracing function", "builtin function")
)
else -> emptyList()
}
return stageBuiltins + glslKeywordSymbols + glslTypeSymbols + glslFunctionSymbols
}
private fun glslSnippetCompletions(property: String): List<Metadata> {
val stage = property.lowercase()
val snippets = mutableListOf(
Metadata(
icon = "keyword",
tail = "Field template",
text = "{% copyPC %}",
displayText = "{% copyPC %}",
documentation = "Copies the previous stage push-constant declaration into this shader."
),
Metadata(
icon = "keyword",
tail = "Field template",
text = "{{ shadertoy.${if (stage == "vertex") "vertexPreamble" else "fragmentPreamble"} }}",
displayText = "shadertoy preamble",
documentation = "Field shader-template interpolation for the Shadertoy helper preamble."
),
Metadata(
icon = "keyword",
tail = "layout",
text = "layout(location = 0) in vec3 position;",
displayText = "layout(location) in",
documentation = "Common GLSL interface input declaration."
),
Metadata(
icon = "keyword",
tail = "layout",
text = "layout(set = 0, binding = 0) uniform sampler2D sourceTex;",
displayText = "layout(set,binding) sampler2D",
documentation = "Common Field/Vulkan texture binding declaration."
)
)
if (stage == "compute") {
snippets += Metadata(
icon = "keyword",
tail = "compute",
text = "layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;",
displayText = "layout(local_size) in",
documentation = "Compute workgroup-size declaration."
)
}
return snippets
}
private fun templateTagCompletions(): List<Metadata> =
listOf(
"if " to "Field template conditional",
"else" to "Field template conditional branch",
"endif" to "Field template conditional end",
"include \"\"" to "Field template include",
"copyPC" to "Copy previous stage push constants"
).map { (text, doc) ->
Metadata(
icon = "keyword",
tail = "Field template",
text = text,
displayText = text,
documentation = doc
)
}
private fun templateExpressionCompletions(inventory: GlslCompletionInventory): List<Metadata> =
listOf(
Metadata("property", "Field template", "shadertoy.vertexPreamble", "shadertoy.vertexPreamble", "Shadertoy vertex helper preamble."),
Metadata("property", "Field template", "shadertoy.fragmentPreamble", "shadertoy.fragmentPreamble", "Shadertoy fragment helper preamble.")
) + listGlobalShaderTemplateNames().map {
Metadata("property", "Field template", it, it, "Global shader template.")
} + inventory.symbols.map { it.toMetadata() }
private fun templateShadertoyMemberCompletions(): List<Metadata> =
listOf(
Metadata("property", "Field template", "vertexPreamble", "vertexPreamble", "Shadertoy vertex helper preamble."),
Metadata("property", "Field template", "fragmentPreamble", "fragmentPreamble", "Shadertoy fragment helper preamble.")
)
private fun stageSummaryResponse(property: String, inventory: GlslCompletionInventory): SymbolContextResponse {
if (inventory.symbols.isEmpty()) return SymbolContextResponse()
val runtime = inventory.symbols.count { it.tail.startsWith("runtime") }
val locals = inventory.symbols.count { it.tail.startsWith("local") }
val detail = buildString {
append(runtime)
append(" reflected")
if (locals > 0) append(", ").append(locals).append(" local")
if (inventory.introspectionCount == 0) append(" · no compiled introspection yet")
}
return SymbolContextResponse(
symbol = property,
html = glslContextHtml(
heading = "GLSL ${property.lowercase()}",
signature = "${inventory.symbols.size} known symbols",
detail = detail
)
)
}
private fun glslContextHtml(
heading: String,
signature: String,
detail: String = "",
members: List<GlslMemberInfo> = emptyList()
): String {
val memberHtml = if (members.isEmpty()) "" else {
val rows = members.take(24).joinToString("") {
"<div><code>${escapeGlslHtml(it.type)} ${escapeGlslHtml(it.name)}</code> <span style='color:#777;'>${escapeGlslHtml(it.tail)}</span></div>"
}
"<details class='afield-symbol-context-disclosure' style='margin-top:2px;'>" +
"<summary tabindex='-1' onfocus='this.blur()' style='color:#777; cursor:pointer; user-select:none; outline:none; box-shadow:none;'>" +
"${members.size} member${if (members.size == 1) "" else "s"}</summary>" +
"<div class='afield-symbol-context-disclosure-body' style='margin-top:4px; overflow:auto;'>$rows</div>" +
"</details>"
}
return buildString {
append("<div class='afield-glsl-context' style='display:inline-block; max-width: 620px;'>")
append("<strong>")
append(escapeGlslHtml(heading))
append("</strong> <code>")
append(escapeGlslHtml(signature))
append("</code>")
if (detail.isNotBlank()) {
append("<div style='color:#777;'>")
append(escapeGlslHtml(detail))
append("</div>")
}
append(memberHtml)
append("</div>")
}
}
private fun glslDocumentation(
heading: String,
signature: String,
detail: String = "",
members: List<GlslMemberInfo> = emptyList()
): String = buildString {
append("**")
append(heading)
append("**\n\n")
if (signature.isNotBlank()) {
append("```glsl\n")
append(signature)
append("\n```\n")
}
if (detail.isNotBlank()) {
append("\n")
append(detail)
append("\n")
}
if (members.isNotEmpty()) {
append("\n**Members**\n")
members.take(24).forEach {
append("- `")
append(it.type)
append(" ")
append(it.name)
append("`")
if (it.tail.isNotBlank()) append(" - ").append(it.tail)
append("\n")
}
}
}
private fun escapeGlslHtml(value: String): String =
value
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")