diff --git a/adapters/mc-neoforge-1-21-1/adapter-build-logic/build.gradle.kts b/adapters/mc-neoforge-1-21-1/adapter-build-logic/build.gradle.kts
new file mode 100644
index 0000000..63ec97c
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/adapter-build-logic/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ gradlePluginPortal()
+ mavenCentral()
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.21")
+ implementation("net.neoforged.moddev:net.neoforged.moddev.gradle.plugin:2.0.141")
+}
diff --git a/adapters/mc-neoforge-1-21-1/adapter-build-logic/settings.gradle.kts b/adapters/mc-neoforge-1-21-1/adapter-build-logic/settings.gradle.kts
new file mode 100644
index 0000000..7f753ac
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/adapter-build-logic/settings.gradle.kts
@@ -0,0 +1,7 @@
+pluginManagement {
+ includeBuild("../../../build-logic")
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ }
+}
diff --git a/adapters/mc-neoforge-1-21-1/adapter-build-logic/src/main/kotlin/dsgl-mc-neoforge-1-21-1.conventions.gradle.kts b/adapters/mc-neoforge-1-21-1/adapter-build-logic/src/main/kotlin/dsgl-mc-neoforge-1-21-1.conventions.gradle.kts
new file mode 100644
index 0000000..7cc971e
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/adapter-build-logic/src/main/kotlin/dsgl-mc-neoforge-1-21-1.conventions.gradle.kts
@@ -0,0 +1,92 @@
+plugins {
+ kotlin("jvm")
+ id("net.neoforged.moddev")
+}
+
+val modId: String by properties
+val modVersion: String by properties
+val neoVersion: String by properties
+val parchmentVersion: String by properties
+val mcVersion: String by properties
+
+neoForge {
+ version = neoVersion
+
+ parchment {
+ mappingsVersion = parchmentVersion
+ minecraftVersion = mcVersion
+ }
+
+ runs {
+ create("client") {
+ client()
+ gameDirectory = project.file("runs/client")
+ programArguments.addAll("--username", "Developer")
+ systemProperty("neoforge.enabledGameTestNamespaces", modId)
+ }
+ create("client2") {
+ client()
+ gameDirectory = project.file("runs/client2")
+ programArguments.addAll("--username", "Developer2")
+ systemProperty("neoforge.enabledGameTestNamespaces", modId)
+ }
+
+ create("server") {
+ server()
+ gameDirectory = project.file("runs/server")
+ programArgument("--nogui")
+ systemProperty("neoforge.enabledGameTestNamespaces", modId)
+ }
+
+ // This run config launches GameTestServer and runs all registered gametests, then exits.
+ // By default, the server will crash when no gametests are provided.
+ // The gametest system is also enabled by default for other run configs under the /test command.
+ create("gameTestServer") {
+ type = "gameTestServer"
+ gameDirectory = project.file("runs/gameTestServer")
+ systemProperty("neoforge.enabledGameTestNamespaces", modId)
+ }
+
+ create("data") {
+ data()
+ gameDirectory = project.file("runs/data")
+
+ programArguments.addAll("--mod", modId, "--all", "--output", file("src/generated/resources/").absolutePath, "--existing", file("src/main/resources/").absolutePath)
+ }
+
+ configureEach {
+ // The markers can be added/remove as needed separated by commas.
+ // "SCAN": For mods scan.
+ // "REGISTRIES": For firing of registry events.
+ // "REGISTRYDUMP": For getting the contents of all registries.
+ systemProperty("forge.logging.markers", "REGISTRIES")
+
+ //logLevel = org.slf4j.event.Level.DEBUG
+
+ // Colorful logs
+ jvmArgument("-XX:+AllowEnhancedClassRedefinition")
+ systemProperty("terminal.jline", "true")
+ loggingConfigFile.set(project.file("log4j2_config.xml"))
+ }
+ }
+
+ mods {
+ create(modId) {
+ sourceSet(sourceSets.main.get())
+ }
+ }
+}
+
+repositories {
+ gradlePluginPortal()
+ mavenCentral()
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+kotlin {
+ jvmToolchain(21)
+}
diff --git a/adapters/mc-neoforge-1-21-1/build.gradle.kts b/adapters/mc-neoforge-1-21-1/build.gradle.kts
new file mode 100644
index 0000000..975d66a
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ id("dsgl-mc-adapter.conventions")
+ id("dsgl-mc-neoforge-1-21-1.conventions")
+ id("dsgl-releaseable-module.conventions")
+}
+
+dsglRelease {
+ syncKeys.add("modVersion")
+}
+
+dependencies {
+ val coreProject = findProject(":core")
+ ?: findProject(":dsgl:core")
+ ?: error("DSGL core project not found (expected :core or :dsgl:core).")
+ implementation(coreProject)
+ testImplementation(kotlin("test-junit"))
+ testImplementation(kotlin("test"))
+}
diff --git a/adapters/mc-neoforge-1-21-1/gradle.properties b/adapters/mc-neoforge-1-21-1/gradle.properties
new file mode 100644
index 0000000..ec1a7c1
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/gradle.properties
@@ -0,0 +1,38 @@
+publishEnabled=false
+
+# Minecraft and Forge params
+neoVersion=21.1.221
+neoVersionRange=[21.1.0,)
+parchmentVersion=2024.11.17
+mcVersion=1.21.1
+mcVersionRange=[1.21.1, 1.22)
+
+# Mod params
+modGroup=org.dreamfinity
+modId=dsgl-demo
+modName=dsgl-demo
+modArchivesName=dsgl-demo
+modAuthor=Veritaris
+modIcon=
+modDescription=Dreamfinity Simple GUI Library Demo
+modCredits=Veritaris
+buildVersion=1
+modVersion=0.0.1
+
+# Mod dev params
+isClientBuild=false
+clientRunArgs="--username" "Dreamfinity"
+serverRunArgs="--no-gui"
+hotReload=true
+msdfDebug=false
+msdfDebugDecorations=false
+msdfDebugPerformance=false
+rebuildTrace=false
+perfDebug=false
+dsglOverlayDebug=true
+dsglOverlayControls=true
+
+startParameter.offline=true
+
+# Publishing params
+publishProjectDepsOnly=true
diff --git a/adapters/mc-neoforge-1-21-1/log4j2_config.xml b/adapters/mc-neoforge-1-21-1/log4j2_config.xml
new file mode 100644
index 0000000..6b64dc1
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/log4j2_config.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/DsglFonts.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/DsglFonts.kt
new file mode 100644
index 0000000..e7299bd
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/DsglFonts.kt
@@ -0,0 +1,53 @@
+package org.dreamfinity.dsgl.mcNeoforge1211
+
+import org.dreamfinity.dsgl.core.font.FontPreloadSummary
+import org.dreamfinity.dsgl.core.font.FontRegistry
+import java.io.File
+
+object DsglFonts {
+ private val lock = Any()
+
+ @Volatile
+ private var initialized: Boolean = false
+
+ @Volatile
+ private var lastSummary: FontPreloadSummary? = null
+
+ private val warmFontIds: Set = linkedSetOf(
+ FontRegistry.DEFAULT_FONT_ID,
+ FontRegistry.FONT_UBUNTU,
+ FontRegistry.FONT_JB_MONO,
+ FontRegistry.TELEGRAFICO,
+ FontRegistry.FALLBACK_FONT_ID
+ )
+
+ fun ensureInitialized(gameDir: File, classLoader: ClassLoader = javaClass.classLoader): FontPreloadSummary {
+ if (initialized) {
+ return lastSummary ?: FontRegistry.discoverAndPreloadFonts(
+ externalFontsDir = File(gameDir, "dsgl/fonts"),
+ classLoader = classLoader
+ ).also { lastSummary = it }
+ }
+ synchronized(lock) {
+ if (initialized) {
+ return lastSummary ?: FontRegistry.discoverAndPreloadFonts(
+ externalFontsDir = File(gameDir, "dsgl/fonts"),
+ classLoader = classLoader
+ ).also { lastSummary = it }
+ }
+ val summary = FontRegistry.discoverAndPreloadFonts(
+ externalFontsDir = File(gameDir, "dsgl/fonts"),
+ classLoader = classLoader
+ )
+ val warmed = FontRegistry.predecodeAtlases(warmFontIds)
+ if (warmed > 0) {
+ println("[DSGL-MSDF] predecoded atlases for $warmed warm fonts")
+ }
+ lastSummary = summary
+ initialized = true
+ return summary
+ }
+ }
+
+ fun summaryOrNull(): FontPreloadSummary? = lastSummary
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/DsglScreenHost.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/DsglScreenHost.kt
new file mode 100644
index 0000000..9402844
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/DsglScreenHost.kt
@@ -0,0 +1,1420 @@
+package org.dreamfinity.dsgl.mcNeoforge1211
+
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiGraphics
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.network.chat.Component
+import org.dreamfinity.dsgl.core.DomTree
+import org.dreamfinity.dsgl.core.DsglWindow
+import org.dreamfinity.dsgl.core.HotReloadBridge
+import org.dreamfinity.dsgl.core.animation.StyleAnimationEngine
+import org.dreamfinity.dsgl.core.colorpicker.*
+import org.dreamfinity.dsgl.core.contextmenu.ContextMenuRuntime
+import org.dreamfinity.dsgl.core.debug.OverlayDebugControlHost
+import org.dreamfinity.dsgl.core.debug.OverlayLayerDebugState
+import org.dreamfinity.dsgl.core.dnd.DndRuntime
+import org.dreamfinity.dsgl.core.dom.DOMNode
+import org.dreamfinity.dsgl.core.dom.elements.ColorPickerInlineNode
+import org.dreamfinity.dsgl.core.dom.elements.RangeInputNode
+import org.dreamfinity.dsgl.core.dom.elements.SingleLineInputNode
+import org.dreamfinity.dsgl.core.dom.elements.TextAreaNode
+import org.dreamfinity.dsgl.core.event.*
+import org.dreamfinity.dsgl.core.hooks.HookHotReloadRemountException
+import org.dreamfinity.dsgl.core.hooks.HookRenderSessionMode
+import org.dreamfinity.dsgl.core.host.DsglWindowHost
+import org.dreamfinity.dsgl.core.host.Viewport
+import org.dreamfinity.dsgl.core.host.rawMouseToDsglX
+import org.dreamfinity.dsgl.core.host.rawMouseToDsglY
+import org.dreamfinity.dsgl.core.input.ClipboardAccess
+import org.dreamfinity.dsgl.core.input.ClipboardBridge
+import org.dreamfinity.dsgl.core.inspector.InspectorController
+import org.dreamfinity.dsgl.core.inspector.InspectorMode
+import org.dreamfinity.dsgl.core.overlay.ApplicationOverlayHost
+import org.dreamfinity.dsgl.core.overlay.OverlayLayerContracts
+import org.dreamfinity.dsgl.core.overlay.OverlayOwnerScope
+import org.dreamfinity.dsgl.core.overlay.UiLayerId
+import org.dreamfinity.dsgl.core.overlay.system.SystemOverlayHost
+import org.dreamfinity.dsgl.core.render.RenderCommand
+import org.dreamfinity.dsgl.core.select.SelectRuntime
+import org.dreamfinity.dsgl.core.style.Display
+import org.dreamfinity.dsgl.core.style.StyleEngine
+import org.lwjgl.input.Keyboard
+import org.lwjgl.input.Mouse
+import java.io.File
+import java.time.Instant
+import java.time.ZoneId
+import java.util.*
+
+/**
+ * Minecraft 1.21.1 host that owns UI lifecycle and boilerplate.
+ *
+ * Subclass or instantiate with a [DsglWindow] and open it via
+ * `Minecraft.getMinecraft().displayGuiScreen(...)`.
+ */
+abstract class DsglScreenHost(
+ private val windowFactory: () -> DsglWindow,
+ var rendersCount: Long = 0,
+ title: Component = Component.empty()
+) : Screen(title), DsglWindowHost {
+ companion object {
+ @Volatile
+ private var stylesPreloadedOnce: Boolean = false
+ }
+
+ constructor(window: DsglWindow) : this({ window })
+
+ override lateinit var window: DsglWindow
+ private lateinit var adapter: Mc1211UiAdapter
+ private var domTree: DomTree? = null
+ private var lastWidth: Int = 0
+ private var lastHeight: Int = 0
+ private var lastViewport: Viewport = Viewport(width = 0, height = 0)
+ private var needsRender: Boolean = true
+ private var needsLayout: Boolean = true
+ private var lastMouseEvent: Long = 0
+ private var eventButton: Int = -1
+ private var lastMouseX: Int = 0
+ private var lastMouseY: Int = 0
+ private var lastMoveX: Int = Int.MIN_VALUE
+ private var lastMoveY: Int = Int.MIN_VALUE
+ private val pressedKeys: MutableSet = HashSet()
+ private val hoverChain: MutableList = mutableListOf()
+ private var hoverTarget: DOMNode? = null
+ private var dragCaptureTarget: DOMNode? = null
+ private var dragCaptureKey: Any? = null
+ private var dragCaptureClass: Class? = null
+ private var dragCaptureFocusKey: Any? = null
+ private var inspectorPointerCaptured: Boolean = false
+ private var layoutRevision: Long = 0L
+ private val pendingCleanupRoots: MutableSet =
+ Collections.newSetFromMap(IdentityHashMap())
+ private val composedCommandsBuffer: MutableList = ArrayList(512)
+ private val stagingCommandsBuffer: MutableList = ArrayList(512)
+ private val applicationOverlayCommandsBuffer: MutableList = ArrayList(256)
+ private var activeTarget: DOMNode? = null
+ private var lastFrameNanos: Long = 0L
+ private val inspector: InspectorController = InspectorController()
+ private val applicationOverlayHost: ApplicationOverlayHost = ApplicationOverlayHost()
+ private val systemOverlayHost: SystemOverlayHost = SystemOverlayHost(inspector)
+ private val debugOverlayHost: OverlayDebugControlHost = OverlayDebugControlHost()
+ private val colorSamplerOwnershipRouter: ActiveColorSamplerOwnershipRouter = ActiveColorSamplerOwnershipRouter()
+ private var activeColorSamplerOwner: ActiveColorSamplerOwner = ActiveColorSamplerOwner.None
+ private var activeInlineColorSamplerNode: ColorPickerInlineNode? = null
+ private val inspectorInputDebug: Boolean = false
+ private val perfDebug: Boolean = java.lang.Boolean.getBoolean("dsgl.perf.debug")
+ private val phaseTraceDebug: Boolean = java.lang.Boolean.getBoolean("dsgl.rebuild.trace")
+ private var lastPerfLogMs: Long = 0L
+ private var frameIndex: Long = 0L
+ private var blankFrameGuardSkips: Long = 0L
+ private val pipelineErrorLogTimes: MutableMap = linkedMapOf()
+ private val clipboardAccess: ClipboardAccess = object : ClipboardAccess {
+ override fun readText(): String {
+ return try {
+ clipboardAccess.readText()
+ } catch (_: Exception) {
+ ""
+ }
+ }
+
+ override fun writeText(value: String) {
+ try {
+ clipboardAccess.writeText(value)
+ } catch (_: Exception) {
+ }
+ }
+ }
+ val mc: Minecraft
+ get() = Minecraft.getInstance()
+
+ override fun init() {
+ DsglFonts.ensureInitialized(mc.gameDirectory, javaClass.classLoader)
+ adapter = Mc1211UiAdapter(mc)
+ ClipboardBridge.install(clipboardAccess)
+ ScreenColorSamplerBridge.install(
+ object : ScreenColorSampler {
+ override fun sampleColorAt(x: Int, y: Int): Int? = adapter.sampleScreenColor(x, y)
+
+ override fun sampleArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean {
+ return adapter.sampleScreenArea(x, y, width, height, outArgb)
+ }
+ }
+ )
+ inspector.deactivate()
+ inspectorPointerCaptured = false
+ colorSamplerOwnershipRouter.reset()
+ activeColorSamplerOwner = ActiveColorSamplerOwner.None
+ activeInlineColorSamplerNode = null
+ layoutRevision = 0L
+ StyleEngine.clearAllInspectorOverrides()
+ StyleAnimationEngine.clear()
+ StyleEngine.setStylesDirectory(File(mc.gameDirectory, "dsgl/styles"))
+ if (!stylesPreloadedOnce) {
+ StyleEngine.forceReloadStylesheets()
+ stylesPreloadedOnce = true
+ }
+ window = windowFactory()
+ window.attachHost(this)
+ window.markOpened(Instant.now(), ZoneId.systemDefault())
+ needsRender = true
+ needsLayout = true
+ window.onOpen()
+ updateSize(force = true)
+ }
+
+ override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float){
+ if (!::adapter.isInitialized) return
+ frameIndex += 1
+ tracePhase("draw.start")
+ updateSize(force = false)
+ val dsglMouseX = lastViewport.rawMouseToDsglX(mouseX)
+ val dsglMouseY = lastViewport.rawMouseToDsglY(mouseY)
+ window.onFrame(System.currentTimeMillis())
+ val rebuiltThisFrame = rebuildIfNeeded()
+ val tree = domTree ?: return
+ val nowNanos = System.nanoTime()
+ val dtSeconds = if (lastFrameNanos == 0L) {
+ 1.0 / 60.0
+ } else {
+ ((nowNanos - lastFrameNanos).toDouble() / 1_000_000_000.0).coerceIn(0.0, 0.25)
+ }
+ lastFrameNanos = nowNanos
+ OverlayLayerDebugState.updateFrameTiming(dtSeconds)
+ window.tick(dtSeconds.toFloat(), partialTick)
+ val animationVisualsChanged = StyleAnimationEngine.tickAndApply(tree.root, dtSeconds, partialTick)
+ if (animationVisualsChanged) {
+ tree.markVisualDirty()
+ }
+ var stylesAlreadyApplied = false
+ var layoutCommittedThisFrame = false
+ if (needsLayout) {
+ tracePhase("layout.start")
+ if (tryCommitLayout(tree, "drawScreen")) {
+ needsLayout = false
+ stylesAlreadyApplied = true
+ layoutCommittedThisFrame = true
+ tracePhase("layout.end")
+ } else {
+ tracePhase("layout.fail")
+ adapter.paint(composedCommandsBuffer)
+ flushPendingCleanup()
+ super.render(guiGraphics, mouseX, mouseY, partialTick)
+ captureColorPickerEyedropperSamples()
+ return
+ }
+ }
+ inspector.onLayoutCommitted(tree.root, layoutRevision)
+ inspector.onCursorMoved(dsglMouseX, dsglMouseY)
+ inspectorPointerCaptured = inspector.isPointerCaptured
+ if (inspectorPointerCaptured) {
+ inspector.onCapturedPointerMove(dsglMouseX, dsglMouseY, lastWidth, lastHeight)
+ }
+ val appOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(UiLayerId.ApplicationOverlay)
+ val systemOverlayRenderEnabled = OverlayLayerDebugState.isRenderEnabled(UiLayerId.SystemOverlay)
+ val appOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(UiLayerId.ApplicationOverlay)
+ val systemOverlayInputEnabled = OverlayLayerDebugState.isInputEnabled(UiLayerId.SystemOverlay)
+ val inspectorBlocks = systemOverlayInputEnabled && (
+ inspectorPointerCaptured || inspector.shouldConsumePointer(dsglMouseX, dsglMouseY)
+ )
+ tracePhase("commands.start")
+ if (!stylesAlreadyApplied) {
+ tracePhase("style.start")
+ }
+ val commands = try {
+ tree.paint(adapter, applyStyles = !stylesAlreadyApplied)
+ } catch (error: Throwable) {
+ logPipelineError(
+ key = "draw.paint",
+ message = "[DSGL] Paint pipeline failed; rendering previous committed frame: ${error.message}"
+ )
+ adapter.paint(composedCommandsBuffer)
+ flushPendingCleanup()
+ super.render(guiGraphics, mouseX, mouseY, partialTick)
+ captureColorPickerEyedropperSamples()
+ return
+ }
+ if (!stylesAlreadyApplied) {
+ tracePhase("style.end")
+ }
+ ContextMenuRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f)
+ SelectRuntime.engine.onFrame(adapter, lastWidth, lastHeight, 1f)
+ ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight)
+ ColorPickerRuntime.engine.onCursorPosition(dsglMouseX, dsglMouseY)
+ refreshActiveColorSamplerOwner(tree.root)
+ val applicationOverlayCommands = if (!appOverlayRenderEnabled) {
+ emptyList()
+ } else {
+ try {
+ applicationOverlayHost.render(adapter, lastWidth, lastHeight)
+ applicationOverlayHost.paint(adapter)
+ } catch (error: Throwable) {
+ logPipelineError(
+ key = "draw.applicationOverlay",
+ message = "[DSGL] Application overlay paint failed; skipping app overlay frame: ${error.message}"
+ )
+ emptyList()
+ }
+ }
+ systemOverlayHost.syncFrame(
+ inspectedRoot = tree.root,
+ inspectedLayoutRevision = layoutRevision,
+ cursorX = dsglMouseX,
+ cursorY = dsglMouseY,
+ inspectorPointerCaptured = inspectorPointerCaptured
+ )
+ val systemOverlayCommands = if (!systemOverlayRenderEnabled) {
+ emptyList()
+ } else {
+ try {
+ systemOverlayHost.render(adapter, lastWidth, lastHeight)
+ systemOverlayHost.paint(adapter)
+ } catch (error: Throwable) {
+ logPipelineError(
+ key = "draw.systemOverlay",
+ message = "[DSGL] System overlay paint failed; skipping system overlay frame: ${error.message}"
+ )
+ emptyList()
+ }
+ }
+ val debugOverlayCommands = runCatching {
+ debugOverlayHost.render(lastWidth, lastHeight)
+ debugOverlayHost.paint(adapter)
+ }.getOrElse {
+ emptyList()
+ }
+ val contextMenuBlocks = appOverlayInputEnabled && !inspectorBlocks && ContextMenuRuntime.engine.isOpen()
+ val selectBlocks = appOverlayInputEnabled && !inspectorBlocks && SelectRuntime.engine.isOpen()
+ val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline
+ val colorPickerBlocks = !inspectorBlocks && (
+ (systemOverlayInputEnabled && systemOverlayHost.isSystemColorPickerOpen()) ||
+ (appOverlayInputEnabled && ColorPickerRuntime.engine.isOpen() && !inlineSamplerOwnsSession)
+ )
+ if (!inspectorBlocks && !contextMenuBlocks && !selectBlocks && !colorPickerBlocks) {
+ DndRuntime.engine.onMouseMove(tree.root, dsglMouseX, dsglMouseY)
+ }
+ DndRuntime.engine.onFrame(tree.root, dtSeconds)
+ val prevX = if (lastMoveX == Int.MIN_VALUE) dsglMouseX else lastMoveX
+ val prevY = if (lastMoveY == Int.MIN_VALUE) dsglMouseY else lastMoveY
+ val dx = dsglMouseX - prevX
+ val dy = dsglMouseY - prevY
+ if (inspectorBlocks || contextMenuBlocks || selectBlocks || colorPickerBlocks) {
+ clearHoverChainStates()
+ hoverTarget = null
+ } else {
+ updateHover(tree.root, hoverChain, dsglMouseX, dsglMouseY, dx, dy)
+ hoverTarget = hoverChain.lastOrNull()
+ if (dragCaptureTarget != null && hasFocusChangedSinceCapture()) {
+ releaseDragCapture()
+ }
+ if (dx != 0 || dy != 0) {
+ val moveEvent = MouseMoveEvent(dsglMouseX, dsglMouseY, prevX, prevY)
+ moveEvent.target = resolveForcedPointerTarget() ?: dragCaptureTarget ?: hoverTarget
+ EventBus.post(moveEvent)
+ }
+ }
+ lastMoveX = dsglMouseX
+ lastMoveY = dsglMouseY
+ applicationOverlayCommandsBuffer.clear()
+ if (appOverlayRenderEnabled) {
+ applicationOverlayCommandsBuffer.addAll(applicationOverlayCommands)
+ DndRuntime.engine.appendPlaceholderCommands(applicationOverlayCommandsBuffer)
+ DndRuntime.engine.appendOverlayCommands(
+ tree.root,
+ adapter,
+ lastWidth,
+ lastHeight,
+ applicationOverlayCommandsBuffer
+ )
+ SelectRuntime.engine.appendOverlayCommands(adapter, lastWidth, lastHeight, applicationOverlayCommandsBuffer)
+ ContextMenuRuntime.engine.appendOverlayCommands(
+ adapter,
+ lastWidth,
+ lastHeight,
+ applicationOverlayCommandsBuffer
+ )
+ ColorPickerRuntime.engine.appendOverlayCommands(applicationOverlayCommandsBuffer)
+ appendInlineColorPickerOverlayCommands(applicationOverlayCommandsBuffer)
+ }
+ OverlayLayerContracts.composePaintCommands(
+ applicationRoot = commands,
+ applicationOverlay = applicationOverlayCommandsBuffer,
+ systemOverlay = systemOverlayCommands,
+ debug = debugOverlayCommands,
+ out = stagingCommandsBuffer,
+ shouldRenderLayer = OverlayLayerDebugState::isRenderEnabled
+ )
+ val keepPrevious = shouldKeepPreviousFrameCommands(
+ tree = tree,
+ rebuiltThisFrame = rebuiltThisFrame,
+ layoutCommittedThisFrame = layoutCommittedThisFrame,
+ candidate = stagingCommandsBuffer
+ )
+ if (!keepPrevious) {
+ composedCommandsBuffer.clear()
+ composedCommandsBuffer.addAll(stagingCommandsBuffer)
+ } else {
+ blankFrameGuardSkips += 1
+ tracePhase("commands.guard-preserved")
+ }
+ tracePhase("commands.end")
+ adapter.paint(composedCommandsBuffer)
+ tracePhase("draw.end")
+ maybeLogPerf(tree)
+ flushPendingCleanup()
+ super.render(guiGraphics, mouseX, mouseY, partialTick)
+ captureColorPickerEyedropperSamples()
+ }
+
+ // TODO Neoforge-1.21 Probably should be split in core?
+ // This method doesn't exist with both typedChar and keyCode as args. 2 separate methods exist instead (below)
+ override fun keyTyped(typedChar: Char, keyCode: Int) {
+ window.onKeyTyped(typedChar, keyCode)
+ }
+
+ override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ return super.keyPressed(keyCode, scanCode, modifiers)
+ }
+
+ override fun charTyped(codePoint: Char, modifiers: Int): Boolean {
+ return super.charTyped(codePoint, modifiers)
+ }
+
+ override fun onClose() {
+ ClipboardBridge.install(null)
+ ScreenColorSamplerBridge.install(null)
+ FocusManager.clearFocus()
+ DndRuntime.engine.cancelActiveDrag()
+ ColorPickerRuntime.engine.closeAll()
+ SelectRuntime.engine.closeAll()
+ ContextMenuRuntime.engine.closeAll()
+ clearActiveTarget()
+ flushPendingCleanup()
+ clearHoverChainStates()
+ inspector.deactivate()
+ inspectorPointerCaptured = false
+ colorSamplerOwnershipRouter.reset()
+ activeColorSamplerOwner = ActiveColorSamplerOwner.None
+ activeInlineColorSamplerNode = null
+ layoutRevision = 0L
+ StyleEngine.clearAllInspectorOverrides()
+ StyleAnimationEngine.clear()
+ domTree?.clearRefs()
+ applicationOverlayHost.clearRefs()
+ systemOverlayHost.clearRefs()
+ debugOverlayHost.clearRefs()
+ domTree?.root?.let { root ->
+ EventBus.run { root.clearListenersDeep() }
+ }
+ hoverChain.clear()
+ hoverTarget = null
+ releaseDragCapture()
+ lastFrameNanos = 0L
+ window.disposeHookRuntime()
+ window.onClose()
+ super.onClose()
+ }
+
+ override fun isPauseScreen(): Boolean = false
+
+ override fun requestRebuild(reason: String?) {
+ needsRender = true
+ }
+
+ override fun requestRedraw() {
+ }
+
+ override fun getViewport(): Viewport {
+ return lastViewport
+ }
+
+ private fun updateSize(force: Boolean) {
+ val viewport = adapter.viewport()
+ val width = viewport.width
+ val height = viewport.height
+ lastViewport = viewport
+ if (force || width != lastWidth || height != lastHeight) {
+ ContextMenuRuntime.engine.closeAll()
+ lastWidth = width
+ lastHeight = height
+ needsLayout = true
+ needsRender = true
+ window.onResize(width, height)
+ }
+ }
+
+ private fun rebuildIfNeeded(): Boolean {
+ val hotSwapped = HotReloadBridge.consumeHotSwap()
+ if (!hotSwapped && !needsRender && domTree != null) {
+ return false
+ }
+
+ if (hotSwapped) {
+ println("Hot swapped - re-building the DOM")
+ }
+
+ return try {
+ tracePhase("rebuild.start")
+ rendersCount++
+ val nextTree = renderWithHookSession(hotSwapped)
+ val currentTree = domTree
+ if (currentTree == null) {
+ domTree = nextTree
+ } else {
+ val reconcile = currentTree.reconcileWith(nextTree)
+ if (reconcile.detachedRoots.isNotEmpty()) {
+ pendingCleanupRoots.addAll(reconcile.detachedRoots)
+ flushPendingCleanup()
+ }
+ domTree = currentTree
+ }
+ // Reconcile may involve selector-state mutations on template nodes.
+ // Force a full style pass on the active retained tree to avoid one-frame unstyled flashes.
+ StyleEngine.markSelectorStateChanged()
+ needsRender = false
+ needsLayout = true
+ domTree?.root?.let { root ->
+ FocusManager.retainFocus(root)
+ restoreDragCapture(root)
+ DndRuntime.engine.rebindAfterReconcile(root)
+ }
+ window.commitRenderBuild()
+ tracePhase("rebuild.end")
+ true
+ } catch (error: Throwable) {
+ window.discardRenderBuild()
+ logPipelineError(
+ key = "rebuild",
+ message = "[DSGL] Rebuild failed; keeping previous committed frame/tree: ${error.message}"
+ )
+ false
+ }
+ }
+
+ private fun renderWithHookSession(hotSwapped: Boolean): DomTree {
+ val mode = if (hotSwapped) HookRenderSessionMode.HotReload else HookRenderSessionMode.Normal
+ val maxAttempts = if (hotSwapped) 8 else 1
+ var attempt = 0
+ var lastRemountRequest: HookHotReloadRemountException? = null
+
+ while (attempt < maxAttempts) {
+ attempt += 1
+ window.beginRenderBuild(mode)
+ var remountRequested = false
+ try {
+ return window.render()
+ } catch (remount: HookHotReloadRemountException) {
+ if (!hotSwapped) {
+ throw remount
+ }
+ remountRequested = true
+ lastRemountRequest = remount
+ println(remount.message)
+ } finally {
+ window.endRenderBuild()
+ if (remountRequested) {
+ window.discardRenderBuild()
+ }
+ }
+ }
+
+ throw IllegalStateException(
+ "Hot-reload hook remount recovery exceeded $maxAttempts attempts: ${lastRemountRequest?.message}"
+ )
+ }
+
+ // TODO Neoforge-1.21 Keyboard handling is done in KeyboardHandler class instead of GUI
+ // Maybe replace with onKeyPressed, onKeyReleased
+ override fun handleKeyboardInput() {
+ updateSize(force = false)
+ KeyModifiers.sync(
+ shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT),
+ control = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL),
+ meta = Keyboard.isKeyDown(Keyboard.KEY_LMETA) || Keyboard.isKeyDown(Keyboard.KEY_RMETA)
+ )
+ systemOverlayHost.onInputFrame(lastWidth, lastHeight)
+ ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight)
+ val keyCode = Keyboard.getEventKey()
+ val keyChar = Keyboard.getEventCharacter()
+ val inspectorMouseX = if (lastMoveX == Int.MIN_VALUE) lastMouseX else lastMoveX
+ val inspectorMouseY = if (lastMoveY == Int.MIN_VALUE) lastMouseY else lastMoveY
+ if (Keyboard.getEventKeyState()) {
+ if (!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12) {
+ inspector.toggle()
+ inspectorPointerCaptured = false
+ if (inspector.active) {
+ DndRuntime.engine.cancelActiveDrag()
+ releaseDragCapture()
+ clearActiveTarget()
+ clearHoverChainStates()
+ }
+ mc.dispatchKeypresses()
+ return
+ }
+ if (Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) && keyCode == Keyboard.KEY_F12 && inspector.active) {
+ inspector.toggleMode()
+ mc.dispatchKeypresses()
+ return
+ }
+ if (keyCode == Keyboard.KEY_F10) {
+ val demoAnchorX = if (lastMoveX == Int.MIN_VALUE) inspectorMouseX else lastMoveX
+ val demoAnchorY = if (lastMoveY == Int.MIN_VALUE) inspectorMouseY else lastMoveY
+ systemOverlayHost.togglePanelDemo(demoAnchorX, demoAnchorY)
+ mc.dispatchKeypresses()
+ return
+ }
+ if (keyCode == Keyboard.KEY_ESCAPE && inspector.cancelPickMode()) {
+ logInspectorInput("escape cancelled inspector pick mode")
+ mc.dispatchKeypresses()
+ return
+ }
+ if (consumeOverlayKeyDown(
+ keyCode = keyCode,
+ keyChar = keyChar,
+ inspectorMouseX = inspectorMouseX,
+ inspectorMouseY = inspectorMouseY
+ )
+ ) {
+ mc.dispatchKeypresses()
+ return
+ }
+ if (keyCode == Keyboard.KEY_F6) {
+ StyleEngine.forceReloadStylesheets()
+ requestRebuild("style reload")
+ }
+ if (pressedKeys.add(keyCode)) {
+ val downEvent = KeyboardKeyDownEvent(keyChar, keyCode)
+ EventBus.post(downEvent)
+ if (downEvent.cancelled) {
+ pressedKeys.remove(keyCode)
+ } else {
+ window.onKeyTyped(keyChar, keyCode)
+ if (keyCode == Keyboard.KEY_ESCAPE) {
+ mc.displayGuiScreen(null)
+ }
+ }
+ }
+ } else {
+ val keyboardBlocked = inspector.active && (
+ inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) ||
+ inspector.mode == InspectorMode.Locked
+ )
+ if (keyboardBlocked) {
+ pressedKeys.remove(keyCode)
+ logInspectorInput("keyboard up consumed keyCode=$keyCode")
+ mc.dispatchKeypresses()
+ return
+ }
+ if (pressedKeys.remove(keyCode)) {
+ EventBus.post(KeyboardKeyUpEvent(keyChar, keyCode))
+ }
+ }
+
+ mc.dispatchKeypresses()
+ }
+
+ // TODO NeoForge-1.21 Mouse input is handled in MouseHandler instead
+ // Maybe should be replaced by appropriate handlers?
+ override fun handleMouseInput() {
+ updateSize(force = false)
+ rebuildIfNeeded()
+ val tree = domTree ?: return
+ if (needsLayout) {
+ if (tryCommitLayout(tree, "handleMouseInput")) {
+ needsLayout = false
+ } else {
+ return
+ }
+ }
+
+ val mouseX = lastViewport.rawMouseToDsglX(Mouse.getEventX())
+ val mouseY = lastViewport.rawMouseToDsglY(Mouse.getEventY())
+ val dWheel = Mouse.getDWheel()
+ val mouseButton = Mouse.getEventButton()
+ inspector.onCursorMoved(mouseX, mouseY)
+ ContextMenuRuntime.engine.onFrame(
+ measureContext = adapter,
+ viewportWidth = lastWidth,
+ viewportHeight = lastHeight,
+ viewportScale = 1f
+ )
+ SelectRuntime.engine.onFrame(
+ measureContext = adapter,
+ viewportWidth = lastWidth,
+ viewportHeight = lastHeight,
+ viewportScale = 1f
+ )
+ systemOverlayHost.onInputFrame(lastWidth, lastHeight)
+ inspectorPointerCaptured = inspector.isPointerCaptured
+ systemOverlayHost.syncFrame(
+ inspectedRoot = tree.root,
+ inspectedLayoutRevision = layoutRevision,
+ cursorX = mouseX,
+ cursorY = mouseY,
+ inspectorPointerCaptured = inspectorPointerCaptured
+ )
+ ColorPickerRuntime.engine.onFrame(lastWidth, lastHeight)
+ refreshActiveColorSamplerOwner(tree.root)
+ val appPressMove = mouseButton == -1 && eventButton != -1
+ if (!appPressMove && consumeOverlayPointerEvent(mouseX, mouseY, dWheel, mouseButton)) {
+ consumeOverlayPointerState(mouseX, mouseY)
+ return
+ }
+
+
+ refreshHoverTarget(mouseX, mouseY)
+
+ if (mouseButton > 2) return
+
+ if (Mouse.getEventButtonState()) {
+ eventButton = mouseButton
+ lastMouseEvent = Minecraft.getSystemTime()
+ mapButton(mouseButton)?.let { mappedButton ->
+ val event = MouseDownEvent(mouseX, mouseY, mappedButton)
+ event.target = resolvePointerDownTarget()
+ EventBus.post(event)
+ DndRuntime.engine.onMouseDown(tree.root, event.target ?: hoverTarget, event)
+ if (mappedButton == MouseButton.LEFT) {
+ setActiveTarget(event.target ?: hoverTarget)
+ val captureTarget = resolveDragCaptureTarget(event.target ?: hoverTarget, mouseX, mouseY)
+ if (captureTarget != null) {
+ setDragCapture(captureTarget)
+ captureTarget.beginPointerCapture(mouseX, mouseY, mappedButton)
+ } else if (dragCaptureTarget != null) {
+ releaseDragCapture()
+ }
+ }
+ }
+ } else if (mouseButton != -1 && eventButton == mouseButton) {
+ val releaseTarget = resolvePointerUpTarget()
+ val hadDragCapture = dragCaptureTarget != null
+ eventButton = -1
+ mapButton(mouseButton)?.let { mappedButton ->
+ val upEvent = MouseUpEvent(mouseX, mouseY, mappedButton)
+ upEvent.target = releaseTarget
+ EventBus.post(upEvent)
+ dragCaptureTarget?.endPointerCapture(mouseX, mouseY, mappedButton)
+ val dndConsumed = DndRuntime.engine.onMouseUp(tree.root, upEvent)
+ if (!hadDragCapture && !dndConsumed) {
+ val clickEvent = MouseClickEvent(mouseX, mouseY, mappedButton)
+ clickEvent.target = resolveClickTarget()
+ EventBus.post(clickEvent)
+ }
+ }
+ clearActiveTarget()
+ releaseDragCapture()
+ } else if (eventButton != -1 && lastMouseEvent > 0L) {
+ mapButton(eventButton)?.let { mappedButton ->
+ val dx = mouseX - lastMouseX
+ val dy = mouseY - lastMouseY
+ if (dx != 0 || dy != 0) {
+ DndRuntime.engine.onMouseMove(tree.root, mouseX, mouseY)
+ val dragEvent = MouseDragEvent(
+ lastMouseX,
+ lastMouseY,
+ dx,
+ dy,
+ mappedButton
+ )
+ if (!DndRuntime.engine.isDragging) {
+ dragEvent.target = dragCaptureTarget ?: hoverTarget
+ EventBus.post(dragEvent)
+ }
+ dragCaptureTarget?.continuePointerCapture(
+ mouseX = mouseX,
+ mouseY = mouseY,
+ mouseDX = dx,
+ mouseDY = dy,
+ button = mappedButton
+ )
+ }
+ }
+ }
+
+ if (dWheel != 0) {
+ val wheelTarget = resolveWheelTarget()
+ if (wheelTarget != null) {
+ val wheelEvent = MouseWheelEvent(mouseX, mouseY, dWheel)
+ wheelEvent.target = wheelTarget
+ EventBus.post(wheelEvent)
+ if (!wheelEvent.cancelled) {
+ bubbleGenericWheel(wheelTarget, mouseX, mouseY, dWheel)
+ }
+ }
+ }
+
+ lastMouseX = mouseX
+ lastMouseY = mouseY
+ }
+
+ private fun consumeOverlayKeyDown(
+ keyCode: Int,
+ keyChar: Char,
+ inspectorMouseX: Int,
+ inspectorMouseY: Int
+ ): Boolean {
+ val consumedBy = OverlayLayerContracts.firstInputConsumer(
+ canConsume = { layer ->
+ when (layer) {
+ UiLayerId.Debug -> debugOverlayHost.handleKeyDown(keyCode, keyChar)
+ UiLayerId.SystemOverlay -> consumeSystemOverlayKeyDown(
+ keyCode = keyCode,
+ keyChar = keyChar,
+ inspectorMouseX = inspectorMouseX,
+ inspectorMouseY = inspectorMouseY
+ )
+
+ UiLayerId.ApplicationOverlay -> consumeApplicationOverlayKeyDown(keyCode, keyChar)
+ UiLayerId.ApplicationRoot -> false
+ }
+ },
+ isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled
+ )
+ return consumedBy != null
+ }
+
+ private fun consumeSystemOverlayKeyDown(
+ keyCode: Int,
+ keyChar: Char,
+ inspectorMouseX: Int,
+ inspectorMouseY: Int
+ ): Boolean {
+ if (systemOverlayHost.handleKeyDown(keyCode, keyChar)) {
+ return true
+ }
+ val keyboardBlocked = inspector.active && (
+ inspector.shouldConsumeKeyboard(inspectorMouseX, inspectorMouseY) ||
+ inspector.mode == InspectorMode.Locked
+ )
+ if (keyboardBlocked) {
+ logInspectorInput("keyboard down consumed keyCode=$keyCode")
+ return true
+ }
+ return false
+ }
+
+ private fun consumeApplicationOverlayKeyDown(keyCode: Int, keyChar: Char): Boolean {
+ if (ColorPickerRuntime.engine.handleKeyDown(keyCode, keyChar)) {
+ return true
+ }
+ if (applicationOverlayHost.handleKeyDown(keyCode, keyChar)) {
+ return true
+ }
+ if (SelectRuntime.engine.handleKeyDown(keyCode, keyChar)) {
+ return true
+ }
+ if (ContextMenuRuntime.engine.handleKeyDown(keyCode)) {
+ return true
+ }
+ return false
+ }
+
+ private fun consumeOverlayPointerEvent(
+ mouseX: Int,
+ mouseY: Int,
+ dWheel: Int,
+ mouseButton: Int
+ ): Boolean {
+ val mappedButton = mapButton(mouseButton)
+ val buttonPressed = Mouse.getEventButtonState()
+ val consumedBy = OverlayLayerContracts.firstInputConsumer(
+ canConsume = { layer ->
+ when (layer) {
+ UiLayerId.Debug -> consumeDebugPointerEvent(
+ mouseX = mouseX,
+ mouseY = mouseY,
+ dWheel = dWheel,
+ mappedButton = mappedButton,
+ mouseButton = mouseButton,
+ buttonPressed = buttonPressed
+ )
+
+ UiLayerId.SystemOverlay -> consumeSystemOverlayPointerEvent(
+ mouseX = mouseX,
+ mouseY = mouseY,
+ dWheel = dWheel,
+ mouseButton = mouseButton,
+ mappedButton = mappedButton,
+ buttonPressed = buttonPressed
+ )
+
+ UiLayerId.ApplicationOverlay -> consumeApplicationOverlayPointerEvent(
+ mouseX = mouseX,
+ mouseY = mouseY,
+ dWheel = dWheel,
+ mouseButton = mouseButton,
+ mappedButton = mappedButton,
+ buttonPressed = buttonPressed
+ )
+
+ UiLayerId.ApplicationRoot -> false
+ }
+ },
+ isLayerInputEnabled = OverlayLayerDebugState::isInputEnabled
+ )
+ return consumedBy != null
+ }
+
+ private fun consumeDebugPointerEvent(
+ mouseX: Int,
+ mouseY: Int,
+ dWheel: Int,
+ mappedButton: MouseButton?,
+ mouseButton: Int,
+ buttonPressed: Boolean
+ ): Boolean {
+ if (dWheel != 0 && debugOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) {
+ return true
+ }
+ if (mouseButton != -1 && mappedButton != null) {
+ return if (buttonPressed) {
+ debugOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton)
+ } else {
+ debugOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton)
+ }
+ }
+ if (mouseButton == -1 && debugOverlayHost.handleMouseMove(mouseX, mouseY)) {
+ return true
+ }
+ return false
+ }
+
+ private fun consumeSystemOverlayPointerEvent(
+ mouseX: Int,
+ mouseY: Int,
+ dWheel: Int,
+ mouseButton: Int,
+ mappedButton: MouseButton?,
+ buttonPressed: Boolean
+ ): Boolean {
+ if (dWheel != 0 && systemOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) {
+ return true
+ }
+ if (mouseButton != -1 && mappedButton != null) {
+ val consumedBySystemOverlay = if (buttonPressed) {
+ systemOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton)
+ } else {
+ systemOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton)
+ }
+ if (consumedBySystemOverlay) {
+ return true
+ }
+ } else if (mouseButton == -1 && systemOverlayHost.handleMouseMove(mouseX, mouseY)) {
+ return true
+ }
+
+ val inspectorConsumesPointer = inspector.shouldConsumePointer(mouseX, mouseY)
+ if (!inspectorConsumesPointer) return false
+ if (!buttonPressed && mouseButton != -1) {
+ inspectorPointerCaptured = false
+ }
+ logInspectorInput("pointer event consumed by inspector bounds button=$mouseButton wheel=$dWheel")
+ return true
+ }
+
+ private fun consumeApplicationOverlayPointerEvent(
+ mouseX: Int,
+ mouseY: Int,
+ dWheel: Int,
+ mouseButton: Int,
+ mappedButton: MouseButton?,
+ buttonPressed: Boolean
+ ): Boolean {
+ val inlineSamplerOwnsSession = activeColorSamplerOwner is ActiveColorSamplerOwner.Inline
+ if (!inlineSamplerOwnsSession) {
+ if (dWheel != 0 && ColorPickerRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) {
+ return true
+ }
+ if (mouseButton != -1 && mappedButton != null) {
+ val consumedByColorPicker = if (buttonPressed) {
+ ColorPickerRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton)
+ } else {
+ ColorPickerRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton)
+ }
+ if (consumedByColorPicker) {
+ return true
+ }
+ } else if (mouseButton == -1 && ColorPickerRuntime.engine.handleMouseMove(mouseX, mouseY)) {
+ return true
+ }
+ }
+
+ if (dWheel != 0 && applicationOverlayHost.handleMouseWheel(mouseX, mouseY, dWheel)) {
+ return true
+ }
+ if (mouseButton != -1 && mappedButton != null) {
+ val consumedByAppOverlay = if (buttonPressed) {
+ applicationOverlayHost.handleMouseDown(mouseX, mouseY, mappedButton)
+ } else {
+ applicationOverlayHost.handleMouseUp(mouseX, mouseY, mappedButton)
+ }
+ if (consumedByAppOverlay) {
+ return true
+ }
+ } else if (mouseButton == -1 && applicationOverlayHost.handleMouseMove(mouseX, mouseY)) {
+ return true
+ }
+
+ if (dWheel != 0 && ContextMenuRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) {
+ return true
+ }
+ if (dWheel != 0 && SelectRuntime.engine.handleMouseWheel(mouseX, mouseY, dWheel)) {
+ return true
+ }
+ if (mouseButton != -1 && mappedButton != null) {
+ val consumedByContextMenu = if (buttonPressed) {
+ ContextMenuRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton)
+ } else {
+ ContextMenuRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton)
+ }
+ if (consumedByContextMenu) {
+ return true
+ }
+ val consumedBySelect = if (buttonPressed) {
+ SelectRuntime.engine.handleMouseDown(mouseX, mouseY, mappedButton)
+ } else {
+ SelectRuntime.engine.handleMouseUp(mouseX, mouseY, mappedButton)
+ }
+ if (consumedBySelect) {
+ return true
+ }
+ return false
+ }
+ if (mouseButton == -1 && ContextMenuRuntime.engine.handleMouseMove(mouseX, mouseY)) {
+ return true
+ }
+ if (mouseButton == -1 && SelectRuntime.engine.handleMouseMove(mouseX, mouseY)) {
+ return true
+ }
+ return false
+ }
+
+ private fun consumeOverlayPointerState(mouseX: Int, mouseY: Int) {
+ eventButton = -1
+ clearActiveTarget()
+ releaseDragCapture()
+ lastMouseX = mouseX
+ lastMouseY = mouseY
+ }
+
+ private fun mapButton(button: Int): MouseButton? {
+ return when (button) {
+ 0 -> MouseButton.LEFT
+ 1 -> MouseButton.RIGHT
+ 2 -> MouseButton.MIDDLE
+ else -> null
+ }
+ }
+
+ init {
+ inspector.installColorPickerHost(systemOverlayHost.systemInspectorColorPickerPopupHost())
+ }
+
+ private fun refreshActiveColorSamplerOwner(root: DOMNode?) {
+ val inlineByToken = LinkedHashMap()
+ if (root != null) {
+ collectActiveInlineColorSamplers(root, inlineByToken)
+ }
+ val focusedInline = FocusManager.focusedNode() as? ColorPickerInlineNode
+ if (focusedInline != null && focusedInline.wantsGlobalPointerInput()) {
+ inlineByToken.putIfAbsent(colorSamplerToken(focusedInline), focusedInline)
+ }
+ activeColorSamplerOwner = colorSamplerOwnershipRouter.update(
+ popupEyedropperActive = ColorPickerRuntime.engine.hasActiveEyedropper(),
+ inlineActiveTokens = inlineByToken.keys.toSet()
+ )
+ activeInlineColorSamplerNode = when (val owner = activeColorSamplerOwner) {
+ is ActiveColorSamplerOwner.Inline -> inlineByToken[owner.token]
+ else -> null
+ }
+ }
+
+ private fun collectActiveInlineColorSamplers(
+ node: DOMNode,
+ out: MutableMap
+ ) {
+ if (node is ColorPickerInlineNode && node.wantsGlobalPointerInput()) {
+ out.putIfAbsent(colorSamplerToken(node), node)
+ }
+ for (child in node.children) {
+ collectActiveInlineColorSamplers(child, out)
+ }
+ }
+
+ private fun colorSamplerToken(node: ColorPickerInlineNode): Any {
+ return node.key ?: node
+ }
+
+ private fun resolveForcedPointerTarget(): DOMNode? {
+ if (activeColorSamplerOwner is ActiveColorSamplerOwner.Inline) {
+ val inline = activeInlineColorSamplerNode
+ if (inline != null && inline.wantsGlobalPointerInput()) {
+ return inline
+ }
+ }
+ return null
+ }
+
+ private fun appendInlineColorPickerOverlayCommands(out: MutableList) {
+ val layer = OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.Application)
+ if (layer != UiLayerId.ApplicationOverlay) return
+ if (activeColorSamplerOwner is ActiveColorSamplerOwner.Inline) {
+ val inline = activeInlineColorSamplerNode ?: return
+ if (!inline.wantsGlobalPointerInput()) return
+ inline.appendEyedropperOverlayCommands(
+ viewportWidth = lastWidth.coerceAtLeast(1),
+ viewportHeight = lastHeight.coerceAtLeast(1),
+ out = out
+ )
+ }
+ }
+
+ private fun captureColorPickerEyedropperSamples() {
+ refreshActiveColorSamplerOwner(domTree?.root)
+ if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.System) == UiLayerId.SystemOverlay) {
+ systemOverlayHost.captureSystemColorPickerEyedropperSample()
+ }
+ if (OverlayLayerContracts.resolveTransientLayer(OverlayOwnerScope.Application) != UiLayerId.ApplicationOverlay) {
+ return
+ }
+ when (activeColorSamplerOwner) {
+ ActiveColorSamplerOwner.Popup -> ColorPickerRuntime.engine.captureEyedropperSample()
+ is ActiveColorSamplerOwner.Inline -> {
+ val inline = activeInlineColorSamplerNode
+ if (inline != null && inline.wantsGlobalPointerInput()) {
+ inline.captureEyedropperSample()
+ }
+ }
+
+ ActiveColorSamplerOwner.None -> {
+ if (ColorPickerRuntime.engine.hasActiveEyedropper()) {
+ ColorPickerRuntime.engine.captureEyedropperSample()
+ }
+ }
+ }
+ }
+
+ private fun flushPendingCleanup() {
+ if (pendingCleanupRoots.isEmpty()) return
+ val detachedRoots = pendingCleanupRoots.toList()
+ pendingCleanupRoots.clear()
+ detachedRoots.forEach { root ->
+ EventBus.run { root.clearListenersDeep() }
+ }
+ }
+
+ internal fun debugPendingCleanupCount(): Int = pendingCleanupRoots.size
+
+ internal fun debugBindTreeForTests(tree: DomTree, needsLayout: Boolean = false) {
+ domTree = tree
+ this.needsLayout = needsLayout
+ }
+
+ internal fun debugRefreshHoverTargetForTests(mouseX: Int, mouseY: Int) {
+ refreshHoverTarget(mouseX, mouseY)
+ }
+
+ internal fun debugHoverTargetForTests(): DOMNode? = hoverTarget
+
+ internal fun debugResolvePointerDownTargetForTests(): DOMNode? = resolvePointerDownTarget()
+
+ internal fun debugResolveClickTargetForTests(): DOMNode? = resolveClickTarget()
+
+ internal fun debugSetNeedsRenderForTests(value: Boolean) {
+ needsRender = value
+ }
+
+ internal fun debugRebuildIfNeededForTests(): Boolean = rebuildIfNeeded()
+
+ private fun setDragCapture(target: DOMNode) {
+ dragCaptureTarget = target
+ dragCaptureKey = target.key
+ dragCaptureClass = target.javaClass
+ dragCaptureFocusKey = FocusManager.focusedNode()?.key
+ }
+
+ private fun releaseDragCapture() {
+ dragCaptureTarget?.cancelPointerCapture()
+ RangeInputNode.clearActiveDrag()
+ SingleLineInputNode.clearActiveDrag()
+ TextAreaNode.clearActiveDrag()
+ dragCaptureTarget = null
+ dragCaptureKey = null
+ dragCaptureClass = null
+ dragCaptureFocusKey = null
+ }
+
+ private fun setActiveTarget(target: DOMNode?) {
+ if (target?.styleDisabled == true) return
+ if (activeTarget === target) return
+ activeTarget?.setActiveState(false)
+ activeTarget = target
+ activeTarget?.setActiveState(true)
+ }
+
+ private fun clearActiveTarget() {
+ activeTarget?.setActiveState(false)
+ activeTarget = null
+ }
+
+ private fun resolveDragCaptureTarget(start: DOMNode?, mouseX: Int, mouseY: Int): DOMNode? {
+ var current = start
+ while (current != null) {
+ when (current) {
+ is RangeInputNode -> return current
+ is SingleLineInputNode -> if (current.shouldCaptureTextSelectionDrag(mouseX, mouseY)) return current
+ is TextAreaNode -> if (current.shouldCaptureAnyDrag(mouseX, mouseY)) return current
+ }
+ if (current.shouldCapturePointerDrag(mouseX, mouseY)) {
+ return current
+ }
+ current = current.parent
+ }
+ return null
+ }
+
+ private fun restoreDragCapture(root: DOMNode) {
+ if (dragCaptureTarget == null) return
+ val key = dragCaptureKey
+ val cls = dragCaptureClass
+ if (cls == null) {
+ releaseDragCapture()
+ return
+ }
+ if (key == null) {
+ val captured = dragCaptureTarget
+ if (captured != null && captured.javaClass == cls) {
+ if (eventButton != -1) {
+ return
+ }
+ if (isSameOrAncestor(root, captured)) {
+ return
+ }
+ }
+ releaseDragCapture()
+ return
+ }
+
+ val restored = findByKeyAndClass(root, key, cls)
+ if (restored != null) {
+ dragCaptureTarget = restored
+ } else {
+ releaseDragCapture()
+ }
+ }
+
+ private fun findByKeyAndClass(
+ node: DOMNode,
+ key: Any,
+ cls: Class
+ ): DOMNode? {
+ if (node.key == key && node.javaClass == cls) return node
+ for (child in node.children) {
+ val found = findByKeyAndClass(child, key, cls)
+ if (found != null) {
+ return found
+ }
+ }
+ return null
+ }
+
+ private fun hasFocusChangedSinceCapture(): Boolean {
+ if (dragCaptureFocusKey == null) return false
+ val currentFocusKey = FocusManager.focusedNode()?.key
+ return currentFocusKey != dragCaptureFocusKey
+ }
+
+ private fun refreshHoverTarget(mouseX: Int, mouseY: Int) {
+ val tree = domTree ?: return
+ if (needsLayout) {
+ if (tryCommitLayout(tree, "refreshHoverTarget")) {
+ needsLayout = false
+ } else {
+ return
+ }
+ }
+ val chain = collectHoverChain(tree.root, mouseX, mouseY)
+ hoverTarget = chain.lastOrNull()
+ }
+
+ private fun resolvePointerDownTarget(): DOMNode? {
+ return resolveForcedPointerTarget() ?: hoverTarget
+ }
+
+ private fun resolvePointerUpTarget(): DOMNode? {
+ return dragCaptureTarget ?: resolveForcedPointerTarget() ?: hoverTarget
+ }
+
+ private fun resolveClickTarget(): DOMNode? {
+ return hoverTarget
+ }
+
+ private fun resolveWheelTarget(): DOMNode? {
+ val focused = FocusManager.focusedNode()
+ if (focused is TextAreaNode) {
+ val hovered = hoverTarget
+ if (!isSameOrAncestor(focused, hovered)) {
+ return focused
+ }
+ }
+ return hoverTarget
+ }
+
+ private fun bubbleGenericWheel(target: DOMNode, mouseX: Int, mouseY: Int, delta: Int): Boolean {
+ var current: DOMNode? = target
+ while (current != null) {
+ if (current.handleGenericWheel(mouseX, mouseY, delta)) {
+ return true
+ }
+ current = current.parent
+ }
+ return false
+ }
+
+ private fun isSameOrAncestor(candidate: DOMNode, node: DOMNode?): Boolean {
+ var current = node
+ while (current != null) {
+ if (current === candidate) return true
+ current = current.parent
+ }
+ return false
+ }
+
+ private fun clearHoverChainStates() {
+ hoverChain.forEach { node ->
+ node.setHoveredState(false)
+ }
+ hoverChain.clear()
+ }
+
+ private fun logInspectorInput(message: String) {
+ if (!inspectorInputDebug) return
+ println("[DSGL-InspectorInput] $message")
+ }
+
+ private fun tryCommitLayout(tree: DomTree, phase: String): Boolean {
+ return try {
+ tree.render(adapter, lastWidth, lastHeight)
+ val rootBounds = tree.root.bounds
+ if (lastWidth > 0 && lastHeight > 0 && (rootBounds.width <= 0 || rootBounds.height <= 0)) {
+ logPipelineError(
+ key = "layout.$phase.invalidBounds",
+ message = "[DSGL] Layout commit produced invalid root bounds ${rootBounds.width}x${rootBounds.height} in $phase."
+ )
+ return false
+ }
+ layoutRevision++
+ inspector.onLayoutCommitted(tree.root, layoutRevision)
+ true
+ } catch (error: Throwable) {
+ logPipelineError(
+ key = "layout.$phase",
+ message = "[DSGL] Layout commit failed in $phase; keeping previous frame: ${error.message}"
+ )
+ false
+ }
+ }
+
+ private fun shouldKeepPreviousFrameCommands(
+ tree: DomTree,
+ rebuiltThisFrame: Boolean,
+ layoutCommittedThisFrame: Boolean,
+ candidate: List
+ ): Boolean {
+ val shape = validateCommandShape(candidate)
+ if (!shape.valid) {
+ logPipelineError(
+ key = "shape.guard",
+ message = "[DSGL] Guarded invalid command shape (clip=${shape.clipDepth}, transform=${shape.transformDepth}, opacity=${shape.opacityDepth}); keeping previous frame."
+ )
+ return composedCommandsBuffer.isNotEmpty()
+ }
+ if (candidate.isNotEmpty()) return false
+ if (composedCommandsBuffer.isEmpty()) return false
+ if (!rebuiltThisFrame && !layoutCommittedThisFrame) return false
+ if (!hasRenderableNodes(tree.root)) return false
+ logPipelineError(
+ key = "blank.guard",
+ message = "[DSGL] Guarded against blank rebuild frame; keeping previous commands."
+ )
+ return true
+ }
+
+ private data class CommandShape(
+ val valid: Boolean,
+ val clipDepth: Int,
+ val transformDepth: Int,
+ val opacityDepth: Int
+ )
+
+ private fun validateCommandShape(commands: List): CommandShape {
+ var clipDepth = 0
+ var transformDepth = 0
+ var opacityDepth = 0
+ for (command in commands) {
+ when (command) {
+ is RenderCommand.PushClip -> clipDepth += 1
+ is RenderCommand.PopClip -> {
+ clipDepth -= 1
+ if (clipDepth < 0) return CommandShape(false, clipDepth, transformDepth, opacityDepth)
+ }
+
+ is RenderCommand.PushTransform -> transformDepth += 1
+ is RenderCommand.PopTransform -> {
+ transformDepth -= 1
+ if (transformDepth < 0) return CommandShape(false, clipDepth, transformDepth, opacityDepth)
+ }
+
+ is RenderCommand.PushOpacity -> opacityDepth += 1
+ is RenderCommand.PopOpacity -> {
+ opacityDepth -= 1
+ if (opacityDepth < 0) return CommandShape(false, clipDepth, transformDepth, opacityDepth)
+ }
+
+ else -> Unit
+ }
+ }
+ return CommandShape(
+ valid = clipDepth == 0 && transformDepth == 0 && opacityDepth == 0,
+ clipDepth = clipDepth,
+ transformDepth = transformDepth,
+ opacityDepth = opacityDepth
+ )
+ }
+
+ private fun hasRenderableNodes(node: DOMNode): Boolean {
+ if (node.display != Display.None && node.children.isNotEmpty()) {
+ return true
+ }
+ node.children.forEach { child ->
+ if (hasRenderableNodes(child)) return true
+ }
+ return false
+ }
+
+ private fun tracePhase(phase: String) {
+ if (!phaseTraceDebug) return
+ println("[DSGL-RebuildTrace] frame=$frameIndex phase=$phase needsRender=$needsRender needsLayout=$needsLayout")
+ }
+
+ private fun logPipelineError(key: String, message: String) {
+ val now = System.currentTimeMillis()
+ val previous = pipelineErrorLogTimes[key] ?: 0L
+ if (now - previous < 2_000L) return
+ pipelineErrorLogTimes[key] = now
+ println(message)
+ }
+
+ private fun maybeLogPerf(tree: DomTree) {
+ if (!perfDebug) return
+ val now = System.currentTimeMillis()
+ if (now - lastPerfLogMs < 2_000L) return
+ lastPerfLogMs = now
+ val paintStats = tree.paintStats()
+ val styleStats = StyleEngine.lastStyleApplyReport()
+ println(
+ "[DSGL-PERF] frames=${paintStats.frames} commandRebuilds=${paintStats.commandRebuilds} " +
+ "chunkVisited=${paintStats.chunkNodesVisitedLastFrame} chunkRebuilt=${paintStats.chunkNodesRebuiltLastFrame} " +
+ "styled=${styleStats.visitedNodes} styleCacheHit=${styleStats.cacheHits} " +
+ "styleRecomputed=${styleStats.recomputedNodes} blankGuardSkips=$blankFrameGuardSkips"
+ )
+ }
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/GlUtils.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/GlUtils.kt
new file mode 100644
index 0000000..6c0c1d0
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/GlUtils.kt
@@ -0,0 +1,92 @@
+package org.dreamfinity.dsgl.mcNeoforge1211
+
+import net.minecraft.client.renderer.RenderHelper
+import org.lwjgl.opengl.GL11
+
+/**
+ * Executes a block with an OpenGL matrix/attribute stack push/pop.
+ */
+inline fun withStack(
+ attributes: List,
+ block: () -> Unit
+) {
+ val attributesBitMask = attributes.reduce { a, b -> a or b }
+ withStack(attributesBitMask) { block() }
+}
+
+/**
+ * Executes a block with an OpenGL matrix/attribute stack push/pop.
+ */
+inline fun withStack(
+ attributesBitMask: Int = 0,
+ block: () -> Unit
+) {
+ if (attributesBitMask > 0) {
+ GL11.glPushAttrib(attributesBitMask)
+ }
+ GL11.glPushMatrix()
+ try {
+ block()
+ } finally {
+ GL11.glPopMatrix()
+ if (attributesBitMask > 0) {
+ GL11.glPopAttrib()
+ }
+ }
+}
+
+/**
+ * Enables and disables GL capabilities for the duration of [block].
+ */
+inline fun withAttributes(
+ enable: List = emptyList(),
+ disable: List = emptyList(),
+ block: () -> Unit
+) {
+ for (capability in enable) {
+ GL11.glEnable(capability)
+ }
+ for (capability in disable) {
+ GL11.glDisable(capability)
+ }
+ try {
+ block()
+ } finally {
+ for (capability in enable) {
+ GL11.glDisable(capability)
+ }
+ for (capability in disable) {
+ GL11.glEnable(capability)
+ }
+ }
+}
+
+/**
+ * Executes a block with an OpenGL matrix/attribute stack push/pop.
+ */
+inline fun withAttributes(
+ enableBitMask: Int? = null,
+ disableBitMask: Int? = null,
+ block: () -> Unit
+) {
+ enableBitMask?.let { GL11.glEnable(it) }
+ disableBitMask?.let { GL11.glDisable(it) }
+ try {
+ block()
+ } finally {
+ enableBitMask?.let { GL11.glDisable(it) }
+ disableBitMask?.let { GL11.glEnable(it) }
+ }
+}
+
+/**
+ * Runs [block] with standard item lighting enabled.
+ */
+inline fun withItemGuiLightning(block: () -> Unit) {
+ RenderHelper.enableGUIStandardItemLighting()
+ try {
+ block()
+ } finally {
+ RenderHelper.disableStandardItemLighting()
+ }
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/Mc1211UiAdapter.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/Mc1211UiAdapter.kt
new file mode 100644
index 0000000..39f2171
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/Mc1211UiAdapter.kt
@@ -0,0 +1,1587 @@
+package org.dreamfinity.dsgl.mcNeoforge1211
+
+import com.mojang.blaze3d.platform.NativeImage
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.Gui
+import net.minecraft.client.renderer.entity.ItemRenderer
+import net.minecraft.client.renderer.texture.DynamicTexture
+import net.minecraft.resources.ResourceLocation
+import net.minecraft.world.item.BlockItem
+import net.minecraft.world.item.ItemStack
+import org.dreamfinity.dsgl.core.dom.layout.FontLineMetrics
+import org.dreamfinity.dsgl.core.dom.layout.UiMeasureContext
+import org.dreamfinity.dsgl.core.font.FontRegistry
+import org.dreamfinity.dsgl.core.host.Viewport
+import org.dreamfinity.dsgl.core.host.dsglRectToGlScissor
+import org.dreamfinity.dsgl.core.render.RenderCommand
+import org.dreamfinity.dsgl.mcNeoforge1211.scissorsHelper.ScissorContext
+import org.dreamfinity.dsgl.mcNeoforge1211.text.MsdfTextRenderer
+import org.lwjgl.BufferUtils
+import org.lwjgl.opengl.*
+import java.io.File
+import java.net.URL
+
+/**
+ * Minecraft 1.21.1 adapter that turns DSGL render commands into Minecraft calls.
+ */
+class Mc1211UiAdapter(private val mc: Minecraft, var paintsCount: Long = 0L) : UiMeasureContext {
+ private enum class ReadbackApi {
+ OpenGl30,
+ ArbFramebufferObject,
+ ExtFramebufferObject,
+ Legacy
+ }
+
+ private data class ReadbackBindingState(
+ val readFramebufferBinding: Int,
+ val drawFramebufferBinding: Int,
+ val framebufferBinding: Int,
+ val currentReadBuffer: Int
+ ) {
+ val usingFramebufferObject: Boolean
+ get() = readFramebufferBinding != 0
+ }
+
+ private data class ReadbackSetup(
+ val previousReadBuffer: Int,
+ val appliedReadBuffer: Int,
+ val shouldRestore: Boolean
+ )
+
+ private data class FramebufferBindingSnapshot(
+ val readFramebufferBinding: Int,
+ val drawFramebufferBinding: Int,
+ val framebufferBinding: Int
+ )
+
+ private data class SceneTextureSource(
+ val textureId: Int,
+ val textureWidth: Int,
+ val textureHeight: Int
+ )
+
+ private data class MagnifierCaptureShader(
+ val programId: Int,
+ val sourceTextureUniform: Int,
+ val sourceOriginUniform: Int,
+ val sourceSizeUniform: Int,
+ val viewportSizeUniform: Int,
+ val sourceTextureSizeUniform: Int,
+ val fallbackColorUniform: Int
+ )
+
+ companion object {
+ private val imageCache: MutableMap = HashMap()
+ private val dynamicTexturesCache: MutableMap = HashMap()
+ private val MAGNIFIER_CAPTURE_VERTEX_SHADER: String = """
+ #version 120
+ varying vec2 vUv;
+ void main() {
+ gl_Position = gl_Vertex;
+ vUv = gl_MultiTexCoord0.xy;
+ }
+ """.trimIndent()
+ private val MAGNIFIER_CAPTURE_FRAGMENT_SHADER: String = """
+ #version 120
+ uniform sampler2D uSourceTexture;
+ uniform vec2 uSourceOriginTopLeft;
+ uniform vec2 uSourceSize;
+ uniform vec2 uViewportSize;
+ uniform vec2 uSourceTextureSize;
+ uniform vec4 uFallbackColor;
+ varying vec2 vUv;
+ void main() {
+ vec2 dstPixel = floor(vUv * uSourceSize);
+ float sourceX = uSourceOriginTopLeft.x + dstPixel.x;
+ float sourceYTop = uSourceOriginTopLeft.y + (uSourceSize.y - 1.0 - dstPixel.y);
+ bool inside =
+ sourceX >= 0.0 &&
+ sourceYTop >= 0.0 &&
+ sourceX < uViewportSize.x &&
+ sourceYTop < uViewportSize.y;
+ if (!inside) {
+ gl_FragColor = uFallbackColor;
+ return;
+ }
+ float sourceYBottom = (uViewportSize.y - 1.0) - sourceYTop;
+ vec2 sourceUv = vec2(
+ (sourceX + 0.5) / uSourceTextureSize.x,
+ (sourceYBottom + 0.5) / uSourceTextureSize.y
+ );
+ vec4 sampled = texture2D(uSourceTexture, sourceUv);
+ gl_FragColor = vec4(sampled.rgb, 1.0);
+ }
+ """.trimIndent()
+ }
+
+ private val itemRenderer: ItemRenderer
+ get() = Minecraft.getInstance().itemRenderer
+ private val textRenderer: MsdfTextRenderer = MsdfTextRenderer()
+ private val opacityStack: MutableList = ArrayList(8)
+ private var opacityMultiplier: Float = 1f
+ private val errorLogTimes: MutableMap = linkedMapOf()
+ private val readbackDiagnosticsVerbose: Boolean = java.lang.Boolean.getBoolean("dsgl.readback.diagnostics.verbose")
+ private val readbackApi: ReadbackApi by lazy(LazyThreadSafetyMode.NONE) { resolveReadbackApi() }
+
+ private val samplePixelBuffer = BufferUtils.createByteBuffer(4)
+ private var sampleAreaBuffer = BufferUtils.createByteBuffer(4 * 256)
+ private val glIntStateQueryBuffer = BufferUtils.createIntBuffer(16)
+ private val glFloatStateQueryBuffer = BufferUtils.createFloatBuffer(16)
+
+ private var capturedRegionTextureId: Int = 0
+ private var capturedRegionFramebufferId: Int = 0
+ private var capturedRegionWidth: Int = 0
+ private var capturedRegionHeight: Int = 0
+ private var capturedRegionValid: Boolean = false
+ private var capturedRegionFallbackColor: Int = 0xFF000000.toInt()
+ private var magnifierCaptureShader: MagnifierCaptureShader? = null
+ private var magnifierCaptureShaderInitFailed: Boolean = false
+
+ private val checkerTextureCache: LinkedHashMap = LinkedHashMap(16, 0.75f, true)
+ private val checkerTextureUploadBuffer = BufferUtils.createByteBuffer(16)
+ private val maxCheckerTextures: Int = 32
+
+ private var cachedViewport: Viewport = Viewport(width = 1, height = 1, scale = 1f, x = 0, y = 0)
+ private var cachedDisplayWidth: Int = -1
+ private var cachedDisplayHeight: Int = -1
+
+ override fun measureText(text: String): Int = textRenderer.measureText(text, null, null)
+ override fun measureText(text: String, fontId: String?, fontSize: Int?): Int {
+ return textRenderer.measureText(text, fontId, fontSize)
+ }
+ override fun measureTextRange(
+ text: String,
+ startIndex: Int,
+ endIndexExclusive: Int,
+ fontId: String?,
+ fontSize: Int?
+ ): Int {
+ return textRenderer.measureTextRange(text, startIndex, endIndexExclusive, fontId, fontSize)
+ }
+
+ override val fontHeight: Int
+ get() = textRenderer.lineHeight(FontRegistry.DEFAULT_FONT_ID, null)
+
+ override fun fontHeight(fontId: String?, fontSize: Int?): Int {
+ return textRenderer.lineHeight(fontId, fontSize)
+ }
+
+ override fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics? {
+ return textRenderer.fontLineMetrics(fontId, fontSize)
+ }
+
+ fun viewport(): Viewport {
+ val displayWidth = mc.window.width.coerceAtLeast(1)
+ val displayHeight = mc.window.height.coerceAtLeast(1)
+ if (displayWidth != cachedDisplayWidth || displayHeight != cachedDisplayHeight) {
+ cachedDisplayWidth = displayWidth
+ cachedDisplayHeight = displayHeight
+ cachedViewport = Viewport(
+ width = displayWidth,
+ height = displayHeight,
+ scale = 1f,
+ x = 0,
+ y = 0
+ )
+ }
+ return cachedViewport
+ }
+
+ fun sampleScreenColor(x: Int, y: Int): Int? {
+ val viewport = viewport()
+ if (x < 0 || y < 0 || x >= viewport.width || y >= viewport.height) return null
+ val readY = viewport.height - 1 - y
+ samplePixelBuffer.clear()
+ return try {
+ val setup = beginReadback()
+ if (readbackDiagnosticsVerbose) {
+ diagnoseReadbackSource(
+ path = "sampleScreenColor",
+ sourceX = x,
+ sourceY = y,
+ sourceWidth = 1,
+ sourceHeight = 1,
+ setup = setup
+ )
+ }
+ try {
+ GL11.glReadPixels(x, readY, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, samplePixelBuffer)
+ } finally {
+ endReadback(setup)
+ }
+ val r = samplePixelBuffer.get(0).toInt() and 0xFF
+ val g = samplePixelBuffer.get(1).toInt() and 0xFF
+ val b = samplePixelBuffer.get(2).toInt() and 0xFF
+ val a = samplePixelBuffer.get(3).toInt() and 0xFF
+ (a shl 24) or (r shl 16) or (g shl 8) or b
+ } catch (_: Throwable) {
+ null
+ }
+ }
+
+ fun sampleScreenArea(x: Int, y: Int, width: Int, height: Int, outArgb: IntArray): Boolean {
+ if (width <= 0 || height <= 0) return false
+ val required = width * height
+ if (outArgb.size < required) return false
+ val viewport = viewport()
+ var i = 0
+ while (i < required) {
+ outArgb[i] = 0
+ i++
+ }
+
+ val srcX = x.coerceIn(0, viewport.width)
+ val srcY = y.coerceIn(0, viewport.height)
+ val maxW = viewport.width - srcX
+ val maxH = viewport.height - srcY
+ val srcW = minOf(width, maxW).coerceAtLeast(0)
+ val srcH = minOf(height, maxH).coerceAtLeast(0)
+ if (srcW <= 0 || srcH <= 0) return false
+
+ val byteCount = srcW * srcH * 4
+ if (sampleAreaBuffer.capacity() < byteCount) {
+ sampleAreaBuffer = BufferUtils.createByteBuffer(byteCount)
+ }
+ sampleAreaBuffer.clear()
+ sampleAreaBuffer.limit(byteCount)
+ return try {
+ val readY = viewport.height - (srcY + srcH)
+ val setup = beginReadback()
+ if (readbackDiagnosticsVerbose) {
+ diagnoseReadbackSource(
+ path = "sampleScreenArea",
+ sourceX = srcX,
+ sourceY = srcY,
+ sourceWidth = srcW,
+ sourceHeight = srcH,
+ setup = setup
+ )
+ }
+ try {
+ GL11.glReadPixels(srcX, readY, srcW, srcH, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, sampleAreaBuffer)
+ } finally {
+ endReadback(setup)
+ }
+ val dstOffsetX = (srcX - x).coerceAtLeast(0)
+ val dstOffsetY = (srcY - y).coerceAtLeast(0)
+ var row = 0
+ while (row < srcH) {
+ val glRow = srcH - 1 - row
+ var col = 0
+ while (col < srcW) {
+ val srcIndex = (glRow * srcW + col) * 4
+ val r = sampleAreaBuffer.get(srcIndex).toInt() and 0xFF
+ val g = sampleAreaBuffer.get(srcIndex + 1).toInt() and 0xFF
+ val b = sampleAreaBuffer.get(srcIndex + 2).toInt() and 0xFF
+ val a = sampleAreaBuffer.get(srcIndex + 3).toInt() and 0xFF
+ val dstX = dstOffsetX + col
+ val dstY = dstOffsetY + row
+ outArgb[dstY * width + dstX] = (a shl 24) or (r shl 16) or (g shl 8) or b
+ col++
+ }
+ row++
+ }
+ true
+ } catch (_: Throwable) {
+ false
+ }
+ }
+
+ private fun captureScreenRegion(command: RenderCommand.CaptureScreenRegion, viewport: Viewport) {
+ val sourceWidth = command.sourceWidth.coerceAtLeast(1)
+ val sourceHeight = command.sourceHeight.coerceAtLeast(1)
+ capturedRegionFallbackColor = command.fallbackColor
+ ensureCapturedRegionTexture(sourceWidth, sourceHeight)
+ if (capturedRegionTextureId == 0) {
+ capturedRegionValid = false
+ return
+ }
+ val sceneTextureSource = resolveActiveSceneTextureSource()
+ val shader = ensureMagnifierCaptureShader()
+ if (sceneTextureSource == null || shader == null || sceneTextureSource.textureId == capturedRegionTextureId) {
+ capturedRegionValid = fillCapturedRegionFallbackTexture(command.fallbackColor, sourceWidth, sourceHeight)
+ if (readbackDiagnosticsVerbose) {
+ logRateLimited(
+ key = "magnifier:capture:fallback",
+ message = "[DSGL-Magnifier] Falling back to solid fill preview. sourceTexture=${sceneTextureSource?.textureId ?: 0} shaderReady=${shader != null}"
+ )
+ }
+ return
+ }
+ val framebufferSnapshot = snapshotFramebufferBindings()
+ val previousReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER)
+ val previousDrawBuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER)
+ val previousTextureBinding = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D)
+ snapshotViewportState()
+ val previousViewportX = viewportXFromSnapshot()
+ val previousViewportY = viewportYFromSnapshot()
+ val previousViewportWidth = viewportWidthFromSnapshot()
+ val previousViewportHeight = viewportHeightFromSnapshot()
+ var renderingSucceeded = false
+ try {
+ if (!ensureCapturedRegionFramebuffer()) {
+ return
+ }
+ bindDrawFramebuffer(capturedRegionFramebufferId)
+ attachCapturedRegionTextureToFramebuffer()
+ if (!isCurrentFramebufferComplete()) {
+ return
+ }
+ val attachment = defaultColorAttachmentReadBuffer()
+ GL11.glDrawBuffer(attachment)
+ GL11.glReadBuffer(attachment)
+ GL11.glViewport(0, 0, sourceWidth, sourceHeight)
+ GL11.glDisable(GL11.GL_SCISSOR_TEST)
+ GL11.glDisable(GL11.GL_BLEND)
+ GL11.glDisable(GL11.GL_CULL_FACE)
+ GL11.glDisable(GL11.GL_DEPTH_TEST)
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ ARBShaderObjects.glUseProgramObjectARB(shader.programId)
+ GL13.glActiveTexture(GL13.GL_TEXTURE0)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, sceneTextureSource.textureId)
+ ARBShaderObjects.glUniform1iARB(shader.sourceTextureUniform, 0)
+ ARBShaderObjects.glUniform2fARB(
+ shader.sourceOriginUniform,
+ command.sourceX.toFloat(),
+ command.sourceY.toFloat()
+ )
+ ARBShaderObjects.glUniform2fARB(shader.sourceSizeUniform, sourceWidth.toFloat(), sourceHeight.toFloat())
+ ARBShaderObjects.glUniform2fARB(
+ shader.viewportSizeUniform,
+ viewport.width.toFloat(),
+ viewport.height.toFloat()
+ )
+ ARBShaderObjects.glUniform2fARB(
+ shader.sourceTextureSizeUniform,
+ sceneTextureSource.textureWidth.toFloat(),
+ sceneTextureSource.textureHeight.toFloat()
+ )
+ val fallbackAlpha = ((command.fallbackColor ushr 24) and 0xFF) / 255f
+ val fallbackRed = ((command.fallbackColor ushr 16) and 0xFF) / 255f
+ val fallbackGreen = ((command.fallbackColor ushr 8) and 0xFF) / 255f
+ val fallbackBlue = (command.fallbackColor and 0xFF) / 255f
+ ARBShaderObjects.glUniform4fARB(
+ shader.fallbackColorUniform,
+ fallbackRed,
+ fallbackGreen,
+ fallbackBlue,
+ fallbackAlpha
+ )
+ GL11.glColor4f(1f, 1f, 1f, 1f)
+ GL11.glBegin(GL11.GL_QUADS)
+ GL11.glTexCoord2f(0f, 0f)
+ GL11.glVertex2f(-1f, -1f)
+ GL11.glTexCoord2f(1f, 0f)
+ GL11.glVertex2f(1f, -1f)
+ GL11.glTexCoord2f(1f, 1f)
+ GL11.glVertex2f(1f, 1f)
+ GL11.glTexCoord2f(0f, 1f)
+ GL11.glVertex2f(-1f, 1f)
+ GL11.glEnd()
+ renderingSucceeded = true
+ } catch (error: Throwable) {
+ if (readbackDiagnosticsVerbose) {
+ logRateLimited(
+ key = "magnifier:capture:error",
+ message = "[DSGL-Magnifier] GPU capture failed: ${error.message ?: error::class.java.simpleName}"
+ )
+ }
+ } finally {
+ ARBShaderObjects.glUseProgramObjectARB(0)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, previousTextureBinding)
+ GL11.glReadBuffer(previousReadBuffer)
+ GL11.glDrawBuffer(previousDrawBuffer)
+ restoreFramebufferBindings(framebufferSnapshot)
+ GL11.glViewport(
+ previousViewportX,
+ previousViewportY,
+ previousViewportWidth,
+ previousViewportHeight
+ )
+ }
+ capturedRegionValid =
+ renderingSucceeded || fillCapturedRegionFallbackTexture(command.fallbackColor, sourceWidth, sourceHeight)
+ }
+
+ private fun drawCapturedScreenRegion(command: RenderCommand.DrawCapturedScreenRegion) {
+ if (command.width <= 0 || command.height <= 0) return
+ if (!capturedRegionValid || capturedRegionTextureId == 0) {
+ Gui.drawRect(
+ command.x,
+ command.y,
+ command.x + command.width,
+ command.y + command.height,
+ applyOpacity(capturedRegionFallbackColor)
+ )
+ return
+ }
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ GL11.glDisable(GL11.GL_CULL_FACE)
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, capturedRegionTextureId)
+ GL11.glColor4f(1f, 1f, 1f, opacityMultiplier.coerceIn(0f, 1f))
+ GL11.glBegin(GL11.GL_QUADS)
+ GL11.glTexCoord2f(0f, 1f)
+ GL11.glVertex2f(command.x.toFloat(), command.y.toFloat())
+ GL11.glTexCoord2f(1f, 1f)
+ GL11.glVertex2f((command.x + command.width).toFloat(), command.y.toFloat())
+ GL11.glTexCoord2f(1f, 0f)
+ GL11.glVertex2f((command.x + command.width).toFloat(), (command.y + command.height).toFloat())
+ GL11.glTexCoord2f(0f, 0f)
+ GL11.glVertex2f(command.x.toFloat(), (command.y + command.height).toFloat())
+ GL11.glEnd()
+ }
+
+ private fun drawCheckerboard(command: RenderCommand.DrawCheckerboard) {
+ if (command.width <= 0 || command.height <= 0) return
+ val cellSize = command.cellSize.coerceAtLeast(1)
+ val textureId = resolveCheckerTextureId(command.lightColor, command.darkColor)
+ if (textureId == 0) {
+ Gui.drawRect(
+ command.x,
+ command.y,
+ command.x + command.width,
+ command.y + command.height,
+ applyOpacity(command.lightColor)
+ )
+ return
+ }
+ val patternSize = (cellSize * 2f).coerceAtLeast(1f)
+ val u0 = (command.x + command.offsetX) / patternSize
+ val v0 = (command.y + command.offsetY) / patternSize
+ val u1 = u0 + (command.width / patternSize)
+ val v1 = v0 + (command.height / patternSize)
+
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ GL11.glDisable(GL11.GL_CULL_FACE)
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId)
+ GL11.glColor4f(1f, 1f, 1f, opacityMultiplier.coerceIn(0f, 1f))
+ GL11.glBegin(GL11.GL_QUADS)
+ GL11.glTexCoord2f(u0, v0)
+ GL11.glVertex2f(command.x.toFloat(), command.y.toFloat())
+ GL11.glTexCoord2f(u1, v0)
+ GL11.glVertex2f((command.x + command.width).toFloat(), command.y.toFloat())
+ GL11.glTexCoord2f(u1, v1)
+ GL11.glVertex2f((command.x + command.width).toFloat(), (command.y + command.height).toFloat())
+ GL11.glTexCoord2f(u0, v1)
+ GL11.glVertex2f(command.x.toFloat(), (command.y + command.height).toFloat())
+ GL11.glEnd()
+ }
+
+ private fun resolveCheckerTextureId(lightColor: Int, darkColor: Int): Int {
+ val key = checkerTextureKey(lightColor, darkColor)
+ checkerTextureCache[key]?.let { return it }
+
+ val textureId = GL11.glGenTextures()
+ if (textureId == 0) return 0
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT)
+
+ val light = argbToRgbaBytes(lightColor)
+ val dark = argbToRgbaBytes(darkColor)
+ checkerTextureUploadBuffer.clear()
+ checkerTextureUploadBuffer.put(light[0]).put(light[1]).put(light[2]).put(light[3])
+ checkerTextureUploadBuffer.put(dark[0]).put(dark[1]).put(dark[2]).put(dark[3])
+ checkerTextureUploadBuffer.put(dark[0]).put(dark[1]).put(dark[2]).put(dark[3])
+ checkerTextureUploadBuffer.put(light[0]).put(light[1]).put(light[2]).put(light[3])
+ checkerTextureUploadBuffer.flip()
+
+ GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1)
+ GL11.glTexImage2D(
+ GL11.GL_TEXTURE_2D,
+ 0,
+ GL11.GL_RGBA,
+ 2,
+ 2,
+ 0,
+ GL11.GL_RGBA,
+ GL11.GL_UNSIGNED_BYTE,
+ checkerTextureUploadBuffer
+ )
+
+ checkerTextureCache[key] = textureId
+ while (checkerTextureCache.size > maxCheckerTextures) {
+ val eldest = checkerTextureCache.entries.iterator().next()
+ GL11.glDeleteTextures(eldest.value)
+ checkerTextureCache.remove(eldest.key)
+ }
+ return textureId
+ }
+
+ private fun checkerTextureKey(lightColor: Int, darkColor: Int): Long {
+ return (lightColor.toLong() shl 32) xor (darkColor.toLong() and 0xFFFF_FFFFL)
+ }
+
+ private fun ensureCapturedRegionTexture(width: Int, height: Int) {
+ if (capturedRegionTextureId == 0) {
+ capturedRegionTextureId = GL11.glGenTextures()
+ if (capturedRegionTextureId == 0) return
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, capturedRegionTextureId)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP)
+ capturedRegionWidth = 0
+ capturedRegionHeight = 0
+ }
+ if (capturedRegionWidth == width && capturedRegionHeight == height) return
+ capturedRegionWidth = width
+ capturedRegionHeight = height
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, capturedRegionTextureId)
+ GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1)
+ GL11.glTexImage2D(
+ GL11.GL_TEXTURE_2D,
+ 0,
+ GL11.GL_RGBA,
+ capturedRegionWidth,
+ capturedRegionHeight,
+ 0,
+ GL11.GL_RGBA,
+ GL11.GL_UNSIGNED_BYTE,
+ null as java.nio.ByteBuffer?
+ )
+ }
+
+ private fun ensureCapturedRegionFramebuffer(): Boolean {
+ if (capturedRegionFramebufferId != 0) return true
+ capturedRegionFramebufferId = generateFramebufferObject()
+ return capturedRegionFramebufferId != 0
+ }
+
+ private fun resolveActiveSceneTextureSource(): SceneTextureSource? {
+ val state = detectReadbackBindingState()
+ if (!state.usingFramebufferObject) return null
+ val colorAttachment = if (isColorAttachmentReadBuffer(state.currentReadBuffer)) {
+ state.currentReadBuffer
+ } else {
+ defaultColorAttachmentReadBuffer()
+ }
+ val objectType = getFramebufferAttachmentObjectType(colorAttachment)
+ if (objectType != GL11.GL_TEXTURE) return null
+ val textureId = getFramebufferAttachmentObjectName(colorAttachment)
+ if (textureId <= 0) return null
+ val previousTextureBinding = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D)
+ return try {
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId)
+ val textureWidth = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_WIDTH)
+ val textureHeight = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_HEIGHT)
+ if (textureWidth <= 0 || textureHeight <= 0) return null
+ SceneTextureSource(textureId = textureId, textureWidth = textureWidth, textureHeight = textureHeight)
+ } finally {
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, previousTextureBinding)
+ }
+ }
+
+ private fun getFramebufferAttachmentObjectType(colorAttachment: Int): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.glGetFramebufferAttachmentParameteri(
+ GL30.GL_READ_FRAMEBUFFER,
+ colorAttachment,
+ GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE
+ )
+
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glGetFramebufferAttachmentParameteri(
+ ARBFramebufferObject.GL_READ_FRAMEBUFFER,
+ colorAttachment,
+ ARBFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE
+ )
+
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glGetFramebufferAttachmentParameteriEXT(
+ EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
+ colorAttachment,
+ EXTFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE_EXT
+ )
+
+ ReadbackApi.Legacy -> GL11.GL_NONE
+ }
+ }
+
+ private fun getFramebufferAttachmentObjectName(colorAttachment: Int): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.glGetFramebufferAttachmentParameteri(
+ GL30.GL_READ_FRAMEBUFFER,
+ colorAttachment,
+ GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME
+ )
+
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glGetFramebufferAttachmentParameteri(
+ ARBFramebufferObject.GL_READ_FRAMEBUFFER,
+ colorAttachment,
+ ARBFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME
+ )
+
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glGetFramebufferAttachmentParameteriEXT(
+ EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
+ colorAttachment,
+ EXTFramebufferObject.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT
+ )
+
+ ReadbackApi.Legacy -> 0
+ }
+ }
+
+ private fun ensureMagnifierCaptureShader(): MagnifierCaptureShader? {
+ magnifierCaptureShader?.let { return it }
+ if (magnifierCaptureShaderInitFailed) return null
+ return try {
+ val vertexShader = compileShaderObject(
+ type = ARBVertexShader.GL_VERTEX_SHADER_ARB,
+ source = MAGNIFIER_CAPTURE_VERTEX_SHADER
+ )
+ val fragmentShader = compileShaderObject(
+ type = ARBFragmentShader.GL_FRAGMENT_SHADER_ARB,
+ source = MAGNIFIER_CAPTURE_FRAGMENT_SHADER
+ )
+ val program = ARBShaderObjects.glCreateProgramObjectARB()
+ ARBShaderObjects.glAttachObjectARB(program, vertexShader)
+ ARBShaderObjects.glAttachObjectARB(program, fragmentShader)
+ ARBShaderObjects.glLinkProgramARB(program)
+ val linkStatus = ARBShaderObjects.glGetObjectParameteriARB(
+ program,
+ ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB
+ )
+ if (linkStatus == GL11.GL_FALSE) {
+ val info = ARBShaderObjects.glGetInfoLogARB(program, 4096)
+ throw IllegalStateException("Magnifier shader link failed: $info")
+ }
+ val shader = MagnifierCaptureShader(
+ programId = program,
+ sourceTextureUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceTexture"),
+ sourceOriginUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceOriginTopLeft"),
+ sourceSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceSize"),
+ viewportSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uViewportSize"),
+ sourceTextureSizeUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uSourceTextureSize"),
+ fallbackColorUniform = ARBShaderObjects.glGetUniformLocationARB(program, "uFallbackColor")
+ )
+ magnifierCaptureShader = shader
+ shader
+ } catch (error: Throwable) {
+ magnifierCaptureShaderInitFailed = true
+ if (readbackDiagnosticsVerbose) {
+ logRateLimited(
+ key = "magnifier:shader:init",
+ message = "[DSGL-Magnifier] Failed to initialize capture shader: ${error.message ?: error::class.java.simpleName}"
+ )
+ }
+ null
+ }
+ }
+
+ private fun compileShaderObject(type: Int, source: String): Int {
+ val shader = ARBShaderObjects.glCreateShaderObjectARB(type)
+ ARBShaderObjects.glShaderSourceARB(shader, source)
+ ARBShaderObjects.glCompileShaderARB(shader)
+ val compileStatus = ARBShaderObjects.glGetObjectParameteriARB(
+ shader,
+ ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB
+ )
+ if (compileStatus == GL11.GL_FALSE) {
+ val info = ARBShaderObjects.glGetInfoLogARB(shader, 4096)
+ throw IllegalStateException("Magnifier shader compile failed: $info")
+ }
+ return shader
+ }
+
+ private fun generateFramebufferObject(): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.glGenFramebuffers()
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glGenFramebuffers()
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glGenFramebuffersEXT()
+ ReadbackApi.Legacy -> 0
+ }
+ }
+
+ private fun snapshotFramebufferBindings(): FramebufferBindingSnapshot {
+ return FramebufferBindingSnapshot(
+ readFramebufferBinding = currentReadFramebufferBinding(),
+ drawFramebufferBinding = currentDrawFramebufferBinding(),
+ framebufferBinding = currentFramebufferBinding()
+ )
+ }
+
+ private fun restoreFramebufferBindings(snapshot: FramebufferBindingSnapshot) {
+ when (readbackApi) {
+ ReadbackApi.OpenGl30 -> {
+ GL30.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, snapshot.readFramebufferBinding)
+ GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, snapshot.drawFramebufferBinding)
+ }
+
+ ReadbackApi.ArbFramebufferObject -> {
+ ARBFramebufferObject.glBindFramebuffer(
+ ARBFramebufferObject.GL_READ_FRAMEBUFFER,
+ snapshot.readFramebufferBinding
+ )
+ ARBFramebufferObject.glBindFramebuffer(
+ ARBFramebufferObject.GL_DRAW_FRAMEBUFFER,
+ snapshot.drawFramebufferBinding
+ )
+ }
+
+ ReadbackApi.ExtFramebufferObject -> {
+ EXTFramebufferObject.glBindFramebufferEXT(
+ EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
+ snapshot.framebufferBinding
+ )
+ }
+
+ ReadbackApi.Legacy -> Unit
+ }
+ }
+
+ private fun bindDrawFramebuffer(framebufferId: Int) {
+ when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebufferId)
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glBindFramebuffer(
+ ARBFramebufferObject.GL_DRAW_FRAMEBUFFER,
+ framebufferId
+ )
+
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glBindFramebufferEXT(
+ EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
+ framebufferId
+ )
+
+ ReadbackApi.Legacy -> Unit
+ }
+ }
+
+ private fun attachCapturedRegionTextureToFramebuffer() {
+ when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.glFramebufferTexture2D(
+ GL30.GL_DRAW_FRAMEBUFFER,
+ GL30.GL_COLOR_ATTACHMENT0,
+ GL11.GL_TEXTURE_2D,
+ capturedRegionTextureId,
+ 0
+ )
+
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glFramebufferTexture2D(
+ ARBFramebufferObject.GL_DRAW_FRAMEBUFFER,
+ ARBFramebufferObject.GL_COLOR_ATTACHMENT0,
+ GL11.GL_TEXTURE_2D,
+ capturedRegionTextureId,
+ 0
+ )
+
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glFramebufferTexture2DEXT(
+ EXTFramebufferObject.GL_FRAMEBUFFER_EXT,
+ EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT,
+ GL11.GL_TEXTURE_2D,
+ capturedRegionTextureId,
+ 0
+ )
+
+ ReadbackApi.Legacy -> Unit
+ }
+ }
+
+ private fun isCurrentFramebufferComplete(): Boolean {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.glCheckFramebufferStatus(GL30.GL_DRAW_FRAMEBUFFER) == GL30.GL_FRAMEBUFFER_COMPLETE
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.glCheckFramebufferStatus(
+ ARBFramebufferObject.GL_DRAW_FRAMEBUFFER
+ ) == ARBFramebufferObject.GL_FRAMEBUFFER_COMPLETE
+
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.glCheckFramebufferStatusEXT(
+ EXTFramebufferObject.GL_FRAMEBUFFER_EXT
+ ) == EXTFramebufferObject.GL_FRAMEBUFFER_COMPLETE_EXT
+
+ ReadbackApi.Legacy -> false
+ }
+ }
+
+ private fun fillCapturedRegionFallbackTexture(
+ fallbackColor: Int,
+ width: Int,
+ height: Int
+ ): Boolean {
+ val snapshot = snapshotFramebufferBindings()
+ val previousReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER)
+ val previousDrawBuffer = GL11.glGetInteger(GL11.GL_DRAW_BUFFER)
+ snapshotViewportState()
+ val previousViewportX = viewportXFromSnapshot()
+ val previousViewportY = viewportYFromSnapshot()
+ val previousViewportWidth = viewportWidthFromSnapshot()
+ val previousViewportHeight = viewportHeightFromSnapshot()
+ snapshotClearColorState()
+ val previousClearRed = clearRedFromSnapshot()
+ val previousClearGreen = clearGreenFromSnapshot()
+ val previousClearBlue = clearBlueFromSnapshot()
+ val previousClearAlpha = clearAlphaFromSnapshot()
+ return try {
+ if (!ensureCapturedRegionFramebuffer()) return false
+ bindDrawFramebuffer(capturedRegionFramebufferId)
+ attachCapturedRegionTextureToFramebuffer()
+ if (!isCurrentFramebufferComplete()) return false
+ val attachment = defaultColorAttachmentReadBuffer()
+ GL11.glDrawBuffer(attachment)
+ GL11.glViewport(0, 0, width, height)
+ val alpha = ((fallbackColor ushr 24) and 0xFF) / 255f
+ val red = ((fallbackColor ushr 16) and 0xFF) / 255f
+ val green = ((fallbackColor ushr 8) and 0xFF) / 255f
+ val blue = (fallbackColor and 0xFF) / 255f
+ GL11.glClearColor(red, green, blue, alpha)
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT)
+ true
+ } catch (_: Throwable) {
+ false
+ } finally {
+ GL11.glClearColor(
+ previousClearRed,
+ previousClearGreen,
+ previousClearBlue,
+ previousClearAlpha
+ )
+ GL11.glReadBuffer(previousReadBuffer)
+ GL11.glDrawBuffer(previousDrawBuffer)
+ restoreFramebufferBindings(snapshot)
+ GL11.glViewport(
+ previousViewportX,
+ previousViewportY,
+ previousViewportWidth,
+ previousViewportHeight
+ )
+ }
+ }
+
+ private fun snapshotViewportState() {
+ glIntStateQueryBuffer.clear()
+ GL11.glGetIntegerv(GL11.GL_VIEWPORT, glIntStateQueryBuffer)
+ }
+
+ private fun viewportXFromSnapshot(): Int = glIntStateQueryBuffer.get(0)
+ private fun viewportYFromSnapshot(): Int = glIntStateQueryBuffer.get(1)
+ private fun viewportWidthFromSnapshot(): Int = glIntStateQueryBuffer.get(2)
+ private fun viewportHeightFromSnapshot(): Int = glIntStateQueryBuffer.get(3)
+
+ private fun snapshotClearColorState() {
+ glFloatStateQueryBuffer.clear()
+ GL11.glGetFloatv(GL11.GL_COLOR_CLEAR_VALUE, glFloatStateQueryBuffer)
+ }
+
+ private fun clearRedFromSnapshot(): Float = glFloatStateQueryBuffer.get(0)
+ private fun clearGreenFromSnapshot(): Float = glFloatStateQueryBuffer.get(1)
+ private fun clearBlueFromSnapshot(): Float = glFloatStateQueryBuffer.get(2)
+ private fun clearAlphaFromSnapshot(): Float = glFloatStateQueryBuffer.get(3)
+
+ private fun argbToRgbaBytes(argb: Int, forceOpaqueAlpha: Boolean = false): ByteArray {
+ val r = ((argb ushr 16) and 0xFF).toByte()
+ val g = ((argb ushr 8) and 0xFF).toByte()
+ val b = (argb and 0xFF).toByte()
+ val a = if (forceOpaqueAlpha) 0xFF.toByte() else ((argb ushr 24) and 0xFF).toByte()
+ return byteArrayOf(r, g, b, a)
+ }
+
+ // TODO NeoForge-1.21 blit (methods like drawRect) require GuiGraphics argument to be passed from host render method
+ // ItemStack rendering methods require PoseStack and BufferSource
+ /** Executes DSGL render commands using Minecraft rendering APIs. */
+ override fun paint(commands: List) {
+ paintsCount++
+ opacityStack.clear()
+ opacityMultiplier = 1f
+ val transformStack = RenderCommandTransformStack()
+ transformStack.reset()
+ val viewport = viewport()
+ GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS)
+ try {
+ ScissorContext.clear()
+ GL11.glDisable(GL11.GL_SCISSOR_TEST)
+ GL11.glViewport(viewport.x, viewport.y, viewport.width, viewport.height)
+ GL11.glMatrixMode(GL11.GL_PROJECTION)
+ GL11.glPushMatrix()
+ GL11.glLoadIdentity()
+ GL11.glOrtho(0.0, viewport.width.toDouble(), viewport.height.toDouble(), 0.0, -1000.0, 1000.0)
+ GL11.glMatrixMode(GL11.GL_MODELVIEW)
+ GL11.glPushMatrix()
+ GL11.glLoadIdentity()
+ GL11.glAlphaFunc(GL11.GL_GREATER, 0.0f)
+ try {
+ for (command in commands) {
+ when (command) {
+ is RenderCommand.DrawRect -> {
+ Gui.drawRect(
+ command.x,
+ command.y,
+ command.x + command.width,
+ command.y + command.height,
+ applyOpacity(command.color)
+ )
+ }
+
+ is RenderCommand.DrawColorField -> {
+ drawColorField(
+ x = command.x,
+ y = command.y,
+ width = command.width,
+ height = command.height,
+ hueDeg = command.hueDeg
+ )
+ }
+
+ is RenderCommand.DrawHueBar -> {
+ drawHueBar(
+ x = command.x,
+ y = command.y,
+ width = command.width,
+ height = command.height
+ )
+ }
+
+ is RenderCommand.DrawAlphaBar -> {
+ drawAlphaBar(
+ x = command.x,
+ y = command.y,
+ width = command.width,
+ height = command.height,
+ rgbColor = command.rgbColor
+ )
+ }
+
+ is RenderCommand.DrawCheckerboard -> {
+ drawCheckerboard(command)
+ }
+
+ is RenderCommand.DrawText -> {
+ try {
+ textRenderer.draw(
+ command = command,
+ opacityMultiplier = opacityMultiplier
+ )
+ } catch (error: LinkageError) {
+ logRateLimited(
+ key = "drawText:linkage",
+ message = "[DSGL] Skipping DrawText due linkage error in text renderer: ${error.message}"
+ )
+ } catch (error: Throwable) {
+ logRateLimited(
+ key = "drawText:runtime",
+ message = "[DSGL] Skipping DrawText due renderer error: ${error.message}"
+ )
+ }
+ }
+
+ is RenderCommand.DrawImage -> {
+ val location = resolveImage(command.resource) ?: continue
+ mc.textureManager.getTexture(location).bind()
+ GL11.glColor4f(1f, 1f, 1f, opacityMultiplier.coerceIn(0f, 1f))
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ Gui.drawModalRectWithCustomSizedTexture(
+ command.x,
+ command.y,
+ 0f,
+ 0f,
+ command.width,
+ command.height,
+ command.width.toFloat(),
+ command.height.toFloat()
+ )
+ }
+
+ is RenderCommand.CaptureScreenRegion -> {
+ captureScreenRegion(command, viewport)
+ }
+
+ is RenderCommand.DrawCapturedScreenRegion -> {
+ drawCapturedScreenRegion(command)
+ }
+
+ is RenderCommand.DrawItemStack -> {
+ val stack = (command.stack as? McItemStackRef)?.stack ?: continue
+ drawItemStack(
+ stack = stack,
+ x = command.x,
+ y = command.y,
+ size = command.size,
+ width = command.width,
+ rotY = command.rotYDeg,
+ rotX = command.rotXDeg
+ )
+ }
+
+ is RenderCommand.PushClip -> {
+ val transformedClip = transformStack.resolveClipRect(
+ x = command.x,
+ y = command.y,
+ width = command.width,
+ height = command.height
+ )
+ pushClip(
+ viewport = viewport,
+ guiX = transformedClip.x,
+ guiY = transformedClip.y,
+ guiWidth = transformedClip.width,
+ guiHeight = transformedClip.height
+ )
+ }
+
+ is RenderCommand.PopClip -> {
+ ScissorContext.pop()
+ }
+
+ is RenderCommand.PushTransform -> {
+ transformStack.push(command)
+ GL11.glPushMatrix()
+ GL11.glTranslatef(command.originX, command.originY, 0f)
+ GL11.glTranslatef(command.translateX, command.translateY, 0f)
+ GL11.glRotatef(command.rotateDeg, 0f, 0f, 1f)
+ GL11.glScalef(command.scaleX, command.scaleY, 1f)
+ GL11.glTranslatef(-command.originX, -command.originY, 0f)
+ }
+
+ is RenderCommand.PopTransform -> {
+ transformStack.pop()
+ GL11.glPopMatrix()
+ }
+
+ is RenderCommand.PushOpacity -> {
+ opacityStack.add(opacityMultiplier)
+ opacityMultiplier = (opacityMultiplier * command.opacity).coerceIn(0f, 1f)
+ }
+
+ is RenderCommand.PopOpacity -> {
+ opacityMultiplier =
+ if (opacityStack.isEmpty()) 1f else opacityStack.removeAt(opacityStack.lastIndex)
+ }
+ }
+ }
+ } finally {
+ GL11.glAlphaFunc(GL11.GL_GREATER, 0.1f)
+ GL11.glMatrixMode(GL11.GL_MODELVIEW)
+ GL11.glPopMatrix()
+ GL11.glMatrixMode(GL11.GL_PROJECTION)
+ GL11.glPopMatrix()
+ GL11.glMatrixMode(GL11.GL_MODELVIEW)
+ }
+ } finally {
+ ScissorContext.clear()
+ transformStack.reset()
+ opacityStack.clear()
+ opacityMultiplier = 1f
+ GL11.glPopAttrib()
+ }
+ }
+
+ private fun applyOpacity(color: Int): Int {
+ if (opacityMultiplier >= 0.999f) return color
+ val alpha = ((color ushr 24) and 0xFF)
+ val scaled = (alpha * opacityMultiplier).toInt().coerceIn(0, 255)
+ return (color and 0x00FF_FFFF) or (scaled shl 24)
+ }
+
+ private fun drawColorField(x: Int, y: Int, width: Int, height: Int, hueDeg: Float) {
+ if (width <= 0 || height <= 0) return
+
+ val normalizedHue = ((hueDeg % 360f) + 360f) % 360f
+ val hueColor = (hsvToArgbInt(normalizedHue, 1f, 1f) and 0x00FF_FFFF) or (0xFF shl 24)
+
+ drawGradientBlock {
+ drawHorizontalGradientRectRaw(
+ x, y, width, height,
+ applyOpacity(0xFFFFFFFF.toInt()),
+ applyOpacity(hueColor)
+ )
+ drawVerticalGradientRectRaw(
+ x, y, width, height,
+ applyOpacity(0x00000000),
+ applyOpacity(0xFF000000.toInt())
+ )
+ }
+ }
+
+ private fun drawHueBar(x: Int, y: Int, width: Int, height: Int) {
+ if (width <= 0 || height <= 0) return
+ val segments = 6
+ val hueStops = floatArrayOf(0f, 60f, 120f, 180f, 240f, 300f, 360f)
+ var index = 0
+ while (index < segments) {
+ val startX = x + (width * index) / segments
+ val endX = if (index == segments - 1) x + width else x + (width * (index + 1)) / segments
+ val segmentWidth = (endX - startX).coerceAtLeast(1)
+ val startColor = applyOpacity(hsvToArgbInt(hueStops[index], 1f, 1f))
+ val endColor = applyOpacity(hsvToArgbInt(hueStops[index + 1], 1f, 1f))
+ drawHorizontalGradientRect(startX, y, segmentWidth, height, startColor, endColor)
+ index += 1
+ }
+ }
+
+ private fun drawAlphaBar(x: Int, y: Int, width: Int, height: Int, rgbColor: Int) {
+ if (width <= 0 || height <= 0) return
+ val rgbOnly = rgbColor and 0x00FF_FFFF
+ val leftColor = applyOpacity(rgbOnly)
+ val rightColor = applyOpacity(rgbOnly or (0xFF shl 24))
+ drawHorizontalGradientRect(x, y, width, height, leftColor, rightColor)
+ }
+
+ private fun drawHorizontalGradientRect(x: Int, y: Int, width: Int, height: Int, leftColor: Int, rightColor: Int) {
+ if (width <= 0 || height <= 0) return
+ GL11.glDisable(GL11.GL_TEXTURE_2D)
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ GL11.glShadeModel(GL11.GL_SMOOTH)
+ drawHorizontalGradientRectRaw(x, y, width, height, leftColor, rightColor)
+ GL11.glShadeModel(GL11.GL_FLAT)
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ GL11.glColor4f(1f, 1f, 1f, 1f)
+ }
+
+ private fun drawVerticalGradientRect(x: Int, y: Int, width: Int, height: Int, topColor: Int, bottomColor: Int) {
+ if (width <= 0 || height <= 0) return
+ GL11.glDisable(GL11.GL_TEXTURE_2D)
+ GL11.glDisable(GL11.GL_ALPHA)
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ GL11.glShadeModel(GL11.GL_SMOOTH)
+ drawVerticalGradientRectRaw(x, y, width, height, topColor, bottomColor)
+ GL11.glEnable(GL11.GL_ALPHA)
+ GL11.glShadeModel(GL11.GL_FLAT)
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ GL11.glColor4f(1f, 1f, 1f, 1f)
+ }
+
+ private inline fun drawGradientBlock(block: () -> Unit) {
+ GL11.glDisable(GL11.GL_TEXTURE_2D)
+ GL11.glDisable(GL11.GL_DEPTH_TEST)
+ GL11.glDepthMask(false)
+ GL11.glDisable(GL11.GL_ALPHA_TEST)
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ GL11.glShadeModel(GL11.GL_SMOOTH)
+
+ block()
+
+ GL11.glShadeModel(GL11.GL_FLAT)
+ GL11.glDisable(GL11.GL_BLEND)
+ GL11.glEnable(GL11.GL_ALPHA_TEST)
+ GL11.glDepthMask(true)
+ GL11.glEnable(GL11.GL_DEPTH_TEST)
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ GL11.glColor4f(1f, 1f, 1f, 1f)
+ }
+
+ private fun drawHorizontalGradientRectRaw(
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ leftColor: Int,
+ rightColor: Int
+ ) {
+ GL11.glBegin(GL11.GL_QUADS)
+ glColor(leftColor)
+ GL11.glVertex2f(x.toFloat(), y.toFloat())
+ GL11.glVertex2f(x.toFloat(), (y + height).toFloat())
+ glColor(rightColor)
+ GL11.glVertex2f((x + width).toFloat(), (y + height).toFloat())
+ GL11.glVertex2f((x + width).toFloat(), y.toFloat())
+ GL11.glEnd()
+ }
+
+ private fun drawVerticalGradientRectRaw(
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ topColor: Int,
+ bottomColor: Int
+ ) {
+ GL11.glBegin(GL11.GL_QUADS)
+ glColor(topColor)
+ GL11.glVertex2f((x + width).toFloat(), y.toFloat())
+ GL11.glVertex2f(x.toFloat(), y.toFloat())
+ glColor(bottomColor)
+ GL11.glVertex2f(x.toFloat(), (y + height).toFloat())
+ GL11.glVertex2f((x + width).toFloat(), (y + height).toFloat())
+ GL11.glEnd()
+ }
+
+ private fun drawBilinearGradientRect(
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ topLeftColor: Int,
+ topRightColor: Int,
+ bottomRightColor: Int,
+ bottomLeftColor: Int
+ ) {
+ if (width <= 0 || height <= 0) return
+ GL11.glDisable(GL11.GL_TEXTURE_2D)
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ GL11.glShadeModel(GL11.GL_SMOOTH)
+ GL11.glBegin(GL11.GL_QUADS)
+ glColor(topLeftColor)
+ GL11.glVertex2f(x.toFloat(), y.toFloat())
+ glColor(bottomLeftColor)
+ GL11.glVertex2f(x.toFloat(), (y + height).toFloat())
+ glColor(bottomRightColor)
+ GL11.glVertex2f((x + width).toFloat(), (y + height).toFloat())
+ glColor(topRightColor)
+ GL11.glVertex2f((x + width).toFloat(), y.toFloat())
+ GL11.glEnd()
+ GL11.glShadeModel(GL11.GL_FLAT)
+ GL11.glEnable(GL11.GL_TEXTURE_2D)
+ GL11.glColor4f(1f, 1f, 1f, 1f)
+ }
+
+ private fun glColor(argb: Int) {
+ val a = ((argb ushr 24) and 0xFF) / 255f
+ val r = ((argb ushr 16) and 0xFF) / 255f
+ val g = ((argb ushr 8) and 0xFF) / 255f
+ val b = (argb and 0xFF) / 255f
+ GL11.glColor4f(r, g, b, a)
+ }
+
+ private fun hsvToArgbInt(hueDeg: Float, saturation: Float, value: Float): Int {
+ val h = ((hueDeg % 360f) + 360f) % 360f
+ val s = saturation.coerceIn(0f, 1f)
+ val v = value.coerceIn(0f, 1f)
+ val c = v * s
+ val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f))
+ val m = v - c
+ val (r1, g1, b1) = when {
+ h < 60f -> Triple(c, x, 0f)
+ h < 120f -> Triple(x, c, 0f)
+ h < 180f -> Triple(0f, c, x)
+ h < 240f -> Triple(0f, x, c)
+ h < 300f -> Triple(x, 0f, c)
+ else -> Triple(c, 0f, x)
+ }
+ val r = ((r1 + m) * 255f).toInt().coerceIn(0, 255)
+ val g = ((g1 + m) * 255f).toInt().coerceIn(0, 255)
+ val b = ((b1 + m) * 255f).toInt().coerceIn(0, 255)
+ return (0xFF shl 24) or (r shl 16) or (g shl 8) or b
+ }
+
+ private fun beginReadback(): ReadbackSetup {
+ val previousReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER)
+ val desiredReadBuffer = selectReadBufferForActiveTarget(previousReadBuffer)
+ if (desiredReadBuffer == previousReadBuffer) {
+ return ReadbackSetup(
+ previousReadBuffer = previousReadBuffer,
+ appliedReadBuffer = desiredReadBuffer,
+ shouldRestore = false
+ )
+ }
+ GL11.glReadBuffer(desiredReadBuffer)
+ return ReadbackSetup(
+ previousReadBuffer = previousReadBuffer,
+ appliedReadBuffer = desiredReadBuffer,
+ shouldRestore = true
+ )
+ }
+
+ private fun endReadback(setup: ReadbackSetup) {
+ if (!setup.shouldRestore) return
+ if (setup.previousReadBuffer == setup.appliedReadBuffer) return
+ GL11.glReadBuffer(setup.previousReadBuffer)
+ }
+
+ private fun selectReadBufferForActiveTarget(currentReadBuffer: Int): Int {
+ val readFramebufferBinding = currentReadFramebufferBinding()
+ if (readFramebufferBinding == 0) {
+ return GL11.GL_BACK
+ }
+ if (isColorAttachmentReadBuffer(currentReadBuffer)) {
+ return currentReadBuffer
+ }
+ return defaultColorAttachmentReadBuffer()
+ }
+
+ private fun resolveReadbackApi(): ReadbackApi {
+ val caps = GLContext.getCapabilities()
+ return when {
+ caps.OpenGL30 -> ReadbackApi.OpenGl30
+ caps.GL_ARB_framebuffer_object -> ReadbackApi.ArbFramebufferObject
+ caps.GL_EXT_framebuffer_object -> ReadbackApi.ExtFramebufferObject
+ else -> ReadbackApi.Legacy
+ }
+ }
+
+ private fun currentReadFramebufferBinding(): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING)
+ ReadbackApi.ArbFramebufferObject -> GL11.glGetInteger(ARBFramebufferObject.GL_READ_FRAMEBUFFER_BINDING)
+ ReadbackApi.ExtFramebufferObject -> GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT)
+ ReadbackApi.Legacy -> 0
+ }
+ }
+
+ private fun currentDrawFramebufferBinding(): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING)
+ ReadbackApi.ArbFramebufferObject -> GL11.glGetInteger(ARBFramebufferObject.GL_DRAW_FRAMEBUFFER_BINDING)
+ ReadbackApi.ExtFramebufferObject -> GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT)
+ ReadbackApi.Legacy -> 0
+ }
+ }
+
+ private fun currentFramebufferBinding(): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING)
+ ReadbackApi.ArbFramebufferObject -> GL11.glGetInteger(ARBFramebufferObject.GL_FRAMEBUFFER_BINDING)
+ ReadbackApi.ExtFramebufferObject -> GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT)
+ ReadbackApi.Legacy -> 0
+ }
+ }
+
+ private fun defaultColorAttachmentReadBuffer(): Int {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> GL30.GL_COLOR_ATTACHMENT0
+ ReadbackApi.ArbFramebufferObject -> ARBFramebufferObject.GL_COLOR_ATTACHMENT0
+ ReadbackApi.ExtFramebufferObject -> EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT
+ ReadbackApi.Legacy -> GL11.GL_BACK
+ }
+ }
+
+ private fun detectReadbackBindingState(): ReadbackBindingState {
+ val readFramebufferBinding = currentReadFramebufferBinding()
+ return ReadbackBindingState(
+ readFramebufferBinding = readFramebufferBinding,
+ drawFramebufferBinding = currentDrawFramebufferBinding(),
+ framebufferBinding = currentFramebufferBinding(),
+ currentReadBuffer = GL11.glGetInteger(GL11.GL_READ_BUFFER)
+ )
+ }
+
+ private fun diagnoseReadbackSource(
+ path: String,
+ sourceX: Int,
+ sourceY: Int,
+ sourceWidth: Int,
+ sourceHeight: Int,
+ setup: ReadbackSetup
+ ) {
+ val state = detectReadbackBindingState()
+ val recommended = selectReadBufferForActiveTarget(state.currentReadBuffer)
+ val appliedCompatible = isReadBufferCompatibleWithActiveTarget(setup.appliedReadBuffer, state)
+ val previousCompatible = isReadBufferCompatibleWithActiveTarget(setup.previousReadBuffer, state)
+ val message = buildString {
+ append("[DSGL-Readback] path=").append(path)
+ append(" src=(").append(sourceX).append(',').append(sourceY).append(' ')
+ append(sourceWidth).append('x').append(sourceHeight).append(')')
+ append(" readFbo=").append(state.readFramebufferBinding)
+ append(" drawFbo=").append(state.drawFramebufferBinding)
+ append(" fbo=").append(state.framebufferBinding)
+ append(" previousReadBuffer=").append(glEnumName(setup.previousReadBuffer))
+ append(" appliedReadBuffer=").append(glEnumName(setup.appliedReadBuffer))
+ append(" currentReadBuffer=").append(glEnumName(state.currentReadBuffer))
+ append(" changed=").append(setup.shouldRestore)
+ append(" previousCompatible=").append(previousCompatible)
+ append(" appliedCompatible=").append(appliedCompatible)
+ append(" recommendedReadBuffer=").append(glEnumName(recommended))
+ append(" api=").append(readbackApi.name)
+ }
+ logRateLimited(
+ key = "readback:$path:${state.readFramebufferBinding}:${setup.appliedReadBuffer}",
+ message = message
+ )
+ }
+
+ private fun isReadBufferCompatibleWithActiveTarget(readBuffer: Int, state: ReadbackBindingState): Boolean {
+ return if (state.usingFramebufferObject) {
+ readBuffer == GL11.GL_NONE || isColorAttachmentReadBuffer(readBuffer)
+ } else {
+ when (readBuffer) {
+ GL11.GL_BACK,
+ GL11.GL_FRONT,
+ GL11.GL_LEFT,
+ GL11.GL_RIGHT,
+ GL11.GL_FRONT_LEFT,
+ GL11.GL_FRONT_RIGHT,
+ GL11.GL_BACK_LEFT,
+ GL11.GL_BACK_RIGHT -> true
+
+ else -> false
+ }
+ }
+ }
+
+ private fun isColorAttachmentReadBuffer(readBuffer: Int): Boolean {
+ return when (readbackApi) {
+ ReadbackApi.OpenGl30 -> readBuffer in GL30.GL_COLOR_ATTACHMENT0..(GL30.GL_COLOR_ATTACHMENT0 + 31)
+ ReadbackApi.ArbFramebufferObject -> readBuffer in ARBFramebufferObject.GL_COLOR_ATTACHMENT0..(ARBFramebufferObject.GL_COLOR_ATTACHMENT0 + 15)
+ ReadbackApi.ExtFramebufferObject -> readBuffer in EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT..(EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT + 15)
+ ReadbackApi.Legacy -> false
+ }
+ }
+
+ private fun glEnumName(value: Int): String {
+ return when (value) {
+ GL11.GL_NONE -> "GL_NONE"
+ GL11.GL_FRONT -> "GL_FRONT"
+ GL11.GL_BACK -> "GL_BACK"
+ GL11.GL_LEFT -> "GL_LEFT"
+ GL11.GL_RIGHT -> "GL_RIGHT"
+ GL11.GL_FRONT_LEFT -> "GL_FRONT_LEFT"
+ GL11.GL_FRONT_RIGHT -> "GL_FRONT_RIGHT"
+ GL11.GL_BACK_LEFT -> "GL_BACK_LEFT"
+ GL11.GL_BACK_RIGHT -> "GL_BACK_RIGHT"
+ GL30.GL_COLOR_ATTACHMENT0,
+ ARBFramebufferObject.GL_COLOR_ATTACHMENT0,
+ EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT -> "GL_COLOR_ATTACHMENT0"
+ else -> {
+ val hex = Integer.toHexString(value).uppercase()
+ "0x$hex"
+ }
+ }
+ }
+
+ private fun logRateLimited(key: String, message: String) {
+ val now = System.currentTimeMillis()
+ val previous = errorLogTimes[key] ?: 0L
+ if (now - previous < 3_000L) return
+ errorLogTimes[key] = now
+ println(message)
+ }
+
+ private fun pushClip(viewport: Viewport, guiX: Int, guiY: Int, guiWidth: Int, guiHeight: Int) {
+ val scissor = viewport.dsglRectToGlScissor(guiX, guiY, guiWidth, guiHeight)
+ ScissorContext.push(scissor.x, scissor.y, scissor.width, scissor.height)
+ }
+
+ private fun isBlockStack(stack: ItemStack): Boolean {
+ return stack.item is BlockItem
+ }
+
+ private fun draw2DItem(stack: ItemStack, x: Int, y: Int, size: Int, maxWidth: Int) {
+ val drawX = x + ((maxWidth - size) / 2).coerceAtLeast(0)
+ withStack {
+ withAttributes(enable = listOf(GL11.GL_DEPTH_TEST)) {
+ val scale = size / 16.0f
+ GL11.glTranslatef(drawX.toFloat(), y.toFloat(), 0.0f)
+ GL11.glScalef(scale, scale, 1.0f)
+ itemRenderer.renderItemAndEffectIntoGUI(mc.font, mc.textureManager, stack, 0, 0)
+ }
+ }
+ }
+
+ private fun draw3DItem(stack: ItemStack, x: Int, y: Int, size: Int, width: Int, rotY: Double, rotX: Double) {
+ val scale = size / 16.0f
+ val drawX = x + ((width - size) / 2).coerceAtLeast(0)
+
+ withStack {
+ withAttributes(enable = listOf(GL11.GL_BLEND, GL11.GL_DEPTH_TEST, GL12.GL_RESCALE_NORMAL)) {
+ withItemGuiLightning {
+ GL11.glTranslated(drawX.toDouble(), y.toDouble(), 100.0)
+ GL11.glScaled(scale.toDouble(), scale.toDouble(), scale.toDouble())
+ GL11.glTranslated(8.0, 8.0, 0.0)
+ GL11.glRotated(rotX, 1.0, 0.0, 0.0)
+ GL11.glRotated(rotY, 0.0, 1.0, 0.0)
+ GL11.glTranslated(-8.0, -8.0, 0.0)
+ itemRenderer.renderItemAndEffectIntoGUI(mc.font, mc.textureManager, stack, 0, 0)
+ }
+ }
+ }
+ }
+
+ // TODO Neoforge-1.21 requires PoseStack and BufferSource from render method
+ private fun drawItemStack(
+ stack: ItemStack,
+ x: Int,
+ y: Int,
+ size: Int,
+ width: Int,
+ rotY: Double,
+ rotX: Double
+ ) {
+ withStack(attributesBitMask = GL11.GL_ALL_ATTRIB_BITS) {
+ val previousZ = itemRenderer.zLevel
+ try {
+ itemRenderer.zLevel = 0f
+ GL11.glColor4f(1f, 1f, 1f, opacityMultiplier.coerceIn(0f, 1f))
+ if (isBlockStack(stack)) {
+ draw3DItem(stack, x, y, size, width, rotY, rotX)
+ } else {
+ draw2DItem(stack, x, y, size, width)
+ }
+ } finally {
+ itemRenderer.zLevel = previousZ
+ }
+ }
+ }
+
+ private fun resolveImage(source: String): ResourceLocation? {
+ imageCache[source]?.let { return it }
+
+ return when {
+ source.startsWith("http://") || source.startsWith("https://") -> {
+ val url = runCatching { URL(source) }.getOrNull() ?: return null
+ val file = remoteFileFor(url)
+ if (!file.exists()) {
+ if (!downloadToFile(url, file)) return null
+ }
+ loadDynamicTexture(file, source)
+ }
+
+ source.startsWith("file://") -> {
+ var relative = source.removePrefix("file://")
+ while (relative.startsWith("/") || relative.startsWith("\\")) {
+ relative = relative.substring(1)
+ }
+ val baseDir = File(mc.gameDirectory, "dsgl")
+ val file = File(baseDir, relative)
+ loadDynamicTexture(file, source)
+ }
+
+ else -> {
+ val location = ResourceLocation.parse(source)
+ imageCache[source] = location
+ location
+ }
+ }
+ }
+
+ private fun remoteFileFor(url: URL): File {
+ val host = if (url.host.isNullOrBlank()) "unknown" else url.host
+ var path = url.path
+ if (path.isBlank() || path == "/") {
+ path = "/index"
+ }
+ if (path.startsWith("/")) {
+ path = path.substring(1)
+ }
+ path = path.replace("..", "_")
+ val baseDir = File(mc.gameDirectory, "dsgl/cache/downloads")
+ return File(baseDir, host + File.separator + path)
+ }
+
+ private fun downloadToFile(url: URL, file: File): Boolean {
+ return try {
+ file.parentFile?.mkdirs()
+ url.openStream().use { input ->
+ file.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ }
+ true
+ } catch (ex: Exception) {
+ false
+ }
+ }
+
+ private fun loadDynamicTexture(file: File, cacheKey: String): ResourceLocation? {
+ if (!file.exists()) return null
+ val cached = imageCache[cacheKey]
+ if (cached != null) return cached
+ return try {
+ file.inputStream().use { inS ->
+ val image = NativeImage.read(inS) ?: return null
+ val texture = DynamicTexture(image)
+ dynamicTexturesCache[cacheKey] = texture
+ val name = "dsgl_${cacheKey.hashCode().toString(16)}"
+ val location = mc.textureManager.register(name, texture)
+ imageCache[cacheKey] = location
+ location
+ }
+ } catch (ex: Exception) {
+ null
+ }
+ }
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/McItemStackRef.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/McItemStackRef.kt
new file mode 100644
index 0000000..7d98934
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/McItemStackRef.kt
@@ -0,0 +1,9 @@
+package org.dreamfinity.dsgl.mcNeoforge1211
+
+import net.minecraft.world.item.ItemStack
+import org.dreamfinity.dsgl.core.ItemStackRef
+
+/**
+ * Wrapper for Minecraft 1.21.1 [ItemStack] to satisfy [ItemStackRef].
+ */
+class McItemStackRef(val stack: ItemStack) : ItemStackRef
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/RenderCommandTransformStack.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/RenderCommandTransformStack.kt
new file mode 100644
index 0000000..39d0931
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/RenderCommandTransformStack.kt
@@ -0,0 +1,79 @@
+package org.dreamfinity.dsgl.mcNeoforge1211
+
+import kotlin.math.ceil
+import kotlin.math.floor
+import org.dreamfinity.dsgl.core.dom.layout.AffineTransform2D
+import org.dreamfinity.dsgl.core.render.RenderCommand
+
+internal data class GuiClipRect(
+ val x: Int,
+ val y: Int,
+ val width: Int,
+ val height: Int
+)
+
+internal class RenderCommandTransformStack {
+ private val stack: ArrayDeque = ArrayDeque()
+ private var current: AffineTransform2D = AffineTransform2D.IDENTITY
+
+ fun reset() {
+ stack.clear()
+ current = AffineTransform2D.IDENTITY
+ }
+
+ fun push(command: RenderCommand.PushTransform) {
+ stack.addLast(current)
+ current = current.times(command.toAffineTransform())
+ }
+
+ fun pop() {
+ current = if (stack.isNotEmpty()) stack.removeLast() else AffineTransform2D.IDENTITY
+ }
+
+ fun currentTransform(): AffineTransform2D = current
+
+ fun transformPoint(x: Float, y: Float): Pair {
+ return current.transform(x, y)
+ }
+
+ fun resolveClipRect(x: Int, y: Int, width: Int, height: Int): GuiClipRect {
+ val safeWidth = width.coerceAtLeast(0)
+ val safeHeight = height.coerceAtLeast(0)
+ if (safeWidth == 0 || safeHeight == 0) {
+ return GuiClipRect(x, y, 0, 0)
+ }
+ if (current == AffineTransform2D.IDENTITY) {
+ return GuiClipRect(x, y, safeWidth, safeHeight)
+ }
+
+ val topLeft = current.transform(x.toFloat(), y.toFloat())
+ val topRight = current.transform((x + safeWidth).toFloat(), y.toFloat())
+ val bottomLeft = current.transform(x.toFloat(), (y + safeHeight).toFloat())
+ val bottomRight = current.transform((x + safeWidth).toFloat(), (y + safeHeight).toFloat())
+
+ val minX = minOf(topLeft.first, topRight.first, bottomLeft.first, bottomRight.first)
+ val maxX = maxOf(topLeft.first, topRight.first, bottomLeft.first, bottomRight.first)
+ val minY = minOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second)
+ val maxY = maxOf(topLeft.second, topRight.second, bottomLeft.second, bottomRight.second)
+
+ val resolvedX = floor(minX.toDouble()).toInt()
+ val resolvedY = floor(minY.toDouble()).toInt()
+ val resolvedWidth = ceil((maxX - minX).toDouble()).toInt().coerceAtLeast(0)
+ val resolvedHeight = ceil((maxY - minY).toDouble()).toInt().coerceAtLeast(0)
+ return GuiClipRect(resolvedX, resolvedY, resolvedWidth, resolvedHeight)
+ }
+
+ private fun RenderCommand.PushTransform.toAffineTransform(): AffineTransform2D {
+ val toOrigin = AffineTransform2D.translation(originX, originY)
+ val translate = AffineTransform2D.translation(translateX, translateY)
+ val rotate = AffineTransform2D.rotation(rotateDeg)
+ val scale = AffineTransform2D.scale(scaleX, scaleY)
+ val fromOrigin = AffineTransform2D.translation(-originX, -originY)
+ return toOrigin
+ .times(translate)
+ .times(rotate)
+ .times(scale)
+ .times(fromOrigin)
+ }
+}
+
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/scissorsHelper/ScissorContext.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/scissorsHelper/ScissorContext.kt
new file mode 100644
index 0000000..ea5a647
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/scissorsHelper/ScissorContext.kt
@@ -0,0 +1,42 @@
+package org.dreamfinity.dsgl.mcNeoforge1211.scissorsHelper
+
+import org.lwjgl.opengl.GL11
+import java.util.*
+
+object ScissorContext {
+ val instance = ScissorContext
+ val stack: Deque = ArrayDeque()
+ var scissorsEnabledByContext = false
+
+ fun push(x: Number, y: Number, width: Number, height: Number): ScissorsArea {
+ if (stack.isEmpty()) {
+ scissorsEnabledByContext = !GL11.glIsEnabled(GL11.GL_SCISSOR_TEST)
+ if (scissorsEnabledByContext) GL11.glEnable(GL11.GL_SCISSOR_TEST)
+ }
+ val scissorsArea =
+ ScissorsArea(x.toInt(), y.toInt(), width.toInt(), height.toInt()) intersectionWith stack.peekFirst()
+ stack.push(scissorsArea)
+ GL11.glScissor(scissorsArea.x, scissorsArea.y, scissorsArea.width, scissorsArea.height)
+ return scissorsArea
+ }
+
+ fun pop(): ScissorsArea? {
+ if (stack.isEmpty()) return null
+
+ val removed = stack.pop()
+ val current = stack.peekFirst()
+ if (current != null) {
+ GL11.glScissor(current.x, current.y, current.width, current.height)
+ } else {
+ if (scissorsEnabledByContext) GL11.glDisable(GL11.GL_SCISSOR_TEST)
+ scissorsEnabledByContext = false
+ }
+ return removed
+ }
+
+ fun clear() {
+ while (stack.isNotEmpty()) {
+ pop()
+ }
+ }
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/scissorsHelper/ScissorsArea.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/scissorsHelper/ScissorsArea.kt
new file mode 100644
index 0000000..9a93210
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/scissorsHelper/ScissorsArea.kt
@@ -0,0 +1,21 @@
+package org.dreamfinity.dsgl.mcNeoforge1211.scissorsHelper
+
+import kotlin.math.max
+import kotlin.math.min
+
+data class ScissorsArea(val x: Int, val y: Int, val width: Int, val height: Int)
+
+infix fun ScissorsArea.intersectionWith(another: ScissorsArea?): ScissorsArea {
+ return another?.let {
+ val x1 = max(this.x, another.x)
+ val x2 = min(this.x + this.width, another.x + another.width)
+ val y1 = max(this.y, another.y)
+ val y2 = min(this.y + this.height, another.y + another.height)
+ ScissorsArea(
+ x1,
+ y1,
+ max(0, x2 - x1),
+ max(0, y2 - y1)
+ )
+ } ?: this
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/text/MsdfRuntimeDebugSettings.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/text/MsdfRuntimeDebugSettings.kt
new file mode 100644
index 0000000..b0c6a49
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/text/MsdfRuntimeDebugSettings.kt
@@ -0,0 +1,6 @@
+package org.dreamfinity.dsgl.mcNeoforge1211.text
+
+object MsdfRuntimeDebugSettings {
+ @Volatile
+ var decorationGuidesEnabled: Boolean = java.lang.Boolean.getBoolean("dsgl.msdf.debug.decorations")
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/text/MsdfTextRenderer.kt b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/text/MsdfTextRenderer.kt
new file mode 100644
index 0000000..5fda72d
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/kotlin/org/dreamfinity/dsgl/mcNeoforge1211/text/MsdfTextRenderer.kt
@@ -0,0 +1,1188 @@
+package org.dreamfinity.dsgl.mcNeoforge1211.text
+
+import org.dreamfinity.dsgl.core.font.*
+import org.dreamfinity.dsgl.core.dom.layout.FontLineMetrics
+import org.dreamfinity.dsgl.core.render.RenderCommand
+import org.dreamfinity.dsgl.core.style.TextFormatting
+import org.dreamfinity.dsgl.core.text.*
+import org.lwjgl.BufferUtils
+import org.lwjgl.opengl.*
+import java.nio.ByteBuffer
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+
+internal class MsdfTextRenderer {
+ private data class PreparedText(
+ val text: String,
+ val styleSpans: List
+ )
+
+ private data class LayoutCacheKey(
+ val text: String,
+ val primaryFontId: String,
+ val fontSize: Int,
+ val textFormatting: TextFormatting,
+ val baseFlagsMask: Int,
+ val styleSpansHash: Int
+ )
+
+ private data class CachedLineLayout(
+ val start: Int,
+ val shaped: ShapedText
+ )
+
+ private data class LayoutCacheEntry(val lines: List)
+
+ private class SegmentBuffer(initialCapacity: Int = 64) {
+ private var startX = FloatArray(initialCapacity)
+ private var endX = FloatArray(initialCapacity)
+ private var y = FloatArray(initialCapacity)
+ private var thickness = FloatArray(initialCapacity)
+ private var color = IntArray(initialCapacity)
+ private var kind = IntArray(initialCapacity)
+
+ var size: Int = 0
+ private set
+
+ fun clear() {
+ size = 0
+ }
+
+ fun appendMerged(
+ segmentKind: Int,
+ segmentStartX: Float,
+ segmentEndX: Float,
+ segmentY: Float,
+ segmentThickness: Float,
+ segmentColor: Int
+ ) {
+ if (segmentEndX <= segmentStartX) return
+ val last = size - 1
+ if (last >= 0 &&
+ kind[last] == segmentKind &&
+ color[last] == segmentColor &&
+ kotlin.math.abs(endX[last] - segmentStartX) <= 0.51f &&
+ kotlin.math.abs(y[last] - segmentY) <= 0.51f &&
+ kotlin.math.abs(thickness[last] - segmentThickness) <= 0.1f
+ ) {
+ endX[last] = segmentEndX
+ return
+ }
+ ensureCapacity(size + 1)
+ kind[size] = segmentKind
+ startX[size] = segmentStartX
+ endX[size] = segmentEndX
+ y[size] = segmentY
+ thickness[size] = segmentThickness
+ color[size] = segmentColor
+ size += 1
+ }
+
+ fun kindAt(index: Int): Int = kind[index]
+ fun startXAt(index: Int): Float = startX[index]
+ fun endXAt(index: Int): Float = endX[index]
+ fun yAt(index: Int): Float = y[index]
+ fun thicknessAt(index: Int): Float = thickness[index]
+ fun colorAt(index: Int): Int = color[index]
+
+ private fun ensureCapacity(required: Int) {
+ if (required <= startX.size) return
+ var next = startX.size
+ while (next < required) {
+ next = (next * 2).coerceAtLeast(required)
+ }
+ startX = startX.copyOf(next)
+ endX = endX.copyOf(next)
+ y = y.copyOf(next)
+ thickness = thickness.copyOf(next)
+ color = color.copyOf(next)
+ kind = kind.copyOf(next)
+ }
+ }
+
+ private data class RendererDebugCounters(
+ var drawCalls: Long = 0L,
+ var layoutCacheHits: Long = 0L,
+ var layoutCacheMisses: Long = 0L,
+ var glyphVectorRequests: Long = 0L,
+ var glyphResolutionRequests: Long = 0L,
+ var textureUploads: Long = 0L,
+ var textureUploadBytes: Long = 0L
+ )
+
+ private data class DecorationSegment(
+ var startX: Float,
+ var endX: Float,
+ val y: Float,
+ val thickness: Float,
+ val color: Int
+ )
+
+ private data class ObfuscationBuckets(
+ val byAdvanceBucket: Map>,
+ val expandedByAdvanceBucket: Map>,
+ val sortedKeys: List,
+ val allGlyphs: List
+ )
+
+ private data class LineSlice(
+ val start: Int,
+ val endExclusive: Int
+ )
+
+ private val textures: MutableMap = linkedMapOf()
+ private val layoutCache: MutableMap =
+ object : LinkedHashMap(64, 0.75f, true) {
+ override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
+ return size > MAX_LAYOUT_CACHE_ENTRIES
+ }
+ }
+ private var programId: Int = 0
+ private var uniformAtlas: Int = -1
+ private var uniformPxRange: Int = -1
+ private val errorLogTimes: MutableMap = linkedMapOf()
+ private val debugLogKeys: MutableSet = Collections.newSetFromMap(ConcurrentHashMap())
+ private val debugGlyphResolutionEnabled: Boolean by lazy(mode = LazyThreadSafetyMode.NONE) {
+ java.lang.Boolean.getBoolean("dsgl.msdf.debug")
+ }
+ private val obfuscationBuckets: MutableMap = linkedMapOf()
+ private var obfuscationLastNano: Long = System.nanoTime()
+ private var obfuscationAccumSec: Double = 0.0
+ private var obfuscationTimeSlice: Long = 0
+ private val maxTextureSize: Int by lazy { GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE).coerceAtLeast(1) }
+ private val segmentBuffer = SegmentBuffer(96)
+ private val debugCounters = RendererDebugCounters()
+ private val debugPerformanceEnabled: Boolean by lazy(mode = LazyThreadSafetyMode.NONE) {
+ java.lang.Boolean.getBoolean("dsgl.msdf.debug.performance")
+ }
+ private var debugLastLogMs: Long = 0L
+ fun measureText(text: String, fontId: String?, fontSize: Int?): Int {
+ return FontRegistry.measureText(text, fontId, fontSize)
+ }
+
+ fun measureTextRange(
+ text: String,
+ startIndex: Int,
+ endIndexExclusive: Int,
+ fontId: String?,
+ fontSize: Int?
+ ): Int {
+ val shaped = FontRegistry.shapeTextRange(
+ text = text,
+ startIndex = startIndex,
+ endIndexExclusive = endIndexExclusive,
+ fontId = fontId,
+ fontSize = fontSize,
+ formattingMode = "plain"
+ )
+ return shaped.width.toInt().coerceAtLeast(0)
+ }
+
+ fun lineHeight(fontId: String?, fontSize: Int?): Int {
+ return FontRegistry.lineHeight(fontId, fontSize)
+ }
+
+ fun fontLineMetrics(fontId: String?, fontSize: Int?): FontLineMetrics? {
+ val font = FontRegistry.get(fontId) ?: return null
+ val metrics = font.meta.metrics
+ if (metrics.emSize <= 0f || metrics.lineHeight <= 0f) return null
+ return FontLineMetrics(
+ emSize = metrics.emSize,
+ lineHeightEm = metrics.lineHeight,
+ ascenderEm = metrics.ascender,
+ descenderEm = metrics.descender
+ )
+ }
+
+ fun draw(command: RenderCommand.DrawText, opacityMultiplier: Float) {
+ debugCounters.drawCalls += 1
+ val primaryFont = FontRegistry.get(command.fontId) ?: return
+ val runtimeFallbackFont = FontRegistry.get(FontRegistry.FALLBACK_FONT_ID)
+ ?.takeIf { it.descriptor.fontId != primaryFont.descriptor.fontId }
+ val missingGlyphFont = FontRegistry.get(FontRegistry.FALLBACK_FONT_ID)
+ ?: FontRegistry.get(FontRegistry.DEFAULT_FONT_ID)
+ val fontSize = FontRegistry.resolveFontSize(command.fontSize)
+ val prepared = prepareText(command)
+ val layoutEntry = getOrBuildLayoutCacheEntry(
+ command = command,
+ prepared = prepared,
+ primaryFont = primaryFont,
+ fontSize = fontSize
+ )
+ if (prepared.text.isEmpty() || layoutEntry.lines.isEmpty()) return
+
+ val debugDecorationGuidesEnabled = MsdfRuntimeDebugSettings.decorationGuidesEnabled
+ updateObfuscationClock()
+ segmentBuffer.clear()
+
+ val depthWasEnabled = GL11.glIsEnabled(GL11.GL_DEPTH_TEST)
+ if (depthWasEnabled) {
+ GL11.glDisable(GL11.GL_DEPTH_TEST)
+ }
+
+ try {
+ val primaryScalePx = TextDecorationLayout.scalePx(fontSize, primaryFont.meta.metrics.emSize)
+ val lineHeight = primaryFont.meta.lineHeightPx(fontSize).toFloat().coerceAtLeast(1f)
+ val fontDecorationMetrics = DecorationFontMetrics(
+ emSize = primaryFont.meta.metrics.emSize,
+ lineHeightEm = primaryFont.meta.metrics.lineHeight,
+ ascenderEm = primaryFont.meta.metrics.ascender,
+ descenderEm = primaryFont.meta.metrics.descender,
+ underlineYEm = primaryFont.meta.metrics.underlineY,
+ underlineThicknessEm = primaryFont.meta.metrics.underlineThickness
+ )
+ debugGlyphResolution(prepared.text, primaryFont)
+
+ if (!useProgram()) return
+
+ GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+
+ var lineTop = command.y.toFloat()
+ var lineIndex = 0
+ var spanIndex = 0
+ var globalGlyphIndex = 0
+ var activeFontId: String? = null
+ var activeTexture: FontTextureHandle? = null
+ var glBegun = false
+ var currentDrawColor: Int = Int.MIN_VALUE
+ var activeResolvedSpanIndex = Int.MIN_VALUE
+ var activeStyleColor = withOpacity(command.color, opacityMultiplier)
+ var activeStyleFlags = baseFlagsMask(command)
+
+ fun beginForFont(font: LoadedMsdfFont): FontTextureHandle? {
+ if (activeFontId == font.descriptor.fontId && activeTexture != null && glBegun) {
+ return activeTexture
+ }
+ if (glBegun) {
+ GL11.glEnd()
+ glBegun = false
+ }
+ val texture = textureFor(font) ?: return null
+ GL13.glActiveTexture(GL13.GL_TEXTURE0)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture.textureId)
+ ARBShaderObjects.glUniform1iARB(uniformAtlas, 0)
+ ARBShaderObjects.glUniform1fARB(uniformPxRange, font.meta.atlas.distanceRange)
+ GL11.glBegin(GL11.GL_QUADS)
+ glBegun = true
+ activeFontId = font.descriptor.fontId
+ activeTexture = texture
+ currentDrawColor = Int.MIN_VALUE
+ return texture
+ }
+
+ try {
+ layoutEntry.lines.forEach { line ->
+ val baselineY = TextDecorationLayout.baselineY(
+ lineTopY = lineTop,
+ ascenderEm = primaryFont.meta.metrics.ascender,
+ scalePx = primaryScalePx
+ )
+ val shaped = line.shaped
+ val lineRecord = TextVisualLine(
+ lineIndex = lineIndex,
+ lineTopY = lineTop,
+ baselineY = baselineY,
+ lineHeightPx = lineHeight,
+ glyphStartIndex = globalGlyphIndex,
+ glyphEndIndexExclusive = globalGlyphIndex + shaped.glyphs.size
+ )
+ val lineMetrics = TextDecorationLayout.resolveLineMetrics(
+ line = lineRecord,
+ fontMetrics = fontDecorationMetrics,
+ fontPx = fontSize
+ )
+ var lineStartX = command.x.toFloat()
+ var lineEndX = lineStartX
+ var glyphIndexInLine = 0
+ var lastObfuscatedGlyphIndex: Int? = null
+ var cachedShapedFontId: String? = null
+ var cachedShapedFont: LoadedMsdfFont = primaryFont
+
+ shaped.glyphs.forEach { shapedGlyph ->
+ val globalCharStart = line.start + shapedGlyph.charStart
+ while (spanIndex < prepared.styleSpans.size && globalCharStart >= prepared.styleSpans[spanIndex].end) {
+ spanIndex += 1
+ }
+ val resolvedSpanIndex = if (
+ spanIndex < prepared.styleSpans.size &&
+ globalCharStart >= prepared.styleSpans[spanIndex].start &&
+ globalCharStart < prepared.styleSpans[spanIndex].end
+ ) {
+ spanIndex
+ } else {
+ -1
+ }
+ if (resolvedSpanIndex != activeResolvedSpanIndex) {
+ activeResolvedSpanIndex = resolvedSpanIndex
+ if (resolvedSpanIndex >= 0) {
+ val span = prepared.styleSpans[resolvedSpanIndex]
+ activeStyleColor = withOpacity(span.color, opacityMultiplier)
+ activeStyleFlags = flagsMask(
+ bold = span.bold,
+ italic = span.italic,
+ underline = span.underline,
+ strikethrough = span.strikethrough,
+ obfuscated = span.obfuscated
+ )
+ } else {
+ activeStyleColor = withOpacity(command.color, opacityMultiplier)
+ activeStyleFlags = baseFlagsMask(command)
+ }
+ }
+
+ val shapedFont = if (shapedGlyph.fontId == cachedShapedFontId) {
+ cachedShapedFont
+ } else {
+ (FontRegistry.get(shapedGlyph.fontId) ?: primaryFont).also { resolved ->
+ cachedShapedFontId = shapedGlyph.fontId
+ cachedShapedFont = resolved
+ }
+ }
+ val forceMissingGlyphFont = if (
+ shapedGlyph.sourceCodepoint == REPLACEMENT_CODEPOINT ||
+ isShapedGlyphMissingInFont(shapedFont, shapedGlyph)
+ ) {
+ missingGlyphFont ?: runtimeFallbackFont ?: shapedFont
+ } else {
+ null
+ }
+
+ var glyphFont = forceMissingGlyphFont ?: shapedFont
+ var glyph = if (forceMissingGlyphFont != null) {
+ preferredMissingGlyph(glyphFont)
+ } else {
+ resolveGlyphForShapedInput(shapedFont, shapedGlyph)
+ }
+ if (glyph == null && runtimeFallbackFont != null) {
+ val fallbackByCodepoint = resolveGlyphForShapedInput(runtimeFallbackFont, shapedGlyph)
+ if (fallbackByCodepoint != null) {
+ glyphFont = runtimeFallbackFont
+ glyph = fallbackByCodepoint
+ } else {
+ preferredMissingGlyph(runtimeFallbackFont)?.let { fallbackDefault ->
+ glyphFont = runtimeFallbackFont
+ glyph = fallbackDefault
+ }
+ }
+ }
+ debugCounters.glyphResolutionRequests += 1
+
+ val styleBold = (activeStyleFlags and STYLE_FLAG_BOLD) != 0
+ val styleItalic = (activeStyleFlags and STYLE_FLAG_ITALIC) != 0
+ val styleUnderline = (activeStyleFlags and STYLE_FLAG_UNDERLINE) != 0
+ val styleStrikethrough = (activeStyleFlags and STYLE_FLAG_STRIKETHROUGH) != 0
+ val styleObfuscated = (activeStyleFlags and STYLE_FLAG_OBFUSCATED) != 0
+
+ val boldAdvance =
+ if (styleBold && !TextStyleMetrics.isWhitespaceCodepoint(shapedGlyph.sourceCodepoint)) {
+ BOLD_ADVANCE_EXTRA_PX.toFloat()
+ } else {
+ 0f
+ }
+ val glyphAdvance = shapedGlyph.advance + boldAdvance
+ val glyphStartX = command.x + shapedGlyph.x
+ val glyphEndX = glyphStartX + glyphAdvance
+ if (glyphStartX < lineStartX) lineStartX = glyphStartX
+ if (glyphEndX > lineEndX) lineEndX = glyphEndX
+
+ val resolvedGlyph = glyph
+ if (resolvedGlyph != null && resolvedGlyph.drawable) {
+ val texture = beginForFont(glyphFont)
+ if (texture == null) {
+ lastObfuscatedGlyphIndex = null
+ glyphIndexInLine += 1
+ globalGlyphIndex += 1
+ return@forEach
+ }
+ if (activeStyleColor != currentDrawColor) {
+ val r = ((activeStyleColor ushr 16) and 0xFF) / 255f
+ val g = ((activeStyleColor ushr 8) and 0xFF) / 255f
+ val b = (activeStyleColor and 0xFF) / 255f
+ val a = ((activeStyleColor ushr 24) and 0xFF) / 255f
+ GL11.glColor4f(r, g, b, a)
+ currentDrawColor = activeStyleColor
+ }
+
+ val drawGlyph =
+ if (styleObfuscated && ObfuscationTextSelector.shouldObfuscateCodepoint(shapedGlyph.sourceCodepoint)) {
+ resolveObfuscatedGlyph(
+ font = glyphFont,
+ sourceKey = command.sourceKey ?: command.text,
+ original = resolvedGlyph,
+ lineIndex = lineIndex,
+ glyphIndexInLine = glyphIndexInLine,
+ avoidGlyphIndex = lastObfuscatedGlyphIndex
+ )
+ } else {
+ resolvedGlyph
+ }
+
+ val effectiveGlyph = drawGlyph ?: resolvedGlyph
+ val glyphScale = TextDecorationLayout.scalePx(fontSize, glyphFont.meta.metrics.emSize)
+ emitGlyphQuad(
+ glyph = effectiveGlyph,
+ baselineY = baselineY + shapedGlyph.y,
+ cursorX = glyphStartX,
+ atlasWidth = texture.width,
+ atlasHeight = texture.height,
+ fontScalePx = glyphScale,
+ italic = styleItalic,
+ italicSkewPx = glyphScale * 0.2f
+ )
+ if (styleBold) {
+ emitGlyphQuad(
+ glyph = effectiveGlyph,
+ baselineY = baselineY + shapedGlyph.y,
+ cursorX = glyphStartX + 0.75f,
+ atlasWidth = texture.width,
+ atlasHeight = texture.height,
+ fontScalePx = glyphScale,
+ italic = styleItalic,
+ italicSkewPx = glyphScale * 0.2f
+ )
+ }
+ lastObfuscatedGlyphIndex = if (styleObfuscated) effectiveGlyph.glyphIndex else null
+ } else {
+ lastObfuscatedGlyphIndex = null
+ }
+
+ if (glyphEndX > glyphStartX) {
+ if (styleUnderline) {
+ segmentBuffer.appendMerged(
+ segmentKind = SEGMENT_UNDERLINE,
+ segmentStartX = glyphStartX,
+ segmentEndX = glyphEndX,
+ segmentY = lineMetrics.underlineY,
+ segmentThickness = lineMetrics.underlineThickness,
+ segmentColor = activeStyleColor
+ )
+ }
+ if (styleStrikethrough) {
+ segmentBuffer.appendMerged(
+ segmentKind = SEGMENT_STRIKETHROUGH,
+ segmentStartX = glyphStartX,
+ segmentEndX = glyphEndX,
+ segmentY = lineMetrics.strikethroughY,
+ segmentThickness = lineMetrics.strikethroughThickness,
+ segmentColor = activeStyleColor
+ )
+ }
+ }
+
+ globalGlyphIndex += 1
+ glyphIndexInLine += 1
+ }
+
+ if (debugDecorationGuidesEnabled && lineEndX > lineStartX) {
+ segmentBuffer.appendMerged(
+ segmentKind = SEGMENT_DEBUG_BASELINE,
+ segmentStartX = lineStartX,
+ segmentEndX = lineEndX,
+ segmentY = lineRecord.baselineY.coerceIn(
+ lineRecord.lineTopY,
+ lineRecord.lineTopY + lineRecord.lineHeightPx
+ ),
+ segmentThickness = 1f,
+ segmentColor = 0x66FFAA00
+ )
+ segmentBuffer.appendMerged(
+ segmentKind = SEGMENT_DEBUG_UNDERLINE,
+ segmentStartX = lineStartX,
+ segmentEndX = lineEndX,
+ segmentY = lineMetrics.underlineY,
+ segmentThickness = lineMetrics.underlineThickness,
+ segmentColor = 0x6600FF00
+ )
+ segmentBuffer.appendMerged(
+ segmentKind = SEGMENT_DEBUG_STRIKE,
+ segmentStartX = lineStartX,
+ segmentEndX = lineEndX,
+ segmentY = lineMetrics.strikethroughY,
+ segmentThickness = lineMetrics.strikethroughThickness,
+ segmentColor = 0x66FF00FF
+ )
+ }
+
+ lineTop += lineHeight
+ lineIndex += 1
+ }
+ } finally {
+ if (glBegun) {
+ GL11.glEnd()
+ }
+ ARBShaderObjects.glUseProgramObjectARB(0)
+ }
+
+ drawDecorationSegments(segmentBuffer, debugDecorationGuidesEnabled)
+ maybeLogPerformance()
+ } finally {
+ if (depthWasEnabled) {
+ GL11.glEnable(GL11.GL_DEPTH_TEST)
+ }
+ }
+ }
+
+ private fun prepareText(command: RenderCommand.DrawText): PreparedText {
+ if (command.textFormatting != TextFormatting.Minecraft) {
+ return PreparedText(
+ text = command.text,
+ styleSpans = command.textStyleSpans
+ )
+ }
+
+ if (command.textStyleSpans.isNotEmpty()) {
+ return PreparedText(
+ text = command.text,
+ styleSpans = command.textStyleSpans
+ )
+ }
+
+ val parsed = MinecraftFormattingParser.parse(command.text, TextFormatting.Minecraft)
+ val spans = MinecraftFormattingParser.resolveStyleSpans(
+ parsed = parsed,
+ baseColor = command.color,
+ baseFlags = TextStyleFlags(
+ bold = command.bold,
+ italic = command.italic,
+ underline = command.underline,
+ strikethrough = command.strikethrough,
+ obfuscated = command.obfuscated
+ )
+ ).map { span ->
+ RenderCommand.TextStyleSpan(
+ start = span.start,
+ end = span.end,
+ color = span.color,
+ bold = span.flags.bold,
+ italic = span.flags.italic,
+ underline = span.flags.underline,
+ strikethrough = span.flags.strikethrough,
+ obfuscated = span.flags.obfuscated
+ )
+ }
+ return PreparedText(
+ text = parsed.plainText,
+ styleSpans = spans
+ )
+ }
+
+ private fun splitLines(text: String): List {
+ if (text.isEmpty()) return listOf(LineSlice(0, 0))
+ val lines = ArrayList(4)
+ var start = 0
+ var index = 0
+ while (index < text.length) {
+ if (text[index] == '\n') {
+ lines += LineSlice(start = start, endExclusive = index)
+ start = index + 1
+ }
+ index += 1
+ }
+ lines += LineSlice(start = start, endExclusive = text.length)
+ return lines
+ }
+
+ private fun resolveGlyphForShapedInput(font: LoadedMsdfFont, shapedGlyph: ShapedGlyph): MsdfGlyph? {
+ val sourceCodepoint = shapedGlyph.sourceCodepoint
+ val canUseGlyphIndex = shapedGlyph.fontId == font.descriptor.fontId
+ val fromIndex = if (canUseGlyphIndex) {
+ font.meta.glyphByIndex(shapedGlyph.glyphIndex)
+ } else {
+ null
+ }
+ val indexLooksMissing = isMissingGlyphIndex(font, shapedGlyph.glyphIndex, fromIndex)
+ val indexMatchesSource = if (!indexLooksMissing) {
+ val fromIndexCodepoint = fromIndex?.codepoint
+ fromIndex != null && (
+ fromIndexCodepoint == null ||
+ fromIndexCodepoint == sourceCodepoint
+ )
+ } else {
+ false
+ }
+
+ if (sourceCodepoint == REPLACEMENT_CODEPOINT) {
+ return preferredMissingGlyph(font)
+ }
+
+ if (indexMatchesSource) return fromIndex
+
+ val fromCodepoint = font.meta.glyph(sourceCodepoint)
+ if (fromCodepoint != null) return fromCodepoint
+
+ return preferredMissingGlyph(font)
+ }
+
+ private fun preferredMissingGlyph(font: LoadedMsdfFont): MsdfGlyph? {
+ val meta = font.meta
+ val replacementByCodepoint = meta.glyph(REPLACEMENT_CODEPOINT)
+ if (replacementByCodepoint != null) return replacementByCodepoint
+ val questionByCodepoint = meta.glyph('?'.code)
+ if (questionByCodepoint != null) return questionByCodepoint
+
+ val questionByIndex = font.preferredQuestionGlyphIndex?.let(meta::glyphByIndex)
+ if (questionByIndex != null) return questionByIndex
+ val replacementByIndex = font.preferredMissingGlyphIndex
+ ?.takeIf { it != 0 }
+ ?.let(meta::glyphByIndex)
+ if (replacementByIndex != null) return replacementByIndex
+ val notDef = meta.glyphByIndex(0)
+ if (notDef != null) return notDef
+ return meta.fallbackGlyph()
+ }
+
+ private fun isShapedGlyphMissingInFont(font: LoadedMsdfFont, shapedGlyph: ShapedGlyph): Boolean {
+ if (TextStyleMetrics.isWhitespaceCodepoint(shapedGlyph.sourceCodepoint)) return false
+ val fromIndex = font.meta.glyphByIndex(shapedGlyph.glyphIndex)
+ return isMissingGlyphIndex(font, shapedGlyph.glyphIndex, fromIndex)
+ }
+
+ private fun isMissingGlyphIndex(font: LoadedMsdfFont, glyphIndex: Int, glyph: MsdfGlyph?): Boolean {
+ if (glyph == null) return true
+ val preferredMissingIndex = font.preferredMissingGlyphIndex
+ if (preferredMissingIndex != null && glyphIndex == preferredMissingIndex) return true
+ if (glyphIndex == 0 && (glyph.codepoint == null || glyph.codepoint == REPLACEMENT_CODEPOINT)) return true
+ return false
+ }
+
+ private fun emitGlyphQuad(
+ glyph: MsdfGlyph,
+ baselineY: Float,
+ cursorX: Float,
+ atlasWidth: Int,
+ atlasHeight: Int,
+ fontScalePx: Float,
+ italic: Boolean,
+ italicSkewPx: Float
+ ) {
+ val plane = glyph.planeBounds ?: return
+ val atlas = glyph.atlasBounds ?: return
+
+ val x0 = cursorX + plane.left * fontScalePx
+ val x1 = cursorX + plane.right * fontScalePx
+ val y0 = baselineY - plane.top * fontScalePx
+ val y1 = baselineY - plane.bottom * fontScalePx
+ val skew = if (italic) italicSkewPx else 0f
+
+ val u0 = atlas.left / atlasWidth.toFloat()
+ val u1 = atlas.right / atlasWidth.toFloat()
+ val v0 = atlas.bottom / atlasHeight.toFloat()
+ val v1 = atlas.top / atlasHeight.toFloat()
+
+ GL11.glTexCoord2f(u0, v0)
+ GL11.glVertex2f(x0, y1)
+ GL11.glTexCoord2f(u1, v0)
+ GL11.glVertex2f(x1, y1)
+ GL11.glTexCoord2f(u1, v1)
+ GL11.glVertex2f(x1 + skew, y0)
+ GL11.glTexCoord2f(u0, v1)
+ GL11.glVertex2f(x0 + skew, y0)
+ }
+
+ private fun getOrBuildLayoutCacheEntry(
+ command: RenderCommand.DrawText,
+ prepared: PreparedText,
+ primaryFont: LoadedMsdfFont,
+ fontSize: Int
+ ): LayoutCacheEntry {
+ val key = LayoutCacheKey(
+ text = prepared.text,
+ primaryFontId = primaryFont.descriptor.fontId,
+ fontSize = fontSize,
+ textFormatting = command.textFormatting,
+ baseFlagsMask = baseFlagsMask(command),
+ styleSpansHash = styleSpansFingerprint(prepared.styleSpans)
+ )
+ synchronized(layoutCache) {
+ val cached = layoutCache[key]
+ if (cached != null) {
+ debugCounters.layoutCacheHits += 1
+ return cached
+ }
+ }
+
+ debugCounters.layoutCacheMisses += 1
+ val slices = splitLines(prepared.text)
+ val lines = ArrayList(slices.size)
+ slices.forEach { slice ->
+ debugCounters.glyphVectorRequests += 1
+ val shaped = FontRegistry.shapeTextRange(
+ text = prepared.text,
+ startIndex = slice.start,
+ endIndexExclusive = slice.endExclusive,
+ fontId = command.fontId,
+ fontSize = fontSize,
+ formattingMode = command.textFormatting.name
+ )
+ lines += CachedLineLayout(
+ start = slice.start,
+ shaped = shaped
+ )
+ }
+
+ val built = LayoutCacheEntry(lines = lines)
+ synchronized(layoutCache) {
+ layoutCache[key] = built
+ }
+ return built
+ }
+
+ private fun drawDecorationSegments(segments: SegmentBuffer, includeDebug: Boolean) {
+ if (segments.size == 0) return
+ val texture2dWasEnabled = GL11.glIsEnabled(GL11.GL_TEXTURE_2D)
+ val blendWasEnabled = GL11.glIsEnabled(GL11.GL_BLEND)
+ val alphaTestWasEnabled = GL11.glIsEnabled(GL11.GL_ALPHA_TEST)
+ val lightingWasEnabled = GL11.glIsEnabled(GL11.GL_LIGHTING)
+ val cullWasEnabled = GL11.glIsEnabled(GL11.GL_CULL_FACE)
+ ARBShaderObjects.glUseProgramObjectARB(0)
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0)
+ if (lightingWasEnabled) GL11.glDisable(GL11.GL_LIGHTING)
+ if (alphaTestWasEnabled) GL11.glDisable(GL11.GL_ALPHA_TEST)
+ if (cullWasEnabled) GL11.glDisable(GL11.GL_CULL_FACE)
+ if (!blendWasEnabled) GL11.glEnable(GL11.GL_BLEND)
+ GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)
+ if (texture2dWasEnabled) GL11.glDisable(GL11.GL_TEXTURE_2D)
+ GL11.glBegin(GL11.GL_QUADS)
+ try {
+ var index = 0
+ while (index < segments.size) {
+ val kind = segments.kindAt(index)
+ val isDebug = kind == SEGMENT_DEBUG_BASELINE ||
+ kind == SEGMENT_DEBUG_UNDERLINE ||
+ kind == SEGMENT_DEBUG_STRIKE
+ if (isDebug && !includeDebug) {
+ index += 1
+ continue
+ }
+ val color = segments.colorAt(index)
+ val r = ((color ushr 16) and 0xFF) / 255f
+ val g = ((color ushr 8) and 0xFF) / 255f
+ val b = (color and 0xFF) / 255f
+ val a = ((color ushr 24) and 0xFF) / 255f
+ GL11.glColor4f(r, g, b, a)
+ val y0 = segments.yAt(index)
+ val y1 = maxOf(
+ y0 + 0.5f,
+ y0 + segments.thicknessAt(index)
+ )
+ GL11.glVertex2f(segments.startXAt(index), y0)
+ GL11.glVertex2f(segments.endXAt(index), y0)
+ GL11.glVertex2f(segments.endXAt(index), y1)
+ GL11.glVertex2f(segments.startXAt(index), y1)
+ index += 1
+ }
+ } finally {
+ GL11.glEnd()
+ if (texture2dWasEnabled) GL11.glEnable(GL11.GL_TEXTURE_2D)
+ if (!blendWasEnabled) GL11.glDisable(GL11.GL_BLEND)
+ if (alphaTestWasEnabled) GL11.glEnable(GL11.GL_ALPHA_TEST)
+ if (lightingWasEnabled) GL11.glEnable(GL11.GL_LIGHTING)
+ if (cullWasEnabled) GL11.glEnable(GL11.GL_CULL_FACE)
+ }
+ }
+
+ private fun baseFlagsMask(command: RenderCommand.DrawText): Int {
+ return flagsMask(
+ bold = command.bold,
+ italic = command.italic,
+ underline = command.underline,
+ strikethrough = command.strikethrough,
+ obfuscated = command.obfuscated
+ )
+ }
+
+ private fun flagsMask(
+ bold: Boolean,
+ italic: Boolean,
+ underline: Boolean,
+ strikethrough: Boolean,
+ obfuscated: Boolean
+ ): Int {
+ var mask = 0
+ if (bold) mask = mask or STYLE_FLAG_BOLD
+ if (italic) mask = mask or STYLE_FLAG_ITALIC
+ if (underline) mask = mask or STYLE_FLAG_UNDERLINE
+ if (strikethrough) mask = mask or STYLE_FLAG_STRIKETHROUGH
+ if (obfuscated) mask = mask or STYLE_FLAG_OBFUSCATED
+ return mask
+ }
+
+ private fun styleSpansFingerprint(spans: List): Int {
+ if (spans.isEmpty()) return 0
+ var hash = 1
+ spans.forEach { span ->
+ hash = 31 * hash + span.start
+ hash = 31 * hash + span.end
+ hash = 31 * hash + if (span.bold) 1 else 0
+ hash = 31 * hash + if (span.italic) 1 else 0
+ hash = 31 * hash + if (span.underline) 1 else 0
+ hash = 31 * hash + if (span.strikethrough) 1 else 0
+ hash = 31 * hash + if (span.obfuscated) 1 else 0
+ }
+ return hash
+ }
+
+ private fun resolveObfuscatedGlyph(
+ font: LoadedMsdfFont,
+ sourceKey: String,
+ original: MsdfGlyph,
+ lineIndex: Int,
+ glyphIndexInLine: Int,
+ avoidGlyphIndex: Int?
+ ): MsdfGlyph? {
+ val buckets = obfuscationBuckets.getOrPut(font.descriptor.fontId) {
+ buildObfuscationBuckets(font)
+ }
+ if (buckets.allGlyphs.isEmpty()) return original
+ val baseBucket = advanceBucketKey(original.advance)
+ val candidates = buckets.expandedByAdvanceBucket[baseBucket]
+ ?: nearestExpandedCandidates(buckets, baseBucket)
+ ?: buckets.allGlyphs
+ if (candidates.isEmpty()) return original
+
+ val originalKey = original.codepoint ?: original.glyphIndex
+ val primaryIndex = ObfuscationTextSelector.selectCandidateIndex(
+ sourceKey = sourceKey,
+ lineIndex = lineIndex,
+ glyphIndexInLine = glyphIndexInLine,
+ timeSlice = obfuscationTimeSlice,
+ originalCodepoint = originalKey,
+ candidateCount = candidates.size
+ )
+ val primary = candidates[primaryIndex]
+ if (avoidGlyphIndex == null || candidates.size <= 1 || primary.glyphIndex != avoidGlyphIndex) {
+ return primary
+ }
+ val secondaryIndex = (primaryIndex + 1 + (obfuscationTimeSlice.toInt() and 3)) % candidates.size
+ val secondary = candidates[secondaryIndex]
+ if (secondary.glyphIndex != avoidGlyphIndex) return secondary
+ return candidates.firstOrNull { it.glyphIndex != avoidGlyphIndex } ?: primary
+ }
+
+ private fun buildObfuscationBuckets(font: LoadedMsdfFont): ObfuscationBuckets {
+ val glyphs = font.meta.glyphsByIndex.values
+ .filter { glyph ->
+ val codepoint = glyph.codepoint
+ glyph.drawable && (codepoint == null || !TextStyleMetrics.isWhitespaceCodepoint(codepoint))
+ }
+ if (glyphs.isEmpty()) {
+ return ObfuscationBuckets(
+ byAdvanceBucket = emptyMap(),
+ expandedByAdvanceBucket = emptyMap(),
+ sortedKeys = emptyList(),
+ allGlyphs = emptyList()
+ )
+ }
+ val grouped = linkedMapOf>()
+ glyphs.forEach { glyph ->
+ grouped.getOrPut(advanceBucketKey(glyph.advance)) { ArrayList() }.add(glyph)
+ }
+ val sorted = grouped.keys.sorted()
+ val frozenGrouped = grouped.mapValues { (_, value) -> value.toList() }
+ val expanded = linkedMapOf>()
+ sorted.forEach { key ->
+ expanded[key] = expandCandidatesForBucket(
+ grouped = frozenGrouped,
+ sortedKeys = sorted,
+ baseKey = key
+ )
+ }
+ return ObfuscationBuckets(
+ byAdvanceBucket = frozenGrouped,
+ expandedByAdvanceBucket = expanded,
+ sortedKeys = sorted,
+ allGlyphs = glyphs
+ )
+ }
+
+ private fun advanceBucketKey(advance: Float): Int {
+ return (advance * 100f).toInt()
+ }
+
+ private fun nearestExpandedCandidates(buckets: ObfuscationBuckets, key: Int): List? {
+ val keys = buckets.sortedKeys
+ if (keys.isEmpty()) return null
+ var nearest = keys.first()
+ var distance = kotlin.math.abs(nearest - key)
+ keys.forEach { candidate ->
+ val nextDistance = kotlin.math.abs(candidate - key)
+ if (nextDistance < distance) {
+ distance = nextDistance
+ nearest = candidate
+ }
+ }
+ return buckets.expandedByAdvanceBucket[nearest]
+ }
+
+ private fun expandCandidatesForBucket(
+ grouped: Map>,
+ sortedKeys: List,
+ baseKey: Int
+ ): List {
+ val byDistance = sortedKeys.sortedBy { key -> kotlin.math.abs(key - baseKey) }
+ val out = ArrayList(MIN_OBFUSCATION_CANDIDATES)
+ byDistance.forEach { key ->
+ val candidates = grouped[key].orEmpty()
+ if (candidates.isNotEmpty()) {
+ out.addAll(candidates)
+ }
+ if (out.size >= MIN_OBFUSCATION_CANDIDATES) {
+ return@forEach
+ }
+ }
+ return if (out.isEmpty()) grouped.values.flatten() else out
+ }
+
+ private fun updateObfuscationClock() {
+ val now = System.nanoTime()
+ val dt = (now - obfuscationLastNano).coerceAtLeast(0L) / 1_000_000_000.0
+ obfuscationLastNano = now
+ obfuscationAccumSec += dt
+ val step = OBFUSCATION_TIME_STEP_SEC
+ if (obfuscationAccumSec >= step) {
+ val ticks = (obfuscationAccumSec / step).toLong()
+ obfuscationAccumSec -= ticks * step
+ obfuscationTimeSlice += ticks
+ }
+ }
+
+ private fun textureFor(font: LoadedMsdfFont): FontTextureHandle? {
+ val fontId = font.descriptor.fontId
+ textures[fontId]?.let { return it }
+ font.handle?.let { return it }
+
+ return runCatching {
+ val handle = uploadTexture(font)
+ textures[fontId] = handle
+ handle
+ }.onSuccess {
+ font.handle = it
+ font.atlasPayload.markLoadedToGPUTexture()
+ }.onFailure { error ->
+ logRateLimited("texture:$fontId", "[DSGL-MSDF] Failed to load atlas '$fontId': ${error.message}")
+ }.getOrNull()
+ }
+
+ private fun uploadTexture(font: LoadedMsdfFont): FontTextureHandle {
+ val bitmap = font.atlasPayload.ensureDecoded()
+ val width = bitmap.width.coerceAtLeast(1)
+ val height = bitmap.height.coerceAtLeast(1)
+ if (width > maxTextureSize || height > maxTextureSize) {
+ throw IllegalStateException(
+ "Atlas '${font.descriptor.fontId}' is ${width}x${height}, exceeds GL_MAX_TEXTURE_SIZE=$maxTextureSize"
+ )
+ }
+ val buffer = BufferUtils.createByteBuffer(width * height * 4)
+ buffer.put(ByteBuffer.wrap(bitmap.rgbaBytes))
+ buffer.flip()
+
+ val textureId = GL11.glGenTextures()
+ GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP)
+ GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP)
+ val glError = scopedGlError {
+ GL11.glTexImage2D(
+ GL11.GL_TEXTURE_2D,
+ 0,
+ GL11.GL_RGBA8,
+ width,
+ height,
+ 0,
+ GL11.GL_RGBA,
+ GL11.GL_UNSIGNED_BYTE,
+ buffer
+ )
+ }
+ if (glError != GL11.GL_NO_ERROR) {
+ GL11.glDeleteTextures(textureId)
+ throw IllegalStateException(
+ "glTexImage2D failed for '${font.descriptor.fontId}' (${width}x${height}), glError=0x${
+ glError.toString(16)
+ }"
+ )
+ }
+ debugCounters.textureUploads += 1
+ debugCounters.textureUploadBytes += (width.toLong() * height.toLong() * 4L)
+ return FontTextureHandle(textureId = textureId, width = width, height = height)
+ }
+
+ private fun drainGlErrors(): Int {
+ var firstError = GL11.GL_NO_ERROR
+ while (true) {
+ val error = GL11.glGetError()
+ if (error == GL11.GL_NO_ERROR) break
+ if (firstError == GL11.GL_NO_ERROR) {
+ firstError = error
+ }
+ }
+ return firstError
+ }
+
+ private inline fun scopedGlError(block: () -> Unit): Int {
+ drainGlErrors()
+ block()
+ return drainGlErrors()
+ }
+
+ private fun useProgram(): Boolean {
+ if (programId == 0) {
+ val loaded = runCatching { createProgram() }
+ .onFailure { error ->
+ logRateLimited("shader:init", "[DSGL-MSDF] Failed to initialize shader: ${error.message}")
+ }
+ .getOrNull() ?: return false
+ programId = loaded
+ uniformAtlas = ARBShaderObjects.glGetUniformLocationARB(programId, "uAtlas")
+ uniformPxRange = ARBShaderObjects.glGetUniformLocationARB(programId, "uPxRange")
+ }
+ ARBShaderObjects.glUseProgramObjectARB(programId)
+ return true
+ }
+
+ private fun createProgram(): Int {
+ val vertexShader = compileShader(
+ type = ARBVertexShader.GL_VERTEX_SHADER_ARB,
+ source = VERTEX_SHADER_SOURCE
+ )
+ val fragmentShader = compileShader(
+ type = ARBFragmentShader.GL_FRAGMENT_SHADER_ARB,
+ source = FRAGMENT_SHADER_SOURCE
+ )
+
+ val program = ARBShaderObjects.glCreateProgramObjectARB()
+ ARBShaderObjects.glAttachObjectARB(program, vertexShader)
+ ARBShaderObjects.glAttachObjectARB(program, fragmentShader)
+ ARBShaderObjects.glLinkProgramARB(program)
+ val linkStatus = ARBShaderObjects.glGetObjectParameteriARB(
+ program,
+ ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB
+ )
+ if (linkStatus == GL11.GL_FALSE) {
+ val info = ARBShaderObjects.glGetInfoLogARB(program, 4096)
+ throw IllegalStateException("Program link failed: $info")
+ }
+ return program
+ }
+
+ private fun compileShader(type: Int, source: String): Int {
+ val shader = ARBShaderObjects.glCreateShaderObjectARB(type)
+ ARBShaderObjects.glShaderSourceARB(shader, source)
+ ARBShaderObjects.glCompileShaderARB(shader)
+ val compileStatus = ARBShaderObjects.glGetObjectParameteriARB(
+ shader,
+ ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB
+ )
+ if (compileStatus == GL11.GL_FALSE) {
+ val info = ARBShaderObjects.glGetInfoLogARB(shader, 4096)
+ throw IllegalStateException("Shader compile failed: $info")
+ }
+ return shader
+ }
+
+ private fun withOpacity(color: Int, opacityMultiplier: Float): Int {
+ if (opacityMultiplier >= 0.999f) return color
+ val alpha = ((color ushr 24) and 0xFF)
+ val scaled = (alpha * opacityMultiplier).toInt().coerceIn(0, 255)
+ return (color and 0x00FF_FFFF) or (scaled shl 24)
+ }
+
+ private fun logRateLimited(key: String, message: String) {
+ val now = System.currentTimeMillis()
+ val previous = errorLogTimes[key] ?: 0L
+ if (now - previous < 3_000L) return
+ errorLogTimes[key] = now
+ println(message)
+ }
+
+ private fun maybeLogPerformance() {
+ if (!debugPerformanceEnabled) return
+ val now = System.currentTimeMillis()
+ if (now - debugLastLogMs < 1_000L) return
+ debugLastLogMs = now
+ val layoutSize = synchronized(layoutCache) { layoutCache.size }
+ println(
+ "[DSGL-MSDF] drawCalls=${debugCounters.drawCalls} " +
+ "layoutCache hit=${debugCounters.layoutCacheHits} miss=${debugCounters.layoutCacheMisses} size=$layoutSize " +
+ "glyphVectors=${debugCounters.glyphVectorRequests} glyphResolves=${debugCounters.glyphResolutionRequests} " +
+ "textureUploads=${debugCounters.textureUploads} " +
+ "textureUploadBytes=${debugCounters.textureUploadBytes}"
+ )
+ }
+
+ private fun debugGlyphResolution(text: String, font: LoadedMsdfFont) {
+ if (!debugGlyphResolutionEnabled) return
+ val sample = text.take(64)
+ val key = "${font.descriptor.fontId}|$sample"
+ if (!debugLogKeys.add(key)) return
+
+ val shaped = FontRegistry.shapeText(
+ text = sample,
+ fontId = font.descriptor.fontId,
+ fontSize = FontRegistry.DEFAULT_FONT_SIZE,
+ formattingMode = "debug"
+ )
+ println("[DSGL-MSDF] text='$sample' glyphs=${shaped.glyphs.size} runs=${shaped.runs.size}")
+ shaped.glyphs.take(32).forEach { glyph ->
+ val loaded = FontRegistry.get(glyph.fontId)
+ val atlasGlyph = loaded?.meta?.glyphByIndex(glyph.glyphIndex)
+ println(
+ "[DSGL-MSDF] font=${glyph.fontId} glyphIndex=${glyph.glyphIndex} sourceCp=U+%04X found=%s".format(
+ glyph.sourceCodepoint,
+ atlasGlyph != null
+ )
+ )
+ }
+ }
+
+ companion object {
+ private const val MAX_LAYOUT_CACHE_ENTRIES: Int = 512
+ private const val MIN_OBFUSCATION_CANDIDATES: Int = 24
+ private const val OBFUSCATION_TIME_STEP_SEC: Double = 0.05
+ private const val STYLE_FLAG_BOLD: Int = 1 shl 0
+ private const val STYLE_FLAG_ITALIC: Int = 1 shl 1
+ private const val STYLE_FLAG_UNDERLINE: Int = 1 shl 2
+ private const val STYLE_FLAG_STRIKETHROUGH: Int = 1 shl 3
+ private const val STYLE_FLAG_OBFUSCATED: Int = 1 shl 4
+ private const val SEGMENT_UNDERLINE: Int = 1
+ private const val SEGMENT_STRIKETHROUGH: Int = 2
+ private const val SEGMENT_DEBUG_BASELINE: Int = 3
+ private const val SEGMENT_DEBUG_UNDERLINE: Int = 4
+ private const val SEGMENT_DEBUG_STRIKE: Int = 5
+ private const val REPLACEMENT_CODEPOINT: Int = 0xFFFD
+
+ private const val VERTEX_SHADER_SOURCE: String = """
+ #version 120
+ varying vec2 vTexCoord;
+ varying vec4 vColor;
+
+ void main() {
+ gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
+ vTexCoord = gl_MultiTexCoord0.st;
+ vColor = gl_Color;
+ }
+ """
+
+ private const val FRAGMENT_SHADER_SOURCE: String = """
+ #version 120
+ uniform sampler2D uAtlas;
+ uniform float uPxRange;
+ varying vec2 vTexCoord;
+ varying vec4 vColor;
+
+ float median(float a, float b, float c) {
+ return max(min(a, b), min(max(a, b), c));
+ }
+
+ void main() {
+ vec4 sample = texture2D(uAtlas, vTexCoord);
+ float dist = max(median(sample.r, sample.g, sample.b), sample.a) - 0.5;
+ float w = 0.5 / max(uPxRange, 0.0001);
+ float alpha = smoothstep(-w, w, dist);
+ gl_FragColor = vec4(vColor.rgb, vColor.a * alpha);
+ }
+ """
+ }
+}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/resources/META-INF/MANIFEST.MF b/adapters/mc-neoforge-1-21-1/src/main/resources/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..9db1830
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/resources/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@
+Manifest-Version: 1.0
+Specification-Title: ${modName}
+Specification-Version: ${modVersion}
+Specification-Vendor: ${modAuthor}
+Implementation-Title: ${modId}
+Implementation-Version: ${modVersion}
+Implementation-Vendor: ${modAuthor}
+Implementation-Vendor-Id: ${modGroup}
+Built-For-MC: ${gameVersion}
diff --git a/adapters/mc-neoforge-1-21-1/src/main/resources/mcmod.info b/adapters/mc-neoforge-1-21-1/src/main/resources/mcmod.info
new file mode 100644
index 0000000..8ed582e
--- /dev/null
+++ b/adapters/mc-neoforge-1-21-1/src/main/resources/mcmod.info
@@ -0,0 +1,18 @@
+[
+ {
+ "modid": "${modId}",
+ "name": "${modName}",
+ "description": "${modDescription}",
+ "version": "${modVersion}",
+ "mcversion": "${gameVersion}",
+ "url": "",
+ "updateUrl": "",
+ "authorList": [
+ "${modAuthor}"
+ ],
+ "credits": "${modCredits}",
+ "logoFile": "${modIcon}",
+ "screenshots": [],
+ "dependencies": []
+ }
+]
diff --git a/gradle.properties b/gradle.properties
index 50da364..ad270bc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,4 +3,5 @@ version=0.0.1
startParameter.offline=true
org.gradle.jvmargs=-Xmx4g
# adapters turn on / off for development
-enableMinecraftForge1710=true
+enableMinecraftForge1710=false
+enableMinecraftNeoforge1211=true
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3b9f101..5b49397 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -14,6 +14,9 @@ pluginManagement {
if (isAdapterEnabled("MinecraftForge1710")) {
includeBuild("adapters/mc-forge-1-7-10/adapter-build-logic")
}
+ if (isAdapterEnabled("MinecraftNeoforge1211")) {
+ includeBuild("adapters/mc-neoforge-1-21-1/adapter-build-logic")
+ }
}
rootProject.name = "dsgl"
@@ -24,3 +27,7 @@ if (isAdapterEnabled("MinecraftForge1710")) {
include(":adapters:mc-forge-1-7-10")
include(":adapters:mc-forge-1-7-10:demo")
}
+if (isAdapterEnabled("MinecraftNeoforge1211")) {
+ include(":adapters:mc-neoforge-1-21-1")
+ include(":adapters:mc-neoforge-1-21-1:demo")
+}