Fixes and Text

This commit is contained in:
lily 2026-07-10 17:36:51 -04:00
parent e6ca5fce3b
commit abf67dd663
10 changed files with 171 additions and 37 deletions

View file

@ -24,9 +24,13 @@ dependencies {
implementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" implementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}"
implementation "com.mojang:brigadier:1.2.9" implementation "com.mojang:brigadier:1.2.9"
// NanoVG is not part of Minecraft's LWJGL modules, so bundle it (and its
// natives) into the mod jar via include so it is present at runtime.
implementation "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}" implementation "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}"
include "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}"
['linux', 'linux-arm64', 'windows', 'macos', 'macos-arm64'].each { platform -> ['linux', 'linux-arm64', 'windows', 'macos', 'macos-arm64'].each { platform ->
runtimeOnly "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}:natives-${platform}" runtimeOnly "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}:natives-${platform}"
include "org.lwjgl:lwjgl-nanovg:${project.nanovg_version}:natives-${platform}"
} }
} }
@ -63,3 +67,17 @@ publishing {
mavenLocal() mavenLocal()
} }
} }
// Copies the built lattice and example mod jars into a Minecraft mods folder.
// Override the target with -PmodsDir=/path/to/mods (defaults to the Prism instance).
def defaultModsDir = "${System.getProperty('user.home')}/.local/share/PrismLauncher/instances/Lattice testing/minecraft/mods"
tasks.register('deployMods', Copy) {
group = 'lattice'
description = 'Copies the lattice and example mod jars into a Minecraft mods folder.'
dependsOn tasks.named('jar'), project(':example').tasks.named('jar')
from tasks.named('jar').flatMap { it.archiveFile }
from project(':example').tasks.named('jar').flatMap { it.archiveFile }
into providers.gradleProperty('modsDir').orElse(defaultModsDir)
}

View file

@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx4G org.gradle.jvmargs=-Xmx10G
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.configuration-cache=false org.gradle.configuration-cache=false
loom.ignoreDependencyLoomVersionValidation=true loom.ignoreDependencyLoomVersionValidation=true

View file

