Compare commits

...
Author SHA1 Message Date
MustafarYasmin 580b72829d Merge branch 'core-profile' into core-profile-dsa 2026-02-27 12:54:03 +01:00
Jason Mitchell f31161fcc7 OSX 2026-02-26 18:54:33 -08:00
Jason Mitchell 6e6d90aceb Handle frag only shaders and converge vert shaders on ffp names where applicable 2026-02-26 18:31:33 -08:00
Jason Mitchell d96c10d52f Fix botania shaders
maybe implement glClipPlane
2026-02-26 16:23:36 -08:00
MustafarYasmin ef3f500b1c Small opt 2026-02-26 23:21:55 +01:00
MustafarYasmin be2f76e23e Sync binding 2026-02-26 23:20:04 +01:00
MustafarYasmin 6ef0c77848 Add more DSA support for glTexImage2D and glTexSubImage2D.
Remove some redundant checks.
DSA handle long instead of different buffers now, buffers get converted in GLSM this is to reduce duplicate code in DSA and reduce overhead.
2026-02-26 20:49:44 +01:00
MustafarYasmin 67656e6342 Fix shader 2026-02-26 19:33:04 +01:00
Jason Mitchell 09f1cc7f01 Handle universium shader 2026-02-26 09:16:26 -08:00
Jason Mitchell c8619a5e27 Cloud tweaks 2026-02-25 21:56:25 -08:00
Jason Mitchell bca80d5518 J8 fixes/workarounds 2026-02-25 17:12:36 -08:00
Jason Mitchell 4c0c0970aa More compat
* Update CompatShaderTransformer - tested against all glsl in MM & GT5u; handle `texture` attrib/uniform upgrading in glGetAttribLocation and glGetUniformLocation
* Use reflection to reuse the ContextAttribs and request 4.6 as well
* GLU emulation
2026-02-25 16:04:07 -08:00
user e1d150f4d5 add glTextureImage2DEXT DSA 2026-02-25 14:03:56 -08:00
user 28f62ca04b Get highest GL version(excluding 4.6 since lwjgl2 troll)
# Conflicts:
#	build.gradle.kts
#	src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinForgeHooksClient_CoreProfile.java
2026-02-25 14:03:56 -08:00
Jason Mitchell 75c5b0f26f Handle ARBShaderObjects 2026-02-25 10:22:26 -08:00
Jason Mitchell a62fae73c3 Small optimizations
* Font Glyph: Don't check and get, just get with default
* Cache FrameBuffer Bindings
* Only track AO state when we're under celeritas
2026-02-24 21:56:56 -08:00
Jason Mitchell 16a0e1b8a4 Support glScissor in DisplayLists & emulate glLineStipple 2026-02-24 21:17:24 -08:00
Jason Mitchell 91a45c2b69 Persistent Streaming Buffer for the TessellatorStreamingDrawer 2026-02-24 20:20:12 -08:00
Jason Mitchell 3deec73d3e Address comments - vertex flags, and implement glInterleavedArrays 2026-02-24 17:38:06 -08:00
Jason Mitchell c0ea254d62 Address comments about SubVBO.render() 2026-02-24 08:34:38 -08:00
Jason Mitchell dea84d4279 Subset of glTexGen, better legacy shader compatability, and better client array state handling 2026-02-24 07:52:45 -08:00
Jason MitchellandDeath Fuel 7a2286f3d5 Update src/main/resources/assets/angelica/shaders/fontFilter.fsh
Co-authored-by: Death Fuel <[email protected]>
2026-02-24 07:52:45 -08:00
Jason Mitchell c4c44bd14e Move default VAO to Angelica 2026-02-24 07:52:45 -08:00
Jason Mitchell 936d6f82b3 Splash crash fix 2026-02-24 07:52:45 -08:00
Jason Mitchell 6225fcd344 Target OpenGL 3.3 core profile
* Emulates (most of) the Fixed Function Pipeline - with heavy inspiration from Mesa
2026-02-24 07:52:42 -08:00
Alexdoru 8e26a0179b Speedup block transformer by transforming every class directly (#1420)
* speedup block transformer by transforming every class directly

* stop using the cstpoolparser.add method
2026-02-24 15:45:35 +01:00
Alexdoru 1d96af8e3a re-enable Block Transformer when not running with rfb (#1418) 2026-02-23 14:58:40 -08:00
Yan Huang f7eb86cdb6 Parallelizing CTMUtils (#1393) 2026-02-23 20:21:35 +00:00
Alexdoru b38fafa831 ASM redirectors micro optimizations (#1416)
* redirector transformer micro optimizations

* use map for block fields redirect
2026-02-23 20:20:38 +01:00
Jason Mitchell 7dc2625f8c Maybe less leaky (#1414) 2026-02-23 08:57:21 -08:00
Jason Mitchell 3d97977050 Maybe fix holo inventory again - v2 - final.xlsx (#1413) 2026-02-22 20:37:08 -08:00
Eclipse-Sol 5f41c60bc2 Remove force blend as commit 0325a96b fixed this properly. (#1409) 2026-02-22 11:35:35 -08:00
Jason Mitchell 0325a96bc2 Add unknown blend state and BufferBlendOverride handling from modern Iris (#1408) 2026-02-21 15:27:22 -08:00
Caedis ad123d0bd5 Now with 200% more screen shake (#1402) 2026-02-18 09:20:47 -05:00
Caedis 4b89997985 Add slider for hurt screen shake intensity (#1401) 2026-02-18 04:23:57 +01:00
Death Fuel f5c9fa3301 slightly less stupid glyph size loading (#1390) 2026-02-15 18:49:44 -08:00
Death Fuel d6a6d3b525 Less logging + small refactor (#1379)
* less logging

* refactor

* missed a spot

* consistency
2026-02-15 18:48:51 -08:00
Ruling-0 f079bed977 TESRs Translucency Sorting (#1384)
* Iterate blocks in region towards the camera

* Sort translucent TESRs when translucency sorting is enabled
2026-02-15 18:48:05 -08:00
Caedis c9ec40e94b use mc.renderViewEntity instead of mc.thePlayer in shader shadow rendering (#1392) 2026-02-15 18:47:19 -08:00
embeddedt 8380f3853d Detect quads rendered by vanilla AO pipeline and only recalculate lighting for those (#1391)
* Detect quads rendered by vanilla AO pipeline and only recalculate lighting for those

* Handle oldLighting correctly
2026-02-15 18:46:58 -08:00
Yan Huang 534ae6b05c Remove support for NotEnoughIds-Legacy and update BetterCrashes dependency version, as the old files have been removed from the GTNH source. (#1388) 2026-02-15 09:35:09 -08:00
Jason Mitchell 68d523d3df Sign Culling (#1386) 2026-02-14 04:09:00 -08:00
Jason Mitchell 2f22a7b3ad Swap vanilla clipping helper with joml (#1385) 2026-02-13 14:29:31 -08:00
Jason Mitchell 7b79b90dd6 Fix crash without speedupAnimations enabled (#1377) 2026-02-09 12:00:36 -08:00
Jason Mitchell 417f23f826 Add runtime toggle for HUD Caching, and disable by default (for now at least) (#1374) 2026-02-07 14:44:49 -08:00
TeterosandTeteros ca3c46c873 Add PFAA Geologica cobble/bricks to the BlockCrackFix Whitelist (#1370)
Co-authored-by: Teteros <[email protected]>
2026-02-06 11:36:41 -08:00
Jason Mitchell 82fa9ade2b Add new emitter modes (#1368)
* Indirect
* Individual
2026-02-05 20:58:10 -08:00
Jason Mitchell aaab8fd0bc Thread safe biome cache & VAO Redirection (#1367)
* Thread safe biome cache

* VAO Redirection
* Delegate GLSM VAO calls to UniversalVAO
* Redirect usages of UniversalVAO & LWJGLService VAO functions to GLSM
* Avoid infinite recursion
2026-02-05 13:56:44 -08:00
Jason Mitchell bd56ccc4b8 Totally not experimental 2026-02-04 22:42:03 -08:00
sisyphussy e17f60308d Display List Refactor (#1360) 2026-02-04 10:39:20 -08:00
Kotmatross 1dd98978ad [Hotfix] Fix NTM:Space mixin targets for X5593_H261 (#1356) 2026-02-02 10:02:36 -08:00
Jason Mitchell dae986eb0b Fix crash with clearBufferSubData (#1355) 2026-02-01 12:41:53 -08:00
Jason Mitchell 5fdf84edd0 Camera and Menu Tweaks (#1354) 2026-02-01 12:14:32 -08:00
Jason Mitchell b19d023c11 Bump gtnhgradle 2026-02-01 09:26:19 -08:00
Jason Mitchell 41eafbd151 GLSL Downgrading (#1348) 2026-01-31 13:23:02 -08:00
sisyphussy dd970b4c6a Fix nested display lists rendering twice (#1349)
* Fix nested display lists rendering twice

* cleanup
2026-01-31 05:45:48 -08:00
Eclipse-Sol f79ad7f22a Fix arrows inheriting item material IDs (#1347) 2026-01-30 18:35:04 -08:00
Jason Mitchell 6947ffa3e2 Add IS_IRIS define as an option. 2026-01-30 18:10:27 -08:00
Jason Mitchell 969d2c3710 Backports tessellation shader support, safe zone shadow culling, and shader pack rendering directives. 2026-01-30 18:10:27 -08:00
Jason Mitchell c5988d29c3 Fix camera position and nether portal 3rd party rendering 2026-01-30 18:10:27 -08:00
Jason Mitchell dc68c2c265 Fix end portal w/ Shaders 2026-01-30 18:10:27 -08:00
Jason Mitchell 51533a81ec Misc Shaderpack loading fixes (#1343) 2026-01-30 14:53:41 -08:00
Eclipse-Sol 90eabb5d40 Add hardcoded entities and player_cape (#1327) 2026-01-30 14:53:26 -08:00
sisyphussyandJason Mitchell 74843cb1f9 Glc redirects (#1340)
Co-authored-by: Jason Mitchell <[email protected]>
2026-01-30 14:50:54 -08:00
Death Fuel 5e0344a529 remove unnecessary resource refresh (#1342) 2026-01-30 14:40:51 -08:00
Jason Mitchell 3cdd934987 First pass at setup/begin pass support (#1335) 2026-01-29 07:53:04 -08:00
Léa Gris 9d90ff137c fix(render): adapt hodgepodge fixBottomFaceUV (#1334) 2026-01-28 10:55:53 -08:00
Jason Mitchell bee9541797 Shadow Compute Shaders (#1333) 2026-01-28 00:03:04 -08:00
Jason Mitchell b75a5d8f7d Ya that won't work (#1332) 2026-01-27 15:32:42 -08:00
Jason Mitchell 8d3ab41c53 Use entity position instead of camera pos (#1330) 2026-01-26 22:19:54 -08:00
Jason Mitchell 48d032e654 Tweaks (#1329) 2026-01-26 21:08:09 -08:00
sisyphussy 1968914582 Fix hud flickering (#1325) 2026-01-26 10:19:49 -08:00
Guus Waals a70bd76ff3 Fix deadlock in CTMUtils (#1189) 2026-01-26 10:08:32 -08:00
mist475 db919b5f37 add biomeDictionary and lotr compat to BiomeUniforms (#1324) 2026-01-26 09:58:08 -08:00
MalTeeez cde47ea423 Add option to disable f3 additions (#1308) 2026-01-26 09:56:11 -08:00
Jason Mitchell 5ef6cb11c0 Don't double check infinite bounds (#1321) 2026-01-25 21:13:14 -08:00
Eclipse-Sol b7015bf10e Adds "name_tag" feature (#1318) 2026-01-25 20:00:49 -08:00
Jason Mitchell 46602ab921 Chunk Thread Saftey (#1320) 2026-01-25 19:58:33 -08:00
Jason Mitchell 63e9fa0df1 Dynamic Lights Improvements (#1319) 2026-01-25 19:07:11 -08:00
Eclipse-Sol dcea970a77 Various Shader Fixes (#1315) 2026-01-25 16:56:54 -08:00
Jason Mitchell b29240d6da Use the proper blockAccess on the main thread (#1317) 2026-01-25 16:55:45 -08:00
Jason Mitchell 9d5a02ed21 Optimize TileEntities (#1314) 2026-01-25 10:54:45 -08:00
Jason Mitchell b454483aae Add a nvidia workarounds from celeritas (#1286) 2026-01-24 18:52:52 -08:00
Jason Mitchell 72b13543c6 AABB Caching (#1309) 2026-01-24 18:52:22 -08:00
Jason Mitchell 36d98fcb22 Fix currentRenderedItemId for ItemBlocks (#1307) 2026-01-24 12:43:03 -08:00
Jason Mitchell 44c5b9617a Lighting fixes (#1306) 2026-01-24 12:23:59 -08:00
Kotmatross f3f1677db8 [Shaders] Fix NTM:Space sunset not rendering {2.0 regression} (#1275) 2026-01-24 19:47:30 +00:00
Jason Mitchell d396b611ad Buffer fixes (#1304) 2026-01-24 11:30:05 -08:00
Jason Mitchell 08f4c0f9db Make feature flag defines available in shader.properties preprocessing (#1303) 2026-01-24 10:10:13 -08:00
Eclipse-Sol 50b0d22dc2 Fix entity material handling. (#1298) 2026-01-23 20:24:27 -08:00
Jason Mitchell 7fed54264e No more OpenGLHelper (#1297) 2026-01-23 19:48:52 -08:00
embeddedt be87401be6 Improve behavior of Celeritas-based lighting pipeline 2026-01-23 19:37:57 -08:00
Jason Mitchell 94254006df Properly cleanup (#1294) 2026-01-23 18:19:46 -08:00
Jason Mitchell f79774df65 Respect shader render layer overrides (#1293) 2026-01-23 16:43:01 -08:00
Eclipse-Sol 2f2fc22df3 Add support for dimension.properties (#1283) 2026-01-22 19:17:16 -08:00
Jason Mitchell 26300bbb1b Celeritas Smooth Lighting is now default and working. UseSeparateAO, Emissives, and oldLighting=false now work. 2026-01-22 16:32:15 -08:00
Jason Mitchell 88e4d2fd6c Use Separate AO & SmoothLightPipeline 2026-01-22 16:32:15 -08:00
Jason Mitchell bb52476d27 Custom images, textures, SSBO, and ShaderPack/ShaderProperties backports 2026-01-22 16:32:15 -08:00
Jason Mitchell 6d60477b93 Actually allocate the (minimal) display lists so we stop clobbering the hand renderer on shader reload (#1278) 2026-01-19 13:05:27 -08:00
Jason MitchellandEclipse-Sol 362724610c Add Various Features (#1209) (#1277)
Co-authored-by: Eclipse-Sol <[email protected]>
2026-01-19 13:03:56 -08:00
Jason Mitchell ea4a3cdf0b Fix a race condition (#1273) 2026-01-16 16:46:36 -08:00
Jason Mitchell 65959268d0 Support glCallLists (#1265) 2026-01-13 10:41:09 -08:00
Jason Mitchell 58d820cd7e Fix OSX F3 crash
Fixes #1256
2026-01-13 09:37:28 -08:00
Death Fuel 685d68bd9c Improve glyph replacement logic (#1247) 2026-01-12 15:17:20 -08:00
Jason MitchellandCaedis ad63d2d62c Dynamic light improvements (#1255)
Co-authored-by: Caedis <[email protected]>
2026-01-10 23:50:32 -08:00
Jason Mitchell d2730ccca8 Better video settings search (#1254) 2026-01-10 21:25:03 -08:00
Jason Mitchell e9257b8739 Fixes (#1251) 2026-01-10 15:20:19 -08:00
Jason Mitchell 3984225faa Better OSX J8 Support (#1248) 2026-01-09 12:58:42 -08:00
Jason Mitchell 52db6b56d6 Fix GLSL 120 support (#1243) 2026-01-08 21:34:40 -08:00
381 changed files with 20561 additions and 7785 deletions

No files matched your search

+1
View File
@@ -36,3 +36,4 @@ addon.local.gradle
addon.local.gradle.kts
addon.late.local.gradle
addon.late.local.gradle.kts
local/
+15 -2
View File
@@ -12,6 +12,14 @@
* Complementary + Euphoria Patches is the primary compatibility target
* Sildurs, Seus, and any other shaders might work
## Core Profile
Targets GL 3.3+ core profile
* FFP emulation via generated GLSL shaders
* Display list emulation
* Software matrix stacks and state management through GLSM
* GL_QUADS → GL_TRIANGLES conversion
* Automatic transformation of mod shaders that use compat-only GLSL builtins (`gl_ModelViewMatrix`, `texture2D`, etc.)
# Required Dependencies
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.23
@@ -56,9 +64,9 @@
* The [Iris](https://github.com/IrisShaders/Iris) Team: coderbot, IMS212, Justsnoopy30, FoundationGames
* Asek3 for the Iris Forge port [Oculus](https://github.com/Asek3/Oculus/tree/839ce8eca8cf0c4f6b7a1322b906e5c99125ca69)
* Backported code under the LGPL
* Sodium
* Sodium / Celeritas
* JellySquid & the CaffeineMC team, for making Sodium in the first place
* Embeddedt for [Embeddium](https://github.com/embeddedt/embeddium/tree/dc59ca357c25beefd6288f0d1d40b4cd8e670ab8) under the [LGPL 3.0](https://github.com/embeddedt/embeddium/blob/16.x/forge/LICENSE)
* Embeddedt for Celeritas used under the LGPL 3.0
* Reese's Sodium Options
* FlashyReese for [Reese's Sodium Options](https://github.com/FlashyReese/reeses-sodium-options/tree/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b) under the [MIT license](https://github.com/FlashyReese/reeses-sodium-options/blob/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b/LICENSE.txt)
* Neodymium
@@ -85,3 +93,8 @@
* FalsePattern for [FalseTweaks](https://github.com/falsepattern/falsetweaks) - Item Rendering Optimizations taken from FalseTweaks under the LGPL 3.0 license
* Beddium
* Venn and Falsepattern for [Beddium](https://github.com/vfx-dev/Beddium) - MinimapRenderer, SafeChunkTracker, and some debug commands adapted under the LGPL 3.0 License
* [Mesa](https://gitlab.freedesktop.org/mesa/mesa) — Code ported and/or inspired from Mesa - under the MIT licensed
* Display list ID allocator ported from [util_idalloc](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/util/u_idalloc.c) (Copyright 2017 Valve Corporation)
* FFP vertex shader generation inspired by ffvertex_prog.c (Copyright 2007 VMware, Inc.)
* FFP fragment shader generation inspired by ff_fragment_shader.c (Copyright 2007, 2009 VMware, Inc. / 2010-2011 Intel Corporation)
* Fog parameter optimization inspired by st_nir_lower_fog.c (Copyright 2023 Google LLC) and prog_statevars.c (Copyright 1999-2007 Brian Paul)
+14
View File
@@ -1,3 +1,5 @@
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
plugins {
id("com.gtnewhorizons.gtnhconvention")
}
@@ -45,6 +47,18 @@ tasks.register<Copy>("copyDependencies") {
val embedOnly: Configuration by configurations
// Downgrade embedOnly jars for the tests
val downgradeEmbedOnlyForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = embedOnly
}
tasks.test {
dependsOn(downgradeEmbedOnlyForTest)
classpath = classpath
.minus(embedOnly)
.plus(files(downgradeEmbedOnlyForTest.map { it.outputCollection }))
}
tasks.shadowJar {
from(embedOnly.map(::zipTree))
relocate("com.mitchej123", "com.mitchej123")
+16 -8
View File
@@ -53,6 +53,13 @@ configurations {
embedOnly.canBeResolved = true
compileClasspath.extendsFrom(embedOnly)
testCompileClasspath.extendsFrom(embedOnly)
testRuntimeClasspath.extendsFrom(embedOnly)
}
// Exclude self-reference from all configurations - prevents transitive Angelica deps from resolving against this project
configurations.all {
exclude group: "com.github.GTNewHorizons", module: "Angelica"
}
dependencies {
@@ -65,17 +72,18 @@ dependencies {
// Iris Shaders
compileOnly('org.jetbrains:annotations:26.0.2')
api("com.github.GTNewHorizons:GTNHLib:0.9.0:dev")
api("com.github.GTNewHorizons:GTNHLib:0.9.10:dev")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-25.g3a943bb") {
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
exclude module: "antlr4" // we only want to shadow the runtime module
}
shadowImplementation("org.antlr:antlr4-runtime:4.13.2")
compileOnly "org.apache.ant:ant:1.8.2"
// Celeritas (transitive=false: deps provided by GTNHLib/runtime)
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.0-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-lwjgl2-service:2.5.0-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.2-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-lwjgl2-service:2.5.2-GTNH") { transitive = false }
// Because who doesn't want NEI
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.8.35-GTNH:dev')
@@ -101,14 +109,14 @@ dependencies {
compileOnly("curse.maven:minefactory-reloaded-66672:2277486")
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev") // Mixin Version
compileOnly("com.github.GTNewHorizons:NotEnoughIds-Legacy:1.4.7:dev") // ASM Version
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev")
compileOnly(rfg.deobf("curse.maven:ntm-space-1121235:6974420"))
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5593_H261"))
compileOnly(rfg.deobf("curse.maven:campfirebackport-387444:4611675"))
compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684"))
compileOnly(rfg.deobf("curse.maven:security-craft-64760:2818228"))
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
// runtimeOnlyNonPublishable("com.github.GTNewHorizons:CoreTweaks:0.3.3.6-GTNH")
// For testing alternative splash screens, can be sourced from https://github.com/MalTeeez/ModernSplash/releases
@@ -118,7 +126,7 @@ dependencies {
transformedMod("net.industrial-craft:industrialcraft-2:2.2.828-experimental:dev")
// Better Crashes
compileOnly("com.github.GTNewHorizons:BetterCrashes:1.4.0-GTNH:dev")
compileOnly("com.github.GTNewHorizons:BetterCrashes:1.4.1-GTNH:dev")
afterEvaluate {
dependencies {
+32 -2
View File
@@ -39,11 +39,33 @@ remoteMappings = https\://raw.githubusercontent.com/MinecraftForge/FML/1.7.10/co
# `./gradlew runClient --username=AnotherPlayer`, or configuring this command in your IDE.
developmentEnvironmentUserName = Developer
# Enables modern Java syntax support. Valid values:
# - false: No modern syntax, Java 8 only
# - jabel: Jabel syntax-only support, compiles to J8 bytecode
# - jvmDowngrader: Full modern Java via JVM Downgrader (syntax + stdlib APIs)
# - modern: Native modern Java bytecode, no downgrading
enableModernJavaSyntax = jvmDowngrader
jvmDowngraderStubsProvider = gtnhlib
# If set, ignores the above setting and compiles with the given toolchain. This may cause unexpected issues,
# and should *not* be used in most situations. -1 disables this.
# forceToolchainVersion = -1
# Target JVM version for JVM Downgrader bytecode downgrading.
# Only used when enableModernJavaSyntax = jvmDowngrader
# downgradeTargetVersion = 8
# Comma-separated list of Java versions for multi-release jar support (JVM Downgrader only).
# Classes will be available in META-INF/versions/N/ for each version N in this list.
# Default: "21,25" (J25+ gets native classes, J21-24 gets partial downgrade, J8-20 gets full downgrade).
jvmDowngraderMultiReleaseVersions = 17, 21, 25
#forceToolchainVersion = 25
# Specifies how JVM Downgrader API stubs are provided. Options:
# - shade: Shade minimized stubs into the jar
# - gtnhlib: GTNHLib provides stubs at runtime (adds version constraint)
# - external: Another dependency provides stubs (no constraint, no warning)
# - (empty): Warning reminding you to configure stubs
# Note: 'shade' option requires you to verify license compliance, see: https://github.com/unimined/JvmDowngrader/blob/main/LICENSE.md
jvmDowngraderStubsProvider = gtnhlib
# Enables injecting missing generics into the decompiled source code for a better coding experience.
# Turns most publicly visible List, Map, etc. into proper List<E>, Map<K, V> types.
@@ -57,6 +79,14 @@ generateGradleTokenClass = com.gtnewhorizons.angelica.Tags
# Name of the token containing the project's current version to generate/replace.
gradleTokenVersion = VERSION
# [DEPRECATED]
# Multiple source files can be defined here by providing a comma-separated list: Class1.java,Class2.java,Class3.java
# public static final String VERSION = "GRADLETOKEN_VERSION";
# The string's content will be replaced with your mod's version when compiled. You should use this to specify your mod's
# version in @Mod([...], version = VERSION, [...]).
# Leave these properties empty to skip individual token replacements.
# replaceGradleTokenInFile =
# In case your mod provides an API for other mods to implement you may declare its package here. Otherwise, you can
# leave this property empty.
# Example value: (apiPackage = api) + (modGroup = com.myname.mymodid) -> com.myname.mymodid.api
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+2 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,8 +210,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+1 -2
View File
@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
+1 -1
View File
@@ -16,5 +16,5 @@ pluginManagement {
}
plugins {
id("com.gtnewhorizons.gtnhsettingsconvention") version("2.0.11")
id("com.gtnewhorizons.gtnhsettingsconvention") version("2.0.20")
}
@@ -1,19 +1,21 @@
package com.gtnewhorizons.angelica;
import static com.gtnewhorizons.angelica.AngelicaMod.MOD_ID;
import com.gtnewhorizons.angelica.common.BlockError;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.config.ConfigMigrator;
import com.gtnewhorizons.angelica.proxy.CommonProxy;
import com.gtnewhorizons.angelica.utils.AnimationMode;
import com.gtnewhorizons.angelica.utils.ManagedEnum;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.SidedProxy;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.gtnewhorizons.angelica.AngelicaMod.MOD_ID;
@Mod(
modid = MOD_ID,
@@ -33,6 +35,15 @@ public class AngelicaMod {
public static final String MOD_ID = "angelica";
public static final Logger LOGGER = LogManager.getLogger("Angelica");
private static SodiumGameOptions CONFIG;
public static SodiumGameOptions options() {
if (CONFIG == null) {
CONFIG = SodiumGameOptions.load(ConfigMigrator.handleConfigMigration("angelica-options.json"));
}
return CONFIG;
}
@SidedProxy(clientSide = "com.gtnewhorizons.angelica.proxy.ClientProxy", serverSide = "com.gtnewhorizons.angelica.proxy.CommonProxy")
public static CommonProxy proxy;
@@ -1,11 +1,11 @@
package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
import cpw.mods.fml.client.SplashProgress;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.coderbot.iris.gl.program.Program;
import net.coderbot.iris.gl.program.ProgramBuilder;
@@ -13,8 +13,9 @@ import net.minecraft.client.gui.FontRenderer;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.IOUtils;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import java.io.IOException;
@@ -25,7 +26,13 @@ import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Objects;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
/**
* A batching replacement for {@code FontRenderer}
@@ -50,8 +57,11 @@ public class BatchingFontRenderer {
private final int AAMode;
private final int AAStrength;
private final int texBoundAttrLocation;
private final int mvpMatrixLocation;
private final int fontShaderId;
private static int fontVAO = 0;
private static int vboPositions, vboColors, vboTexCoords, vboTexBounds, vboIndices;
private static int capPositions, capColors, capTexCoords, capTexBounds, capIndices;
final boolean isSGA;
final boolean isSplash;
@@ -76,11 +86,9 @@ public class BatchingFontRenderer {
}
}
public BatchingFontRenderer(FontRenderer underlying, int[] charWidth, byte[] glyphWidth,
int[] colorCode, ResourceLocation locationFontTexture) {
public BatchingFontRenderer(FontRenderer underlying, int[] charWidth, int[] colorCode, ResourceLocation locationFontTexture) {
this.underlying = underlying;
this.charWidth = charWidth;
this.glyphWidth = glyphWidth;
this.colorCode = colorCode;
this.locationFontTexture = locationFontTexture;
@@ -93,13 +101,12 @@ public class BatchingFontRenderer {
FontProviderMC.get(this.isSGA).charWidth = this.charWidth;
FontProviderMC.get(this.isSGA).locationFontTexture = this.locationFontTexture;
FontProviderUnicode.get().glyphWidth = this.glyphWidth;
//noinspection deprecation
fontShaderId = FontAAShader.getProgram().getProgramId();
AAMode = GL20.glGetUniformLocation(fontShaderId, "aaMode");
AAStrength = GL20.glGetUniformLocation(fontShaderId, "strength");
texBoundAttrLocation = GL20.glGetAttribLocation(fontShaderId, "texBounds");
mvpMatrixLocation = GL20.glGetUniformLocation(fontShaderId, "u_MVPMatrix");
}
// === Batched rendering
@@ -108,7 +115,7 @@ public class BatchingFontRenderer {
private int vtxWriterIndex = 0;
private int idxWriterIndex = 0;
private static final int INITIAL_BATCH_SIZE = 256;
private static final int INITIAL_BATCH_SIZE = 2048;
private static final ResourceLocation DUMMY_RESOURCE_LOCATION = new ResourceLocation("angelica$dummy",
"this is invalid!");
private FloatBuffer batchVtxPositions = memAllocFloat(INITIAL_BATCH_SIZE * 2);
@@ -275,9 +282,21 @@ public class BatchingFontRenderer {
}
}
int lastActiveProgram;
int fontAAModeLast = -1;
int fontAAStrengthLast = -1;
private static int streamUpload(int target, int vbo, long address, int bytes, int cap) {
GL15.glBindBuffer(target, vbo);
final ByteBuffer data = memByteBuffer(address, bytes);
if (bytes > cap) {
GL15.glBufferData(target, data, GL15.GL_STREAM_DRAW);
return bytes;
}
GL15.glBufferData(target, cap, GL15.GL_STREAM_DRAW);
GL15.glBufferSubData(target, 0, data);
return cap;
}
private static final Matrix4f scratchMvp = new Matrix4f();
private int fontAAModeLast = -1;
private int fontAAStrengthLast = -1;
private void flushBatch() {
final int prevProgram = GLStateManager.glGetInteger(GL20.GL_CURRENT_PROGRAM);
@@ -295,11 +314,8 @@ public class BatchingFontRenderer {
GLStateManager.tryBlendFuncSeparate(blendSrcRGB, blendDstRGB, GL11.GL_ONE, GL11.GL_ZERO);
GLStateManager.glShadeModel(GL11.GL_FLAT);
final boolean canUseAA = FontConfig.fontAAMode != 0 && prevProgram == 0;
if (canUseAA) {
GL20.glVertexAttribPointer(texBoundAttrLocation, 4, false, 0, batchVtxTexBounds);
GL20.glEnableVertexAttribArray(texBoundAttrLocation);
lastActiveProgram = prevProgram;
final boolean useFontShader = prevProgram == 0;
if (useFontShader) {
GLStateManager.glUseProgram(fontShaderId);
if (FontConfig.fontAAMode != fontAAModeLast) {
fontAAModeLast = FontConfig.fontAAMode;
@@ -309,19 +325,56 @@ public class BatchingFontRenderer {
fontAAStrengthLast = FontConfig.fontAAStrength;
GL20.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
}
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), scratchMvp);
scratchMvp.get(mvpBuf);
GL20.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
}
}
GL13.glClientActiveTexture(GL13.GL_TEXTURE0);
GL11.glTexCoordPointer(2, 0, batchVtxTexCoords);
GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
GL11.glColorPointer(4, GL11.GL_UNSIGNED_BYTE, 0, batchVtxColors);
GL11.glEnableClientState(GL11.GL_COLOR_ARRAY);
GL11.glVertexPointer(2, 0, batchVtxPositions);
GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY);
if (fontVAO == 0) {
fontVAO = org.lwjgl.opengl.GL30.glGenVertexArrays();
vboPositions = GL15.glGenBuffers();
vboColors = GL15.glGenBuffers();
vboTexCoords = GL15.glGenBuffers();
vboTexBounds = GL15.glGenBuffers();
vboIndices = GL15.glGenBuffers();
}
GLStateManager.glBindVertexArray(fontVAO);
final int vtxCount = vtxWriterIndex;
// Location 0: position (vec2, float)
batchVtxPositions.position(0).limit(vtxCount * 2);
capPositions = streamUpload(GL15.GL_ARRAY_BUFFER, vboPositions, memAddress(batchVtxPositions), batchVtxPositions.remaining() * 4, capPositions);
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(0);
// Location 1: color (vec4, unsigned byte normalized)
batchVtxColors.position(0).limit(vtxCount * 4);
capColors = streamUpload(GL15.GL_ARRAY_BUFFER, vboColors, memAddress(batchVtxColors), batchVtxColors.remaining(), capColors);
GL20.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, 0, 0L);
GL20.glEnableVertexAttribArray(1);
// Location 2: texcoord0 (vec2, float)
batchVtxTexCoords.position(0).limit(vtxCount * 2);
capTexCoords = streamUpload(GL15.GL_ARRAY_BUFFER, vboTexCoords, memAddress(batchVtxTexCoords), batchVtxTexCoords.remaining() * 4, capTexCoords);
GL20.glVertexAttribPointer(2, 2, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(2);
// Location 3: texbounds (vec4, float)
batchVtxTexBounds.position(0).limit(vtxCount * 4);
capTexBounds = streamUpload(GL15.GL_ARRAY_BUFFER, vboTexBounds, memAddress(batchVtxTexBounds), batchVtxTexBounds.remaining() * 4, capTexBounds);
GL20.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(3);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY);
GL11.glNormal3f(0.0f, 0.0f, 1.0f);
// Stream upload index buffer
batchIndices.position(0).limit(idxWriterIndex);
capIndices = streamUpload(GL15.GL_ELEMENT_ARRAY_BUFFER, vboIndices, memAddress(batchIndices), batchIndices.remaining() * 4, capIndices);
// Use plain for loop to avoid allocations
final FontDrawCmd[] cmdsData = batchCommands.elements();
@@ -340,19 +393,14 @@ public class BatchingFontRenderer {
}
lastTexture = cmd.texture;
}
batchIndices.limit(cmd.startVtx + cmd.idxCount);
batchIndices.position(cmd.startVtx);
GL11.glDrawElements(GL11.GL_TRIANGLES, batchIndices);
GL11.glDrawElements(GL11.GL_TRIANGLES, cmd.idxCount, GL11.GL_UNSIGNED_INT, (long) cmd.startVtx * 4L);
}
if (canUseAA) {
GLStateManager.glUseProgram(lastActiveProgram);
GL20.glDisableVertexAttribArray(texBoundAttrLocation);
if (useFontShader) {
GLStateManager.glUseProgram(prevProgram);
}
GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
GL11.glDisableClientState(GL11.GL_COLOR_ARRAY);
GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY);
GLStateManager.glBindVertexArray(0);
if (isTextureEnabledBefore) {
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
@@ -366,8 +414,11 @@ public class BatchingFontRenderer {
batchCommands.clear();
vtxWriterIndex = 0;
idxWriterIndex = 0;
batchVtxPositions.limit(batchVtxPositions.capacity());
batchVtxColors.limit(batchVtxColors.capacity());
batchVtxTexCoords.limit(batchVtxTexCoords.capacity());
batchVtxTexBounds.limit(batchVtxTexBounds.capacity());
batchIndices.limit(batchIndices.capacity());
batchIndices.position(0);
}
// === Actual text mesh generation
@@ -433,6 +484,7 @@ public class BatchingFontRenderer {
boolean curUnderline = false;
float glyphScaleY = getGlyphScaleY();
float glyphScaleX = getGlyphScaleX();
float heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY / 2);
final float underlineY = heightNorth + (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY;
@@ -508,10 +560,16 @@ public class BatchingFontRenderer {
continue;
}
if (FontConfig.enableCustomFont) {
if (FontConfig.enableCustomFont && FontConfig.enableGlyphReplacements) {
String chrReplacement = GlyphReplacements.customGlyphs.get(String.valueOf(chr));
if (chrReplacement != null) {
chr = chrReplacement.charAt(0);
char replacement = chrReplacement.charAt(0);
boolean isReplacementCharAvailable =
FontProviderCustom.getPrimary().isGlyphAvailable(replacement)
|| FontProviderCustom.getFallback().isGlyphAvailable(replacement);
if (isReplacementCharAvailable) {
chr = replacement;
}
}
}
@@ -532,8 +590,8 @@ public class BatchingFontRenderer {
final float uStart = fontProvider.getUStart(chr);
final float vStart = fontProvider.getVStart(chr);
final float xAdvance = fontProvider.getXAdvance(chr) * getGlyphScaleX();
final float glyphW = fontProvider.getGlyphW(chr) * getGlyphScaleX();
final float xAdvance = fontProvider.getXAdvance(chr) * glyphScaleX;
final float glyphW = fontProvider.getGlyphW(chr) * glyphScaleX;
final float uSz = fontProvider.getUSize(chr);
final float vSz = fontProvider.getVSize(chr);
final float itOff = curItalic ? 1.0F : 0.0F; // italic offset
@@ -1,6 +1,5 @@
package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
import com.gtnewhorizons.angelica.config.FontConfig;
import lombok.Setter;
import lombok.Value;
@@ -46,7 +45,7 @@ public final class FontProviderCustom implements FontProvider {
default -> null;
};
if (Objects.equals(myFontName, "(none)")) {
font = null;
this.font = null;
return;
}
int fontPos = -1;
@@ -58,10 +57,10 @@ public final class FontProviderCustom implements FontProvider {
}
if (fontPos == -1) {
LOGGER.info("Could not find previously set font \"{}\". ", myFontName);
font = null;
this.font = null;
return;
}
font = availableFonts[fontPos].deriveFont(currentFontQuality);
this.font = availableFonts[fontPos].deriveFont(this.currentFontQuality);
}
private static class InstLoader {
static final FontProviderCustom instance0 = new FontProviderCustom((byte)0);
@@ -71,8 +70,8 @@ public final class FontProviderCustom implements FontProvider {
public static FontProviderCustom getFallback() { return InstLoader.instance1; }
public void reloadFont(int fontID) {
currentFontQuality = FontConfig.customFontQuality;
font = FontStrategist.getAvailableFonts()[fontID].deriveFont(currentFontQuality);
this.currentFontQuality = FontConfig.customFontQuality;
this.font = FontStrategist.getAvailableFonts()[fontID].deriveFont(this.currentFontQuality);
File[] files = new File(getFontDir()).listFiles();
if (files != null) {
@@ -94,7 +93,7 @@ public final class FontProviderCustom implements FontProvider {
}
}
fontAtlases = new FontAtlas[ATLAS_COUNT];
this.fontAtlases = new FontAtlas[ATLAS_COUNT];
}
private String getFontDir() {
@@ -134,50 +133,44 @@ public final class FontProviderCustom implements FontProvider {
}
void construct(Font font) {
float separator = currentFontQuality / 3f;
int atlasChars = 0;
for (int i = 0; i < ATLAS_SIZE; i++) {
final char ch = (char) (i + ATLAS_SIZE * this.id);
if (font.canDisplay(ch)) { atlasChars++; }
}
if (atlasChars == 0) { return; }
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
g2d.dispose();
int width = 0;
int actualChars = 0;
for (int i = 0; i < ATLAS_SIZE; i++) {
final char ch = (char)(i + ATLAS_SIZE * this.id);
if (font.canDisplay(ch)) {
width += (int) (separator + fm.charWidth(ch));
actualChars++;
}
}
if (actualChars == 0) {
return;
}
final int atlasTilesX = (int) Math.ceil(Math.sqrt(actualChars) * 1.5f);
final int atlasTilesY = (int) Math.ceil((double) actualChars / atlasTilesX);
LOGGER.info("constructing custom font atlas (ID {}) of nominal size {} chars, actual: {}, {} rows by {} columns",
this.id, ATLAS_SIZE, actualChars, atlasTilesX, atlasTilesY);
width = 0;
actualChars = 0;
final int atlasTilesX = (int) Math.ceil(Math.sqrt(atlasChars) * 1.5f);
final int atlasTilesY = (int) Math.ceil((double) atlasChars / atlasTilesX);
final float charSeparator = currentFontQuality / 3f;
int rowWidth = 0;
int maxRowWidth = 0;
atlasChars = 0;
for (int i = 0; i < ATLAS_SIZE; i++) {
if (actualChars % atlasTilesX == 0) {
maxRowWidth = Math.max(maxRowWidth, width);
width = 0;
if (atlasChars % atlasTilesX == 0) {
maxRowWidth = Math.max(maxRowWidth, rowWidth);
rowWidth = 0;
}
final char ch = (char)(i + ATLAS_SIZE * this.id);
if (font.canDisplay(ch)) {
width += (int) (separator + fm.charWidth(ch));
actualChars++;
rowWidth += (int) (charSeparator + fm.charWidth(ch));
atlasChars++;
}
}
maxRowWidth = Math.max(maxRowWidth, width);
maxRowWidth = Math.max(maxRowWidth, rowWidth);
final int lineHeight = fm.getHeight();
final float desc = fm.getDescent();
final int imageWidth = (int) (maxRowWidth + separator);
final int imageHeight = (int) ((separator + lineHeight) * atlasTilesY + separator);
final int imageWidth = (int) (maxRowWidth + charSeparator);
final int imageHeight = (int) ((charSeparator + lineHeight) * atlasTilesY + charSeparator);
image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
g2d = image.createGraphics();
@@ -192,35 +185,34 @@ public final class FontProviderCustom implements FontProvider {
fm = g2d.getFontMetrics();
int tileX = 0, tileY = 0; // position in atlas tiles
int imgX = (int) separator; // position in pixels
int imgX = (int) charSeparator; // position in pixels
for (int i = 0; i < ATLAS_SIZE; i++) {
final char ch = (char)(i + ATLAS_SIZE * this.id);
final char ch = (char) (i + ATLAS_SIZE * this.id);
if (!font.canDisplay(ch)) { continue; }
if (tileX >= atlasTilesX) {
tileX = 0;
imgX = (int) separator;
imgX = (int) charSeparator;
tileY++;
}
final int charWidth = fm.charWidth(ch);
final float charAspectRatio = (float) charWidth / lineHeight;
final float inset = currentFontQuality / 16;
g2d.drawString(Character.toString(ch), imgX, (lineHeight + separator) * (tileY + 1) - desc);
g2d.drawString(Character.toString(ch), imgX, (lineHeight + charSeparator) * (tileY + 1) - desc);
final float uStart = (float) (imgX - inset * charAspectRatio) / imageWidth;
final float vStart = ((lineHeight + separator) * (tileY + 1) - lineHeight - inset) / imageHeight;
final float vStart = ((lineHeight + charSeparator) * (tileY + 1) - lineHeight - inset) / imageHeight;
final float xAdvance = charAspectRatio * 8 * charWidth / (charWidth + 2 * inset * charAspectRatio);
final float glyphW = charAspectRatio * 8 + 1;
final float uSz = (float) (charWidth + 2 * inset * charAspectRatio) / imageWidth;
final float vSz = (float) (lineHeight + 2 * inset) / imageHeight;
glyphData[i] = new GlyphData(uStart, vStart, xAdvance, glyphW, uSz, vSz);
imgX += (int) (charWidth + separator);
this.glyphData[i] = new GlyphData(uStart, vStart, xAdvance, glyphW, uSz, vSz);
imgX += (int) (charWidth + charSeparator);
tileX++;
}
g2d.dispose();
try {
LOGGER.info("writing custom font atlas texture ({}x{} px) to {}", imageWidth, imageHeight, getAtlasFullPath(this.id));
Files.createDirectories(Paths.get(getFontDir()));
ImageIO.write(image, "png", new File(getAtlasFullPath(this.id)));
} catch (IOException e) {
@@ -232,18 +224,18 @@ public final class FontProviderCustom implements FontProvider {
private FontAtlas getAtlas(char chr) {
int id = chr / ATLAS_SIZE;
FontAtlas fa = fontAtlases[id];
FontAtlas fa = this.fontAtlases[id];
if (fa == null) {
fa = new FontAtlas(id);
fa.construct(font);
fa.construct(this.font);
this.fontAtlases[id] = fa;
}
fontAtlases[id] = fa;
return fa;
}
@Override
public boolean isGlyphAvailable(char chr) {
if (font == null) { return false; }
if (this.font == null) { return false; }
return (getAtlas(chr).glyphData[chr % ATLAS_SIZE] != null);
}
@@ -28,16 +28,14 @@ public final class FontProviderMC implements FontProvider {
public int[] charWidth;
static {
MCFONT_ASCII_MAP.defaultReturnValue((short) -1);
for (short i = 0; i < MCFONT_CHARS.length(); i++) {
MCFONT_ASCII_MAP.put(MCFONT_CHARS.charAt(i), i);
}
}
public int lookupMcFontPosition(char ch) {
if (isGlyphAvailable(ch)) {
return MCFONT_ASCII_MAP.get(ch);
}
return -1;
return MCFONT_ASCII_MAP.get(ch);
}
@Override
@@ -1,16 +1,31 @@
package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizons.angelica.config.FontConfig;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ResourceLocation;
import java.io.IOException;
import java.io.InputStream;
public final class FontProviderUnicode implements FontProvider {
private FontProviderUnicode() {}
private FontProviderUnicode() {
try {
InputStream inputstream = Minecraft.getMinecraft().getResourceManager()
.getResource(new ResourceLocation("font/glyph_sizes.bin")).getInputStream();
//noinspection ResultOfMethodCallIgnored
inputstream.read(this.glyphWidth);
}
catch (IOException ioexception) {
throw new RuntimeException(ioexception);
}
}
private static class InstLoader { static final FontProviderUnicode instance = new FontProviderUnicode(); }
public static FontProviderUnicode get() { return FontProviderUnicode.InstLoader.instance; }
private static final ResourceLocation[] unicodePageLocations = new ResourceLocation[256];
public byte[] glyphWidth;
private final byte[] glyphWidth = new byte[65536];
private ResourceLocation getUnicodePageLocation(int page) {
final ResourceLocation lookup = unicodePageLocations[page];
@@ -72,7 +72,6 @@ public class FontStrategist {
DefaultResourcePack fontResourcePack = new DefaultResourcePack(packMap);
List defaultResourcePacks = ((ResourceAccessor) Minecraft.getMinecraft()).angelica$getDefaultResourcePacks();
defaultResourcePacks.add(fontResourcePack);
Minecraft.getMinecraft().refreshResources();
}
/**
@@ -0,0 +1,105 @@
package com.gtnewhorizons.angelica.client.gui;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfigStorage;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityTypeEntry;
import jss.notfine.core.Settings;
import me.jellysquid.mods.sodium.client.gui.options.Option;
import me.jellysquid.mods.sodium.client.gui.options.OptionGroup;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpl;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpact;
import me.jellysquid.mods.sodium.client.gui.options.OptionPage;
import me.jellysquid.mods.sodium.client.gui.options.control.TickBoxControl;
import net.minecraft.client.resources.I18n;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class DynamicLightsOptionPages {
public static OptionPage dynamicLights() {
if (!DynamicLights.configEnabled) {
return null;
}
List<OptionGroup> groups = new ArrayList<>();
groups.add(OptionGroup.createBuilder()
.add(Settings.DYNAMIC_LIGHTS.option)
.add(Settings.DYNAMIC_LIGHTS_SHADER_FORCE.option)
.build());
groups.add(OptionGroup.createBuilder()
.add(Settings.DYNAMIC_LIGHTS_FRUSTUM_CULLING.option)
.add(Settings.DYNAMIC_LIGHTS_ADAPTIVE_TICKING.option)
.add(Settings.DYNAMIC_LIGHTS_CULL_TIMEOUT.option)
.build());
groups.add(OptionGroup.createBuilder()
.add(Settings.DYNAMIC_LIGHTS_SLOW_DIST.option)
.add(Settings.DYNAMIC_LIGHTS_SLOWER_DIST.option)
.add(Settings.DYNAMIC_LIGHTS_BACKGROUND_DIST.option)
.build());
// Entity type toggles - grouped by mod ID
List<EntityTypeEntry> allEntities = EntityLightConfig.getAllEntityTypes();
Map<String, List<EntityTypeEntry>> byMod = new LinkedHashMap<>();
for (EntityTypeEntry entry : allEntities) {
byMod.computeIfAbsent(entry.getModId(), _ -> new ArrayList<>()).add(entry);
}
// Create a group for each mod's entities
for (Map.Entry<String, List<EntityTypeEntry>> modEntry : byMod.entrySet()) {
String modId = modEntry.getKey();
List<EntityTypeEntry> entities = modEntry.getValue();
OptionGroup.Builder groupBuilder = OptionGroup.createBuilder();
for (EntityTypeEntry entity : entities) {
Option<Boolean> option = createEntityOption(entity);
if (option != null) {
groupBuilder.add(option);
}
}
// Only add group if it has options
OptionGroup group = groupBuilder.build();
if (!group.getOptions().isEmpty()) {
groups.add(group);
}
}
return new OptionPage(I18n.format("options.dynamiclights.page"), ImmutableList.copyOf(groups));
}
private static Option<Boolean> createEntityOption(EntityTypeEntry entry) {
final String className = entry.getClassName();
// Resolve class once during option creation, not on every get/set
Class<?> resolvedClass;
try {
resolvedClass = Class.forName(className);
} catch (ClassNotFoundException e) {
// Class not loaded, skip this option
return null;
}
final Class<?> entityClass = resolvedClass;
return OptionImpl.createBuilder(Boolean.class, EntityLightConfigStorage.INSTANCE)
.setName(entry.getDisplayName() + " (" + entry.getModId() + ")")
.setTooltip(I18n.format("options.dynamiclights.entity.tooltip", entry.getDisplayName()))
.setControl(TickBoxControl::new)
.setBinding(
(_, value) -> EntityLightConfig.setEntityTypeEnabled(entityClass, value),
storage -> EntityLightConfig.isEntityTypeEnabled(entityClass)
)
.setImpact(OptionImpact.LOW)
.build();
}
}
@@ -1,12 +1,12 @@
package com.gtnewhorizons.angelica.client.gui;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.resources.I18n;
import org.lwjgl.opengl.GL11;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -123,7 +123,7 @@ public class SliderClone extends GuiButton {
this.displayString = displayStringFormatter.apply(f);
}
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
this.drawTexturedModalRect(this.xPosition + (int)(this.sliderPercentage * (float)(this.width - 8)), this.yPosition, 0, 66, 4, 20);
this.drawTexturedModalRect(this.xPosition + (int)(this.sliderPercentage * (float)(this.width - 8)) + 4, this.yPosition, 196, 66, 4, 20);
}
@@ -0,0 +1,55 @@
package com.gtnewhorizons.angelica.compat;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.ARBDebugOutput;
import org.lwjgl.opengl.GL11;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
/**
* Checks for known problematic driver configurations and applies workarounds. Adapted from Embeddium/Sodium's LateDriverScanner.
*
* <p>The NVIDIA workaround can be disabled via: {@code -Dangelica.disableNvidiaWorkaround=true}
*/
public class DriverCompatabilityCheck {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
private static final boolean DISABLE_NVIDIA_WORKAROUND = Boolean.getBoolean("angelica.disableNvidiaWorkaround");
/**
* Should be called after GL context is created and GLStateManager is initialized.
*/
public static void checkDriverCompatibility() {
final String vendor = GL11.glGetString(GL11.GL_VENDOR);
final String renderer = GL11.glGetString(GL11.GL_RENDERER);
final String version = GL11.glGetString(GL11.GL_VERSION);
LOGGER.info("OpenGL Vendor: {}", vendor);
LOGGER.info("OpenGL Renderer: {}", renderer);
LOGGER.info("OpenGL Version: {}", version);
if (vendor != null && vendor.contains("NVIDIA")) {
if (DISABLE_NVIDIA_WORKAROUND) {
LOGGER.info("NVIDIA workaround disabled via -Dangelica.disableNvidiaWorkaround=true");
} else {
applyNvidiaWorkaround();
}
}
}
/**
* https://github.com/godotengine/godot/issues/33969#issuecomment-917846774
* Attempt to force the driver to disable its submission thread by enabling synchronous debug output.
* This usually suppresses bad behavior from NVIDIA's "Threaded Optimizations" feature.
*/
private static void applyNvidiaWorkaround() {
if (!GLStateManager.capabilities.GL_ARB_debug_output) {
LOGGER.warn("Cannot apply NVIDIA workaround: ARB_debug_output not available");
return;
}
LOGGER.info("Enabling workaround for NVIDIA threaded optimizations");
GL11.glEnable(ARBDebugOutput.GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB);
}
}
@@ -6,7 +6,6 @@ import com.gtnewhorizons.neid.mixins.interfaces.IExtendedBlockStorageMixin;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.NibbleArray;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import ru.fewizz.idextender.Hooks;
public class ExtendedBlockStorageExt extends ExtendedBlockStorage {
public boolean hasSky;
@@ -39,12 +38,6 @@ public class ExtendedBlockStorageExt extends ExtendedBlockStorage {
System.arraycopy(((IExtendedBlockStorageMixin)(Object)storage).getBlock16BMetaArray(), 0, block16BMetaArray, 0, block16BMetaArray.length);
}
}
else if (ModStatus.isOldNEIDLoaded){
final short[] blockLSBArray = Hooks.get(this);
System.arraycopy(Hooks.get(storage), 0, blockLSBArray, 0, blockLSBArray.length);
// getBlockMSBArray is nuked in asm version
arrayLen = blockLSBArray.length;
}
else {
final byte[] blockLSBArray = this.getBlockLSBArray();
System.arraycopy(storage.getBlockLSBArray(), 0, blockLSBArray, 0, blockLSBArray.length);
@@ -12,15 +12,8 @@ public class ModStatus {
public static final Logger LOGGER = LogManager.getLogger("ModCompat");
public static BackhandReflectionCompat backhandCompat;
/**
* Mixin Version
*/
public static boolean isNEIDLoaded;
/**
* ASM Version
*/
public static boolean isOldNEIDLoaded;
public static boolean isBetterCrashesLoaded;
public static boolean isNEIDLoaded;
public static boolean isNEIDMetadataExtended;
public static boolean isLotrLoaded;
public static boolean isChunkAPILoaded;
@@ -42,7 +35,6 @@ public class ModStatus {
isBetterCrashesLoaded = Loader.isModLoaded("bettercrashes");
isNEIDLoaded = Loader.isModLoaded("neid");
isOldNEIDLoaded = Loader.isModLoaded("notenoughIDs");
isLotrLoaded = Loader.isModLoaded("lotr");
isChunkAPILoaded = Loader.isModLoaded("chunkapi");
isEIDBiomeLoaded = Loader.isModLoaded("endlessids_biome");
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.compat.iris;
import com.gtnewhorizons.angelica.compat.ModStatus;
import net.coderbot.iris.parsing.BiomeCategories;
import net.minecraft.world.biome.BiomeGenBase;
@@ -8,7 +9,7 @@ import java.util.Map;
/**
* Detects and categorizes biomes from popular modded biome mods using cached class checks.
* Currently supports: BiomesOPlenty (BOP), Realistic World Gen (RWG)
* Currently supports: BiomesOPlenty (BOP), Realistic World Gen (RWG), lotr (b36.15)
*/
public class ModdedBiomeDetector {
private static boolean initialized = false;
@@ -23,12 +24,15 @@ public class ModdedBiomeDetector {
// LinkedHashMap maintains insertion order for deterministic behavior
private static final Map<Class<?>, BiomeCategories> rwgClassMap = new LinkedHashMap<>();
private static Class<?> lotrBiomeClass = null;
private static synchronized void initialize() {
if (initialized) return;
initialized = true;
initializeBOP();
initializeRWG();
initializeLOTR();
}
private static void initializeBOP() {
@@ -59,6 +63,16 @@ public class ModdedBiomeDetector {
tryAddRWGClass("rwg.biomes.base.BaseBiomeSnowForest", BiomeCategories.TAIGA); // Snow forests are taiga
}
private static void initializeLOTR() {
if (ModStatus.isLotrLoaded) {
try {
lotrBiomeClass = Class.forName("lotr.common.world.biome.LOTRBiome");
} catch (ClassNotFoundException ignored) {
// lotr not installed
}
}
}
private static void tryAddRWGClass(String className, BiomeCategories category) {
try {
final Class<?> clazz = Class.forName(className);
@@ -91,12 +105,64 @@ public class ModdedBiomeDetector {
// fall through to name/property-based detection
// Realistic World Gen detection
for (Map.Entry<Class<?>, BiomeCategories> entry : rwgClassMap.entrySet()) {
if (entry.getKey().isInstance(biome)) {
return entry.getValue();
if (biome.getBiomeClass().getName().contains("rwg.biomes")) {
for (Map.Entry<Class<?>, BiomeCategories> entry : rwgClassMap.entrySet()) {
if (entry.getKey().isInstance(biome)) {
return entry.getValue();
}
}
}
// lotr
if (lotrBiomeClass!= null && lotrBiomeClass.isInstance(biome)) {
return detectLOTRBiome(biome);
}
return null;
}
private static BiomeCategories detectLOTRBiome(BiomeGenBase biome) {
return switch (biome.biomeName) {
case "river", "farHaradJungleLake", "lake" -> BiomeCategories.RIVER;
case "rohan", "rivendell", "rhunIsland", "rhunLandHills", "rhunLandSteppe", "rhunLand", "andrast",
"blackrootVale", "lamedon", "imlothMelui", "towerHills", "dorwinion", "dale", "shireMoors", "wold",
"anduinVale", "rhun", "lebennin", "pertorogwaith", "breeland", "island", "pinnathGelin",
"minhiriath", "pelennor", "adornland", "nanCurunir", "ithilienWasteland", "fangornClearing",
"barrowDowns", "dorEnErnilHills", "dorEnErnil", "nurn", "wilderland", "eastBight", "lindon",
"eregion", "enedwaith", "meneltarma", "anduinHills", "eriador", "angle", "loneLandsHills",
"loneLands", "pelargir", "rohanUrukHighlands", "celebrant", "gondor", "shire", "nurnen", "farHarad" ->
BiomeCategories.PLAINS;
case "mistyMountains", "windMountains", "farHaradVolcano", "redMountains", "haradMountains",
"angmarMountains", "blueMountains", "greyMountains", "whiteMountains", "mordorMountains" ->
BiomeCategories.MOUNTAIN;
case "shireWoodlands", "rhunIslandForest", "rhunRedForest", "pukel", "lossarnach", "gulfHaradForest",
"nearHaradFertileForest", "dolGuldur", "rhunForest", "tauredainClearing", "umbarForest",
"farHaradForest", "chetwood", "woodlandRealmHills", "mirkwoodNorth", "whiteDowns", "ithilienHills",
"gondorWoodlands", "rohanWoodlands", "fangornWasteland", "mirkwoodMountains", "lindonWoodlands",
"lothlorienEdge", "erynVorn", "oldForest", "fangorn", "dunland", "ithilien", "mirkwoodCorrupted",
"woodlandRealm", "trollshaws", "lothlorien", "farHaradKanuka" -> BiomeCategories.FOREST;
case "mordor", "lastDesert", "harnedor", "easternDesolation", "morgulVale", "gorgoroth", "udun",
"nearHaradRedDesert", "nearHaradSemiDesert", "lostladen",
"nearHaradHills", "nearHarad", "nanUngol", "dagorlad", "brownLands" -> BiomeCategories.DESERT;
case "ironHills", "rivendellHills", "windMountainsFoothills", "lamedonHills", "redMountainsFoothills",
"tolfalas", "dorwinionHills", "whiteMountainsFoothills", "blueMountainsFoothills",
"greyMountainsFoothills", "mistyMountainsFoothills", "erebor", "angmar", "eriadorDowns",
"ettenmoors", "emynMuil" -> BiomeCategories.EXTREME_HILLS;
case "deadMarshes", "shireMarshes", "farHaradMangrove", "farHaradSwamp", "swanfleet", "nindalf",
"longMarshes", "entwashMouth", "nurnMarshes", "gladdenFields", "midgewater", "anduinMouth" ->
BiomeCategories.SWAMP;
case "harondor", "nearHaradOasis", "nearHaradRiverbank", "farHaradBushlandHills", "farHaradBushland",
"gulfHarad", "nearHaradFertile", "umbarHills", "umbar", "farHaradAridHills", "farHaradArid" ->
BiomeCategories.SAVANNA;
case "ocean" -> BiomeCategories.OCEAN;
case "forodwaith", "forodwaithGlacier", "tundra", "forodwaithMountains" -> BiomeCategories.ICY;
case "lindonCoast", "beachWhite", "farHaradCoast", "forodwaithCoast", "beachGravel", "beach" ->
BiomeCategories.BEACH;
case "coldfells", "wilderlandNorth", "taiga" -> BiomeCategories.TAIGA;
case "farHaradJungle", "halfTrollForest", "farHaradJungleMountains",
"farHaradCloudForest", "farHaradJungleEdge" -> BiomeCategories.JUNGLE;
case "utumno" -> BiomeCategories.NONE;
default -> null;
};
}
}
@@ -19,6 +19,7 @@ public class Camera {
public static final Camera INSTANCE = new Camera();
private final Vector3d pos = new Vector3d();
private final Vector3d entityPos = new Vector3d();
private final BlockPos blockPos = new BlockPos();
private final Vector3f offset = new Vector3f();
private final Matrix4f inverseModelView = new Matrix4f();
@@ -42,12 +43,14 @@ public class Camera {
this.partialTicks = partialTicks;
this.entity = entity;
// Entity position (interpolated, at feet level)
// Entity position (interpolated, at eye level)
final double entityX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks;
final double entityY = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks;
final double entityZ = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * partialTicks;
final double eyeHeight = entity.getEyeHeight();
entityPos.set(entityX, entityY + eyeHeight, entityZ);
thirdPerson = Minecraft.getMinecraft().gameSettings.thirdPersonView > 0;
final double camX, camY, camZ;
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.compat.toremove;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import me.jellysquid.mods.sodium.client.SodiumClientMod;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.renderer.texture.TextureMap;
@@ -147,7 +147,7 @@ public abstract class RenderPhase {
FOG = new Fog("fog", () -> {
// Unclear what this should do
// BackgroundRenderer.setFogBlack(), also levelFogColor()
SodiumClientMod.LOGGER.debug("Fog - Not setting level fog color");
AngelicaMod.LOGGER.debug("Fog - Not setting level fog color");
GLStateManager.enableFog();
}, GLStateManager::disableFog);
BLACK_FOG = new Fog("black_fog", () -> {
@@ -156,7 +156,7 @@ public abstract class RenderPhase {
}, () -> {
// Unclear what this should do
// BackgroundRenderer.setFogBlack(), also levelFogColor()
SodiumClientMod.LOGGER.debug("Fog - Not setting level fog color");
AngelicaMod.LOGGER.debug("Fog - Not setting level fog color");
GLStateManager.disableFog();
});
MAIN_TARGET = new Target("main_target", () -> {
@@ -164,7 +164,7 @@ public abstract class RenderPhase {
});
OUTLINE_TARGET = new Target("outline_target", () -> {
// TODO: Sodium
SodiumClientMod.LOGGER.debug("NOT enabling the entity outline framebuffer");
AngelicaMod.LOGGER.debug("NOT enabling the entity outline framebuffer");
//MinecraftClient.getInstance().worldRenderer.getEntityOutlinesFramebuffer().beginWrite(false);
}, () -> {
Minecraft.getMinecraft().getFramebuffer().bindFramebuffer(false);
@@ -172,7 +172,7 @@ public abstract class RenderPhase {
TRANSLUCENT_TARGET = new Target("translucent_target", () -> {
if (Minecraft.isFancyGraphicsEnabled()) {
// TODO: Sodium
SodiumClientMod.LOGGER.debug("NOT enabling the translucent framebuffer");
AngelicaMod.LOGGER.debug("NOT enabling the translucent framebuffer");
// MinecraftClient.getInstance().worldRenderer.getTranslucentFramebuffer().beginWrite(false);
}
@@ -345,7 +345,7 @@ public abstract class RenderPhase {
super("lightmap", () -> {
if (lightmap) {
// TODO: Sodium - LightmapTextureManager
SodiumClientMod.LOGGER.debug("Lightmap - enable (not implemented)");
AngelicaMod.LOGGER.debug("Lightmap - enable (not implemented)");
// throw new RuntimeException("Not Implemented Yet");
// MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().enable();
@@ -354,7 +354,7 @@ public abstract class RenderPhase {
}, () -> {
if (lightmap) {
// TODO: Sodium - LightmapTextureManager
SodiumClientMod.LOGGER.debug("Lightmap - disable (not implemented)");
AngelicaMod.LOGGER.debug("Lightmap - disable (not implemented)");
// throw new RuntimeException("Not Implemented Yet");
// MinecraftClient.getInstance().gameRenderer.getLightmapTextureManager().disable();
}
@@ -88,12 +88,16 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean enableHudCachingEventTransformer;
@Config.Comment("Enable HUD Caching at runtime. Requires enableHudCaching to be on at startup. [Experimental]")
@Config.DefaultBoolean(false)
public static boolean hudCachingActive;
@Config.Comment("The amount of frames to wait before updating the HUD elements. [Experimental]")
@Config.DefaultInt(20)
@Config.RangeInt(min = 1, max = 60)
public static int hudCachingFPS = 20;
@Config.Comment("Batch drawScreen fonts [Experimental]")
@Config.Comment("Batch drawScreen fonts")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean enableFontRenderer;
@@ -200,7 +204,7 @@ public class AngelicaConfig {
"net.minecraft.block.BlockStairs"
})
public static String[] blockCrackFixBlacklist;
@Config.Comment({"Block classes that have render pass other than 0 but still need to be manipulated.",
"Add a block class here if you see flickering (z-fighting) with fixBlockCrack enabled"
})
@@ -210,6 +214,8 @@ public class AngelicaConfig {
"shukaro.artifice.block.world.BlockOre",
"bartworks.system.material.BWMetaGeneratedOres",
"gtPlusPlus.core.block.base.BlockBaseOre",
"org.pfaa.geologica.block.BrokenGeoBlock",
"org.pfaa.geologica.block.BrickGeoBlock",
})
public static String[] blockCrackFixRenderPassWhitelist__;
@@ -217,8 +223,16 @@ public class AngelicaConfig {
@Config.DefaultBoolean(false)
public static boolean enableHardcodedCustomUniforms;
@Config.Comment("Define IS_IRIS in shader macros.")
@Config.DefaultBoolean(true)
public static boolean defineIsIris;
@Config.Comment("Renders chunks before neighbors are ready. Improves loading at render distance edges, useful for low render distance servers.")
@Config.DefaultBoolean(false)
public static boolean useVanillaChunkTracking;
@Config.Comment("Disables additional F3 information added by Angelica.")
@Config.DefaultBoolean(false)
public static boolean disableF3Additions;
}
@@ -70,4 +70,11 @@ public class FontConfig {
@Config.DefaultFloat(1.5F)
@Config.RangeFloat(min = 0.1F, max = 3F)
public static float customFontScale;
@Config.Comment({"Mods can provide Angelica with a mapping of special characters to either get replaced with " +
"different characters or rendered using the default Unicode font. Setting this to false disables the " +
"replacement, which is useful in cases where custom fonts have direct support for these special characters. ",
"See for additional details: https://github.com/GTNewHorizons/Angelica/issues/1239#issuecomment-3729877936"})
@Config.DefaultBoolean(true)
public static boolean enableGlyphReplacements;
}
@@ -29,6 +29,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import lombok.Getter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ChunkProviderClient;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.world.chunk.EmptyChunk;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
@@ -97,17 +98,18 @@ public class ChunkDebugMinimap {
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.glBegin(GL11.GL_QUADS);
final Tessellator tess = Tessellator.instance;
tess.startDrawingQuads();
final ChunkProviderClient chunkProvider = (ChunkProviderClient) mc.theWorld.getChunkProvider();
for (int x = playerChunkX - RANGE; x < playerChunkX + RANGE; x++) {
for (int z = playerChunkZ - RANGE; z < playerChunkZ + RANGE; z++) {
drawChunk(x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
drawChunk(tess, x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
}
}
GL11.glEnd();
tess.draw();
// Restore GL state
GLStateManager.enableLighting();
@@ -117,7 +119,7 @@ public class ChunkDebugMinimap {
GLStateManager.glPopAttrib();
}
private void drawChunk(int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
private void drawChunk(Tessellator tess, int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
boolean initialized = false;
float r = 0, g = 0, b = 0;
@@ -143,16 +145,16 @@ public class ChunkDebugMinimap {
}
}
GL11.glColor4f(r, g, b, 0.8f);
tess.setColorRGBA_F(r, g, b, 0.8f);
final int xStart = chunkX - playerChunkX;
final int zStart = chunkZ - playerChunkZ;
final int xEnd = xStart + 1;
final int zEnd = zStart + 1;
GLStateManager.glVertex3d(xStart, zStart, 0);
GLStateManager.glVertex3d(xStart, zEnd, 0);
GLStateManager.glVertex3d(xEnd, zEnd, 0);
GLStateManager.glVertex3d(xEnd, zStart, 0);
tess.addVertex(xStart, zStart, 0);
tess.addVertex(xStart, zEnd, 0);
tess.addVertex(xEnd, zEnd, 0);
tess.addVertex(xEnd, zStart, 0);
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.debug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.Tessellator;
@@ -10,31 +11,31 @@ import org.lwjgl.opengl.GL11;
public class F3Direction {
public static void renderWorldDirectionsEvent(Minecraft mc, RenderGameOverlayEvent.Pre event) {
if (mc.gameSettings.showDebugInfo && mc.gameSettings.thirdPersonView == 0) {
ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int width = scaledresolution.getScaledWidth();
int height = scaledresolution.getScaledHeight();
final ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
final int width = scaledresolution.getScaledWidth();
final int height = scaledresolution.getScaledHeight();
GL11.glPushMatrix();
GLStateManager.glPushMatrix();
GL11.glTranslatef((float) (width / 2), (float) (height / 2), -90);
GLStateManager.glTranslatef((float) (width / 2), (float) (height / 2), -90);
Entity entity = mc.renderViewEntity;
GL11.glRotatef(entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * event.partialTicks, -1.0F, 0.0F, 0.0F);
GL11.glRotatef(entity.prevRotationYaw + (entity.rotationYaw - entity.prevRotationYaw) * event.partialTicks, 0.0F, 1.0F, 0.0F);
final Entity entity = mc.renderViewEntity;
GLStateManager.glRotatef(entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * event.partialTicks, -1.0F, 0.0F, 0.0F);
GLStateManager.glRotatef(entity.prevRotationYaw + (entity.rotationYaw - entity.prevRotationYaw) * event.partialTicks, 0.0F, 1.0F, 0.0F);
GL11.glScalef(-1.0F, -1.0F, -1.0F);
GLStateManager.glScalef(-1.0F, -1.0F, -1.0F);
renderWorldDirections();
GL11.glPopMatrix();
GLStateManager.glPopMatrix();
}
}
public static void renderWorldDirections() {
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glDepthMask(false);
Tessellator tessellator = Tessellator.instance;
GLStateManager.disableTexture();
GLStateManager.glDepthMask(false);
final Tessellator tessellator = Tessellator.instance;
GL11.glLineWidth(2.0F);
GLStateManager.glLineWidth(2.0F);
tessellator.startDrawing(GL11.GL_LINES);
//X
@@ -54,8 +55,8 @@ public class F3Direction {
tessellator.draw();
GL11.glLineWidth(1.0F);
GL11.glDepthMask(true);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.glLineWidth(1.0F);
GLStateManager.glDepthMask(true);
GLStateManager.enableTexture();
}
}
@@ -1,44 +1,33 @@
package com.gtnewhorizons.angelica.debug;
import static org.lwjgl.opengl.GL11.GL_ALPHA_TEST;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.opengl.GL11.GL_VERTEX_ARRAY;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glColor4f;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glDisableClientState;
import static org.lwjgl.opengl.GL11.glDrawArrays;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnableClientState;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL20.glDisableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glUniform1;
import static org.lwjgl.opengl.GL20.glUniform1f;
import static org.lwjgl.opengl.GL20.glUniform1i;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.*;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.client.renderer.shader.ShaderProgram;
import java.nio.FloatBuffer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL30;
import java.nio.FloatBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
import static com.gtnewhorizons.angelica.glsm.GLStateManager.glBindBuffer;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glUniform1;
import static org.lwjgl.opengl.GL20.glUniform1f;
import static org.lwjgl.opengl.GL20.glUniform1i;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
public abstract class F3Graph {
private static final int NUM_SAMPLES = 240;
@@ -63,6 +52,7 @@ public abstract class F3Graph {
private int uHeadIdx;
private int uSamples;
private int vertBuf;
private int vao;
private final ResourceLocation texture;
private final float pxPerNs;
private final boolean left;
@@ -117,8 +107,8 @@ public abstract class F3Graph {
"shaders/debug_graph.frag.glsl");
shader.use();
// Register attributes
aPos = shader.getAttribLocation("pos");
// Attribute location is declared in shader via layout(location = 0)
aPos = 0;
// Register uniforms
uFBWidth = shader.getUniformLocation("fbWidth");
@@ -129,7 +119,9 @@ public abstract class F3Graph {
final int uPxPerNs = shader.getUniformLocation("pxPerNs");
final int uLeft = shader.getUniformLocation("left");
// Load vertex buffer
vao = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
vertBuf = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vertBuf);
try (final MemoryStack stack = stackPush()) {
@@ -146,7 +138,13 @@ public abstract class F3Graph {
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
}
// Configure vertex attribute in the VAO
glVertexAttribPointer(aPos, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
glEnableVertexAttribArray(aPos);
glBindBuffer(GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final Minecraft mc = Minecraft.getMinecraft();
final ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
@@ -186,21 +184,21 @@ public abstract class F3Graph {
// Setup GL state
final Tessellator tess = Tessellator.instance;
glDisable(GL_DEPTH_TEST);
glEnable(GL_ALPHA_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.disableDepthTest();
GLStateManager.enableAlphaTest();
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Draw background
glDisable(GL_TEXTURE_2D);
glColor4f(0x50 / 255.0F, 0x50 / 255.0F, 0x50 / 255.0F, 0x90 / 255.0F);
GLStateManager.disableTexture();
GLStateManager.glColor4f(0x50 / 255.0F, 0x50 / 255.0F, 0x50 / 255.0F, 0x90 / 255.0F);
tess.startDrawingQuads();
for (int i = 0; i < 4; ++i) {
tess.addVertex(getVertX(sr, i), getVertY(sr, i), 0);
}
tess.draw();
glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
glEnable(GL_TEXTURE_2D);
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
GLStateManager.enableTexture();
// Draw samples
@@ -213,21 +211,16 @@ public abstract class F3Graph {
glUniform1i(uHeadIdx, samplesHead);
glUniform1(uSamples, sampleBuf);
glBindBuffer(GL_ARRAY_BUFFER, vertBuf);
glEnableVertexAttribArray(aPos);
glVertexAttribPointer(aPos, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
glEnableClientState(GL_VERTEX_ARRAY);
GLStateManager.glBindVertexArray(vao);
// Left side graphs are inverted, so temporarily disable culling
if (!left) glDisable(GL_CULL_FACE);
if (!left) GLStateManager.disableCull();
glDrawArrays(GL_TRIANGLE_STRIP, 0, VERT_COUNT);
GLStateManager.glDrawArrays(GL_TRIANGLE_STRIP, 0, VERT_COUNT);
if (!left) glEnable(GL_CULL_FACE);
if (!left) GLStateManager.enableCull();
glDisableClientState(GL_VERTEX_ARRAY);
glDisableVertexAttribArray(aPos);
glBindBuffer(GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
ShaderProgram.clear();
@@ -242,9 +235,9 @@ public abstract class F3Graph {
tess.draw();
// Reset GL state
glEnable(GL_DEPTH_TEST);
glDisable(GL_ALPHA_TEST);
glDisable(GL_BLEND);
GLStateManager.enableDepthTest();
GLStateManager.disableAlphaTest();
GLStateManager.disableBlend();
// Draw text
// Ensure running counters are reset on first sample
@@ -0,0 +1,70 @@
package com.gtnewhorizons.angelica.dynamiclights;
import org.jetbrains.annotations.NotNull;
/**
* Calculates the appropriate tick mode for a dynamic light source based on distance from camera and position relative to camera direction.
*/
public final class AdaptiveTickCalculator {
// Distance thresholds in blocks (configurable via settings)
private static int slowDistance = 32; // ~2 chunks
private static int slowerDistance = 64; // ~4 chunks
private static int backgroundDistance = 128; // ~8 chunks
// Squared thresholds for efficient distance comparison
private static int slowDistanceSq = slowDistance * slowDistance;
private static int slowerDistanceSq = slowerDistance * slowerDistance;
private static int backgroundDistanceSq = backgroundDistance * backgroundDistance;
private AdaptiveTickCalculator() {}
/**
* Calculate the appropriate tick mode for a light source.
*/
public static AdaptiveTickMode calculate(@NotNull IDynamicLightSource source, double cameraX, double cameraY, double cameraZ, double lookDirX, double lookDirZ) {
double dx = source.angelica$getDynamicLightX() - cameraX;
double dy = source.angelica$getDynamicLightY() - cameraY;
double dz = source.angelica$getDynamicLightZ() - cameraZ;
double distSq = dx * dx + dy * dy + dz * dz;
// Check if behind camera (dot product of direction to source with look direction)
// Only use X and Z for horizontal check (more stable as player looks up/down)
double dot = dx * lookDirX + dz * lookDirZ;
boolean behindCamera = dot < 0;
// Behind camera always gets BACKGROUND mode
if (behindCamera) {
return AdaptiveTickMode.BACKGROUND;
}
// Distance-based mode selection
if (distSq > backgroundDistanceSq) {
return AdaptiveTickMode.BACKGROUND;
}
if (distSq > slowerDistanceSq) {
return AdaptiveTickMode.SLOWER;
}
if (distSq > slowDistanceSq) {
return AdaptiveTickMode.SLOW;
}
return AdaptiveTickMode.REAL_TIME;
}
public static void setSlowDistance(int distance) {
slowDistance = distance;
slowDistanceSq = distance * distance;
}
public static void setSlowerDistance(int distance) {
slowerDistance = distance;
slowerDistanceSq = distance * distance;
}
public static void setBackgroundDistance(int distance) {
backgroundDistance = distance;
backgroundDistanceSq = distance * distance;
}
}
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.dynamiclights;
/**
* Defines tick modes for adaptive dynamic light updates. Distant or off-screen light sources update less frequently to save CPU.
*/
public enum AdaptiveTickMode {
/** Updates every tick - for nearby, visible light sources */
REAL_TIME(1),
/** Updates every 5 ticks - for light sources at moderate distance */
SLOW(5),
/** Updates every 10 ticks - for light sources at far distance */
SLOWER(10),
/** Updates every 20 ticks - for light sources behind the camera or very far */
BACKGROUND(20);
private final int delay;
AdaptiveTickMode(int delay) {
this.delay = delay;
}
public int getDelay() {
return delay;
}
public boolean shouldTickThisFrame(int worldTick, int sourceHash) {
if (delay <= 1) {
return true;
}
return (worldTick % delay) == Math.abs(sourceHash % delay);
}
}
@@ -0,0 +1,152 @@
package com.gtnewhorizons.angelica.dynamiclights;
import com.gtnewhorizon.gtnhlib.util.CoordinatePacker;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongIterator;
import org.embeddedt.embeddium.impl.render.viewport.Viewport;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Manages deferred chunk rebuilds for dynamic lights with optional frustum culling. Instead of scheduling chunk rebuilds immediately
* when light sources move, this tracks pending rebuilds and only schedules visible chunks.
*/
public class ChunkRebuildManager {
/** Maps packed chunk position to ticks waiting */
private final Long2IntOpenHashMap pendingRebuilds = new Long2IntOpenHashMap();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
/** Maximum ticks a pending rebuild can wait before being forced through */
private static int maxTicksWaiting = 100;
/** Reusable lists for processVisible - only used from render thread */
private final LongArrayList candidates = new LongArrayList();
private final LongArrayList toRebuild = new LongArrayList();
private final LongArrayList toIncrement = new LongArrayList();
public static void setMaxTicksWaiting(int ticks) {
maxTicksWaiting = Math.max(20, Math.min(ticks, 600)); // Clamp 1-30 seconds (20-600 ticks)
}
public void requestRebuild(int x, int y, int z) {
final long packed = CoordinatePacker.pack(x, y, z);
lock.writeLock().lock();
try {
// Reset wait counter if already pending, otherwise add new entry
pendingRebuilds.put(packed, 0);
} finally {
lock.writeLock().unlock();
}
}
public void processVisible(Viewport viewport, @NotNull IDynamicLightWorldRenderer renderer) {
candidates.clear();
toRebuild.clear();
toIncrement.clear();
// Phase 1: Collect all pending chunks with read lock (fast)
lock.readLock().lock();
try {
if (pendingRebuilds.isEmpty()) {
return;
}
// Collect candidates that might need processing
final var iterator = pendingRebuilds.long2IntEntrySet().fastIterator();
while (iterator.hasNext()) {
final var entry = iterator.next();
final long packed = entry.getLongKey();
final int ticksWaiting = entry.getIntValue();
// Force process if no viewport or waited too long
if (viewport == null || ticksWaiting >= maxTicksWaiting) {
toRebuild.add(packed);
} else {
candidates.add(packed);
}
}
} finally {
lock.readLock().unlock();
}
// Phase 2: Check visibility
for (int i = 0; i < candidates.size(); i++) {
long packed = candidates.getLong(i);
int x = CoordinatePacker.unpackX(packed);
int y = CoordinatePacker.unpackY(packed);
int z = CoordinatePacker.unpackZ(packed);
if (isChunkVisible(viewport, x, y, z)) {
toRebuild.add(packed);
} else {
toIncrement.add(packed);
}
}
// Phase 3: Update map with write lock (brief)
if (!toRebuild.isEmpty() || !toIncrement.isEmpty()) {
lock.writeLock().lock();
try {
// Remove chunks we're going to rebuild
LongIterator rebuildIter = toRebuild.iterator();
while (rebuildIter.hasNext()) {
pendingRebuilds.remove(rebuildIter.nextLong());
}
LongIterator incIter = toIncrement.iterator();
while (incIter.hasNext()) {
long packed = incIter.nextLong();
int current = pendingRebuilds.get(packed);
if (pendingRebuilds.containsKey(packed)) {
pendingRebuilds.put(packed, current + 1);
}
}
} finally {
lock.writeLock().unlock();
}
}
// Phase 4: Schedule rebuilds outside of lock
for (int i = 0; i < toRebuild.size(); i++) {
final long packed = toRebuild.getLong(i);
renderer.scheduleRebuildForChunk(
CoordinatePacker.unpackX(packed),
CoordinatePacker.unpackY(packed),
CoordinatePacker.unpackZ(packed),
false
);
}
}
private boolean isChunkVisible(Viewport viewport, int chunkX, int chunkY, int chunkZ) {
// Convert chunk section coords to block coords
int blockX = chunkX << 4;
int blockY = chunkY << 4;
int blockZ = chunkZ << 4;
// Check if the 16x16x16 chunk section box is visible
return viewport.isBoxVisible(
blockX, blockY, blockZ,
blockX + 16, blockY + 16, blockZ + 16
);
}
public void clear() {
lock.writeLock().lock();
try {
pendingRebuilds.clear();
} finally {
lock.writeLock().unlock();
}
}
public int getPendingCount() {
lock.readLock().lock();
try {
return pendingRebuilds.size();
} finally {
lock.readLock().unlock();
}
}
}
@@ -8,6 +8,8 @@ import com.gtnewhorizons.angelica.api.IDynamicLightProducer;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.compat.battlegear2.Battlegear2Compat;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
import com.gtnewhorizons.angelica.mixins.interfaces.PrimedEntityAccessor;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import mods.battlegear2.api.core.IBattlePlayer;
@@ -21,12 +23,17 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.client.Minecraft;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemStack;
import net.minecraft.util.MathHelper;
import org.embeddedt.embeddium.impl.render.viewport.Viewport;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Predicate;
@@ -36,6 +43,8 @@ public class DynamicLights {
private static IDynamicLightWorldRenderer activeRenderer;
public static DynamicLightsMode Mode = DynamicLightsMode.OFF;
public static boolean ShaderForce = false;
public static boolean FrustumCullingEnabled = true;
public static boolean AdaptiveTickingEnabled = true;
public static final boolean configEnabled = AngelicaConfig.enableDynamicLights;
@@ -43,6 +52,8 @@ public class DynamicLights {
private static final double MAX_RADIUS_SQUARED = MAX_RADIUS * MAX_RADIUS;
private final Set<IDynamicLightSource> dynamicLightSources = new ObjectOpenHashSet<>();
private final ReentrantReadWriteLock lightSourcesLock = new ReentrantReadWriteLock();
private volatile int lightSourceCount = 0;
private final ChunkRebuildManager chunkRebuildManager = new ChunkRebuildManager();
private long lastUpdate = System.currentTimeMillis();
private int lastUpdateCount = 0;
@@ -60,6 +71,10 @@ public class DynamicLights {
(ShaderForce || !IrisApi.getInstance().isShaderPackInUse());
}
public boolean hasLightSources() {
return lightSourceCount > 0;
}
public static IDynamicLightWorldRenderer getActiveRenderer() {
return activeRenderer;
}
@@ -82,8 +97,43 @@ public class DynamicLights {
this.lastUpdate = now;
this.lastUpdateCount = 0;
// Get camera info for adaptive ticking
double cameraX = 0, cameraY = 0, cameraZ = 0;
double lookDirX = 0, lookDirZ = 1;
int worldTick = 0;
if (AdaptiveTickingEnabled) {
Minecraft mc = Minecraft.getMinecraft();
if (mc.theWorld != null && mc.renderViewEntity != null) {
cameraX = mc.renderViewEntity.posX;
cameraY = mc.renderViewEntity.posY + mc.renderViewEntity.getEyeHeight();
cameraZ = mc.renderViewEntity.posZ;
worldTick = (int) (mc.theWorld.getTotalWorldTime() & 0x7FFFFFFF);
// Calculate look direction from yaw (normalized horizontal direction)
float yaw = mc.renderViewEntity.rotationYaw;
double yawRad = Math.toRadians(yaw);
lookDirX = -Math.sin(yawRad);
lookDirZ = Math.cos(yawRad);
}
}
this.lightSourcesLock.readLock().lock();
for (var lightSource : this.dynamicLightSources) {
// Entity type filter
if (!EntityLightConfig.isEntityTypeEnabled(lightSource)) {
continue;
}
// Adaptive ticking filter
if (AdaptiveTickingEnabled) {
AdaptiveTickMode mode = AdaptiveTickCalculator.calculate(
lightSource, cameraX, cameraY, cameraZ, lookDirX, lookDirZ);
if (!mode.shouldTickThisFrame(worldTick, lightSource.hashCode())) {
continue;
}
}
if (lightSource.angelica$updateDynamicLight(renderer))
this.lastUpdateCount++;
}
@@ -97,7 +147,9 @@ public class DynamicLights {
public void addLightSource(IDynamicLightSource lightSource) {
this.lightSourcesLock.writeLock().lock();
this.dynamicLightSources.add(lightSource);
if (this.dynamicLightSources.add(lightSource)) {
this.lightSourceCount = this.dynamicLightSources.size();
}
this.lightSourcesLock.writeLock().unlock();
}
@@ -115,6 +167,7 @@ public class DynamicLights {
it = dynamicLightSources.next();
if (it.equals(lightSource)) {
dynamicLightSources.remove();
this.lightSourceCount = this.dynamicLightSources.size();
if (activeRenderer != null)
lightSource.angelica$scheduleTrackedChunksRebuild(activeRenderer);
break;
@@ -138,6 +191,7 @@ public class DynamicLights {
it = dynamicLightSources.next();
if (filter.test(it)) {
dynamicLightSources.remove();
this.lightSourceCount = this.dynamicLightSources.size();
if (activeRenderer != null) {
if (it.angelica$getLuminance() > 0)
it.angelica$resetDynamicLight();
@@ -167,6 +221,7 @@ public class DynamicLights {
it.angelica$scheduleTrackedChunksRebuild(activeRenderer);
}
}
this.lightSourceCount = 0;
this.lightSourcesLock.writeLock().unlock();
}
@@ -290,6 +345,51 @@ public class DynamicLights {
return this.getLightmapWithDynamicLight(this.getDynamicLightLevel(x, y, z), lightmap);
}
public List<IDynamicLightSource> getSourcesForChunk(int chunkMinX, int chunkMinY, int chunkMinZ) {
// Chunk center
final double centerX = chunkMinX + 8.0;
final double centerY = chunkMinY + 8.0;
final double centerZ = chunkMinZ + 8.0;
// Max distance from chunk center to any corner is sqrt(3) * 8 ≈ 13.86
// Add MAX_RADIUS to get the search radius (13.86 + 7.75)^2 ≈ 467
final double searchRadiusSq = 470.0; // Slightly padded for safety
List<IDynamicLightSource> result = null;
this.lightSourcesLock.readLock().lock();
try {
for (IDynamicLightSource source : this.dynamicLightSources) {
double dx = source.angelica$getDynamicLightX() - centerX;
double dy = source.angelica$getDynamicLightY() - centerY;
double dz = source.angelica$getDynamicLightZ() - centerZ;
double distSq = dx * dx + dy * dy + dz * dz;
if (distSq <= searchRadiusSq) {
if (result == null) {
result = new ArrayList<>();
}
result.add(source);
}
}
} finally {
this.lightSourcesLock.readLock().unlock();
}
return result != null ? result : Collections.emptyList();
}
public double getDynamicLightLevelFromSources(double x, double y, double z, List<IDynamicLightSource> sources) {
if (sources == null || sources.isEmpty()) {
return 0;
}
double result = 0;
for (int i = 0, size = sources.size(); i < size; i++) {
result = maxDynamicLightLevelExact(x, y, z, sources.get(i), result);
}
return MathHelper.clamp_double(result, 0, 15);
}
/**
* Returns the lightmap with combined light levels.
*
@@ -335,9 +435,32 @@ public class DynamicLights {
}
public static void scheduleChunkRebuild(@NotNull IDynamicLightWorldRenderer renderer, int x, int y, int z) {
if (FrustumCullingEnabled && instance != null) {
instance.chunkRebuildManager.requestRebuild(x, y, z);
} else {
renderer.scheduleRebuildForChunk(x, y, z, false);
}
}
public static void scheduleChunkRebuildForRemoval(@NotNull IDynamicLightWorldRenderer renderer, int x, int y, int z) {
renderer.scheduleRebuildForChunk(x, y, z, false);
}
public static void scheduleChunkRebuildForRemoval(@NotNull IDynamicLightWorldRenderer renderer, long chunkPos) {
scheduleChunkRebuildForRemoval(renderer, CoordinatePacker.unpackX(chunkPos), CoordinatePacker.unpackY(chunkPos), CoordinatePacker.unpackZ(chunkPos));
}
public void processChunkRebuilds(@Nullable Viewport viewport) {
if (activeRenderer == null) {
return;
}
chunkRebuildManager.processVisible(viewport, activeRenderer);
}
public ChunkRebuildManager getChunkRebuildManager() {
return chunkRebuildManager;
}
/**
* Updates the tracked chunk sets.
*
@@ -378,10 +501,7 @@ public class DynamicLights {
* @param submergedInWater {@code true} if the stack is submerged in water, else {@code false}
* @return the luminance of the item
*/
public static int getLuminanceFromItemStack(@NotNull ItemStack stack, boolean submergedInWater) {
// TODO only have certain items not glow in water?
if (submergedInWater) return 0;
public static int getLuminanceFromItemStack(@NotNull ItemStack stack) {
Item item = stack.getItem();
if (item instanceof ItemBlock itemBlock) {
Block block = itemBlock.field_150939_a;
@@ -399,49 +519,53 @@ public class DynamicLights {
public static int getLuminanceFromEntity(@NotNull Entity entity) {
if (entity.fire > 0) return 15;
if (entity.isBurning()) return 15;
boolean inWater = entity.isInsideOfMaterial(Material.water);
// TODO only have certain items not glow in water?
if (inWater) return 0;
if (entity instanceof EntityItem item) {
return getLuminanceFromItemStack(item.getEntityItem(), item.isInsideOfMaterial(Material.water));
return getLuminanceFromItemStack(item.getEntityItem());
}
int luminance = 0;
if (entity instanceof EntityLivingBase living) {
int luminance = 0;
boolean inWater = living.isInsideOfMaterial(Material.water);
// check equipment + hand for light (should work for all entities)
ItemStack itemStack;
for (int i = 0; i < 5; i++) {
if ((itemStack = living.getEquipmentInSlot(i)) != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(itemStack, inWater));
luminance = Math.max(luminance, getLuminanceFromItemStack(itemStack));
}
}
if (ModStatus.isBattlegearLoaded &&
living instanceof EntityPlayer player &&
player instanceof IBattlePlayer battlePlayer &&
battlePlayer.battlegear2$isBattlemode()
) {
ItemStack offhand = Battlegear2Compat.getBattlegear2Offhand(player);
if (offhand != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(offhand, inWater));
if (living instanceof EntityPlayer player) {
if (ModStatus.isBattlegearLoaded &&
player instanceof IBattlePlayer battlePlayer &&
battlePlayer.battlegear2$isBattlemode()
) {
ItemStack offhand = Battlegear2Compat.getBattlegear2Offhand(player);
if (offhand != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(offhand));
}
}
}
else if (ModStatus.isBackhandLoaded && living instanceof EntityPlayer player){
ItemStack offhand = ModStatus.backhandCompat.getOffhandItem(player);
if (offhand != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(offhand, inWater));
else if (ModStatus.isBackhandLoaded){
ItemStack offhand = ModStatus.backhandCompat.getOffhandItem(player);
if (offhand != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(offhand));
}
}
}
if (ModStatus.isBaublesLoaded && living instanceof EntityPlayer player){
var playerBaubles = PlayerHandler.getPlayerBaubles(player);
if (playerBaubles != null){
for (int i = 0; i < playerBaubles.getSizeInventory(); i++){
var stack = playerBaubles.getStackInSlot(i);
if (stack != null){
luminance = Math.max(luminance, getLuminanceFromItemStack(stack, inWater));
if (ModStatus.isBaublesLoaded){
var playerBaubles = PlayerHandler.getPlayerBaubles(player);
if (playerBaubles != null){
for (int i = 0; i < playerBaubles.getSizeInventory(); i++){
var stack = playerBaubles.getStackInSlot(i);
if (stack != null){
luminance = Math.max(luminance, getLuminanceFromItemStack(stack));
}
}
}
}
@@ -451,7 +575,10 @@ public class DynamicLights {
}
// TODO: Creepers? TNT? Dusts?
if (entity instanceof PrimedEntityAccessor primedEntity) {
luminance = Math.max(luminance, primedEntity.angelica$getLuminance());
}
return 0;
return luminance;
}
}
@@ -73,6 +73,7 @@ public interface IDynamicLightSource {
* Executed at each tick.
*/
void angelica$dynamicLightTick();
default void angelica$updateLights() {}
boolean angelica$updateDynamicLight(@NotNull IDynamicLightWorldRenderer renderer);
@@ -0,0 +1,187 @@
package com.gtnewhorizons.angelica.dynamiclights.config;
import com.gtnewhorizon.gtnhlib.concurrent.cas.CasMap;
import com.gtnewhorizons.angelica.dynamiclights.IDynamicLightSource;
import cpw.mods.fml.common.registry.EntityRegistry;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class EntityLightConfig {
private static final Logger LOGGER = LogManager.getLogger("Angelica/DynamicLights");
private static final Set<String> disabledClassNames = ConcurrentHashMap.newKeySet();
private static final CasMap<Class<?>, Boolean> enabledCache = new CasMap<>();
private static File configFile;
private static boolean initialized = false;
private EntityLightConfig() {}
public static void init(@NotNull File configDir) {
if (initialized) return;
configFile = new File(configDir, "angelica-dynamiclights-entities.txt");
load();
initialized = true;
}
public static boolean isEntityTypeEnabled(@NotNull IDynamicLightSource source) {
if (!initialized) return true;
return isEntityTypeEnabled(source.getClass());
}
public static boolean isEntityTypeEnabled(@NotNull Class<?> entityClass) {
if (!initialized) return true;
Boolean cached = enabledCache.get(entityClass);
if (cached != null) {
return cached;
}
final boolean enabled = !disabledClassNames.contains(entityClass.getName());
Boolean existing = enabledCache.putIfAbsent(entityClass, enabled);
return existing != null ? existing : enabled;
}
public static void setEntityTypeEnabled(@NotNull Class<?> entityClass, boolean enabled) {
enabledCache.put(entityClass, enabled);
final String className = entityClass.getName();
if (enabled) {
disabledClassNames.remove(className);
} else {
disabledClassNames.add(className);
}
}
@SuppressWarnings("unchecked")
public static List<EntityTypeEntry> getAllEntityTypes() {
List<EntityTypeEntry> entries = new ArrayList<>();
Set<String> seenClasses = new HashSet<>();
Map<String, Class<? extends Entity>> stringToClass = EntityList.stringToClassMapping;
for (Map.Entry<String, Class<? extends Entity>> entry : stringToClass.entrySet()) {
String entityId = entry.getKey();
Class<?> entityClass = entry.getValue();
String className = entityClass.getName();
if (seenClasses.contains(className)) continue;
seenClasses.add(className);
String modId = getModIdForEntity(entityClass, entityId);
String displayName = getDisplayName(entityId, entityClass);
boolean enabled = !disabledClassNames.contains(className);
entries.add(new EntityTypeEntry(displayName, className, modId, enabled));
}
// Sort by mod ID, then by display name
entries.sort((a, b) -> {
int modCompare = a.getModId().compareToIgnoreCase(b.getModId());
if (modCompare != 0) return modCompare;
return a.getDisplayName().compareToIgnoreCase(b.getDisplayName());
});
return entries;
}
@SuppressWarnings("unchecked")
private static String getModIdForEntity(Class<?> entityClass, String entityId) {
// Check EntityRegistry for mod entity entries
if (Entity.class.isAssignableFrom(entityClass)) {
EntityRegistry.EntityRegistration registration = EntityRegistry.instance()
.lookupModSpawn((Class<? extends Entity>) entityClass, false);
if (registration != null && registration.getContainer() != null) {
return registration.getContainer().getModId();
}
}
// Check if it's a vanilla entity (no dot in entity ID)
if (!entityId.contains(".")) {
return "minecraft";
}
// Extract mod ID from entity ID (format: modid.EntityName)
int dotIndex = entityId.indexOf('.');
if (dotIndex > 0) {
return entityId.substring(0, dotIndex);
}
return "unknown";
}
private static String getDisplayName(String entityId, Class<?> entityClass) {
// Use the simple class name, removing package
String simpleName = entityClass.getSimpleName();
if (simpleName.isEmpty()) {
// Anonymous class, use entity ID
return entityId;
}
return simpleName;
}
public static void load() {
if (configFile == null || !configFile.exists()) return;
disabledClassNames.clear();
enabledCache.clear();
try (BufferedReader reader = new BufferedReader(new FileReader(configFile))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) continue;
// Lines are entity class names that are DISABLED
disabledClassNames.add(line);
}
LOGGER.info("Loaded {} disabled entity types from config", disabledClassNames.size());
} catch (IOException e) {
LOGGER.warn("Failed to load entity light config: {}", e.getMessage());
}
}
public static void save() {
if (configFile == null) return;
try (BufferedWriter writer = new BufferedWriter(new FileWriter(configFile))) {
writer.write("# Angelica Dynamic Lights - Disabled Entity Types");
writer.newLine();
writer.write("# Each line is a fully qualified class name of an entity type that is DISABLED.");
writer.newLine();
writer.write("# Remove a line to re-enable dynamic lighting for that entity type.");
writer.newLine();
writer.newLine();
List<String> sorted = new ArrayList<>(disabledClassNames);
Collections.sort(sorted);
for (String className : sorted) {
writer.write(className);
writer.newLine();
}
LOGGER.info("Saved {} disabled entity types to config", disabledClassNames.size());
} catch (IOException e) {
LOGGER.warn("Failed to save entity light config: {}", e.getMessage());
}
}
}
@@ -0,0 +1,20 @@
package com.gtnewhorizons.angelica.dynamiclights.config;
import me.jellysquid.mods.sodium.client.gui.options.storage.OptionStorage;
public class EntityLightConfigStorage implements OptionStorage<EntityLightConfigStorage> {
public static final EntityLightConfigStorage INSTANCE = new EntityLightConfigStorage();
private EntityLightConfigStorage() {}
@Override
public EntityLightConfigStorage getData() {
return this;
}
@Override
public void save() {
EntityLightConfig.save();
}
}
@@ -0,0 +1,42 @@
package com.gtnewhorizons.angelica.dynamiclights.config;
import org.jetbrains.annotations.NotNull;
public class EntityTypeEntry {
private final String displayName;
private final String className;
private final String modId;
private boolean enabled;
public EntityTypeEntry(@NotNull String displayName, @NotNull String className, @NotNull String modId, boolean enabled) {
this.displayName = displayName;
this.className = className;
this.modId = modId;
this.enabled = enabled;
}
public String getDisplayName() {
return displayName;
}
public String getClassName() {
return className;
}
public String getModId() {
return modId;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public String toString() {
return displayName + " (" + modId + ") [" + (enabled ? "ON" : "OFF") + "]";
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.opengl.UniversalVAO;
import com.mitchej123.glsm.GLStateManagerService;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -149,17 +150,17 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public int glGenVertexArrays() {
return GL30.glGenVertexArrays();
return UniversalVAO.genVertexArrays();
}
@Override
public void glBindVertexArray(int array) {
GL30.glBindVertexArray(array);
GLStateManager.glBindVertexArray(array);
}
@Override
public void glDeleteVertexArrays(int array) {
GL30.glDeleteVertexArrays(array);
GLStateManager.glDeleteVertexArrays(array);
}
// ===================== BUFFER OPERATIONS =====================
@@ -171,7 +172,7 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public void glBindBuffer(int target, int buffer) {
GL15.glBindBuffer(target, buffer);
GLStateManager.glBindBuffer(target, buffer);
}
@Override
@@ -1,238 +1,67 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw;
import com.gtnewhorizons.angelica.glsm.recording.CommandBuffer;
import com.gtnewhorizons.angelica.glsm.recording.CommandBufferProcessor;
import com.gtnewhorizons.angelica.glsm.recording.CommandBufferProcessor.BufferTransformOptimizer;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* Builds final command buffers from raw command buffers and compiled VBO data.
* Transforms are already collapsed to MultMatrix during recording; the optimizer
* tracks transform state to emit deltas when draws need a different transform.
* Bridges the gap between VBO compilation (glsm types) and buffer processing (recording types).
*/
public final class CommandBufferBuilder {
private static final Logger LOGGER = LogManager.getLogger("CommandBufferBuilder");
private CommandBufferBuilder() {}
/** Build with transform delta optimization and merged draw ranges. */
public static void buildOptimizedFromRawBuffer(
/**
* Build commands with transform collapsing optimization.
* Accumulates MODELVIEW transforms and emits MultMatrix at barriers (draws, CallList).
* Uses mergedRanges where consecutive same-transform draws are combined.
*/
public static void buildFromRawBuffer(
CommandBuffer rawBuffer,
Map<CapturingTessellator.Flags, CompiledFormatBuffer> compiledQuadBuffers,
final CompiledLineBuffer compiledLineBuffer,
final Map<CapturingTessellator.Flags, CompiledPrimitiveBuffers> compiledPrimitiveBuffers,
VertexBuffer[] ownedVbos,
int glListId,
List<AccumulatedDraw> accumulatedDraws,
CommandBuffer finalBuffer) {
// Build VBO index map for efficient lookup (no boxing)
final Object2IntMap<VertexBuffer> vboIndexMap = new Object2IntOpenHashMap<>();
vboIndexMap.defaultReturnValue(-1);
for (int i = 0; i < ownedVbos.length; i++) {
vboIndexMap.put(ownedVbos[i], i);
}
// Collect all merged draw ranges sorted by command index
final List<DrawRangeWithBuffer> allRanges = collectDrawRanges(compiledQuadBuffers, compiledLineBuffer, compiledPrimitiveBuffers);
// Buffer optimizer state
final BufferTransformOptimizer opt = new BufferTransformOptimizer();
rawBuffer.resetRead();
int cmdIndex = 0;
int rangeIndex = 0;
AccumulatedDraw draw;
while (rawBuffer.hasRemaining()) {
// Emit draw ranges at this command position
while (rangeIndex < allRanges.size() && allRanges.get(rangeIndex).range().commandIndex() == cmdIndex) {
emitDrawRangeToBuffer(allRanges.get(rangeIndex++), opt, finalBuffer, vboIndexMap);
}
// Read and process the command
final int opcode = rawBuffer.readInt();
CommandBufferProcessor.processCommandForOptimization(opcode, rawBuffer, opt, finalBuffer);
cmdIndex++;
}
// Emit remaining draw ranges at end of command stream
while (rangeIndex < allRanges.size()) {
emitDrawRangeToBuffer(allRanges.get(rangeIndex++), opt, finalBuffer, vboIndexMap);
}
// Emit residual transform if not identity
if (!opt.isIdentity()) {
opt.emitPendingTransform(finalBuffer);
}
}
/** Debug build: no optimizations, 1:1 draw commands. */
public static void buildUnoptimizedFromRawBuffer(
CommandBuffer rawBuffer,
Map<CapturingTessellator.Flags, CompiledFormatBuffer> compiledQuadBuffers,
CompiledLineBuffer compiledLineBuffer,
Map<CapturingTessellator.Flags, CompiledPrimitiveBuffers> compiledPrimitiveBuffers,
VertexBuffer[] ownedVbos,
CommandBuffer finalBuffer) {
// Build VBO index map for efficient lookup (no boxing)
final Object2IntMap<VertexBuffer> vboIndexMap = new Object2IntOpenHashMap<>();
vboIndexMap.defaultReturnValue(-1);
for (int i = 0; i < ownedVbos.length; i++) {
vboIndexMap.put(ownedVbos[i], i);
}
// Collect per-draw ranges (no merging) sorted by command index
final List<DrawRangeWithBuffer> allRanges = collectDrawRangesUnoptimized(compiledQuadBuffers, compiledLineBuffer, compiledPrimitiveBuffers);
rawBuffer.resetRead();
int cmdIndex = 0;
int rangeIndex = 0;
while (rawBuffer.hasRemaining()) {
// Emit draw ranges at this command position
while (rangeIndex < allRanges.size() && allRanges.get(rangeIndex).range().commandIndex() == cmdIndex) {
final DrawRangeWithBuffer drb = allRanges.get(rangeIndex++);
final int vboIdx = vboIndexMap.getInt(drb.vbo());
if (vboIdx >= 0) {
finalBuffer.writeDrawRange(vboIdx, drb.range().startVertex(), drb.range().vertexCount(), drb.hasBrightness());
DisplayListManager.trackDrawRangeSource(drb.source().toString());
}
}
// Copy the command directly
CommandBufferProcessor.copyCommand(rawBuffer, finalBuffer);
// Emit draw ranges at this command position
while (rangeIndex < accumulatedDraws.size() && (draw = accumulatedDraws.get(rangeIndex)).commandIndex == cmdIndex) {
emitDrawRangeToBuffer(draw, finalBuffer, rangeIndex);
rangeIndex++;
}
cmdIndex++;
}
// Emit remaining draw ranges at end of command stream
while (rangeIndex < allRanges.size()) {
final DrawRangeWithBuffer drb = allRanges.get(rangeIndex++);
final int vboIdx = vboIndexMap.getInt(drb.vbo());
if (vboIdx >= 0) {
finalBuffer.writeDrawRange(vboIdx, drb.range().startVertex(), drb.range().vertexCount(), drb.hasBrightness());
DisplayListManager.trackDrawRangeSource(drb.source().toString());
}
while (rangeIndex < accumulatedDraws.size()) {
emitDrawRangeToBuffer(accumulatedDraws.get(rangeIndex), finalBuffer, rangeIndex);
rangeIndex++;
}
}
/** Collect merged draw ranges, sorted by command index. */
static List<DrawRangeWithBuffer> collectDrawRanges(
Map<CapturingTessellator.Flags, CompiledFormatBuffer> compiledQuadBuffers,
CompiledLineBuffer compiledLineBuffer,
Map<CapturingTessellator.Flags, CompiledPrimitiveBuffers> compiledPrimitiveBuffers) {
return collectDrawRangesInternal(compiledQuadBuffers, compiledLineBuffer, compiledPrimitiveBuffers, true);
}
/** Collect unmerged draw ranges for debug mode. */
static List<DrawRangeWithBuffer> collectDrawRangesUnoptimized(
Map<CapturingTessellator.Flags, CompiledFormatBuffer> compiledQuadBuffers,
CompiledLineBuffer compiledLineBuffer,
Map<CapturingTessellator.Flags, CompiledPrimitiveBuffers> compiledPrimitiveBuffers) {
return collectDrawRangesInternal(compiledQuadBuffers, compiledLineBuffer, compiledPrimitiveBuffers, false);
}
private static List<DrawRangeWithBuffer> collectDrawRangesInternal(
Map<CapturingTessellator.Flags, CompiledFormatBuffer> compiledQuadBuffers,
CompiledLineBuffer compiledLineBuffer,
Map<CapturingTessellator.Flags, CompiledPrimitiveBuffers> compiledPrimitiveBuffers,
boolean useMerged) {
final List<DrawRangeWithBuffer> allRanges = new ArrayList<>();
// Add quad ranges
final CompiledFormatBuffer[] quadBufferArray = compiledQuadBuffers.values().toArray(new CompiledFormatBuffer[0]);
for (int b = 0; b < quadBufferArray.length; b++) {
final CompiledFormatBuffer compiledBuffer = quadBufferArray[b];
final DrawRange[] ranges = useMerged ? compiledBuffer.mergedRanges() : compiledBuffer.perDrawRanges();
for (int i = 0; i < ranges.length; i++) {
allRanges.add(new DrawRangeWithBuffer(ranges[i], compiledBuffer.vbo(), compiledBuffer.flags(),
DrawRangeWithBuffer.DrawSource.TESSELLATOR_QUADS));
}
}
// Add immediate mode line ranges
// Lines use a minimal flags (color only, no texture/brightness/normals)
if (compiledLineBuffer != null) {
final CapturingTessellator.Flags lineFlags = new CapturingTessellator.Flags(false, false, true, false);
final DrawRange[] ranges = useMerged ? compiledLineBuffer.mergedRanges() : compiledLineBuffer.perDrawRanges();
for (int i = 0; i < ranges.length; i++) {
allRanges.add(new DrawRangeWithBuffer(ranges[i], compiledLineBuffer.vbo(), lineFlags,
DrawRangeWithBuffer.DrawSource.IMMEDIATE_MODE_LINES));
}
}
// Add tessellator primitive ranges (lines and triangles) - grouped by format
if (compiledPrimitiveBuffers != null && !compiledPrimitiveBuffers.isEmpty()) {
final CompiledPrimitiveBuffers[] primBufferArray = compiledPrimitiveBuffers.values().toArray(new CompiledPrimitiveBuffers[0]);
for (int p = 0; p < primBufferArray.length; p++) {
final CompiledPrimitiveBuffers primBuffers = primBufferArray[p];
final CapturingTessellator.Flags primFlags = primBuffers.flags();
// Tessellator lines
if (primBuffers.hasLines()) {
final DrawRange[] ranges = useMerged ? primBuffers.lineMergedRanges() : primBuffers.linePerDrawRanges();
final VertexBuffer vbo = primBuffers.lineVbo();
for (int i = 0; i < ranges.length; i++) {
allRanges.add(new DrawRangeWithBuffer(ranges[i], vbo, primFlags,
DrawRangeWithBuffer.DrawSource.TESSELLATOR_LINES));
}
}
// Tessellator triangles
if (primBuffers.hasTriangles()) {
final DrawRange[] ranges = useMerged ? primBuffers.triangleMergedRanges() : primBuffers.trianglePerDrawRanges();
final VertexBuffer vbo = primBuffers.triangleVbo();
for (int i = 0; i < ranges.length; i++) {
allRanges.add(new DrawRangeWithBuffer(ranges[i], vbo, primFlags,
DrawRangeWithBuffer.DrawSource.TESSELLATOR_TRIANGLES));
}
}
}
}
allRanges.sort(Comparator.comparingInt(r -> r.range().commandIndex()));
return allRanges;
}
/** Emit draw range with delta transform. Uses writeDrawRangeRestore for immediate mode. */
/**
* Emit a draw range to the output buffer, with transform if needed.
*/
private static void emitDrawRangeToBuffer(
DrawRangeWithBuffer drb,
BufferTransformOptimizer opt,
AccumulatedDraw draw,
CommandBuffer out,
Object2IntMap<VertexBuffer> vboIndexMap) {
int vboIdx) {
// Emit transform to reach the draw's expected transform
opt.emitTransformTo(out, drb.range().transform());
if (draw.restoreData != null) {
out.writeDrawRangeRestore(vboIdx, draw.restoreData);
return;
}
// Write the draw range command
final int vboIdx = vboIndexMap.getInt(drb.vbo());
if (vboIdx >= 0) {
final var restoreData = drb.range().restoreData();
if (restoreData != null) {
// Immediate mode draw with attribute restoration
final var flags = drb.flags();
out.writeDrawRangeRestore(
vboIdx, drb.range().startVertex(), drb.range().vertexCount(),
drb.hasBrightness(), flags.hasColor, flags.hasNormals, flags.hasTexture,
restoreData.lastColorR(), restoreData.lastColorG(),
restoreData.lastColorB(), restoreData.lastColorA(),
restoreData.lastNormalX(), restoreData.lastNormalY(), restoreData.lastNormalZ(),
restoreData.lastTexCoordS(), restoreData.lastTexCoordT()
);
} else {
// Regular tessellator draw (no restoration needed)
out.writeDrawRange(vboIdx, drb.range().startVertex(), drb.range().vertexCount(), drb.hasBrightness());
}
DisplayListManager.trackDrawRangeSource(drb.source().toString());
}
out.writeDrawRange(vboIdx);
}
}
@@ -0,0 +1,437 @@
package com.gtnewhorizons.angelica.glsm;
import net.minecraft.launchwrapper.Launch;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.Transformer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
/**
* Compat transformation for mod shaders running under core profile.
*
* <p>Handles:
* <ul>
* <li>Matrix builtin replacement (gl_ModelViewMatrix, gl_ProjectionMatrix, etc.)</li>
* <li>Vertex attribute replacement (gl_Vertex, gl_Color, gl_MultiTexCoord0/1, gl_Normal)</li>
* <li>gl_TexCoord[N] varying array → per-index in/out declarations</li>
* <li>Texture function renames (texture2D → texture, etc.)</li>
* <li>Fragment output handling (gl_FragColor → layout-qualified out declarations)</li>
* <li>Fog builtins (gl_Fog, gl_FogFragCoord)</li>
* <li>gl_FrontColor → local variable in vertex shaders</li>
* <li>shadow2D/shadow2DLod → texture/textureLod with vec4 wrapping</li>
* <li>Version upgrade to 330 core minimum</li>
* <li>Reserved word pre-parse renaming (texture-as-variable, sample, etc.)</li>
* </ul>
*/
public class CompatShaderTransformer {
private static final Pattern VERSION_PATTERN =
Pattern.compile("#version\\s+(\\d+)(?:\\s+(\\w+))?");
private static final Pattern DEFINE_PATTERN =
Pattern.compile("^\\s*#\\s*define\\s+.+$", Pattern.MULTILINE);
/** Compat builtins that trigger AST transformation. */
private static final Set<String> COMPAT_BUILTINS = Set.of(
"gl_ModelView", "gl_Projection", "gl_NormalMatrix", "gl_TextureMatrix",
"gl_FragColor", "gl_Fog", "gl_FrontColor", "gl_Color",
"gl_Vertex", "gl_MultiTexCoord", "gl_TexCoord", "gl_Normal", "ftransform",
"texture2D", "texture3D", "texelFetch2D", "texelFetch3D", "textureSize2D",
"shadow2D", "gl_FrontLightModelProduct"
);
private static final Pattern NEEDS_TRANSFORM_PATTERN = Pattern.compile(
String.join("|", COMPAT_BUILTINS) + "|\\b(?:attribute|varying)\\b"
);
private static final Map<String, String> MATRIX_RENAMES = Map.of(
"gl_ModelViewMatrix", "angelica_ModelViewMatrix",
"gl_ModelViewMatrixInverse", "angelica_ModelViewMatrixInverse",
"gl_ProjectionMatrix", "angelica_ProjectionMatrix",
"gl_ProjectionMatrixInverse", "angelica_ProjectionMatrixInverse",
"gl_NormalMatrix", "angelica_NormalMatrix"
);
private static final Path DUMP_DIR;
private static final AtomicInteger dumpCounter = new AtomicInteger(0);
static {
boolean isDev = false;
try {
final Object deobfEnv = Launch.blackboard != null ? Launch.blackboard.get("fml.deobfuscatedEnvironment") : null;
isDev = Boolean.TRUE.equals(deobfEnv);
} catch (Exception ignored) {}
DUMP_DIR = (isDev || Boolean.parseBoolean(System.getProperty("angelica.compat.dumpShaders", "false")))
? Paths.get("compat_shaders") : null;
}
private static final int CACHE_SIZE = 32;
private record CacheKey(String source, boolean isFragment) {}
private static final Map<CacheKey, String> cache = Collections.synchronizedMap(
new LinkedHashMap<>(32, 0.75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<CacheKey, String> eldest) {
return size() > CACHE_SIZE;
}
});
public static void clearCache() {
cache.clear();
}
/**
* Transform a mod shader source for core profile compatibility.
*/
public static String transform(String source, boolean isFragment) {
final boolean needsTransform = needsTransformation(source);
String result;
if (!needsTransform) {
result = fixupVersion(source);
} else {
final CacheKey key = new CacheKey(source, isFragment);
final String cached = cache.get(key);
if (cached != null) {
dumpShader(source, cached, isFragment, needsTransform);
return cached;
}
try {
result = transformInternal(source, isFragment);
cache.put(key, result);
} catch (Exception e) {
LOGGER.warn("CompatShaderTransformer: AST transformation failed, falling back to version fixup only", e);
result = fixupVersion(source);
}
}
dumpShader(source, result, isFragment, needsTransform);
return result;
}
private static boolean needsTransformation(String source) {
// Shaders already at 330+ core don't need compat transformation
final Matcher vm = VERSION_PATTERN.matcher(source);
if (vm.find()) {
final int version = Integer.parseInt(vm.group(1));
if (version >= 330 && "core".equals(vm.group(2))) return false;
}
return NEEDS_TRANSFORM_PATTERN.matcher(source).find();
}
private static String transformInternal(String source, boolean isFragment) {
final Matcher versionMatcher = VERSION_PATTERN.matcher(source);
int declaredVersion = 110;
if (versionMatcher.find()) {
declaredVersion = Integer.parseInt(versionMatcher.group(1));
}
final int targetVersion = Math.max(declaredVersion, 330);
// Pre-parse reserved word renaming — prevents ANTLR parse failures
source = GlslTransformUtils.replaceTexture(source);
source = GlslTransformUtils.renameReservedWords(source, targetVersion);
final ShaderParser.ParsedShader parsedShader = ShaderParser.parseShader(source);
final Transformer transformer = new Transformer(parsedShader.full());
injectMatrixUniforms(transformer);
transformFog(transformer, isFragment, source);
// gl_FrontLightModelProduct.sceneColor → angelica_SceneColor uniform
if (source.contains("gl_FrontLightModelProduct")) {
transformer.injectVariable("uniform vec4 angelica_SceneColor;");
transformer.replaceExpression("gl_FrontLightModelProduct.sceneColor", "angelica_SceneColor");
}
// gl_FrontColor (vertex) → gl_Color (fragment) varying chain
// Vertex side is unconditional: fragment may read gl_Color without vertex writing gl_FrontColor.
// Uses angelica_ prefix to avoid colliding with user-declared varyings (e.g. "v_Color").
if (!isFragment) {
transformer.injectVariable("out vec4 angelica_FrontColor;");
transformer.rename("gl_FrontColor", "angelica_FrontColor");
transformer.prependMain("angelica_FrontColor = vec4(1.0);");
// Vertex attributes — replaces removed FFP vertex inputs with explicit in declarations
transformVertexAttributes(transformer, source);
} else {
if (source.contains("gl_Color")) {
transformer.injectVariable("in vec4 angelica_FrontColor;");
transformer.rename("gl_Color", "angelica_FrontColor");
}
}
// gl_TexCoord[N] varying array → per-index in/out declarations
final Set<Integer> texCoordIndices = new HashSet<>();
transformer.renameArray("gl_TexCoord", "angelica_TexCoord", texCoordIndices);
for (Integer i : texCoordIndices) {
final String qualifier = isFragment ? "in" : "out";
transformer.injectVariable(qualifier + " vec4 angelica_TexCoord" + i + ";");
}
// Fragment output handling + alpha test discard
if (isFragment) {
transformFragmentOutputs(transformer);
}
// texture-as-variable collision handling
if (transformer.containsCall("texture") && transformer.hasVariable("texture")) {
transformer.rename("texture", "gtexture");
}
if (transformer.hasVariable("angelica_renamed_texture")) {
transformer.rename("angelica_renamed_texture", "gtexture");
}
transformer.renameFunctionCall(GlslTransformUtils.TEXTURE_RENAMES);
transformer.renameAndWrapShadow("shadow2D", "texture");
transformer.renameAndWrapShadow("shadow2DLod", "textureLod");
final String versionDirective = "#version " + targetVersion + " core\n";
final String extensions = VERSION_PATTERN.matcher(GlslTransformUtils.getFormattedShader(parsedShader.pre(), "")).replaceFirst("").trim();
// Preserve #define directives
final StringBuilder defines = new StringBuilder();
final Matcher defineMatcher = DEFINE_PATTERN.matcher(source);
while (defineMatcher.find()) {
defines.append(defineMatcher.group().trim()).append('\n');
}
final String header = versionDirective + (extensions.isEmpty() ? "" : "\n" + extensions) + (defines.isEmpty() ? "" : "\n" + defines);
final StringBuilder result = new StringBuilder();
transformer.mutateTree(tree -> result.append(GlslTransformUtils.getFormattedShader(tree, header)));
// Restore pre-parse renames
String output = GlslTransformUtils.restoreReservedWords(result.toString());
// Core profile: attribute → in, varying → out (vertex) / in (fragment)
output = fixupQualifiers(output, isFragment);
return output;
}
/**
* Inject matrix uniforms and rename compat builtins.
*/
private static void injectMatrixUniforms(Transformer transformer) {
transformer.injectVariable("uniform mat4 angelica_ModelViewMatrix;");
transformer.injectVariable("uniform mat4 angelica_ModelViewMatrixInverse;");
transformer.injectVariable("uniform mat4 angelica_ProjectionMatrix;");
transformer.injectVariable("uniform mat4 angelica_ProjectionMatrixInverse;");
transformer.injectVariable("uniform mat3 angelica_NormalMatrix;");
transformer.injectVariable("uniform mat4 angelica_LightmapTextureMatrix;");
transformer.rename(MATRIX_RENAMES);
// Expression replacements
transformer.replaceExpression("gl_ModelViewProjectionMatrix", "(angelica_ProjectionMatrix * angelica_ModelViewMatrix)");
transformer.replaceExpression("gl_TextureMatrix[0]", "mat4(1.0)");
transformer.replaceExpression("gl_TextureMatrix[1]", "angelica_LightmapTextureMatrix");
transformer.replaceExpression("gl_TextureMatrix", "mat4[8](mat4(1.0), angelica_LightmapTextureMatrix, mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0))");
}
/**
* Transform fragment outputs for core profile.
*/
private static void transformFragmentOutputs(Transformer transformer) {
if (transformer.containsCall("gl_FragColor")) {
transformer.replaceExpression("gl_FragColor", "gl_FragData[0]");
}
final Set<Integer> found = new HashSet<>();
transformer.renameArray("gl_FragData", "angelica_FragData", found);
for (Integer i : found) {
transformer.injectVariable("layout (location = " + i + ") out vec4 angelica_FragData" + i + ";");
}
// Core profile: GL_ALPHA_TEST is removed - inject runtime discard using GLSM-tracked alpha reference (uploaded by CompatUniformManager).
if (found.contains(0)) {
transformer.injectVariable("uniform float angelica_currentAlphaTest;");
transformer.appendMain("if (angelica_FragData0.a <= angelica_currentAlphaTest) discard;");
}
}
private static void transformFog(Transformer transformer, boolean isFragment, String source) {
// Vertex side is unconditional: fragment may read gl_FogFragCoord without vertex writing it
transformer.rename("gl_FogFragCoord", "angelica_FogFragCoord");
if (!isFragment) {
transformer.injectVariable("out float angelica_FogFragCoord;");
transformer.prependMain("angelica_FogFragCoord = 0.0;");
} else {
if (source.contains("gl_FogFragCoord")) {
transformer.injectVariable("in float angelica_FogFragCoord;");
}
}
transformer.rename("gl_Fog", "angelica_Fog");
transformer.injectVariable("uniform float angelica_FogDensity;");
transformer.injectVariable("uniform float angelica_FogStart;");
transformer.injectVariable("uniform float angelica_FogEnd;");
transformer.injectVariable("uniform vec4 angelica_FogColor;");
transformer.injectFunction("struct angelica_FogParameters {vec4 color;float density;float start;float end;float scale;};");
transformer.injectFunction(
"angelica_FogParameters angelica_Fog = angelica_FogParameters("
+ "angelica_FogColor, angelica_FogDensity, angelica_FogStart, angelica_FogEnd, "
+ "1.0 / (angelica_FogEnd - angelica_FogStart));");
}
/**
* Replace removed FFP vertex attributes with explicit {@code in} declarations at core profile attribute locations.
*/
private static void transformVertexAttributes(Transformer transformer, String source) {
if (source.contains("gl_Vertex") || source.contains("ftransform")) {
transformer.injectVariable("layout(location = 0) in vec4 angelica_Vertex;");
transformer.rename("gl_Vertex", "angelica_Vertex");
}
// gl_Color in vertex shaders is the per-vertex color attribute, distinct from the fragment gl_Color (interpolated gl_FrontColor) handled above
if (source.contains("gl_Color")) {
transformer.injectVariable("layout(location = 1) in vec4 angelica_Color;");
transformer.rename("gl_Color", "angelica_Color");
}
if (source.contains("gl_MultiTexCoord0")) {
transformer.injectVariable("layout(location = 2) in vec4 angelica_MultiTexCoord0;");
transformer.rename("gl_MultiTexCoord0", "angelica_MultiTexCoord0");
}
if (source.contains("gl_MultiTexCoord1")) {
transformer.injectVariable("layout(location = 3) in vec4 angelica_MultiTexCoord1;");
transformer.rename("gl_MultiTexCoord1", "angelica_MultiTexCoord1");
}
if (source.contains("gl_Normal")) {
transformer.injectVariable("layout(location = 4) in vec3 angelica_Normal;");
transformer.rename("gl_Normal", "angelica_Normal");
}
if (source.contains("ftransform")) {
transformer.replaceExpression("ftransform()", "(angelica_ProjectionMatrix * angelica_ModelViewMatrix * angelica_Vertex)");
}
}
private static void dumpShader(String original, String transformed, boolean isFragment, boolean wasTransformed) {
if (DUMP_DIR == null) return;
final int id = dumpCounter.getAndIncrement();
final String suffix = isFragment ? ".frag.glsl" : ".vert.glsl";
try {
Files.createDirectories(DUMP_DIR);
// Capture caller info for identification
final String caller = identifyCaller();
final String header = "// Compat shader dump #" + id + " (" + (isFragment ? "fragment" : "vertex") + ")"
+ "\n// Transformed: " + wasTransformed
+ "\n// Caller: " + caller + "\n\n";
Files.writeString(DUMP_DIR.resolve(id + "_original" + suffix), header + original, StandardCharsets.UTF_8);
Files.writeString(DUMP_DIR.resolve(id + "_transformed" + suffix), header + transformed, StandardCharsets.UTF_8);
} catch (IOException e) {
LOGGER.warn("Failed to dump compat shader: {}", e.getMessage());
}
}
private static String identifyCaller() {
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
final String cls = frame.getClassName();
if (!cls.startsWith("com.gtnewhorizons.angelica.glsm.")
&& !cls.startsWith("java.")
&& !cls.equals("org.lwjgl.opengl.GL20")) {
return cls + "." + frame.getMethodName() + ":" + frame.getLineNumber();
}
}
return "unknown";
}
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\battribute\\b");
private static final Pattern VARYING_PATTERN = Pattern.compile("\\bvarying\\b");
/** Replace legacy storage qualifiers removed in core profile. Safe on AST-serialized output (no comments). */
public static String fixupQualifiers(String source, boolean isFragment) {
source = ATTRIBUTE_PATTERN.matcher(source).replaceAll("in");
source = VARYING_PATTERN.matcher(source).replaceAll(isFragment ? "in" : "out");
return source;
}
// Patterns for parsing fragment shader in declarations to generate passthrough vertex shaders
private static final Pattern FRAG_IN_COLOR = Pattern.compile("\\bin\\s+vec4\\s+angelica_FrontColor\\b");
private static final Pattern FRAG_IN_TEXCOORD = Pattern.compile("\\bin\\s+vec4\\s+angelica_TexCoord(\\d+)\\b");
private static final Pattern FRAG_IN_FOGCOORD = Pattern.compile("\\bin\\s+float\\s+angelica_FogFragCoord\\b");
/**
* Generate a passthrough vertex shader for a fragment-only program.
*
* @param fragmentSource the transformed fragment shader source
* @return a complete vertex shader source string
*/
public static String generatePassthroughVertexShader(String fragmentSource) {
final StringBuilder sb = new StringBuilder(512);
sb.append("#version 330 core\n\n");
// Always have position
sb.append("layout(location = 0) in vec4 a_Position;\n");
// Matrix uniforms for position transform
sb.append("uniform mat4 angelica_ModelViewMatrix;\n");
sb.append("uniform mat4 angelica_ProjectionMatrix;\n");
final StringBuilder varyings = new StringBuilder();
final StringBuilder assignments = new StringBuilder();
if (FRAG_IN_COLOR.matcher(fragmentSource).find()) {
sb.append("layout(location = 1) in vec4 a_Color;\n");
varyings.append("out vec4 angelica_FrontColor;\n");
assignments.append(" angelica_FrontColor = a_Color;\n");
}
final Matcher texCoordMatcher = FRAG_IN_TEXCOORD.matcher(fragmentSource);
final Set<Integer> texCoordIndices = new HashSet<>();
while (texCoordMatcher.find()) {
texCoordIndices.add(Integer.parseInt(texCoordMatcher.group(1)));
}
for (int i : texCoordIndices) {
// PRIMARY_UV=2, SECONDARY_UV=3. Only indices 0 and 1 are supported; higher indices collide at location 3.
final int location = i == 0 ? 2 : 3;
sb.append("layout(location = ").append(location).append(") in vec4 a_TexCoord").append(i).append(";\n");
varyings.append("out vec4 angelica_TexCoord").append(i).append(";\n");
assignments.append(" angelica_TexCoord").append(i).append(" = a_TexCoord").append(i).append(";\n");
}
if (FRAG_IN_FOGCOORD.matcher(fragmentSource).find()) {
varyings.append("out float angelica_FogFragCoord;\n");
assignments.append(" angelica_FogFragCoord = abs((angelica_ModelViewMatrix * a_Position).z);\n");
}
sb.append(varyings);
sb.append("\nvoid main() {\n");
sb.append(" gl_Position = angelica_ProjectionMatrix * angelica_ModelViewMatrix * a_Position;\n");
sb.append(assignments);
sb.append("}\n");
return sb.toString();
}
/** Ensure #version is at least 330 core, strip 'compatibility' profile. */
private static String fixupVersion(String source) {
final Matcher m = VERSION_PATTERN.matcher(source);
if (!m.find()) {
return "#version 330 core\n" + source;
}
final int version = Integer.parseInt(m.group(1));
final String profile = m.group(2);
if (version >= 330 && !"compatibility".equals(profile)) {
if ("core".equals(profile)) return source;
return m.replaceFirst("#version " + version + " core");
}
final int targetVersion = Math.max(version, 330);
return m.replaceFirst("#version " + targetVersion + " core");
}
}
@@ -0,0 +1,306 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.LightModelState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
/**
* Per-program compat uniform location cache and upload manager.
*
* <p>Manages {@code angelica_*} uniforms injected by {@link CompatShaderTransformer} and {@code iris_*} matrix uniforms used by Iris/celeritas shader programs.
*/
public class CompatUniformManager {
// Uniform location indices within the cached array (package-private for tests)
static final int LOC_MODELVIEW = 0;
static final int LOC_MODELVIEW_INVERSE = 1;
static final int LOC_PROJECTION = 2;
static final int LOC_PROJECTION_INVERSE = 3;
static final int LOC_NORMAL = 4;
static final int LOC_LIGHTMAP_TEXTURE_MATRIX = 5;
static final int LOC_FOG_DENSITY = 6;
static final int LOC_FOG_START = 7;
static final int LOC_FOG_END = 8;
static final int LOC_FOG_COLOR = 9;
static final int LOC_IRIS_MODELVIEW = 10;
static final int LOC_IRIS_PROJECTION = 11;
static final int LOC_IRIS_NORMAL = 12;
static final int LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX = 13;
static final int LOC_ALPHA_TEST_REF = 14;
static final int LOC_SCENE_COLOR = 15;
static final int LOC_CLIP_PLANES = 16;
static final int LOC_CLIP_PLANES_ENABLED = 17;
static final int LOC_COUNT = 18;
private static final String[] UNIFORM_NAMES = {
"angelica_ModelViewMatrix",
"angelica_ModelViewMatrixInverse",
"angelica_ProjectionMatrix",
"angelica_ProjectionMatrixInverse",
"angelica_NormalMatrix",
"angelica_LightmapTextureMatrix",
"angelica_FogDensity",
"angelica_FogStart",
"angelica_FogEnd",
"angelica_FogColor",
"iris_ModelViewMatrix",
"iris_ProjectionMatrix",
"iris_NormalMatrix",
"iris_LightmapTextureMatrix",
"angelica_currentAlphaTest",
"angelica_SceneColor",
"angelica_ClipPlane[0]",
"angelica_ClipPlanesEnabled",
};
/** Per-program cached uniform locations. Maps program ID → int[LOC_COUNT]. */
private static final Int2ObjectOpenHashMap<int[]> programLocations = new Int2ObjectOpenHashMap<>();
// Reusable NIO buffers for upload
private static final FloatBuffer mat4Buf = BufferUtils.createFloatBuffer(16);
private static final FloatBuffer mat3Buf = BufferUtils.createFloatBuffer(9);
private static final FloatBuffer vec4Buf = BufferUtils.createFloatBuffer(4);
private static final FloatBuffer clipPlaneBuf = BufferUtils.createFloatBuffer(32); // 8 planes * vec4
private static final Matrix3f normalMatrix = new Matrix3f();
private static final Matrix4f scratchMatrix = new Matrix4f();
private static final float LIGHTMAP_SCALE = 1.0f / 256.0f;
private static final FloatBuffer lightmapMatrixBuf;
static {
lightmapMatrixBuf = BufferUtils.createFloatBuffer(16);
new Matrix4f().scale(LIGHTMAP_SCALE).translate(8.0f, 8.0f, 8.0f).get(lightmapMatrixBuf);
}
// Dirty tracking: skip uploads when state hasn't changed and program is the same
private static int lastProgram = -1;
private static int lastMvGen = -1;
private static int lastProjGen = -1;
private static int lastTexMatGen = -1;
private static int lastFragmentGen = -1;
private static int lastLightingGen = -1;
private static int lastClipPlaneGen = -1;
private CompatUniformManager() {}
public static void onLinkProgram(int program) {
int[] locs = new int[LOC_COUNT];
boolean hasAny = false;
for (int i = 0; i < LOC_COUNT; i++) {
locs[i] = GL20.glGetUniformLocation(program, UNIFORM_NAMES[i]);
if (locs[i] != -1) hasAny = true;
}
if (hasAny) {
programLocations.put(program, locs);
LOGGER.debug("CompatUniformManager: program {} has compat uniforms", program);
}
}
public static void onUseProgram(int program) {
if (program == 0) return;
int[] locs = programLocations.get(program);
if (locs == null) return;
final boolean programChanged = program != lastProgram;
lastProgram = program;
// Matrix uniforms — skip if generation unchanged and same program
final int mvGen = GLStateManager.mvGeneration;
final int projGen = GLStateManager.projGeneration;
final int texMatGen = GLStateManager.texMatrixGeneration;
final boolean mvChanged = programChanged || mvGen != lastMvGen;
final boolean projChanged = programChanged || projGen != lastProjGen;
final boolean texMatChanged = programChanged || texMatGen != lastTexMatGen;
if (mvChanged || projChanged || texMatChanged) {
uploadMatrices(locs, mvChanged, projChanged, texMatChanged);
lastMvGen = mvGen;
lastProjGen = projGen;
lastTexMatGen = texMatGen;
}
// Fragment-category uniforms (fog, alpha) — skip if generation unchanged
final int fragGen = GLStateManager.fragmentGeneration;
if (programChanged || fragGen != lastFragmentGen) {
lastFragmentGen = fragGen;
uploadFragmentUniforms(locs);
}
// Lighting-derived uniforms (scene color) — skip if lighting state unchanged
if (locs[LOC_SCENE_COLOR] != -1) {
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
lastLightingGen = litGen;
uploadSceneColor(locs);
}
}
// Clip plane equations + enabled bool — uploaded when enable state or equations change
if (locs[LOC_CLIP_PLANES] != -1 || locs[LOC_CLIP_PLANES_ENABLED] != -1) {
final int cpGen = GLStateManager.clipPlaneGeneration;
if (programChanged || cpGen != lastClipPlaneGen) {
lastClipPlaneGen = cpGen;
uploadClipPlanes(locs);
}
}
}
private static void uploadMatrices(int[] locs, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
if (mvChanged) {
// ModelView
if (locs[LOC_MODELVIEW] != -1 || locs[LOC_IRIS_MODELVIEW] != -1) {
mv.get(mat4Buf);
if (locs[LOC_MODELVIEW] != -1) {
GL20.glUniformMatrix4(locs[LOC_MODELVIEW], false, mat4Buf);
}
if (locs[LOC_IRIS_MODELVIEW] != -1) {
GL20.glUniformMatrix4(locs[LOC_IRIS_MODELVIEW], false, mat4Buf);
}
}
// ModelView Inverse
if (locs[LOC_MODELVIEW_INVERSE] != -1) {
mv.invert(scratchMatrix);
scratchMatrix.get(mat4Buf);
GL20.glUniformMatrix4(locs[LOC_MODELVIEW_INVERSE], false, mat4Buf);
}
// Normal Matrix (inverse-transpose of upper-left 3x3 of ModelView)
if (locs[LOC_NORMAL] != -1 || locs[LOC_IRIS_NORMAL] != -1) {
mv.normal(normalMatrix);
normalMatrix.get(mat3Buf);
if (locs[LOC_NORMAL] != -1) {
GL20.glUniformMatrix3(locs[LOC_NORMAL], false, mat3Buf);
}
if (locs[LOC_IRIS_NORMAL] != -1) {
GL20.glUniformMatrix3(locs[LOC_IRIS_NORMAL], false, mat3Buf);
}
}
}
if (projChanged) {
// Projection
if (locs[LOC_PROJECTION] != -1 || locs[LOC_IRIS_PROJECTION] != -1) {
proj.get(mat4Buf);
if (locs[LOC_PROJECTION] != -1) {
GL20.glUniformMatrix4(locs[LOC_PROJECTION], false, mat4Buf);
}
if (locs[LOC_IRIS_PROJECTION] != -1) {
GL20.glUniformMatrix4(locs[LOC_IRIS_PROJECTION], false, mat4Buf);
}
}
// Projection Inverse
if (locs[LOC_PROJECTION_INVERSE] != -1) {
proj.invert(scratchMatrix);
scratchMatrix.get(mat4Buf);
GL20.glUniformMatrix4(locs[LOC_PROJECTION_INVERSE], false, mat4Buf);
}
}
if (texMatChanged) {
// Iris lightmap texture matrix (constant)
if (locs[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX] != -1) {
GL20.glUniformMatrix4(locs[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX], false, lightmapMatrixBuf);
}
// angelica_LightmapTextureMatrix — actual GLSM texture unit 1 matrix (set by enableLightmap())
if (locs[LOC_LIGHTMAP_TEXTURE_MATRIX] != -1) {
GLStateManager.getTextures().getTextureUnitMatrix(1).get(mat4Buf);
GL20.glUniformMatrix4(locs[LOC_LIGHTMAP_TEXTURE_MATRIX], false, mat4Buf);
}
}
}
private static void uploadFragmentUniforms(int[] locs) {
// Fog uniforms
FogStateStack fog = GLStateManager.getFogState();
if (locs[LOC_FOG_DENSITY] != -1) {
GL20.glUniform1f(locs[LOC_FOG_DENSITY], Math.max(0.0f, fog.getDensity()));
}
if (locs[LOC_FOG_START] != -1) {
GL20.glUniform1f(locs[LOC_FOG_START], fog.getStart());
}
if (locs[LOC_FOG_END] != -1) {
GL20.glUniform1f(locs[LOC_FOG_END], fog.getEnd());
}
if (locs[LOC_FOG_COLOR] != -1) {
vec4Buf.clear();
vec4Buf.put((float) fog.getFogColor().x).put((float) fog.getFogColor().y)
.put((float) fog.getFogColor().z).put(fog.getFogAlpha());
vec4Buf.flip();
GL20.glUniform4(locs[LOC_FOG_COLOR], vec4Buf);
}
// Alpha test reference — core profile replacement for GL_ALPHA_TEST
// Return -1.0f for ALWAYS or disabled so `alpha <= ref` never discards.
if (locs[LOC_ALPHA_TEST_REF] != -1) {
final float ref;
if (!GLStateManager.getAlphaTest().isEnabled()
|| GLStateManager.getAlphaState().getFunction() == GL11.GL_ALWAYS) {
ref = -1.0f;
} else {
ref = GLStateManager.getAlphaState().getReference();
}
GL20.glUniform1f(locs[LOC_ALPHA_TEST_REF], ref);
}
}
/**
* Scene color = emission + ambient * lightModel.ambient, alpha = diffuse.a
* Same computation as {@code Uniforms.uploadLightingPreComputed()}.
*/
private static void uploadSceneColor(int[] locs) {
final MaterialState mat = GLStateManager.getFrontMaterial();
final LightModelState lm = GLStateManager.getLightModel();
vec4Buf.clear();
vec4Buf.put(mat.emission.x + mat.ambient.x * lm.ambient.x)
.put(mat.emission.y + mat.ambient.y * lm.ambient.y)
.put(mat.emission.z + mat.ambient.z * lm.ambient.z)
.put(mat.diffuse.w);
vec4Buf.flip();
GL20.glUniform4(locs[LOC_SCENE_COLOR], vec4Buf);
}
private static void uploadClipPlanes(int[] locs) {
if (locs[LOC_CLIP_PLANES_ENABLED] != -1) {
GL20.glUniform1i(locs[LOC_CLIP_PLANES_ENABLED], GLStateManager.anyClipPlaneEnabled() ? 1 : 0);
}
if (locs[LOC_CLIP_PLANES] != -1) {
final ClipPlaneState cps = GLStateManager.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
clipPlaneBuf.flip();
GL20.glUniform4(locs[LOC_CLIP_PLANES], clipPlaneBuf);
}
}
public static void onDeleteProgram(int program) {
programLocations.remove(program);
}
public static boolean hasProgram(int program) {
return programLocations.containsKey(program);
}
public static int[] getLocations(int program) {
return programLocations.get(program);
}
}
@@ -1,23 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
/**
* A compiled VBO with its associated draw ranges.
* All ranges reference vertices within this single VBO.
* <p>
* Contains two range arrays:
* <ul>
* <li><b>mergedRanges</b>: For optimized path - consecutive same-transform draws merged</li>
* <li><b>perDrawRanges</b>: For unoptimized path - 1:1 with original draws</li>
* </ul>
*/
@Desugar
public record CompiledFormatBuffer(
VertexBuffer vbo,
CapturingTessellator.Flags flags,
DrawRange[] mergedRanges,
DrawRange[] perDrawRanges
) {}
@@ -1,21 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
/**
* A compiled VBO containing all line geometry with associated draw ranges.
* All ranges reference vertices within this single VBO.
* <p>
* Contains two range arrays:
* <ul>
* <li><b>mergedRanges</b>: For optimized path - consecutive same-transform draws merged</li>
* <li><b>perDrawRanges</b>: For unoptimized path - 1:1 with original draws</li>
* </ul>
*/
@Desugar
public record CompiledLineBuffer(
VertexBuffer vbo,
DrawRange[] mergedRanges,
DrawRange[] perDrawRanges
) {}
@@ -1,31 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
/**
* Compiled VBOs containing tessellator primitives (lines and triangles) for a single vertex format.
* Separate VBOs are used for lines (GL_LINES) and triangles (GL_TRIANGLES) since they have different draw modes.
* <p>
* Contains range arrays for both optimized (merged) and unoptimized (per-draw) paths.
* Similar to CompiledFormatBuffer but handles primitives instead of quads.
*/
@Desugar
public record CompiledPrimitiveBuffers(
CapturingTessellator.Flags flags,
VertexBuffer lineVbo,
DrawRange[] lineMergedRanges,
DrawRange[] linePerDrawRanges,
VertexBuffer triangleVbo,
DrawRange[] triangleMergedRanges,
DrawRange[] trianglePerDrawRanges
) {
public boolean hasLines() {
return lineVbo != null;
}
public boolean hasTriangles() {
return triangleVbo != null;
}
}
@@ -0,0 +1,187 @@
package com.gtnewhorizons.angelica.glsm;
/*
* Ported from Mesa's util_idalloc (src/util/u_idalloc.c).
*
* Original copyright and license:
*
* Copyright 2017 Valve Corporation
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sub license, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice (including the
* next paragraph) shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Original author: Samuel Pitoiset <[email protected]>
*/
/**
* Bitmap ID allocator for display list IDs.
*
* <p>Bitmap-based: each bit in the {@code data} array represents one ID.
* Bit set = allocated, bit clear = free. ID 0 is reserved (never returned).
*/
public class DisplayListIDAllocator {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
public DisplayListIDAllocator() {
this(32); // 1024 IDs initially
}
public DisplayListIDAllocator(int initialWords) {
if (initialWords < 1) initialWords = 1;
this.data = new int[initialWords];
this.numElements = initialWords;
this.numSetElements = 0;
this.lowestFreeIdx = 0;
// Reserve ID 0 (never returned), matching Mesa's skip_zero pattern
data[0] = 1;
numSetElements = 1;
}
public int allocRange(int count) {
if (count <= 0) throw new IllegalArgumentException("count must be > 0");
if (count == 1) return allocSingle();
return allocMultiple(count);
}
private int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
private int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
}
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
}
private int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
}
public void free(int id) {
if (id <= 0) return; // ID 0 is reserved
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
}
public void freeRange(int base, int count) {
if (count <= 0 || base <= 0) return;
int startIdx = base / 32;
if (startIdx >= numElements) return;
int end = base + count;
for (int id = base; id < end; id++) {
int idx = id / 32;
if (idx >= numElements) break;
data[idx] &= ~(1 << (id % 32));
}
lowestFreeIdx = Math.min(startIdx, lowestFreeIdx);
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
public boolean isAllocated(int id) {
if (id <= 0) return false;
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
}
}
File diff suppressed because it is too large. Load diff
@@ -1,25 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw;
import org.joml.Matrix4f;
/**
* Represents a vertex range within a shared VBO, along with its transform.
* Used during format-based batching to track where each draw's vertices are stored.
*
* <p>The restoreData field holds last vertex attributes for GL state restoration
* after VBO rendering (immediate mode draws only). Null for tessellator draws.
*/
@Desugar
public record DrawRange(
int startVertex,
int vertexCount,
Matrix4f transform,
int commandIndex,
AccumulatedDraw.RestoreData restoreData
) {
DrawRange(int startVertex, int vertexCount, Matrix4f transform, int commandIndex) {
this(startVertex, vertexCount, transform, commandIndex, null);
}
}
@@ -1,43 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
/**
* Helper record pairing a DrawRange with its VBO and vertex format flags.
* Used for sorting ranges by command index while retaining VBO reference.
* Works for both quad ranges (from CompiledFormatBuffer) and line ranges (from CompiledLineBuffer).
*/
@Desugar
record DrawRangeWithBuffer(
DrawRange range,
VertexBuffer vbo,
CapturingTessellator.Flags flags,
DrawSource source
) {
boolean hasBrightness() {
return flags != null && flags.hasBrightness;
}
/**
* Source of a draw command for debug logging.
*/
enum DrawSource {
TESSELLATOR_QUADS("tessellator quads"),
TESSELLATOR_LINES("tessellator lines"),
TESSELLATOR_TRIANGLES("tessellator triangles"),
IMMEDIATE_MODE_LINES("immediate mode lines");
private final String description;
DrawSource(String description) {
this.description = description;
}
@Override
public String toString() {
return description;
}
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -87,22 +88,20 @@ public class Feature {
GLStateManager.alphaTest // GL_ALPHA_TEST enable bit
, GLStateManager.alphaState // Alpha test function and reference value
, GLStateManager.blendMode // GL_BLEND enable bit
, GLStateManager.blendState // Blending source and destination functions
// Constant blend color
// Blending equation
, GLStateManager.blendState // Blending source/destination functions, equation, blend color
, GLStateManager.colorLogicOpState // GL_COLOR_LOGIC_OP enable bit
, GLStateManager.ditherState // GL_DITHER enable bit
, GLStateManager.drawBuffer // GL_DRAW_BUFFER setting
, GLStateManager.indexLogicOpState // GL_INDEX_LOGIC_OP enable bit
// Logic op function
, GLStateManager.logicOpMode // Logic op function
, GLStateManager.colorMask // Color-mode and index-mode writemasks
, GLStateManager.clearColor // Color-mode and index-mode clear values
));
attribToFeatures.put(GL11.GL_CURRENT_BIT, ImmutableSet.of(
GLStateManager.color // Current RGBA color
, ShaderManager.getNormalStack() // Current normal vector
, ShaderManager.getTexCoordStack() // Current texture coordinates
// Current color index
// Current normal vector
// Current texture coordinates
// Current raster position
// GL_CURRENT_RASTER_POSITION_VALID flag
// RGBA color associated with current raster position
@@ -300,6 +299,8 @@ public class Feature {
));
final Set<IStateStack<?>> textureAttribs = new HashSet<>(ImmutableSet.of(
GLStateManager.activeTextureUnit // Active texture unit
, GLStateManager.texEnvMode // GL_TEXTURE_ENV_MODE
, GLStateManager.texEnvColor // GL_TEXTURE_ENV_COLOR
// Enable bits for the four texture coordinates
// Border color for each texture image
@@ -307,7 +308,6 @@ public class Feature {
// Magnification function for each texture image
// Texture coordinates and wrap mode for each texture image
// Color and mode for each texture environment
// Enable bits GL_TEXTURE_GEN_x, x is S, T, R, and Q
// GL_TEXTURE_GEN_MODE setting for S, T, R, and Q
// glTexGen plane equations for S, T, R, and Q
@@ -1,106 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.List;
/**
* Accumulates geometry for a single vertex format during display list compilation.
* Tracks merged ranges (same-transform draws combined) and per-draw ranges (1:1).
*/
class FormatBuffer {
final CapturingTessellator.Flags flags;
final List<ModelQuadViewMutable> allQuads = new ArrayList<>();
// Merged ranges for optimized path (consecutive same-transform draws combined)
final List<DrawRange> mergedRanges = new ArrayList<>();
// Per-draw ranges for unoptimized path (1:1 with input draws)
final List<DrawRange> perDrawRanges = new ArrayList<>();
int currentVertexOffset = 0;
// Track the current range being built for merging consecutive same-transform draws
private int pendingStartVertex = -1;
private int pendingVertexCount = 0;
private Matrix4f pendingTransform = null;
private int pendingCommandIndex = -1;
private int pendingGeneration = -1;
private int pendingStateGen = -1;
// Track last restore data for merged draws (uses the LAST draw's data)
private AccumulatedDraw.RestoreData pendingRestoreData = null;
FormatBuffer(CapturingTessellator.Flags flags) {
this.flags = flags;
}
/** Add a draw, tracking both merged and per-draw ranges. */
void addDraw(AccumulatedDraw draw) {
final int vertexCount = draw.quads.size() * 4; // 4 vertices per quad
// Always track per-draw range for unoptimized path (includes restoreData)
perDrawRanges.add(new DrawRange(
currentVertexOffset, vertexCount, draw.transform, draw.commandIndex, draw.restoreData
));
// Merge logic for optimized path:
// - same matrixGeneration = same matrix transform context
// - same stateGeneration = no state commands (draw barriers) between draws
boolean canMerge = pendingGeneration != -1
&& pendingGeneration == draw.matrixGeneration
&& pendingStateGen == draw.stateGeneration;
if (canMerge) {
// Extend the pending range
pendingVertexCount += vertexCount;
// Update restoreData to the LAST draw's data (used after merged draw completes)
pendingRestoreData = draw.restoreData;
} else {
// Flush pending range if any
flushPendingRange();
// Start new pending range
pendingStartVertex = currentVertexOffset;
pendingVertexCount = vertexCount;
pendingTransform = new Matrix4f(draw.transform);
pendingCommandIndex = draw.commandIndex;
pendingGeneration = draw.matrixGeneration;
pendingStateGen = draw.stateGeneration;
pendingRestoreData = draw.restoreData;
}
allQuads.addAll(draw.quads);
currentVertexOffset += vertexCount;
}
private void flushPendingRange() {
if (pendingGeneration != -1) {
mergedRanges.add(new DrawRange(
pendingStartVertex, pendingVertexCount, pendingTransform, pendingCommandIndex, pendingRestoreData
));
pendingTransform = null;
pendingGeneration = -1;
pendingStateGen = -1;
pendingRestoreData = null;
}
}
/** Compile accumulated quads into a VBO. */
CompiledFormatBuffer finish() {
// Flush any remaining pending range
flushPendingRange();
VertexBuffer vbo = DisplayListManager.compileQuads(allQuads, flags);
return new CompiledFormatBuffer(
vbo,
flags,
mergedRanges.toArray(new DrawRange[0]),
perDrawRanges.toArray(new DrawRange[0])
);
}
}
@@ -19,9 +19,6 @@ import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.KHRDebug;
import org.lwjgl.opengl.KHRDebugCallback;
import java.io.PrintStream;
import java.util.function.Consumer;
import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
import static org.lwjgl.opengl.ARBDebugOutput.glDebugMessageCallbackARB;
@@ -35,20 +32,9 @@ public final class GLDebug {
LOGGER.warn("setupDebugMessageCallback called from non-main thread!");
return 0;
}
return setupDebugMessageCallback(System.err);
return setupDebugMessageCallbackImpl();
}
private static void trace(Consumer<String> output) {
/*
* We can not just use a fixed stacktrace element offset, because some methods
* are intercepted and some are not. So, check the package name.
*/
StackTraceElement[] elems = filterStackTrace(new Throwable(), 4).getStackTrace();
for (StackTraceElement ste : elems) {
output.accept(ste.toString());
}
}
public static Throwable filterStackTrace(Throwable throwable, int offset) {
StackTraceElement[] elems = throwable.getStackTrace();
StackTraceElement[] filtered = new StackTraceElement[elems.length];
@@ -66,36 +52,48 @@ public final class GLDebug {
return throwable;
}
public static void printTrace(PrintStream stream) {
trace(new Consumer<String>() {
boolean first = true;
private static String buildStackTrace() {
StackTraceElement[] elems = filterStackTrace(new Throwable(), 4).getStackTrace();
StringBuilder sb = new StringBuilder();
for (StackTraceElement elem : elems) {
sb.append("\n\t").append(elem.toString());
}
return sb.toString();
}
public void accept(String str) {
if (first) {
printDetail(stream, "Stacktrace", str);
first = false;
} else {
printDetailLine(stream, "Stacktrace", str);
}
}
});
private static void logDebugMessage(int id, String source, String type, String severity, String message) {
String fullMessage = String.format("[GL] %s %s (0x%X) from %s: %s%s", severity, type, id, source, message, buildStackTrace());
if ("HIGH".equals(severity)) {
LOGGER.error(fullMessage);
} else if ("MEDIUM".equals(severity)) {
LOGGER.warn(fullMessage);
} else {
LOGGER.info(fullMessage);
}
}
private static void logDebugMessageAMD(int id, String category, String severity, String message) {
String fullMessage = String.format("[GL] %s %s (0x%X): %s%s", severity, category, id, message, buildStackTrace());
if ("HIGH".equals(severity)) {
LOGGER.error(fullMessage);
} else if ("MEDIUM".equals(severity)) {
LOGGER.warn(fullMessage);
} else {
LOGGER.info(fullMessage);
}
}
/**
* Sets up debug callbacks
* @return 0 for failure, 1 for success, 2 for restart required.
*/
public static int setupDebugMessageCallback(PrintStream stream) {
private static int setupDebugMessageCallbackImpl() {
if (GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_KHR_debug) {
LOGGER.info("[GL] Using OpenGL 4.3 for error logging.");
KHRDebugCallback proc = new KHRDebugCallback((source, type, id, severity, message) -> {
stream.println("[LWJGL] OpenGL debug message");
printDetail(stream, "ID", String.format("0x%X", id));
printDetail(stream, "Source", getDebugSource(source));
printDetail(stream, "Type", getDebugType(type));
printDetail(stream, "Severity", getDebugSeverity(severity));
printDetail(stream, "Message", message);
printTrace(stream);
final KHRDebugCallback proc = new KHRDebugCallback((source, type, id, severity, message) -> {
logDebugMessage(id, getDebugSource(source), getDebugType(type), getDebugSeverity(severity), message);
});
GL43.glDebugMessageControl(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_HIGH, null, true);
GL43.glDebugMessageControl(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_MEDIUM, null, false);
@@ -114,14 +112,8 @@ public final class GLDebug {
return 1;
} else if (GLStateManager.capabilities.GL_ARB_debug_output) {
LOGGER.info("[GL] Using ARB_debug_output for error logging.");
ARBDebugOutputCallback proc = new ARBDebugOutputCallback((source, type, id, severity, message) -> {
stream.println("[LWJGL] ARB_debug_output message");
printDetail(stream, "ID", String.format("0x%X", id));
printDetail(stream, "Source", getSourceARB(source));
printDetail(stream, "Type", getTypeARB(type));
printDetail(stream, "Severity", getSeverityARB(severity));
printDetail(stream, "Message", message);
printTrace(stream);
final ARBDebugOutputCallback proc = new ARBDebugOutputCallback((source, type, id, severity, message) -> {
logDebugMessage(id, getSourceARB(source), getTypeARB(type), getSeverityARB(severity), message);
});
ARBDebugOutput.glDebugMessageControlARB(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_HIGH, null, true);
ARBDebugOutput.glDebugMessageControlARB(GL11.GL_DONT_CARE, GL11.GL_DONT_CARE, GL43.GL_DEBUG_SEVERITY_MEDIUM, null, false);
@@ -134,13 +126,8 @@ public final class GLDebug {
return 1;
} else if (GLStateManager.capabilities.GL_AMD_debug_output) {
LOGGER.info("[GL] Using AMD_debug_output for error logging.");
AMDDebugOutputCallback proc = new AMDDebugOutputCallback((id, category, severity, message) -> {
stream.println("[LWJGL] AMD_debug_output message");
printDetail(stream, "ID", String.format("0x%X", id));
printDetail(stream, "Category", getCategoryAMD(category));
printDetail(stream, "Severity", getSeverityAMD(severity));
printDetail(stream, "Message", message);
printTrace(stream);
final AMDDebugOutputCallback proc = new AMDDebugOutputCallback((id, category, severity, message) -> {
logDebugMessageAMD(id, getCategoryAMD(category), getSeverityAMD(severity), message);
});
AMDDebugOutput.glDebugMessageEnableAMD(0, GL43.GL_DEBUG_SEVERITY_HIGH, null, true);
AMDDebugOutput.glDebugMessageEnableAMD(0, GL43.GL_DEBUG_SEVERITY_MEDIUM, null, false);
@@ -176,17 +163,6 @@ public final class GLDebug {
}
}
private static void printDetail(PrintStream stream, String type, String message) {
stream.printf("\t%s: %s\n", type, message);
}
private static void printDetailLine(PrintStream stream, String type, String message) {
stream.append(" ");
for (int i = 0; i < type.length(); i++) {
stream.append(" ");
}
stream.append(message).append("\n");
}
private static String getDebugSource(int source) {
return switch (source) {
@@ -577,6 +553,57 @@ public final class GLDebug {
};
}
public static String getFramebufferStatusName(int status) {
return switch (status) {
case GL30.GL_FRAMEBUFFER_COMPLETE -> "COMPLETE";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT -> "INCOMPLETE_ATTACHMENT";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT -> "INCOMPLETE_MISSING_ATTACHMENT";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER -> "INCOMPLETE_DRAW_BUFFER";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER -> "INCOMPLETE_READ_BUFFER";
case GL30.GL_FRAMEBUFFER_UNSUPPORTED -> "UNSUPPORTED";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE -> "INCOMPLETE_MULTISAMPLE";
case 0 -> "NO_FRAMEBUFFER_BOUND";
default -> String.format("UNKNOWN(0x%X)", status);
};
}
public static String getInternalFormatName(int format) {
return switch (format) {
// Basic
case GL11.GL_RGBA -> "RGBA";
case GL11.GL_RGB -> "RGB";
// 8-bit normalized
case GL30.GL_R8 -> "R8";
case GL30.GL_RG8 -> "RG8";
case GL11.GL_RGB8 -> "RGB8";
case GL11.GL_RGBA8 -> "RGBA8";
// 8-bit signed normalized
case 0x8F94 -> "R8_SNORM";
case 0x8F95 -> "RG8_SNORM";
case 0x8F96 -> "RGB8_SNORM";
case 0x8F97 -> "RGBA8_SNORM";
// 16-bit normalized
case GL30.GL_R16 -> "R16";
case GL30.GL_RG16 -> "RG16";
case GL11.GL_RGB16 -> "RGB16";
case GL11.GL_RGBA16 -> "RGBA16";
// 16-bit float
case GL30.GL_R16F -> "R16F";
case GL30.GL_RG16F -> "RG16F";
case GL30.GL_RGB16F -> "RGB16F";
case GL30.GL_RGBA16F -> "RGBA16F";
// 32-bit float
case GL30.GL_R32F -> "R32F";
case GL30.GL_RG32F -> "RG32F";
case GL30.GL_RGB32F -> "RGB32F";
case GL30.GL_RGBA32F -> "RGBA32F";
// Packed
case GL30.GL_R11F_G11F_B10F -> "R11F_G11F_B10F";
case GL11.GL_RGB10_A2 -> "RGB10_A2";
default -> String.format("0x%X", format);
};
}
private static DebugState debugState;
private interface DebugState {
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,111 @@
package com.gtnewhorizons.angelica.glsm;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Shared GLSL shader transformation utilities.
*/
public class GlslTransformUtils {
private static final String RENAMED_PREFIX = "angelica_renamed_";
/** Texture function renames */
public static final Map<String, String> TEXTURE_RENAMES = Map.ofEntries(
Map.entry("texture2D", "texture"),
Map.entry("texture3D", "texture"),
Map.entry("texture2DLod", "textureLod"),
Map.entry("texture3DLod", "textureLod"),
Map.entry("texture2DProj", "textureProj"),
Map.entry("texture3DProj", "textureProj"),
Map.entry("texture2DGrad", "textureGrad"),
Map.entry("texture2DGradARB", "textureGrad"),
Map.entry("texture3DGrad", "textureGrad"),
Map.entry("texelFetch2D", "texelFetch"),
Map.entry("texelFetch3D", "texelFetch"),
Map.entry("textureSize2D", "textureSize")
);
private static final Pattern TEXTURE_PATTERN = Pattern.compile("\\btexture\\s*\\(|(\\btexture\\b)");
/** Reserved words added in later GLSL versions that may appear as identifiers in older shaders. */
private record ReservedWordRename(Pattern pattern, String replacement) {}
private static final Map<Integer, List<ReservedWordRename>> VERSIONED_RESERVED_WORDS = Map.of(
400, List.of(new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"))
);
public static String replaceTexture(String input) {
final Matcher matcher = TEXTURE_PATTERN.matcher(input);
final StringBuilder builder = new StringBuilder();
while (matcher.find()) {
if (matcher.group(1) != null) {
matcher.appendReplacement(builder, RENAMED_PREFIX + "texture");
} else {
matcher.appendReplacement(builder, Matcher.quoteReplacement(matcher.group(0)));
}
}
matcher.appendTail(builder);
return builder.toString();
}
public static String renameReservedWords(String source, int targetVersion) {
for (var entry : VERSIONED_RESERVED_WORDS.entrySet()) {
if (targetVersion < entry.getKey()) {
for (var rename : entry.getValue()) {
source = rename.pattern().matcher(source).replaceAll(rename.replacement());
}
}
}
return source;
}
public static String restoreReservedWords(String source) {
source = source.replace(RENAMED_PREFIX + "texture", "texture");
source = source.replace(RENAMED_PREFIX + "sample", "sample");
return source;
}
public static String getFormattedShader(ParseTree tree, String header) {
final StringBuilder sb = new StringBuilder(header + "\n");
final String[] tabHolder = {""};
getFormattedShader(tree, sb, tabHolder);
return sb.toString();
}
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder) {
if (tree instanceof TerminalNode) {
final String text = tree.getText();
if (text.equals("<EOF>")) {
return;
}
if (text.equals("#")) {
stringBuilder.append("\n#");
return;
}
stringBuilder.append(text);
if (text.equals("{")) {
stringBuilder.append(" \n\t");
tabHolder[0] = "\t";
}
if (text.equals("}")) {
if (stringBuilder.length() >= 2) {
stringBuilder.deleteCharAt(stringBuilder.length() - 2);
}
tabHolder[0] = "";
stringBuilder.append(" \n");
} else {
stringBuilder.append(text.equals(";") ? " \n" + tabHolder[0] : " ");
}
} else {
for (int i = 0; i < tree.getChildCount(); ++i) {
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder);
}
}
}
}
@@ -1,134 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedLineDraw;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Accumulates line geometry during display list compilation.
* All line draws are collected here, then compiled into a single VBO.
* <p>
* Lines all use the same format (POSITION_COLOR), so no format-based grouping needed.
* <p>
* Tracks two types of ranges:
* <ul>
* <li><b>mergedRanges</b>: Consecutive same-transform draws merged for optimized path</li>
* <li><b>perDrawRanges</b>: One range per input draw for unoptimized path (1:1 with addDraw calls)</li>
* </ul>
*/
class LineBuffer {
final List<ImmediateModeRecorder.LineVertex> allVertices = new ArrayList<>();
// Merged ranges for optimized path (consecutive same-transform draws combined)
final List<DrawRange> mergedRanges = new ArrayList<>();
// Per-draw ranges for unoptimized path (1:1 with input draws)
final List<DrawRange> perDrawRanges = new ArrayList<>();
int currentVertexOffset = 0;
// Track the current range being built for merging consecutive same-transform draws
private int pendingStartVertex = -1;
private int pendingVertexCount = 0;
private Matrix4f pendingTransform = null;
private int pendingCommandIndex = -1;
private int pendingStateGen = -1;
/**
* Add a line draw to this buffer.
* Tracks both merged (for optimized) and per-draw (for unoptimized) ranges.
*/
void addDraw(AccumulatedLineDraw draw) {
int vertexCount = draw.lines.size(); // Each LineVertex is one vertex, lines come in pairs
// Always track per-draw range for unoptimized path
perDrawRanges.add(new DrawRange(currentVertexOffset, vertexCount, draw.transform, draw.commandIndex));
// Merge logic for optimized path:
// - same transform
// - same stateGeneration = no state commands (draw barriers) between draws
boolean canMerge = pendingTransform != null
&& pendingTransform.equals(draw.transform)
&& pendingStateGen == draw.stateGeneration;
if (canMerge) {
// Extend the pending range
pendingVertexCount += vertexCount;
} else {
// Flush pending range if any
flushPendingRange();
// Start new pending range
pendingStartVertex = currentVertexOffset;
pendingVertexCount = vertexCount;
pendingTransform = new Matrix4f(draw.transform);
pendingCommandIndex = draw.commandIndex;
pendingStateGen = draw.stateGeneration;
}
allVertices.addAll(draw.lines);
currentVertexOffset += vertexCount;
}
private void flushPendingRange() {
if (pendingTransform != null) {
mergedRanges.add(new DrawRange(pendingStartVertex, pendingVertexCount, pendingTransform, pendingCommandIndex));
pendingTransform = null;
pendingStateGen = -1;
}
}
/**
* Compile all accumulated line vertices into a single VBO.
* @return The compiled buffer with VBO and both range types, or null if empty
*/
CompiledLineBuffer finish() {
if (allVertices.isEmpty()) {
return null;
}
// Flush any remaining pending range
flushPendingRange();
VertexBuffer vbo = compileLinesToVBO(allVertices);
return new CompiledLineBuffer(
vbo,
mergedRanges.toArray(new DrawRange[0]),
perDrawRanges.toArray(new DrawRange[0])
);
}
/**
* Compile line vertices into a VertexBuffer (VAO or VBO).
* Lines use POSITION_COLOR format (x, y, z, color).
*/
private static VertexBuffer compileLinesToVBO(List<ImmediateModeRecorder.LineVertex> lines) {
final VertexFormat format = DefaultVertexFormat.POSITION_COLOR;
final VertexBuffer vao = VAOManager.createVAO(format, GL11.GL_LINES);
// POSITION_COLOR format: 3 floats (12 bytes) + 4 bytes color = 16 bytes per vertex
final int vertexSize = format.getVertexSize();
final ByteBuffer buffer = org.lwjgl.BufferUtils.createByteBuffer(vertexSize * lines.size());
for (ImmediateModeRecorder.LineVertex v : lines) {
buffer.putFloat(v.x);
buffer.putFloat(v.y);
buffer.putFloat(v.z);
buffer.putInt(v.color);
}
buffer.flip();
vao.upload(buffer);
return vao;
}
}
@@ -1,12 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
/**
* Result of building an optimized display list.
* Contains both the commands and the shared VBOs that need lifecycle management.
*/
@Desugar
record OptimizedListResult(DisplayListCommand[] commands, VertexBuffer[] ownedVbos) {}
@@ -0,0 +1,297 @@
package com.gtnewhorizons.angelica.glsm;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetByte;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetShort;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutShort;
/**
* Shared quad-to-triangle EBO for core profile GL_QUADS emulation.
*
* <p>Maintains a single growable element buffer with pre-computed indices mapping
* GL_QUADS vertex order to GL_TRIANGLES: {@code (0,1,3, 1,2,3)} per quad.
* Uses {@code GL_LAST_VERTEX_CONVENTION} winding (Mesa {@code do_quad} LAST path)
* so both emitted triangles share the quad's provoking vertex (v3).
*/
public final class QuadConverter {
public static final int INDEX_TYPE = GL11.GL_UNSIGNED_INT;
private static int eboId;
private static int maxQuads;
/** Scratch EBO for converted quad-element draws (drawQuadElementsAsTriangles). */
private static int scratchEboId;
private static int scratchEboCapacity; // in bytes
private QuadConverter() {}
private static void ensureCapacity(int quadCount) {
if (quadCount <= maxQuads) return;
// Round up to power of 2 for amortized growth
int newMaxQuads = Math.max(256, maxQuads);
while (newMaxQuads < quadCount) {
newMaxQuads *= 2;
}
if (eboId == 0) {
eboId = GL15.glGenBuffers();
}
// Generate index data: 6 ints per quad
final ByteBuffer indexData = memAlloc(newMaxQuads * 6 * 4);
long ptr = memAddress0(indexData);
for (int i = 0; i < newMaxQuads; i++) {
final int base = i * 4;
// GL_LAST_VERTEX_CONVENTION: both triangles share v3 as provoking vertex
// Triangle 1: 0, 1, 3
memPutInt(ptr, base);
memPutInt(ptr + 4, base + 1);
memPutInt(ptr + 8, base + 3);
// Triangle 2: 1, 2, 3
memPutInt(ptr + 12, base + 1);
memPutInt(ptr + 16, base + 2);
memPutInt(ptr + 20, base + 3);
ptr += 24;
}
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indexData, GL15.GL_STATIC_DRAW);
memFree(indexData);
maxQuads = newMaxQuads;
}
/**
* Convert a GL_QUADS glDrawArrays call to indexed GL_TRIANGLES.
* Binds the shared EBO to the current VAO and issues glDrawElements.
*
* @param first first vertex index (must be aligned to quad boundary, i.e. multiple of 4)
* @param vertexCount number of vertices (must be multiple of 4)
*/
public static void drawQuadsAsTriangles(int first, int vertexCount) {
assert first % 4 == 0 : "QuadConverter: first (" + first + ") must be a multiple of 4";
assert vertexCount % 4 == 0 : "QuadConverter: vertexCount (" + vertexCount + ") must be a multiple of 4";
final int quadCount = vertexCount / 4;
ensureCapacity(first / 4 + quadCount);
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
// Index offset: first vertex / 4 quads * 6 indices * 4 bytes per int
final long indexOffset = (long) (first / 4) * 6 * 4;
GL11.glDrawElements(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset);
}
/**
* Upload converted triangle indices to the scratch EBO and draw.
* Saves/restores the caller's EBO binding so subsequent indexed draws
* (or glGetBufferSubData reads) still see the original EBO.
*
* @param dst off-heap buffer with triangle index data (freed by this method)
* @param triIndexCount number of triangle indices
* @param indexType GL_UNSIGNED_INT or GL_UNSIGNED_SHORT
* @param bytesPerIndex bytes per index element
*/
private static void uploadAndDraw(ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex) {
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = GLStateManager.getBoundEBO();
if (scratchEboId == 0) {
scratchEboId = GL15.glGenBuffers();
}
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, scratchEboId);
if (needed > scratchEboCapacity) {
// Power-of-2 growth — allocate full capacity, upload actual data
int newCap = Math.max(4096, scratchEboCapacity);
while (newCap < needed) newCap *= 2;
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, newCap, GL15.GL_STREAM_DRAW);
scratchEboCapacity = newCap;
}
GL15.glBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, 0, dst);
GL11.glDrawElements(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L);
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
memFree(dst);
}
/**
* Convert an indexed GL_QUADS draw to GL_TRIANGLES by reading the source EBO,
* building a triangle index buffer, and issuing the converted draw.
*
* @param indexCount number of quad indices (must be multiple of 4)
* @param type GL_UNSIGNED_INT, GL_UNSIGNED_SHORT, or GL_UNSIGNED_BYTE
* @param offset byte offset into the currently bound EBO
*/
public static void drawQuadElementsAsTriangles(int indexCount, int type, long offset) {
if (indexCount == 0) return;
assert indexCount % 4 == 0 : "QuadConverter: indexCount must be multiple of 4";
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final int bytesPerIndex;
switch (type) {
case GL11.GL_UNSIGNED_INT: bytesPerIndex = 4; break;
case GL11.GL_UNSIGNED_SHORT: bytesPerIndex = 2; break;
case GL11.GL_UNSIGNED_BYTE: bytesPerIndex = 1; break;
default: return;
}
// Read source indices from caller's EBO
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
GL15.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, offset, src);
final long srcAddr = memAddress0(src);
// Allocate output as GL_UNSIGNED_INT
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
default /* GL_UNSIGNED_BYTE */ -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
}
// GL_LAST_VERTEX_CONVENTION: (a,b,d), (b,c,d)
final long o = (long) i * 24;
memPutInt(dstAddr + o, a);
memPutInt(dstAddr + o + 4, b);
memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b);
memPutInt(dstAddr + o + 16, c);
memPutInt(dstAddr + o + 20, d);
}
memFree(src);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Convert a client-side IntBuffer of quad indices to triangles.
*/
public static void drawQuadElementsAsTriangles(IntBuffer indices) {
final int indexCount = indices.remaining();
if (indexCount == 0) return;
assert indexCount % 4 == 0;
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final int a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Convert a client-side ShortBuffer of quad indices to triangles.
*/
public static void drawQuadElementsAsTriangles(ShortBuffer indices) {
final int indexCount = indices.remaining();
if (indexCount == 0) return;
assert indexCount % 4 == 0;
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 2);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final short a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 12;
memPutShort(dstAddr + o, a); memPutShort(dstAddr + o + 2, b); memPutShort(dstAddr + o + 4, d);
memPutShort(dstAddr + o + 6, b); memPutShort(dstAddr + o + 8, c); memPutShort(dstAddr + o + 10, d);
}
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
}
/**
* Convert a client-side ByteBuffer of quad indices (type-agnostic) to triangles.
*/
public static void drawQuadElementsAsTriangles(int count, int type, ByteBuffer indices) {
if (count == 0) return;
assert count % 4 == 0;
final int quadCount = count / 4;
final int triIndexCount = quadCount * 6;
final long srcAddr = memAddress0(indices) + indices.position();
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
case GL11.GL_UNSIGNED_BYTE -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
default -> { memFree(dst); return; }
}
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Clean up the shared EBO.
*/
public static void destroy() {
if (eboId != 0) {
GL15.glDeleteBuffers(eboId);
eboId = 0;
maxQuads = 0;
}
if (scratchEboId != 0) {
GL15.glDeleteBuffers(scratchEboId);
scratchEboId = 0;
scratchEboCapacity = 0;
}
}
}
@@ -1,23 +1,37 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSAAccess;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSACore;
import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import net.coderbot.iris.gl.shader.StandardMacros;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Vector3i;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.ARBBufferStorage;
import org.lwjgl.opengl.ARBClearBufferObject;
import org.lwjgl.opengl.ARBClearTexture;
import org.lwjgl.opengl.ARBShaderImageLoadStore;
import org.lwjgl.opengl.ARBShaderStorageBufferObject;
import org.lwjgl.opengl.EXTShaderImageLoadStore;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL33;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL42;
import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.GL44;
import org.lwjgl.opengl.NVXGpuMemoryInfo;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
@@ -31,13 +45,26 @@ public class RenderSystem {
private static final Logger LOGGER = LogManager.getLogger("RenderSystem");
private static DSAAccess dsaState;
private static boolean supportsCompute;
private static boolean supportsImageLoadStore;
private static boolean supportsSSBO;
private static boolean supportsBufferStorage;
private static boolean supportsClearTexture;
private static boolean supportsTesselation;
private static boolean supportsSamplerObjects;
private static int maxImageUnits;
private static int maxSSBOBindings;
private static int maxGlslVersion;
private static boolean supportsGpuShader4;
// Sampler object state tracking (null if unsupported)
private static int[] samplers;
private RenderSystem() {}
public static void initRenderer() {
try {
if (GLStateManager.capabilities.OpenGL45) {
dsaState = new DSACore();
dsaState = (Runtime.version().feature() > 8 && GLStateManager.capabilities.GL_EXT_direct_state_access) ? new DSAEXT() : new DSACore();
LOGGER.info("OpenGL 4.5 detected, enabling DSA.");
}
@@ -54,6 +81,60 @@ public class RenderSystem {
}
supportsCompute = supportsCompute();
supportsTesselation = GLStateManager.capabilities.GL_ARB_tessellation_shader || GLStateManager.capabilities.OpenGL40;
supportsImageLoadStore = GLStateManager.capabilities.OpenGL42 || GLStateManager.capabilities.GL_ARB_shader_image_load_store || GLStateManager.capabilities.GL_EXT_shader_image_load_store;
supportsSSBO = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_shader_storage_buffer_object;
supportsBufferStorage = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_buffer_storage;
supportsClearTexture = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_clear_texture;
// Cache maximum image units
if (supportsImageLoadStore) {
if (GLStateManager.capabilities.OpenGL42) {
maxImageUnits = GL11.glGetInteger(GL42.GL_MAX_IMAGE_UNITS);
} else if (GLStateManager.capabilities.GL_ARB_shader_image_load_store) {
maxImageUnits = GL11.glGetInteger(ARBShaderImageLoadStore.GL_MAX_IMAGE_UNITS);
} else {
maxImageUnits = GL11.glGetInteger(EXTShaderImageLoadStore.GL_MAX_IMAGE_UNITS_EXT);
}
} else {
maxImageUnits = 0;
}
// Cache maximum SSBO bindings
if (supportsSSBO) {
if (GLStateManager.capabilities.OpenGL43) {
maxSSBOBindings = GL11.glGetInteger(GL43.GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
} else {
maxSSBOBindings = GL11.glGetInteger(ARBShaderStorageBufferObject.GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS);
}
} else {
maxSSBOBindings = 0;
}
// Check for sampler objects support (GL 3.3+ or ARB extension)
supportsSamplerObjects = GLStateManager.capabilities.OpenGL33
|| GLStateManager.capabilities.GL_ARB_sampler_objects;
if (supportsSamplerObjects) {
samplers = new int[GL11.glGetInteger(GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS)];
}
maxGlslVersion = Integer.parseInt(StandardMacros.getGlVersion(GL20.GL_SHADING_LANGUAGE_VERSION));
supportsGpuShader4 = GLStateManager.capabilities.GL_EXT_gpu_shader4;
LOGGER.info("Max GLSL version: {}, GPU Shader4: {}", maxGlslVersion, supportsGpuShader4);
LOGGER.info("Image Load/Store: {}, Max Image Units: {}", supportsImageLoadStore, maxImageUnits);
LOGGER.info("SSBO: {}, Max SSBO Bindings: {}", supportsSSBO, maxSSBOBindings);
LOGGER.info("Buffer Storage: {}, Clear Texture: {}, Sampler Objects: {}", supportsBufferStorage, supportsClearTexture, supportsSamplerObjects);
if (GLStateManager.capabilities.OpenGL32) {
final int profileMask = GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
LOGGER.info("GL 3.3 core profile detected, enabling FFP shader emulation.");
ShaderManager.getInstance().enable();
GLStateManager.lineWidthMax = 1.0f;
}
}
}
public static void generateMipmaps(int texture, int mipmapTarget) {
@@ -65,7 +146,7 @@ public class RenderSystem {
}
public static void texImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLStateManager.glBindTexture(target, texture);
GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
@@ -131,20 +212,23 @@ public class RenderSystem {
dsaState.texParameterf(texture, target, pname, param);
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) {
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
public static void texImage1D(int texture, int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) {
GL11.glBindTexture(target, texture);
GL11.glTexImage1D(target, level, internalformat, width, border, format, type, pixels);
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
public static void texImage3D(int texture, int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) {
GL11.glBindTexture(target, texture);
GL12.glTexImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels);
}
public static String getProgramInfoLog(int program) {
@@ -208,13 +292,27 @@ public class RenderSystem {
}
public static int getMaxImageUnits() {
if (GLStateManager.capabilities.OpenGL42) {
return GL11.glGetInteger(GL42.GL_MAX_IMAGE_UNITS);
} else if (GLStateManager.capabilities.GL_EXT_shader_image_load_store) {
return GL11.glGetInteger(EXTShaderImageLoadStore.GL_MAX_IMAGE_UNITS_EXT);
} else {
return 0;
}
return maxImageUnits;
}
public static boolean supportsImageLoadStore() {
return supportsImageLoadStore;
}
public static boolean supportsSSBO() {
return supportsSSBO;
}
public static boolean supportsBufferStorage() {
return supportsBufferStorage;
}
public static boolean supportsClearTexture() {
return supportsClearTexture;
}
public static int getMaxSSBOBindings() {
return maxSSBOBindings;
}
public static void getProgramiv(int program, int value, IntBuffer storage) {
@@ -229,6 +327,14 @@ public class RenderSystem {
GL43.glDispatchCompute(workGroups.x, workGroups.y, workGroups.z);
}
public static void dispatchComputeIndirect(long offset) {
GL43.glDispatchComputeIndirect(offset);
}
public static void bindBuffer(int target, int buffer) {
GLStateManager.glBindBuffer(target, buffer);
}
public static void memoryBarrier(int barriers) {
if (supportsCompute) {
GL42.glMemoryBarrier(barriers);
@@ -241,10 +347,12 @@ public class RenderSystem {
public static void disableBufferBlend(int buffer) {
GL30.glDisablei(GL11.GL_BLEND, buffer);
GLStateManager.getBlendMode().setUnknownState();
}
public static void enableBufferBlend(int buffer) {
GL30.glEnablei(GL11.GL_BLEND, buffer);
GLStateManager.getBlendMode().setUnknownState();
}
public static void blendFuncSeparatei(int buffer, int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) {
@@ -255,6 +363,10 @@ public class RenderSystem {
dsaState.bindTextureToUnit(unit, texture);
}
public static void bindTextureToUnit(int target, int unit, int texture) {
dsaState.bindTextureToUnit(target, unit, texture);
}
public static final FloatBuffer PROJECTION_MATRIX_BUFFER = BufferUtils.createFloatBuffer(16);
public static void setupProjectionMatrix(Matrix4f matrix) {
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
@@ -290,4 +402,122 @@ public class RenderSystem {
return false;
}
}
public static int createBuffers() {
return GL15.glGenBuffers();
}
public static void bindBufferBase(int target, int index, int buffer) {
GL30.glBindBufferBase(target, index, buffer);
}
public static void bufferStorage(int target, long size, int flags) {
if (GLStateManager.capabilities.OpenGL44) {
GL44.glBufferStorage(target, size, flags);
} else if (GLStateManager.capabilities.GL_ARB_buffer_storage) {
ARBBufferStorage.glBufferStorage(target, size, flags);
} else {
// Fallback: use mutable storage
GL15.glBufferData(target, size, GL15.GL_DYNAMIC_DRAW);
}
}
public static void clearBufferSubData(int target, int internalFormat, long offset, long size, int format, int type, int[] data) {
final ByteBuffer buf = BufferUtils.createByteBuffer(data.length * 4);
buf.asIntBuffer().put(data);
if (GLStateManager.capabilities.OpenGL43) {
GL43.glClearBufferSubData(target, internalFormat, offset, size, format, type, buf);
} else if (GLStateManager.capabilities.GL_ARB_clear_buffer_object) {
ARBClearBufferObject.glClearBufferSubData(target, internalFormat, offset, size, format, type, buf);
}
}
public static void deleteBuffers(int buffer) {
GL15.glDeleteBuffers(buffer);
}
public static long getVRAM() {
if (GLStateManager.capabilities.GL_NVX_gpu_memory_info) {
return GL11.glGetInteger(NVXGpuMemoryInfo.GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX) * 1024L;
} else {
// Default to 4GB if we can't query VRAM
return 4294967296L;
}
}
public static void clearTexImage(int texture, int target, int level, int format, int type) {
if (GLStateManager.capabilities.OpenGL44) {
GL44.glClearTexImage(texture, level, format, type, (ByteBuffer) null);
} else {
ARBClearTexture.glClearTexImage(texture, level, format, type, (ByteBuffer) null);
}
}
public static void textureStorage1D(int texture, int target, int levels, int internalFormat, int width) {
dsaState.textureStorage1D(texture, target, levels, internalFormat, width);
}
public static void textureStorage2D(int texture, int target, int levels, int internalFormat, int width, int height) {
dsaState.textureStorage2D(texture, target, levels, internalFormat, width, height);
}
public static void textureStorage3D(int texture, int target, int levels, int internalFormat, int width, int height, int depth) {
dsaState.textureStorage3D(texture, target, levels, internalFormat, width, height, depth);
}
public static int getMaxGlslVersion() {
return maxGlslVersion;
}
public static boolean supportsGpuShader4() {
return supportsGpuShader4;
}
public static boolean supportsSnormFormats() {
return GLStateManager.capabilities.OpenGL31;
}
public static boolean supportsPackedFloatRenderable() {
return GLStateManager.capabilities.OpenGL30 && maxGlslVersion >= 130;
}
public static boolean supportsTesselation() {
return supportsTesselation;
}
public static boolean supportsSamplerObjects() {
return supportsSamplerObjects;
}
public static int genSampler() {
if (!supportsSamplerObjects) return 0;
return GL33.glGenSamplers();
}
public static void destroySampler(int sampler) {
if (!supportsSamplerObjects || sampler == 0) return;
GL33.glDeleteSamplers(sampler);
}
public static void samplerParameteri(int sampler, int pname, int param) {
if (!supportsSamplerObjects || sampler == 0) return;
GL33.glSamplerParameteri(sampler, pname, param);
}
public static void bindSamplerToUnit(int unit, int sampler) {
if (!supportsSamplerObjects) return;
if (samplers[unit] == sampler) return;
GL33.glBindSampler(unit, sampler);
samplers[unit] = sampler;
}
public static void unbindAllSamplers() {
if (!supportsSamplerObjects) return;
for (int i = 0; i < samplers.length; i++) {
if (samplers[i] != 0) {
GL33.glBindSampler(i, 0);
samplers[i] = 0;
}
}
}
}
@@ -1,246 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.line.ModelLine;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.primitive.ModelPrimitiveView;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.tri.ModelTriangle;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedPrimitiveDraw;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Accumulates tessellator primitives (lines, triangles) for a single vertex format during display list compilation.
* Similar to FormatBuffer but handles ModelPrimitiveView instances instead of quads.
* <p>
* Unlike LineBuffer which handles ImmediateModeRecorder.LineVertex (fixed 16-byte: pos+color),
* this handles ModelLine/ModelTriangle from the tessellator with format-optimized vertex size.
* <p>
* Primitives are separated by draw mode (GL_LINES, GL_TRIANGLES) into separate VBOs.
*/
class TessellatorPrimitiveBuffer {
final CapturingTessellator.Flags flags;
// Accumulated primitives separated by type
final List<ModelLine> allLines = new ArrayList<>();
final List<ModelTriangle> allTriangles = new ArrayList<>();
// Ranges for lines
final List<DrawRange> lineMergedRanges = new ArrayList<>();
final List<DrawRange> linePerDrawRanges = new ArrayList<>();
int currentLineVertexOffset = 0;
private int pendingLineStartVertex = -1;
private int pendingLineVertexCount = 0;
private Matrix4f pendingLineTransform = null;
private int pendingLineCommandIndex = -1;
private int pendingLineStateGen = -1;
// Ranges for triangles
final List<DrawRange> triangleMergedRanges = new ArrayList<>();
final List<DrawRange> trianglePerDrawRanges = new ArrayList<>();
int currentTriangleVertexOffset = 0;
private int pendingTriangleStartVertex = -1;
private int pendingTriangleVertexCount = 0;
private Matrix4f pendingTriangleTransform = null;
private int pendingTriangleCommandIndex = -1;
private int pendingTriangleStateGen = -1;
TessellatorPrimitiveBuffer(CapturingTessellator.Flags flags) {
this.flags = flags;
}
/**
* Add a primitive draw to this buffer.
* Separates lines and triangles, tracking ranges for both merged and per-draw paths.
*/
void addDraw(AccumulatedPrimitiveDraw draw) {
final List<ModelPrimitiveView> primitives = draw.primitives;
final int size = primitives.size();
// Single pass: count and collect primitives by type
int lineCount = 0;
int triangleCount = 0;
for (int i = 0; i < size; i++) {
final ModelPrimitiveView prim = primitives.get(i);
if (prim instanceof ModelLine ml) {
allLines.add(ml);
lineCount++;
} else if (prim instanceof ModelTriangle mt) {
allTriangles.add(mt);
triangleCount++;
}
}
// Process lines
if (lineCount > 0) {
final int lineVertexCount = lineCount * 2; // 2 vertices per line
linePerDrawRanges.add(new DrawRange(currentLineVertexOffset, lineVertexCount, draw.transform, draw.commandIndex));
// Merge logic for optimized path:
// - same transform
// - same stateGeneration = no state commands (draw barriers) between draws
boolean canMerge = pendingLineTransform != null
&& pendingLineTransform.equals(draw.transform)
&& pendingLineStateGen == draw.stateGeneration;
if (canMerge) {
pendingLineVertexCount += lineVertexCount;
} else {
flushPendingLineRange();
pendingLineStartVertex = currentLineVertexOffset;
pendingLineVertexCount = lineVertexCount;
pendingLineTransform = new Matrix4f(draw.transform);
pendingLineCommandIndex = draw.commandIndex;
pendingLineStateGen = draw.stateGeneration;
}
currentLineVertexOffset += lineVertexCount;
}
// Process triangles
if (triangleCount > 0) {
final int triangleVertexCount = triangleCount * 3; // 3 vertices per triangle
trianglePerDrawRanges.add(new DrawRange(currentTriangleVertexOffset, triangleVertexCount, draw.transform, draw.commandIndex));
// Merge logic for optimized path:
// - same transform
// - same stateGeneration = no state commands (draw barriers) between draws
boolean canMerge = pendingTriangleTransform != null
&& pendingTriangleTransform.equals(draw.transform)
&& pendingTriangleStateGen == draw.stateGeneration;
if (canMerge) {
pendingTriangleVertexCount += triangleVertexCount;
} else {
flushPendingTriangleRange();
pendingTriangleStartVertex = currentTriangleVertexOffset;
pendingTriangleVertexCount = triangleVertexCount;
pendingTriangleTransform = new Matrix4f(draw.transform);
pendingTriangleCommandIndex = draw.commandIndex;
pendingTriangleStateGen = draw.stateGeneration;
}
currentTriangleVertexOffset += triangleVertexCount;
}
}
private void flushPendingLineRange() {
if (pendingLineTransform != null) {
lineMergedRanges.add(new DrawRange(pendingLineStartVertex, pendingLineVertexCount, pendingLineTransform, pendingLineCommandIndex));
pendingLineTransform = null;
pendingLineStateGen = -1;
}
}
private void flushPendingTriangleRange() {
if (pendingTriangleTransform != null) {
triangleMergedRanges.add(new DrawRange(pendingTriangleStartVertex, pendingTriangleVertexCount, pendingTriangleTransform, pendingTriangleCommandIndex));
pendingTriangleTransform = null;
pendingTriangleStateGen = -1;
}
}
/**
* Compile all accumulated primitives into VBOs using optimal vertex format.
* @return The compiled buffers with separate VBOs for lines and triangles, or null if empty
*/
CompiledPrimitiveBuffers finish() {
flushPendingLineRange();
flushPendingTriangleRange();
if (allLines.isEmpty() && allTriangles.isEmpty()) {
return null;
}
// Use optimal format based on flags (same logic as quads)
final VertexFormat format = DisplayListManager.selectOptimalFormat(flags);
final VertexBuffer lineVbo = allLines.isEmpty() ? null : compileLinesToVBO(format);
final VertexBuffer triangleVbo = allTriangles.isEmpty() ? null : compileTrianglesToVBO(format);
return new CompiledPrimitiveBuffers(
flags,
lineVbo,
lineMergedRanges.toArray(new DrawRange[0]),
linePerDrawRanges.toArray(new DrawRange[0]),
triangleVbo,
triangleMergedRanges.toArray(new DrawRange[0]),
trianglePerDrawRanges.toArray(new DrawRange[0])
);
}
private VertexBuffer compileLinesToVBO(VertexFormat format) {
final int vertexSize = format.getVertexSize();
final int size = allLines.size();
final ByteBuffer buffer = BufferUtils.createByteBuffer(vertexSize * size * 2);
for (int i = 0; i < size; i++) {
writePrimitiveToBuffer(allLines.get(i), buffer);
}
buffer.flip();
final VertexBuffer vbo = VAOManager.createVAO(format, GL11.GL_LINES);
vbo.upload(buffer);
return vbo;
}
private VertexBuffer compileTrianglesToVBO(VertexFormat format) {
final int vertexSize = format.getVertexSize();
final int size = allTriangles.size();
final ByteBuffer buffer = BufferUtils.createByteBuffer(vertexSize * size * 3);
for (int i = 0; i < size; i++) {
writePrimitiveToBuffer(allTriangles.get(i), buffer);
}
buffer.flip();
final VertexBuffer vbo = VAOManager.createVAO(format, GL11.GL_TRIANGLES);
vbo.upload(buffer);
return vbo;
}
/**
* Write a primitive's vertices to the buffer using the format specified by flags.
* Only writes attributes that are present in the format.
*/
private void writePrimitiveToBuffer(ModelPrimitiveView prim, ByteBuffer buffer) {
final int vertexCount = prim.getVertexCount();
final boolean hasTexture = flags.hasTexture;
final boolean hasBrightness = flags.hasBrightness;
final boolean hasColor = flags.hasColor;
final boolean hasNormals = flags.hasNormals;
for (int i = 0; i < vertexCount; i++) {
// Position (always present) - 12 bytes
buffer.putFloat(prim.getX(i));
buffer.putFloat(prim.getY(i));
buffer.putFloat(prim.getZ(i));
// Color (4 bytes, ABGR) - if present
if (hasColor) {
buffer.putInt(prim.getColor(i));
}
// Texture (2 floats = 8 bytes) - if present
if (hasTexture) {
buffer.putFloat(prim.getTexU(i));
buffer.putFloat(prim.getTexV(i));
}
// Light/Brightness (4 bytes, packed) - if present
if (hasBrightness) {
buffer.putInt(prim.getLight(i));
}
// Normal (4 bytes, packed) - if present
if (hasNormals) {
buffer.putInt(prim.getForgeNormal(i));
}
}
}
}
@@ -16,30 +16,21 @@ public class DSAARB extends DSAUnsupported {
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
// Note: DSA glTextureImage2D doesn't exist in ARB_direct_state_access
// We need to use glTextureStorage2D + glTextureSubImage2D, or fall back to bind-based approach
// For simplicity, use the fallback which binds, uploads, and restores
super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels);
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) {
ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
@Override
public void texParameteri(int texture, int target, int pname, int param) {
param = GLStateManager.remapTexClamp(pname, param);
if(!GLStateManager.updateTexParameteriCache(target, texture, pname, param)) return;
ARBDirectStateAccess.glTextureParameteri(texture, pname, param);
@@ -47,6 +38,7 @@ public class DSAARB extends DSAUnsupported {
@Override
public void texParameterf(int texture, int target, int pname, float param) {
param = GLStateManager.remapTexClamp(pname, param);
if(!GLStateManager.updateTexParameterfCache(target, texture, pname, param)) return;
ARBDirectStateAccess.glTextureParameterf(texture, pname, param);
@@ -54,6 +46,7 @@ public class DSAARB extends DSAUnsupported {
@Override
public void texParameteriv(int texture, int target, int pname, IntBuffer params) {
GLStateManager.remapTexClampBuffer(pname, params);
ARBDirectStateAccess.glTextureParameter(texture, pname, params);
}
@@ -94,6 +87,11 @@ public class DSAARB extends DSAUnsupported {
}
}
@Override
public void bindTextureToUnit(int target, int unit, int texture) {
bindTextureToUnit(unit, texture);
}
@Override
public int bufferStorage(int target, FloatBuffer data, int usage) {
final int buffer = GL45.glCreateBuffers();
@@ -121,4 +119,19 @@ public class DSAARB extends DSAUnsupported {
public int createTexture(int target) {
return ARBDirectStateAccess.glCreateTextures(target);
}
@Override
public void textureStorage1D(int texture, int target, int levels, int internalFormat, int width) {
ARBDirectStateAccess.glTextureStorage1D(texture, levels, internalFormat, width);
}
@Override
public void textureStorage2D(int texture, int target, int levels, int internalFormat, int width, int height) {
ARBDirectStateAccess.glTextureStorage2D(texture, levels, internalFormat, width, height);
}
@Override
public void textureStorage3D(int texture, int target, int levels, int internalFormat, int width, int height, int depth) {
ARBDirectStateAccess.glTextureStorage3D(texture, levels, internalFormat, width, height, depth);
}
}
@@ -8,10 +8,8 @@ public interface DSAAccess {
void generateMipmaps(int texture, int target);
// Texture image upload - DSA allows specifying texture directly without binding
void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels);
void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels);
void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels);
void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels);
void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset);
void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset);
void texParameteri(int texture, int target, int pname, int param);
void texParameterf(int texture, int target, int pname, float param);
@@ -28,6 +26,7 @@ public interface DSAAccess {
void copyTexSubImage2D(int destTexture, int target, int i, int i1, int i2, int i3, int i4, int width, int height);
void bindTextureToUnit(int unit, int texture);
void bindTextureToUnit(int target, int unit, int texture);
int bufferStorage(int target, FloatBuffer data, int usage);
@@ -37,4 +36,9 @@ public interface DSAAccess {
int createFramebuffer();
int createTexture(int target);
// Texture storage methods for custom images
void textureStorage1D(int texture, int target, int levels, int internalFormat, int width);
void textureStorage2D(int texture, int target, int levels, int internalFormat, int width, int height);
void textureStorage3D(int texture, int target, int levels, int internalFormat, int width, int height, int depth);
}
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.glsm.dsa;
import org.lwjgl.opengl.EXTDirectStateAccess;
public class DSAEXT extends DSACore {
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
EXTDirectStateAccess.glTextureImage2DEXT(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
@@ -2,14 +2,15 @@ package com.gtnewhorizons.angelica.glsm.dsa;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import net.minecraft.client.renderer.OpenGlHelper;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL42;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -22,36 +23,19 @@ public class DSAUnsupported implements DSAAccess {
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
// Get what cache thinks is bound - we'll restore to this to keep cache/GL in sync
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GL11.glBindTexture(target, cachedBinding); // Restore to cache value
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
if(cachedBinding != texture) GL11.glBindTexture(target, cachedBinding);
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GL11.glBindTexture(target, cachedBinding);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
GL11.glBindTexture(target, cachedBinding);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
GL11.glBindTexture(target, cachedBinding);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
if(cachedBinding != texture) GL11.glBindTexture(target, cachedBinding);
}
@Override
@@ -74,13 +58,13 @@ public class DSAUnsupported implements DSAAccess {
@Override
public void readBuffer(int framebuffer, int buffer) {
OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, framebuffer);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GL11.glReadBuffer(buffer);
}
@Override
public void drawBuffers(int framebuffer, IntBuffer buffers) {
OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, framebuffer);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GL20.glDrawBuffers(buffers);
}
@@ -113,12 +97,18 @@ public class DSAUnsupported implements DSAAccess {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
}
@Override
public void bindTextureToUnit(int target, int unit, int texture) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + unit);
GLStateManager.glBindTexture(target, texture);
}
@Override
public int bufferStorage(int target, FloatBuffer data, int usage) {
final int buffer = GL15.glGenBuffers();
GL15.glBindBuffer(target, buffer);
GLStateManager.glBindBuffer(target, buffer);
RenderSystem.bufferData(target, data, usage);
GL15.glBindBuffer(target, 0);
GLStateManager.glBindBuffer(target, 0);
return buffer;
}
@@ -126,21 +116,21 @@ public class DSAUnsupported implements DSAAccess {
@Override
public void blitFramebuffer(int source, int dest, int offsetX, int offsetY, int width, int height, int offsetX2, int offsetY2, int width2, int height2,
int bufferChoice, int filter) {
OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_READ_FRAMEBUFFER, source);
OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_DRAW_FRAMEBUFFER, dest);
GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, source);
GLStateManager.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, dest);
GL30.glBlitFramebuffer(offsetX, offsetY, width, height, offsetX2, offsetY2, width2, height2, bufferChoice, filter);
}
@Override
public void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) {
OpenGlHelper.func_153171_g/*glBindFramebuffer*/(fbtarget, fb);
GLStateManager.glBindFramebuffer(fbtarget, fb);
GL30.glFramebufferTexture2D(fbtarget, attachment, target, texture, levels);
}
@Override
public int createFramebuffer() {
final int framebuffer = OpenGlHelper.func_153165_e/*glGenFramebuffers*/();
OpenGlHelper.func_153171_g/*glBindFramebuffer*/(GL30.GL_FRAMEBUFFER, framebuffer);
final int framebuffer = GLStateManager.glGenFramebuffers();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
return framebuffer;
}
@@ -150,4 +140,28 @@ public class DSAUnsupported implements DSAAccess {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
return texture;
}
@Override
public void textureStorage1D(int texture, int target, int levels, int internalFormat, int width) {
final int previous = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_1D);
GL11.glBindTexture(target, texture);
GL42.glTexStorage1D(target, levels, internalFormat, width);
GL11.glBindTexture(target, previous);
}
@Override
public void textureStorage2D(int texture, int target, int levels, int internalFormat, int width, int height) {
final int previous = (target == GL11.GL_TEXTURE_2D) ? GLStateManager.getBoundTextureForServerState() : GL11.glGetInteger(GL31.GL_TEXTURE_BINDING_RECTANGLE);
GL11.glBindTexture(target, texture);
GL42.glTexStorage2D(target, levels, internalFormat, width, height);
GL11.glBindTexture(target, previous);
}
@Override
public void textureStorage3D(int texture, int target, int levels, int internalFormat, int width, int height, int depth) {
final int previous = GL11.glGetInteger(GL12.GL_TEXTURE_BINDING_3D);
GL11.glBindTexture(target, texture);
GL42.glTexStorage3D(target, levels, internalFormat, width, height, depth);
GL11.glBindTexture(target, previous);
}
}
@@ -0,0 +1,153 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
/**
* Compact key capturing the fragment shader permutation axes for FFP emulation.
* Packed into a single long for fast hash/compare.
*/
public final class FragmentKey {
private final long packed;
private static final int BIT_FOG_MODE = 0;
private static final int BIT_ALPHA_TEST = 2;
private static final int BIT_ALPHA_FUNC = 3;
private static final int BIT_SEPARATE_SPECULAR = 6;
private static final int BIT_TEXTURE = 7;
private static final int BIT_LIGHTMAP = 8;
private static final int BIT_TEX_ENV_MODE = 9; // 3 bits: 0=MODULATE, 1=REPLACE, 2=ADD, 3=DECAL, 4=BLEND
public static final int FOG_NONE = 0;
public static final int FOG_LINEAR = 1;
public static final int FOG_EXP = 2;
public static final int FOG_EXP2 = 3;
private FragmentKey(long packed) {
this.packed = packed;
}
public long pack() { return packed; }
public int fogMode() { return (int)(packed & 0x3); }
public boolean alphaTestEnabled() { return ((packed >> BIT_ALPHA_TEST) & 1) != 0; }
public int alphaTestFunc() { return (int)((packed >> BIT_ALPHA_FUNC) & 0x7); }
public boolean separateSpecular() { return ((packed >> BIT_SEPARATE_SPECULAR) & 1) != 0; }
public boolean textureEnabled() { return ((packed >> BIT_TEXTURE) & 1) != 0; }
public boolean lightmapEnabled() { return ((packed >> BIT_LIGHTMAP) & 1) != 0; }
public int texEnvMode() { return (int)((packed >> BIT_TEX_ENV_MODE) & 0x7); }
public static final int TEX_ENV_MODULATE = 0;
public static final int TEX_ENV_REPLACE = 1;
public static final int TEX_ENV_ADD = 2;
public static final int TEX_ENV_DECAL = 3;
public static final int TEX_ENV_BLEND = 4;
public static long packFromState() {
long bits = 0;
// Fog
if (GLStateManager.getFogMode().isEnabled()) {
final int glFogMode = GLStateManager.getFogState().getFogMode();
final int fogMode = switch (glFogMode) {
case GL11.GL_LINEAR -> FOG_LINEAR;
case GL11.GL_EXP -> FOG_EXP;
case GL11.GL_EXP2 -> FOG_EXP2;
default -> FOG_NONE;
};
bits |= ((long) fogMode & 0x3);
}
// Alpha test
if (GLStateManager.getAlphaTest().isEnabled()) {
bits |= (1L << BIT_ALPHA_TEST);
final int func = encodeAlphaFunc(GLStateManager.getAlphaState().getFunction());
bits |= ((long) func & 0x7) << BIT_ALPHA_FUNC;
}
// Separate specular
if (GLStateManager.getLightingState().isEnabled()
&& GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
bits |= (1L << BIT_SEPARATE_SPECULAR);
}
// Texture unit 0
if (GLStateManager.getTextures().getTextureUnitStates(0).isEnabled()) {
bits |= (1L << BIT_TEXTURE);
// Tex env mode only matters when texture is enabled
final int envMode = encodeTexEnvMode(GLStateManager.getTexEnvMode().getValue());
bits |= ((long) envMode & 0x7) << BIT_TEX_ENV_MODE;
}
// Texture unit 1 (lightmap)
if (GLStateManager.getTextures().getTextureUnitStates(1).isEnabled()) {
bits |= (1L << BIT_LIGHTMAP);
}
return bits;
}
public static FragmentKey fromState() {
return new FragmentKey(packFromState());
}
static FragmentKey fromPacked(long packed) {
return new FragmentKey(packed);
}
private static int encodeAlphaFunc(int glFunc) {
return glFunc & 0x7;
}
private static int encodeTexEnvMode(int glMode) {
return switch (glMode) {
case GL11.GL_MODULATE -> TEX_ENV_MODULATE;
case GL11.GL_REPLACE -> TEX_ENV_REPLACE;
case GL11.GL_ADD -> TEX_ENV_ADD;
case GL11.GL_DECAL -> TEX_ENV_DECAL;
case GL11.GL_BLEND -> TEX_ENV_BLEND;
default -> TEX_ENV_MODULATE;
};
}
public static int decodeAlphaFunc(int encoded) {
return 0x0200 | encoded;
}
@Override
public int hashCode() {
return Long.hashCode(packed);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof FragmentKey other)) return false;
return packed == other.packed;
}
@Override
public String toString() {
final String fogName = switch (fogMode()) {
case FOG_NONE -> "NONE";
case FOG_LINEAR -> "LINEAR";
case FOG_EXP -> "EXP";
case FOG_EXP2 -> "EXP2";
default -> "?";
};
final String envName = switch (texEnvMode()) {
case TEX_ENV_MODULATE -> "MOD";
case TEX_ENV_REPLACE -> "REPL";
case TEX_ENV_ADD -> "ADD";
case TEX_ENV_DECAL -> "DEC";
case TEX_ENV_BLEND -> "BL";
default -> "?";
};
return String.format("FFPFragmentKey[0x%03X: fog=%s alpha=%b(%s) specSep=%b tex=%b(env=%s) lm=%b]",
packed, fogName, alphaTestEnabled(),
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-",
separateSpecular(), textureEnabled(), envName, lightmapEnabled());
}
}
@@ -0,0 +1,167 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import org.lwjgl.opengl.GL11;
/**
* Generates GLSL 330 core fragment shaders for FFP emulation.
* Follows Mesa's ff_fragment_shader.c and st_nir_lower_fog.c.
*/
public final class FragmentShaderGenerator {
private FragmentShaderGenerator() {}
public static String generate(FragmentKey key) {
final StringBuilder sb = new StringBuilder(1024);
sb.append("#version 330 core\n\n");
emitInputs(sb, key);
emitUniforms(sb, key);
sb.append("out vec4 fragColor;\n\n");
sb.append("void main() {\n");
emitTextureSampling(sb, key);
emitColorCombine(sb, key);
emitSpecularAdd(sb, key);
emitAlphaTest(sb, key);
emitFog(sb, key);
sb.append(" fragColor = color;\n");
sb.append("}\n");
return sb.toString();
}
private static void emitInputs(StringBuilder sb, FragmentKey key) {
sb.append("// Inputs from vertex shader\n");
sb.append("in vec4 v_Color;\n");
if (key.separateSpecular()) {
sb.append("in vec3 v_SpecularColor;\n");
}
if (key.textureEnabled()) {
sb.append("in vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("in vec4 v_TexCoord1;\n");
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("in float v_FogCoord;\n");
}
sb.append('\n');
}
private static void emitUniforms(StringBuilder sb, FragmentKey key) {
if (key.textureEnabled()) {
sb.append("uniform sampler2D u_Sampler0;\n");
}
if (key.lightmapEnabled()) {
sb.append("uniform sampler2D u_Sampler1;\n");
}
if (key.alphaTestEnabled()) {
sb.append("uniform float u_AlphaRef;\n");
}
if (key.textureEnabled() && key.texEnvMode() == FragmentKey.TEX_ENV_BLEND) {
sb.append("uniform vec4 u_TexEnvColor;\n");
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("uniform vec4 u_FogParams;\n");
sb.append("uniform vec4 u_FogColor;\n");
}
sb.append('\n');
}
private static void emitTextureSampling(StringBuilder sb, FragmentKey key) {
if (key.textureEnabled()) {
sb.append(" vec4 texColor = texture(u_Sampler0, v_TexCoord0.st);\n");
}
if (key.lightmapEnabled()) {
sb.append(" vec4 lightmapColor = texture(u_Sampler1, v_TexCoord1.st);\n");
}
}
private static void emitColorCombine(StringBuilder sb, FragmentKey key) {
if (!key.textureEnabled()) {
sb.append(" // No texture — vertex color only\n");
sb.append(" vec4 color = v_Color;\n");
} else {
final int envMode = key.texEnvMode();
switch (envMode) {
case FragmentKey.TEX_ENV_REPLACE -> {
sb.append(" // Color combine (REPLACE)\n");
sb.append(" vec4 color = texColor;\n");
}
case FragmentKey.TEX_ENV_ADD -> {
sb.append(" // Color combine (ADD)\n");
sb.append(" vec4 color = vec4(clamp(v_Color.rgb + texColor.rgb, 0.0, 1.0), v_Color.a * texColor.a);\n");
}
case FragmentKey.TEX_ENV_DECAL -> {
sb.append(" // Color combine (DECAL)\n");
sb.append(" vec4 color = vec4(mix(v_Color.rgb, texColor.rgb, texColor.a), v_Color.a);\n");
}
case FragmentKey.TEX_ENV_BLEND -> {
sb.append(" // Color combine (BLEND)\n");
sb.append(" vec4 color = vec4(v_Color.rgb * (vec3(1.0) - texColor.rgb) + u_TexEnvColor.rgb * texColor.rgb, v_Color.a * texColor.a);\n");
}
default -> {
sb.append(" // Color combine (MODULATE)\n");
sb.append(" vec4 color = v_Color * texColor;\n");
}
}
}
if (key.lightmapEnabled()) {
sb.append(" color *= lightmapColor;\n");
}
}
private static void emitSpecularAdd(StringBuilder sb, FragmentKey key) {
if (key.separateSpecular()) {
sb.append(" // Add separate specular\n");
sb.append(" color.rgb += v_SpecularColor;\n");
}
}
private static void emitAlphaTest(StringBuilder sb, FragmentKey key) {
if (!key.alphaTestEnabled()) return;
sb.append(" // Alpha test\n");
final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc());
switch (func) {
case GL11.GL_NEVER ->
sb.append(" discard;\n");
case GL11.GL_LESS ->
sb.append(" if (color.a >= u_AlphaRef) discard;\n");
case GL11.GL_EQUAL ->
sb.append(" if (color.a != u_AlphaRef) discard;\n");
case GL11.GL_LEQUAL ->
sb.append(" if (color.a > u_AlphaRef) discard;\n");
case GL11.GL_GREATER ->
sb.append(" if (color.a <= u_AlphaRef) discard;\n");
case GL11.GL_NOTEQUAL ->
sb.append(" if (color.a == u_AlphaRef) discard;\n");
case GL11.GL_GEQUAL ->
sb.append(" if (color.a < u_AlphaRef) discard;\n");
case GL11.GL_ALWAYS -> {}
}
}
private static void emitFog(StringBuilder sb, FragmentKey key) {
final int fogMode = key.fogMode();
if (fogMode == FragmentKey.FOG_NONE) return;
sb.append(" // Fog\n");
sb.append(" float f;\n");
switch (fogMode) {
case FragmentKey.FOG_LINEAR ->
sb.append(" f = v_FogCoord * u_FogParams.x + u_FogParams.y;\n");
case FragmentKey.FOG_EXP ->
sb.append(" f = exp2(-(v_FogCoord * u_FogParams.z));\n");
case FragmentKey.FOG_EXP2 -> {
sb.append(" float fogTmp = v_FogCoord * u_FogParams.w;\n");
sb.append(" f = exp2(-(fogTmp * fogTmp));\n");
}
}
sb.append(" f = clamp(f, 0.0, 1.0);\n");
sb.append(" color.rgb = mix(u_FogColor.rgb, color.rgb, f);\n");
}
}
@@ -0,0 +1,236 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import lombok.Getter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.KHRDebug;
/**
* A compiled and linked FFP emulation program (vertex + fragment shader pair). Owns the GL program handle and caches uniform locations.
*/
public class Program {
private static final Logger LOGGER = LogManager.getLogger("FFPProgram");
@Getter private final int programId;
@Getter private final VertexKey vertexKey;
@Getter private final FragmentKey fragmentKey;
// Uniform locations (-1 = not present in this variant)
// Matrices
public int locModelViewMatrix = -1;
public int locProjectionMatrix = -1;
public int locMVPMatrix = -1;
public int locNormalMatrix = -1;
public int locTextureMatrix0 = -1;
// Current state defaults
public int locCurrentNormal = -1;
public int locCurrentColor = -1;
public int locCurrentTexCoord = -1;
public int locCurrentLightmapCoord = -1;
public int locLightmapTextureMatrix = -1;
// Lighting - color material path (raw values)
public int locLightModelAmbient = -1;
public int locMaterialEmission = -1;
public int locMaterialAmbient = -1;
public int locMaterialDiffuse = -1;
public int locMaterialSpecular = -1;
public int locMaterialShininess = -1;
public int locLight0Ambient = -1;
public int locLight0Diffuse = -1;
public int locLight0Specular = -1;
public int locLight1Ambient = -1;
public int locLight1Diffuse = -1;
public int locLight1Specular = -1;
// Lighting - pre-computed path
public int locSceneColor = -1;
public int locLightProd0Ambient = -1;
public int locLightProd0Diffuse = -1;
public int locLightProd0Specular = -1;
public int locLightProd1Ambient = -1;
public int locLightProd1Diffuse = -1;
public int locLightProd1Specular = -1;
// Light positions
public int locLight0Position = -1;
public int locLight1Position = -1;
// Normal scale (for rescale normals)
public int locNormalScale = -1;
// TexGen plane uniforms
public int locTexGenObjPlaneS = -1;
public int locTexGenObjPlaneT = -1;
public int locTexGenObjPlaneR = -1;
public int locTexGenObjPlaneQ = -1;
public int locTexGenEyePlaneS = -1;
public int locTexGenEyePlaneT = -1;
public int locTexGenEyePlaneR = -1;
public int locTexGenEyePlaneQ = -1;
// Clip planes
public int locClipPlanes = -1;
// Fragment uniforms
public int locSampler0 = -1;
public int locSampler1 = -1;
public int locAlphaRef = -1;
public int locTexEnvColor = -1;
public int locFogParams = -1;
public int locFogColor = -1;
Program(int programId, VertexKey vertexKey, FragmentKey fragmentKey) {
this.programId = programId;
this.vertexKey = vertexKey;
this.fragmentKey = fragmentKey;
resolveLocations();
}
private void resolveLocations() {
locModelViewMatrix = loc("u_ModelViewMatrix");
locProjectionMatrix = loc("u_ProjectionMatrix");
locMVPMatrix = loc("u_MVPMatrix");
locNormalMatrix = loc("u_NormalMatrix");
locTextureMatrix0 = loc("u_TextureMatrix0");
locCurrentNormal = loc("u_CurrentNormal");
locCurrentColor = loc("u_CurrentColor");
locCurrentTexCoord = loc("u_CurrentTexCoord");
locCurrentLightmapCoord = loc("u_CurrentLightmapCoord");
locLightmapTextureMatrix = loc("u_LightmapTextureMatrix");
// Color material path
locLightModelAmbient = loc("u_LightModelAmbient");
locMaterialEmission = loc("u_MaterialEmission");
locMaterialAmbient = loc("u_MaterialAmbient");
locMaterialDiffuse = loc("u_MaterialDiffuse");
locMaterialSpecular = loc("u_MaterialSpecular");
locMaterialShininess = loc("u_MaterialShininess");
locLight0Ambient = loc("u_Light0Ambient");
locLight0Diffuse = loc("u_Light0Diffuse");
locLight0Specular = loc("u_Light0Specular");
locLight1Ambient = loc("u_Light1Ambient");
locLight1Diffuse = loc("u_Light1Diffuse");
locLight1Specular = loc("u_Light1Specular");
// Pre-computed path
locSceneColor = loc("u_SceneColor");
locLightProd0Ambient = loc("u_LightProd0Ambient");
locLightProd0Diffuse = loc("u_LightProd0Diffuse");
locLightProd0Specular = loc("u_LightProd0Specular");
locLightProd1Ambient = loc("u_LightProd1Ambient");
locLightProd1Diffuse = loc("u_LightProd1Diffuse");
locLightProd1Specular = loc("u_LightProd1Specular");
// Light positions
locLight0Position = loc("u_Light0Position");
locLight1Position = loc("u_Light1Position");
locNormalScale = loc("u_NormalScale");
// TexGen
locTexGenObjPlaneS = loc("u_TexGenObjPlaneS");
locTexGenObjPlaneT = loc("u_TexGenObjPlaneT");
locTexGenObjPlaneR = loc("u_TexGenObjPlaneR");
locTexGenObjPlaneQ = loc("u_TexGenObjPlaneQ");
locTexGenEyePlaneS = loc("u_TexGenEyePlaneS");
locTexGenEyePlaneT = loc("u_TexGenEyePlaneT");
locTexGenEyePlaneR = loc("u_TexGenEyePlaneR");
locTexGenEyePlaneQ = loc("u_TexGenEyePlaneQ");
// Clip planes
locClipPlanes = loc("u_ClipPlane[0]");
// Fragment
locSampler0 = loc("u_Sampler0");
locSampler1 = loc("u_Sampler1");
locAlphaRef = loc("u_AlphaRef");
locTexEnvColor = loc("u_TexEnvColor");
locFogParams = loc("u_FogParams");
locFogColor = loc("u_FogColor");
}
private int loc(String name) {
return GL20.glGetUniformLocation(programId, name);
}
public void destroy() {
GL20.glDeleteProgram(programId);
}
/**
* Compile a shader, link it into a program, and return the FFPProgram.
*/
static Program create(VertexKey vk, FragmentKey fk, String vertSrc, String fragSrc) {
final int vs = compileShader(GL20.GL_VERTEX_SHADER, vertSrc, "ffp_v_" + Long.toHexString(vk.pack()));
final int fs = compileShader(GL20.GL_FRAGMENT_SHADER, fragSrc, "ffp_f_" + Long.toHexString(fk.pack()));
final int program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
GL20.glAttachShader(program, fs);
GL20.glLinkProgram(program);
final String log = RenderSystem.getProgramInfoLog(program);
if (!log.isEmpty()) {
LOGGER.warn("FFP program link log (vk=0x{}, fk=0x{}): {}", Long.toHexString(vk.pack()), Long.toHexString(fk.pack()), log);
}
final int linkStatus = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
if (linkStatus != GL11.GL_TRUE) {
GL20.glDeleteProgram(program);
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
throw new RuntimeException("FFP shader link failed (vk=0x" + Long.toHexString(vk.pack()) + ", fk=0x" + Long.toHexString(fk.pack()) + "): " + log);
}
final String debugName = "FFP(v=0x" + Long.toHexString(vk.pack()) + ",f=0x" + Long.toHexString(fk.pack()) + ")";
GLDebug.nameObject(KHRDebug.GL_PROGRAM, program, debugName);
// Detach and delete individual shaders — they're linked into the program
GL20.glDetachShader(program, vs);
GL20.glDetachShader(program, fs);
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
final Program ffpProgram = new Program(program, vk, fk);
final int previousProgram = GLStateManager.getActiveProgram();
GL20.glUseProgram(program);
if (ffpProgram.locSampler0 != -1) GL20.glUniform1i(ffpProgram.locSampler0, 0);
if (ffpProgram.locSampler1 != -1) GL20.glUniform1i(ffpProgram.locSampler1, 1);
GL20.glUseProgram(previousProgram);
return ffpProgram;
}
private static int compileShader(int type, String src, String name) {
final int shader = GL20.glCreateShader(type);
GL20.glShaderSource(shader, src);
GL20.glCompileShader(shader);
final String typeName = (type == GL20.GL_VERTEX_SHADER) ? "vertex" : "fragment";
GLDebug.nameObject(KHRDebug.GL_SHADER, shader, name + "(" + typeName + ")");
final String log = RenderSystem.getShaderInfoLog(shader);
if (!log.isEmpty()) {
LOGGER.warn("FFP {} shader compilation log for {}: {}", typeName, name, log);
}
final int result = GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS);
if (result != GL11.GL_TRUE) {
LOGGER.error("FFP {} shader source:\n{}", typeName, src);
GL20.glDeleteShader(shader);
throw new RuntimeException("FFP " + typeName + " shader compilation failed for " + name + ": " + log);
}
return shader;
}
}
@@ -0,0 +1,133 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import lombok.Setter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Cache for FFP shader program variants. Keyed on (vertexKey, fragmentKey) pairs.
*/
public class ShaderCache {
private static final Logger LOGGER = LogManager.getLogger("FFPShaderCache");
private final Long2ObjectOpenHashMap<String> vertexSourceCache = new Long2ObjectOpenHashMap<>();
private final Long2ObjectOpenHashMap<String> fragmentSourceCache = new Long2ObjectOpenHashMap<>();
private final Long2ObjectOpenHashMap<Program> programCache = new Long2ObjectOpenHashMap<>();
private long lastCombinedKey = Long.MIN_VALUE;
private Program lastProgram = null;
@Setter private Path dumpDir = null;
/**
* Get or create an FFP program for the given packed state keys.
*/
public Program getOrCreate(long vkPacked, long fkPacked) {
final long combinedKey = (fkPacked << 32) | (vkPacked & 0xFFFFFFFFL);
if (combinedKey == lastCombinedKey && lastProgram != null) {
return lastProgram;
}
Program program = programCache.get(combinedKey);
if (program != null) {
lastCombinedKey = combinedKey;
lastProgram = program;
return program;
}
final VertexKey vk = VertexKey.fromPacked(vkPacked);
final FragmentKey fk = FragmentKey.fromPacked(fkPacked);
final String vertSrc = getOrGenerateVertex(vk);
final String fragSrc = getOrGenerateFragment(fk);
program = Program.create(vk, fk, vertSrc, fragSrc);
programCache.put(combinedKey, program);
lastCombinedKey = combinedKey;
lastProgram = program;
LOGGER.debug("FFP variant compiled: {} + {} (total: {} programs)", vk, fk, programCache.size());
// Debug dump
if (dumpDir != null) {
dumpShader(vkPacked, ".vert.glsl", vertSrc);
dumpShader(fkPacked, ".frag.glsl", fragSrc);
}
return program;
}
private String getOrGenerateVertex(VertexKey vk) {
return vertexSourceCache.computeIfAbsent(vk.pack(), k -> VertexShaderGenerator.generate(vk));
}
private String getOrGenerateFragment(FragmentKey fk) {
return fragmentSourceCache.computeIfAbsent(fk.pack(), k -> FragmentShaderGenerator.generate(fk));
}
private void dumpShader(long key, String suffix, String source) {
if (dumpDir == null) return;
try {
Files.createDirectories(dumpDir);
final Path file = dumpDir.resolve("ffp_" + Long.toHexString(key) + suffix);
if (!Files.exists(file)) {
Files.writeString(file, source, StandardCharsets.UTF_8);
}
} catch (IOException e) {
LOGGER.warn("Failed to dump FFP shader to {}: {}", dumpDir, e.getMessage());
}
}
/**
* Dump all cached shaders to disk. Useful for debugging.
*/
public void dumpAll(Path outputDir) {
try {
Files.createDirectories(outputDir);
for (var entry : vertexSourceCache.entrySet()) {
dumpShader(entry.getKey(), ".vert.glsl", entry.getValue());
}
for (var entry : fragmentSourceCache.entrySet()) {
dumpShader(entry.getKey(), ".frag.glsl", entry.getValue());
}
LOGGER.info("Dumped {} vertex and {} fragment FFP shader sources to {}", vertexSourceCache.size(), fragmentSourceCache.size(), outputDir);
} catch (IOException e) {
LOGGER.warn("Failed to dump FFP shaders: {}", e.getMessage());
}
}
/**
* Destroy all cached programs and clear caches.
*/
public void destroy() {
for (Program program : programCache.values()) {
program.destroy();
}
programCache.clear();
vertexSourceCache.clear();
fragmentSourceCache.clear();
lastProgram = null;
lastCombinedKey = Long.MIN_VALUE;
LOGGER.debug("FFP shader cache cleared");
}
public int getProgramCount() {
return programCache.size();
}
public int getVertexVariantCount() {
return vertexSourceCache.size();
}
public int getFragmentVariantCount() {
return fragmentSourceCache.size();
}
}
@@ -0,0 +1,200 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.CompatUniformManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.stacks.Vec3fStack;
import com.gtnewhorizons.angelica.glsm.stacks.Vec4fStack;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
import net.coderbot.iris.gl.blending.BlendModeStorage;
import net.minecraft.launchwrapper.Launch;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL20;
import java.nio.file.Paths;
/**
* FFP shader manager
*/
public class ShaderManager {
private static final Logger LOGGER = LogManager.getLogger("FFPShaderManager");
private static final class Holder { static final ShaderManager INSTANCE = new ShaderManager(); }
private final ShaderCache cache = new ShaderCache();
private final Uniforms uniforms = new Uniforms();
private final Int2IntOpenHashMap vaoVertexFlags = new Int2IntOpenHashMap();
@Getter
private boolean active = false;
private Program currentProgram = null;
private long currentVertexKeyPacked = Long.MIN_VALUE;
private long currentFragmentKeyPacked = Long.MIN_VALUE;
@Getter private static final Vector3f currentNormal = new Vector3f(0.0f, 0.0f, 1.0f);
@Getter private static final Vector4f currentTexCoord = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
@Getter private static final Vec3fStack normalStack = new Vec3fStack(currentNormal);
@Getter private static final Vec4fStack texCoordStack = new Vec4fStack(currentTexCoord);
@Getter private static int normalGeneration;
@Getter private static int texCoordGeneration;
@Getter private boolean enabled = false;
private int currentVertexFlags = VertexFlags.TEXTURE_BIT | VertexFlags.COLOR_BIT | VertexFlags.NORMAL_BIT;
private ShaderManager() {
vaoVertexFlags.defaultReturnValue(-1);
boolean isDev = false;
try {
final Object deobfEnv = Launch.blackboard != null ? Launch.blackboard.get("fml.deobfuscatedEnvironment") : null;
isDev = Boolean.TRUE.equals(deobfEnv);
} catch (Exception ignored) {}
if (isDev || Boolean.parseBoolean(System.getProperty("angelica.ffp.dumpShaders", "false"))) {
cache.setDumpDir(Paths.get("ffp_shaders"));
}
}
public static ShaderManager getInstance() {
return Holder.INSTANCE;
}
public void enable() {
enabled = true;
VertexFormat.registerSetupBufferStateOverride((format, offset) -> {
currentVertexFlags = format.getVertexFlags();
final int boundVao = GLStateManager.getBoundVAO();
if (boundVao != 0) {
vaoVertexFlags.put(boundVao, currentVertexFlags);
}
return false;
});
LOGGER.info("FFP shader emulation enabled");
}
public void disable() {
enabled = false;
}
public void activate() {
active = true;
updateVariant(true, true, true, true);
uploadUniforms();
}
public void deactivate() {
active = false;
currentProgram = null;
currentVertexKeyPacked = Long.MIN_VALUE;
currentFragmentKeyPacked = Long.MIN_VALUE;
}
public void preDraw(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
GLStateManager.flushDeferredVertexAttribs();
BlendModeStorage.flushDeferredBlend();
if (!active) {
if (enabled) {
final int currentProgramId = GLStateManager.getActiveProgram();
if (currentProgramId != 0) {
CompatUniformManager.onUseProgram(currentProgramId);
return;
}
active = true;
} else {
return;
}
}
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
final long fkPacked = FragmentKey.packFromState();
if (vkPacked != currentVertexKeyPacked || fkPacked != currentFragmentKeyPacked) {
currentVertexKeyPacked = vkPacked;
currentFragmentKeyPacked = fkPacked;
currentProgram = cache.getOrCreate(vkPacked, fkPacked);
GL20.glUseProgram(currentProgram.getProgramId());
}
uploadUniforms();
}
public void preDraw(int vertexFlags) {
currentVertexFlags = vertexFlags;
preDraw(
(vertexFlags & VertexFlags.COLOR_BIT) != 0,
(vertexFlags & VertexFlags.NORMAL_BIT) != 0,
(vertexFlags & VertexFlags.TEXTURE_BIT) != 0,
(vertexFlags & VertexFlags.BRIGHTNESS_BIT) != 0
);
}
public void preDraw() {
preDraw(currentVertexFlags);
}
private void updateVariant(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
currentVertexKeyPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
currentFragmentKeyPacked = FragmentKey.packFromState();
currentProgram = cache.getOrCreate(currentVertexKeyPacked, currentFragmentKeyPacked);
GL20.glUseProgram(currentProgram.getProgramId());
}
private void uploadUniforms() {
if (currentProgram != null) {
uniforms.upload(currentProgram);
}
}
public static void setCurrentNormal(float x, float y, float z) {
currentNormal.set(x, y, z);
normalGeneration++;
}
public static void setCurrentTexCoord(float s, float t, float r, float q) {
currentTexCoord.set(s, t, r, q);
texCoordGeneration++;
}
public static void bumpNormalGeneration() { normalGeneration++; }
public static void bumpTexCoordGeneration() { texCoordGeneration++; }
public void enableClientVertexFlag(int flag) { currentVertexFlags |= flag; }
public void disableClientVertexFlag(int flag) { currentVertexFlags &= ~flag; }
public int getCurrentVertexFlags() { return currentVertexFlags; }
public void setCurrentVertexFlags(int flags) { currentVertexFlags = flags; }
public void onBindVertexArray(int vaoId) {
final int flags = vaoVertexFlags.get(vaoId);
if (flags != -1) {
currentVertexFlags = flags;
}
}
public void onDeleteVertexArray(int vaoId) {
vaoVertexFlags.remove(vaoId);
}
public void destroy() {
cache.destroy();
uniforms.destroy();
TessellatorStreamingDrawer.destroy();
com.gtnewhorizons.angelica.glsm.QuadConverter.destroy();
active = false;
currentProgram = null;
}
public String getDebugInfo() {
return String.format("FFP: %d programs (%d vert, %d frag variants)",
cache.getProgramCount(), cache.getVertexVariantCount(), cache.getFragmentVariantCount());
}
}
@@ -0,0 +1,267 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.streaming.OrphanStreamingBuffer;
import com.gtnewhorizons.angelica.glsm.streaming.PersistentStreamingBuffer;
import net.minecraft.client.renderer.Tessellator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Replaces the vanilla Tessellator's FFP client-array draw path with a streaming VBO+VAO approach for GL 3.3 core profile compatibility.
* <p>
* Uses a persistent-mapped ring buffer on GL4.4+ hardware (zero-copy uploads with fence sync),
* falling back to the classic orphan pattern on older hardware or on overflow.
* Maintains two VAO sets (persistent + orphan) per vertex format (up to 16 combinations).
*/
public class TessellatorStreamingDrawer {
private static final Logger LOGGER = LogManager.getLogger("TessellatorStreamingDrawer");
private static final int FORMAT_COUNT = VertexFlags.BITSET_SIZE; // 16
private static PersistentStreamingBuffer persistentBuffer;
private static final OrphanStreamingBuffer[] orphanBuffers = new OrphanStreamingBuffer[FORMAT_COUNT];
private static final int[] persistentVAOs = new int[FORMAT_COUNT];
private static final int[] orphanVAOs = new int[FORMAT_COUNT];
private static ByteBuffer repackBuffer;
private static long repackAddress;
private static int repackCapacity;
private static boolean initialized = false;
static {
// Initial repack buffer: 64KB
repackCapacity = 0x10000;
repackBuffer = memAlloc(repackCapacity);
repackAddress = memAddress0(repackBuffer);
}
private static void init() {
if (initialized) return;
initialized = true;
if (RenderSystem.supportsBufferStorage()) {
try {
persistentBuffer = new PersistentStreamingBuffer();
LOGGER.info("Persistent streaming buffer created ({}MB)", PersistentStreamingBuffer.DEFAULT_CAPACITY / (1024 * 1024));
} catch (Exception e) {
LOGGER.warn("Failed to create persistent streaming buffer, using orphan fallback", e);
persistentBuffer = null;
}
}
}
/**
* Draw the vanilla Tessellator's data using streaming VBO+VAO instead of FFP client arrays.
*/
public static int draw(Tessellator tess) {
if (!tess.isDrawing) {
throw new IllegalStateException("Not tesselating!");
}
tess.isDrawing = false;
final int vertexCount = tess.vertexCount;
if (vertexCount == 0) {
final int result = tess.rawBufferIndex * 4;
tess.reset();
return result;
}
// Determine the optimal vertex format from the tessellator's flags
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
final int vertexSize = format.getVertexSize();
final int requiredBytes = vertexCount * vertexSize;
ensureRepackCapacity(requiredBytes);
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
ensureVAO(flags, format);
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(repackBuffer, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(repackBuffer, vertexSize);
firstVertex = 0;
}
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(tess.drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
// Shrink rawBuffer if oversized
if (tess.rawBufferSize > 0x20000 && tess.rawBufferIndex < (tess.rawBufferSize << 3)) {
tess.rawBufferSize = 0x10000;
tess.rawBuffer = new int[tess.rawBufferSize];
}
final int result = tess.rawBufferIndex * 4;
tess.reset();
return result;
}
/**
* Draw DirectTessellator data via streaming VBO+VAO. Used for live immediate mode emulation.
*/
public static void drawDirect(DirectTessellator dt) {
final VertexFormat format = dt.getVertexFormat();
if (format == null) return;
final int vertexCount = dt.getVertexCount();
if (vertexCount == 0) return;
final int drawMode = dt.getDrawMode();
final int flags = format.getVertexFlags();
final ByteBuffer buffer = dt.getWriteBuffer();
final int vertexSize = format.getVertexSize();
ensureVAO(flags, format);
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(buffer, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(buffer, vertexSize);
firstVertex = 0;
}
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
}
public static String getDebugInfo() {
if (!initialized) return "Stream: not initialized";
int orphanCount = 0;
int orphanBytes = 0;
for (int i = 0; i < FORMAT_COUNT; i++) {
if (orphanBuffers[i] != null) {
orphanCount++;
orphanBytes += orphanBuffers[i].getCapacity();
}
}
if (persistentBuffer != null) {
return String.format("Stream: Persistent %s (%s free) + %d orphan (%s)",
formatBytes(persistentBuffer.getCapacity()), formatBytes(persistentBuffer.getRemaining()),
orphanCount, formatBytes(orphanBytes));
}
return String.format("Stream: Orphan (%d bufs, %s)", orphanCount, formatBytes(orphanBytes));
}
private static String formatBytes(int bytes) {
if (bytes >= 1024 * 1024) return String.format("%dMB", bytes / (1024 * 1024));
if (bytes >= 1024) return String.format("%dKB", bytes / 1024);
return bytes + "B";
}
public static void endFrame() {
if (persistentBuffer != null) {
persistentBuffer.postDraw();
}
}
private static void drawWithQuadConversion(int drawMode, int firstVertex, int vertexCount) {
if (drawMode == GL11.GL_QUADS) {
QuadConverter.drawQuadsAsTriangles(firstVertex, vertexCount);
} else {
GL11.glDrawArrays(drawMode, firstVertex, vertexCount);
}
}
private static void ensureRepackCapacity(int requiredBytes) {
if (requiredBytes <= repackCapacity) return;
int newCapacity = repackCapacity;
while (newCapacity < requiredBytes) {
newCapacity *= 2;
}
memFree(repackBuffer);
repackBuffer = memAlloc(newCapacity);
repackAddress = memAddress0(repackBuffer);
repackCapacity = newCapacity;
}
private static void ensureVAO(int flags, VertexFormat format) {
init();
if (orphanVAOs[flags] == 0) {
orphanBuffers[flags] = new OrphanStreamingBuffer();
orphanVAOs[flags] = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, orphanBuffers[flags].getBufferId());
format.setupBufferState(0L);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
}
if (persistentBuffer != null && persistentVAOs[flags] == 0) {
persistentVAOs[flags] = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, persistentBuffer.getBufferId());
format.setupBufferState(0L);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
}
}
/**
* Clean up all VAOs, streaming buffers, and the repack buffer.
*/
public static void destroy() {
for (int i = 0; i < FORMAT_COUNT; i++) {
if (persistentVAOs[i] != 0) { GLStateManager.glDeleteVertexArrays(persistentVAOs[i]); persistentVAOs[i] = 0; }
if (orphanVAOs[i] != 0) { GLStateManager.glDeleteVertexArrays(orphanVAOs[i]); orphanVAOs[i] = 0; }
if (orphanBuffers[i] != null) { orphanBuffers[i].destroy(); orphanBuffers[i] = null; }
}
if (persistentBuffer != null) {
persistentBuffer.destroy();
persistentBuffer = null;
}
if (repackBuffer != null) {
memFree(repackBuffer);
repackBuffer = null;
repackAddress = 0;
repackCapacity = 0;
}
initialized = false;
}
}
@@ -0,0 +1,469 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import net.minecraft.client.renderer.OpenGlHelper;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Uploads GLSM cached state to the active FFP program's uniforms.
*
* Follows Mesa's prog_statevars.c patterns:
* - Pre-computed derived state (light products, scene color) when material is static
* - Raw light/material values when color material is active
* - Optimized fog params (pre-divided for single MAD in shader)
*/
public class Uniforms {
// Reusable temp buffers for uniform upload
private final FloatBuffer mat4Buf = memAllocFloat(16);
private final FloatBuffer mat3Buf = memAllocFloat(9);
private final FloatBuffer vec4Buf = memAllocFloat(4);
private final FloatBuffer vec3Buf = memAllocFloat(3);
private final FloatBuffer clipPlaneBuf = memAllocFloat(32); // 8 planes * vec4
// Derived matrices (computed on CPU)
private final Matrix4f mvpMatrix = new Matrix4f();
private final Matrix3f normalMatrix = new Matrix3f();
// Temp vectors for light product and normal scale computation
private final Vector4f tempVec4 = new Vector4f();
private final org.joml.Vector3f tempVec3 = new org.joml.Vector3f();
// Pre-computed constants for fog params (Mesa STATE_FOG_PARAMS_OPTIMIZED)
private static final double LN2 = Math.log(2.0);
private static final double SQRT_LN2 = Math.sqrt(LN2);
// Dirty tracking: last-uploaded generation per category + program ID.
// Program change forces full re-upload since uniform locations differ.
private int lastProgramId = -1;
private int lastMvGen = -1;
private int lastProjGen = -1;
private int lastTexMatGen = -1;
private int lastLightingGen = -1;
private int lastFragmentGen = -1;
private int lastColorGen = -1;
private int lastNormalGen = -1;
private int lastTexCoordGen = -1;
private float lastLightmapX = Float.NaN;
private float lastLightmapY = Float.NaN;
private int lastTexGenGen = -1;
private int lastClipPlaneGen = -1;
/**
* Upload all relevant uniforms to the given FFP program based on current GLSM state.
* Uses generation counters to skip categories that haven't changed since last upload.
*/
public void upload(Program program) {
final boolean programChanged = program.getProgramId() != lastProgramId;
lastProgramId = program.getProgramId();
final int mvGen = GLStateManager.mvGeneration;
final int projGen = GLStateManager.projGeneration;
final int texMatGen = GLStateManager.texMatrixGeneration;
final boolean mvChanged = programChanged || mvGen != lastMvGen;
final boolean projChanged = programChanged || projGen != lastProjGen;
final boolean texMatChanged = programChanged || texMatGen != lastTexMatGen;
if (mvChanged || projChanged || texMatChanged) {
uploadMatrices(program, mvChanged, projChanged, texMatChanged);
lastMvGen = mvGen;
lastProjGen = projGen;
lastTexMatGen = texMatGen;
}
if (program.getVertexKey().lightingEnabled()) {
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
uploadLighting(program);
lastLightingGen = litGen;
}
}
// Current color/normal/texcoord — skip if generation unchanged
if (!program.getVertexKey().hasVertexColor()) {
final int colorGen = GLStateManager.colorGeneration;
if (programChanged || colorGen != lastColorGen) {
uploadCurrentColor(program);
lastColorGen = colorGen;
}
}
if (!program.getVertexKey().hasVertexNormal() && program.getVertexKey().lightingEnabled()) {
final int normalGen = ShaderManager.getNormalGeneration();
if (programChanged || normalGen != lastNormalGen) {
uploadCurrentNormal(program);
lastNormalGen = normalGen;
}
}
if (!program.getVertexKey().hasVertexTexCoord() && program.getVertexKey().textureEnabled()) {
final int texGen = ShaderManager.getTexCoordGeneration();
if (programChanged || texGen != lastTexCoordGen) {
uploadCurrentTexCoord(program);
lastTexCoordGen = texGen;
}
}
if (program.getVertexKey().lightmapEnabled() && !program.getVertexKey().hasVertexLightmap()) {
if (programChanged || OpenGlHelper.lastBrightnessX != lastLightmapX || OpenGlHelper.lastBrightnessY != lastLightmapY) {
uploadCurrentLightmapCoord(program);
lastLightmapX = OpenGlHelper.lastBrightnessX;
lastLightmapY = OpenGlHelper.lastBrightnessY;
}
}
if (program.getVertexKey().texGenEnabled()) {
final int tgGen = GLStateManager.texGenGeneration;
if (programChanged || tgGen != lastTexGenGen) {
uploadTexGen(program);
lastTexGenGen = tgGen;
}
}
if (program.getVertexKey().clipPlanesEnabled()) {
final int cpGen = GLStateManager.clipPlaneGeneration;
if (programChanged || cpGen != lastClipPlaneGen) {
uploadClipPlanes(program);
lastClipPlaneGen = cpGen;
}
}
final int fragGen = GLStateManager.fragmentGeneration;
if (programChanged || fragGen != lastFragmentGen) {
uploadFragmentUniforms(program);
lastFragmentGen = fragGen;
}
}
private void uploadMatrices(Program program, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
// ModelView + derived (normal matrix, normal scale)
if (mvChanged) {
if (program.locModelViewMatrix != -1) {
mv.get(mat4Buf);
GL20.glUniformMatrix4(program.locModelViewMatrix, false, mat4Buf);
}
// Normal matrix = inverse transpose of upper-left 3x3 of ModelView
if (program.locNormalMatrix != -1 || program.locNormalScale != -1) {
mv.normal(normalMatrix);
}
if (program.locNormalMatrix != -1) {
normalMatrix.get(mat3Buf);
GL20.glUniformMatrix3(program.locNormalMatrix, false, mat3Buf);
}
// Normal scale (for GL_RESCALE_NORMAL without GL_NORMALIZE)
if (program.locNormalScale != -1) {
// Scale factor = 1/length of first column of normal matrix
final float scale = 1.0f / normalMatrix.getColumn(0, tempVec3).length();
GL20.glUniform1f(program.locNormalScale, scale);
}
}
// Projection
if (projChanged) {
if (program.locProjectionMatrix != -1) {
proj.get(mat4Buf);
GL20.glUniformMatrix4(program.locProjectionMatrix, false, mat4Buf);
}
}
// MVP = Projection * ModelView — needs recompute if either input changed
if ((mvChanged || projChanged) && program.locMVPMatrix != -1) {
proj.mul(mv, mvpMatrix);
mvpMatrix.get(mat4Buf);
GL20.glUniformMatrix4(program.locMVPMatrix, false, mat4Buf);
}
// Texture matrices
if (texMatChanged) {
// Texture matrix unit 0
if (program.locTextureMatrix0 != -1) {
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
texMat.get(mat4Buf);
GL20.glUniformMatrix4(program.locTextureMatrix0, false, mat4Buf);
}
// Texture matrix unit 1 (lightmap)
if (program.locLightmapTextureMatrix != -1) {
final Matrix4f lmTexMat = GLStateManager.getTextures().getTextureUnitMatrix(1);
lmTexMat.get(mat4Buf);
GL20.glUniformMatrix4(program.locLightmapTextureMatrix, false, mat4Buf);
}
}
}
private void uploadLighting(Program program) {
final VertexKey vk = program.getVertexKey();
if (vk.colorMaterialEnabled()) {
uploadLightingColorMaterial(program, vk);
} else {
uploadLightingPreComputed(program, vk);
}
}
/**
* Color material active: upload raw light and material values.
* The shader multiplies them with vertex color at runtime.
*/
private void uploadLightingColorMaterial(Program program, VertexKey vk) {
final MaterialState mat = GLStateManager.getFrontMaterial();
uploadVec4(program.locLightModelAmbient, GLStateManager.getLightModel().ambient);
uploadVec4(program.locMaterialEmission, mat.emission);
uploadVec4(program.locMaterialAmbient, mat.ambient);
uploadVec4(program.locMaterialDiffuse, mat.diffuse);
uploadVec4(program.locMaterialSpecular, mat.specular);
if (program.locMaterialShininess != -1) {
GL20.glUniform1f(program.locMaterialShininess, mat.shininess);
}
if (vk.light0Enabled()) {
final LightState light0 = GLStateManager.getLightDataStates()[0];
uploadVec4(program.locLight0Ambient, light0.ambient);
uploadVec4(program.locLight0Diffuse, light0.diffuse);
uploadVec4(program.locLight0Specular, light0.specular);
uploadVec4(program.locLight0Position, light0.position);
}
if (vk.light1Enabled()) {
final LightState light1 = GLStateManager.getLightDataStates()[1];
uploadVec4(program.locLight1Ambient, light1.ambient);
uploadVec4(program.locLight1Diffuse, light1.diffuse);
uploadVec4(program.locLight1Specular, light1.specular);
uploadVec4(program.locLight1Position, light1.position);
}
}
/**
* No color material: upload pre-computed light products and scene color.
* Fewer shader multiplies since material is static.
*/
private void uploadLightingPreComputed(Program program, VertexKey vk) {
final MaterialState mat = GLStateManager.getFrontMaterial();
// Scene color = emission + ambient * lightModel.ambient
// Alpha = material.diffuse.a
if (program.locSceneColor != -1) {
tempVec4.set(
mat.emission.x + mat.ambient.x * GLStateManager.getLightModel().ambient.x,
mat.emission.y + mat.ambient.y * GLStateManager.getLightModel().ambient.y,
mat.emission.z + mat.ambient.z * GLStateManager.getLightModel().ambient.z,
mat.diffuse.w // alpha from diffuse
);
uploadVec4(program.locSceneColor, tempVec4);
}
if (program.locMaterialShininess != -1) {
GL20.glUniform1f(program.locMaterialShininess, mat.shininess);
}
// Per-light products
if (vk.light0Enabled()) {
final LightState light0 = GLStateManager.getLightDataStates()[0];
uploadVec4(program.locLight0Position, light0.position);
uploadLightProduct(program.locLightProd0Ambient, light0.ambient, mat.ambient);
uploadLightProduct(program.locLightProd0Diffuse, light0.diffuse, mat.diffuse);
uploadLightProduct(program.locLightProd0Specular, light0.specular, mat.specular);
}
if (vk.light1Enabled()) {
final LightState light1 = GLStateManager.getLightDataStates()[1];
uploadVec4(program.locLight1Position, light1.position);
uploadLightProduct(program.locLightProd1Ambient, light1.ambient, mat.ambient);
uploadLightProduct(program.locLightProd1Diffuse, light1.diffuse, mat.diffuse);
uploadLightProduct(program.locLightProd1Specular, light1.specular, mat.specular);
}
}
/**
* Upload pre-multiplied light product: light.X * material.X (RGB only).
*/
private void uploadLightProduct(int loc, Vector4f lightVal, Vector4f materialVal) {
if (loc == -1) return;
vec3Buf.clear();
vec3Buf.put(lightVal.x * materialVal.x);
vec3Buf.put(lightVal.y * materialVal.y);
vec3Buf.put(lightVal.z * materialVal.z);
vec3Buf.flip();
GL20.glUniform3(loc, vec3Buf);
}
private void uploadCurrentColor(Program program) {
if (program.locCurrentColor == -1) return;
// Upload the current color from GLSM
final var color = GLStateManager.getColor();
vec4Buf.clear();
vec4Buf.put(color.getRed());
vec4Buf.put(color.getGreen());
vec4Buf.put(color.getBlue());
vec4Buf.put(color.getAlpha());
vec4Buf.flip();
GL20.glUniform4(program.locCurrentColor, vec4Buf);
}
private void uploadCurrentTexCoord(Program program) {
if (program.locCurrentTexCoord == -1) return;
final var tc = ShaderManager.getCurrentTexCoord();
uploadVec4(program.locCurrentTexCoord, tc);
}
private void uploadCurrentLightmapCoord(Program program) {
if (program.locCurrentLightmapCoord == -1) return;
GL20.glUniform2f(program.locCurrentLightmapCoord,
OpenGlHelper.lastBrightnessX, OpenGlHelper.lastBrightnessY);
}
private void uploadCurrentNormal(Program program) {
if (program.locCurrentNormal == -1) return;
// Upload current normal from FFPShaderManager's tracked normal
final var normal = ShaderManager.getCurrentNormal();
vec3Buf.clear();
vec3Buf.put(normal.x);
vec3Buf.put(normal.y);
vec3Buf.put(normal.z);
vec3Buf.flip();
GL20.glUniform3(program.locCurrentNormal, vec3Buf);
}
private void uploadTexGen(Program program) {
final VertexKey vk = program.getVertexKey();
final TexGenState tg = GLStateManager.getTextures().getTexGenState(0);
if (vk.texGenModeS() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneS, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_S));
} else if (vk.texGenModeS() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneS, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_S));
}
if (vk.texGenModeT() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneT, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_T));
} else if (vk.texGenModeT() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneT, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_T));
}
if (vk.texGenModeR() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneR, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_R));
} else if (vk.texGenModeR() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneR, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_R));
}
if (vk.texGenModeQ() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneQ, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_Q));
} else if (vk.texGenModeQ() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneQ, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_Q));
}
}
private void uploadPlane(int loc, float[] plane) {
if (loc == -1) return;
vec4Buf.clear();
vec4Buf.put(plane[0]);
vec4Buf.put(plane[1]);
vec4Buf.put(plane[2]);
vec4Buf.put(plane[3]);
vec4Buf.flip();
GL20.glUniform4(loc, vec4Buf);
}
private void uploadClipPlanes(Program program) {
if (program.locClipPlanes == -1) return;
final ClipPlaneState cps = GLStateManager.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
clipPlaneBuf.flip();
GL20.glUniform4(program.locClipPlanes, clipPlaneBuf);
}
private void uploadFragmentUniforms(Program program) {
final FragmentKey fk = program.getFragmentKey();
// Alpha test reference
if (fk.alphaTestEnabled() && program.locAlphaRef != -1) {
GL20.glUniform1f(program.locAlphaRef, GLStateManager.getAlphaState().getReference());
}
// Tex env color (only used by GL_BLEND mode)
if (program.locTexEnvColor != -1) {
final var envColor = GLStateManager.getTexEnvColor();
vec4Buf.clear();
vec4Buf.put(envColor.getRed());
vec4Buf.put(envColor.getGreen());
vec4Buf.put(envColor.getBlue());
vec4Buf.put(envColor.getAlpha());
vec4Buf.flip();
GL20.glUniform4(program.locTexEnvColor, vec4Buf);
}
// Fog
if (fk.fogMode() != FragmentKey.FOG_NONE) {
uploadFog(program);
}
}
/**
* Upload optimized fog params (following Mesa's STATE_FOG_PARAMS_OPTIMIZED):
*/
private void uploadFog(Program program) {
final FogState fog = GLStateManager.getFogState();
if (program.locFogParams != -1) {
final float start = fog.getStart();
final float end = fog.getEnd();
final float density = fog.getDensity();
final float range = end - start;
vec4Buf.clear();
vec4Buf.put(range != 0.0f ? -1.0f / range : 0.0f); // [0]: -1/(end-start)
vec4Buf.put(range != 0.0f ? end / range : 1.0f); // [1]: end/(end-start)
vec4Buf.put((float)(density / LN2)); // [2]: density/ln(2)
vec4Buf.put((float)(density / SQRT_LN2)); // [3]: density/sqrt(ln(2))
vec4Buf.flip();
GL20.glUniform4(program.locFogParams, vec4Buf);
}
if (program.locFogColor != -1) {
vec4Buf.clear();
vec4Buf.put((float) fog.getFogColor().x);
vec4Buf.put((float) fog.getFogColor().y);
vec4Buf.put((float) fog.getFogColor().z);
vec4Buf.put(fog.getFogAlpha());
vec4Buf.flip();
GL20.glUniform4(program.locFogColor, vec4Buf);
}
}
private void uploadVec4(int loc, Vector4f v) {
if (loc == -1) return;
vec4Buf.clear();
vec4Buf.put(v.x);
vec4Buf.put(v.y);
vec4Buf.put(v.z);
vec4Buf.put(v.w);
vec4Buf.flip();
GL20.glUniform4(loc, vec4Buf);
}
public void destroy() {
memFree(mat4Buf);
memFree(mat3Buf);
memFree(vec4Buf);
memFree(vec3Buf);
memFree(clipPlaneBuf);
}
}
@@ -0,0 +1,257 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
/**
* Compact key capturing the vertex shader permutation axes for FFP emulation.
* Packed into a single long for fast hash/compare.
*/
public final class VertexKey {
private final long packed;
private static final int BIT_LIGHTING = 0;
private static final int BIT_LIGHT0 = 1;
private static final int BIT_LIGHT1 = 2;
private static final int BIT_LIGHT0_DIR = 3;
private static final int BIT_LIGHT1_DIR = 4;
private static final int BIT_COLOR_MATERIAL = 5;
private static final int BIT_SEPARATE_SPECULAR = 6;
private static final int BIT_FOG = 7;
private static final int BIT_FOG_DIST_MODE = 8;
private static final int BIT_NORMALIZE = 10;
private static final int BIT_RESCALE_NORMAL = 11;
private static final int BIT_TEXTURE = 12;
private static final int BIT_LIGHTMAP = 13;
private static final int BIT_TEX_MATRIX = 14;
private static final int BIT_HAS_VERTEX_COLOR = 15;
private static final int BIT_HAS_VERTEX_NORMAL = 16;
private static final int BIT_HAS_VERTEX_TEX = 17;
private static final int BIT_HAS_VERTEX_LIGHTMAP = 18;
private static final int BIT_COLOR_MAT_MODE = 19;
// bits 19-21: colorMaterialMode (3 bits)
// TexGen modes: 3 bits each (0=NONE, 1=OBJ_LINEAR, 2=EYE_LINEAR)
private static final int BIT_TEXGEN_S = 22;
private static final int BIT_TEXGEN_T = 25;
private static final int BIT_TEXGEN_R = 28;
private static final int BIT_TEXGEN_Q = 31;
// bits 31-33: texGenModeQ (3 bits)
private static final int BIT_CLIP_PLANES = 34;
public static final int TG_NONE = 0;
public static final int TG_OBJ_LINEAR = 1;
public static final int TG_EYE_LINEAR = 2;
public static final int CM_AMBIENT = 0;
public static final int CM_DIFFUSE = 1;
public static final int CM_SPECULAR = 2;
public static final int CM_EMISSION = 3;
public static final int CM_AMBIENT_AND_DIFFUSE = 4;
private VertexKey(long packed) {
this.packed = packed;
}
public long pack() { return packed; }
public boolean lightingEnabled() { return bit(BIT_LIGHTING); }
public boolean light0Enabled() { return bit(BIT_LIGHT0); }
public boolean light1Enabled() { return bit(BIT_LIGHT1); }
public boolean light0Directional() { return bit(BIT_LIGHT0_DIR); }
public boolean light1Directional() { return bit(BIT_LIGHT1_DIR); }
public boolean colorMaterialEnabled() { return bit(BIT_COLOR_MATERIAL); }
public boolean separateSpecular() { return bit(BIT_SEPARATE_SPECULAR); }
public boolean fogEnabled() { return bit(BIT_FOG); }
public int fogDistanceMode() { return (int)((packed >> BIT_FOG_DIST_MODE) & 0x3); }
public boolean normalizeEnabled() { return bit(BIT_NORMALIZE); }
public boolean rescaleNormalsEnabled() { return bit(BIT_RESCALE_NORMAL); }
public boolean textureEnabled() { return bit(BIT_TEXTURE); }
public boolean lightmapEnabled() { return bit(BIT_LIGHTMAP); }
public boolean textureMatrixEnabled() { return bit(BIT_TEX_MATRIX); }
public boolean hasVertexColor() { return bit(BIT_HAS_VERTEX_COLOR); }
public boolean hasVertexNormal() { return bit(BIT_HAS_VERTEX_NORMAL); }
public boolean hasVertexTexCoord() { return bit(BIT_HAS_VERTEX_TEX); }
public boolean hasVertexLightmap() { return bit(BIT_HAS_VERTEX_LIGHTMAP); }
/** Color material mode (CM_AMBIENT, CM_DIFFUSE, CM_SPECULAR, CM_EMISSION, CM_AMBIENT_AND_DIFFUSE). Only meaningful when colorMaterialEnabled(). */
public int colorMaterialMode() { return (int)((packed >> BIT_COLOR_MAT_MODE) & 0x7); }
/** TexGen mode for S coordinate (TG_NONE, TG_OBJ_LINEAR, TG_EYE_LINEAR). */
public int texGenModeS() { return (int)((packed >> BIT_TEXGEN_S) & 0x7); }
/** TexGen mode for T coordinate. */
public int texGenModeT() { return (int)((packed >> BIT_TEXGEN_T) & 0x7); }
/** TexGen mode for R coordinate. */
public int texGenModeR() { return (int)((packed >> BIT_TEXGEN_R) & 0x7); }
/** TexGen mode for Q coordinate. */
public int texGenModeQ() { return (int)((packed >> BIT_TEXGEN_Q) & 0x7); }
/** Whether any texgen coordinate is enabled. */
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
/** Whether any clip plane is enabled. */
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
/** Whether vertex color replaces material ambient in this variant. */
public boolean cmReplacesAmbient() { final int m = colorMaterialMode(); return m == CM_AMBIENT || m == CM_AMBIENT_AND_DIFFUSE; }
/** Whether vertex color replaces material diffuse in this variant. */
public boolean cmReplacesDiffuse() { final int m = colorMaterialMode(); return m == CM_DIFFUSE || m == CM_AMBIENT_AND_DIFFUSE; }
/** Whether vertex color replaces material specular in this variant. */
public boolean cmReplacesSpecular() { return colorMaterialMode() == CM_SPECULAR; }
/** Whether vertex color replaces material emission in this variant. */
public boolean cmReplacesEmission() { return colorMaterialMode() == CM_EMISSION; }
private boolean bit(int pos) { return ((packed >> pos) & 1) != 0; }
static int encodeTexGenMode(int glMode) {
return switch (glMode) {
case GL11.GL_OBJECT_LINEAR -> TG_OBJ_LINEAR;
case GL11.GL_EYE_LINEAR -> TG_EYE_LINEAR;
default -> TG_NONE;
};
}
static int encodeColorMaterialMode(int glMode) {
return switch (glMode) {
case GL11.GL_AMBIENT -> CM_AMBIENT;
case GL11.GL_DIFFUSE -> CM_DIFFUSE;
case GL11.GL_SPECULAR -> CM_SPECULAR;
case GL11.GL_EMISSION -> CM_EMISSION;
case GL11.GL_AMBIENT_AND_DIFFUSE -> CM_AMBIENT_AND_DIFFUSE;
default -> CM_AMBIENT_AND_DIFFUSE;
};
}
public static long packFromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
long bits = 0;
final boolean lighting = GLStateManager.getLightingState().isEnabled();
if (lighting) {
bits |= (1L << BIT_LIGHTING);
if (GLStateManager.getLightStates()[0].isEnabled()) {
bits |= (1L << BIT_LIGHT0);
// position.w == 0 means directional
if (GLStateManager.getLightDataStates()[0].position.w == 0.0f) {
bits |= (1L << BIT_LIGHT0_DIR);
}
}
if (GLStateManager.getLightStates()[1].isEnabled()) {
bits |= (1L << BIT_LIGHT1);
if (GLStateManager.getLightDataStates()[1].position.w == 0.0f) {
bits |= (1L << BIT_LIGHT1_DIR);
}
}
if (GLStateManager.getColorMaterial().isEnabled()) {
bits |= (1L << BIT_COLOR_MATERIAL);
final int cmMode = encodeColorMaterialMode(GLStateManager.getColorMaterialParameter().getValue());
bits |= ((long) cmMode & 0x7) << BIT_COLOR_MAT_MODE;
}
if (GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
bits |= (1L << BIT_SEPARATE_SPECULAR);
}
}
if (GLStateManager.getFogMode().isEnabled()) {
bits |= (1L << BIT_FOG);
final int fogDistMode = GLStateManager.getFogState().getFogDistanceMode();
bits |= ((long) fogDistMode & 0x3) << BIT_FOG_DIST_MODE;
}
if (GLStateManager.getNormalizeState().isEnabled()) {
bits |= (1L << BIT_NORMALIZE);
}
if (GLStateManager.getRescaleNormalState().isEnabled()) {
bits |= (1L << BIT_RESCALE_NORMAL);
}
// Texture unit 0 (base texture)
if (GLStateManager.getTextures().getTextureUnitStates(0).isEnabled()) {
bits |= (1L << BIT_TEXTURE);
}
// Texture unit 1 (lightmap)
if (GLStateManager.getTextures().getTextureUnitStates(1).isEnabled()) {
bits |= (1L << BIT_LIGHTMAP);
}
// Check if texture matrix unit 0 is non-identity
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
if (!DisplayListManager.isIdentity(texMat)) {
bits |= (1L << BIT_TEX_MATRIX);
}
// TexGen (unit 0 only) — encode per-coordinate mode if enabled
final var texUnit = GLStateManager.getTextures();
final TexGenState tg = texUnit.getTexGenState(0);
boolean anyTexGen = false;
if (texUnit.getTexGenSStates(0).isEnabled() && tg.getMode(GL11.GL_S) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_S));
bits |= ((long) m & 0x7) << BIT_TEXGEN_S;
if (m != TG_NONE) anyTexGen = true;
}
if (texUnit.getTexGenTStates(0).isEnabled() && tg.getMode(GL11.GL_T) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_T));
bits |= ((long) m & 0x7) << BIT_TEXGEN_T;
if (m != TG_NONE) anyTexGen = true;
}
if (texUnit.getTexGenRStates(0).isEnabled() && tg.getMode(GL11.GL_R) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_R));
bits |= ((long) m & 0x7) << BIT_TEXGEN_R;
if (m != TG_NONE) anyTexGen = true;
}
if (texUnit.getTexGenQStates(0).isEnabled() && tg.getMode(GL11.GL_Q) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_Q));
bits |= ((long) m & 0x7) << BIT_TEXGEN_Q;
if (m != TG_NONE) anyTexGen = true;
}
// Force texture matrix on when texgen is active (end portal pattern always uses it)
if (anyTexGen) {
bits |= (1L << BIT_TEX_MATRIX);
}
// Vertex format flags
if (hasColor) bits |= (1L << BIT_HAS_VERTEX_COLOR);
if (hasNormal) bits |= (1L << BIT_HAS_VERTEX_NORMAL);
if (hasTexCoord) bits |= (1L << BIT_HAS_VERTEX_TEX);
if (hasLightmap) bits |= (1L << BIT_HAS_VERTEX_LIGHTMAP);
// Clip planes
if (GLStateManager.anyClipPlaneEnabled()) {
bits |= (1L << BIT_CLIP_PLANES);
}
return bits;
}
public static VertexKey fromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
return new VertexKey(packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap));
}
static VertexKey fromPacked(long packed) {
return new VertexKey(packed);
}
@Override
public int hashCode() {
return Long.hashCode(packed);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof VertexKey other)) return false;
return packed == other.packed;
}
@Override
public String toString() {
return String.format("FFPVertexKey[0x%09X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%b lm=%b col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b]",
packed, lightingEnabled(), light0Enabled(), light1Enabled(),
colorMaterialEnabled(), fogEnabled(), textureEnabled(), lightmapEnabled(),
hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(),
texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled());
}
}
@@ -0,0 +1,408 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
/**
* Generates GLSL 330 core vertex shaders for FFP emulation.
* Follows Mesa's ffvertex_prog.c build order.
*
* Attribute locations are sourced from {@link VertexFormatElement.Usage} to stay in sync with GTNHLib's vertex format setup.
*/
public final class VertexShaderGenerator {
private VertexShaderGenerator() {}
public static String generate(VertexKey key) {
final StringBuilder sb = new StringBuilder(2048);
sb.append("#version 330 core\n\n");
emitAttributes(sb, key);
emitUniforms(sb, key);
emitOutputs(sb, key);
sb.append("void main() {\n");
emitPositionTransform(sb, key);
if (key.lightingEnabled()) {
emitNormalTransform(sb, key);
emitLighting(sb, key);
} else {
emitColorPassthrough(sb, key);
}
emitTexCoordPassthrough(sb, key);
if (key.fogEnabled()) {
emitFogDistance(sb, key);
}
if (key.clipPlanesEnabled()) {
emitClipDistances(sb);
}
sb.append("}\n");
return sb.toString();
}
private static void emitAttributes(StringBuilder sb, VertexKey key) {
sb.append("// Vertex attributes\n");
sb.append("layout(location = ").append(VertexFormatElement.Usage.POSITION.getAttributeLocation()).append(") in vec3 a_Position;\n");
if (key.hasVertexColor()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.COLOR.getAttributeLocation()).append(") in vec4 a_Color;\n");
}
if (key.hasVertexTexCoord()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord0;\n");
}
if (key.hasVertexLightmap()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.SECONDARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord1;\n");
}
if (key.hasVertexNormal()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.NORMAL.getAttributeLocation()).append(") in vec3 a_Normal;\n");
}
sb.append('\n');
}
private static void emitUniforms(StringBuilder sb, VertexKey key) {
sb.append("// Matrices\n");
sb.append("uniform mat4 u_ModelViewMatrix;\n");
sb.append("uniform mat4 u_ProjectionMatrix;\n");
sb.append("uniform mat4 u_MVPMatrix;\n");
if (key.lightingEnabled()) {
sb.append("uniform mat3 u_NormalMatrix;\n");
}
if (key.textureMatrixEnabled()) {
sb.append("uniform mat4 u_TextureMatrix0;\n");
}
// TexGen plane uniforms
if (key.texGenModeS() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneS;\n");
if (key.texGenModeS() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneS;\n");
if (key.texGenModeT() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneT;\n");
if (key.texGenModeT() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneT;\n");
if (key.texGenModeR() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneR;\n");
if (key.texGenModeR() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneR;\n");
if (key.texGenModeQ() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneQ;\n");
if (key.texGenModeQ() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneQ;\n");
if (!key.hasVertexNormal() && key.lightingEnabled()) {
sb.append("uniform vec3 u_CurrentNormal;\n");
}
if (!key.hasVertexColor()) {
sb.append("uniform vec4 u_CurrentColor;\n");
}
if (!key.hasVertexTexCoord() && key.textureEnabled()) {
sb.append("uniform vec4 u_CurrentTexCoord;\n");
}
if (key.lightmapEnabled() && !key.hasVertexLightmap()) {
sb.append("uniform vec2 u_CurrentLightmapCoord;\n");
}
if (key.lightmapEnabled()) {
sb.append("uniform mat4 u_LightmapTextureMatrix;\n");
}
if (key.lightingEnabled()) {
emitLightingUniforms(sb, key);
}
if (key.lightingEnabled() && (key.normalizeEnabled() || key.rescaleNormalsEnabled())) {
// normalize is done in shader; rescale uses normalScale
if (key.rescaleNormalsEnabled() && !key.normalizeEnabled()) {
sb.append("uniform float u_NormalScale;\n");
}
}
if (key.clipPlanesEnabled()) {
sb.append("uniform vec4 u_ClipPlane[8];\n");
}
sb.append('\n');
}
private static void emitLightingUniforms(StringBuilder sb, VertexKey key) {
sb.append("\n// Lighting\n");
if (key.colorMaterialEnabled()) {
// When color material is active, we can't pre-compute light products so we upload raw light and material values
sb.append("uniform vec4 u_LightModelAmbient;\n");
sb.append("uniform vec4 u_MaterialEmission;\n");
sb.append("uniform vec4 u_MaterialAmbient;\n");
sb.append("uniform vec4 u_MaterialDiffuse;\n");
sb.append("uniform vec4 u_MaterialSpecular;\n");
sb.append("uniform float u_MaterialShininess;\n");
if (key.light0Enabled()) {
sb.append("uniform vec4 u_Light0Ambient;\n");
sb.append("uniform vec4 u_Light0Diffuse;\n");
sb.append("uniform vec4 u_Light0Specular;\n");
sb.append("uniform vec4 u_Light0Position;\n");
}
if (key.light1Enabled()) {
sb.append("uniform vec4 u_Light1Ambient;\n");
sb.append("uniform vec4 u_Light1Diffuse;\n");
sb.append("uniform vec4 u_Light1Specular;\n");
sb.append("uniform vec4 u_Light1Position;\n");
}
} else {
// Pre-computed: sceneColor + lightProducts
sb.append("uniform vec4 u_SceneColor;\n");
if (key.light0Enabled()) {
sb.append("uniform vec4 u_Light0Position;\n");
sb.append("uniform vec3 u_LightProd0Ambient;\n");
sb.append("uniform vec3 u_LightProd0Diffuse;\n");
sb.append("uniform vec3 u_LightProd0Specular;\n");
}
if (key.light1Enabled()) {
sb.append("uniform vec4 u_Light1Position;\n");
sb.append("uniform vec3 u_LightProd1Ambient;\n");
sb.append("uniform vec3 u_LightProd1Diffuse;\n");
sb.append("uniform vec3 u_LightProd1Specular;\n");
}
sb.append("uniform float u_MaterialShininess;\n");
}
}
private static void emitOutputs(StringBuilder sb, VertexKey key) {
sb.append("// Outputs\n");
sb.append("out vec4 v_Color;\n");
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append("out vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("out vec4 v_TexCoord1;\n");
}
if (key.fogEnabled()) {
sb.append("out float v_FogCoord;\n");
}
sb.append('\n');
}
private static void emitPositionTransform(StringBuilder sb, VertexKey key) {
sb.append(" // Position transform\n");
sb.append(" vec4 pos4 = vec4(a_Position, 1.0);\n");
sb.append(" gl_Position = u_MVPMatrix * pos4;\n");
// Eye position needed for lighting, fog, EYE_LINEAR texgen, and clip planes
if (key.lightingEnabled() || key.fogEnabled() || texGenNeedsEyePos(key) || key.clipPlanesEnabled()) {
sb.append(" vec4 eyePos = u_ModelViewMatrix * pos4;\n");
}
sb.append('\n');
}
private static void emitNormalTransform(StringBuilder sb, VertexKey key) {
sb.append(" // Normal transform\n");
if (key.hasVertexNormal()) {
sb.append(" vec3 normal = u_NormalMatrix * a_Normal;\n");
} else {
sb.append(" vec3 normal = u_NormalMatrix * u_CurrentNormal;\n");
}
if (key.normalizeEnabled()) {
sb.append(" normal = normalize(normal);\n");
} else if (key.rescaleNormalsEnabled()) {
sb.append(" normal *= u_NormalScale;\n");
}
sb.append('\n');
}
private static void emitLighting(StringBuilder sb, VertexKey key) {
sb.append(" // Gouraud lighting\n");
if (key.colorMaterialEnabled()) {
// Color material: vertex color replaces specific material properties per glColorMaterial mode (Mesa ffvertex_prog.c get_material() bitmask approach)
sb.append(" vec4 matColor = ");
sb.append(key.hasVertexColor() ? "a_Color" : "u_CurrentColor");
sb.append(";\n");
// Emission source: vertex color for CM_EMISSION, else material uniform
final String emSrc = key.cmReplacesEmission() ? "matColor.rgb" : "u_MaterialEmission.rgb";
// Ambient source for scene color: vertex color for CM_AMBIENT / CM_AMBIENT_AND_DIFFUSE
final String amSrc = key.cmReplacesAmbient() ? "matColor.rgb" : "u_MaterialAmbient.rgb";
sb.append(" vec3 sceneColor = ").append(emSrc).append(" + ").append(amSrc).append(" * u_LightModelAmbient.rgb;\n");
// Alpha from diffuse source (per GL spec)
sb.append(" float outAlpha = ").append(key.cmReplacesDiffuse() ? "matColor.a" : "u_MaterialDiffuse.a").append(";\n");
} else {
// Pre-computed scene color
sb.append(" vec3 sceneColor = u_SceneColor.rgb;\n");
sb.append(" float outAlpha = u_SceneColor.a;\n");
}
sb.append(" vec3 color0 = sceneColor;\n");
if (key.separateSpecular()) {
sb.append(" vec3 specAccum = vec3(0.0);\n");
}
// Per-light accumulation
if (key.light0Enabled()) {
emitLightContribution(sb, key, 0);
}
if (key.light1Enabled()) {
emitLightContribution(sb, key, 1);
}
sb.append(" v_Color = vec4(clamp(color0, 0.0, 1.0), outAlpha);\n");
if (key.separateSpecular()) {
sb.append(" v_SpecularColor = clamp(specAccum, 0.0, 1.0);\n");
}
sb.append('\n');
}
private static void emitLightContribution(StringBuilder sb, VertexKey key, int lightIndex) {
final String li = String.valueOf(lightIndex);
final boolean directional = (lightIndex == 0) ? key.light0Directional() : key.light1Directional();
final String posUniform = "u_Light" + li + "Position";
sb.append(" { // Light ").append(li).append('\n');
// Light vector
if (directional) {
// Directional: position is already normalized direction
sb.append(" vec3 VPpli = normalize(").append(posUniform).append(".xyz);\n");
} else {
// Positional light
sb.append(" vec3 VPpli = ").append(posUniform).append(".xyz - eyePos.xyz;\n");
sb.append(" VPpli = normalize(VPpli);\n");
}
// Dot products
sb.append(" float NdotVP = max(dot(normal, VPpli), 0.0);\n");
// Half vector for specular (infinite viewer: H = normalize(L + (0,0,1)))
sb.append(" vec3 halfVec = normalize(VPpli + vec3(0.0, 0.0, 1.0));\n");
sb.append(" float NdotHV = max(dot(normal, halfVec), 0.0);\n");
// Specular power
sb.append(" float spec = (NdotVP > 0.0 && u_MaterialShininess > 0.0) ? pow(NdotHV, u_MaterialShininess) : 0.0;\n");
// Accumulate — per-property source depends on color material mode
if (key.colorMaterialEnabled()) {
final String ambU = "u_Light" + li + "Ambient";
final String difU = "u_Light" + li + "Diffuse";
final String speU = "u_Light" + li + "Specular";
// Ambient product: light.ambient * (vertex color or material)
final String ambMat = key.cmReplacesAmbient() ? "matColor.rgb" : "u_MaterialAmbient.rgb";
sb.append(" vec3 ambient = ").append(ambU).append(".rgb * ").append(ambMat).append(";\n");
// Diffuse product
final String difMat = key.cmReplacesDiffuse() ? "matColor.rgb" : "u_MaterialDiffuse.rgb";
sb.append(" vec3 diffuse = ").append(difU).append(".rgb * ").append(difMat).append(";\n");
// Specular product
final String speMat = key.cmReplacesSpecular() ? "matColor.rgb" : "u_MaterialSpecular.rgb";
sb.append(" vec3 specular = ").append(speU).append(".rgb * ").append(speMat).append(";\n");
} else {
sb.append(" vec3 ambient = u_LightProd").append(li).append("Ambient;\n");
sb.append(" vec3 diffuse = u_LightProd").append(li).append("Diffuse;\n");
sb.append(" vec3 specular = u_LightProd").append(li).append("Specular;\n");
}
sb.append(" color0 += ambient + NdotVP * diffuse;\n");
if (key.separateSpecular()) {
sb.append(" specAccum += spec * specular;\n");
} else {
sb.append(" color0 += spec * specular;\n");
}
sb.append(" }\n");
}
private static void emitColorPassthrough(StringBuilder sb, VertexKey key) {
sb.append(" // Color passthrough (no lighting)\n");
if (key.hasVertexColor()) {
sb.append(" v_Color = a_Color;\n");
} else {
sb.append(" v_Color = u_CurrentColor;\n");
}
sb.append('\n');
}
private static void emitTexCoordPassthrough(StringBuilder sb, VertexKey key) {
if (key.texGenEnabled()) {
emitTexGenCoordGeneration(sb, key);
} else if (key.textureEnabled() || key.hasVertexTexCoord()) {
sb.append(" // Texture coordinates\n");
if (key.hasVertexTexCoord()) {
if (key.textureMatrixEnabled()) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * vec4(a_TexCoord0, 0.0, 1.0);\n");
} else {
sb.append(" v_TexCoord0 = vec4(a_TexCoord0, 0.0, 1.0);\n");
}
} else {
if (key.textureMatrixEnabled()) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * u_CurrentTexCoord;\n");
} else {
sb.append(" v_TexCoord0 = u_CurrentTexCoord;\n");
}
}
}
if (key.lightmapEnabled()) {
if (key.hasVertexLightmap()) {
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(a_TexCoord1, 0.0, 1.0);\n");
} else {
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(u_CurrentLightmapCoord, 0.0, 1.0);\n");
}
}
}
/**
* Emit per-coordinate texgen code following Mesa's ffvertex_prog.c build order.
* OBJ_LINEAR: dot(pos4, objPlane), EYE_LINEAR: dot(eyePos, eyePlane).
* Coordinates without texgen use vertex attrib or default (0,0,0,1).
*/
private static void emitTexGenCoordGeneration(StringBuilder sb, VertexKey key) {
sb.append(" // TexGen coordinate generation\n");
sb.append(" vec4 texGenCoord = vec4(0.0, 0.0, 0.0, 1.0);\n");
emitTexGenComponent(sb, key.texGenModeS(), "s", "S", key);
emitTexGenComponent(sb, key.texGenModeT(), "t", "T", key);
emitTexGenComponent(sb, key.texGenModeR(), "r", "R", key);
emitTexGenComponent(sb, key.texGenModeQ(), "q", "Q", key);
// Always apply texture matrix when texgen is active (forced on in VertexKey)
sb.append(" v_TexCoord0 = u_TextureMatrix0 * texGenCoord;\n");
}
private static void emitTexGenComponent(StringBuilder sb, int mode, String swizzle, String coordName, VertexKey key) {
switch (mode) {
case VertexKey.TG_OBJ_LINEAR ->
sb.append(" texGenCoord.").append(swizzle).append(" = dot(pos4, u_TexGenObjPlane").append(coordName).append(");\n");
case VertexKey.TG_EYE_LINEAR ->
sb.append(" texGenCoord.").append(swizzle).append(" = dot(eyePos, u_TexGenEyePlane").append(coordName).append(");\n");
// TG_NONE: keep default (0 for s/t/r, 1 for q) — already set in texGenCoord init
}
}
private static boolean texGenNeedsEyePos(VertexKey key) {
return key.texGenModeS() == VertexKey.TG_EYE_LINEAR
|| key.texGenModeT() == VertexKey.TG_EYE_LINEAR
|| key.texGenModeR() == VertexKey.TG_EYE_LINEAR
|| key.texGenModeQ() == VertexKey.TG_EYE_LINEAR;
}
private static void emitFogDistance(StringBuilder sb, VertexKey key) {
sb.append(" // Fog distance\n");
// Mesa ffvertex_prog.c build_fog(): FDM_EYE_RADIAL/EYE_PLANE/EYE_PLANE_ABS
final int fogDistMode = key.fogDistanceMode();
switch (fogDistMode) {
case 0 -> // FDM_EYE_RADIAL: Euclidean distance
sb.append(" v_FogCoord = length(eyePos.xyz);\n");
case 1 -> // FDM_EYE_PLANE: raw Z (can be negative, clamped by fog factor)
sb.append(" v_FogCoord = eyePos.z;\n");
case 2 -> // FDM_EYE_PLANE_ABS: absolute Z
sb.append(" v_FogCoord = abs(eyePos.z);\n");
default ->
sb.append(" v_FogCoord = abs(eyePos.z);\n");
}
}
private static void emitClipDistances(StringBuilder sb) {
sb.append(" // Clip distances\n");
for (int i = 0; i < 8; i++) {
sb.append(" gl_ClipDistance[").append(i).append("] = dot(u_ClipPlane[").append(i).append("], eyePos);\n");
}
}
}
@@ -1,11 +1,10 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil;
import org.joml.Matrix4f;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
@@ -13,54 +12,53 @@ import java.util.List;
* Matrix-as-Data Architecture: Stores untransformed vertices + captured matrix.
* The matrix will be applied at runtime, not baked into vertices.
*/
public class AccumulatedDraw {
public final List<ModelQuadViewMutable> quads;
public final Matrix4f transform;
public final CapturingTessellator.Flags flags;
public final class AccumulatedDraw {
public final VertexFormat format;
public final List<ByteBuffer> drawBuffers;
public final int drawMode;
public final int commandIndex; // Position in command list for state tracking
public final int matrixGeneration; // For batching: same generation = same transform context
public final int stateGeneration; // Same value = no state commands between draws, can merge
public final int stateGeneration; // Position in command list for state tracking
public RestoreData restoreData;
/**
* Last vertex restoration data for immediate mode draws.
* Null for tessellator draws (no restoration needed).
*/
public final RestoreData restoreData;
public AccumulatedDraw(DirectTessellator tessellator, int commandIndex, int stateGeneration, boolean copyLast) {
this.format = tessellator.getVertexFormat();
this.drawMode = tessellator.drawMode;
this.drawBuffers = new ArrayList<>();
this.drawBuffers.add(tessellator.allocateBufferCopy());
this.commandIndex = commandIndex;
this.stateGeneration = stateGeneration;
if (copyLast) {
this.restoreData = new RestoreData(tessellator);
}
}
public void mergeDraw(DirectTessellator tessellator, boolean copyLast) {
this.drawBuffers.add(tessellator.allocateBufferCopy());
if (copyLast) {
this.restoreData = new RestoreData(tessellator);
}
}
/**
* Holds last vertex attribute values for GL state restoration after VBO draw.
* Used to sync GLSM cache with actual GL state after VBO rendering.
*/
@Desugar
public record RestoreData(
float lastColorR, float lastColorG, float lastColorB, float lastColorA,
float lastNormalX, float lastNormalY, float lastNormalZ,
float lastTexCoordS, float lastTexCoordT) {}
public static final class RestoreData {
public final int lastColor;
public final int lastNormal;
public final float lastTexCoordU;
public final float lastTexCoordV;
public final int flags;
public AccumulatedDraw(List<ModelQuadViewMutable> quads, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex, int matrixGeneration) {
this(quads, transform, flags, commandIndex, matrixGeneration, 0, null);
}
public AccumulatedDraw(List<ModelQuadViewMutable> quads, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex, int matrixGeneration, int stateGeneration) {
this(quads, transform, flags, commandIndex, matrixGeneration, stateGeneration, null);
}
public AccumulatedDraw(List<ModelQuadViewMutable> quads, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex, int matrixGeneration, int stateGeneration, RestoreData restoreData) {
// Deep copy quads - pooled quads are reused after callback
this.quads = ModelQuadUtil.deepCopyQuads(quads);
this.transform = new Matrix4f(transform); // Snapshot for runtime application
this.flags = flags;
this.commandIndex = commandIndex;
this.matrixGeneration = matrixGeneration;
this.stateGeneration = stateGeneration;
this.restoreData = restoreData;
}
@Override
public String toString() {
return String.format("AccumulatedDraw[quads=%d, cmdIdx=%d, matrixGen=%d, stateGen=%d, flags=C%dT%dL%dN%d, restore=%s]",
quads.size(), commandIndex, matrixGeneration, stateGeneration, flags.hasColor ? 1 : 0, flags.hasTexture ? 1 : 0, flags.hasBrightness ? 1 : 0, flags.hasNormals ? 1 : 0,
restoreData != null ? "yes" : "no");
public RestoreData(DirectTessellator tessellator) {
this.lastColor = tessellator.getPackedColor();
this.lastNormal = tessellator.getPackedNormal();
this.lastTexCoordU = (float) tessellator.getLastTextureU();
this.lastTexCoordV = (float) tessellator.getLastTextureV();
this.flags = tessellator.getVertexFormat().getVertexFlags();
}
}
}
@@ -1,39 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.List;
/**
* Represents an accumulated line draw call during display list compilation.
* Similar to AccumulatedDraw but for line primitives.
* Stores untransformed line vertices + captured matrix.
*/
public class AccumulatedLineDraw {
public final List<ImmediateModeRecorder.LineVertex> lines;
public final Matrix4f transform;
public final CapturingTessellator.Flags flags;
public final int commandIndex; // Position in command list for state tracking
public final int stateGeneration; // Same value = no state commands between draws, can merge
public AccumulatedLineDraw(List<ImmediateModeRecorder.LineVertex> lines, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex) {
this(lines, transform, flags, commandIndex, 0);
}
public AccumulatedLineDraw(List<ImmediateModeRecorder.LineVertex> lines, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex, int stateGeneration) {
// Copy lines - the source list may be cleared
this.lines = new ArrayList<>(lines);
this.transform = new Matrix4f(transform); // Snapshot for runtime application
this.flags = flags;
this.commandIndex = commandIndex;
this.stateGeneration = stateGeneration;
}
@Override
public String toString() {
return String.format("AccumulatedLineDraw[lines=%d, cmdIdx=%d, flags=C%dT%dL%dN%d]",
lines.size() / 2, commandIndex, flags.hasColor ? 1 : 0, flags.hasTexture ? 1 : 0, flags.hasBrightness ? 1 : 0, flags.hasNormals ? 1 : 0);
}
}
@@ -1,66 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.line.ModelLine;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.primitive.ModelPrimitiveView;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.tri.ModelTriangle;
import org.joml.Matrix4f;
import java.util.ArrayList;
import java.util.List;
/**
* Represents an accumulated primitive draw call during display list compilation.
* Stores primitives (lines, triangles) from tessellator callback with their transform.
* <p>
* Unlike AccumulatedLineDraw which stores ImmediateModeRecorder.LineVertex (16-byte: pos+color),
* this stores ModelPrimitiveView (32-byte: pos+color+tex+light+normal) from the tessellator.
*/
public class AccumulatedPrimitiveDraw {
public final List<ModelPrimitiveView> primitives;
public final Matrix4f transform;
public final CapturingTessellator.Flags flags;
public final int commandIndex;
public final int matrixGeneration; // For batching: same generation = same transform context
public final int stateGeneration; // Same value = no state commands between draws, can merge
public AccumulatedPrimitiveDraw(List<ModelPrimitiveView> primitives, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex, int matrixGeneration) {
this(primitives, transform, flags, commandIndex, matrixGeneration, 0);
}
public AccumulatedPrimitiveDraw(List<ModelPrimitiveView> primitives, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex, int matrixGeneration, int stateGeneration) {
// Deep copy primitives - pooled primitives are reused after callback
this.primitives = deepCopyPrimitives(primitives);
this.transform = new Matrix4f(transform);
this.flags = flags;
this.commandIndex = commandIndex;
this.matrixGeneration = matrixGeneration;
this.stateGeneration = stateGeneration;
}
/**
* Creates deep copies of all primitives in the list.
* Required because CapturingTessellator pools and reuses primitive objects.
*/
private static List<ModelPrimitiveView> deepCopyPrimitives(List<ModelPrimitiveView> source) {
final int size = source.size();
final List<ModelPrimitiveView> copies = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
final ModelPrimitiveView prim = source.get(i);
if (prim instanceof ModelLine ml) {
copies.add(new ModelLine().copyFrom(ml));
} else if (prim instanceof ModelTriangle mt) {
copies.add(new ModelTriangle().copyFrom(mt));
}
}
return copies;
}
@Override
public String toString() {
return String.format("AccumulatedPrimitiveDraw[primitives=%d, cmdIdx=%d, matrixGen=%d, stateGen=%d, mode=%d, flags=C%dT%dL%dN%d]",
primitives.size(), commandIndex, matrixGeneration, stateGeneration, flags.drawMode,
flags.hasColor ? 1 : 0, flags.hasTexture ? 1 : 0,
flags.hasBrightness ? 1 : 0, flags.hasNormals ? 1 : 0);
}
}
@@ -14,12 +14,6 @@ import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
public final class CommandBuffer {
private static final int DEFAULT_CAPACITY = 4096; // Guess at typical display list size
// Flag bits for DRAW_RANGE_RESTORE command
public static final int FLAG_HAS_BRIGHTNESS = 1 << 0;
public static final int FLAG_HAS_COLOR = 1 << 1;
public static final int FLAG_HAS_NORMALS = 1 << 2;
public static final int FLAG_HAS_TEXTURE = 1 << 3;
private ByteBuffer buffer;
private long basePointer;
private long writePointer;
@@ -42,6 +36,11 @@ public final class CommandBuffer {
writePointer += 4;
}
private void writeLong(long value) {
memPutLong(writePointer, value);
writePointer += 8;
}
private void writeFloat(float value) {
memPutFloat(writePointer, value);
writePointer += 4;
@@ -264,6 +263,15 @@ public final class CommandBuffer {
writeInt(height);
}
public void writeScissor(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.SCISSOR);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void writeBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_FUNC);
@@ -546,15 +554,22 @@ public final class CommandBuffer {
}
}
public void writeClipPlane(int plane, double a, double b, double c, double d) {
ensureCapacity(40);
writeInt(GLCommand.CLIP_PLANE);
writeInt(plane);
writeDouble(a);
writeDouble(b);
writeDouble(c);
writeDouble(d);
}
// === Draw commands ===
public void writeDrawRange(int vboIndex, int startVertex, int vertexCount, boolean hasBrightness) {
ensureCapacity(20);
public void writeDrawRange(int vboIndex) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_RANGE);
writeInt(vboIndex);
writeInt(startVertex);
writeInt(vertexCount);
writeInt(hasBrightness ? 1 : 0);
}
/**
@@ -562,51 +577,21 @@ public final class CommandBuffer {
* Used for immediate mode VBOs to restore GL_CURRENT_COLOR, GL_CURRENT_NORMAL, GL_CURRENT_TEXTURE_COORDS.
*
* @param vboIndex Index into ownedVbos array
* @param startVertex First vertex to draw
* @param vertexCount Number of vertices to draw
* @param hasBrightness True if VBO has brightness/lightmap data
* @param hasColor True if color should be restored after draw
* @param hasNormals True if normal should be restored after draw
* @param hasTexture True if texcoord should be restored after draw
* @param lastColorR Last vertex's red color component
* @param lastColorG Last vertex's green color component
* @param lastColorB Last vertex's blue color component
* @param lastColorA Last vertex's alpha color component
* @param lastNormalX Last vertex's normal X
* @param lastNormalY Last vertex's normal Y
* @param lastNormalZ Last vertex's normal Z
* @param lastTexS Last vertex's texture S coordinate
* @param lastTexT Last vertex's texture T coordinate
*/
public void writeDrawRangeRestore(
int vboIndex, int startVertex, int vertexCount,
boolean hasBrightness, boolean hasColor, boolean hasNormals, boolean hasTexture,
float lastColorR, float lastColorG, float lastColorB, float lastColorA,
float lastNormalX, float lastNormalY, float lastNormalZ,
float lastTexS, float lastTexT
int vboIndex, AccumulatedDraw.RestoreData restoreData
) {
ensureCapacity(56);
ensureCapacity(28);
writeInt(GLCommand.DRAW_RANGE_RESTORE);
writeInt(vboIndex);
writeInt(startVertex);
writeInt(vertexCount);
int flags = (hasBrightness ? FLAG_HAS_BRIGHTNESS : 0)
| (hasColor ? FLAG_HAS_COLOR : 0)
| (hasNormals ? FLAG_HAS_NORMALS : 0)
| (hasTexture ? FLAG_HAS_TEXTURE : 0);
writeInt(flags);
// Last color (4 floats = 16 bytes)
writeFloat(lastColorR);
writeFloat(lastColorG);
writeFloat(lastColorB);
writeFloat(lastColorA);
// Last normal (3 floats = 12 bytes)
writeFloat(lastNormalX);
writeFloat(lastNormalY);
writeFloat(lastNormalZ);
writeInt(restoreData.flags);
// Last color (4 bytes)
writeInt(restoreData.lastColor);
// Last normal (4 bytes)
writeInt(restoreData.lastNormal);
// Last texcoord (2 floats = 8 bytes)
writeFloat(lastTexS);
writeFloat(lastTexT);
writeFloat(restoreData.lastTexCoordU);
writeFloat(restoreData.lastTexCoordV);
}
public void writeCallList(int listId) {
@@ -615,6 +600,35 @@ public final class CommandBuffer {
writeInt(listId);
}
public void writeDrawArrays(int mode, int start, int count) {
ensureCapacity(16);
writeInt(GLCommand.DRAW_ARRAYS);
writeInt(mode);
writeInt(start);
writeInt(count);
}
public void writeDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
ensureCapacity(24);
writeInt(GLCommand.DRAW_ELEMENTS);
writeInt(mode);
writeInt(indices_count);
writeInt(type);
writeLong(indices_buffer_offset);
}
public void writeBindVBO(int vbo) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VBO);
writeInt(vbo);
}
public void writeBindVAO(int vao) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VAO);
writeInt(vao);
}
public void writeDrawBuffer(int mode) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_BUFFER);
@@ -685,6 +699,12 @@ public final class CommandBuffer {
return value;
}
public long readLong() {
final long value = memGetLong(readPointer);
readPointer += 8;
return value;
}
/**
* Read a float from the current position.
*/
@@ -1,17 +1,20 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizon.gtnhlib.client.opengl.UniversalVAO;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizons.angelica.glsm.recording.CommandBuffer.*;
/**
* Executes commands from a CommandBuffer
@@ -23,6 +26,8 @@ public final class CommandBufferExecutor {
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer(16);
// Static reusable buffer for fog/light/material params
private static final FloatBuffer PARAMS_BUFFER = BufferUtils.createFloatBuffer(4);
// Static reusable buffer for clip plane equation
private static final DoubleBuffer PARAMS_DOUBLE_BUFFER = BufferUtils.createDoubleBuffer(4);
// Static reusable buffer for draw buffers
private static final int MAX_DRAW_BUFFERS = 8;
private static final IntBuffer DRAW_BUFFERS_BUFFER = BufferUtils.createIntBuffer(MAX_DRAW_BUFFERS);
@@ -34,7 +39,7 @@ public final class CommandBufferExecutor {
* @param complexObjects Array of complex objects (TexImage2DCmd, etc.)
* @param ownedVbos Array of VBOs owned by the display list (indexed by DrawRange commands)
*/
public static void execute(ByteBuffer buffer, Object[] complexObjects, VertexBuffer[] ownedVbos) {
public static void execute(ByteBuffer buffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
long ptr = memAddress(buffer);
final long end = ptr + buffer.limit();
@@ -194,6 +199,14 @@ public final class CommandBufferExecutor {
ptr += 16;
GLStateManager.glViewport(x, y, width, height);
}
case GLCommand.SCISSOR -> {
final int x = memGetInt(ptr);
final int y = memGetInt(ptr + 4);
final int width = memGetInt(ptr + 8);
final int height = memGetInt(ptr + 12);
ptr += 16;
GLStateManager.glScissor(x, y, width, height);
}
case GLCommand.BLEND_FUNC -> {
final int srcRgb = memGetInt(ptr);
final int dstRgb = memGetInt(ptr + 4);
@@ -447,51 +460,55 @@ public final class CommandBufferExecutor {
GLStateManager.glMaterial(face, pname, PARAMS_BUFFER);
}
case GLCommand.CLIP_PLANE -> {
final int plane = memGetInt(ptr);
final double a = memGetDouble(ptr + 4);
final double b = memGetDouble(ptr + 12);
final double c = memGetDouble(ptr + 20);
final double d = memGetDouble(ptr + 28);
ptr += 36;
PARAMS_DOUBLE_BUFFER.clear();
PARAMS_DOUBLE_BUFFER.put(a).put(b).put(c).put(d);
PARAMS_DOUBLE_BUFFER.flip();
GLStateManager.glClipPlane(plane, PARAMS_DOUBLE_BUFFER);
}
// === Draw commands ===
case GLCommand.DRAW_RANGE -> {
final int vboIndex = memGetInt(ptr);
final int start = memGetInt(ptr + 4);
final int count = memGetInt(ptr + 8);
final boolean hasBrightness = memGetInt(ptr + 12) != 0;
ptr += 16;
final VertexBuffer vbo = ownedVbos[vboIndex];
vbo.setupState();
vbo.draw(start, count);
vbo.cleanupState();
ptr += 4;
ownedVbos.render(vboIndex);
}
case GLCommand.DRAW_RANGE_RESTORE -> {
// Draw VBO range then restore GL current state from last vertex attributes
final int vboIndex = memGetInt(ptr);
final int start = memGetInt(ptr + 4);
final int count = memGetInt(ptr + 8);
final int flags = memGetInt(ptr + 12);
final int flags = memGetInt(ptr + 4);
// Draw the VBO
final VertexBuffer vbo = ownedVbos[vboIndex];
vbo.setupState();
vbo.draw(start, count);
vbo.cleanupState();
ownedVbos.render(vboIndex);
// Restore attributes based on flags
if ((flags & FLAG_HAS_COLOR) != 0) {
final float r = memGetFloat(ptr + 16);
final float g = memGetFloat(ptr + 20);
final float b = memGetFloat(ptr + 24);
final float a = memGetFloat(ptr + 28);
GLStateManager.glColor4f(r, g, b, a);
if ((flags & VertexFlags.COLOR_BIT) != 0) {
final int color = memGetInt(ptr + 8);
byte a = (byte) ((color >> 24) & 0xFF);
byte r = (byte) ((color >> 16) & 0xFF);
byte g = (byte) ((color >> 8) & 0xFF);
byte b = (byte) ((color & 0xFF));
GLStateManager.glColor4ub(r, g, b, a);
}
if ((flags & FLAG_HAS_NORMALS) != 0) {
final float nx = memGetFloat(ptr + 32);
final float ny = memGetFloat(ptr + 36);
final float nz = memGetFloat(ptr + 40);
if ((flags & VertexFlags.NORMAL_BIT) != 0) {
final int normal = memGetInt(ptr + 12);
float nx = (byte)(normal) / 127.0f;
float ny = (byte)(normal >> 8) / 127.0f;
float nz = (byte)(normal >> 16) / 127.0f;
GLStateManager.glNormal3f(nx, ny, nz);
}
if ((flags & FLAG_HAS_TEXTURE) != 0) {
final float s = memGetFloat(ptr + 44);
final float t = memGetFloat(ptr + 48);
if ((flags & VertexFlags.TEXTURE_BIT) != 0) {
final float s = memGetFloat(ptr + 16);
final float t = memGetFloat(ptr + 20);
GLStateManager.glTexCoord2f(s, t);
}
ptr += 52; // Skip full command size (56 - 4 for cmd already read)
ptr += 24; // Skip full command size (28 - 4 for cmd already read)
}
case GLCommand.CALL_LIST -> {
final int listId = memGetInt(ptr);
@@ -513,6 +530,22 @@ public final class CommandBufferExecutor {
ptr += 4 * MAX_DRAW_BUFFERS;
GLStateManager.glDrawBuffers(DRAW_BUFFERS_BUFFER);
}
case GLCommand.DRAW_ARRAYS -> {
GLStateManager.glDrawArrays(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8));
ptr += 12;
}
case GLCommand.DRAW_ELEMENTS -> {
GLStateManager.glDrawElements(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8), memGetLong(ptr + 12));
ptr += 20;
}
case GLCommand.BIND_VBO -> {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, memGetInt(ptr));
ptr += 4;
}
case GLCommand.BIND_VAO -> {
GLStateManager.glBindVertexArray(memGetInt(ptr));
ptr += 4;
}
// === Complex object reference ===
case GLCommand.COMPLEX_REF -> {
@@ -17,73 +17,6 @@ public final class CommandBufferProcessor {
private CommandBufferProcessor() {}
public static void processCommandForOptimization(int opcode, CommandBuffer raw, BufferTransformOptimizer opt, CommandBuffer out) {
switch (opcode) {
// === MultMatrix - already collapsed transforms from flushMatrix() ===
case GLCommand.MULT_MATRIX -> {
raw.readMatrix4f(tempMatrix);
opt.multMatrix(tempMatrix); // accumulated = accumulated * tempMatrix
out.writeMultMatrix(tempMatrix); // Copy to output
opt.markLastEmittedAsAccumulated(); // lastEmitted = accumulated (what GL now has)
}
// === Load commands - flush pending, emit, reset accumulator ===
case GLCommand.LOAD_IDENTITY -> {
opt.emitPendingTransform(out); // Emit any pending transforms first
out.writeLoadIdentity(); // Always emit LoadIdentity
opt.loadIdentity(); // Reset accumulated to identity
opt.resetLastEmitted(); // Reset lastEmitted to identity
opt.checkAndClearAbsoluteMatrix(); // Clear the flag if set
}
case GLCommand.LOAD_MATRIX -> {
raw.readMatrix4f(tempMatrix);
opt.emitPendingTransform(out);
out.writeLoadMatrix(tempMatrix);
opt.loadIdentity();
opt.resetLastEmitted(); // Reset lastEmitted to identity
opt.markAbsoluteMatrix();
}
// === Stack operations - flush/update optimizer state and copy ===
case GLCommand.PUSH_MATRIX -> {
opt.emitPendingTransform(out);
opt.push();
// Reset accumulated/lastEmitted to identity after push.
opt.loadIdentity();
opt.resetLastEmitted();
out.writePushMatrix();
}
case GLCommand.POP_MATRIX -> {
opt.pop();
out.writePopMatrix();
}
// === Barriers - emit pending transform before copying ===
case GLCommand.MATRIX_MODE -> {
final int mode = raw.readInt();
opt.emitPendingTransform(out); // Pending transforms apply to OLD mode
out.writeMatrixMode(mode);
opt.loadIdentity(); // Reset accumulated for new mode
opt.resetLastEmitted(); // Reset lastEmitted for new mode
}
case GLCommand.CALL_LIST -> {
final int listId = raw.readInt();
opt.emitPendingTransform(out);
out.writeCallList(listId);
}
case GLCommand.PUSH_ATTRIB -> {
final int mask = raw.readInt();
if ((mask & GL11.GL_TRANSFORM_BIT) != 0) {
opt.emitPendingTransform(out);
}
out.writePushAttrib(mask);
}
// === All other commands - just copy ===
default -> copyCommandData(opcode, raw, out);
}
}
/**
* Copy a complete command (opcode already read) from raw to final buffer.
*/
@@ -134,6 +67,7 @@ public final class CommandBufferProcessor {
// Four int commands
case GLCommand.VIEWPORT -> out.writeViewport(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.SCISSOR -> out.writeScissor(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.BLEND_FUNC -> out.writeBlendFunc(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.COLOR_MASK -> out.writeColorMask(raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0);
case GLCommand.STENCIL_FUNC_SEPARATE -> out.writeStencilFuncSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
@@ -209,9 +143,23 @@ public final class CommandBufferProcessor {
out.writeMaterial(face, pname, fb);
}
// Clip plane command (4 doubles)
case GLCommand.CLIP_PLANE -> {
final int plane = raw.readInt();
final double a = raw.readDouble();
final double b = raw.readDouble();
final double c = raw.readDouble();
final double d = raw.readDouble();
out.writeClipPlane(plane, a, b, c, d);
}
// Draw/call commands
case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt() != 0);
case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt());
case GLCommand.CALL_LIST -> out.writeCallList(raw.readInt());
case GLCommand.DRAW_ARRAYS -> out.writeDrawArrays(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.DRAW_ELEMENTS -> out.writeDrawElements(raw.readInt(), raw.readInt(), raw.readInt(), raw.readLong());
case GLCommand.BIND_VBO -> out.writeBindVBO(raw.readInt());
case GLCommand.BIND_VAO -> out.writeBindVAO(raw.readInt());
// Complex object reference - transfer the object to output buffer
case GLCommand.COMPLEX_REF -> {
@@ -223,94 +171,4 @@ public final class CommandBufferProcessor {
default -> LOGGER.warn("[CommandBufferProcessor] Unknown opcode in buffer copy: {}", opcode);
}
}
/**
* Lightweight transform optimizer for buffer-to-buffer processing.
* Tracks accumulated transform (mode-agnostic) and emits MultMatrix commands when needed.
*
* <p>This optimizer processes raw transform commands (TRANSLATE, ROTATE, SCALE) and
* collapses them into single MultMatrix commands at barriers. It must stay synchronized
* with the relativeTransform tracking in DisplayListManager - both systems must reset
* at the same barriers (MatrixMode, LoadIdentity, LoadMatrix, Push/Pop).</p>
*/
public static class BufferTransformOptimizer {
private final Matrix4f accumulated = new Matrix4f();
private final Matrix4f lastEmitted = new Matrix4f();
private final Matrix4f delta = new Matrix4f(); // Reused for delta computation
private final Deque<Matrix4f> stack = new ArrayDeque<>();
private boolean absoluteMatrixFlag = false;
public BufferTransformOptimizer() {
accumulated.identity();
lastEmitted.identity();
}
public void multMatrix(Matrix4f m) {
accumulated.mul(m);
}
public void loadIdentity() {
accumulated.identity();
}
public void push() {
stack.push(new Matrix4f(accumulated));
}
public void pop() {
if (!stack.isEmpty()) {
accumulated.set(stack.pop());
lastEmitted.set(accumulated);
} else {
accumulated.identity();
lastEmitted.identity();
}
}
public boolean isIdentity() {
return DisplayListManager.isIdentity(accumulated);
}
public void markAbsoluteMatrix() {
absoluteMatrixFlag = true;
}
public boolean checkAndClearAbsoluteMatrix() {
final boolean was = absoluteMatrixFlag;
absoluteMatrixFlag = false;
return was;
}
public void resetLastEmitted() {
lastEmitted.identity();
}
public void setLastEmitted(Matrix4f m) {
lastEmitted.set(m);
}
public void markLastEmittedAsAccumulated() {
lastEmitted.set(accumulated);
}
public void emitPendingTransform(CommandBuffer out) {
if (!accumulated.equals(lastEmitted)) {
emitTransformTo(out, accumulated);
}
}
public void emitTransformTo(CommandBuffer out, Matrix4f target) {
if (target.equals(lastEmitted)) {
return;
}
if (DisplayListManager.isIdentity(lastEmitted)) {
out.writeMultMatrix(target);
} else {
delta.set(lastEmitted).invert().mul(target);
out.writeMultMatrix(delta);
}
lastEmitted.set(target);
}
}
}
@@ -9,7 +9,7 @@ import java.nio.IntBuffer;
/**
* Records GL commands to a CommandBuffer during display list compilation.
*/
public class CommandRecorder {
public final class CommandRecorder {
private final CommandBuffer buffer;
private int commandCount;
@@ -172,6 +172,11 @@ public class CommandRecorder {
commandCount++;
}
public void recordScissor(int x, int y, int width, int height) {
buffer.writeScissor(x, y, width, height);
commandCount++;
}
public void recordPointSize(float size) {
buffer.writePointSize(size);
commandCount++;
@@ -287,6 +292,11 @@ public class CommandRecorder {
commandCount++;
}
public void recordClipPlane(int plane, double a, double b, double c, double d) {
buffer.writeClipPlane(plane, a, b, c, d);
commandCount++;
}
public void recordStencilFunc(int func, int ref, int mask) {
buffer.writeStencilFunc(func, ref, mask);
commandCount++;
@@ -337,6 +347,26 @@ public class CommandRecorder {
commandCount++;
}
public void recordDrawArrays(int mode, int start, int count) {
buffer.writeDrawArrays(mode, start, count);
commandCount++;
}
public void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
buffer.writeDrawElements(mode, indices_count, type, indices_buffer_offset);
commandCount++;
}
public void recordBindVBO(int vbo) {
buffer.writeBindVBO(vbo);
commandCount++;
}
public void recordBindVAO(int vao) {
buffer.writeBindVAO(vao);
commandCount++;
}
public void recordComplexCommand(DisplayListCommand cmd) {
buffer.writeComplexRef(cmd);
commandCount++;
@@ -1,6 +1,6 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
@@ -30,9 +30,9 @@ public final class CompiledDisplayList {
private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed
private final Object[] complexObjects; // Complex commands (TexImage2D, etc.)
private final VertexBuffer[] ownedVbos; // GPU resources referenced by index
private final DisplayListVBO ownedVbos; // GPU resources referenced by index
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, VertexBuffer[] ownedVbos) {
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
this.commandBuffer = commandBuffer;
this.complexObjects = complexObjects;
this.ownedVbos = ownedVbos;
@@ -62,11 +62,7 @@ public final class CompiledDisplayList {
// Close VBOs
if (ownedVbos != null) {
for (VertexBuffer vbo : ownedVbos) {
if (vbo != null) {
vbo.close();
}
}
ownedVbos.delete();
}
// Free the command buffer (off-heap memory)
@@ -89,10 +85,7 @@ public final class CompiledDisplayList {
return complexObjects;
}
/**
* Get owned VBOs for inlining into another display list.
*/
public VertexBuffer[] getOwnedVbos() {
public DisplayListVBO getOwnedVbos() {
return ownedVbos;
}
@@ -112,7 +105,7 @@ public final class CompiledDisplayList {
while (ptr < end) {
final int cmd = memGetInt(ptr);
counts.mergeInt(cmd, 1, Integer::sum);
ptr += getCommandSize(cmd, ptr);
ptr += GLCommand.getCommandSize(cmd, ptr);
}
return counts;
}
@@ -131,74 +124,13 @@ public final class CompiledDisplayList {
while (ptr < end) {
final int cmd = memGetInt(ptr);
opcodes.add(cmd);
ptr += getCommandSize(cmd, ptr);
ptr += GLCommand.getCommandSize(cmd, ptr);
}
return opcodes;
}
/**
* Get size of a command in bytes (including the opcode).
*/
private static int getCommandSize(int cmd, long ptr) {
return switch (cmd) {
// Opcode-only commands (4 bytes)
case GLCommand.LOAD_IDENTITY, GLCommand.PUSH_MATRIX, GLCommand.POP_MATRIX -> 4;
// Single int commands (8 bytes)
case GLCommand.ENABLE, GLCommand.DISABLE, GLCommand.CLEAR, GLCommand.CLEAR_STENCIL,
GLCommand.CULL_FACE, GLCommand.DEPTH_FUNC, GLCommand.SHADE_MODEL, GLCommand.LOGIC_OP,
GLCommand.MATRIX_MODE, GLCommand.ACTIVE_TEXTURE, GLCommand.USE_PROGRAM,
GLCommand.PUSH_ATTRIB, GLCommand.POP_ATTRIB, GLCommand.STENCIL_MASK,
GLCommand.DEPTH_MASK, GLCommand.FRONT_FACE, GLCommand.POINT_SIZE, GLCommand.LINE_WIDTH,
GLCommand.CALL_LIST, GLCommand.COMPLEX_REF -> 8;
// Two int commands (12 bytes)
case GLCommand.BIND_TEXTURE, GLCommand.POLYGON_MODE, GLCommand.COLOR_MATERIAL,
GLCommand.LINE_STIPPLE, GLCommand.STENCIL_MASK_SEPARATE, GLCommand.FOGI,
GLCommand.HINT, GLCommand.POLYGON_OFFSET, GLCommand.ALPHA_FUNC, GLCommand.FOGF,
GLCommand.LIGHT_MODELF, GLCommand.LIGHT_MODELI -> 12;
// Three int commands (16 bytes)
case GLCommand.STENCIL_FUNC, GLCommand.STENCIL_OP, GLCommand.TEX_PARAMETERI,
GLCommand.LIGHTF, GLCommand.LIGHTI,
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF -> 16;
// Four int commands (20 bytes)
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
GLCommand.STENCIL_FUNC_SEPARATE, GLCommand.STENCIL_OP_SEPARATE,
GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.BLEND_COLOR,
GLCommand.DRAW_RANGE -> 20;
// DRAW_RANGE_RESTORE: 56 bytes
// [cmd:4][vboIndex:4][start:4][count:4][flags:4][color:16f][normal:12f][texcoord:8f]
case GLCommand.DRAW_RANGE_RESTORE -> 56;
// DRAW_BUFFER: 8 bytes [cmd:4][mode:4]
case GLCommand.DRAW_BUFFER -> 8;
// DRAW_BUFFERS: 40 bytes [cmd:4][count:4][bufs:4*8]
case GLCommand.DRAW_BUFFERS -> 40;
// Double commands
case GLCommand.TRANSLATE, GLCommand.SCALE -> 28; // cmd + 3 doubles
case GLCommand.CLEAR_DEPTH -> 12; // cmd + 1 double
case GLCommand.ROTATE -> 36; // cmd + 4 doubles
case GLCommand.ORTHO, GLCommand.FRUSTUM -> 52; // cmd + 6 doubles
// Matrix commands
case GLCommand.MULT_MATRIX, GLCommand.LOAD_MATRIX -> 68; // cmd + 16 floats
// Buffer commands (variable, read count)
case GLCommand.FOG, GLCommand.LIGHT_MODEL -> {
final int count = memGetInt(ptr + 8); // pname at +4, count at +8
yield 12 + count * 4;
}
case GLCommand.LIGHT, GLCommand.MATERIAL -> {
final int count = memGetInt(ptr + 12); // light/face at +4, pname at +8, count at +12
yield 16 + count * 4;
}
default -> throw new IllegalStateException("Unknown command: " + cmd);
};
@Override
public String toString() {
return DisplayListManager.getCompiledDisplayListString(0, this, null);
}
}
@@ -0,0 +1,58 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
/**
* A class that stores multiple vertex formats & their corresponding buffers.
*/
public final class DisplayListVBO {
private final SubVBO[] vbos;
DisplayListVBO(SubVBO[] vbos) {
this.vbos = vbos;
}
public void delete() {
for (SubVBO vbo : vbos) {
vbo.delete();
}
}
public SubVBO[] getVBOs() {
return vbos;
}
public void render(int index) {
vbos[index].render();
}
public static final class SubVBO {
private final IVertexArrayObject vao;
private final int drawMode;
private final int start;
private final int count;
public SubVBO(IVertexArrayObject vao, int drawMode, int start, int count) {
this.vao = vao;
this.drawMode = drawMode;
this.start = start;
this.count = count;
}
public void delete() {
vao.delete();
}
public void render() {
vao.bind();
vao.draw(drawMode, start, count);
vao.unbind();
}
public IVertexArrayObject getVAO() {
return vao;
}
}
}
@@ -0,0 +1,131 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexedVAO;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.IVertexBuffer;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.nmemFree;
/**
* Compiles multiple ByteBuffers of the same Vertex format into a VBO. Any buffer added to the builder will be freed afterward.
*/
public final class DisplayListVBOBuilder {
// Removes the need for a HashMap. Since it's only an array of 16 values, this should be faster
private final List<FormatData>[] formats = new List[VertexFlags.BITSET_SIZE];
private int count;
public DisplayListVBOBuilder addDraw(VertexFormat format, int drawMode, List<ByteBuffer> buffers) {
List<FormatData> data = formats[format.getVertexFlags()];
if (data == null) {
data = new ArrayList<>();
formats[format.getVertexFlags()] = data;
}
data.add(new FormatData(buffers, drawMode, count));
count++;
return this;
}
public DisplayListVBOBuilder addDraws(List<AccumulatedDraw> draws) {
for (AccumulatedDraw draw : draws) {
addDraw(draw.format, draw.drawMode, draw.drawBuffers);
}
return this;
}
public DisplayListVBO build() {
final DisplayListVBO.SubVBO[] vbos = new DisplayListVBO.SubVBO[count];
final ArrayList<ByteBuffer> allBuffers = new ArrayList<>();
for (int i = 0; i < VertexFlags.BITSET_SIZE; i++) {
List<FormatData> formatData = formats[i];
if (formatData == null) continue;
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[i];
int start = 0;
final IVertexArrayObject vao = VAOManager.createStorageVAO(format, -1, 0); // drawMode will be ignored
final IVertexBuffer vbo = vao.getVBO();
for (FormatData data : formatData) {
int vertexCount;
final List<ByteBuffer> drawBuffers = data.buffers;
if (drawBuffers.size() == 1) {
final ByteBuffer buffer = drawBuffers.get(0);
allBuffers.add(buffer);
vertexCount = format.getVertexCount(buffer);
} else {
allBuffers.addAll(drawBuffers);
int size = 0;
for (ByteBuffer buffer : drawBuffers) {
size += buffer.remaining();
}
vertexCount = format.getVertexCount(size);
}
if (data.drawMode == GL11.GL_QUADS) {
final IVertexArrayObject indexedVAO = new IndexedVAO(vbo, IndexBuffer.convertQuadsToTrigs(start, start + vertexCount));
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(indexedVAO, GL11.GL_TRIANGLES, 0, vertexCount / 4 * 6);
} else {
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, data.drawMode, start, vertexCount);
}
start += vertexCount;
}
ByteBuffer bigBuffer = mergeAndDelete(allBuffers);
vbo.allocate(bigBuffer, start);
memFree(bigBuffer);
allBuffers.clear();
}
return new DisplayListVBO(vbos);
}
private static ByteBuffer mergeAndDelete(List<ByteBuffer> buffers) {
if (buffers.size() == 1) {
return buffers.get(0);
}
int needed = 0;
for (ByteBuffer buffer : buffers) {
needed += buffer.remaining();
}
ByteBuffer out = memAlloc(needed);
long dst = memAddress0(out);
for (ByteBuffer buffer : buffers) {
final int len = buffer.limit();
final long address = memAddress0(buffer);
memCopy(address, dst, len);
dst += len;
nmemFree(address);
}
out.limit(needed);
return out;
}
private static final class FormatData {
private final List<ByteBuffer> buffers;
private final int drawMode;
private final int drawIndex;
public FormatData(List<ByteBuffer> buffers, int drawMode, int drawIndex) {
this.buffers = buffers;
this.drawMode = drawMode;
this.drawIndex = drawIndex;
}
}
}
@@ -1,5 +1,7 @@
package com.gtnewhorizons.angelica.glsm.recording;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
/**
* Command opcodes for ByteBuffer-based display list serialization.
* Each command has a fixed layout described in comments.
@@ -49,6 +51,7 @@ public final class GLCommand {
public static final int COLOR_MASK = 42; // [cmd:4][r:4][g:4][b:4][a:4] (0 or 1 each)
public static final int STENCIL_FUNC_SEPARATE = 43; // [cmd:4][face:4][func:4][ref:4][mask:4]
public static final int STENCIL_OP_SEPARATE = 44; // [cmd:4][face:4][sfail:4][dpfail:4][dppass:4]
public static final int SCISSOR = 45; // [cmd:4][x:4][y:4][width:4][height:4]
// === Float commands ===
public static final int POINT_SIZE = 50; // [cmd:4][size:4f]
@@ -86,18 +89,27 @@ public final class GLCommand {
public static final int LIGHT = 91; // [cmd:4][light:4][pname:4][count:4][params:16f] = 32 bytes
public static final int LIGHT_MODEL = 92; // [cmd:4][pname:4][count:4][params:16f] = 28 bytes
public static final int MATERIAL = 93; // [cmd:4][face:4][pname:4][count:4][params:16f] = 32 bytes
public static final int CLIP_PLANE = 94; // [cmd:4][plane:4][a:8d][b:8d][c:8d][d:8d] = 40 bytes
// === Draw commands ===
public static final int DRAW_RANGE = 100; // [cmd:4][vboIndex:4][start:4][count:4][hasBrightness:4] = 20 bytes
public static final int CALL_LIST = 101; // [cmd:4][listId:4] = 8 bytes
public static final int DRAW_BUFFER = 102; // [cmd:4][mode:4] = 8 bytes
public static final int DRAW_BUFFERS = 103; // [cmd:4][count:4][bufs:4*8] = 40 bytes (up to 8 buffers)
public static final int DRAW_RANGE = 100; // [cmd:4][vboIndex:4] = 8 bytes
public static final int DRAW_RANGE_RESTORE = 101; // [cmd:4][vboIndex:4][flags:4][color:4][normal:4][lastTexCoord:8f] = 28 bytes
public static final int CALL_LIST = 102; // [cmd:4][listId:4] = 8 bytes
public static final int DRAW_ARRAYS = 110; // [cmd:4][drawMode:4][start:4][count:4] = 20 bytes
public static final int DRAW_ELEMENTS = 111; // [cmd:4][mode:4][indices_count:4][type:4][indices_buffer_offset:8] = 28 bytes
public static final int DRAW_BUFFER = 112; // [cmd:4][mode:4] = 8 bytes
public static final int DRAW_BUFFERS = 113; // [cmd:4][count:4][bufs:4*8] = 40 bytes (up to 8 buffers)
/**
* Draw VBO range with attribute restoration after draw.
* Used for immediate mode VBOs to restore GL current state (color, normal, texcoord).
*/
public static final int DRAW_RANGE_RESTORE = 104; // [cmd:4][vboIndex:4][start:4][count:4][flags:4]
// [lastColor:16f][lastNormal:12f][lastTexCoord:8f] = 56 bytes
// === Bind commands ===
public static final int BIND_VBO = 120;
public static final int BIND_VAO = 121;
// === Complex object reference ===
public static final int COMPLEX_REF = 255; // [cmd:4][index:4] = 8 bytes
@@ -137,6 +149,7 @@ public final class GLCommand {
case STENCIL_OP -> "STENCIL_OP";
case TEX_PARAMETERI -> "TEX_PARAMETERI";
case VIEWPORT -> "VIEWPORT";
case SCISSOR -> "SCISSOR";
case BLEND_FUNC -> "BLEND_FUNC";
case COLOR_MASK -> "COLOR_MASK";
case STENCIL_FUNC_SEPARATE -> "STENCIL_FUNC_SEPARATE";
@@ -167,13 +180,85 @@ public final class GLCommand {
case LIGHT -> "LIGHT";
case LIGHT_MODEL -> "LIGHT_MODEL";
case MATERIAL -> "MATERIAL";
case CLIP_PLANE -> "CLIP_PLANE";
case DRAW_RANGE -> "DRAW_RANGE";
case CALL_LIST -> "CALL_LIST";
case DRAW_BUFFER -> "DRAW_BUFFER";
case DRAW_BUFFERS -> "DRAW_BUFFERS";
case DRAW_RANGE_RESTORE -> "DRAW_RANGE_RESTORE";
case COMPLEX_REF -> "COMPLEX_REF";
case DRAW_RANGE_RESTORE -> "DRAW_RANGE_RESTORE";
case BIND_VAO -> "BIND_VAO";
case BIND_VBO -> "BIND_VBO";
default -> "UNKNOWN(" + opcode + ")";
};
}
/**
* Get size of a command in bytes (including the opcode).
*/
public static int getCommandSize(int cmd, long ptr) {
return switch (cmd) {
// Opcode-only commands (4 bytes)
case GLCommand.LOAD_IDENTITY, GLCommand.PUSH_MATRIX, GLCommand.POP_MATRIX -> 4;
// Single int commands (8 bytes)
case GLCommand.ENABLE, GLCommand.DISABLE, GLCommand.CLEAR, GLCommand.CLEAR_STENCIL,
GLCommand.CULL_FACE, GLCommand.DEPTH_FUNC, GLCommand.SHADE_MODEL, GLCommand.LOGIC_OP,
GLCommand.MATRIX_MODE, GLCommand.ACTIVE_TEXTURE, GLCommand.USE_PROGRAM,
GLCommand.PUSH_ATTRIB, GLCommand.POP_ATTRIB, GLCommand.STENCIL_MASK,
GLCommand.DEPTH_MASK, GLCommand.FRONT_FACE, GLCommand.POINT_SIZE, GLCommand.LINE_WIDTH,
GLCommand.CALL_LIST, GLCommand.COMPLEX_REF, GLCommand.DRAW_RANGE, GLCommand.BIND_VBO,
GLCommand.BIND_VAO -> 8;
// Two int commands (12 bytes)
case GLCommand.BIND_TEXTURE, GLCommand.POLYGON_MODE, GLCommand.COLOR_MATERIAL,
GLCommand.LINE_STIPPLE, GLCommand.STENCIL_MASK_SEPARATE, GLCommand.FOGI,
GLCommand.HINT, GLCommand.POLYGON_OFFSET, GLCommand.ALPHA_FUNC, GLCommand.FOGF,
GLCommand.LIGHT_MODELF, GLCommand.LIGHT_MODELI -> 12;
// Three int commands (16 bytes)
case GLCommand.STENCIL_FUNC, GLCommand.STENCIL_OP, GLCommand.TEX_PARAMETERI,
GLCommand.LIGHTF, GLCommand.LIGHTI,
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF, GLCommand.DRAW_ARRAYS -> 16;
// Four int commands (20 bytes)
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
GLCommand.STENCIL_FUNC_SEPARATE, GLCommand.STENCIL_OP_SEPARATE,
GLCommand.SCISSOR,
GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.BLEND_COLOR -> 20;
case GLCommand.DRAW_ELEMENTS -> 24;
// DRAW_RANGE_RESTORE: 28 bytes
case GLCommand.DRAW_RANGE_RESTORE -> 28;
// DRAW_BUFFER: 8 bytes [cmd:4][mode:4]
case GLCommand.DRAW_BUFFER -> 8;
// DRAW_BUFFERS: 40 bytes [cmd:4][count:4][bufs:4*8]
case GLCommand.DRAW_BUFFERS -> 40;
// Double commands
case GLCommand.TRANSLATE, GLCommand.SCALE -> 28; // cmd + 3 doubles
case GLCommand.CLEAR_DEPTH -> 12; // cmd + 1 double
case GLCommand.ROTATE -> 36; // cmd + 4 doubles
case GLCommand.ORTHO, GLCommand.FRUSTUM -> 52; // cmd + 6 doubles
// Matrix commands
case GLCommand.MULT_MATRIX, GLCommand.LOAD_MATRIX -> 68; // cmd + 16 floats
// Buffer commands (variable, read count)
case GLCommand.FOG, GLCommand.LIGHT_MODEL -> {
final int count = memGetInt(ptr + 8); // pname at +4, count at +8
yield 12 + count * 4;
}
case GLCommand.LIGHT, GLCommand.MATERIAL -> {
final int count = memGetInt(ptr + 12); // light/face at +4, pname at +8, count at +12
yield 16 + count * 4;
}
case GLCommand.CLIP_PLANE -> 40; // cmd + plane + 4 doubles
default -> throw new IllegalStateException("Unknown command: " + cmd);
};
}
}
@@ -1,156 +1,102 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuad;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.gtnewhorizons.angelica.glsm.states.ClientArrayState;
import org.lwjgl.opengl.GL11;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Records immediate mode GL calls (glBegin/glEnd/glVertex) during display list compilation.
* Converts immediate mode geometry to ModelQuad format for VBO compilation.
*
* <p>Tracks current immediate mode state:
* <ul>
* <li>TexCoord: Set by glTexCoord* calls</li>
* <li>Normal: Set by glNormal* calls</li>
* <li>Color: Read from GLStateManager.getColor() at vertex emission time</li>
* </ul>
*
* <p>Supported primitive types:
* <ul>
* <li>GL_QUADS: 4 vertices per quad (native support)</li>
* <li>GL_TRIANGLES: 3 vertices per triangle (converted to degenerate quad)</li>
* <li>GL_TRIANGLE_FAN: Fan of triangles (converted to individual quads)</li>
* <li>GL_TRIANGLE_STRIP: Strip of triangles (converted to individual quads)</li>
* <li>GL_QUAD_STRIP: Strip of quads (converted to individual quads)</li>
* <li>GL_POLYGON: Polygon (treated as triangle fan)</li>
* </ul>
*
* <p>Unsupported primitive types (logged warning, geometry discarded):
* <ul>
* <li>GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP: Line primitives</li>
* <li>GL_POINTS: Point primitives</li>
* </ul>
* Records immediate mode GL calls (glBegin/glEnd/glVertex) via {@link DirectTessellator}.
* Used both during display list compilation and for live immediate mode emulation in core profile.
*/
public class ImmediateModeRecorder {
private static final Logger LOGGER = LogManager.getLogger("ImmediateModeRecorder");
public final class ImmediateModeRecorder {
private static final DirectTessellator tessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
// Accumulated quads from completed primitives
private final List<ModelQuadViewMutable> quads = new ArrayList<>();
/** Separate tessellator for the splash thread. Nulled after splash completes. */
private static DirectTessellator splashTessellator;
// Accumulated line vertices (pairs of vertices for GL_LINES)
private final List<LineVertex> lineVertices = new ArrayList<>();
private ImmediateModeRecorder() {
// Current primitive state
private int currentPrimitiveType = -1;
private boolean inPrimitive = false;
// Vertices for current primitive (cleared on glEnd)
private final List<ImmediateVertex> currentVertices = new ArrayList<>();
// Current immediate mode state (set by glTexCoord*, glNormal*)
private float texCoordS = 0.0f;
private float texCoordT = 0.0f;
private float normalX = 0.0f;
private float normalY = 0.0f;
private float normalZ = 1.0f; // Default normal points +Z
// Track which attributes have been set (for flags)
private boolean hasTexCoord = false;
private boolean hasNormal = false;
private boolean hasColor = false;
/**
* Represents a single vertex captured during immediate mode.
*/
private static class ImmediateVertex {
float x, y, z; // Position
int color; // Packed ABGR color
float texU, texV; // Texture coordinates
int normal; // Packed normal
ImmediateVertex(float x, float y, float z, int color, float texU, float texV, int normal) {
this.x = x;
this.y = y;
this.z = z;
this.color = color;
this.texU = texU;
this.texV = texV;
this.normal = normal;
}
}
/**
* Represents a vertex for line primitives.
* Simpler than ImmediateVertex - just position and color.
*/
public static class LineVertex {
public final float x, y, z;
public final int color; // Packed ABGR color
public LineVertex(float x, float y, float z, int color) {
this.x = x;
this.y = y;
this.z = z;
this.color = color;
}
public static void initSplashTessellator() {
splashTessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
}
public ImmediateModeRecorder() {
// Recorder is ready to capture geometry
public static void destroySplashTessellator() {
splashTessellator = null;
}
private static DirectTessellator tessellator() {
final DirectTessellator splash = splashTessellator;
if (splash != null && Thread.currentThread() != GLStateManager.getMainThread()) {
return splash;
}
return tessellator;
}
public static DirectTessellator getInternalTessellator() {
return tessellator();
}
public static boolean isDrawing() {
return tessellator().isDrawing;
}
/**
* Set the current texture coordinate.
* Called by GLStateManager.glTexCoord*.
*/
public void setTexCoord(float s, float t) {
this.texCoordS = s;
this.texCoordT = t;
this.hasTexCoord = true;
public static void setTexCoord(float s, float t) {
tessellator().setTextureUV(s, t);
}
/**
* Set the current normal vector.
* Called by GLStateManager.glNormal*.
*/
public void setNormal(float x, float y, float z) {
this.normalX = x;
this.normalY = y;
this.normalZ = z;
this.hasNormal = true;
public static void setNormal(float x, float y, float z) {
tessellator().setNormal(x, y, z);
}
/**
* Check if currently in a primitive (between glBegin and glEnd).
* Set the current color on the internal tessellator.
* Called by GLStateManager.glColor* when inside a glBegin/glEnd block.
*/
public boolean isInPrimitive() {
return inPrimitive;
public static void setColor(float r, float g, float b, float a) {
tessellator().setColorRGBA((int) (r * 255.0f), (int) (g * 255.0f), (int) (b * 255.0f), (int) (a * 255.0f));
}
/**
* Start recording a new primitive.
* Resets the tessellator to clear any data from the previous draw.
*
* @param primitiveType GL primitive type (GL_QUADS, GL_TRIANGLES, etc.)
*/
public void begin(int primitiveType) {
if (inPrimitive) {
throw new IllegalStateException("glBegin called while already in primitive");
}
this.currentPrimitiveType = primitiveType;
this.currentVertices.clear();
this.inPrimitive = true;
public static void begin(int primitiveType) {
final DirectTessellator t = tessellator();
t.reset();
t.startDrawing(primitiveType);
}
/**
* Start recording a new primitive with initial color sync from GLSM cache.
* Used for live immediate mode emulation only (not display list recording).
*/
public static void beginLive(int primitiveType) {
final DirectTessellator t = tessellator();
t.reset();
t.startDrawing(primitiveType);
final var c = GLStateManager.getColor();
t.setColorRGBA((int) (c.getRed() * 255.0f), (int) (c.getGreen() * 255.0f), (int) (c.getBlue() * 255.0f), (int) (c.getAlpha() * 255.0f));
}
/**
@@ -159,346 +105,29 @@ public class ImmediateModeRecorder {
*
* @return Result containing quads, lines, and flags, or null if no geometry was produced
*/
public Result end() {
if (!inPrimitive) {
public static DirectTessellator end() {
final DirectTessellator t = tessellator();
if (!t.isDrawing) {
throw new IllegalStateException("glEnd called without glBegin");
}
// Capture last vertex attributes BEFORE clearing currentVertices
// These will be used to restore GL current state after VBO playback
float lastColorR = 1.0f, lastColorG = 1.0f, lastColorB = 1.0f, lastColorA = 1.0f;
float lastNormalX = 0.0f, lastNormalY = 0.0f, lastNormalZ = 1.0f;
float lastTexS = 0.0f, lastTexT = 0.0f;
t.isDrawing = false;
if (!currentVertices.isEmpty()) {
ImmediateVertex last = currentVertices.get(currentVertices.size() - 1);
if (t.isEmpty()) return null;
// Unpack color from ABGR format
lastColorR = (last.color & 0xFF) / 255.0f;
lastColorG = ((last.color >> 8) & 0xFF) / 255.0f;
lastColorB = ((last.color >> 16) & 0xFF) / 255.0f;
lastColorA = ((last.color >> 24) & 0xFF) / 255.0f;
// Unpack normal from NormI8 format (signed bytes packed into int)
lastNormalX = NormI8.unpackX(last.normal);
lastNormalY = NormI8.unpackY(last.normal);
lastNormalZ = NormI8.unpackZ(last.normal);
// Texture coords are stored directly as floats
lastTexS = last.texU;
lastTexT = last.texV;
}
// Convert this primitive's vertices to quads or lines
convertPrimitiveToQuads();
this.currentVertices.clear();
this.inPrimitive = false;
// Return geometry from this primitive immediately (don't accumulate)
if (quads.isEmpty() && lineVertices.isEmpty()) {
return null;
}
// Copy geometry and create flags
List<ModelQuadViewMutable> resultQuads = new ArrayList<>(quads);
List<LineVertex> resultLines = new ArrayList<>(lineVertices);
CapturingTessellator.Flags flags = new CapturingTessellator.Flags(
hasTexCoord, // hasTexture
false, // hasBrightness - immediate mode doesn't typically use lightmap
hasColor, // hasColor
hasNormal // hasNormals
);
// Clear for next primitive (but keep attribute tracking state)
quads.clear();
lineVertices.clear();
return new Result(resultQuads, resultLines, flags,
lastColorR, lastColorG, lastColorB, lastColorA,
lastNormalX, lastNormalY, lastNormalZ,
lastTexS, lastTexT);
return t;
}
/**
* Record a vertex with current attributes.
* Color is read from GLStateManager at call time.
*/
public void vertex(float x, float y, float z) {
if (!inPrimitive) {
public static void vertex(float x, float y, float z) {
final DirectTessellator t = tessellator();
if (!t.isDrawing) {
throw new IllegalStateException("glVertex called outside glBegin/glEnd");
}
// Get current color from GLStateManager
int packedColor = packColor(
GLStateManager.getColor().getRed(),
GLStateManager.getColor().getGreen(),
GLStateManager.getColor().getBlue(),
GLStateManager.getColor().getAlpha()
);
if (packedColor != 0xFFFFFFFF) {
hasColor = true;
}
// Pack normal
int packedNormal = NormI8.pack(normalX, normalY, normalZ);
currentVertices.add(new ImmediateVertex(
x, y, z,
packedColor,
texCoordS, texCoordT,
packedNormal
));
}
/**
* Pack RGBA floats (0-1) into ABGR int format used by ModelQuad.
*/
private static int packColor(float r, float g, float b, float a) {
int ri = (int) (r * 255.0f) & 0xFF;
int gi = (int) (g * 255.0f) & 0xFF;
int bi = (int) (b * 255.0f) & 0xFF;
int ai = (int) (a * 255.0f) & 0xFF;
// ABGR format (little-endian: stored as RGBA bytes)
return (ai << 24) | (bi << 16) | (gi << 8) | ri;
}
/**
* Convert the current primitive vertices to quads.
* Handles different primitive types.
*/
private void convertPrimitiveToQuads() {
int vertexCount = currentVertices.size();
if (vertexCount == 0) {
return;
}
switch (currentPrimitiveType) {
case GL11.GL_QUADS -> convertQuads();
case GL11.GL_TRIANGLES -> convertTriangles();
case GL11.GL_TRIANGLE_FAN, GL11.GL_POLYGON -> convertTriangleFan();
case GL11.GL_TRIANGLE_STRIP -> convertTriangleStrip();
case GL11.GL_QUAD_STRIP -> convertQuadStrip();
case GL11.GL_LINES -> convertLines();
case GL11.GL_LINE_STRIP -> convertLineStripToLines();
case GL11.GL_LINE_LOOP -> convertLineLoopToLines();
case GL11.GL_POINTS -> {
LOGGER.warn("[ImmediateModeRecorder] Point primitives not supported, discarding {} vertices", vertexCount);
}
default -> {
LOGGER.warn("[ImmediateModeRecorder] Unknown primitive type {}, discarding {} vertices",
currentPrimitiveType, vertexCount);
}
}
}
/**
* Convert GL_QUADS vertices to ModelQuads (4 vertices per quad).
*/
private void convertQuads() {
int vertexCount = currentVertices.size();
int fullQuads = vertexCount / 4;
for (int i = 0; i < fullQuads; i++) {
int base = i * 4;
addQuad(
currentVertices.get(base),
currentVertices.get(base + 1),
currentVertices.get(base + 2),
currentVertices.get(base + 3)
);
}
int remaining = vertexCount % 4;
if (remaining > 0) {
LOGGER.warn("[ImmediateModeRecorder] GL_QUADS: Incomplete quad, discarding {} vertices", remaining);
}
}
/**
* Convert GL_TRIANGLES vertices to degenerate quads (3 vertices per triangle, duplicate last).
*/
private void convertTriangles() {
int vertexCount = currentVertices.size();
int fullTriangles = vertexCount / 3;
for (int i = 0; i < fullTriangles; i++) {
int base = i * 3;
ImmediateVertex v0 = currentVertices.get(base);
ImmediateVertex v1 = currentVertices.get(base + 1);
ImmediateVertex v2 = currentVertices.get(base + 2);
// Degenerate quad: duplicate third vertex
addQuad(v0, v1, v2, v2);
}
int remaining = vertexCount % 3;
if (remaining > 0) {
LOGGER.warn("[ImmediateModeRecorder] GL_TRIANGLES: Incomplete triangle, discarding {} vertices", remaining);
}
}
/**
* Convert GL_TRIANGLE_FAN vertices to triangles then to degenerate quads.
* First vertex is center, subsequent vertices form fan.
*/
private void convertTriangleFan() {
int vertexCount = currentVertices.size();
if (vertexCount < 3) {
LOGGER.warn("[ImmediateModeRecorder] GL_TRIANGLE_FAN: Need at least 3 vertices, got {}", vertexCount);
return;
}
ImmediateVertex center = currentVertices.get(0);
for (int i = 1; i < vertexCount - 1; i++) {
ImmediateVertex v1 = currentVertices.get(i);
ImmediateVertex v2 = currentVertices.get(i + 1);
// Triangle: center, v1, v2 -> degenerate quad
addQuad(center, v1, v2, v2);
}
}
/**
* Convert GL_TRIANGLE_STRIP vertices to triangles then to degenerate quads.
*/
private void convertTriangleStrip() {
int vertexCount = currentVertices.size();
if (vertexCount < 3) {
LOGGER.warn("[ImmediateModeRecorder] GL_TRIANGLE_STRIP: Need at least 3 vertices, got {}", vertexCount);
return;
}
for (int i = 0; i < vertexCount - 2; i++) {
ImmediateVertex v0, v1, v2;
if ((i & 1) == 0) {
// Even triangle: v[i], v[i+1], v[i+2]
v0 = currentVertices.get(i);
v1 = currentVertices.get(i + 1);
v2 = currentVertices.get(i + 2);
} else {
// Odd triangle: v[i+1], v[i], v[i+2] (reverse winding)
v0 = currentVertices.get(i + 1);
v1 = currentVertices.get(i);
v2 = currentVertices.get(i + 2);
}
addQuad(v0, v1, v2, v2);
}
}
/**
* Convert GL_QUAD_STRIP vertices to quads.
* Vertices come in pairs: (v0,v1), (v2,v3), (v4,v5)...
* Each quad uses 4 consecutive vertices in pattern: v[2i], v[2i+1], v[2i+3], v[2i+2]
*/
private void convertQuadStrip() {
int vertexCount = currentVertices.size();
if (vertexCount < 4) {
LOGGER.warn("[ImmediateModeRecorder] GL_QUAD_STRIP: Need at least 4 vertices, got {}", vertexCount);
return;
}
int numQuads = (vertexCount - 2) / 2;
for (int i = 0; i < numQuads; i++) {
int base = i * 2;
// Quad strip winding: v[0], v[1], v[3], v[2]
addQuad(
currentVertices.get(base),
currentVertices.get(base + 1),
currentVertices.get(base + 3),
currentVertices.get(base + 2)
);
}
}
/**
* Convert GL_LINES vertices to line segments (pairs of vertices).
*/
private void convertLines() {
int vertexCount = currentVertices.size();
int fullLines = vertexCount / 2;
for (int i = 0; i < fullLines; i++) {
int base = i * 2;
ImmediateVertex v0 = currentVertices.get(base);
ImmediateVertex v1 = currentVertices.get(base + 1);
lineVertices.add(new LineVertex(v0.x, v0.y, v0.z, v0.color));
lineVertices.add(new LineVertex(v1.x, v1.y, v1.z, v1.color));
}
int remaining = vertexCount % 2;
if (remaining > 0) {
LOGGER.warn("[ImmediateModeRecorder] GL_LINES: Incomplete line, discarding {} vertices", remaining);
}
}
/**
* Convert GL_LINE_STRIP to individual line segments.
* Vertices 0-1-2-3 become lines: 0-1, 1-2, 2-3
*/
private void convertLineStripToLines() {
int vertexCount = currentVertices.size();
if (vertexCount < 2) {
LOGGER.warn("[ImmediateModeRecorder] GL_LINE_STRIP: Need at least 2 vertices, got {}", vertexCount);
return;
}
for (int i = 0; i < vertexCount - 1; i++) {
ImmediateVertex v0 = currentVertices.get(i);
ImmediateVertex v1 = currentVertices.get(i + 1);
lineVertices.add(new LineVertex(v0.x, v0.y, v0.z, v0.color));
lineVertices.add(new LineVertex(v1.x, v1.y, v1.z, v1.color));
}
}
/**
* Convert GL_LINE_LOOP to individual line segments.
* Like LINE_STRIP but also closes the loop from last to first vertex.
*/
private void convertLineLoopToLines() {
int vertexCount = currentVertices.size();
if (vertexCount < 2) {
LOGGER.warn("[ImmediateModeRecorder] GL_LINE_LOOP: Need at least 2 vertices, got {}", vertexCount);
return;
}
// Convert as strip first
convertLineStripToLines();
// Close the loop: add last-to-first segment
ImmediateVertex vLast = currentVertices.get(vertexCount - 1);
ImmediateVertex vFirst = currentVertices.get(0);
lineVertices.add(new LineVertex(vLast.x, vLast.y, vLast.z, vLast.color));
lineVertices.add(new LineVertex(vFirst.x, vFirst.y, vFirst.z, vFirst.color));
}
/**
* Create a ModelQuad from 4 ImmediateVertex objects.
*/
private void addQuad(ImmediateVertex v0, ImmediateVertex v1, ImmediateVertex v2, ImmediateVertex v3) {
ModelQuad quad = new ModelQuad();
setVertex(quad, 0, v0);
setVertex(quad, 1, v1);
setVertex(quad, 2, v2);
setVertex(quad, 3, v3);
// Set light face based on computed normal
quad.setLightFace(ModelQuadUtil.findLightFace(quad.getComputedFaceNormal()));
quads.add(quad);
}
/**
* Set a single vertex in a ModelQuad.
*/
private void setVertex(ModelQuad quad, int idx, ImmediateVertex v) {
quad.setX(idx, v.x);
quad.setY(idx, v.y);
quad.setZ(idx, v.z);
quad.setColor(idx, v.color);
quad.setTexU(idx, v.texU);
quad.setTexV(idx, v.texV);
quad.setForgeNormal(idx, v.normal);
// Light defaults to full brightness (no lightmap for immediate mode typically)
quad.setLight(idx, ModelQuadUtil.DEFAULT_LIGHTMAP);
t.addVertex(x, y, z);
}
/**
@@ -508,87 +137,16 @@ public class ImmediateModeRecorder {
* is returned from end() immediately. This method is kept for edge cases
* where primitives might fail to convert (e.g., incomplete quads).
*/
public boolean hasGeometry() {
return !quads.isEmpty() || !lineVertices.isEmpty();
public static boolean hasGeometry() {
return tessellator().vertexCount != 0;
}
/**
* Get any remaining accumulated geometry and clear the recorder.
*
* <p>Note: With immediate flush mode, this will typically return null as geometry
* is returned from end() immediately. This method is kept for cleanup and
* edge cases where geometry might remain (e.g., if end() was never called).
*
* @return Result containing remaining quads, lines, and flags, or null if no geometry
*/
public Result getQuadsAndClear() {
if (inPrimitive) {
throw new IllegalStateException("Cannot get geometry while in primitive (missing glEnd)");
}
if (quads.isEmpty() && lineVertices.isEmpty()) {
return null;
}
// Copy geometry and create flags
List<ModelQuadViewMutable> resultQuads = new ArrayList<>(quads);
List<LineVertex> resultLines = new ArrayList<>(lineVertices);
CapturingTessellator.Flags flags = new CapturingTessellator.Flags(
hasTexCoord, // hasTexture
false, // hasBrightness - immediate mode doesn't typically use lightmap
hasColor, // hasColor
hasNormal // hasNormals
);
// Clear state for reuse
quads.clear();
lineVertices.clear();
hasColor = false;
hasTexCoord = false;
hasNormal = false;
// Note: current texcoord/normal values are NOT reset - they persist per OpenGL spec
// For getQuadsAndClear, we don't have last vertex data (it was cleared in end())
// Use default values - this method is for edge cases/cleanup anyway
return new Result(resultQuads, resultLines, flags,
1.0f, 1.0f, 1.0f, 1.0f, // Default white color
0.0f, 0.0f, 1.0f, // Default +Z normal
0.0f, 0.0f); // Default (0,0) texcoord
}
/**
* Result of immediate mode recording: quads, lines, attribute flags, and last vertex state.
* The last vertex state is used to restore GL_CURRENT_COLOR etc. after VBO playback.
*/
@com.github.bsideup.jabel.Desugar
public record Result(
List<ModelQuadViewMutable> quads,
List<LineVertex> lines,
CapturingTessellator.Flags flags,
// Last vertex attributes for restoration after VBO draw
float lastColorR, float lastColorG, float lastColorB, float lastColorA,
float lastNormalX, float lastNormalY, float lastNormalZ,
float lastTexCoordS, float lastTexCoordT
) {}
/**
* Reset all state including current texcoord/normal.
* Called when starting a new display list.
*/
public void reset() {
quads.clear();
lineVertices.clear();
currentVertices.clear();
inPrimitive = false;
currentPrimitiveType = -1;
texCoordS = 0.0f;
texCoordT = 0.0f;
normalX = 0.0f;
normalY = 0.0f;
normalZ = 1.0f;
hasColor = false;
hasTexCoord = false;
hasNormal = false;
public static void reset() {
tessellator().reset();
}
/**
@@ -600,17 +158,23 @@ public class ImmediateModeRecorder {
* @param count Number of vertices
* @return Result containing converted geometry, or null if no geometry produced
*/
public Result processDrawArrays(int mode, int first, int count) {
final var cas = GLStateManager.getClientArrayState();
final var vertexPointer = cas.isVertexArrayEnabled() ? cas.getVertexPointer() : null;
final var colorPointer = cas.isColorArrayEnabled() ? cas.getColorPointer() : null;
public static DirectTessellator processDrawArrays(int mode, int first, int count) {
final ClientArrayState cas = GLStateManager.getClientArrayState();
final Buffer vertexPointer = cas.isVertexArrayEnabled() ? cas.getVertexPointer() : null;
final Buffer colorPointer = cas.isColorArrayEnabled() ? cas.getColorPointer() : null;
final Buffer normalPointer = cas.isNormalArrayEnabled() ? cas.getNormalPointer() : null;
final Buffer texCoordPointer = cas.isTexCoordArrayEnabled() ? cas.getTexCoordPointer() : null;
return convertClientArrays(
mode, first, count,
vertexPointer,
cas.getVertexPointerType(), cas.getVertexPointerSize(), cas.getVertexPointerStride(),
colorPointer,
cas.getColorPointerType(), cas.getColorPointerSize(), cas.getColorPointerStride()
cas.getColorPointerType(), cas.getColorPointerSize(), cas.getColorPointerStride(),
normalPointer,
cas.getNormalPointerType(), cas.getNormalPointerStride(),
texCoordPointer,
cas.getTexCoordPointerType(), cas.getTexCoordPointerSize(), cas.getTexCoordPointerStride()
);
}
@@ -618,39 +182,27 @@ public class ImmediateModeRecorder {
* Convert client array data to immediate mode.
* Called by glDrawArrays during display list recording.
* Like Mesa's save_DrawArrays, reads vertex data from arrays and emits as immediate mode.
*
* @param mode Primitive type (GL_QUADS, GL_LINES, etc.)
* @param first Starting index in arrays
* @param count Number of vertices
* @param vertexPointer Vertex position buffer (or null if not enabled)
* @param vertexType GL type (GL_FLOAT, GL_DOUBLE, etc.)
* @param vertexSize Size of vertex (2, 3, or 4)
* @param vertexStride Stride in bytes (0 for tightly packed)
* @param colorPointer Color buffer (or null if not enabled)
* @param colorType GL type (GL_FLOAT, GL_UNSIGNED_BYTE, etc.)
* @param colorSize Size of color (3 or 4)
* @param colorStride Stride in bytes (0 for tightly packed)
* @return Result containing converted geometry, or null if no geometry produced
*/
public Result convertClientArrays(
public static DirectTessellator convertClientArrays(
int mode, int first, int count,
java.nio.Buffer vertexPointer, int vertexType, int vertexSize, int vertexStride,
java.nio.Buffer colorPointer, int colorType, int colorSize, int colorStride
java.nio.Buffer colorPointer, int colorType, int colorSize, int colorStride,
java.nio.Buffer normalPointer, int normalType, int normalStride,
java.nio.Buffer texCoordPointer, int texCoordType, int texCoordSize, int texCoordStride
) {
begin(mode);
final DirectTessellator t = tessellator();
// Hoist type checks and stride calculations outside the loop for performance.
// Mesa's algorithm: offset_bytes = index * effective_stride
// effective_stride = (stride == 0) ? (numComponents * sizeof(element)) : stride
// Determine color reader (hoisted out of loop)
final ColorReader colorReader;
final int colorEffectiveStride;
// Determine color reader
final ArrayReader colorReader;
if (colorPointer == null) {
colorReader = null;
colorEffectiveStride = 0;
} else if (colorType == GL11.GL_FLOAT && colorPointer instanceof FloatBuffer fb) {
colorEffectiveStride = (colorStride == 0) ? colorSize * 4 : colorStride;
final int colorEffectiveStride = (colorStride == 0) ? colorSize * 4 : colorStride;
final int strideInFloats = colorEffectiveStride / 4;
colorReader = (idx) -> {
final int offset = idx * strideInFloats;
@@ -661,7 +213,7 @@ public class ImmediateModeRecorder {
GLStateManager.glColor4f(r, g, b, a);
};
} else if ((colorType == GL11.GL_UNSIGNED_BYTE || colorType == GL11.GL_BYTE) && colorPointer instanceof ByteBuffer bb) {
colorEffectiveStride = (colorStride == 0) ? colorSize : colorStride;
final int colorEffectiveStride = (colorStride == 0) ? colorSize : colorStride;
colorReader = (idx) -> {
final int offset = idx * colorEffectiveStride;
final float r = (bb.get(offset) & 0xFF) / 255.0f;
@@ -672,12 +224,49 @@ public class ImmediateModeRecorder {
};
} else {
// Unsupported color type - use white
colorEffectiveStride = 0;
colorReader = (idx) -> GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
// Determine vertex reader (hoisted out of loop)
final VertexReader vertexReader;
// Determine normal reader
final ArrayReader normalReader;
if (normalPointer == null) {
normalReader = null;
} else if (normalType == GL11.GL_FLOAT && normalPointer instanceof FloatBuffer fb) {
final int effectiveStride = (normalStride == 0) ? 3 * 4 : normalStride;
final int strideInFloats = effectiveStride / 4;
normalReader = (idx) -> {
final int offset = idx * strideInFloats;
setNormal(fb.get(offset), fb.get(offset + 1), fb.get(offset + 2));
};
} else if (normalType == GL11.GL_BYTE && normalPointer instanceof ByteBuffer bb) {
final int effectiveStride = (normalStride == 0) ? 3 : normalStride;
normalReader = (idx) -> {
final int offset = idx * effectiveStride;
setNormal(bb.get(offset) / 127.0f, bb.get(offset + 1) / 127.0f, bb.get(offset + 2) / 127.0f);
};
} else {
normalReader = null;
}
// Determine texcoord reader
final ArrayReader texCoordReader;
if (texCoordPointer == null) {
texCoordReader = null;
} else if (texCoordType == GL11.GL_FLOAT && texCoordPointer instanceof FloatBuffer fb) {
final int effectiveStride = (texCoordStride == 0) ? texCoordSize * 4 : texCoordStride;
final int strideInFloats = effectiveStride / 4;
texCoordReader = (idx) -> {
final int offset = idx * strideInFloats;
final float s = fb.get(offset);
final float tt = (texCoordSize >= 2) ? fb.get(offset + 1) : 0.0f;
setTexCoord(s, tt);
};
} else {
texCoordReader = null;
}
// Determine vertex reader
final ArrayReader vertexReader;
if (vertexPointer == null) {
vertexReader = null;
} else if (vertexType == GL11.GL_FLOAT && vertexPointer instanceof FloatBuffer fb) {
@@ -688,7 +277,7 @@ public class ImmediateModeRecorder {
final float x = fb.get(offset);
final float y = fb.get(offset + 1);
final float z = (vertexSize >= 3) ? fb.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_DOUBLE && vertexPointer instanceof DoubleBuffer db) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 8 : vertexStride;
@@ -698,7 +287,7 @@ public class ImmediateModeRecorder {
final float x = (float) db.get(offset);
final float y = (float) db.get(offset + 1);
final float z = (vertexSize >= 3) ? (float) db.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_INT && vertexPointer instanceof IntBuffer ib) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 4 : vertexStride;
@@ -708,7 +297,7 @@ public class ImmediateModeRecorder {
final float x = ib.get(offset);
final float y = ib.get(offset + 1);
final float z = (vertexSize >= 3) ? ib.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_SHORT && vertexPointer instanceof ShortBuffer sb) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 2 : vertexStride;
@@ -718,7 +307,7 @@ public class ImmediateModeRecorder {
final float x = sb.get(offset);
final float y = sb.get(offset + 1);
final float z = (vertexSize >= 3) ? sb.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else {
// Unsupported vertex type
@@ -727,6 +316,12 @@ public class ImmediateModeRecorder {
// Main loop - no type checks, just function calls
for (int i = first; i < first + count; i++) {
if (normalReader != null) {
normalReader.read(i);
}
if (texCoordReader != null) {
texCoordReader.read(i);
}
if (colorReader != null) {
colorReader.read(i);
}
@@ -739,12 +334,7 @@ public class ImmediateModeRecorder {
}
@FunctionalInterface
private interface ColorReader {
void read(int index);
}
@FunctionalInterface
private interface VertexReader {
private interface ArrayReader {
void read(int index);
}
}
@@ -17,22 +17,4 @@ public interface DisplayListCommand {
default void delete() {
// Most commands don't hold resources
}
/**
* Handle this command during display list optimization.
*
* <p>Commands override this to implement their optimization behavior:
* <ul>
* <li>Matrix transforms: accumulate into the optimizer, return false</li>
* <li>Push/Pop: update stack, return true to emit</li>
* <li>Barriers: emit pending transforms, return true to emit</li>
* <li>State changes: return true to emit</li>
* </ul>
*
* @param ctx The optimization context providing access to transform state
* @return true if this command should be added to output, false if handled internally
*/
default boolean handleOptimization(OptimizationContext ctx) {
return true;
}
}
@@ -1,14 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.github.bsideup.jabel.Desugar;
/**
* Represents a single draw call within a compiled display list.
* Each DrawCommand corresponds to one glBegin/glEnd block.
*/
@Desugar
public record DrawCommand(
int firstVertex,
int vertexCount,
int primitiveType
) {}
@@ -1,42 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import org.jetbrains.annotations.NotNull;
/**
* Command: Draw a range of vertices from a shared VBO.
* <p>
* DrawRangeCmd references a shared VBO owned by CompiledDisplayList.ownedVbos.
* Multiple DrawRangeCmds can reference different ranges within the same VBO,
* enabling format-based batching where all draws with the same vertex format
* share a single VBO.
* <p>
* Transform is NOT stored here - handled by TransformOptimizer emitting
* MultMatrixCmd before this command during optimization.
*/
@Desugar
public record DrawRangeCmd(
VertexBuffer sharedVbo,
int startVertex,
int vertexCount,
boolean hasBrightness
) implements DisplayListCommand {
@Override
public void execute() {
sharedVbo.setupState();
sharedVbo.draw(startVertex, vertexCount);
sharedVbo.cleanupState();
}
@Override
public void delete() {
// No-op: VBO lifetime is managed by CompiledDisplayList.ownedVbos
}
@Override
public @NotNull String toString() {
return "DrawRange(start=" + startVertex + ", count=" + vertexCount + ", hasBrightness=" + hasBrightness + ")";
}
}
@@ -5,7 +5,6 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.jetbrains.annotations.NotNull;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
@@ -1,42 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import org.joml.Matrix4f;
/**
* Context provided to commands during display list optimization.
* Allows commands to interact with the transform optimizer and draw batcher.
*/
public interface OptimizationContext {
/** Get the accumulated MODELVIEW transform matrix. */
Matrix4f getAccumulatedTransform();
/** Reset accumulated transform to identity. */
void loadIdentity();
/** Push current accumulated transform onto stack. */
void pushTransform();
/** Pop transform from stack, restoring previous accumulated value. */
void popTransform();
/** Emit a MultMatrix command if accumulated differs from last emitted. */
void emitPendingTransform();
/** Emit a MultMatrix command to reach the target transform. */
void emitTransformTo(Matrix4f target);
/** Add a command to the output list. */
void emit(DisplayListCommand cmd);
/**
* Mark that the GL matrix is at an absolute value (after LoadMatrix).
* Subsequent LoadIdentity commands must be emitted to reset to identity.
*/
void markAbsoluteMatrix();
/**
* Check if the GL matrix is at an absolute value and clear the flag.
* @return true if LoadMatrix was called and not yet reset by LoadIdentity
*/
boolean checkAndClearAbsoluteMatrix();
}
@@ -11,6 +11,7 @@ import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
/**
* Command: glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels)
@@ -26,18 +27,35 @@ public record TexImage2DCmd(
int border,
int format,
int type,
@Nullable byte[] pixelData
byte @Nullable [] pixelData
) implements DisplayListCommand {
/**
* Create command from ShortBuffer
*/
public static TexImage2DCmd fromShortBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ShortBuffer pixels) {
byte[] data = null;
if (pixels != null) {
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 2; // 2 bytes per short
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asShortBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data);
}
/**
* Create command from IntBuffer
*/
public static TexImage2DCmd fromIntBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per int
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per int
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asIntBuffer().put(pixels);
@@ -52,9 +70,9 @@ public record TexImage2DCmd(
public static TexImage2DCmd fromFloatBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable FloatBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per float
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per float
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asFloatBuffer().put(pixels);
@@ -69,9 +87,9 @@ public record TexImage2DCmd(
public static TexImage2DCmd fromDoubleBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable DoubleBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 8; // 8 bytes per double
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 8; // 8 bytes per double
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asDoubleBuffer().put(pixels);
@@ -86,9 +104,9 @@ public record TexImage2DCmd(
public static TexImage2DCmd fromByteBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = limit - pos;
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = limit - pos;
data = new byte[size];
pixels.get(data);
pixels.position(pos); // Restore position
@@ -109,7 +127,7 @@ public record TexImage2DCmd(
@Override
public @NotNull String toString() {
String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
final String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
return String.format("TexImage2D(target=%s, level=%d, internalformat=%s, %dx%d, border=%d, format=%s, type=%s, data=%s)",
GLDebug.getTextureTargetName(target), level, GLDebug.getTextureFormatName(internalformat),
width, height, border, GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr);
@@ -11,6 +11,7 @@ import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
/**
* Command: glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels)
@@ -26,18 +27,35 @@ public record TexSubImage2DCmd(
int height,
int format,
int type,
@Nullable byte[] pixelData
byte @Nullable [] pixelData
) implements DisplayListCommand {
/**
* Create command from ShortBuffer
*/
public static TexSubImage2DCmd fromShortBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ShortBuffer pixels) {
byte[] data = null;
if (pixels != null) {
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 2; // 2 bytes per short
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asShortBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data);
}
/**
* Create command from IntBuffer
*/
public static TexSubImage2DCmd fromIntBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable IntBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per int
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per int
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asIntBuffer().put(pixels);
@@ -52,9 +70,9 @@ public record TexSubImage2DCmd(
public static TexSubImage2DCmd fromFloatBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable FloatBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per float
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per float
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asFloatBuffer().put(pixels);
@@ -69,9 +87,9 @@ public record TexSubImage2DCmd(
public static TexSubImage2DCmd fromDoubleBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable DoubleBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 8; // 8 bytes per double
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 8; // 8 bytes per double
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asDoubleBuffer().put(pixels);
@@ -86,9 +104,9 @@ public record TexSubImage2DCmd(
public static TexSubImage2DCmd fromByteBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ByteBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = limit - pos;
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = limit - pos;
data = new byte[size];
pixels.get(data);
pixels.position(pos); // Restore position
@@ -109,7 +127,7 @@ public record TexSubImage2DCmd(
@Override
public @NotNull String toString() {
String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
final String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
return String.format("TexSubImage2D(target=%s, level=%d, offset=(%d,%d), %dx%d, format=%s, type=%s, data=%s)",
GLDebug.getTextureTargetName(target), level, xoffset, yoffset, width, height,
GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr);
@@ -22,8 +22,7 @@ public class BooleanStateStack extends BooleanState implements IStateStack<Boole
protected int savedDepth;
public BooleanStateStack(int glCap) {
// Most booleans default to false
this(glCap, false);
this(glCap, false, false);
}
/**
@@ -34,7 +33,18 @@ public class BooleanStateStack extends BooleanState implements IStateStack<Boole
* @param initialState initial enabled state
*/
public BooleanStateStack(int glCap, boolean initialState) {
super(glCap);
this(glCap, initialState, false);
}
/**
* Create a BooleanStateStack with custom initial state and FFP flag.
*
* @param glCap GL capability constant
* @param initialState initial enabled state
* @param ffpStateOnly when true, GL calls are skipped when FFP emulation is active
*/
public BooleanStateStack(int glCap, boolean initialState, boolean ffpStateOnly) {
super(glCap, ffpStateOnly);
this.enabled = initialState;
stack = new boolean[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
}
@@ -12,12 +12,12 @@ public class TextureUnitBooleanStateStack extends BooleanStateStack {
private final int unitIndex;
public TextureUnitBooleanStateStack(int glCap, int unitIndex) {
super(glCap);
super(glCap, false, true); // All texture enable/disable states are FFP-only
this.unitIndex = unitIndex;
}
public TextureUnitBooleanStateStack(int glCap, int unitIndex, boolean initialState) {
super(glCap, initialState);
super(glCap, initialState, true);
this.unitIndex = unitIndex;
}
@@ -41,12 +41,14 @@ public class TextureUnitBooleanStateStack extends BooleanStateStack {
if (bypass || enabled != this.enabled) {
if (!bypass) this.enabled = enabled;
int currentUnit = GLStateManager.getActiveTextureUnit();
boolean needsSwitch = currentUnit != unitIndex;
if (!ffpStateOnly) {
final int currentUnit = GLStateManager.getActiveTextureUnit();
final boolean needsSwitch = currentUnit != unitIndex;
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + unitIndex);
if (enabled) GL11.glEnable(glCap); else GL11.glDisable(glCap);
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + currentUnit);
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + unitIndex);
if (enabled) GL11.glEnable(glCap); else GL11.glDisable(glCap);
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + currentUnit);
}
}
}
@@ -0,0 +1,47 @@
package com.gtnewhorizons.angelica.glsm.stacks;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Vector3f;
/**
* Stack for a Vector3f value (e.g. current normal).
*/
public class Vec3fStack implements IStateStack<Vec3fStack> {
private final Vector3f value;
private final float[][] stack;
private int pointer;
public Vec3fStack(Vector3f value) {
this.value = value;
stack = new float[GLStateManager.MAX_ATTRIB_STACK_DEPTH][3];
}
@Override
public Vec3fStack push() {
if (pointer == stack.length) {
throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached");
}
stack[pointer][0] = value.x;
stack[pointer][1] = value.y;
stack[pointer][2] = value.z;
pointer++;
return this;
}
@Override
public Vec3fStack pop() {
if (pointer == 0) {
throw new IllegalStateException("Stack underflow");
}
pointer--;
value.set(stack[pointer][0], stack[pointer][1], stack[pointer][2]);
return this;
}
@Override
public boolean isEmpty() {
return pointer == 0;
}
}
@@ -0,0 +1,48 @@
package com.gtnewhorizons.angelica.glsm.stacks;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Vector4f;
/**
* Stack for a Vector4f value (e.g. current texture coordinates).
*/
public class Vec4fStack implements IStateStack<Vec4fStack> {
private final Vector4f value;
private final float[][] stack;
private int pointer;
public Vec4fStack(Vector4f value) {
this.value = value;
stack = new float[GLStateManager.MAX_ATTRIB_STACK_DEPTH][4];
}
@Override
public Vec4fStack push() {
if (pointer == stack.length) {
throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached");
}
stack[pointer][0] = value.x;
stack[pointer][1] = value.y;
stack[pointer][2] = value.z;
stack[pointer][3] = value.w;
pointer++;
return this;
}
@Override
public Vec4fStack pop() {
if (pointer == 0) {
throw new IllegalStateException("Stack underflow");
}
pointer--;
value.set(stack[pointer][0], stack[pointer][1], stack[pointer][2], stack[pointer][3]);
return this;
}
@Override
public boolean isEmpty() {
return pointer == 0;
}
}
@@ -7,7 +7,7 @@ import org.lwjgl.opengl.GL11;
@Getter
public class AlphaState implements ISettableState<AlphaState> {
@Setter protected int function = GL11.GL_ALWAYS;
@Setter protected float reference = -1.0F;
@Setter protected float reference = 0.0F;
@Override
public AlphaState set(AlphaState state) {
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.states;
import lombok.Getter;
import lombok.Setter;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL14;
@Getter @Setter
@@ -11,6 +12,12 @@ public class BlendState implements ISettableState<BlendState> {
protected int dstRgb = GL11.GL_ZERO;
protected int srcAlpha = GL11.GL_ONE;
protected int dstAlpha = GL11.GL_ZERO;
protected int equationRgb = GL14.GL_FUNC_ADD;
protected int equationAlpha = GL14.GL_FUNC_ADD;
protected float blendColorR = 0.0f;
protected float blendColorG = 0.0f;
protected float blendColorB = 0.0f;
protected float blendColorA = 0.0f;
public BlendState() {}
@@ -39,6 +46,12 @@ public class BlendState implements ISettableState<BlendState> {
this.dstRgb = state.dstRgb;
this.srcAlpha = state.srcAlpha;
this.dstAlpha = state.dstAlpha;
this.equationRgb = state.equationRgb;
this.equationAlpha = state.equationAlpha;
this.blendColorR = state.blendColorR;
this.blendColorG = state.blendColorG;
this.blendColorB = state.blendColorB;
this.blendColorA = state.blendColorA;
return this;
}
@@ -46,7 +59,13 @@ public class BlendState implements ISettableState<BlendState> {
public boolean sameAs(Object state) {
if (this == state) return true;
if (!(state instanceof BlendState blendState)) return false;
return srcRgb == blendState.srcRgb && dstRgb == blendState.dstRgb && srcAlpha == blendState.srcAlpha && dstAlpha == blendState.dstAlpha;
return srcRgb == blendState.srcRgb && dstRgb == blendState.dstRgb
&& srcAlpha == blendState.srcAlpha && dstAlpha == blendState.dstAlpha
&& equationRgb == blendState.equationRgb && equationAlpha == blendState.equationAlpha
&& Float.compare(blendColorR, blendState.blendColorR) == 0
&& Float.compare(blendColorG, blendState.blendColorG) == 0
&& Float.compare(blendColorB, blendState.blendColorB) == 0
&& Float.compare(blendColorA, blendState.blendColorA) == 0;
}
@Override
@@ -6,11 +6,22 @@ import org.lwjgl.opengl.GL11;
public class BooleanState implements ISettableState<BooleanState> {
protected final int glCap;
protected final boolean ffpStateOnly;
@Getter protected boolean enabled;
protected boolean stateUnknown;
public BooleanState(int glCap) {
this(glCap, false);
}
public BooleanState(int glCap, boolean ffpStateOnly) {
this.glCap = glCap;
this.ffpStateOnly = ffpStateOnly;
}
public void setUnknownState() {
stateUnknown = true;
}
public void disable() {
@@ -23,15 +34,17 @@ public class BooleanState implements ISettableState<BooleanState> {
public void setEnabled(boolean enabled) {
final boolean bypass = GLStateManager.shouldBypassCache();
if (bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) {
if (stateUnknown || bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) {
stateUnknown = false;
if (!bypass) {
this.enabled = enabled;
}
// Always call GL - the calling method controls whether we reach here based on recording mode
if (enabled) {
GL11.glEnable(this.glCap);
} else {
GL11.glDisable(this.glCap);
if (!ffpStateOnly) {
if (enabled) {
GL11.glEnable(this.glCap);
} else {
GL11.glDisable(this.glCap);
}
}
}
}
@@ -1,8 +1,9 @@
package com.gtnewhorizons.angelica.glsm.states;
import java.nio.Buffer;
import org.lwjgl.opengl.GL11;
import java.nio.Buffer;
/**
* Tracks OpenGL client array state (glVertexPointer, glColorPointer, etc.)
* This state is NOT part of the server attribute stack (push/pop attrib),
@@ -59,6 +60,19 @@ public class ClientArrayState {
return vertexPointerStride;
}
// Normal array state
private boolean normalArrayEnabled = false;
private Buffer normalPointer = null;
private int normalPointerType = GL11.GL_FLOAT;
private int normalPointerStride = 0;
// TexCoord array state
private boolean texCoordArrayEnabled = false;
private Buffer texCoordPointer = null;
private int texCoordPointerSize = 4;
private int texCoordPointerType = GL11.GL_FLOAT;
private int texCoordPointerStride = 0;
// Color array methods
public void setColorPointer(int size, int type, int stride, Buffer pointer) {
this.colorPointerSize = size;
@@ -90,4 +104,63 @@ public class ClientArrayState {
public int getColorPointerStride() {
return colorPointerStride;
}
// Normal array methods
public void setNormalPointer(int type, int stride, Buffer pointer) {
this.normalPointerType = type;
this.normalPointerStride = stride;
this.normalPointer = pointer;
}
public boolean isNormalArrayEnabled() {
return normalArrayEnabled;
}
public void setNormalArrayEnabled(boolean enabled) {
this.normalArrayEnabled = enabled;
}
public Buffer getNormalPointer() {
return normalPointer;
}
public int getNormalPointerType() {
return normalPointerType;
}
public int getNormalPointerStride() {
return normalPointerStride;
}
// TexCoord array methods
public void setTexCoordPointer(int size, int type, int stride, Buffer pointer) {
this.texCoordPointerSize = size;
this.texCoordPointerType = type;
this.texCoordPointerStride = stride;
this.texCoordPointer = pointer;
}
public boolean isTexCoordArrayEnabled() {
return texCoordArrayEnabled;
}
public void setTexCoordArrayEnabled(boolean enabled) {
this.texCoordArrayEnabled = enabled;
}
public Buffer getTexCoordPointer() {
return texCoordPointer;
}
public int getTexCoordPointerSize() {
return texCoordPointerSize;
}
public int getTexCoordPointerType() {
return texCoordPointerType;
}
public int getTexCoordPointerStride() {
return texCoordPointerStride;
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm.states;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import java.nio.FloatBuffer;
/**
* Stores eye-space clip plane equations for core profile emulation.
*/
public class ClipPlaneState {
private final float[][] eyePlanes = new float[GLStateManager.MAX_CLIP_PLANES][4];
private static final Vector4f tempPlane = new Vector4f();
private static final Matrix4f tempInverse = new Matrix4f();
public void setPlane(int index, double a, double b, double c, double d, Matrix4f modelView) {
final float[] dest = eyePlanes[index];
modelView.invert(tempInverse);
tempPlane.set((float) a, (float) b, (float) c, (float) d);
tempInverse.transformTranspose(tempPlane);
dest[0] = tempPlane.x;
dest[1] = tempPlane.y;
dest[2] = tempPlane.z;
dest[3] = tempPlane.w;
}
/** Writes the 4-component eye-space plane equation into {@code buf} at its current position. */
public void putEyePlane(int index, FloatBuffer buf) {
final float[] p = eyePlanes[index];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
}
}
@@ -8,11 +8,13 @@ import org.lwjgl.opengl.GL11;
public class DepthState implements ISettableState<DepthState> {
protected boolean enabled = true;
protected int func = GL11.GL_LESS;
protected double clearValue = 1.0;
@Override
public DepthState set(DepthState state) {
this.enabled = state.enabled;
this.func = state.func;
this.clearValue = state.clearValue;
return this;
}
@@ -20,7 +22,7 @@ public class DepthState implements ISettableState<DepthState> {
public boolean sameAs(Object state) {
if (this == state) return true;
if (!(state instanceof DepthState depthState)) return false;
return enabled == depthState.enabled && func == depthState.func;
return enabled == depthState.enabled && func == depthState.func && Double.compare(clearValue, depthState.clearValue) == 0;
}
@Override
public DepthState copy() {
Loaded 100 of 381 files, more files were not shown because too many files have changed in this diff. Show more