@ -1,7 +1,5 @@
package xyz.meowing.lattice package xyz.meowing.lattice
import net.minecraft.resources.Identifier
import xyz.meowing.lattice.client.Client.minecraft
import xyz.meowing.lattice.event.EventBus import xyz.meowing.lattice.event.EventBus
import xyz.meowing.lattice.render.Font import xyz.meowing.lattice.render.Font
import xyz.meowing.lattice.render.NVGRenderer import xyz.meowing.lattice.render.NVGRenderer
@ -14,10 +12,7 @@ object Lattice {
val eventBus = EventBus() val eventBus = EventBus()
@JvmStatic @JvmStatic
val defaultFont: Font by lazy { val defaultFont: Font by lazy { Font("Default", "/assets/$MOD_ID/font.ttf") }
val id = Identifier.fromNamespaceAndPath(MOD_ID, "font.ttf")
Font("Default", minecraft.resourceManager.getResourceStack(id).first().open())
}
private var _renderer: Renderer? = null private var _renderer: Renderer? = null

View file

@ -19,7 +19,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder.literal
* The tree must always end in an [Executable][xyz.meowing.lattice.command.nodes.Executable], * The tree must always end in an [Executable][xyz.meowing.lattice.command.nodes.Executable],
* which serves as an exit point for a command. * which serves as an exit point for a command.
*/ */
open class Commodore(private val root: LiteralNode) { open class Commodore(@PublishedApi internal val root: LiteralNode) {
constructor( constructor(
vararg name: String, vararg name: String,
@ -35,10 +35,18 @@ open class Commodore(private val root: LiteralNode) {
/** /**
* DSL access to the root node for object-style definitions. * DSL access to the root node for object-style definitions.
*/ */
fun runs(function: Function<Unit>) = root.runs(function)
fun runs(block: () -> Unit) = root.runs(block) fun runs(block: () -> Unit) = root.runs(block)
inline fun <reified P1> runs(noinline block: (P1) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2> runs(noinline block: (P1, P2) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2, reified P3> runs(noinline block: (P1, P2, P3) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2, reified P3, reified P4> runs(noinline block: (P1, P2, P3, P4) -> Unit) = root.runs(block)
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5> runs(noinline block: (P1, P2, P3, P4, P5) -> Unit) = root.runs(block)
fun literal(string: String, block: LiteralNode.() -> Unit = {}): LiteralNode { fun literal(string: String, block: LiteralNode.() -> Unit = {}): LiteralNode {
return root.literal(string, block) return root.literal(string, block)
} }

View file

@ -2,6 +2,7 @@ package xyz.meowing.lattice.command.functions
import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles import java.lang.invoke.MethodHandles
import java.lang.reflect.ParameterizedType
/** /**
* # FunctionInvoker * # FunctionInvoker
@ -59,7 +60,21 @@ sealed interface FunctionInvoker<T> {
mHandle = MethodHandles.lookup().unreflect(invokeMethod).bindTo(lambda) mHandle = MethodHandles.lookup().unreflect(invokeMethod).bindTo(lambda)
if (parameterTypes.isEmpty()) { if (parameterTypes.isEmpty()) {
parameters = invokeMethod.parameterTypes.mapIndexed { index, type -> val rawTypes = invokeMethod.parameterTypes
// Kotlin K2/IR may compile lambdas so the only non-bridge invoke method
// has erased Object types. Recover concrete types from the FunctionN
// generic interface that the lambda class still declares.
val actualTypes = if (rawTypes.isNotEmpty() && rawTypes.all { it == Any::class.java }) {
recoverFromFunctionInterface(lambdaClass, rawTypes.size)
?: throw IllegalStateException(
"Cannot recover parameter types for lambda ${lambdaClass.name}. " +
"Interfaces: ${lambdaClass.genericInterfaces.toList()}. " +
"Use a typed runs { p: Type -> } overload instead of Function<Unit>."
)
} else {
rawTypes
}
parameters = actualTypes.mapIndexed { index, type ->
Parameter("param$index", type, false) Parameter("param$index", type, false)
} }
} else { } else {
@ -76,6 +91,27 @@ sealed interface FunctionInvoker<T> {
override fun invoke(arguments: List<Any?>): T { override fun invoke(arguments: List<Any?>): T {
return mHandle.invokeWithArguments(arguments) as T return mHandle.invokeWithArguments(arguments) as T
} }
private companion object {
fun recoverFromFunctionInterface(lambdaClass: Class<*>, paramCount: Int): Array<Class<*>>? {
for (iface in lambdaClass.genericInterfaces) {
val pt = iface as? ParameterizedType ?: continue
val raw = pt.rawType as? Class<*> ?: continue
if (!raw.name.startsWith("kotlin.jvm.functions.Function")) continue
val args = pt.actualTypeArguments
// FunctionN<P1,...,Pn,R> — last arg is return type
if (args.size != paramCount + 1) continue
return Array(paramCount) { i ->
when (val arg = args[i]) {
is Class<*> -> arg
is ParameterizedType -> arg.rawType as? Class<*> ?: Any::class.java
else -> Any::class.java
}
}
}
return null
}
}
} }
companion object { companion object {

View file

@ -72,8 +72,31 @@ class Executable : Node() {
* - Use a [ParameterModifier] to apply a custom parser to a certain parameter. * - Use a [ParameterModifier] to apply a custom parser to a certain parameter.
* You need to ensure you're parsing for the correct class. * You need to ensure you're parsing for the correct class.
*/ */
fun runs(function: Function<Unit>) { fun runs(block: () -> Unit) = define(block)
funInvoker = FunctionInvoker.from(function)
inline fun <reified P1> runs(noinline block: (P1) -> Unit) =
define(block, P1::class.java)
inline fun <reified P1, reified P2> runs(noinline block: (P1, P2) -> Unit) =
define(block, P1::class.java, P2::class.java)
inline fun <reified P1, reified P2, reified P3> runs(noinline block: (P1, P2, P3) -> Unit) =
define(block, P1::class.java, P2::class.java, P3::class.java)
inline fun <reified P1, reified P2, reified P3, reified P4> runs(noinline block: (P1, P2, P3, P4) -> Unit) =
define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java)
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5> runs(noinline block: (P1, P2, P3, P4, P5) -> Unit) =
define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java, P5::class.java)
/**
* Low-level entry point that builds the invoker from a function and its
* explicit parameter types. Prefer the typed [runs] overloads, which capture
* the types via reified generics instead of relying on runtime reflection.
*/
@PublishedApi
internal fun define(function: Function<Unit>, vararg explicitTypes: Class<*>) {
funInvoker = FunctionInvoker.from(function, *explicitTypes)
for (parameter in funInvoker.parameters) { for (parameter in funInvoker.parameters) {
val modifier = parameterModifiers[parameter.name] val modifier = parameterModifiers[parameter.name]

View file

@ -94,15 +94,29 @@ open class LiteralNode(val name: String) : Node() {
} }
/** /**
* Creates an [Executable] and apply function directly. * Creates an [Executable] that runs [block] with typed command arguments.
* *
* This won't allow you to apply [modifiers][Executable.ParameterModifier] * Each parameter type is captured via reified generics, so it works with
* to the function's parameters. * Kotlin's invokedynamic lambdas where the types are erased at runtime.
*
* @see runs
*/ */
fun runs(function: Function<Unit>) = executable { inline fun <reified P1> runs(noinline block: (P1) -> Unit) = executable {
runs(function) define(block, P1::class.java)
}
inline fun <reified P1, reified P2> runs(noinline block: (P1, P2) -> Unit) = executable {
define(block, P1::class.java, P2::class.java)
}
inline fun <reified P1, reified P2, reified P3> runs(noinline block: (P1, P2, P3) -> Unit) = executable {
define(block, P1::class.java, P2::class.java, P3::class.java)
}
inline fun <reified P1, reified P2, reified P3, reified P4> runs(noinline block: (P1, P2, P3, P4) -> Unit) = executable {
define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5> runs(noinline block: (P1, P2, P3, P4, P5) -> Unit) = executable {
define(block, P1::class.java, P2::class.java, P3::class.java, P4::class.java, P5::class.java)
} }
/** /**

View file

@ -11,6 +11,12 @@ object GLState {
@JvmStatic @JvmStatic
var previousProgram = -1 var previousProgram = -1
@JvmStatic
var previousSampler = -1
@JvmStatic
var previousVao = -1
@JvmStatic @JvmStatic
var drawing = false var drawing = false
} }

View file

@ -2,6 +2,7 @@ package xyz.meowing.lattice.render
import com.mojang.blaze3d.opengl.GlStateManager import com.mojang.blaze3d.opengl.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem import com.mojang.blaze3d.systems.RenderSystem
import org.apache.logging.log4j.LogManager
import org.lwjgl.nanovg.NVGColor import org.lwjgl.nanovg.NVGColor
import org.lwjgl.nanovg.NVGPaint import org.lwjgl.nanovg.NVGPaint
import org.lwjgl.nanovg.NanoSVG import org.lwjgl.nanovg.NanoSVG
@ -11,6 +12,7 @@ import org.lwjgl.opengl.GL11
import org.lwjgl.opengl.GL13 import org.lwjgl.opengl.GL13
import org.lwjgl.opengl.GL20 import org.lwjgl.opengl.GL20
import org.lwjgl.opengl.GL30 import org.lwjgl.opengl.GL30
import org.lwjgl.opengl.GL33C
import org.lwjgl.stb.STBImage import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryUtil import org.lwjgl.system.MemoryUtil
import xyz.meowing.lattice.client.Client.minecraft import xyz.meowing.lattice.client.Client.minecraft
@ -18,7 +20,6 @@ import xyz.meowing.lattice.render.Color.Companion.alpha
import xyz.meowing.lattice.render.Color.Companion.blue import xyz.meowing.lattice.render.Color.Companion.blue
import xyz.meowing.lattice.render.Color.Companion.green import xyz.meowing.lattice.render.Color.Companion.green
import xyz.meowing.lattice.render.Color.Companion.red import xyz.meowing.lattice.render.Color.Companion.red
import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.math.max import kotlin.math.max
@ -48,23 +49,30 @@ object NVGRenderer : Renderer {
// Resolved reflectively once: GlTexture.getFbo(GlStateManager$DirectStateAccess, depth) is not public API. // Resolved reflectively once: GlTexture.getFbo(GlStateManager$DirectStateAccess, depth) is not public API.
private var fboLookup: Pair<Any, Method>? = null private var fboLookup: Pair<Any, Method>? = null
private var fboLookupFailed = false private var fboLookupFailed = false
private var loggedFbo = false
private fun mainFramebuffer(colorTex: Any): Int { private fun fieldValue(obj: Any, name: String): Any? {
var clazz: Class<*>? = obj.javaClass
while (clazz != null) {
try {
return clazz.getDeclaredField(name).apply { isAccessible = true }.get(obj)
} catch (_: NoSuchFieldException) {
clazz = clazz.superclass
}
}
return null
}
private fun mainFramebuffer(colorTex: Any, depthTex: Any?): Int {
if (fboLookupFailed) return 0 if (fboLookupFailed) return 0
try { try {
val lookup = fboLookup ?: run { val lookup = fboLookup ?: run {
val device = RenderSystem.getDevice() ?: return 0 val device = RenderSystem.getDevice() ?: return 0
var dsaField: Field? = null // GpuDevice wraps the GL backend (GlDevice), which owns directStateAccess.
var clazz: Class<*>? = device.javaClass val backend = fieldValue(device, "backend") ?: device
while (clazz != null && dsaField == null) { val dsa = fieldValue(backend, "directStateAccess")
try { ?: throw IllegalStateException("no directStateAccess")
dsaField = clazz.getDeclaredField("directStateAccess")
} catch (_: NoSuchFieldException) {}
clazz = clazz.superclass
}
dsaField?.isAccessible = true
val dsa = dsaField?.get(device) ?: throw IllegalStateException("no directStateAccess")
var getFbo: Method? = null var getFbo: Method? = null
var texClazz: Class<*>? = colorTex.javaClass var texClazz: Class<*>? = colorTex.javaClass
@ -78,9 +86,12 @@ object NVGRenderer : Renderer {
Pair(dsa, getFbo).also { fboLookup = it } Pair(dsa, getFbo).also { fboLookup = it }
} }
return (lookup.second.invoke(colorTex, lookup.first, null) as? Int) ?: 0 // Pass the depth texture so getFbo returns the FBO with the depth+stencil
} catch (_: Exception) { // attachment; NanoVG's stencil fills render nothing on a color-only FBO.
return (lookup.second.invoke(colorTex, lookup.first, depthTex) as? Int) ?: 0
} catch (e: Throwable) {
fboLookupFailed = true fboLookupFailed = true
LogManager.getLogger("Lattice").warn("FBO lookup failed: device=${RenderSystem.getDevice()?.javaClass?.name} color=${colorTex.javaClass.name} depth=${depthTex?.javaClass?.name}", e)
return 0 return 0
} }
} }
@ -95,10 +106,23 @@ object NVGRenderer : Renderer {
val colorTex = renderTarget.getColorTexture() ?: return val colorTex = renderTarget.getColorTexture() ?: return
if (vg == -1L) return if (vg == -1L) return
GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, mainFramebuffer(colorTex)) val fbo = mainFramebuffer(colorTex, renderTarget.getDepthTexture())
if (!loggedFbo) {
loggedFbo = true
LogManager.getLogger("Lattice").info("NVG beginFrame: fbo=$fbo failed=$fboLookupFailed size=${width}x$height target=${renderTarget.width}x${renderTarget.height}")
}
GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo)
GlStateManager._viewport(0, 0, renderTarget.width, renderTarget.height) GlStateManager._viewport(0, 0, renderTarget.width, renderTarget.height)
GlStateManager._activeTexture(GL30.GL_TEXTURE0) GlStateManager._activeTexture(GL30.GL_TEXTURE0)
// NanoVG binds its own VAO without restoring the previous one.
GLState.previousVao = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING)
// Unbind MC's sampler from unit 0 so NanoVG's font atlas texture samples
// with its own parameters; otherwise glyphs render blank while shapes are fine.
GLState.previousSampler = GL11.glGetInteger(GL33C.GL_SAMPLER_BINDING)
GL33C.glBindSampler(0, 0)
NanoVG.nvgBeginFrame(vg, width, height, 1f) NanoVG.nvgBeginFrame(vg, width, height, 1f)
NanoVG.nvgTextAlign(vg, NanoVG.NVG_ALIGN_LEFT or NanoVG.NVG_ALIGN_TOP) NanoVG.nvgTextAlign(vg, NanoVG.NVG_ALIGN_LEFT or NanoVG.NVG_ALIGN_TOP)
GLState.drawing = true GLState.drawing = true
@ -114,13 +138,23 @@ object NVGRenderer : Renderer {
GlStateManager._enableBlend() GlStateManager._enableBlend()
GlStateManager._blendFuncSeparate(770, 771, 1, 0) GlStateManager._blendFuncSeparate(770, 771, 1, 0)
// NanoVG's stencil strokes can leave the stencil test on and the color mask off.
GL11.glDisable(GL11.GL_STENCIL_TEST)
GL11.glColorMask(true, true, true, true)
if (GLState.previousProgram != -1) GlStateManager._glUseProgram(GLState.previousProgram) if (GLState.previousProgram != -1) GlStateManager._glUseProgram(GLState.previousProgram)
// Restore the sampler on unit 0 that we unbound for the font atlas.
GlStateManager._activeTexture(GL30.GL_TEXTURE0)
if (GLState.previousSampler != -1) GL33C.glBindSampler(0, GLState.previousSampler)
if (GLState.previousActiveTexture != -1) { if (GLState.previousActiveTexture != -1) {
GlStateManager._activeTexture(GLState.previousActiveTexture) GlStateManager._activeTexture(GLState.previousActiveTexture)
if (GLState.previousBoundTexture != -1) GlStateManager._bindTexture(GLState.previousBoundTexture) if (GLState.previousBoundTexture != -1) GlStateManager._bindTexture(GLState.previousBoundTexture)
} }
if (GLState.previousVao != -1) GL30.glBindVertexArray(GLState.previousVao)
GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0) GlStateManager._glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0)
GLState.drawing = false GLState.drawing = false
} }

View file

@ -95,10 +95,10 @@ abstract class UIScreen(screenName: String = "Lattice-Screen") : Screen(Componen
super.onClose() super.onClose()
} }
/** Opens this screen on the next client tick, safe to call from any thread. */ /** Opens this screen on the render thread, safe to call from any thread. */
fun display() { fun display() {
TimeScheduler.schedule(50) { TimeScheduler.schedule(50) {
minecraft.setScreen(this@UIScreen) minecraft.execute { minecraft.setScreen(this@UIScreen) }
} }
} }
} }