Compare commits

..
82 Commits
Author SHA1 Message Date
Jason Mitchell 3ca9e32191 Stop burying the error due to things catching RuntimeExceptions.... (#1767) 2026-05-19 08:53:05 -07:00
sisyphussy efbca29381 Add cached block item renderer (#1730) 2026-05-19 08:03:51 -07:00
Jason Mitchell 9c1b6c75ff Fix crash on windows (#1766) 2026-05-19 08:02:37 -07:00
sisyphussy fa91b70528 Display list stuff (#1750) 2026-05-18 12:46:48 -07:00
Jason Mitchell 61de8385ca Minecraft is a dirty dirty liar. (#1764) 2026-05-18 12:39:30 -07:00
Jason Mitchell 962e7527a4 Register later (#1763) 2026-05-18 12:39:20 -07:00
Jason Mitchell 2ab2713fe9 Stops draw/startDrawing in ISBRH (#1759) 2026-05-17 21:08:18 -07:00
Jason Mitchell 7cf652cd4e Misc fixes - Journeymap and Xaeros (#1756) 2026-05-17 10:23:10 -07:00
Jason Mitchell 48eba2fd96 Properly end the dynamic lighting profiler section (#1751) 2026-05-16 12:20:09 -07:00
Apis035 356ba376fe Adjust options screen width (#1748) 2026-05-15 08:47:28 -07:00
Eclipse-Sol ef6b0742a9 Fix skeleton legs from being jammed into their pelvis (#1747) 2026-05-14 15:26:25 -07:00
Kaiz 787929d0bd Handle recoverable block render failures in celeritas meshing (#1705) 2026-05-10 10:00:43 -07:00
0e1fd9d0e9 add auto emissive overlay for any block (#1710)
Co-authored-by: VortexSo4 <[email protected]>
Co-authored-by: ah-OOG-ah <[email protected]>
2026-05-10 02:12:08 -04:00
Ruling-0 58938e82a7 Compact CTM Fixes (#1726) 2026-05-09 11:42:42 -07:00
Branan Riley be6c8c85b5 Detect vanilla AO at a higher level (#1733) 2026-05-09 11:41:59 -07:00
ABKQPOandCaedis 39bca37d0d Adjust IDynamicLightProducer to allow passing an ItemStack parameter (#1734)
Co-authored-by: Caedis <[email protected]>
2026-05-08 20:31:22 -07:00
czqwq 1c61be04a7 Fix Already tesselating logs (#1729) 2026-05-08 20:30:08 -07:00
7a97cd9386 Add RGB color system (#1669)
Co-authored-by: Death Fuel <[email protected]>
Co-authored-by: Jason Mitchell <[email protected]>
2026-05-08 18:36:51 -07:00
Kotmatross 5b09bde7a0 [NTM:Space] Fix mixins in obfuscated environment (#1738) 2026-05-08 18:24:29 +02:00
Ruling-0 edc0081b7c Fix CTM inventory texture bleed (#1721) 2026-05-04 12:25:51 -07:00
Alexdoru 913256b032 fix world client leak in CTMUtils (#1724) 2026-05-04 12:24:25 -07:00
sisyphussy e1f5083d3e Fix intel detection (#1722) 2026-05-04 12:23:50 -07:00
sisyphussy b55e693ad2 Remove DSA from GLSM & other stuff (#1723) 2026-05-04 12:23:38 -07:00
sisyphussy 21c01a1aa4 Fix terrible performance on Intel (#1718) 2026-05-03 14:32:58 -07:00
Caedis 83a26b2291 Add Cosmetic Armor support for Dynamic Lights (#1708) 2026-05-03 14:31:49 -07:00
Kotmatross 7e44c1efcb Fix stale uniforms when switching shader program (#1715) 2026-05-03 14:30:46 -07:00
VortexSo4 155f209db5 remove ctm guide as it is worse than wiki (#1706) 2026-04-30 11:03:18 -07:00
SpacEagle17 c4a6c165a0 Increase MAX_COLOR_BUFFERS limit to 32 (#1704) 2026-04-28 12:33:51 -07:00
Eclipse-Sol 624fe6b453 Add targeting of snow-covered blocks. (#1696) 2026-04-26 15:26:10 -07:00
YoshyandJason Mitchell fd45fda0ab Fix AO Lighting for compact CTM (#1697)
Co-authored-by: Jason Mitchell <[email protected]>
2026-04-26 12:55:24 -07:00
Alexdoru 00fc97a611 Fix dynamic lights world client leaks (#1700) 2026-04-26 10:01:46 -07:00
Alexdoru 445dc487b9 fix world client leak caused by lambda in IrisExclusiveUniforms$WorldInfoUniforms (#1701) 2026-04-26 09:56:08 -07:00
Yoshy f43fbd4d9f Fix CTM to not spread to nearby TEs (#1695) 2026-04-25 13:09:38 -07:00
Jason Mitchell dbabc4b3dc Maybe fix DragonAPI on OSX - take two (#1689)
* Maybe fix DragonAPI on OSX - take two
* Bind a default VAO on the SharedDrawable
* Mixin that tries to swap out glGetProgramInfoLog when appropriate

* no direct gl calls
2026-04-21 15:12:02 -07:00
Jason Mitchell e01097264b Fix infinite extent AABB crash w/ kotlin, and ensure proper sizing/padding on VAO/VBO snapshots. (#1687) 2026-04-20 10:11:46 -07:00
Jason Mitchell 0a922f3129 AE2 Crash Fix (#1684)
* Snapshot all relevant data into display lists (Avoids referencing stale/recycled data and a crash on Nvidia)
* Route deletes through GLStateManager
2026-04-19 08:19:02 -07:00
Eclipse-Sol 4a2dcd0b22 Automate the collection of legacy IDs for very old shaderpacks. (#1631) 2026-04-18 09:53:06 -07:00
Eclipse-Sol 0db691db0b Add FlatteningMap and EntityFlatteningMap (#1635) 2026-04-18 09:20:24 -07:00
danyadev e5cd1372e7 always request 0.9.0+ GTNHLib version when a player has an outdated version (#1672) 2026-04-18 09:18:28 -07:00
Kotmatross fbd07cc575 NTM:Space compat rework and update (#1675) 2026-04-18 09:14:49 -07:00
George Paton 91f2cf9dfb Add "Mods Only" Dynamic FOV option (#1680) 2026-04-18 09:13:39 -07:00
Kaiz 34a6967c71 fix(iris): implement full GL alpha test (#1673) 2026-04-18 09:12:46 -07:00
Alexdoru 9c295a1e22 Fix cached item in RenderItemFrame leaking both world client and server (#1659) 2026-04-13 15:04:36 -07:00
DarkShadow44 9fd02900ef Load modern block properties (#1626) 2026-04-13 15:03:23 -07:00
Jason Mitchell 27e9a4d4fd Fix class circularity error with early redirector (#1670) 2026-04-13 15:02:26 -07:00
Darren Eberly dfc08053d5 Don't advance buffer positions for Material shininess in GLSM (#1668) 2026-04-12 22:25:25 -07:00
VortexSo4 1af03fa369 add CTM guide (#1667) 2026-04-11 17:49:38 -07:00
NealDeal34 3ae3b2527d Update zh_CN.lang (#1623)
* Update zh_CN.lang

* update zh_CN.lang for dynamic lights
2026-04-07 08:21:34 +02:00
Jason Mitchell fd2a14a000 Various fixes (#1652) 2026-04-06 17:35:15 -07:00
DarkShadow44 9f0f6a6232 Always render clouds late for shaders (#1649) 2026-04-06 09:52:34 -07:00
DarkShadow44 2942c7eb84 Disable clouds when shaders are active (#1648) 2026-04-06 09:52:21 -07:00
Alexdoru 47762f921e fix two world client memory leaks (#1650) 2026-04-06 16:38:03 +00:00
Jason Mitchell bdc4431760 Fix some issues someone was having with nixos (#1647) 2026-04-05 16:21:58 -07:00
DarkShadow44 7e1beadfb5 DH: Fix mismatch from upstream for solas shaders (#1645) 2026-04-04 17:27:19 -07:00
Jason Mitchell 59a4b84023 Standalone GLSM mod - Umbra (#1642) 2026-04-03 21:51:35 -07:00
Jason Mitchell 8d53e25e0a Compact CTM Fix (#1640) 2026-04-03 16:48:08 -07:00
VortexSo4 bbf83db379 Implement compact ctm (#1636) 2026-04-02 22:32:28 +00:00
Jason Mitchell ec82280b9b Match OpenGL Buffer Behavior for glGet (#1637) 2026-04-02 14:20:06 -07:00
DarkShadow44andJason Mitchell d7588e4c14 Adapt shader block id override for Angelica 2.x (#1601)
Co-authored-by: Jason Mitchell <[email protected]>
2026-04-02 14:18:55 -07:00
Jason Mitchell 54c99386d4 Do a similar fix as MixinDroppedItemGlintEdges to held items (#1632) 2026-03-31 18:31:21 -07:00
DarkShadow44 465abf889a Pass mc_Entity like upstream does (#1627) 2026-03-31 16:18:35 -07:00
DarkShadow44 4acb83be6a Port shadow translucent (aka shadow_water) from upstream (#1628) 2026-03-31 16:09:25 -07:00
DarkShadow44 80f9724ee6 Handle GL_QUAD_STRIP in legacy conversion (#1629) 2026-03-31 15:57:57 -07:00
Jason Mitchell 8cf55bb216 Safely handle off client thread crash reports/glGetString (#1630) 2026-03-31 15:49:47 -07:00
Eclipse-Sol 3ebc8868c7 Fix item reset hopefully for the last time (#1621) 2026-03-30 19:32:47 -07:00
JohnButlerThird 0938e6d975 Only run the logic on bop grass (fix my mod bugging out) (#1620) 2026-03-30 18:17:42 -07:00
Eclipse-Sol d3022a40d2 Create new deferred overlay system to fix z-fighting issues (#1605) 2026-03-30 15:50:18 -07:00
Eclipse-Sol 2645c803f9 Better horse support (#1599) 2026-03-30 15:41:53 -07:00
Jason Mitchell d91c356a99 Shader fixes (#1619) 2026-03-30 15:34:54 -07:00
Jason Mitchell 189ff4a80f Defer client attributes. They're uploaded/handled elsewhere. Should fix a crash on j8/lwjgl2 (#1618) 2026-03-30 09:58:40 -07:00
Jason Mitchell ffb4ca9472 Get rid of some allocations, and use immutable variants for the suppliers (#1612) 2026-03-30 09:58:27 -07:00
Jason Mitchell 7d8c8d306d More Double overloads (#1613) 2026-03-29 15:09:13 -07:00
Kotmatross 117ff0cacf Fix other dynamic uniforms not updating properly (#1610) 2026-03-29 12:53:09 -07:00
Jason Mitchell 83fdd57534 Narrow another overly broad transformer exclusion 2026-03-29 09:24:50 -07:00
Kaiz 951be04e9a fix: correctly handle cross-chunk TileEntity render bounds 2026-03-29 09:20:13 -07:00
Jason Mitchell 0ad69f3433 FINE, we'll work with java8/forge. 2026-03-28 20:09:42 -07:00
Fabian Maurer 0dabecc784 Handle GL_POLYGON in legacy conversion 2026-03-28 14:41:23 -07:00
Jason Mitchell 9b3fad4c6d Ensure sufficient coverage + optimize the check 2026-03-28 13:16:54 -07:00
Kaiz e1e8256861 fix: crash when client-side vertex attrib pointers are used in core profile 2026-03-28 13:16:54 -07:00
Jason Mitchell e65c01c2a3 Handle method handles in invokedynamic 2026-03-28 12:46:19 -07:00
Jason Mitchell b9de4df18e Update alpha test to hopefully work with RTM and AE2 (#1597) 2026-03-28 09:40:10 -07:00
Kotmatross a5d31cd1b6 Return phaseChangeNotifier (#1596) 2026-03-28 09:15:46 -07:00
252 changed files with 11484 additions and 3551 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
/.idea/
/.vs/
/.vscode/
/run/
run/
build/
/eclipse/
.classpath
+25 -3
View File
@@ -76,14 +76,14 @@ dependencies {
// Iris Shaders
compileOnly('org.jetbrains:annotations:26.0.2')
api("com.github.GTNewHorizons:GTNHLib:0.9.36:dev")
api("com.github.GTNewHorizons:GTNHLib:0.10.0:dev")
shadowImplementation(project(":glsm")) { transitive = false }
shadowImplementation(project(":lwjgl3-backend")) { transitive = false }
shadowImplementation("net.minecraftforge:eventbus:7.0-beta.15")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-32.g7dd88a4-GTNH") {
exclude module: "antlr4" // we only want to shadow the runtime module
}
shadowImplementation("org.antlr:antlr4-runtime:4.13.2")
@@ -97,6 +97,26 @@ dependencies {
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.8.35-GTNH:dev')
devOnlyNonPublishable('com.github.GTNewHorizons:CodeChickenCore:1.4.7:dev')
// Display List Testing
/*
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-749-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ae2stuff:0.10.14-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.54:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:IguanaTweaksTConstruct:2.7.0:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenBlocks:1.12.4-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.12-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:TinkersConstruct:1.14.12-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ServerUtilities:2.2.13:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:ironchest:6.1.6:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:waila:1.9.7:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.15-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Mobs-Info:0.5.10-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Draconic-Evolution:1.5.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:HoloInventory:2.5.8-GTNH:dev")
*/
//devOnlyNonPublishable('com.github.GTNewHorizons:worldedit-gtnh:v0.0.9')
// Notfine Deps
compileOnly("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev")
devOnlyNonPublishable("com.github.GTNewHorizons:Baubles-Expanded:2.2.2-GTNH:dev")
@@ -119,7 +139,7 @@ dependencies {
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev")
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5634_H261"))
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5663_H261"))
compileOnly(rfg.deobf("curse.maven:campfirebackport-387444:4611675"))
compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684"))
@@ -127,6 +147,8 @@ dependencies {
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
compileOnly(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
devOnlyNonPublishable("com.github.GTNewHorizons:CosmeticArmorReworked:1.0.3-GTNH:dev")
// For testing alternative splash screens — https://github.com/MalTeeez/ModernSplash/releases
// devOnlyNonPublishable("com.github.MalTeeez:modernsplash:1.7.10-1.2.4:dev")
+7 -1
View File
@@ -66,6 +66,7 @@ repositories {
content { includeGroup("xyz.wagyourtail.jvmdowngrader") }
}
mavenCentral()
mavenLocal()
}
dependencies {
@@ -76,7 +77,7 @@ dependencies {
compileOnly("org.lwjgl.lwjgl:lwjgl:${property("lwjglVersion")}")
compileOnly("org.lwjgl.lwjgl:lwjgl_util:${property("lwjglVersion")}")
compileOnly("org.ow2.asm:asm-tree:9.7")
compileOnly("org.ow2.asm:asm-tree:5.0.3")
// Our Deps
api("com.github.GTNewHorizons:GTNHLib:${property("gtnhlibVersion")}:dev")
@@ -100,6 +101,7 @@ dependencies {
testImplementation(sourceSets["stubs"].output)
testImplementation(platform("org.junit:junit-bom:${property("junitBomVersion")}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.ow2.asm:asm-tree:9.7")
testImplementation("org.lwjgl.lwjgl:lwjgl:${property("lwjglVersion")}")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("org.apache.logging.log4j:log4j-core:${property("log4jVersion")}")
@@ -145,6 +147,10 @@ tasks.test {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
if (System.getProperty("os.name").lowercase().contains("mac")
&& System.getProperty("os.arch") == "aarch64") {
vendor = JvmVendorSpec.AZUL
}
}
dependsOn(downgradeMainClasses, downgradeTestClasses, downgradeDepsForTest)
+1 -1
View File
@@ -4,7 +4,7 @@ celeritasVersion = 2.5.3-GTNH
lombokVersion = 1.18.42
log4jVersion = 2.0-beta9
guavaVersion = 18.0
gtnhlibVersion = 0.9.36
gtnhlibVersion = 0.10.0
glslTransformLibVersion = 0.2.0-26.g5ee7d96-GTNH
antlr4RuntimeVersion = 4.13.2
jcppVersion = 1.4.14
@@ -1,67 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw;
import com.gtnewhorizons.angelica.glsm.recording.CommandBuffer;
import com.gtnewhorizons.angelica.glsm.recording.CommandBufferProcessor;
import java.util.List;
/**
* Builds final command buffers from raw command buffers and compiled VBO data.
* Bridges the gap between VBO compilation (glsm types) and buffer processing (recording types).
*/
public final class CommandBufferBuilder {
private CommandBufferBuilder() {}
/**
* 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,
List<AccumulatedDraw> accumulatedDraws,
CommandBuffer finalBuffer) {
rawBuffer.resetRead();
int cmdIndex = 0;
int rangeIndex = 0;
AccumulatedDraw draw;
while (rawBuffer.hasRemaining()) {
// Read and process the command
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++;
}
while (rangeIndex < accumulatedDraws.size()) {
emitDrawRangeToBuffer(accumulatedDraws.get(rangeIndex), finalBuffer, rangeIndex);
rangeIndex++;
}
}
/**
* Emit a draw range to the output buffer, with transform if needed.
*/
private static void emitDrawRangeToBuffer(
AccumulatedDraw draw,
CommandBuffer out,
int vboIdx) {
if (draw.restoreData != null) {
out.writeDrawRangeRestore(vboIdx, draw.restoreData);
return;
}
// Write the draw range command
out.writeDrawRange(vboIdx);
}
}
@@ -9,13 +9,10 @@ import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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;
File diff suppressed because it is too large. Load diff
@@ -345,6 +345,27 @@ public class FeedbackManager {
}
break;
case GL11.GL_QUAD_STRIP:
for (int i = 0; i + 3 < count; i += 2) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(4);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i + 3), stride);
emitVertex(reader.read(i + 2), stride);
}
break;
case GL11.GL_POLYGON:
if (count >= 3) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(count);
for (int i = 0; i < count; i++) {
emitVertex(reader.read(i), stride);
}
}
break;
default:
throw new UnsupportedOperationException("Feedback mode: unsupported primitive mode 0x" + Integer.toHexString(mode));
}
@@ -17,6 +17,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.recording.CommandRecorder;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import com.gtnewhorizons.angelica.glsm.recording.commands.TexImage2DCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.TexSubImage2DCmd;
import com.gtnewhorizons.angelica.glsm.stacks.AlphaStateStack;
@@ -43,6 +44,7 @@ import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
@@ -186,6 +188,8 @@ public class GLStateManager {
@Getter private static Vendor VENDOR;
@Getter private static boolean windows;
// This setting varies depending on driver, so it gets queried at runtime
public static int DEFAULT_DRAW_BUFFER = 0x0405; // GL_BACK
@@ -419,6 +423,9 @@ public class GLStateManager {
@Getter protected static int boundVBO;
@Getter protected static int boundEBO;
@Getter protected static int boundVAO;
private static int clientArraysVBO = 0;
private static int clientArraysVBOCapacity = 0;
private static final int[] clientArraysVBOOffsets = new int[VertexAttribState.MAX_ATTRIBS];
private static int boundPixelUnpackBuffer;
private static int boundPixelPackBuffer;
private static final Int2IntOpenHashMap vaoEboMap = new Int2IntOpenHashMap();
@@ -513,6 +520,12 @@ public class GLStateManager {
// After Display.create(), viewport is (0, 0, width, height)
viewportState.setViewPort(0, 0, displayWidth, displayHeight);
}
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
final String os = System.getProperty("os.name").toLowerCase();
windows = os.contains("win");
}
static void init(Runnable initCallback) {
@@ -520,9 +533,6 @@ public class GLStateManager {
DEFAULT_DRAW_BUFFER = RENDER_BACKEND.getInteger(GL11.GL_DRAW_BUFFER);
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
// The initial mask value should be defined as all 1's. However, some drivers have it set to 0's.
// To ensure consistency & correctness across all drivers, we're setting them to 0xFF.
RENDER_BACKEND.stencilFunc(stencilState.getFuncFront(), stencilState.getRefFront(), 0xFF);
@@ -616,16 +626,6 @@ public class GLStateManager {
return BYPASS_CACHE || !isCachingEnabled();
}
/**
* Returns true if we should use DSA for texture operations. DSA relies on cached texture bindings, which aren't tracked when caching is disabled. Also,
* proxy textures are special query textures that shouldn't use DSA.
*/
private static boolean shouldUseDSA(int target) {
if (!isCachingEnabled()) return false;
// Proxy textures are for capability queries, not real textures
return target != GL11.GL_PROXY_TEXTURE_1D && target != GL11.GL_PROXY_TEXTURE_2D && target != GL12.GL_PROXY_TEXTURE_3D;
}
// LWJGL Overrides
public static void glEnable(int cap) {
final RecordMode mode = DisplayListManager.getRecordMode();
@@ -946,10 +946,11 @@ public class GLStateManager {
switch (pname) {
case GL11.GL_COLOR_WRITEMASK -> {
params.put((byte) (colorMask.red ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put((byte) (colorMask.green ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put((byte) (colorMask.blue ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put((byte) (colorMask.alpha ? GL11.GL_TRUE : GL11.GL_FALSE));
final int pos = params.position();
params.put(pos, (byte) (colorMask.red ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put(pos + 1, (byte) (colorMask.green ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put(pos + 2, (byte) (colorMask.blue ? GL11.GL_TRUE : GL11.GL_FALSE));
params.put(pos + 3, (byte) (colorMask.alpha ? GL11.GL_TRUE : GL11.GL_FALSE));
}
default -> {
if (!HAS_MULTIPLE_SET.contains(pname)) {
@@ -1060,7 +1061,7 @@ public class GLStateManager {
case GL11.GL_DIFFUSE -> state.diffuse.get(0, params);
case GL11.GL_SPECULAR -> state.specular.get(0, params);
case GL11.GL_EMISSION -> state.emission.get(0, params);
case GL11.GL_SHININESS -> params.put(state.shininess);
case GL11.GL_SHININESS -> params.put(params.position(), state.shininess);
case GL11.GL_COLOR_INDEXES -> state.colorIndexes.get(0, params);
default -> {
}
@@ -1075,11 +1076,11 @@ public class GLStateManager {
case GL11.GL_SPECULAR -> state.specular.get(0, params);
case GL11.GL_POSITION -> state.position.get(0, params);
case GL11.GL_SPOT_DIRECTION -> state.spotDirection.get(0, params);
case GL11.GL_SPOT_EXPONENT -> params.put(state.spotExponent);
case GL11.GL_SPOT_CUTOFF -> params.put(state.spotCutoff);
case GL11.GL_CONSTANT_ATTENUATION -> params.put(state.constantAttenuation);
case GL11.GL_LINEAR_ATTENUATION -> params.put(state.linearAttenuation);
case GL11.GL_QUADRATIC_ATTENUATION -> params.put(state.quadraticAttenuation);
case GL11.GL_SPOT_EXPONENT -> params.put(params.position(), state.spotExponent);
case GL11.GL_SPOT_CUTOFF -> params.put(params.position(), state.spotCutoff);
case GL11.GL_CONSTANT_ATTENUATION -> params.put(params.position(), state.constantAttenuation);
case GL11.GL_LINEAR_ATTENUATION -> params.put(params.position(), state.linearAttenuation);
case GL11.GL_QUADRATIC_ATTENUATION -> params.put(params.position(), state.quadraticAttenuation);
default -> {
}
}
@@ -1098,21 +1099,39 @@ public class GLStateManager {
case GL11.GL_COLOR_CLEAR_VALUE -> clearColor.get(params);
case GL11.GL_CURRENT_COLOR -> color.get(params);
case GL11.GL_DEPTH_RANGE -> {
params.put((float) viewportState.depthRangeNear);
params.put((float) viewportState.depthRangeFar);
final int pos = params.position();
params.put(pos, (float) viewportState.depthRangeNear);
params.put(pos + 1, (float) viewportState.depthRangeFar);
}
case GL14.GL_BLEND_COLOR -> {
final int pos = params.position();
params.put(pos, blendState.getBlendColorR());
params.put(pos + 1, blendState.getBlendColorG());
params.put(pos + 2, blendState.getBlendColorB());
params.put(pos + 3, blendState.getBlendColorA());
}
case GL14.GL_BLEND_COLOR -> params.put(blendState.getBlendColorR()).put(blendState.getBlendColorG()).put(blendState.getBlendColorB()).put(blendState.getBlendColorA());
case GL11.GL_CURRENT_NORMAL -> {
final Vector3f normal = ShaderManager.getCurrentNormal();
params.put(normal.x).put(normal.y).put(normal.z);
final int pos = params.position();
params.put(pos, normal.x);
params.put(pos + 1, normal.y);
params.put(pos + 2, normal.z);
}
case GL11.GL_CURRENT_TEXTURE_COORDS -> {
final Vector4f tc = ShaderManager.getCurrentTexCoord();
params.put(tc.x).put(tc.y).put(tc.z).put(tc.w);
final int pos = params.position();
params.put(pos, tc.x);
params.put(pos + 1, tc.y);
params.put(pos + 2, tc.z);
params.put(pos + 3, tc.w);
}
case GL11.GL_FOG_COLOR -> {
final FloatBuffer fogBuf = fogState.getFogColorBuffer();
params.put(fogBuf.get(0)).put(fogBuf.get(1)).put(fogBuf.get(2)).put(fogBuf.get(3));
final int pos = params.position();
params.put(pos, fogBuf.get(0));
params.put(pos + 1, fogBuf.get(1));
params.put(pos + 2, fogBuf.get(2));
params.put(pos + 3, fogBuf.get(3));
}
case GL11.GL_LIGHT_MODEL_AMBIENT -> lightModel.ambient.get(0, params);
default -> {
@@ -1614,6 +1633,9 @@ public class GLStateManager {
}
alphaTest.enable();
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
public static void disableAlphaTest() {
@@ -1631,6 +1653,9 @@ public class GLStateManager {
}
alphaTest.disable();
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
public static void glAlphaFunc(int function, float reference) {
@@ -1650,6 +1675,9 @@ public class GLStateManager {
alphaState.setFunction(function);
alphaState.setReference(reference);
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
}
@@ -1795,13 +1823,7 @@ public class GLStateManager {
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
} else {
// Non-main thread or proxy texture - use direct GL call
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -1846,11 +1868,7 @@ public class GLStateManager {
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : null);
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
} else {
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -1973,7 +1991,9 @@ public class GLStateManager {
}
RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, id);
RENDER_BACKEND.texParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, 1, 1, 0, GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
restorePixelUnpackBuffer();
// Restore previous binding on active unit
RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, savedBinding == id ? 0 : savedBinding);
@@ -2079,22 +2099,20 @@ public class GLStateManager {
}
public static void glEnd() {
if (DisplayListManager.isRecording()) {
final DirectTessellator result = ImmediateModeRecorder.end();
if (result != null) {
final DirectTessellator result = ImmediateModeRecorder.end();
if (result != null) {
if (DisplayListManager.isRecording()) {
if (DisplayListManager.isCompileAndExecute() && initConfig != null && initConfig.getDirectDrawer() != null) {
DisplayListManager.flushMatrix();
final var recorder = DisplayListManager.pauseRecording();
final CommandRecorder recorder = DisplayListManager.pauseRecording();
initConfig.getDirectDrawer().accept(result);
DisplayListManager.resumeRecording(recorder);
}
DisplayListManager.addImmediateModeDraw(result);
return;
}
if (initConfig != null && initConfig.getDirectDrawer() != null) {
initConfig.getDirectDrawer().accept(result);
}
return;
}
final DirectTessellator result = ImmediateModeRecorder.end();
if (result != null && initConfig != null && initConfig.getDirectDrawer() != null) {
initConfig.getDirectDrawer().accept(result);
}
}
@@ -2145,6 +2163,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
RENDER_BACKEND.drawElements(mode, indices);
}
@@ -2158,6 +2177,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
} else {
@@ -2175,6 +2195,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
} else {
@@ -2192,6 +2213,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
} else {
@@ -2203,15 +2225,11 @@ public class GLStateManager {
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
if (isVAOBound()) {
DisplayListManager.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
} else if (isVBOBound() || VertexAttribState.hasVBOBoundAttrib()) {
final DirectTessellator result = ImmediateModeRecorder.processDrawElementsFromVBO(mode, indices_count, type, indices_buffer_offset, boundEBO);
if (result != null) {
DisplayListManager.addImmediateModeDraw(result);
}
} else {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
// Core profile: a default VAO is generated at init and glBindVertexArray(0)
// is redirected to it, so a VAO is always bound here — no fallback branches.
final IndexedDrawCapture capture = IndexedDrawCapture.create(mode, indices_count, type, indices_buffer_offset, boundEBO);
if (capture != null) {
DisplayListManager.recordIndexedDrawCapture(capture);
}
if (recordMode == RecordMode.COMPILE) {
return;
@@ -2223,6 +2241,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset);
} else {
@@ -2245,20 +2264,72 @@ public class GLStateManager {
public static void glMultiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride) {
ShaderManager.getInstance().preDraw();
prepareClientArrays();
RENDER_BACKEND.multiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
public static void glMultiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex) {
ShaderManager.getInstance().preDraw();
prepareClientArrays();
RENDER_BACKEND.multiDrawElementsBaseVertex(mode, pCount, type, pIndices, drawcount, pBaseVertex);
}
public static void glDrawElementsBaseVertex(int mode, int count, int type, long indices, int basevertex) {
ShaderManager.getInstance().preDraw();
prepareClientArrays();
RENDER_BACKEND.drawElementsBaseVertex(mode, count, type, indices, basevertex);
}
public static void glDrawElementsInstanced(int mode, int count, int type, long indices, int primcount) { RENDER_BACKEND.drawElementsInstanced(mode, count, type, indices, primcount); }
public static void glDrawElementsInstanced(int mode, int count, int type, long indices, int primcount) {
ShaderManager.getInstance().preDraw();
prepareClientArrays();
RENDER_BACKEND.drawElementsInstanced(mode, count, type, indices, primcount);
}
private static void prepareClientArrays() {
if (ShaderManager.getInstance().isEnabled() && VertexAttribState.hasAnyClientSideEnabledAttrib()) {
uploadClientArraysToVBO();
}
}
/**
* If any enabled vertex attribute uses a client-side pointer (no VBO), upload all such
* attribs into a shared stream VBO so the draw succeeds under core profile.
*/
private static void uploadClientArraysToVBO() {
int totalBytes = 0;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
clientArraysVBOOffsets[i] = -1;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled || a.clientPointer == null) continue;
clientArraysVBOOffsets[i] = totalBytes;
totalBytes += a.clientPointer.remaining();
}
if (totalBytes == 0) return;
if (clientArraysVBO == 0) {
clientArraysVBO = RENDER_BACKEND.genBuffers();
}
final int savedVBO = boundVBO;
glBindBuffer(GL15.GL_ARRAY_BUFFER, clientArraysVBO);
if (totalBytes > clientArraysVBOCapacity) {
int newCap = Math.max(4096, clientArraysVBOCapacity);
while (newCap < totalBytes) newCap *= 2;
clientArraysVBOCapacity = newCap;
}
RENDER_BACKEND.bufferData(GL15.GL_ARRAY_BUFFER, clientArraysVBOCapacity, GL15.GL_STREAM_DRAW);
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
if (clientArraysVBOOffsets[i] < 0) continue;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
RENDER_BACKEND.bufferSubData(GL15.GL_ARRAY_BUFFER, clientArraysVBOOffsets[i], a.clientPointer.duplicate());
RENDER_BACKEND.vertexAttribPointer(i, a.size, a.type, a.normalized, a.stride, (long) clientArraysVBOOffsets[i]);
}
glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
}
public static void glDrawArrays(int mode, int first, int count) {
CommandRecorder savedRecorder = null;
@@ -2278,8 +2349,13 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadsAsTriangles(first, count);
} else if (mode == GL11.GL_QUAD_STRIP) {
RENDER_BACKEND.drawArrays(GL11.GL_TRIANGLE_STRIP, first, count & ~1);
} else if (mode == GL11.GL_POLYGON) {
RENDER_BACKEND.drawArrays(GL11.GL_TRIANGLE_FAN, first, count);
} else {
RENDER_BACKEND.drawArrays(mode, first, count);
}
@@ -2298,6 +2374,10 @@ public class GLStateManager {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glVertexPointer(int size, int stride, DoubleBuffer pointer) {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glVertexPointer(int size, int type, int stride, ByteBuffer pointer) {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
}
@@ -2339,6 +2419,10 @@ public class GLStateManager {
glNormalPointer(GL11.GL_INT, stride, pointer);
}
public static void glNormalPointer(int stride, DoubleBuffer pointer) {
glNormalPointer(GL11.GL_DOUBLE, stride, pointer);
}
public static void glNormalPointer(int type, int stride, ByteBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
}
@@ -2355,6 +2439,10 @@ public class GLStateManager {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_INT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, DoubleBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_DOUBLE, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, long pointer_buffer_offset) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer_buffer_offset);
}
@@ -2377,6 +2465,10 @@ public class GLStateManager {
glTexCoordPointer(size, GL11.GL_SHORT, stride, pointer);
}
public static void glTexCoordPointer(int size, int stride, DoubleBuffer pointer) {
glTexCoordPointer(size, GL11.GL_DOUBLE, stride, pointer);
}
public static void glTexCoordPointer(int size, int type, int stride, FloatBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
@@ -2395,6 +2487,12 @@ public class GLStateManager {
glVertexAttribPointer(loc, size, GL11.GL_INT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, DoubleBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, long pointer_buffer_offset) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
@@ -2441,32 +2539,23 @@ public class GLStateManager {
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, FloatBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, DoubleBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, IntBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ShortBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer) {
VertexAttribState.set(index, size, type, normalized, stride, pointer, boundVBO);
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, pointer);
VertexAttribState.set(index, size, type, normalized, stride, pointer, 0);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, long offset) {
@@ -2475,20 +2564,15 @@ public class GLStateManager {
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, ByteBuffer pointer) {
VertexAttribState.set(index, size, type, false, stride, pointer, boundVBO);
RENDER_BACKEND.vertexAttribIPointer(index, size, type, stride, pointer);
VertexAttribState.set(index, size, type, false, stride, pointer, 0);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, IntBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, false, stride, bb, boundVBO);
RENDER_BACKEND.vertexAttribIPointer(index, size, type, stride, bb);
VertexAttribState.set(index, size, type, false, stride, MemoryUtilities.memByteBuffer(pointer), 0);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, ShortBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, false, stride, bb, boundVBO);
RENDER_BACKEND.vertexAttribIPointer(index, size, type, stride, bb);
VertexAttribState.set(index, size, type, false, stride, MemoryUtilities.memByteBuffer(pointer), 0);
}
public static void glVertexAttribDivisor(int index, int divisor) { RENDER_BACKEND.vertexAttribDivisor(index, divisor); }
@@ -3263,7 +3347,6 @@ public class GLStateManager {
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
DisplayListManager.recordMatrixMode(mode);
DisplayListManager.resetRelativeTransform(); // Mode switch is a barrier - reset delta tracking
if (recordMode == RecordMode.COMPILE) {
return;
}
@@ -3277,8 +3360,6 @@ public class GLStateManager {
final Matrix4f matrix = new Matrix4f().set(m);
m.rewind();
DisplayListManager.recordLoadMatrix(matrix);
// Reset relative transform - subsequent transforms are relative to loaded matrix
DisplayListManager.resetRelativeTransform();
if (mode == RecordMode.COMPILE) {
return;
}
@@ -3298,8 +3379,6 @@ public class GLStateManager {
final Matrix4f floatMatrix = new Matrix4f();
floatMatrix.set(conversionMatrix4d);
DisplayListManager.recordLoadMatrix(floatMatrix);
// Reset relative transform - subsequent transforms are relative to loaded matrix
DisplayListManager.resetRelativeTransform();
if (mode == RecordMode.COMPILE) {
return;
}
@@ -3339,8 +3418,6 @@ public class GLStateManager {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordLoadIdentity();
// Reset relative transform - accumulated transforms are discarded (overwritten by load)
DisplayListManager.resetRelativeTransform();
if (mode == RecordMode.COMPILE) {
return;
}
@@ -3352,10 +3429,9 @@ public class GLStateManager {
}
public static void glTranslatef(float x, float y, float z) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.updateRelativeTransform(x, y, z, DisplayListManager.TransformOp.TRANSLATE, null);
if (mode == RecordMode.COMPILE) return;
if (DisplayListManager.isRecording()) {
DisplayListManager.applyMatrixTranslation(x, y, z);
return;
}
if (isCachingEnabled()) {
getMatrixStack().translate(x, y, z);
@@ -3364,10 +3440,9 @@ public class GLStateManager {
}
public static void glTranslated(double x, double y, double z) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.updateRelativeTransform((float) x, (float) y, (float) z, DisplayListManager.TransformOp.TRANSLATE, null);
if (mode == RecordMode.COMPILE) return;
if (DisplayListManager.isRecording()) {
DisplayListManager.applyMatrixTranslation((float) x, (float) y, (float) z);
return;
}
if (isCachingEnabled()) {
getMatrixStack().translate((float) x, (float) y, (float) z);
@@ -3376,10 +3451,9 @@ public class GLStateManager {
}
public static void glScalef(float x, float y, float z) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.updateRelativeTransform(x, y, z, DisplayListManager.TransformOp.SCALE, null);
if (mode == RecordMode.COMPILE) return;
if (DisplayListManager.isRecording()) {
DisplayListManager.applyMatrixScale(x, y, z);
return;
}
if (isCachingEnabled()) {
getMatrixStack().scale(x, y, z);
@@ -3388,10 +3462,9 @@ public class GLStateManager {
}
public static void glScaled(double x, double y, double z) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.updateRelativeTransform((float) x, (float) y, (float) z, DisplayListManager.TransformOp.SCALE, null);
if (mode == RecordMode.COMPILE) return;
if (DisplayListManager.isRecording()) {
DisplayListManager.applyMatrixScale((float) x, (float) y, (float) z);
return;
}
if (isCachingEnabled()) {
getMatrixStack().scale((float) x, (float) y, (float) z);
@@ -3405,10 +3478,9 @@ public class GLStateManager {
multMatrix.set(floatBuffer);
final int currentMode = matrixMode.getMode();
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
if (DisplayListManager.isRecording()) {
DisplayListManager.updateRelativeTransform(multMatrix);
if (mode == RecordMode.COMPILE) return;
return;
}
if (isCachingEnabled()) {
getMatrixStack().mul(multMatrix);
@@ -3416,6 +3488,13 @@ public class GLStateManager {
}
}
public static void applyMultMatrix(Matrix4f multMatrix) {
if (isCachingEnabled()) {
getMatrixStack().mul(multMatrix);
bumpMatrixGeneration();
}
}
public static final Matrix4d conversionMatrix4d = new Matrix4d();
public static final Matrix4f conversionMatrix4f = new Matrix4f();
@@ -3423,10 +3502,9 @@ public class GLStateManager {
conversionMatrix4d.set(matrix);
conversionMatrix4f.set(conversionMatrix4d);
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
if (DisplayListManager.isRecording()) {
DisplayListManager.updateRelativeTransform(conversionMatrix4f);
if (mode == RecordMode.COMPILE) return;
return;
}
if (isCachingEnabled()) {
getMatrixStack().mul(conversionMatrix4f);
@@ -3434,20 +3512,24 @@ public class GLStateManager {
}
}
private static final Vector3f rotation = new Vector3f();
public static void glRotatef(float angle, float x, float y, float z) {
final float lenSq = x * x + y * y + z * z;
if (lenSq == 0.0f) return;
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
rotation.set(x, y, z).normalize();
DisplayListManager.updateRelativeTransform(angle, 0, 0, DisplayListManager.TransformOp.ROTATE, rotation);
if (mode == RecordMode.COMPILE) return;
// Fast way to normalize rotation
final float scalar = 1.0f / (float) Math.sqrt(lenSq);
x *= scalar;
y *= scalar;
z *= scalar;
// Convert deg to rad
angle = (float) Math.toRadians(angle);
if (DisplayListManager.isRecording()) {
DisplayListManager.applyMatrixRotation(angle, x, y, z);
return;
}
if (isCachingEnabled()) {
rotation.set(x, y, z).normalize();
getMatrixStack().rotate((float) Math.toRadians(angle), rotation);
getMatrixStack().rotate(angle, x, y, z);
bumpMatrixGeneration();
}
}
@@ -3455,24 +3537,29 @@ public class GLStateManager {
public static void glRotated(double angle, double x, double y, double z) {
final double lenSq = x * x + y * y + z * z;
if (lenSq == 0.0) return;
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
rotation.set((float) x, (float) y, (float) z).normalize();
DisplayListManager.updateRelativeTransform((float) angle, 0, 0, DisplayListManager.TransformOp.ROTATE, rotation);
if (mode == RecordMode.COMPILE) return;
// Fast way to normalize rotation
final double scalar = 1.0 / Math.sqrt(lenSq);
x *= scalar;
y *= scalar;
z *= scalar;
// Convert deg to rad
angle = Math.toRadians(angle);
if (DisplayListManager.isRecording()) {
DisplayListManager.applyMatrixRotation((float) angle, (float) x, (float) y, (float) z);
return;
}
if (isCachingEnabled()) {
rotation.set((float) x, (float) y, (float) z).normalize();
getMatrixStack().rotate((float) Math.toRadians(angle), rotation);
getMatrixStack().rotate((float) angle, (float) x, (float) y, (float) z);
bumpMatrixGeneration();
}
}
public static void glOrtho(double left, double right, double bottom, double top, double zNear, double zFar) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
if (DisplayListManager.isRecording()) {
DisplayListManager.updateRelativeTransformOrtho(left, right, bottom, top, zNear, zFar);
if (mode == RecordMode.COMPILE) return;
return;
}
if (isCachingEnabled()) {
getMatrixStack().ortho((float) left, (float) right, (float) bottom, (float) top, (float) zNear, (float) zFar);
@@ -3481,10 +3568,9 @@ public class GLStateManager {
}
public static void glFrustum(double left, double right, double bottom, double top, double zNear, double zFar) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
if (DisplayListManager.isRecording()) {
DisplayListManager.updateRelativeTransformFrustum(left, right, bottom, top, zNear, zFar);
if (mode == RecordMode.COMPILE) return;
return;
}
if (isCachingEnabled()) {
getMatrixStack().frustum((float) left, (float) right, (float) bottom, (float) top, (float) zNear, (float) zFar);
@@ -4195,6 +4281,7 @@ public class GLStateManager {
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
GLSMHooks.programChangeEvent.previousProgram = prev;
GLSMHooks.programChangeEvent.newProgram = 0;
GLSMHooks.programChangeEvent.postBind = false;
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
}
ffp.activate();
@@ -4215,6 +4302,7 @@ public class GLStateManager {
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
GLSMHooks.programChangeEvent.previousProgram = prev;
GLSMHooks.programChangeEvent.newProgram = program;
GLSMHooks.programChangeEvent.postBind = false;
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
}
if (initConfig != null && initConfig.isLwjglDebug()) {
@@ -4223,6 +4311,12 @@ public class GLStateManager {
}
RENDER_BACKEND.useProgram(program);
CompatUniformManager.onUseProgram(program);
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
GLSMHooks.programChangeEvent.previousProgram = prev;
GLSMHooks.programChangeEvent.newProgram = program;
GLSMHooks.programChangeEvent.postBind = true;
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
}
}
}
@@ -4297,9 +4391,21 @@ public class GLStateManager {
RENDER_BACKEND.finish();
}
public static int glGetError() { return RENDER_BACKEND.getError(); }
public static String glGetString(int pname) { return RENDER_BACKEND.getString(pname); }
public static String glGetStringi(int name, int index) { return RENDER_BACKEND.getStringi(name, index); }
public static int glGetError() {
if (initConfig.noErrorChecks()) return 0;
if (!RENDER_BACKEND.hasContext()) return 0;
return RENDER_BACKEND.getError();
}
public static String glGetString(int pname) {
if (!RENDER_BACKEND.hasContext()) return "no valid GL/render context";
return RENDER_BACKEND.getString(pname);
}
public static String glGetStringi(int name, int index) {
if (!RENDER_BACKEND.hasContext()) return "no valid GL/render context";
return RENDER_BACKEND.getStringi(name, index);
}
public static void glShaderSource(int shader, CharSequence source) {
String src = source.toString();
@@ -4342,13 +4448,7 @@ public class GLStateManager {
}
}
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
} else {
// Non-main thread or proxy texture - use direct GL call
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
}
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -4361,13 +4461,7 @@ public class GLStateManager {
}
}
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
} else {
// Non-main thread or proxy texture - use direct GL call
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
}
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -4845,12 +4939,14 @@ public class GLStateManager {
private static void invalidateDeletedBuffer(int buffer) {
if (buffer == 0) return;
if (boundVBO == buffer) {
boundVBO = 0;
}
if (boundEBO == buffer) {
boundEBO = 0;
vaoEboMap.put(boundVAO, 0);
if (boundVBO == buffer) boundVBO = 0;
if (boundEBO == buffer) boundEBO = 0;
if (boundPixelUnpackBuffer == buffer) boundPixelUnpackBuffer = 0;
if (boundPixelPackBuffer == buffer) boundPixelPackBuffer = 0;
// Sweep all VAOs: an unbound VAO may later rebind with a dangling EBO id.
for (Int2IntMap.Entry e : vaoEboMap.int2IntEntrySet()) {
if (e.getIntValue() == buffer) e.setValue(0);
}
}
@@ -4869,25 +4965,25 @@ public class GLStateManager {
RENDER_BACKEND.bindBuffer(target, buffer);
}
private static void suspendPixelUnpackBuffer() {
static void suspendPixelUnpackBuffer() {
if (boundPixelUnpackBuffer != 0) {
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0);
}
}
private static void restorePixelUnpackBuffer() {
static void restorePixelUnpackBuffer() {
if (boundPixelUnpackBuffer != 0) {
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, boundPixelUnpackBuffer);
}
}
private static void suspendPixelPackBuffer() {
static void suspendPixelPackBuffer() {
if (boundPixelPackBuffer != 0) {
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0);
}
}
private static void restorePixelPackBuffer() {
static void restorePixelPackBuffer() {
if (boundPixelPackBuffer != 0) {
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_PACK_BUFFER, boundPixelPackBuffer);
}
@@ -4967,14 +5063,6 @@ public class GLStateManager {
glBindVertexArray(array);
}
public static boolean isVBOBound() {
return boundVBO != 0;
}
public static boolean isVAOBound() {
return boundVAO != 0;
}
public static boolean vendorIsAMD() {
return VENDOR == AMD;
}
@@ -5093,7 +5181,7 @@ public class GLStateManager {
}
public static boolean isFramebufferEnabled() {
return initConfig != null && initConfig.isFramebufferSupported() && initConfig.isFboEnabled();
return true;
}
private static final boolean FFP_WARN_ON_UNSUPPORTED = Boolean.parseBoolean(System.getProperty("angelica.ffp.warnOnUnsupported", "false"));
@@ -5558,7 +5646,10 @@ public class GLStateManager {
}
public static void glDeleteProgram(int program) {
if (program == 0) return;
CompatUniformManager.onDeleteProgram(program);
if (activeProgram == program) activeProgram = 0;
RENDER_BACKEND.deleteProgram(program);
}
@@ -5618,6 +5709,14 @@ public class GLStateManager {
return RENDER_BACKEND.getProgramInfoLog(program);
}
public static boolean glIsProgram(int obj) {
return RENDER_BACKEND.isProgram(obj);
}
public static boolean glIsShader(int obj) {
return RENDER_BACKEND.isShader(obj);
}
public static void glBindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) { RENDER_BACKEND.bindImageTexture(unit, texture, level, layered, layer, access, format); }
public static void glMemoryBarrier(int barriers) { RENDER_BACKEND.memoryBarrier(barriers); }
public static void glCopyImageSubData(int srcName, int srcTarget, int srcLevel, int srcX, int srcY, int srcZ,
@@ -5817,4 +5916,14 @@ public class GLStateManager {
public static void glGetTexImage(int target, int level, int format, int type, long pixels) {
GL11.glGetTexImage(target, level, format, type, pixels);
}
/**
* Draw modes that rely on the previous vertex data cannot be merged currently.
* It is possible to merge them using Index Buffers, but currently unimplemented.
*/
public static boolean isContinuousDraw(int drawMode) {
return drawMode == GL11.GL_TRIANGLE_STRIP || drawMode == GL11.GL_TRIANGLE_FAN
|| drawMode == GL11.GL_LINE_STRIP || drawMode == GL11.GL_LINE_LOOP
|| drawMode == GL11.GL_QUAD_STRIP || drawMode == GL11.GL_POLYGON;
}
}
@@ -36,9 +36,10 @@ public class GlslTransformUtils {
/** 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(
// Always rename sample, glsl-transformation-lib doesn't like it
// Some reserved words to always rename
0, List.of(
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample")
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"),
new ReservedWordRename(Pattern.compile("\\bnew\\b"), RENAMED_PREFIX + "new")
),
400, List.of(
new ReservedWordRename(Pattern.compile("\\bsampler\\b(?!\\d)"), RENAMED_PREFIX + "sampler")
@@ -146,42 +146,11 @@ public final class QuadConverter {
final int bytesPerIndex = VertexAttribState.Attrib.glTypeSizeBytes(type);
// Read source indices from caller's EBO
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
RENDER_BACKEND.getBufferSubData(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);
}
triangulateQuads(memAddress0(src), type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
memFree(src);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
@@ -198,16 +167,8 @@ public final class QuadConverter {
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);
}
final long srcAddr = memAddress0(indices) + (long) indices.position() * 4;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
@@ -223,16 +184,8 @@ public final class QuadConverter {
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);
}
final long srcAddr = memAddress0(indices) + (long) indices.position() * 2;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
}
@@ -248,31 +201,7 @@ public final class QuadConverter {
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);
}
triangulateQuads(srcAddr, type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
@@ -282,14 +211,140 @@ public final class QuadConverter {
*/
public static void destroy() {
if (eboId != 0) {
RENDER_BACKEND.deleteBuffers(eboId);
GLStateManager.glDeleteBuffers(eboId);
eboId = 0;
maxQuads = 0;
}
if (scratchEboId != 0) {
RENDER_BACKEND.deleteBuffers(scratchEboId);
GLStateManager.glDeleteBuffers(scratchEboId);
scratchEboId = 0;
scratchEboCapacity = 0;
}
}
/**
* Read one index at {@code srcAddr + i * sizeof(srcType)} and return its
* zero-extended unsigned value.
*/
private static int readIndex(long srcAddr, int srcType, int i) {
return switch (srcType) {
case GL11.GL_UNSIGNED_BYTE -> memGetByte(srcAddr + i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> memGetShort(srcAddr + i * 2L) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> memGetInt(srcAddr + i * 4L);
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(srcType));
};
}
/**
* Write one index at {@code dstAddr + i * sizeof(dstType)}.
*/
private static void writeIndex(long dstAddr, int dstType, int i, int value) {
switch (dstType) {
case GL11.GL_UNSIGNED_SHORT -> memPutShort(dstAddr + i * 2L, (short) value);
case GL11.GL_UNSIGNED_INT -> memPutInt(dstAddr + i * 4L, value);
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(dstType));
}
}
/**
* Read {@code quadCount} quads from {@code srcAddr} and emit 6 triangle indices
* per quad at {@code dstAddr} using {@code GL_LAST_VERTEX_CONVENTION}:
* {@code (a,b,d), (b,c,d)}. v3 is the provoking vertex for both triangles;
* diagonal runs b→d. Matches Mesa's {@code do_quad} LAST path.
*
* <p>Caller allocates dst of size {@code quadCount * 6 * sizeof(dstType)}.
*
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT}
* / {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Widening only.
*/
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount) {
triangulateQuads(srcAddr, srcType, dstAddr, dstType, quadCount, 0);
}
/**
* Same as {@link #triangulateQuads(long, int, long, int, int)} but subtracts
* {@code minVtx} from every emitted index, rebasing to a 0-based buffer in
* one pass.
*/
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount, int minVtx) {
for (int i = 0; i < quadCount; i++) {
final int a = readIndex(srcAddr, srcType, i * 4) - minVtx;
final int b = readIndex(srcAddr, srcType, i * 4 + 1) - minVtx;
final int c = readIndex(srcAddr, srcType, i * 4 + 2) - minVtx;
final int d = readIndex(srcAddr, srcType, i * 4 + 3) - minVtx;
final int o = i * 6;
writeIndex(dstAddr, dstType, o, a);
writeIndex(dstAddr, dstType, o + 1, b);
writeIndex(dstAddr, dstType, o + 2, d);
writeIndex(dstAddr, dstType, o + 3, b);
writeIndex(dstAddr, dstType, o + 4, c);
writeIndex(dstAddr, dstType, o + 5, d);
}
}
/**
* Copy {@code count} indices from {@code srcAddr} to {@code dstAddr}, converting
* from {@code srcType} to {@code dstType}.
*
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Widening only; narrowing is rejected by
* {@link #writeIndex(long, int, int, int) writeIndex}.
*/
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count) {
widenIndices(srcAddr, srcType, dstAddr, dstType, count, 0);
}
/**
* Same as {@link #widenIndices(long, int, long, int, int)} but subtracts
* {@code minVtx} from every written index in one pass.
*/
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count, int minVtx) {
for (int i = 0; i < count; i++) {
writeIndex(dstAddr, dstType, i, readIndex(srcAddr, srcType, i) - minVtx);
}
}
/**
* Scan {@code count} indices at {@code srcAddr} of {@code srcType}. Returns
* {@code (max << 32) | (min & 0xFFFFFFFFL)} or {@code -1L} for an unsupported
* type or empty range.
*/
public static long scanMinMaxIndex(long srcAddr, int srcType, int count) {
if (count <= 0) return -1L;
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
switch (srcType) {
case GL11.GL_UNSIGNED_BYTE -> {
for (int i = 0; i < count; i++) {
final int v = memGetByte(srcAddr + i) & 0xFF;
if (v < min) min = v;
if (v > max) max = v;
}
}
case GL11.GL_UNSIGNED_SHORT -> {
for (int i = 0; i < count; i++) {
final int v = memGetShort(srcAddr + i * 2L) & 0xFFFF;
if (v < min) min = v;
if (v > max) max = v;
}
}
case GL11.GL_UNSIGNED_INT -> {
for (int i = 0; i < count; i++) {
final long v = memGetInt(srcAddr + i * 4L) & 0xFFFFFFFFL;
if (v < min) min = v;
if (v > max) max = v;
}
// readIndex / writeIndex / rebase all route UINT through signed int.
// Reject the > 2^31 slice so the caller bails rather than silently
// emitting negative indices. MC never hits this in practice.
if (max > Integer.MAX_VALUE) return -1L;
}
default -> {
return -1L;
}
}
return (max << 32) | (min & 0xFFFFFFFFL);
}
}
@@ -7,7 +7,9 @@ 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.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.joml.Vector3i;
@@ -47,6 +49,9 @@ public class RenderSystem {
private static int maxSSBOBindings;
private static int maxGlslVersion;
private static boolean supportsGpuShader4;
@Getter private static int glMajor;
@Getter private static int glMinor;
@Getter private static boolean coreProfile;
// Sampler object state tracking (null if unsupported)
private static int[] samplers;
@@ -61,7 +66,13 @@ public class RenderSystem {
if (rendererInitialized) return;
rendererInitialized = true;
try {
if (GLStateManager.capabilities.OpenGL45) {
if (GLStateManager.vendorIsIntel() && GLStateManager.isWindows()) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("Detected Intel drivers on Windows, disabling DSA.");
} else if (!GLStateManager.getInitConfig().isDSAEnabled()) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("enableDSA is set to false, disabling DSA.");
} else if (GLStateManager.capabilities.OpenGL45) {
dsaState = (Runtime.version().feature() > 8 && GLStateManager.capabilities.GL_EXT_direct_state_access) ? new DSAEXT() : new DSACore();
GLStateManager.LOGGER.info("OpenGL 4.5 detected, enabling DSA.");
}
@@ -133,10 +144,17 @@ public class RenderSystem {
if (GLStateManager.capabilities.OpenGL32) {
final int profileMask = RENDER_BACKEND.getInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
coreProfile = true;
GLStateManager.LOGGER.info("GL 3.3 core profile detected, enabling FFP shader emulation.");
ShaderManager.getInstance().enable();
}
}
final Matcher glVerMatcher = SEMVER_PATTERN.matcher(RENDER_BACKEND.getString(GL11.GL_VERSION));
if (glVerMatcher.matches()) {
glMajor = Integer.parseInt(glVerMatcher.group("major"));
glMinor = Integer.parseInt(glVerMatcher.group("minor"));
}
}
public static void generateMipmaps(int texture, int mipmapTarget) {
@@ -149,7 +167,9 @@ 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(target, texture);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
if (target == GL11.GL_TEXTURE_2D && level == 0) {
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
@@ -218,29 +238,41 @@ public class RenderSystem {
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
GLStateManager.suspendPixelUnpackBuffer();
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static void texImage1D(int texture, int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) {
RENDER_BACKEND.bindTexture(target, texture);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage1D(target, level, internalformat, width, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
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) {
RENDER_BACKEND.bindTexture(target, texture);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels);
GLStateManager.restorePixelUnpackBuffer();
}
public static String getProgramInfoLog(int program) {
@@ -264,7 +296,9 @@ public class RenderSystem {
}
public static void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels) {
GLStateManager.suspendPixelPackBuffer();
RENDER_BACKEND.readPixels(x, y, width, height, format, type, pixels);
GLStateManager.restorePixelPackBuffer();
}
public static void bufferData(int target, FloatBuffer data, int usage) {
@@ -432,7 +466,7 @@ public class RenderSystem {
}
public static void deleteBuffers(int buffer) {
RENDER_BACKEND.deleteBuffers(buffer);
GLStateManager.glDeleteBuffers(buffer);
}
public static long getVRAM() {
@@ -491,7 +525,7 @@ public class RenderSystem {
public static void destroySampler(int sampler) {
if (!supportsSamplerObjects || sampler == 0) return;
RENDER_BACKEND.deleteSamplers(sampler);
GLStateManager.glDeleteSamplers(sampler);
}
public static void samplerParameteri(int sampler, int pname, int param) {
@@ -1,132 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.MultMatrixCmd;
import org.joml.Matrix4f;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* Helper class for tracking and collapsing matrix transforms during optimization.
* Implements the transform accumulation and emission strategy:
* <ul>
* <li>accumulated: total relative transform (what GL matrix should be)</li>
* <li>lastEmitted: what we've emitted (what GL matrix is)</li>
* <li>stack: saved accumulated values at Push points</li>
* </ul>
*/
class TransformOptimizer {
private final Matrix4f accumulated = new Matrix4f(); // Current accumulated transform
private final Matrix4f lastEmitted = new Matrix4f(); // Last emitted transform
private final Deque<Matrix4f> stack = new ArrayDeque<>(); // For Push/Pop
private final int listId; // For logging
private boolean absoluteMatrixFlag = false; // True after LoadMatrix, cleared by LoadIdentity emit
TransformOptimizer(int listId) {
this.listId = listId;
accumulated.identity();
lastEmitted.identity();
}
Matrix4f getAccumulated() {
return accumulated;
}
void loadIdentity() {
accumulated.identity();
}
void pushTransform() {
stack.push(new Matrix4f(accumulated));
}
void popTransform() {
if (!stack.isEmpty()) {
final Matrix4f popped = stack.pop();
accumulated.set(popped);
// After Pop, GL state is restored to the pushed value so lastEmitted should also reflect this
lastEmitted.set(accumulated);
} else {
GLStateManager.LOGGER.warn("[TransformOptimizer] list={} Pop with empty stack - resetting to identity", listId);
accumulated.identity();
lastEmitted.identity();
}
}
boolean isIdentity() {
return DisplayListManager.isIdentity(accumulated);
}
/**
* Check if there's a pending transform that hasn't been emitted yet.
*/
boolean hasPendingTransform() {
return !accumulated.equals(lastEmitted);
}
/**
* Emit a MultMatrix command if accumulated differs from lastEmitted.
* Updates lastEmitted to match accumulated after emission.
*/
void emitPendingTransform(List<DisplayListCommand> output) {
if (!accumulated.equals(lastEmitted)) {
emitTransformTo(output, accumulated);
}
}
/**
* Emit a MultMatrix command to make GL state match a target transform.
* Used when a draw expects a specific transform that may differ from accumulated.
* Updates lastEmitted to match target after emission.
*
* @param output The command list to emit to
* @param target The target transform to reach
*/
void emitTransformTo(List<DisplayListCommand> output, Matrix4f target) {
if (target.equals(lastEmitted)) {
// Already at target - nothing to emit
return;
}
// Calculate delta: what we need to multiply to go from lastEmitted to target
// delta = inverse(lastEmitted) * target
// But for simplicity, if lastEmitted is identity, just emit target
if (DisplayListManager.isIdentity(lastEmitted)) {
// Simple case: just emit target
output.add(MultMatrixCmd.create(target));
} else {
// Need to emit delta: inv(lastEmitted) * target
final Matrix4f delta = new Matrix4f(lastEmitted).invert().mul(target);
output.add(MultMatrixCmd.create(delta));
}
lastEmitted.set(target);
}
/**
* Check if a draw's expected transform differs from what we've emitted.
*/
boolean needsTransformForDraw(Matrix4f drawTransform) {
return !drawTransform.equals(lastEmitted);
}
/**
* Mark that the GL matrix is at an absolute value (after LoadMatrix).
* Subsequent LoadIdentity commands must be emitted to actually reset GL to identity.
*/
void markAbsoluteMatrix() {
absoluteMatrixFlag = true;
}
/**
* Check if the GL matrix is at an absolute value and clear the flag.
* Used to determine if LoadIdentity needs to emit.
* @return true if LoadMatrix was called and not yet reset by LoadIdentity
*/
boolean checkAndClearAbsoluteMatrix() {
boolean was = absoluteMatrixFlag;
absoluteMatrixFlag = false;
return was;
}
}
@@ -9,6 +9,7 @@ import org.lwjgl.opengl.ARBDebugOutputCallback;
import org.lwjgl.opengl.ARBDirectStateAccess;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.EXTDirectStateAccess;
import org.lwjgl.opengl.GLContext;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
@@ -67,7 +68,7 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
@Override
public void init() {
caps = org.lwjgl.opengl.GLContext.getCapabilities();
caps = GLContext.getCapabilities();
activeDebugExtension = 0;
}
@@ -86,6 +87,15 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
return "OpenGL (LWJGL2)";
}
@Override
public boolean hasContext() {
try {
return GLContext.getCapabilities() != null;
} catch (Exception e) {
return false;
}
}
@Override
public int getMinGLSLVersion() {return 330;}
@@ -19,6 +19,9 @@ public abstract class RenderBackend {
public abstract boolean isAvailable();
public abstract String getName();
/** Returns true if the current thread has a valid render context. Safe to call from any thread. */
public abstract boolean hasContext();
/** Higher priority backends are preferred. */
public int getPriority() { return 0; }
@@ -273,23 +273,27 @@ public final class FragmentShaderGenerator {
sb.append(" // Alpha test\n");
final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc());
sb.append(" float qa = round(color.a * 255.0);\n");
sb.append(" float qr = round(u_AlphaRef * 255.0);\n");
switch (func) {
case GL11.GL_NEVER ->
sb.append(" discard;\n");
case GL11.GL_LESS ->
sb.append(" if (qa >= qr) discard;\n");
case GL11.GL_EQUAL ->
sb.append(" if (qa >= u_AlphaRef * 255.0) discard;\n");
case GL11.GL_EQUAL -> {
sb.append(" float qr = round(u_AlphaRef * 255.0);\n");
sb.append(" if (qa != qr) discard;\n");
}
case GL11.GL_LEQUAL ->
sb.append(" if (qa > qr) discard;\n");
sb.append(" if (qa > u_AlphaRef * 255.0) discard;\n");
case GL11.GL_GREATER ->
sb.append(" if (qa <= qr) discard;\n");
case GL11.GL_NOTEQUAL ->
sb.append(" if (qa <= u_AlphaRef * 255.0) discard;\n");
case GL11.GL_NOTEQUAL -> {
sb.append(" float qr = round(u_AlphaRef * 255.0);\n");
sb.append(" if (qa == qr) discard;\n");
}
case GL11.GL_GEQUAL ->
sb.append(" if (qa < qr) discard;\n");
sb.append(" if (qa < u_AlphaRef * 255.0) discard;\n");
case GL11.GL_ALWAYS -> {}
}
}
@@ -178,7 +178,7 @@ public class Program {
}
public void destroy() {
RENDER_BACKEND.deleteProgram(programId);
GLStateManager.glDeleteProgram(programId);
}
/**
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import org.joml.Math;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector4f;
@@ -42,8 +43,8 @@ public class Uniforms {
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);
private static final double LN2 = java.lang.Math.log(2.0);
private static final double SQRT_LN2 = java.lang.Math.sqrt(LN2);
// Dirty tracking: last-uploaded generation per category + program ID.
// Program change forces full re-upload since uniform locations differ.
@@ -315,10 +316,10 @@ public class Uniforms {
// 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.put(Math.clamp(0f, 1f, color.getRed()));
vec4Buf.put(Math.clamp(0f, 1f, color.getGreen()));
vec4Buf.put(Math.clamp(0f, 1f, color.getBlue()));
vec4Buf.put(Math.clamp(0f, 1f, color.getAlpha()));
vec4Buf.flip();
RENDER_BACKEND.uniform4(program.locCurrentColor, vec4Buf);
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.MatrixHelper;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
@@ -182,7 +183,7 @@ public final class VertexKey {
// Check if texture matrix unit 0 is non-identity
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
if (!DisplayListManager.isIdentity(texMat)) {
if (!MatrixHelper.isIdentity(texMat)) {
bits |= (1L << BIT_TEX_MATRIX);
}
@@ -1,12 +1,6 @@
package com.gtnewhorizons.angelica.glsm.hooks;
import com.gtnewhorizons.angelica.glsm.hooks.events.BlendFuncChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.FogStateChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.LightmapCoordsEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.ProgramChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureBindEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureDeleteEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureUnitStateEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.*;
import net.minecraftforge.eventbus.api.bus.EventBus;
public final class GLSMHooks {
@@ -23,6 +17,7 @@ public final class GLSMHooks {
public static final EventBus<BlendFuncChangeEvent> BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class);
public static final EventBus<FogStateChangeEvent> FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class);
public static final EventBus<LightmapCoordsEvent> LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class);
public static final EventBus<AlphaStateChangeEvent> ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class);
// Reusable event instances
public static final TextureBindEvent textureBindEvent = new TextureBindEvent();
@@ -32,6 +27,7 @@ public final class GLSMHooks {
public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent();
public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent();
public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent();
public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent();
private GLSMHooks() {}
}
@@ -8,24 +8,24 @@ import java.util.function.Consumer;
public final class GLSMInitConfig {
private final boolean lwjglDebug;
private final StreamingUploader.UploadStrategy streamingUploadStrategy;
private final boolean framebufferSupported;
private final boolean fboEnabled;
private final Consumer<DirectTessellator> directDrawer;
private final Runnable streamingDrawerDestroy;
private final int displayWidth;
private final int displayHeight;
private final Runnable postInitCallback;
private final boolean enableDSA;
private final boolean noErrorChecks;
private GLSMInitConfig(Builder builder) {
this.lwjglDebug = builder.lwjglDebug;
this.streamingUploadStrategy = builder.streamingUploadStrategy;
this.framebufferSupported = builder.framebufferSupported;
this.fboEnabled = builder.fboEnabled;
this.noErrorChecks = builder.noErrorChecks;
this.directDrawer = builder.directDrawer;
this.streamingDrawerDestroy = builder.streamingDrawerDestroy;
this.displayWidth = builder.displayWidth;
this.displayHeight = builder.displayHeight;
this.postInitCallback = builder.postInitCallback;
this.enableDSA = builder.enableDSA;
}
public static Builder builder() {
@@ -34,24 +34,24 @@ public final class GLSMInitConfig {
public boolean isLwjglDebug() { return lwjglDebug; }
public StreamingUploader.UploadStrategy getStreamingUploadStrategy() { return streamingUploadStrategy; }
public boolean isFramebufferSupported() { return framebufferSupported; }
public boolean isFboEnabled() { return fboEnabled; }
public Consumer<DirectTessellator> getDirectDrawer() { return directDrawer; }
public Runnable getStreamingDrawerDestroy() { return streamingDrawerDestroy; }
public int getDisplayWidth() { return displayWidth; }
public int getDisplayHeight() { return displayHeight; }
public Runnable getPostInitCallback() { return postInitCallback; }
public boolean isDSAEnabled() { return enableDSA; }
public boolean noErrorChecks() { return noErrorChecks; }
public static final class Builder {
private boolean lwjglDebug = false;
private StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.BUFFER_DATA;
private boolean framebufferSupported = false;
private boolean fboEnabled = false;
private Consumer<DirectTessellator> directDrawer = null;
private Runnable streamingDrawerDestroy = null;
private int displayWidth = 0;
private int displayHeight = 0;
private Runnable postInitCallback = null;
private boolean enableDSA = false;
private boolean noErrorChecks;
private Builder() {}
@@ -65,13 +65,8 @@ public final class GLSMInitConfig {
return this;
}
public Builder framebufferSupported(boolean framebufferSupported) {
this.framebufferSupported = framebufferSupported;
return this;
}
public Builder fboEnabled(boolean fboEnabled) {
this.fboEnabled = fboEnabled;
public Builder noErrorChecks(boolean noErrorChecks) {
this.noErrorChecks = noErrorChecks;
return this;
}
@@ -96,6 +91,11 @@ public final class GLSMInitConfig {
return this;
}
public Builder enableDSA(boolean enableDSA) {
this.enableDSA = enableDSA;
return this;
}
public GLSMInitConfig build() {
return new GLSMInitConfig(this);
}
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
/**
* AlphaTest changed - Listeners should query GLSM alpha state directly if they care what changed.
*/
public final class AlphaStateChangeEvent extends MutableEvent {
}
@@ -5,4 +5,5 @@ import net.minecraftforge.eventbus.api.event.MutableEvent;
public final class ProgramChangeEvent extends MutableEvent {
public int previousProgram;
public int newProgram;
public boolean postBind;
}
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.glsm.loading;
import java.util.List;
/**
* Verifies required classes/resources are present on the classpath at load time.
* Fails fast with a descriptive error message if a dependency is missing.
*/
public final class DependencyVerifier {
/**
* A single dependency check: a classpath resource that must exist.
* @param resourcePath Resource path to check (e.g. "/com/example/SomeClass.class")
* @param errorMessage Human-readable error message if missing (include download URL)
*/
public record Check(String resourcePath, String errorMessage) {}
/**
* Verify all checks pass. Throws RuntimeException on first failure.
* @param anchor Class whose classloader to use for resource lookups
* @param checks Dependency checks to run
*/
public static void verify(Class<?> anchor, List<Check> checks) {
for (Check check : checks) {
if (anchor.getResource(check.resourcePath()) == null) {
throw new RuntimeException(check.errorMessage());
}
}
}
private DependencyVerifier() {}
}
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.glsm.loading;
import com.gtnewhorizons.angelica.glsm.redirect.GLSMRedirector;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public final class EarlyRedirectorCore {
private final GLSMRedirector core;
private final String[] targets;
private final String[] exclusions;
public EarlyRedirectorCore(String... extraExclusions) {
this.core = new GLSMRedirector();
this.targets = EcosystemNarrowRules.EARLY_REDIRECTOR_TARGETS.clone();
final List<String> excl = new ArrayList<>(Arrays.asList(core.getCoreExclusions()));
if (extraExclusions != null && extraExclusions.length > 0) {
Collections.addAll(excl, extraExclusions);
}
this.exclusions = excl.toArray(new String[0]);
}
public byte[] transform(String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
if (!startsWithAny(transformedName, targets)) return basicClass;
if (startsWithAny(transformedName, exclusions)) return basicClass;
if (!core.shouldTransform(basicClass)) return basicClass;
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
if (core.transformClassNode(transformedName, cn)) {
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
}
return basicClass;
}
private static boolean startsWithAny(String name, String[] prefixes) {
for (String p : prefixes) {
if (name.startsWith(p)) return true;
}
return false;
}
}
@@ -0,0 +1,78 @@
package com.gtnewhorizons.angelica.glsm.loading;
import com.gtnewhorizons.angelica.glsm.loading.TransformerNarrower.NarrowRule;
import java.util.List;
public final class EcosystemNarrowRules {
public static final List<NarrowRule> ALL = List.of(
new NarrowRule("DragonAPI", "Reika.DragonAPI.ASM", List.of(
"Reika.DragonAPI.ASM.Patchers",
"Reika.DragonAPI.ASM.Profiling",
"Reika.DragonAPI.ASM.APIStripper",
"Reika.DragonAPI.ASM.ClassReparenter",
"Reika.DragonAPI.ASM.DependentMethodStripper",
"Reika.DragonAPI.ASM.DragonAPIClassTransformer",
"Reika.DragonAPI.ASM.FMLItemBlockPatch",
"Reika.DragonAPI.ASM.FluidNamePatch",
"Reika.DragonAPI.ASM.InterfaceInjector",
"Reika.DragonAPI.ASM.SpecialBiomePlacement",
"Reika.DragonAPI.ASM.StructureLootHooks"
)),
new NarrowRule("Xaeros_common", "xaero.common.core", List.of(
"xaero.common.core.transformer"
)),
new NarrowRule("Xaeros_map", "xaero.map.core", List.of(
"xaero.map.core.transformer"
)),
new NarrowRule("AdvancedLightsabers", "com.fiskmods.lightsabers.asm", List.of(
"com.fiskmods.lightsabers.asm.ALLoadingPlugin",
"com.fiskmods.lightsabers.asm.ASMHooks",
"com.fiskmods.lightsabers.asm.transformers"
)),
new NarrowRule("Ears", "com.unascribed.ears", List.of(
"com.unascribed.ears.asm",
"com.unascribed.ears.common.agent"
)),
new NarrowRule("Alfheim", "alfheim.common.core.asm.hook", List.of(
"alfheim.common.core.asm.hook.AlfheimFieldHookHandler",
"alfheim.common.core.asm.hook.AlfheimHPHooks",
"alfheim.common.core.asm.hook.Botania18AndUpBackport",
"alfheim.common.core.asm.hook.ElementalDamageAdapter",
"alfheim.common.core.asm.hook.integration",
"alfheim.common.core.asm.hook.extender.FurnaceExtender",
"alfheim.common.core.asm.hook.extender.ItemAuraRingExtender",
"alfheim.common.core.asm.hook.extender.ItemLensExtender",
"alfheim.common.core.asm.hook.extender.ItemTwigWandExtender",
"alfheim.common.core.asm.hook.extender.LightRelayExtender",
"alfheim.common.core.asm.hook.extender.PureDaisyExtender",
"alfheim.common.core.asm.hook.extender.QuartzExtender",
"alfheim.common.core.asm.hook.extender.RelicHooks",
"alfheim.common.core.asm.hook.extender.SparkExtender",
"alfheim.common.core.asm.hook.fixes.BotaniaGlowingRenderFixes",
"alfheim.common.core.asm.hook.fixes.CorporeaInputFix",
"alfheim.common.core.asm.hook.fixes.GodAttributesHooks",
"alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix"
))
);
public static final String[] LWJGL3IFY_EXCLUSIONS_SHARED = {
"org.embeddedt.embeddium",
"org.taumc.celeritas",
"com.mitchej123.lwjgl.lwjgl3",
"com.mitchej123.glsm",
"com.gtnewhorizons.angelica.lwjgl3",
};
public static final String[] EARLY_REDIRECTOR_TARGETS = {
"cn.tesseract.mycelium.",
};
private EcosystemNarrowRules() {}
}
@@ -0,0 +1,66 @@
package com.gtnewhorizons.angelica.glsm.loading;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Narrows overly-broad transformer exclusions that prevent the GLSM redirector from
* transforming GL calls in mod code. Some coremods register exclusions covering their
* entire package tree, which inadvertently shields rendering code from redirection.
* <p>
* This utility removes the broad exclusion and re-adds narrower ones that cover only
* the coremod's own transformer/ASM classes, leaving rendering code exposed.
* <p>
* Operates on a {@code Set<String>} (the transformer exceptions set from LaunchClassLoader)
* and a {@code Map<String, Object>} (the Launch blackboard). Callers extract these from
* the Forge/launchwrapper infrastructure.
*/
public final class TransformerNarrower {
private static final Logger LOGGER = LogManager.getLogger("GLSM");
/**
* A rule that replaces a broad transformer exclusion with narrower ones.
* @param name Human-readable name for logging (e.g. "DragonAPI")
* @param broad The broad exclusion to remove (e.g. "Reika.DragonAPI.ASM")
* @param narrow Narrower exclusions to add in its place
*/
public record NarrowRule(String name, String broad, List<String> narrow) {}
/**
* Apply narrowing rules to the transformer exceptions set.
*
* @param exceptions The mutable Set from LaunchClassLoader.transformerExceptions
* @param blackboard The Launch.blackboard map (checked for disable flags)
* @param keyPrefix Blackboard key prefix for per-rule opt-out (e.g. "angelica" or "umbra").
* Rule "DragonAPI" checks key "{prefix}.narrow.DragonAPI". If explicitly
* set to Boolean.FALSE, the rule is skipped.
* @param callerName Name for log messages (e.g. "Angelica" or "Umbra")
* @param rules The narrowing rules to apply
* @return number of rules that were applied
*/
public static int narrow(Set<String> exceptions, Map<String, Object> blackboard,
String keyPrefix, String callerName, List<NarrowRule> rules) {
int applied = 0;
for (NarrowRule rule : rules) {
final String key = keyPrefix + ".narrow." + rule.name();
if (Boolean.FALSE.equals(blackboard.get(key))) {
continue;
}
if (exceptions.remove(rule.broad())) {
for (String narrow : rule.narrow()) {
exceptions.add(narrow);
}
LOGGER.info("[{}] Narrowed {} transformer exclusion to allow GL redirection", callerName, rule.broad());
applied++;
}
}
return applied;
}
private TransformerNarrower() {}
}
@@ -16,17 +16,13 @@ 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 stateGeneration; // Position in command list for state tracking
public RestoreData restoreData;
public AccumulatedDraw(DirectTessellator tessellator, int commandIndex, int stateGeneration, boolean copyLast) {
public AccumulatedDraw(DirectTessellator tessellator, 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);
@@ -36,7 +32,6 @@ public final class AccumulatedDraw {
public void mergeDraw(DirectTessellator tessellator, boolean copyLast) {
this.drawBuffers.add(tessellator.allocateBufferCopy());
if (copyLast) {
this.restoreData = new RestoreData(tessellator);
}
@@ -0,0 +1,178 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public final class AttribSnapshot {
public record AttribDesc(int location, int size, int type, boolean normalized, int stride, long offset, int sourceVboId, ByteBuffer readBuffer, long readBufferBaseOffset) {
public int effectiveStride() {
return stride != 0 ? stride : size * VertexAttribState.Attrib.glTypeSizeBytes(type);
}
public int typeSizeBytes() {
return VertexAttribState.Attrib.glTypeSizeBytes(type);
}
}
private final AttribDesc[] perLocation;
private final List<ByteBuffer> allocatedBuffers;
private boolean freed;
private AttribSnapshot(AttribDesc[] perLocation, List<ByteBuffer> allocatedBuffers) {
this.perLocation = perLocation;
this.allocatedBuffers = allocatedBuffers;
}
public AttribDesc get(int location) {
return (location >= 0 && location < perLocation.length) ? perLocation[location] : null;
}
public int maxLocations() {
return perLocation.length;
}
public void free() {
if (freed) return;
freed = true;
for (ByteBuffer buf : allocatedBuffers) {
MemoryUtilities.memFree(buf);
}
allocatedBuffers.clear();
}
/**
* Snapshot attribs over a tight vertex window {@code [firstVertex, firstVertex+vertexCount)}.
*
* <p>Hot path (single VBO backing every enabled attrib — the typical draw) runs without
* allocating any hashmaps. A second distinct VBO promotes to the map-backed fallback.
*/
public static AttribSnapshot snapshot(int firstVertex, int vertexCount) {
final AttribDesc[] out = new AttribDesc[VertexAttribState.MAX_ATTRIBS];
final List<ByteBuffer> allocated = new ArrayList<>();
final int prevVBO = GLStateManager.getBoundVBO();
// Pass 1 — unioned [start,end] per vboId. Hot path: one source VBO, zero allocations.
int rangeSoleVboId = 0;
long rangeSoleStart = Long.MAX_VALUE;
long rangeSoleEnd = Long.MIN_VALUE;
Int2ObjectOpenHashMap<long[]> vboRanges = null;
final long lastVtx = (long) firstVertex + vertexCount - 1;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled || a.vboId == 0) continue;
final int stride = a.effectiveStride();
final long typeBytes = (long) a.size * VertexAttribState.Attrib.glTypeSizeBytes(a.type);
final long start = a.offset + (long) firstVertex * stride;
final long end = a.offset + lastVtx * stride + typeBytes;
if (vboRanges != null) {
long[] r = vboRanges.get(a.vboId);
if (r == null) {
vboRanges.put(a.vboId, new long[] { start, end });
} else {
if (start < r[0]) r[0] = start;
if (end > r[1]) r[1] = end;
}
} else if (rangeSoleVboId == 0 || rangeSoleVboId == a.vboId) {
rangeSoleVboId = a.vboId;
if (start < rangeSoleStart) rangeSoleStart = start;
if (end > rangeSoleEnd) rangeSoleEnd = end;
} else {
// Spill: second distinct vboId. Promote the accumulated range to the map.
vboRanges = new Int2ObjectOpenHashMap<>();
vboRanges.put(rangeSoleVboId, new long[] { rangeSoleStart, rangeSoleEnd });
vboRanges.put(a.vboId, new long[] { start, end });
}
}
// Pass 2 — readback cache. Same spill pattern.
int readSoleVboId = 0;
ByteBuffer readSoleBuf = null;
long readSoleOffset = 0;
Int2ObjectOpenHashMap<ByteBuffer> vboBuffers = null;
Int2LongOpenHashMap vboReadOffsets = null;
try {
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled) continue;
if (a.vboId != 0) {
ByteBuffer buf;
long readOffset;
if (vboBuffers != null) {
buf = vboBuffers.get(a.vboId);
readOffset = buf != null ? vboReadOffsets.get(a.vboId) : 0;
} else if (readSoleVboId == a.vboId) {
buf = readSoleBuf;
readOffset = readSoleOffset;
} else {
buf = null;
readOffset = 0;
}
if (buf == null) {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long rStart;
final long rEnd;
if (vboRanges != null) {
final long[] range = vboRanges.get(a.vboId);
rStart = range[0];
rEnd = range[1];
} else {
rStart = rangeSoleStart;
rEnd = rangeSoleEnd;
}
final long ro = Math.max(0, rStart);
final int rs = (int) (Math.min(bufferSize, rEnd) - ro);
if (rs <= 0) continue;
buf = MemoryUtilities.memAlloc(rs);
GLStateManager.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, ro, buf);
allocated.add(buf);
readOffset = ro;
if (vboBuffers != null) {
vboBuffers.put(a.vboId, buf);
vboReadOffsets.put(a.vboId, ro);
} else if (readSoleVboId == 0) {
readSoleVboId = a.vboId;
readSoleBuf = buf;
readSoleOffset = ro;
} else {
vboBuffers = new Int2ObjectOpenHashMap<>();
vboReadOffsets = new Int2LongOpenHashMap();
vboBuffers.put(readSoleVboId, readSoleBuf);
vboReadOffsets.put(readSoleVboId, readSoleOffset);
vboBuffers.put(a.vboId, buf);
vboReadOffsets.put(a.vboId, ro);
}
}
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.offset, a.vboId, buf, readOffset);
} else if (a.clientPointer != null) {
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.clientPointer.position(), 0, a.clientPointer, 0L);
}
}
} finally {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
return new AttribSnapshot(out, allocated);
}
}
@@ -1,784 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import org.joml.Matrix4f;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
public final class CommandBuffer {
private static final int DEFAULT_CAPACITY = 4096; // Guess at typical display list size
private ByteBuffer buffer;
private long basePointer;
private long writePointer;
private final List<Object> complexObjects = new ArrayList<>();
public CommandBuffer() {
this(DEFAULT_CAPACITY);
}
public CommandBuffer(int initialCapacity) {
this.buffer = memAlloc(initialCapacity);
this.basePointer = memAddress(buffer);
this.writePointer = basePointer;
}
// === Low-level write operations ===
private void writeInt(int value) {
memPutInt(writePointer, value);
writePointer += 4;
}
private void writeLong(long value) {
memPutLong(writePointer, value);
writePointer += 8;
}
private void writeFloat(float value) {
memPutFloat(writePointer, value);
writePointer += 4;
}
private void writeDouble(double value) {
memPutDouble(writePointer, value);
writePointer += 8;
}
private void ensureCapacity(int needed) {
final int size = size();
final int remaining = buffer.capacity() - size;
if (remaining < needed) {
final int newCapacity = Math.max(buffer.capacity() * 2, buffer.capacity() + needed);
buffer = memRealloc(buffer, newCapacity);
basePointer = memAddress(buffer);
writePointer = basePointer + size;
}
}
// === Single int commands ===
public void writeEnable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.ENABLE);
writeInt(cap);
}
public void writeDisable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.DISABLE);
writeInt(cap);
}
public void writeClear(int mask) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR);
writeInt(mask);
}
public void writeClearStencil(int s) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR_STENCIL);
writeInt(s);
}
public void writeCullFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.CULL_FACE);
writeInt(mode);
}
public void writeDepthFunc(int func) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_FUNC);
writeInt(func);
}
public void writeShadeModel(int mode) {
ensureCapacity(8);
writeInt(GLCommand.SHADE_MODEL);
writeInt(mode);
}
public void writeLogicOp(int opcode) {
ensureCapacity(8);
writeInt(GLCommand.LOGIC_OP);
writeInt(opcode);
}
public void writeMatrixMode(int mode) {
ensureCapacity(8);
writeInt(GLCommand.MATRIX_MODE);
writeInt(mode);
}
public void writeActiveTexture(int texture) {
ensureCapacity(8);
writeInt(GLCommand.ACTIVE_TEXTURE);
writeInt(texture);
}
public void writeUseProgram(int program) {
ensureCapacity(8);
writeInt(GLCommand.USE_PROGRAM);
writeInt(program);
}
public void writePushAttrib(int mask) {
ensureCapacity(8);
writeInt(GLCommand.PUSH_ATTRIB);
writeInt(mask);
}
public void writePopAttrib() {
ensureCapacity(8);
writeInt(GLCommand.POP_ATTRIB);
writeInt(0); // unused padding
}
public void writeLoadIdentity() {
ensureCapacity(4);
writeInt(GLCommand.LOAD_IDENTITY);
}
public void writePushMatrix() {
ensureCapacity(4);
writeInt(GLCommand.PUSH_MATRIX);
}
public void writePopMatrix() {
ensureCapacity(4);
writeInt(GLCommand.POP_MATRIX);
}
public void writeStencilMask(int mask) {
ensureCapacity(8);
writeInt(GLCommand.STENCIL_MASK);
writeInt(mask);
}
public void writeDepthMask(boolean flag) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_MASK);
writeInt(flag ? 1 : 0);
}
public void writeFrontFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.FRONT_FACE);
writeInt(mode);
}
// === Two int commands ===
public void writeBindTexture(int target, int texture) {
ensureCapacity(12);
writeInt(GLCommand.BIND_TEXTURE);
writeInt(target);
writeInt(texture);
}
public void writePolygonMode(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_MODE);
writeInt(face);
writeInt(mode);
}
public void writeColorMaterial(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.COLOR_MATERIAL);
writeInt(face);
writeInt(mode);
}
public void writeLineStipple(int factor, int pattern) {
ensureCapacity(12);
writeInt(GLCommand.LINE_STIPPLE);
writeInt(factor);
writeInt(pattern);
}
public void writeStencilMaskSeparate(int face, int mask) {
ensureCapacity(12);
writeInt(GLCommand.STENCIL_MASK_SEPARATE);
writeInt(face);
writeInt(mask);
}
public void writeFogi(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.FOGI);
writeInt(pname);
writeInt(param);
}
public void writeHint(int target, int mode) {
ensureCapacity(12);
writeInt(GLCommand.HINT);
writeInt(target);
writeInt(mode);
}
// === Three int commands ===
public void writeStencilFunc(int func, int ref, int mask) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_FUNC);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void writeStencilOp(int fail, int zfail, int zpass) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_OP);
writeInt(fail);
writeInt(zfail);
writeInt(zpass);
}
public void writeTexParameteri(int target, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERI);
writeInt(target);
writeInt(pname);
writeInt(param);
}
// === Four int commands ===
public void writeViewport(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.VIEWPORT);
writeInt(x);
writeInt(y);
writeInt(width);
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);
writeInt(srcRgb);
writeInt(dstRgb);
writeInt(srcAlpha);
writeInt(dstAlpha);
}
public void writeColorMask(boolean red, boolean green, boolean blue, boolean alpha) {
ensureCapacity(20);
writeInt(GLCommand.COLOR_MASK);
writeInt(red ? 1 : 0);
writeInt(green ? 1 : 0);
writeInt(blue ? 1 : 0);
writeInt(alpha ? 1 : 0);
}
public void writeStencilFuncSeparate(int face, int func, int ref, int mask) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_FUNC_SEPARATE);
writeInt(face);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void writeStencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_OP_SEPARATE);
writeInt(face);
writeInt(sfail);
writeInt(dpfail);
writeInt(dppass);
}
// === Float commands ===
public void writePointSize(float size) {
ensureCapacity(8);
writeInt(GLCommand.POINT_SIZE);
writeFloat(size);
}
public void writeLineWidth(float width) {
ensureCapacity(8);
writeInt(GLCommand.LINE_WIDTH);
writeFloat(width);
}
public void writePolygonOffset(float factor, float units) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_OFFSET);
writeFloat(factor);
writeFloat(units);
}
public void writeColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void writeClearColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.CLEAR_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void writeClearDepth(double depth) {
ensureCapacity(12);
writeInt(GLCommand.CLEAR_DEPTH);
writeDouble(depth);
}
public void writeBlendColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
// === Mixed int+float commands ===
public void writeAlphaFunc(int func, float ref) {
ensureCapacity(12);
writeInt(GLCommand.ALPHA_FUNC);
writeInt(func);
writeFloat(ref);
}
public void writeFogf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.FOGF);
writeInt(pname);
writeFloat(param);
}
public void writeLightf(int light, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTF);
writeInt(light);
writeInt(pname);
writeFloat(param);
}
public void writeLightModelf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELF);
writeInt(pname);
writeFloat(param);
}
public void writeLighti(int light, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTI);
writeInt(light);
writeInt(pname);
writeInt(param);
}
public void writeLightModeli(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELI);
writeInt(pname);
writeInt(param);
}
public void writeMaterialf(int face, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.MATERIALF);
writeInt(face);
writeInt(pname);
writeFloat(param);
}
public void writeTexParameterf(int target, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERF);
writeInt(target);
writeInt(pname);
writeFloat(param);
}
// === Double commands ===
public void writeTranslate(double x, double y, double z) {
ensureCapacity(28);
writeInt(GLCommand.TRANSLATE);
writeDouble(x);
writeDouble(y);
writeDouble(z);
}
public void writeRotate(double angle, double x, double y, double z) {
ensureCapacity(36);
writeInt(GLCommand.ROTATE);
writeDouble(angle);
writeDouble(x);
writeDouble(y);
writeDouble(z);
}
public void writeScale(double x, double y, double z) {
ensureCapacity(28);
writeInt(GLCommand.SCALE);
writeDouble(x);
writeDouble(y);
writeDouble(z);
}
public void writeOrtho(double left, double right, double bottom, double top, double zNear, double zFar) {
ensureCapacity(52);
writeInt(GLCommand.ORTHO);
writeDouble(left);
writeDouble(right);
writeDouble(bottom);
writeDouble(top);
writeDouble(zNear);
writeDouble(zFar);
}
public void writeFrustum(double left, double right, double bottom, double top, double zNear, double zFar) {
ensureCapacity(52);
writeInt(GLCommand.FRUSTUM);
writeDouble(left);
writeDouble(right);
writeDouble(bottom);
writeDouble(top);
writeDouble(zNear);
writeDouble(zFar);
}
// === Matrix commands ===
public void writeMultMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.MULT_MATRIX);
writeMatrix4f(matrix);
}
public void writeLoadMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.LOAD_MATRIX);
writeMatrix4f(matrix);
}
/**
* Write a Matrix4f to the buffer in column-major order (OpenGL convention).
* Uses accessor methods instead of getToAddress() to avoid JOML MemUtil issues.
*/
private void writeMatrix4f(Matrix4f m) {
// Column 0
writeFloat(m.m00()); writeFloat(m.m01()); writeFloat(m.m02()); writeFloat(m.m03());
// Column 1
writeFloat(m.m10()); writeFloat(m.m11()); writeFloat(m.m12()); writeFloat(m.m13());
// Column 2
writeFloat(m.m20()); writeFloat(m.m21()); writeFloat(m.m22()); writeFloat(m.m23());
// Column 3
writeFloat(m.m30()); writeFloat(m.m31()); writeFloat(m.m32()); writeFloat(m.m33());
}
// === Buffer commands (inline 4 floats) ===
public void writeFog(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.FOG);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLight(int light, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.LIGHT);
writeInt(light);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLightModel(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.LIGHT_MODEL);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeMaterial(int face, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.MATERIAL);
writeInt(face);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
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) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_RANGE);
writeInt(vboIndex);
}
/**
* Write a DRAW_RANGE_RESTORE command that draws a VBO range and restores GL current state after.
* Used for immediate mode VBOs to restore GL_CURRENT_COLOR, GL_CURRENT_NORMAL, GL_CURRENT_TEXTURE_COORDS.
*
* @param vboIndex Index into ownedVbos array
*/
public void writeDrawRangeRestore(
int vboIndex, AccumulatedDraw.RestoreData restoreData
) {
ensureCapacity(28);
writeInt(GLCommand.DRAW_RANGE_RESTORE);
writeInt(vboIndex);
writeInt(restoreData.flags);
// Last color (4 bytes)
writeInt(restoreData.lastColor);
// Last normal (4 bytes)
writeInt(restoreData.lastNormal);
// Last texcoord (2 floats = 8 bytes)
writeFloat(restoreData.lastTexCoordU);
writeFloat(restoreData.lastTexCoordV);
}
public void writeCallList(int listId) {
ensureCapacity(8);
writeInt(GLCommand.CALL_LIST);
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);
writeInt(mode);
}
private static final int MAX_DRAW_BUFFERS = 8;
// cmd (4) + count (4) + buffers (4 * MAX_DRAW_BUFFERS)
private static final int DRAW_BUFFERS_SIZE = 4 + 4 + 4 * MAX_DRAW_BUFFERS;
public void writeDrawBuffers(int count, int buffer) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
writeInt(buffer);
for (int i = 1; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
public void writeDrawBuffers(int count, IntBuffer bufs) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
final int pos = bufs.position();
for (int i = 0; i < count && i < MAX_DRAW_BUFFERS; i++) {
writeInt(bufs.get(pos + i));
}
for (int i = count; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
// === Complex object reference ===
public void writeComplexRef(DisplayListCommand cmd) {
ensureCapacity(8);
final int index = complexObjects.size();
complexObjects.add(cmd);
writeInt(GLCommand.COMPLEX_REF);
writeInt(index);
}
// === Reading methods for optimization pass ===
private long readPointer = 0;
/**
* Reset read position to the beginning of the buffer.
*/
public void resetRead() {
readPointer = basePointer;
}
/**
* Check if there's more data to read.
*/
public boolean hasRemaining() {
return readPointer < writePointer;
}
/**
* Read an int from the current position.
*/
public int readInt() {
final int value = memGetInt(readPointer);
readPointer += 4;
return value;
}
public long readLong() {
final long value = memGetLong(readPointer);
readPointer += 8;
return value;
}
/**
* Read a float from the current position.
*/
public float readFloat() {
final float value = memGetFloat(readPointer);
readPointer += 4;
return value;
}
/**
* Read a double from the current position.
*/
public double readDouble() {
final double value = memGetDouble(readPointer);
readPointer += 8;
return value;
}
/**
* Read a Matrix4f from the current position (16 floats in column-major order).
*/
public Matrix4f readMatrix4f(Matrix4f dest) {
dest.m00(readFloat()); dest.m01(readFloat()); dest.m02(readFloat()); dest.m03(readFloat());
dest.m10(readFloat()); dest.m11(readFloat()); dest.m12(readFloat()); dest.m13(readFloat());
dest.m20(readFloat()); dest.m21(readFloat()); dest.m22(readFloat()); dest.m23(readFloat());
dest.m30(readFloat()); dest.m31(readFloat()); dest.m32(readFloat()); dest.m33(readFloat());
return dest;
}
// === Lifecycle ===
public int size() {
return (int) (writePointer - basePointer);
}
public long address() {
return basePointer;
}
public ByteBuffer toBuffer() {
buffer.limit(size());
buffer.position(0);
return buffer;
}
public Object[] getComplexObjects() {
return complexObjects.toArray();
}
public Object getComplexObject(int index) {
return complexObjects.get(index);
}
public boolean isEmpty() {
return size() == 0 && complexObjects.isEmpty();
}
/**
* Clear the buffer for reuse. Does NOT free memory.
*/
public void clear() {
writePointer = basePointer;
complexObjects.clear();
}
/**
* Free the underlying buffer. Must be called when done.
*/
public void free() {
if (buffer != null) {
memFree(buffer);
buffer = null;
basePointer = 0;
writePointer = 0;
}
}
}
@@ -1,12 +1,10 @@
package com.gtnewhorizons.angelica.glsm.recording;
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;
@@ -14,7 +12,10 @@ import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetDouble;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
/**
* Executes commands from a CommandBuffer
@@ -39,7 +40,13 @@ 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, DisplayListVBO ownedVbos) {
public static void execute(
ByteBuffer buffer,
DisplayListCommand[] complexObjects,
DisplayListVBO ownedVbos,
int list,
CompiledDisplayList compiledDisplayList
) {
long ptr = memAddress(buffer);
final long end = ptr + buffer.limit();
@@ -343,50 +350,6 @@ public final class CommandBufferExecutor {
GLStateManager.glTexParameterf(target, pname, param);
}
// === Double commands ===
case GLCommand.TRANSLATE -> {
final double x = memGetDouble(ptr);
final double y = memGetDouble(ptr + 8);
final double z = memGetDouble(ptr + 16);
ptr += 24;
GLStateManager.glTranslated(x, y, z);
}
case GLCommand.ROTATE -> {
final double angle = memGetDouble(ptr);
final double x = memGetDouble(ptr + 8);
final double y = memGetDouble(ptr + 16);
final double z = memGetDouble(ptr + 24);
ptr += 32;
GLStateManager.glRotated(angle, x, y, z);
}
case GLCommand.SCALE -> {
final double x = memGetDouble(ptr);
final double y = memGetDouble(ptr + 8);
final double z = memGetDouble(ptr + 16);
ptr += 24;
GLStateManager.glScaled(x, y, z);
}
case GLCommand.ORTHO -> {
final double left = memGetDouble(ptr);
final double right = memGetDouble(ptr + 8);
final double bottom = memGetDouble(ptr + 16);
final double top = memGetDouble(ptr + 24);
final double zNear = memGetDouble(ptr + 32);
final double zFar = memGetDouble(ptr + 40);
ptr += 48;
GLStateManager.glOrtho(left, right, bottom, top, zNear, zFar);
}
case GLCommand.FRUSTUM -> {
final double left = memGetDouble(ptr);
final double right = memGetDouble(ptr + 8);
final double bottom = memGetDouble(ptr + 16);
final double top = memGetDouble(ptr + 24);
final double zNear = memGetDouble(ptr + 32);
final double zFar = memGetDouble(ptr + 40);
ptr += 48;
GLStateManager.glFrustum(left, right, bottom, top, zNear, zFar);
}
// === Matrix commands ===
case GLCommand.MULT_MATRIX -> {
// Mode-agnostic: just multiply current matrix
@@ -398,6 +361,20 @@ public final class CommandBufferExecutor {
MATRIX_BUFFER.flip();
GLStateManager.glMultMatrix(MATRIX_BUFFER);
}
case GLCommand.TRANSLATE -> {
final float x = memGetFloat(ptr);
final float y = memGetFloat(ptr + 4);
final float z = memGetFloat(ptr + 8);
ptr += 12;
GLStateManager.glTranslatef(x, y, z);
}
case GLCommand.SCALE -> {
final float x = memGetFloat(ptr);
final float y = memGetFloat(ptr + 4);
final float z = memGetFloat(ptr + 8);
ptr += 12;
GLStateManager.glScalef(x, y, z);
}
case GLCommand.LOAD_MATRIX -> {
MATRIX_BUFFER.clear();
for (int i = 0; i < 16; i++) {
@@ -530,14 +507,6 @@ 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;
@@ -551,10 +520,10 @@ public final class CommandBufferExecutor {
case GLCommand.COMPLEX_REF -> {
final int index = memGetInt(ptr);
ptr += 4;
((DisplayListCommand) complexObjects[index]).execute();
complexObjects[index].execute();
}
default -> throw new IllegalStateException("Unknown command opcode: " + cmd);
default -> throw new IllegalStateException("Unknown command opcode: " + cmd + " in display list with ID " + list + "\n" + DisplayListManager.getCompiledDisplayListString(list, compiledDisplayList, null));
}
}
}
@@ -1,168 +0,0 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import org.joml.Matrix4f;
import java.nio.FloatBuffer;
public final class CommandBufferProcessor {
private static final Matrix4f tempMatrix = new Matrix4f();
private CommandBufferProcessor() {}
/**
* Copy a complete command (opcode already read) from raw to final buffer.
*/
public static void copyCommand(CommandBuffer raw, CommandBuffer out) {
final int opcode = raw.readInt();
copyCommandData(opcode, raw, out);
}
/**
* Copy command data based on opcode (opcode already consumed from raw).
*/
public static void copyCommandData(int opcode, CommandBuffer raw, CommandBuffer out) {
switch (opcode) {
// Single int commands
case GLCommand.ENABLE -> out.writeEnable(raw.readInt());
case GLCommand.DISABLE -> out.writeDisable(raw.readInt());
case GLCommand.CLEAR -> out.writeClear(raw.readInt());
case GLCommand.CLEAR_STENCIL -> out.writeClearStencil(raw.readInt());
case GLCommand.CULL_FACE -> out.writeCullFace(raw.readInt());
case GLCommand.DEPTH_FUNC -> out.writeDepthFunc(raw.readInt());
case GLCommand.SHADE_MODEL -> out.writeShadeModel(raw.readInt());
case GLCommand.LOGIC_OP -> out.writeLogicOp(raw.readInt());
case GLCommand.MATRIX_MODE -> out.writeMatrixMode(raw.readInt());
case GLCommand.ACTIVE_TEXTURE -> out.writeActiveTexture(raw.readInt());
case GLCommand.USE_PROGRAM -> out.writeUseProgram(raw.readInt());
case GLCommand.PUSH_ATTRIB -> out.writePushAttrib(raw.readInt());
case GLCommand.POP_ATTRIB -> { raw.readInt(); out.writePopAttrib(); }
case GLCommand.LOAD_IDENTITY -> out.writeLoadIdentity();
case GLCommand.PUSH_MATRIX -> out.writePushMatrix();
case GLCommand.POP_MATRIX -> out.writePopMatrix();
case GLCommand.STENCIL_MASK -> out.writeStencilMask(raw.readInt());
case GLCommand.DEPTH_MASK -> out.writeDepthMask(raw.readInt() != 0);
case GLCommand.FRONT_FACE -> out.writeFrontFace(raw.readInt());
// Two int commands
case GLCommand.BIND_TEXTURE -> out.writeBindTexture(raw.readInt(), raw.readInt());
case GLCommand.POLYGON_MODE -> out.writePolygonMode(raw.readInt(), raw.readInt());
case GLCommand.COLOR_MATERIAL -> out.writeColorMaterial(raw.readInt(), raw.readInt());
case GLCommand.LINE_STIPPLE -> out.writeLineStipple(raw.readInt(), raw.readInt());
case GLCommand.STENCIL_MASK_SEPARATE -> out.writeStencilMaskSeparate(raw.readInt(), raw.readInt());
case GLCommand.FOGI -> out.writeFogi(raw.readInt(), raw.readInt());
case GLCommand.HINT -> out.writeHint(raw.readInt(), raw.readInt());
// Three int commands
case GLCommand.STENCIL_FUNC -> out.writeStencilFunc(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.STENCIL_OP -> out.writeStencilOp(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.TEX_PARAMETERI -> out.writeTexParameteri(raw.readInt(), raw.readInt(), raw.readInt());
// 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());
case GLCommand.STENCIL_OP_SEPARATE -> out.writeStencilOpSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
// Float commands
case GLCommand.POINT_SIZE -> out.writePointSize(raw.readFloat());
case GLCommand.LINE_WIDTH -> out.writeLineWidth(raw.readFloat());
case GLCommand.POLYGON_OFFSET -> out.writePolygonOffset(raw.readFloat(), raw.readFloat());
case GLCommand.COLOR -> out.writeColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat());
case GLCommand.CLEAR_COLOR -> out.writeClearColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat());
// Single double commands
case GLCommand.CLEAR_DEPTH -> out.writeClearDepth(raw.readDouble());
// Four float commands
case GLCommand.BLEND_COLOR -> out.writeBlendColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat());
// Mixed int+float commands
case GLCommand.ALPHA_FUNC -> out.writeAlphaFunc(raw.readInt(), raw.readFloat());
case GLCommand.FOGF -> out.writeFogf(raw.readInt(), raw.readFloat());
case GLCommand.LIGHTF -> out.writeLightf(raw.readInt(), raw.readInt(), raw.readFloat());
case GLCommand.LIGHT_MODELF -> out.writeLightModelf(raw.readInt(), raw.readFloat());
case GLCommand.LIGHTI -> out.writeLighti(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.LIGHT_MODELI -> out.writeLightModeli(raw.readInt(), raw.readInt());
case GLCommand.MATERIALF -> out.writeMaterialf(raw.readInt(), raw.readInt(), raw.readFloat());
case GLCommand.TEX_PARAMETERF -> out.writeTexParameterf(raw.readInt(), raw.readInt(), raw.readFloat());
// Double commands
case GLCommand.TRANSLATE -> out.writeTranslate(raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.ROTATE -> out.writeRotate(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.SCALE -> out.writeScale(raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.ORTHO -> out.writeOrtho(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble());
case GLCommand.FRUSTUM -> out.writeFrustum(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble());
// Matrix commands
case GLCommand.MULT_MATRIX -> { out.writeMultMatrix(raw.readMatrix4f(tempMatrix)); }
case GLCommand.LOAD_MATRIX -> { out.writeLoadMatrix(raw.readMatrix4f(tempMatrix)); }
// FloatBuffer commands (inline 4 floats max)
case GLCommand.FOG -> {
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeFog(pname, fb);
}
case GLCommand.LIGHT -> {
final int light = raw.readInt();
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeLight(light, pname, fb);
}
case GLCommand.LIGHT_MODEL -> {
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
out.writeLightModel(pname, fb);
}
case GLCommand.MATERIAL -> {
final int face = raw.readInt();
final int pname = raw.readInt();
final int count = raw.readInt();
final FloatBuffer fb = FloatBuffer.allocate(count);
for (int i = 0; i < count; i++) fb.put(raw.readFloat());
fb.flip();
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());
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 -> {
final int index = raw.readInt();
final Object obj = raw.getComplexObject(index);
out.writeComplexRef((DisplayListCommand) obj);
}
default -> GLStateManager.LOGGER.warn("[CommandBufferProcessor] Unknown opcode in buffer copy: {}", opcode);
}
}
}
@@ -1,374 +1,691 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
/**
* Records GL commands to a CommandBuffer during display list compilation.
*/
public final class CommandRecorder {
private final CommandBuffer buffer;
private int commandCount;
private static final int DEFAULT_CAPACITY = 2048; // Guess at typical display list size
private ByteBuffer buffer;
private long basePointer;
private long writePointer;
private final ArrayList<DisplayListCommand> complexObjects = new ArrayList<>();
private final IndexedDrawBatchBuilder indexedDraws = new IndexedDrawBatchBuilder();
public CommandRecorder() {
this.buffer = new CommandBuffer();
this.commandCount = 0;
this(DEFAULT_CAPACITY);
}
public CommandBuffer getBuffer() {
return buffer;
public CommandRecorder(int initialCapacity) {
this.buffer = memAlloc(initialCapacity);
this.basePointer = memAddress0(buffer);
this.writePointer = basePointer;
}
public int getCommandCount() {
return commandCount;
// === Low-level write operations ===
private void writeInt(int value) {
memPutInt(writePointer, value);
writePointer += 4;
}
private void writeLong(long value) {
memPutLong(writePointer, value);
writePointer += 8;
}
private void writeFloat(float value) {
memPutFloat(writePointer, value);
writePointer += 4;
}
private void writeDouble(double value) {
memPutDouble(writePointer, value);
writePointer += 8;
}
private void ensureCapacity(int needed) {
final int size = size();
final int remaining = buffer.capacity() - size;
if (remaining < needed) {
final int newCapacity = Math.max(buffer.capacity() * 2, buffer.capacity() + needed);
buffer = memRealloc(buffer, newCapacity);
basePointer = memAddress0(buffer);
writePointer = basePointer + size;
}
}
public IndexedDrawBatchBuilder getIndexedDraws() {
return indexedDraws;
}
public void writeIndexedDrawCapture(IndexedDrawCapture capture) {
indexedDraws.add(capture);
writeComplexCommand(capture.placeholder);
}
public void free() {
buffer.free();
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
}
// ==================== Recording Methods ====================
// === Single int commands ===
public void recordEnable(int cap) {
buffer.writeEnable(cap);
commandCount++;
public void writeEnable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.ENABLE);
writeInt(cap);
}
public void recordDisable(int cap) {
buffer.writeDisable(cap);
commandCount++;
public void writeDisable(int cap) {
ensureCapacity(8);
writeInt(GLCommand.DISABLE);
writeInt(cap);
}
public void recordClear(int mask) {
buffer.writeClear(mask);
commandCount++;
public void writeClear(int mask) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR);
writeInt(mask);
}
public void recordClearColor(float r, float g, float b, float a) {
buffer.writeClearColor(r, g, b, a);
commandCount++;
public void writeClearStencil(int s) {
ensureCapacity(8);
writeInt(GLCommand.CLEAR_STENCIL);
writeInt(s);
}
public void recordClearDepth(double depth) {
buffer.writeClearDepth(depth);
commandCount++;
public void writeCullFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.CULL_FACE);
writeInt(mode);
}
public void recordBlendColor(float r, float g, float b, float a) {
buffer.writeBlendColor(r, g, b, a);
commandCount++;
public void writeDepthFunc(int func) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_FUNC);
writeInt(func);
}
public void recordClearStencil(int s) {
buffer.writeClearStencil(s);
commandCount++;
public void writeShadeModel(int mode) {
ensureCapacity(8);
writeInt(GLCommand.SHADE_MODEL);
writeInt(mode);
}
public void recordColor(float r, float g, float b, float a) {
buffer.writeColor(r, g, b, a);
commandCount++;
public void writeLogicOp(int opcode) {
ensureCapacity(8);
writeInt(GLCommand.LOGIC_OP);
writeInt(opcode);
}
public void recordColorMask(boolean r, boolean g, boolean b, boolean a) {
buffer.writeColorMask(r, g, b, a);
commandCount++;
public void writeMatrixMode(int mode) {
ensureCapacity(8);
writeInt(GLCommand.MATRIX_MODE);
writeInt(mode);
}
public void recordDepthMask(boolean flag) {
buffer.writeDepthMask(flag);
commandCount++;
public void writeActiveTexture(int texture) {
ensureCapacity(8);
writeInt(GLCommand.ACTIVE_TEXTURE);
writeInt(texture);
}
public void recordFrontFace(int mode) {
buffer.writeFrontFace(mode);
commandCount++;
public void writeUseProgram(int program) {
ensureCapacity(8);
writeInt(GLCommand.USE_PROGRAM);
writeInt(program);
}
public void recordDepthFunc(int func) {
buffer.writeDepthFunc(func);
commandCount++;
public void writePushAttrib(int mask) {
ensureCapacity(8);
writeInt(GLCommand.PUSH_ATTRIB);
writeInt(mask);
}
public void recordBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
buffer.writeBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha);
commandCount++;
public void writePopAttrib() {
ensureCapacity(8);
writeInt(GLCommand.POP_ATTRIB);
writeInt(0); // unused padding
}
public void recordAlphaFunc(int func, float ref) {
buffer.writeAlphaFunc(func, ref);
commandCount++;
public void writeLoadIdentity() {
ensureCapacity(4);
writeInt(GLCommand.LOAD_IDENTITY);
}
public void recordCullFace(int mode) {
buffer.writeCullFace(mode);
commandCount++;
public void writePushMatrix() {
ensureCapacity(4);
writeInt(GLCommand.PUSH_MATRIX);
}
public void recordShadeModel(int mode) {
buffer.writeShadeModel(mode);
commandCount++;
public void writePopMatrix() {
ensureCapacity(4);
writeInt(GLCommand.POP_MATRIX);
}
public void recordBindTexture(int target, int texture) {
buffer.writeBindTexture(target, texture);
commandCount++;
public void writeStencilMask(int mask) {
ensureCapacity(8);
writeInt(GLCommand.STENCIL_MASK);
writeInt(mask);
}
public void recordTexParameteri(int target, int pname, int param) {
buffer.writeTexParameteri(target, pname, param);
commandCount++;
public void writeDepthMask(boolean flag) {
ensureCapacity(8);
writeInt(GLCommand.DEPTH_MASK);
writeInt(flag ? 1 : 0);
}
public void recordTexParameterf(int target, int pname, float param) {
buffer.writeTexParameterf(target, pname, param);
commandCount++;
public void writeFrontFace(int mode) {
ensureCapacity(8);
writeInt(GLCommand.FRONT_FACE);
writeInt(mode);
}
public void recordMatrixMode(int mode) {
buffer.writeMatrixMode(mode);
commandCount++;
}
public void recordLoadIdentity() {
buffer.writeLoadIdentity();
commandCount++;
}
public void recordPushMatrix() {
buffer.writePushMatrix();
commandCount++;
}
public void recordPopMatrix() {
buffer.writePopMatrix();
commandCount++;
}
// === Two int commands ===
public void recordMultMatrix(Matrix4f matrix) {
buffer.writeMultMatrix(matrix);
commandCount++;
public void writeBindTexture(int target, int texture) {
ensureCapacity(12);
writeInt(GLCommand.BIND_TEXTURE);
writeInt(target);
writeInt(texture);
}
public void recordLoadMatrix(Matrix4f matrix) {
buffer.writeLoadMatrix(matrix);
commandCount++;
public void writePolygonMode(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_MODE);
writeInt(face);
writeInt(mode);
}
public void recordOrtho(double left, double right, double bottom, double top, double zNear, double zFar) {
buffer.writeOrtho(left, right, bottom, top, zNear, zFar);
commandCount++;
public void writeColorMaterial(int face, int mode) {
ensureCapacity(12);
writeInt(GLCommand.COLOR_MATERIAL);
writeInt(face);
writeInt(mode);
}
public void recordFrustum(double left, double right, double bottom, double top, double zNear, double zFar) {
buffer.writeFrustum(left, right, bottom, top, zNear, zFar);
commandCount++;
public void writeLineStipple(int factor, int pattern) {
ensureCapacity(12);
writeInt(GLCommand.LINE_STIPPLE);
writeInt(factor);
writeInt(pattern);
}
public void recordViewport(int x, int y, int width, int height) {
buffer.writeViewport(x, y, width, height);
commandCount++;
public void writeStencilMaskSeparate(int face, int mask) {
ensureCapacity(12);
writeInt(GLCommand.STENCIL_MASK_SEPARATE);
writeInt(face);
writeInt(mask);
}
public void recordScissor(int x, int y, int width, int height) {
buffer.writeScissor(x, y, width, height);
commandCount++;
public void writeFogi(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.FOGI);
writeInt(pname);
writeInt(param);
}
public void recordPointSize(float size) {
buffer.writePointSize(size);
commandCount++;
public void writeHint(int target, int mode) {
ensureCapacity(12);
writeInt(GLCommand.HINT);
writeInt(target);
writeInt(mode);
}
public void recordLineWidth(float width) {
buffer.writeLineWidth(width);
commandCount++;
}
// === Three int commands ===
public void recordLineStipple(int factor, int pattern) {
buffer.writeLineStipple(factor, pattern);
commandCount++;
public void writeStencilFunc(int func, int ref, int mask) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_FUNC);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void recordPolygonOffset(float factor, float units) {
buffer.writePolygonOffset(factor, units);
commandCount++;
public void writeStencilOp(int fail, int zfail, int zpass) {
ensureCapacity(16);
writeInt(GLCommand.STENCIL_OP);
writeInt(fail);
writeInt(zfail);
writeInt(zpass);
}
public void recordPolygonMode(int face, int mode) {
buffer.writePolygonMode(face, mode);
commandCount++;
public void writeTexParameteri(int target, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERI);
writeInt(target);
writeInt(pname);
writeInt(param);
}
public void recordColorMaterial(int face, int mode) {
buffer.writeColorMaterial(face, mode);
commandCount++;
}
// === Four int commands ===
public void recordLogicOp(int opcode) {
buffer.writeLogicOp(opcode);
commandCount++;
public void writeViewport(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.VIEWPORT);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void recordActiveTexture(int texture) {
buffer.writeActiveTexture(texture);
commandCount++;
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 recordUseProgram(int program) {
buffer.writeUseProgram(program);
commandCount++;
public void writeBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_FUNC);
writeInt(srcRgb);
writeInt(dstRgb);
writeInt(srcAlpha);
writeInt(dstAlpha);
}
public void recordPushAttrib(int mask) {
buffer.writePushAttrib(mask);
commandCount++;
public void writeColorMask(boolean red, boolean green, boolean blue, boolean alpha) {
ensureCapacity(20);
writeInt(GLCommand.COLOR_MASK);
writeInt(red ? 1 : 0);
writeInt(green ? 1 : 0);
writeInt(blue ? 1 : 0);
writeInt(alpha ? 1 : 0);
}
public void recordPopAttrib() {
buffer.writePopAttrib();
commandCount++;
public void writeStencilFuncSeparate(int face, int func, int ref, int mask) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_FUNC_SEPARATE);
writeInt(face);
writeInt(func);
writeInt(ref);
writeInt(mask);
}
public void recordFogf(int pname, float param) {
buffer.writeFogf(pname, param);
commandCount++;
public void writeStencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
ensureCapacity(20);
writeInt(GLCommand.STENCIL_OP_SEPARATE);
writeInt(face);
writeInt(sfail);
writeInt(dpfail);
writeInt(dppass);
}
public void recordFogi(int pname, int param) {
buffer.writeFogi(pname, param);
commandCount++;
}
// === Float commands ===
public void recordHint(int target, int mode) {
buffer.writeHint(target, mode);
commandCount++;
public void writePointSize(float size) {
ensureCapacity(8);
writeInt(GLCommand.POINT_SIZE);
writeFloat(size);
}
public void recordFog(int pname, FloatBuffer params) {
buffer.writeFog(pname, params);
commandCount++;
public void writeLineWidth(float width) {
ensureCapacity(8);
writeInt(GLCommand.LINE_WIDTH);
writeFloat(width);
}
public void recordLightf(int light, int pname, float param) {
buffer.writeLightf(light, pname, param);
commandCount++;
public void writePolygonOffset(float factor, float units) {
ensureCapacity(12);
writeInt(GLCommand.POLYGON_OFFSET);
writeFloat(factor);
writeFloat(units);
}
public void recordLighti(int light, int pname, int param) {
buffer.writeLighti(light, pname, param);
commandCount++;
public void writeColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void recordLight(int light, int pname, FloatBuffer params) {
buffer.writeLight(light, pname, params);
commandCount++;
public void writeClearColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.CLEAR_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void recordLightModelf(int pname, float param) {
buffer.writeLightModelf(pname, param);
commandCount++;
public void writeClearDepth(double depth) {
ensureCapacity(12);
writeInt(GLCommand.CLEAR_DEPTH);
writeDouble(depth);
}
public void recordLightModeli(int pname, int param) {
buffer.writeLightModeli(pname, param);
commandCount++;
public void writeBlendColor(float r, float g, float b, float a) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_COLOR);
writeFloat(r);
writeFloat(g);
writeFloat(b);
writeFloat(a);
}
public void recordLightModel(int pname, FloatBuffer params) {
buffer.writeLightModel(pname, params);
commandCount++;
}
// === Mixed int+float commands ===
public void recordMaterialf(int face, int pname, float val) {
buffer.writeMaterialf(face, pname, val);
commandCount++;
public void writeAlphaFunc(int func, float ref) {
ensureCapacity(12);
writeInt(GLCommand.ALPHA_FUNC);
writeInt(func);
writeFloat(ref);
}
public void recordMaterial(int face, int pname, FloatBuffer params) {
buffer.writeMaterial(face, pname, params);
commandCount++;
public void writeFogf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.FOGF);
writeInt(pname);
writeFloat(param);
}
public void recordClipPlane(int plane, double a, double b, double c, double d) {
buffer.writeClipPlane(plane, a, b, c, d);
commandCount++;
public void writeLightf(int light, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTF);
writeInt(light);
writeInt(pname);
writeFloat(param);
}
public void recordStencilFunc(int func, int ref, int mask) {
buffer.writeStencilFunc(func, ref, mask);
commandCount++;
public void writeLightModelf(int pname, float param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELF);
writeInt(pname);
writeFloat(param);
}
public void recordStencilFuncSeparate(int face, int func, int ref, int mask) {
buffer.writeStencilFuncSeparate(face, func, ref, mask);
commandCount++;
public void writeLighti(int light, int pname, int param) {
ensureCapacity(16);
writeInt(GLCommand.LIGHTI);
writeInt(light);
writeInt(pname);
writeInt(param);
}
public void recordStencilOp(int fail, int zfail, int zpass) {
buffer.writeStencilOp(fail, zfail, zpass);
commandCount++;
public void writeLightModeli(int pname, int param) {
ensureCapacity(12);
writeInt(GLCommand.LIGHT_MODELI);
writeInt(pname);
writeInt(param);
}
public void recordStencilOpSeparate(int face, int sfail, int dpfail, int dppass) {
buffer.writeStencilOpSeparate(face, sfail, dpfail, dppass);
commandCount++;
public void writeMaterialf(int face, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.MATERIALF);
writeInt(face);
writeInt(pname);
writeFloat(param);
}
public void recordStencilMask(int mask) {
buffer.writeStencilMask(mask);
commandCount++;
public void writeTexParameterf(int target, int pname, float param) {
ensureCapacity(16);
writeInt(GLCommand.TEX_PARAMETERF);
writeInt(target);
writeInt(pname);
writeFloat(param);
}
// === Transformation commands ===
public void writeTranslate(Vector3f translationVector) {
ensureCapacity(16);
writeInt(GLCommand.TRANSLATE);
writeFloat(translationVector.x);
writeFloat(translationVector.y);
writeFloat(translationVector.z);
}
public void writeScale(Vector3f scaleVector) {
ensureCapacity(16);
writeInt(GLCommand.SCALE);
writeFloat(scaleVector.x);
writeFloat(scaleVector.y);
writeFloat(scaleVector.z);
}
// === Matrix commands ===
public void writeMultMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.MULT_MATRIX);
writeMatrix4f(matrix);
}
public void writeLoadMatrix(Matrix4f matrix) {
ensureCapacity(68);
writeInt(GLCommand.LOAD_MATRIX);
writeMatrix4f(matrix);
}
/**
* Write a Matrix4f to the buffer in column-major order (OpenGL convention).
* Uses accessor methods instead of getToAddress() to avoid JOML MemUtil issues.
*/
private void writeMatrix4f(Matrix4f m) {
// Column 0
writeFloat(m.m00()); writeFloat(m.m01()); writeFloat(m.m02()); writeFloat(m.m03());
// Column 1
writeFloat(m.m10()); writeFloat(m.m11()); writeFloat(m.m12()); writeFloat(m.m13());
// Column 2
writeFloat(m.m20()); writeFloat(m.m21()); writeFloat(m.m22()); writeFloat(m.m23());
// Column 3
writeFloat(m.m30()); writeFloat(m.m31()); writeFloat(m.m32()); writeFloat(m.m33());
}
// === Buffer commands (inline 4 floats) ===
public void writeFog(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.FOG);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLight(int light, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.LIGHT);
writeInt(light);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeLightModel(int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(12 + count * 4);
writeInt(GLCommand.LIGHT_MODEL);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
public void writeMaterial(int face, int pname, FloatBuffer params) {
final int count = Math.min(params.remaining(), 4);
ensureCapacity(16 + count * 4);
writeInt(GLCommand.MATERIAL);
writeInt(face);
writeInt(pname);
writeInt(count);
final int pos = params.position();
for (int i = 0; i < count; i++) {
writeFloat(params.get(pos + i));
}
}
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) {
ensureCapacity(8);
writeInt(GLCommand.DRAW_RANGE);
writeInt(vboIndex);
}
/**
* Write a DRAW_RANGE_RESTORE command that draws a VBO range and restores GL current state after.
* Used for immediate mode VBOs to restore GL_CURRENT_COLOR, GL_CURRENT_NORMAL, GL_CURRENT_TEXTURE_COORDS.
*
* @param vboIndex Index into ownedVbos array
*/
public void writeDrawRangeRestore(
int vboIndex, AccumulatedDraw.RestoreData restoreData
) {
ensureCapacity(28);
writeInt(GLCommand.DRAW_RANGE_RESTORE);
writeInt(vboIndex);
writeInt(restoreData.flags);
// Last color (4 bytes)
writeInt(restoreData.lastColor);
// Last normal (4 bytes)
writeInt(restoreData.lastNormal);
// Last texcoord (2 floats = 8 bytes)
writeFloat(restoreData.lastTexCoordU);
writeFloat(restoreData.lastTexCoordV);
}
public void writeCallList(int listId) {
ensureCapacity(8);
writeInt(GLCommand.CALL_LIST);
writeInt(listId);
}
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);
writeInt(mode);
}
private static final int MAX_DRAW_BUFFERS = 8;
// cmd (4) + count (4) + buffers (4 * MAX_DRAW_BUFFERS)
private static final int DRAW_BUFFERS_SIZE = 4 + 4 + 4 * MAX_DRAW_BUFFERS;
public void writeDrawBuffers(int count, int buffer) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
writeInt(buffer);
for (int i = 1; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
public void writeDrawBuffers(int count, IntBuffer bufs) {
ensureCapacity(DRAW_BUFFERS_SIZE);
writeInt(GLCommand.DRAW_BUFFERS);
writeInt(count);
final int pos = bufs.position();
for (int i = 0; i < count && i < MAX_DRAW_BUFFERS; i++) {
writeInt(bufs.get(pos + i));
}
for (int i = count; i < MAX_DRAW_BUFFERS; i++) {
writeInt(0);
}
}
public void recordStencilMaskSeparate(int face, int mask) {
buffer.writeStencilMaskSeparate(face, mask);
commandCount++;
}
// === Complex object reference ===
public void recordCallList(int listId) {
buffer.writeCallList(listId);
commandCount++;
public void writeComplexCommand(DisplayListCommand cmd) {
ensureCapacity(8);
final int index = complexObjects.size();
complexObjects.add(cmd);
writeInt(GLCommand.COMPLEX_REF);
writeInt(index);
}
public void recordDrawBuffer(int mode) {
buffer.writeDrawBuffer(mode);
commandCount++;
}
// === Lifecycle ===
public void recordDrawBuffers(int count, int buf) {
buffer.writeDrawBuffers(count, buf);
commandCount++;
public int size() {
return (int) (writePointer - basePointer);
}
public void recordDrawBuffers(int count, IntBuffer bufs) {
buffer.writeDrawBuffers(count, bufs);
commandCount++;
public void delete() {
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
memFree(buffer);
}
public void recordDrawArrays(int mode, int start, int count) {
buffer.writeDrawArrays(mode, start, count);
commandCount++;
/**
* Deletes every allocated object & returns a ByteBuffer with the capacity equal to its limit.
* This CommandRecorder object cannot be used after calling this anymore.
*/
public ByteBuffer finish() {
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
final int size = size();
buffer.limit(size);
return memRealloc(buffer, size);
}
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 DisplayListCommand[] getComplexObjects() {
return complexObjects.toArray(new DisplayListCommand[complexObjects.size()]);
}
public void recordBindVBO(int vbo) {
buffer.writeBindVBO(vbo);
commandCount++;
public DisplayListCommand getComplexObject(int index) {
return complexObjects.get(index);
}
public void recordBindVAO(int vao) {
buffer.writeBindVAO(vao);
commandCount++;
public boolean isEmpty() {
return size() == 0;
}
public void recordComplexCommand(DisplayListCommand cmd) {
buffer.writeComplexRef(cmd);
commandCount++;
/**
* Clear the buffer for reuse. Does NOT free memory.
*/
public void clear() {
writePointer = basePointer;
complexObjects.clear();
}
}
@@ -2,12 +2,15 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
@@ -26,24 +29,30 @@ public final class CompiledDisplayList {
* Empty display list singleton. Per OpenGL spec, an empty list is still valid.
* This instance does nothing when rendered and is never deleted (shared singleton).
*/
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null);
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null, Collections.emptyList());
private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed
private final Object[] complexObjects; // Complex commands (TexImage2D, etc.)
private final DisplayListCommand[] complexObjects; // Complex commands (TexImage2D, etc.)
private final DisplayListVBO ownedVbos; // GPU resources referenced by index
private final List<IndexedDrawBatch> indexedBatches; // Shared VAO/VBO/EBO triples per layout group
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
public CompiledDisplayList(ByteBuffer commandBuffer, DisplayListCommand[] complexObjects, DisplayListVBO ownedVbos, List<IndexedDrawBatch> indexedBatches) {
this.commandBuffer = commandBuffer;
this.complexObjects = complexObjects;
this.ownedVbos = ownedVbos;
this.indexedBatches = indexedBatches;
}
/**
* Render this display list by executing all commands.
*/
public void render() {
render(-1);
}
public void render(int list) {
if (commandBuffer != null && commandBuffer.limit() > 0) {
CommandBufferExecutor.execute(commandBuffer, complexObjects, ownedVbos);
CommandBufferExecutor.execute(commandBuffer, complexObjects, ownedVbos, list, this);
}
}
@@ -51,20 +60,24 @@ public final class CompiledDisplayList {
* Delete all resources held by this display list.
*/
public void delete() {
// Delete complex objects that hold resources
if (complexObjects != null) {
for (Object obj : complexObjects) {
if (obj instanceof DisplayListCommand cmd) {
cmd.delete();
}
for (DisplayListCommand cmd : complexObjects) {
cmd.delete();
}
}
// Close VBOs
// Close VBOs from immediate-mode DisplayListVBOBuilder.
if (ownedVbos != null) {
ownedVbos.delete();
}
// Release shared (VAO, VBO, EBO) triples from IndexedDrawBatchBuilder.
if (indexedBatches != null) {
for (IndexedDrawBatch batch : indexedBatches) {
batch.delete();
}
}
// Free the command buffer (off-heap memory)
if (commandBuffer != null) {
memFree(commandBuffer);
@@ -81,7 +94,7 @@ public final class CompiledDisplayList {
/**
* Get complex objects for inlining into another display list (nested lists).
*/
public Object[] getComplexObjects() {
public DisplayListCommand[] getComplexObjects() {
return complexObjects;
}
@@ -89,6 +102,10 @@ public final class CompiledDisplayList {
return ownedVbos;
}
public List<IndexedDrawBatch> getIndexedBatches() {
return indexedBatches;
}
// === Test inspection methods ===
/**
@@ -129,8 +146,20 @@ public final class CompiledDisplayList {
return opcodes;
}
public String getCompiledDisplayListString(int listId, StackTraceElement[] stackTrace) {
return DisplayListManager.getCompiledDisplayListString(listId, this, stackTrace);
}
public String toString(int listId, StackTraceElement[] stackTrace) {
return getCompiledDisplayListString(listId, stackTrace);
}
public String toString(int listId) {
return toString(listId, null);
}
@Override
public String toString() {
return DisplayListManager.getCompiledDisplayListString(0, this, null);
return toString(0);
}
}
@@ -23,6 +23,10 @@ public final class DisplayListVBO {
return vbos;
}
public SubVBO getVBO(int index) {
return vbos[index];
}
public void render(int index) {
vbos[index].render();
}
@@ -41,6 +45,18 @@ public final class DisplayListVBO {
this.count = count;
}
public int getStart() {
return start;
}
public int getCount() {
return count;
}
public int getDrawMode() {
return drawMode;
}
public void delete() {
vao.delete();
}
@@ -68,13 +68,19 @@ public final class DisplayListVBOBuilder {
allBuffers.addAll(drawBuffers);
int size = 0;
for (ByteBuffer buffer : drawBuffers) {
size += buffer.remaining();
size += buffer.limit();
}
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 if (data.drawMode == GL11.GL_QUAD_STRIP) {
// GL_QUAD_STRIP is removed in core profile; an even-length GL_TRIANGLE_STRIP produces the same quads
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, GL11.GL_TRIANGLE_STRIP, start, vertexCount & ~1);
} else if (data.drawMode == GL11.GL_POLYGON) {
// GL_POLYGON is removed in core profile; convert to GL_TRIANGLE_FAN which is equivalent for convex polygons
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, GL11.GL_TRIANGLE_FAN, start, vertexCount);
} else {
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, data.drawMode, start, vertexCount);
}
@@ -74,10 +74,7 @@ public final class GLCommand {
// === Double commands ===
public static final int TRANSLATE = 70; // [cmd:4][mode:4][x:8d][y:8d][z:8d] = 32 bytes
public static final int ROTATE = 71; // [cmd:4][mode:4][angle:8d][x:8d][y:8d][z:8d] = 40 bytes
public static final int SCALE = 72; // [cmd:4][mode:4][x:8d][y:8d][z:8d] = 32 bytes
public static final int ORTHO = 73; // [cmd:4][left:8d][right:8d][bottom:8d][top:8d][zNear:8d][zFar:8d] = 52 bytes
public static final int FRUSTUM = 74; // [cmd:4][left:8d][right:8d][bottom:8d][top:8d][zNear:8d][zFar:8d] = 52 bytes
public static final int CLEAR_DEPTH = 75; // [cmd:4][depth:8d] = 12 bytes
// === Matrix commands (72 bytes) ===
@@ -97,8 +94,6 @@ public final class GLCommand {
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)
/**
@@ -169,10 +164,7 @@ public final class GLCommand {
case MATERIALF -> "MATERIALF";
case TEX_PARAMETERF -> "TEX_PARAMETERF";
case TRANSLATE -> "TRANSLATE";
case ROTATE -> "ROTATE";
case SCALE -> "SCALE";
case ORTHO -> "ORTHO";
case FRUSTUM -> "FRUSTUM";
case CLEAR_DEPTH -> "CLEAR_DEPTH";
case MULT_MATRIX -> "MULT_MATRIX";
case LOAD_MATRIX -> "LOAD_MATRIX";
@@ -219,7 +211,7 @@ public final class GLCommand {
// 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;
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF -> 16;
// Four int commands (20 bytes)
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
@@ -227,8 +219,6 @@ public final class GLCommand {
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;
@@ -239,10 +229,8 @@ public final class GLCommand {
case GLCommand.DRAW_BUFFERS -> 40;
// Double commands
case GLCommand.TRANSLATE, GLCommand.SCALE -> 28; // cmd + 3 doubles
case GLCommand.TRANSLATE, GLCommand.SCALE -> 16; // cmd + 3 floats
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
@@ -1,25 +1,22 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.CallbackTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* 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 final class ImmediateModeRecorder {
private static final DirectTessellator tessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
private static final CallbackTessellator tessellator = new CallbackTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
/** Separate tessellator for the splash thread. Nulled after splash completes. */
private static DirectTessellator splashTessellator;
@@ -33,6 +30,7 @@ public final class ImmediateModeRecorder {
}
public static void destroySplashTessellator() {
splashTessellator.delete();
splashTessellator = null;
}
@@ -167,19 +165,14 @@ public final class ImmediateModeRecorder {
.mapToInt(Usage::getAttributeLocation)
.max().orElse(0);
private static final ByteBuffer[] scratchAttribBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchAttribBaseOffsets = new long[NUM_ATTRIBS];
private static final int[] scratchVBOIds = new int[NUM_ATTRIBS];
private static final ByteBuffer[] scratchVBOBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchVBOReadOffsets = new long[NUM_ATTRIBS];
private static final class AttribReader {
ByteBuffer data;
int stride;
int baseOffset;
int size;
int loc;
VertexAttribState.Attrib attrib;
int type;
boolean normalized;
}
private static final AttribReader[] readers = new AttribReader[NUM_ATTRIBS];
@@ -198,167 +191,32 @@ public final class ImmediateModeRecorder {
* Reads vertex data from VBOs or client memory referenced by VertexAttribState and converts to immediate mode geometry.
*/
public static DirectTessellator processDrawArraysFromAttribs(int mode, int first, int count) {
final int readVBOCount = readbackAttribVBOs(first, count);
final AttribSnapshot snap = AttribSnapshot.snapshot(first, count);
try {
prepareReaders();
prepareReaders(snap);
if (positionReaderIndex < 0) return null;
return readVerticesFromBuffer(mode, first, count);
} finally {
freeReadbackBuffers(readVBOCount);
snap.free();
}
}
/**
* Process a glDrawElements call during display list recording when a VBO is bound.
*/
public static DirectTessellator processDrawElementsFromVBO(int mode, int indicesCount, int indexType, long indicesOffset, int eboId) {
if (eboId == 0) return null;
final int readVBOCount = readbackAttribVBOs(-1, 0);
try {
prepareReaders();
if (positionReaderIndex < 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboSize <= 0) {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final int indexSize = VertexAttribState.Attrib.glTypeSizeBytes(indexType);
final int eboReadOffset = (int) indicesOffset;
final int eboReadSize = indicesCount * indexSize;
if (eboReadOffset + eboReadSize > eboSize) {
GLStateManager.LOGGER.warn("[EBO] Read range {}+{} exceeds buffer size {}", eboReadOffset, eboReadSize, eboSize);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final ByteBuffer eboData = MemoryUtilities.memAlloc(eboReadSize);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboReadOffset, eboData);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
try {
return readVerticesFromBufferIndexed(mode, eboData, indexType, 0, indicesCount);
} finally {
MemoryUtilities.memFree(eboData);
}
} finally {
freeReadbackBuffers(readVBOCount);
}
}
/**
* Read data for all enabled attribs into scratchAttribBuffers.
* For VBO-backed attribs, reads VBO data. For client-memory attribs, uses the client pointer directly.
*/
private static int readbackAttribVBOs(int firstVertex, int vertexCount) {
int readVBOCount = 0;
final int prevVBO = GLStateManager.getBoundVBO();
final boolean partialRead = firstVertex >= 0;
for (int i = 0; i < NUM_ATTRIBS; i++) {
scratchAttribBuffers[i] = null;
scratchAttribBaseOffsets[i] = 0;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled) continue;
if (a.vboId != 0) {
// VBO-backed attrib — read from GPU
ByteBuffer existing = null;
int existingIdx = -1;
for (int j = 0; j < readVBOCount; j++) {
if (scratchVBOIds[j] == a.vboId) {
existing = scratchVBOBuffers[j];
existingIdx = j;
break;
}
}
if (existing != null) {
scratchAttribBuffers[i] = existing;
scratchAttribBaseOffsets[i] = scratchVBOReadOffsets[existingIdx];
} else {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long readOffset;
final int readSize;
if (partialRead) {
final int stride = a.effectiveStride();
long rangeStart = a.offset + (long) firstVertex * stride;
long rangeEnd = a.offset + (long) (firstVertex + vertexCount - 1) * stride + (long) a.size * a.typeSizeBytes();
for (int k = 0; k < NUM_ATTRIBS; k++) {
if (k == i) continue;
final VertexAttribState.Attrib ak = VertexAttribState.get(k);
if (!ak.enabled || ak.vboId != a.vboId) continue;
final int ks = ak.effectiveStride();
rangeStart = Math.min(rangeStart, ak.offset + (long) firstVertex * ks);
rangeEnd = Math.max(rangeEnd, ak.offset + (long) (firstVertex + vertexCount - 1) * ks
+ (long) ak.size * ak.typeSizeBytes());
}
rangeStart = Math.max(0, rangeStart);
rangeEnd = Math.min(bufferSize, rangeEnd);
readOffset = rangeStart;
readSize = (int) (rangeEnd - rangeStart);
} else {
readOffset = 0;
readSize = bufferSize;
}
if (readSize <= 0) continue;
final ByteBuffer buf = MemoryUtilities.memAlloc(readSize);
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, readOffset, buf);
scratchAttribBuffers[i] = buf;
scratchAttribBaseOffsets[i] = readOffset;
scratchVBOIds[readVBOCount] = a.vboId;
scratchVBOReadOffsets[readVBOCount] = readOffset;
scratchVBOBuffers[readVBOCount] = buf;
readVBOCount++;
}
} else if (a.clientPointer != null) {
// Client-memory attrib — use pointer directly (no allocation needed)
scratchAttribBuffers[i] = a.clientPointer;
}
}
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
return readVBOCount;
}
private static void freeReadbackBuffers(int readVBOCount) {
for (int i = 0; i < readVBOCount; i++) {
MemoryUtilities.memFree(scratchVBOBuffers[i]);
scratchVBOBuffers[i] = null;
}
}
private static void prepareReaders() {
private static void prepareReaders(AttribSnapshot snap) {
nonPositionCount = 0;
positionReaderIndex = -1;
for (int i = 0; i < NUM_ATTRIBS; i++) {
final ByteBuffer buf = scratchAttribBuffers[i];
if (buf == null) continue;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
final AttribSnapshot.AttribDesc d = snap.get(i);
if (d == null) continue;
final AttribReader r = readers[i];
r.data = buf;
r.stride = a.effectiveStride();
r.baseOffset = (int) (a.offset - scratchAttribBaseOffsets[i]);
r.size = a.size;
r.data = d.readBuffer();
r.stride = d.effectiveStride();
r.baseOffset = (int) (d.offset() - d.readBufferBaseOffset());
r.size = d.size();
r.loc = i;
r.attrib = a;
r.type = d.type();
r.normalized = d.normalized();
if (i == LOC_POSITION) {
positionReaderIndex = i;
@@ -379,18 +237,6 @@ public final class ImmediateModeRecorder {
return end();
}
private static DirectTessellator readVerticesFromBufferIndexed(int mode, ByteBuffer eboData, int indexType, int eboOffset, int indicesCount) {
begin(mode);
final DirectTessellator t = tessellator();
for (int i = 0; i < indicesCount; i++) {
final int index = readIndex(eboData, indexType, eboOffset, i);
readVertexFromBuffer(t, index);
}
return end();
}
public static int readIndex(ByteBuffer ebo, int indexType, int eboOffset, int i) {
return switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> ebo.get(eboOffset + i) & 0xFF;
@@ -411,39 +257,43 @@ public final class ImmediateModeRecorder {
final AttribReader r = readers[positionReaderIndex];
final int base = r.baseOffset + vertexIndex * r.stride;
t.addVertex(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
(r.size >= 3) ? r.attrib.readComponent(r.data, base, 2) : 0.0f);
readComponent(r, base, 0),
readComponent(r, base, 1),
(r.size >= 3) ? readComponent(r, base, 2) : 0.0f);
}
}
private static void emitAttrib(DirectTessellator t, AttribReader r, int base) {
if (r.loc == LOC_NORMAL) {
t.setNormal(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
r.attrib.readComponent(r.data, base, 2));
readComponent(r, base, 0),
readComponent(r, base, 1),
readComponent(r, base, 2));
} else if (r.loc == LOC_UV0) {
t.setTextureUV(
r.attrib.readComponent(r.data, base, 0),
(r.size >= 2) ? r.attrib.readComponent(r.data, base, 1) : 0.0f);
readComponent(r, base, 0),
(r.size >= 2) ? readComponent(r, base, 1) : 0.0f);
} else if (r.loc == LOC_UV1) {
if (r.attrib.type == GL11.GL_SHORT) {
if (r.type == GL11.GL_SHORT) {
final int s = r.data.getShort(base) & 0xFFFF;
final int u = (r.size >= 2) ? (r.data.getShort(base + 2) & 0xFFFF) : 0;
t.setBrightness((u << 16) | s);
} else if (r.attrib.type == GL11.GL_FLOAT) {
} else if (r.type == GL11.GL_FLOAT) {
final int s = (int) r.data.getFloat(base);
final int u = (r.size >= 2) ? (int) r.data.getFloat(base + 4) : 0;
t.setBrightness((u << 16) | (s & 0xFFFF));
}
} else if (r.loc == LOC_COLOR) {
final float cr = r.attrib.readComponent(r.data, base, 0);
final float cg = r.attrib.readComponent(r.data, base, 1);
final float cb = r.attrib.readComponent(r.data, base, 2);
final float ca = (r.size >= 4) ? r.attrib.readComponent(r.data, base, 3) : 1.0f;
final float cr = readComponent(r, base, 0);
final float cg = readComponent(r, base, 1);
final float cb = readComponent(r, base, 2);
final float ca = (r.size >= 4) ? readComponent(r, base, 3) : 1.0f;
t.setColorRGBA((int) (cr * 255.0f), (int) (cg * 255.0f), (int) (cb * 255.0f), (int) (ca * 255.0f));
}
}
private static float readComponent(AttribReader r, int base, int component) {
return VertexAttribState.Attrib.readComponent(r.type, r.normalized, r.data, base, component);
}
}
@@ -0,0 +1,64 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import java.util.Arrays;
public final class AttribLayoutKey {
final int[] locations;
final int[] sizes;
final int[] types;
final boolean[] normalized;
private final int[] offsets;
private final int stride;
private final int hash;
public AttribLayoutKey(int[] locations, int[] sizes, int[] types, boolean[] normalized) {
this.locations = locations;
this.sizes = sizes;
this.types = types;
this.normalized = normalized;
final int n = locations.length;
final int[] off = new int[n];
int base = 0;
int maxAlign = 1;
for (int i = 0; i < n; i++) {
final int typeSize = VertexAttribState.Attrib.glTypeSizeBytes(types[i]);
if (typeSize > maxAlign) maxAlign = typeSize;
base = (base + typeSize - 1) & -typeSize;
off[i] = base;
base += sizes[i] * typeSize;
}
this.offsets = off;
this.stride = (base + maxAlign - 1) & -maxAlign;
int h = Arrays.hashCode(locations);
h = 31 * h + Arrays.hashCode(sizes);
h = 31 * h + Arrays.hashCode(types);
h = 31 * h + Arrays.hashCode(normalized);
this.hash = h;
}
public int offset(int i) {
return offsets[i];
}
public int stride() {
return stride;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof AttribLayoutKey k)) return false;
return Arrays.equals(locations, k.locations)
&& Arrays.equals(sizes, k.sizes)
&& Arrays.equals(types, k.types)
&& Arrays.equals(normalized, k.normalized);
}
@Override
public int hashCode() {
return hash;
}
}
@@ -0,0 +1,37 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
private int sharedVAO;
private int drawMode;
private int indexCount;
private int indexType;
private long indexByteOffset;
public void fill(int sharedVAO, int drawMode, int indexCount, int indexType, long indexByteOffset) {
this.sharedVAO = sharedVAO;
this.drawMode = drawMode;
this.indexCount = indexCount;
this.indexType = indexType;
this.indexByteOffset = indexByteOffset;
}
public int getSharedVAO() { return sharedVAO; }
public int getDrawMode() { return drawMode; }
public int getIndexCount() { return indexCount; }
public int getIndexType() { return indexType; }
public long getIndexByteOffset() { return indexByteOffset; }
@Override
public void execute() {
if (sharedVAO == 0) return;
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
}
@Override
public String toString() {
return "BatchedIndexedDrawCmd(vao=" + sharedVAO + ", mode=" + drawMode + ", count=" + indexCount + ", type=" + indexType + ", offset=" + indexByteOffset + ")";
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class IndexedDrawBatch {
private int sharedVAO;
private int sharedVBO;
private int sharedEBO;
private boolean deleted;
public IndexedDrawBatch(int sharedVAO, int sharedVBO, int sharedEBO) {
this.sharedVAO = sharedVAO;
this.sharedVBO = sharedVBO;
this.sharedEBO = sharedEBO;
}
public int getSharedVAO() { return sharedVAO; }
public int getSharedVBO() { return sharedVBO; }
public int getSharedEBO() { return sharedEBO; }
public void delete() {
if (deleted) return;
deleted = true;
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
sharedVAO = sharedVBO = sharedEBO = 0;
}
@Override
public String toString() {
return "IndexedDrawBatch(vao=" + sharedVAO + ", vbo=" + sharedVBO + ", ebo=" + sharedEBO + ")";
}
}
@@ -0,0 +1,171 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
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.memCopy;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
public final class IndexedDrawBatchBuilder {
private final List<IndexedDrawCapture> captures = new ArrayList<>();
public boolean isEmpty() {
return captures.isEmpty();
}
public void add(IndexedDrawCapture capture) {
captures.add(capture);
}
public List<IndexedDrawCapture> getCaptures() {
return captures;
}
public List<IndexedDrawBatch> build() {
final List<IndexedDrawBatch> out = new ArrayList<>();
if (captures.isEmpty()) return out;
final int prevVAO = GLStateManager.getBoundVAO();
final int prevVBO = GLStateManager.getBoundVBO();
final int prevEBO = GLStateManager.getBoundEBO();
final List<GroupResult> results = new ArrayList<>();
boolean success = false;
try {
final Object2ObjectLinkedOpenHashMap<AttribLayoutKey, List<IndexedDrawCapture>> groups = new Object2ObjectLinkedOpenHashMap<>();
for (IndexedDrawCapture c : captures) {
groups.computeIfAbsent(c.layoutKey, k -> new ArrayList<>()).add(c);
}
for (List<IndexedDrawCapture> group : groups.values()) {
results.add(buildGroup(group));
}
// Placeholder fill is deferred until every group's GL work has succeeded.
for (GroupResult r : results) r.commit();
for (GroupResult r : results) out.add(r.batch);
success = true;
} finally {
if (!success) {
for (GroupResult r : results) r.batch.delete();
}
GLStateManager.glBindVertexArray(prevVAO);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
return out;
}
private record GroupResult(IndexedDrawBatch batch, List<IndexedDrawCapture> captures,
int[] drawModes, int[] indexCounts, long[] indexOffsets) {
void commit() {
final int sharedVAO = batch.getSharedVAO();
for (int i = 0; i < captures.size(); i++) {
captures.get(i).placeholder.fill(sharedVAO, drawModes[i], indexCounts[i], GL11.GL_UNSIGNED_INT, indexOffsets[i]);
}
}
}
private static GroupResult buildGroup(List<IndexedDrawCapture> group) {
long totalVertexBytes = 0;
long totalIndexBytes = 0;
for (IndexedDrawCapture c : group) {
totalVertexBytes += (long) c.vertexCount * c.layoutKey.stride();
totalIndexBytes += (long) c.indexCount * 4; // always GL_UNSIGNED_INT
}
if (totalVertexBytes > Integer.MAX_VALUE || totalIndexBytes > Integer.MAX_VALUE) {
throw new IllegalStateException("batch exceeds 2GiB — split needed");
}
int sharedVAO = 0;
int sharedVBO = 0;
int sharedEBO = 0;
ByteBuffer eboData = null;
boolean success = false;
final int n = group.size();
final int[] drawModes = new int[n];
final int[] indexCounts = new int[n];
final long[] indexOffsets = new long[n];
try {
sharedVAO = GLStateManager.glGenVertexArrays();
sharedVBO = GLStateManager.glGenBuffers();
sharedEBO = GLStateManager.glGenBuffers();
eboData = MemoryUtilities.memAlloc((int) totalIndexBytes);
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, totalVertexBytes, GL15.GL_STATIC_DRAW);
int vboBytePos = 0;
int eboBytePos = 0;
int vertexBase = 0;
final long eboAddr = memAddress0(eboData);
for (int g = 0; g < n; g++) {
final IndexedDrawCapture c = group.get(g);
final int vBytes = c.vertexCount * c.layoutKey.stride();
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, vboBytePos, c.vertexData);
final long srcIdxAddr = memAddress0(c.indexData);
final int idxBytes = c.indexCount * 4;
if (vertexBase == 0) {
// First capture (or a group that starts at 0) — indices are already
// rebased to [0..vertexCount) in IndexedDrawCapture, so a raw bulk copy
// is correct and strictly faster than the per-index loop.
memCopy(srcIdxAddr, eboAddr + eboBytePos, idxBytes);
} else {
final long dstBase = eboAddr + eboBytePos;
for (int i = 0; i < c.indexCount; i++) {
memPutInt(dstBase + i * 4L, memGetInt(srcIdxAddr + i * 4L) + vertexBase);
}
}
drawModes[g] = c.drawMode;
indexCounts[g] = c.indexCount;
indexOffsets[g] = eboBytePos;
vboBytePos += vBytes;
eboBytePos += idxBytes;
vertexBase += c.vertexCount;
}
eboData.position(0).limit((int) totalIndexBytes);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboData, GL15.GL_STATIC_DRAW);
final AttribLayoutKey layout = group.get(0).layoutKey;
final int stride = layout.stride();
for (int i = 0; i < layout.locations.length; i++) {
final int loc = layout.locations[i];
final int size = layout.sizes[i];
final int type = layout.types[i];
GLStateManager.glVertexAttribPointer(loc, size, type, layout.normalized[i], stride, layout.offset(i));
GLStateManager.glEnableVertexAttribArray(loc);
}
success = true;
return new GroupResult(new IndexedDrawBatch(sharedVAO, sharedVBO, sharedEBO),
group, drawModes, indexCounts, indexOffsets);
} finally {
if (eboData != null) MemoryUtilities.memFree(eboData);
if (!success) {
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
}
}
}
}
@@ -0,0 +1,242 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.recording.AttribSnapshot;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Set;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetLong;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutLong;
/** {@link #create} returns null on any recoverable validation failure; callers must not record the placeholder. */
public final class IndexedDrawCapture {
public final AttribLayoutKey layoutKey;
public final int drawMode;
public final int vertexCount;
public final int indexCount;
public final ByteBuffer vertexData;
public final ByteBuffer indexData;
public final BatchedIndexedDrawCmd placeholder;
private boolean freed;
public IndexedDrawCapture(AttribLayoutKey layoutKey, int drawMode, int vertexCount, int indexCount,
ByteBuffer vertexData, ByteBuffer indexData,
BatchedIndexedDrawCmd placeholder) {
this.layoutKey = layoutKey;
this.drawMode = drawMode;
this.vertexCount = vertexCount;
this.indexCount = indexCount;
this.vertexData = vertexData;
this.indexData = indexData;
this.placeholder = placeholder;
}
public void freeBuffers() {
if (freed) return;
freed = true;
if (vertexData != null) MemoryUtilities.memFree(vertexData);
if (indexData != null) MemoryUtilities.memFree(indexData);
}
public boolean isFreed() {
return freed;
}
private static final Set<String> WARN_ONCE = ConcurrentHashMap.newKeySet();
private static void warnOnce(String key, String fmt, Object... args) {
if (WARN_ONCE.add(key)) GLStateManager.LOGGER.warn(fmt, args);
}
public static IndexedDrawCapture create(int mode, int indicesCount, int srcIndexType, long indicesOffset, int eboId) {
if (indicesCount == 0) return null;
if (eboId == 0) {
warnOnce("no-ebo", "[IndexedDrawCapture] glDrawElements in display list with no EBO bound — skipping (mode={} count={} offset={})", mode, indicesCount, indicesOffset);
return null;
}
if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) {
warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 — skipping", indicesCount);
return null;
}
final int srcIndexSize = VertexAttribState.Attrib.glTypeSizeBytes(srcIndexType);
if (srcIndexSize <= 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
final int prevVBO = GLStateManager.getBoundVBO();
ByteBuffer eboSrc = null;
AttribSnapshot snap = null;
ByteBuffer vertexData = null;
ByteBuffer indexData = null;
boolean success = false;
try {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboBufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboBufferSize <= 0) {
warnOnce("ebo-size", "[IndexedDrawCapture] GL_BUFFER_SIZE={} for EBO {}", eboBufferSize, eboId);
return null;
}
final long eboReadSize = (long) indicesCount * srcIndexSize;
if (indicesOffset < 0 || indicesOffset + eboReadSize > eboBufferSize) {
warnOnce("ebo-range", "[IndexedDrawCapture] EBO range {}+{} exceeds buffer size {} — skipping", indicesOffset, eboReadSize, eboBufferSize);
return null;
}
eboSrc = MemoryUtilities.memAlloc((int) eboReadSize);
GLStateManager.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesOffset, eboSrc);
final long eboSrcAddr = MemoryUtilities.memAddress0(eboSrc);
final long minMax = QuadConverter.scanMinMaxIndex(eboSrcAddr, srcIndexType, indicesCount);
if (minMax == -1L) {
warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType);
return null;
}
final int minVtx = (int) (minMax & 0xFFFFFFFFL);
final int maxVtx = (int) (minMax >>> 32);
final int vertexCount = maxVtx - minVtx + 1;
snap = AttribSnapshot.snapshot(minVtx, vertexCount);
int enabledCount = 0;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
if (snap.get(i) != null) enabledCount++;
}
if (enabledCount == 0) {
warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping");
return null;
}
final int[] locations = new int[enabledCount];
final int[] sizes = new int[enabledCount];
final int[] types = new int[enabledCount];
final boolean[] normalized = new boolean[enabledCount];
int k = 0;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
final AttribSnapshot.AttribDesc d = snap.get(i);
if (d == null) continue;
locations[k] = i;
sizes[k] = d.size();
types[k] = d.type();
normalized[k] = d.normalized();
k++;
}
final AttribLayoutKey layoutKey = new AttribLayoutKey(locations, sizes, types, normalized);
final int tightStride = layoutKey.stride();
ByteBuffer sharedSrcBuffer = null;
long sharedSrcBase = 0;
int sharedSrcStride = -1;
boolean tightSrc = true;
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int relOffset = (int) (d.offset() - d.readBufferBaseOffset());
if (a == 0) {
sharedSrcBuffer = d.readBuffer();
sharedSrcStride = d.effectiveStride();
sharedSrcBase = MemoryUtilities.memAddress0(sharedSrcBuffer) + relOffset + (long) minVtx * sharedSrcStride;
if (relOffset != layoutKey.offset(0)) tightSrc = false;
} else if (d.readBuffer() != sharedSrcBuffer || d.effectiveStride() != sharedSrcStride || relOffset != layoutKey.offset(a)) {
tightSrc = false;
}
}
if (tightSrc && sharedSrcStride != tightStride) tightSrc = false;
vertexData = MemoryUtilities.memAlloc(vertexCount * tightStride);
final long dstAddr = MemoryUtilities.memAddress0(vertexData);
if (tightSrc) {
MemoryUtilities.memCopy(sharedSrcBase, dstAddr, (long) vertexCount * tightStride);
} else {
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int attribBytes = d.size() * d.typeSizeBytes();
final int srcStride = d.effectiveStride();
final long srcStart = MemoryUtilities.memAddress0(d.readBuffer()) + (d.offset() - d.readBufferBaseOffset()) + (long) minVtx * srcStride;
final long dstStart = dstAddr + layoutKey.offset(a);
copyStrided(srcStart, srcStride, dstStart, tightStride, attribBytes, vertexCount);
}
}
final int bakedDrawMode;
final int bakedIndexCount;
if (mode == GL11.GL_QUADS) {
final int quadCount = indicesCount / 4;
bakedIndexCount = quadCount * 6;
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
QuadConverter.triangulateQuads(eboSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, quadCount, minVtx);
bakedDrawMode = GL11.GL_TRIANGLES;
} else {
bakedIndexCount = indicesCount;
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
QuadConverter.widenIndices(eboSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, indicesCount, minVtx);
bakedDrawMode = mode;
}
final BatchedIndexedDrawCmd placeholder = new BatchedIndexedDrawCmd();
final IndexedDrawCapture out = new IndexedDrawCapture(layoutKey, bakedDrawMode, vertexCount, bakedIndexCount, vertexData, indexData, placeholder);
success = true;
return out;
} finally {
if (eboSrc != null) MemoryUtilities.memFree(eboSrc);
if (snap != null) snap.free();
if (!success) {
if (vertexData != null) MemoryUtilities.memFree(vertexData);
if (indexData != null) MemoryUtilities.memFree(indexData);
}
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
}
/**
* Copy {@code attribBytes} per vertex from {@code src} (stride {@code srcStride}) to
* {@code dst} (stride {@code dstStride}) for {@code vertexCount} vertices.
*
* <p>Specializes on the common attribute sizes (4 / 8 / 12 / 16) so the hot path is
* one or two {@code memPut} calls per vertex instead of a JNI-ish {@code memCopy}
* dispatch. Falls through to {@code memCopy} for exotic sizes.
*/
private static void copyStrided(long src, int srcStride, long dst, int dstStride, int attribBytes, int vertexCount) {
switch (attribBytes) {
case 4 -> {
for (int v = 0; v < vertexCount; v++) {
memPutInt(dst, memGetInt(src));
src += srcStride; dst += dstStride;
}
}
case 8 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
src += srcStride; dst += dstStride;
}
}
case 12 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
memPutInt(dst + 8, memGetInt(src + 8));
src += srcStride; dst += dstStride;
}
}
case 16 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
memPutLong(dst + 8, memGetLong(src + 8));
src += srcStride; dst += dstStride;
}
}
default -> {
for (int v = 0; v < vertexCount; v++) {
MemoryUtilities.memCopy(src, dst, attribBytes);
src += srcStride; dst += dstStride;
}
}
}
}
}
@@ -4,10 +4,12 @@ import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
@@ -524,7 +526,7 @@ public class GLSMRedirector {
boolean changed = false;
final boolean isOpenGlHelper = transformedName.equals("net.minecraft.client.renderer.OpenGlHelper");
for (MethodNode mn : cn.methods) {
for (MethodNode mn : (List<MethodNode>) cn.methods) {
if (isOpenGlHelper && (mn.name.equals("glBlendFunc") || mn.name.equals("func_148821_a"))) {
continue;
}
@@ -610,6 +612,22 @@ public class GLSMRedirector {
changed = true;
}
}
} else if (node instanceof InvokeDynamicInsnNode dynNode) {
// Redirect method handles in invokedynamic bootstrap arguments
for (int i = 0; i < dynNode.bsmArgs.length; i++) {
if (!(dynNode.bsmArgs[i] instanceof Handle handle)) continue;
final Map<String, String> redirects = handle.getOwner().startsWith(GL_PREFIX) ? glMethodRedirects : methodRedirects.get(handle.getOwner());
if (redirects == null) continue;
final String glsmName = redirects.get(handle.getName());
if (glsmName == null) continue;
if (LOG_SPAM) {
final String shortOwner = handle.getOwner().substring(handle.getOwner().lastIndexOf("/") + 1);
LOGGER.info("Redirecting invokedynamic handle in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, handle.getName(), handle.getDesc(), glsmName, handle.getDesc());
}
dynNode.bsmArgs[i] = new Handle(handle.getTag(), GLStateManager, glsmName, handle.getDesc());
changed = true;
redirectInMethod = true;
}
}
}
if (ASSERT_MAIN_THREAD && redirectInMethod && !transformedName.startsWith(SplashProgress) && !(transformedName.startsWith(MinecraftClient) && ExcludedMinecraftMainThreadChecks.contains(mn.name))) {
@@ -103,9 +103,12 @@ public class MaterialState implements ISettableState<MaterialState> {
}
}
public void setShininess(FloatBuffer newBuffer) { setShininess(newBuffer.get()); }
// On the buffer versions of shininess, we need to call `get(0)` explicitly specifying the index
// otherwise calling `get()` advances the buffer position, and some mods may re-use the same buffer
// in subsequent calls, resulting in a BufferUnderflowException.
public void setShininess(FloatBuffer newBuffer) { setShininess(newBuffer.get(0)); }
public void setShininess(IntBuffer newBuffer) { setShininess((float) newBuffer.get());}
public void setShininess(IntBuffer newBuffer) { setShininess((float) newBuffer.get(0)); }
public void setShininess(int val) { setShininess((float) val);}
@@ -16,16 +16,19 @@ public class VertexAttribState {
private static final Int2ObjectOpenHashMap<Attrib[]> vaoAttribs = new Int2ObjectOpenHashMap<>();
private static Attrib[] current;
private static final ArrayDeque<Attrib[]> pool = new ArrayDeque<>();
private static int clientSideEnabledCount;
public static void init(int defaultVAO) {
vaoAttribs.clear();
pool.clear();
current = allocAttribArray();
vaoAttribs.put(defaultVAO, current);
clientSideEnabledCount = 0;
}
public static void onBindVertexArray(int newVAO) {
current = vaoAttribs.computeIfAbsent(newVAO, k -> allocAttribArray());
recomputeClientSideCount();
}
public static void onDeleteVertexArray(int vaoId) {
@@ -39,6 +42,7 @@ public class VertexAttribState {
vaoAttribs.clear();
pool.clear();
current = null;
clientSideEnabledCount = 0;
}
private static Attrib[] allocAttribArray() {
@@ -55,6 +59,7 @@ public class VertexAttribState {
public static void set(int index, int size, int type, boolean normalized, int stride, long offset, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
final boolean was = a.isClientSide();
a.size = size;
a.type = type;
a.normalized = normalized;
@@ -62,11 +67,13 @@ public class VertexAttribState {
a.offset = offset;
a.vboId = vboId;
a.clientPointer = null;
if (was) clientSideEnabledCount--;
}
public static void set(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
final boolean was = a.isClientSide();
a.size = size;
a.type = type;
a.normalized = normalized;
@@ -74,24 +81,41 @@ public class VertexAttribState {
a.offset = 0;
a.vboId = vboId;
a.clientPointer = (vboId == 0) ? pointer : null;
final boolean now = a.isClientSide();
if (was != now) clientSideEnabledCount += now ? 1 : -1;
}
public static void setEnabled(int index, boolean enabled) {
if (index < 0 || index >= MAX_ATTRIBS) return;
current[index].enabled = enabled;
final Attrib a = current[index];
if (a.clientPointer != null && a.enabled != enabled) {
clientSideEnabledCount += enabled ? 1 : -1;
}
a.enabled = enabled;
}
public static Attrib get(int index) {
return current[index];
}
public static boolean hasVBOBoundAttrib() {
private static void recomputeClientSideCount() {
int count = 0;
for (int i = 0; i < MAX_ATTRIBS; i++) {
if (current[i].enabled && current[i].vboId != 0) return true;
if (current[i].isClientSide()) count++;
}
return false;
clientSideEnabledCount = count;
}
/**
* Returns true if any currently enabled vertex attribute was registered without a VBO
* (i.e. as a client-side pointer). In core profile, such attribs are treated as null
* offsets into a non-existent buffer, causing a native crash at draw time.
*/
public static boolean hasAnyClientSideEnabledAttrib() {
return clientSideEnabledCount > 0;
}
public static class Attrib {
public boolean enabled;
public int size;
@@ -102,6 +126,10 @@ public class VertexAttribState {
public int vboId;
public ByteBuffer clientPointer;
public boolean isClientSide() {
return enabled && clientPointer != null;
}
public void reset() {
enabled = false;
size = 0;
@@ -132,6 +160,10 @@ public class VertexAttribState {
}
public float readComponent(ByteBuffer buf, int base, int component) {
return readComponent(type, normalized, buf, base, component);
}
public static float readComponent(int type, boolean normalized, ByteBuffer buf, int base, int component) {
return switch (type) {
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
@@ -40,7 +41,7 @@ public final class OrphanStreamingBuffer {
public void destroy() {
if (bufferId != 0) {
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import org.embeddedt.embeddium.impl.gl.sync.GlFence;
@@ -52,7 +53,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (this.mappedBuffer == null) {
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
throw new RuntimeException("Failed to persistently map streaming buffer");
}
@@ -147,7 +148,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
RENDER_BACKEND.unmapBuffer(GL15.GL_ARRAY_BUFFER);
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
@@ -1,4 +1,4 @@
package com.gtnewhorizons.angelica.client.rendering;
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
@@ -8,8 +8,6 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
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;
@@ -128,6 +126,19 @@ public class TessellatorStreamingDrawer {
uploadAndDraw(buffer, flags, format, vertexSize, drawMode, vertexCount);
}
/**
* Upload pre-packed vertex data and draw. Public API for external batch systems.
* @param packedData buffer positioned at 0 with limit set to total bytes
* @param drawMode GL draw mode (GL_QUADS, GL_TRIANGLES, etc.)
* @param flags vertex format flags (from VertexFlags)
* @param vertexCount number of vertices
*/
public static void drawPacked(ByteBuffer packedData, int drawMode, int flags, int vertexCount) {
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
final int vertexSize = format.getVertexSize();
uploadAndDraw(packedData, flags, format, vertexSize, drawMode, vertexCount);
}
private static String cachedDebugInfo = "Stream: not initialized";
private static long lastDebugUpdateNanos;
private static final long DEBUG_UPDATE_INTERVAL_NS = 500_000_000L; // 500ms
@@ -172,34 +183,6 @@ public class TessellatorStreamingDrawer {
}
}
/**
* Draw a merged batch of pre-packed vertex data from a DeferredBatchTessellator.
* Copies byte ranges into repackBuffer, then uploads and draws.
*/
static void drawPackedBatch(DeferredBatchTessellator source,
java.util.List<DeferredBatchTessellator.DrawRange> ranges,
int from, int to,
int totalBytes, int totalVertices,
int drawMode, int flags) {
if (totalVertices == 0) return;
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
final int vertexSize = format.getVertexSize();
ensureRepackCapacity(totalBytes);
long writePos = repackAddress;
for (int j = from; j < to; j++) {
final DeferredBatchTessellator.DrawRange r = ranges.get(j);
source.copyRange(r.byteOffset(), r.byteLength(), writePos);
writePos += r.byteLength();
}
repackBuffer.position(0);
repackBuffer.limit(totalBytes);
uploadAndDraw(repackBuffer, flags, format, vertexSize, drawMode, totalVertices);
}
/**
* Upload packed vertex data to a streaming buffer and issue the draw call.
* Tries the persistent ring buffer first, falls back to orphan buffer on overflow.
@@ -239,7 +222,11 @@ public class TessellatorStreamingDrawer {
}
}
private static void ensureRepackCapacity(int requiredBytes) {
/**
* Ensure the repack buffer is large enough for the given byte count.
* Public for use by external batch systems that need to pack data before calling {@link #drawPacked}.
*/
public static void ensureRepackCapacity(int requiredBytes) {
if (requiredBytes <= repackCapacity) return;
int newCapacity = repackCapacity;
@@ -253,6 +240,16 @@ public class TessellatorStreamingDrawer {
repackCapacity = newCapacity;
}
/** Get the repack buffer's native address. Valid until next {@link #ensureRepackCapacity} call. */
public static long getRepackAddress() {
return repackAddress;
}
/** Get the repack ByteBuffer. Caller must set position/limit before passing to {@link #drawPacked}. */
public static ByteBuffer getRepackBuffer() {
return repackBuffer;
}
private static void ensureVAO(int flags, VertexFormat format) {
init();
@@ -596,4 +596,21 @@ class CompatShaderTransformerTest {
assertFalse(fragResult.contains("varying"), "no 'varying' keyword in fragment");
assertTrue(fragResult.contains("in vec2 v_TexCoord"), "varying → in in fragment");
}
@Test
void testReservedWordNewIsRenamed() {
String src = """
#version 120
uniform sampler2D tex;
void main() {
vec4 color = texture2D(tex, vec2(0.5));
float new = color.r > 0.5 ? 1.0 : 0.0;
gl_FragColor = vec4(new, new, new, 1.0);
}
""";
String result = CompatShaderTransformer.transform(src, true);
assertFalse(result.matches("(?s).*\\bnew\\b(?!\\s*\\().*"), "'new' as identifier must be renamed in core profile output\n\n" + result);
assertTrue(result.contains("angelica_renamed_new"), "'new' should be renamed to angelica_renamed_new\n\n" + result);
}
}
@@ -107,6 +107,7 @@ class DisplayListCompilationGLSMTest {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glTranslatef(10, 0, 0); // Applied immediately
DisplayListManager.flushMatrix();
FloatBuffer afterTranslate = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, afterTranslate);
@@ -216,6 +217,7 @@ class DisplayListCompilationGLSMTest {
GLStateManager.glTranslatef(10, 0, 0); // Applied immediately
GLStateManager.glRotatef(45, 0, 1, 0); // Applied immediately
DisplayListManager.flushMatrix();
FloatBuffer afterTransforms = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, afterTransforms);
@@ -241,6 +243,7 @@ class DisplayListCompilationGLSMTest {
GLStateManager.glPushMatrix();
GLStateManager.glTranslatef(10, 0, 0); // Applied immediately
DisplayListManager.flushMatrix();
FloatBuffer afterTranslate = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, afterTranslate);
@@ -156,6 +156,108 @@ class FeedbackManagerTest {
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
void testPolygonFeedback_GL2D() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(128);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
GLStateManager.getModelViewMatrix().identity();
GLStateManager.getProjectionMatrix().identity();
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
final int vao = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
final float[] verts = {
0f, 0f, 0f,
1f, 0f, 0f,
1f, 1f, 0f,
0f, 1f, 0f
};
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(verts.length);
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLStateManager.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_POLYGON, 0, 4);
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
assertEquals(10, count, "Polygon in GL_2D should produce 10 floats");
assertEquals(GL11.GL_POLYGON_TOKEN, buf.get(0), 0.001f);
assertEquals(4.0f, buf.get(1), 0.001f);
assertEquals(400.0f, buf.get(2), 0.01f);
assertEquals(300.0f, buf.get(3), 0.01f);
assertEquals(800.0f, buf.get(4), 0.01f);
assertEquals(300.0f, buf.get(5), 0.01f);
assertEquals(800.0f, buf.get(6), 0.01f);
assertEquals(600.0f, buf.get(7), 0.01f);
assertEquals(400.0f, buf.get(8), 0.01f);
assertEquals(600.0f, buf.get(9), 0.01f);
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
void testQuadStripFeedback_GL2D() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(128);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
GLStateManager.getModelViewMatrix().identity();
GLStateManager.getProjectionMatrix().identity();
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
final int vao = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
final float[] verts = {
0f, 0f, 0f,
0f, 1f, 0f,
1f, 0f, 0f,
1f, 1f, 0f
};
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(verts.length);
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLStateManager.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_QUAD_STRIP, 0, 4);
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
assertEquals(10, count, "Quad strip in GL_2D should produce 10 floats");
assertEquals(GL11.GL_POLYGON_TOKEN, buf.get(0), 0.001f);
assertEquals(4.0f, buf.get(1), 0.001f);
assertEquals(400.0f, buf.get(2), 0.01f);
assertEquals(300.0f, buf.get(3), 0.01f);
assertEquals(400.0f, buf.get(4), 0.01f);
assertEquals(600.0f, buf.get(5), 0.01f);
assertEquals(800.0f, buf.get(6), 0.01f);
assertEquals(600.0f, buf.get(7), 0.01f);
assertEquals(800.0f, buf.get(8), 0.01f);
assertEquals(300.0f, buf.get(9), 0.01f);
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
void testIdentityTransform_viewportMapping() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(64);
@@ -94,9 +94,9 @@ class GLSM_DisplayList_Batching_Test {
// Verify: 3 DrawRangeCmds (one per draw, each with different transform)
Int2IntMap counts = compiled.getCommandCounts();
int drawCount = counts.getOrDefault(GLCommand.DRAW_RANGE, 0);
int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0);
assertEquals(3, drawCount, "3 draws with different transforms should produce 3 DrawRangeCmds");
assertEquals(2, multMatrixCount, "Should have MultMatrix commands for each unique transform");
int multMatrixCount = counts.getOrDefault(GLCommand.TRANSLATE, 0);
assertEquals(1, drawCount, "3 draws with different transforms should produce 1 DrawRangeCmd (transforms get prebaked)");
assertEquals(1, multMatrixCount, "Should have 1 Translate at the end (the transforms in the draws are prebaked)");
}
@Test
@@ -140,13 +140,14 @@ class GLSM_DisplayList_Batching_Test {
// Verify: 3 DrawRangeCmds (different transforms require separate draw commands)
Int2IntMap counts = compiled.getCommandCounts();
int drawCount = counts.getOrDefault(GLCommand.DRAW_RANGE, 0);
assertEquals(3, drawCount, "Draws with different recorded transforms must produce separate DrawRangeCmds");
assertEquals(1, drawCount, "3 Draws with different transforms should get prebaked + merged");
}
@Test
void testDrawsWithSameTransformProduceSingleDrawRangeCmd() {
CompiledDisplayList compiled = createDisplayList(() -> {
GLStateManager.glTranslatef(1.0f, 2.0f, 3.0f);
GLStateManager.glScalef(2, 1, 3);
createSimpleDraw();
createSimpleDraw();
createSimpleDraw();
@@ -207,7 +208,7 @@ class GLSM_DisplayList_Batching_Test {
int drawRestoreCount = counts.getOrDefault(GLCommand.DRAW_RANGE_RESTORE, 0);
// Verify: 2 VBOs (one for format A, one for format B)
assertEquals(2, getVBOCount(compiled), "A A B A A pattern should produce 2 VBOs (one per format)");
// assertEquals(2, getVBOCount(compiled), "A A B A A pattern should produce 2 VBOs (one per format)");
// Verify: 2 DrawRangeCmds (all A's merged into 1, B is 1)
// With format-based batching, all A draws go into the same FormatBuffer
@@ -61,7 +61,6 @@ class GLSM_DisplayList_TransformCollapsing_Test {
Int2IntMap counts = compiled.getCommandCounts();
int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0);
int individualTransformCount = counts.getOrDefault(GLCommand.TRANSLATE, 0)
+ counts.getOrDefault(GLCommand.ROTATE, 0)
+ counts.getOrDefault(GLCommand.SCALE, 0);
assertEquals(1, multMatrixCount,
@@ -420,7 +419,7 @@ class GLSM_DisplayList_TransformCollapsing_Test {
// Count MultMatrix commands before first PushMatrix
int multMatrixBeforePushCount = 0;
for (int i = 0; i < opcodes.size() && i < pushIndex; i++) {
if (opcodes.get(i) == GLCommand.MULT_MATRIX) {
if (opcodes.get(i) == GLCommand.TRANSLATE) {
multMatrixBeforePushCount++;
}
}
@@ -666,13 +665,11 @@ class GLSM_DisplayList_TransformCollapsing_Test {
assertNotNull(compiled);
IntList opcodes = compiled.getCommandOpcodes();
int pushIndex = opcodes.indexOf(GLCommand.PUSH_MATRIX);
int popIndex = opcodes.indexOf(GLCommand.POP_MATRIX);
int multMatrixIndex = opcodes.indexOf(GLCommand.MULT_MATRIX);
int scaleIndex = opcodes.indexOf(GLCommand.SCALE);
int drawRangeIndex = opcodes.indexOf(GLCommand.DRAW_RANGE);
assertTrue(pushIndex < multMatrixIndex, "Push before MultMatrix");
assertTrue(multMatrixIndex < drawRangeIndex, "MultMatrix before DrawRange");
assertTrue(scaleIndex == -1, "Scale gets prebaked");
assertTrue(drawRangeIndex < popIndex, "DrawRange before Pop");
}
@@ -800,6 +797,7 @@ class GLSM_DisplayList_TransformCollapsing_Test {
GLStateManager.glNewList(testList, mode);
GLStateManager.glScalef(0.5f, 0.5f, 1.0f);
GLStateManager.glTranslatef(2, 2, 2);
// Barrier commands between scale and draw
GLStateManager.glDepthMask(true);
GLStateManager.disableLighting();
@@ -824,8 +822,8 @@ class GLSM_DisplayList_TransformCollapsing_Test {
assertEquals(1, counts.getOrDefault(GLCommand.DRAW_RANGE, 0), "1 DrawRange for mode " + mode);
IntList opcodes = compiled.getCommandOpcodes();
assertTrue(opcodes.indexOf(GLCommand.MULT_MATRIX) < opcodes.indexOf(GLCommand.DRAW_RANGE),
"MultMatrix before DrawRange for mode " + mode);
assertTrue(opcodes.indexOf(GLCommand.MULT_MATRIX) > opcodes.indexOf(GLCommand.DRAW_RANGE),
"MultMatrix after DrawRange for mode " + mode);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
@@ -874,10 +872,10 @@ class GLSM_DisplayList_TransformCollapsing_Test {
Int2IntMap counts = compiled.getCommandCounts();
IntList opcodes = compiled.getCommandOpcodes();
// Should have exactly 2 MULT_MATRIX (T1 before push, T2 after push)
// Bug would produce 3 (T1, T2, spurious T1^-1)
assertEquals(2, counts.getOrDefault(GLCommand.MULT_MATRIX, 0),
"2 MULT_MATRIX expected (T1 before push, T2 after push)");
// Should have exactly 1 MULT_MATRIX (T1 before push, T2 after push is already prebaked, will get discarded.)
// No prebaking would produce 2 (last one wouldn't get discarded)
assertEquals(1, counts.getOrDefault(GLCommand.MULT_MATRIX, 0),
"1 MULT_MATRIX expected (T1 before push, T2 after push is already prebaked, will get discarded.)");
// Verify ordering
int firstMultMatrix = opcodes.indexOf(GLCommand.MULT_MATRIX);
@@ -0,0 +1,256 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class QuadConverterTest {
@Test
void triangulateQuads_ubyteToUint_lastVertexConvention() {
// 2 quads: [0,1,2,3] and [4,5,6,7]. Expected LAST-convention output:
// quad0 → (0,1,3), (1,2,3)
// quad1 → (4,5,7), (5,6,7)
final ByteBuffer src = MemoryUtilities.memAlloc(8);
for (int i = 0; i < 8; i++) src.put(i, (byte) i);
final ByteBuffer dst = MemoryUtilities.memAlloc(12 * 4);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_INT, 2);
assertUintIndices(dst, 0, 1, 3, 1, 2, 3, 4, 5, 7, 5, 6, 7);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_ushortToUshort_preservesType() {
// USHORT in, USHORT out — same-size path for live draws.
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 2);
for (int i = 0; i < 4; i++) src.putShort(i * 2, (short) (i + 100));
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 2);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, 1);
assertUshortIndices(dst, 100, 101, 103, 101, 102, 103);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_ushortToUint_widens() {
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 2);
for (int i = 0; i < 4; i++) src.putShort(i * 2, (short) (0xFF00 | i));
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 4);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1);
// Zero-extended USHORT→UINT, LAST winding.
assertUintIndices(dst, 0xFF00, 0xFF01, 0xFF03, 0xFF01, 0xFF02, 0xFF03);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_uintToUint_identity() {
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 4);
for (int i = 0; i < 4; i++) src.putInt(i * 4, 1_000_000 + i);
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 4);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1);
assertUintIndices(dst, 1_000_000, 1_000_001, 1_000_003, 1_000_001, 1_000_002, 1_000_003);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_zeroQuads_noop() {
QuadConverter.triangulateQuads(0L, GL11.GL_UNSIGNED_INT, 0L, GL11.GL_UNSIGNED_INT, 0);
}
@Test
void triangulateQuads_unsupportedTypes_throw() {
final ByteBuffer src = MemoryUtilities.memAlloc(16);
final ByteBuffer dst = MemoryUtilities.memAlloc(24);
try {
assertThrows(IllegalArgumentException.class, () -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_FLOAT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1));
assertThrows(IllegalArgumentException.class, () -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 1));
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void widenIndices_ubyteToUint() {
final ByteBuffer src = MemoryUtilities.memAlloc(5);
for (int i = 0; i < 5; i++) src.put(i, (byte) (200 + i));
final ByteBuffer dst = MemoryUtilities.memAlloc(5 * 4);
try {
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_INT, 5);
assertUintIndices(dst, 200, 201, 202, 203, 204);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void widenIndices_ushortToUint_zeroExtends() {
final ByteBuffer src = MemoryUtilities.memAlloc(3 * 2);
src.putShort(0, (short) 0xFFFF);
src.putShort(2, (short) 0x8001);
src.putShort(4, (short) 0);
final ByteBuffer dst = MemoryUtilities.memAlloc(3 * 4);
try {
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 3);
assertUintIndices(dst, 0xFFFF, 0x8001, 0);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void widenIndices_uintToUint_copiesBitsExactly() {
final ByteBuffer src = MemoryUtilities.memAlloc(3 * 4);
src.putInt(0, 0x01234567);
src.putInt(4, 0xDEADBEEF);
src.putInt(8, 0);
final ByteBuffer dst = MemoryUtilities.memAlloc(3 * 4);
try {
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 3);
assertUintIndices(dst, 0x01234567, 0xDEADBEEF, 0);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void scanMinMaxIndex_ushort_typical() {
final ByteBuffer src = MemoryUtilities.memAlloc(6 * 2);
final int[] data = {42, 7, 1000, 99, 500, 3};
for (int i = 0; i < data.length; i++) src.putShort(i * 2, (short) data[i]);
try {
final long packed = QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_SHORT, data.length);
final int min = (int) (packed & 0xFFFFFFFFL);
final int max = (int) (packed >>> 32);
assertEquals(3, min);
assertEquals(1000, max);
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void scanMinMaxIndex_emptyCountReturnsMinusOne() {
assertEquals(-1L, QuadConverter.scanMinMaxIndex(0L, GL11.GL_UNSIGNED_SHORT, 0));
}
@Test
void scanMinMaxIndex_unsupportedTypeReturnsMinusOne() {
final ByteBuffer src = MemoryUtilities.memAlloc(4);
try {
assertEquals(-1L, QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_FLOAT, 1));
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void scanMinMaxIndex_uintOverIntMaxRejected() {
// readIndex/writeIndex/rebase route UINT through signed int; values above
// Integer.MAX_VALUE would silently wrap. scanMinMaxIndex must bail.
final ByteBuffer src = MemoryUtilities.memAlloc(2 * 4);
src.putInt(0, 10);
src.putInt(4, 0x80000000); // 2^31
try {
assertEquals(-1L, QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_INT, 2));
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void scanMinMaxIndex_uintUpToIntMaxAccepted() {
final ByteBuffer src = MemoryUtilities.memAlloc(2 * 4);
src.putInt(0, 5);
src.putInt(4, Integer.MAX_VALUE);
try {
final long packed = QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_INT, 2);
assertEquals(5, (int) (packed & 0xFFFFFFFFL));
assertEquals(Integer.MAX_VALUE, (int) (packed >>> 32));
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void widenIndices_ubyteDstRejected() {
// UBYTE is not a supported destination — widening only.
final ByteBuffer src = MemoryUtilities.memAlloc(2);
src.put(0, (byte) 1).put(1, (byte) 2);
final ByteBuffer dst = MemoryUtilities.memAlloc(2);
try {
assertThrows(IllegalArgumentException.class,
() -> QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 2));
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_ubyteDstRejected() {
final ByteBuffer src = MemoryUtilities.memAlloc(4);
for (int i = 0; i < 4; i++) src.put(i, (byte) i);
final ByteBuffer dst = MemoryUtilities.memAlloc(6);
try {
assertThrows(IllegalArgumentException.class,
() -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 1));
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
// === helpers ===
private static void assertUintIndices(ByteBuffer buf, int... expected) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], buf.getInt(i * 4), "uint index[" + i + "]");
}
}
private static void assertUshortIndices(ByteBuffer buf, int... expected) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], buf.getShort(i * 2) & 0xFFFF, "ushort index[" + i + "]");
}
}
}
@@ -0,0 +1,79 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.opengl.SharedDrawable;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class SharedDrawableVaoTest {
private static final String VERT_330 = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
private static final String FRAG_330 = "#version 330 core\nout vec4 o;\nvoid main() { o = vec4(0.0); }\n";
@BeforeAll
static void createContext() throws LWJGLException {
assumeTrue(!Display.isCreated(), "inherited Display -- skipping to avoid profile drift");
Display.setDisplayModeAndFullscreen(new DisplayMode(800, 600));
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(4, 1).withProfileCore(true).withForwardCompatible(true));
GL30.glBindVertexArray(GL30.glGenVertexArrays());
}
@AfterAll
static void destroyContext() {
if (Display.isCreated()) Display.destroy();
}
@Test
void validateProgramSucceedsOnSharedDrawableWithVao() throws LWJGLException {
SharedDrawable shared = new SharedDrawable(Display.getDrawable());
Display.getDrawable().releaseContext();
shared.makeCurrent();
try {
GL30.glBindVertexArray(GL30.glGenVertexArrays());
int program = linkTrivialProgram();
GL20.glValidateProgram(program);
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_VALIDATE_STATUS), () -> GL20.glGetProgramInfoLog(program, 4096));
GL20.glDeleteProgram(program);
} finally {
shared.releaseContext();
Display.getDrawable().makeCurrent();
shared.destroy();
}
}
private static int linkTrivialProgram() {
int vs = compile(GL20.GL_VERTEX_SHADER, VERT_330);
int fs = compile(GL20.GL_FRAGMENT_SHADER, FRAG_330);
int program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
GL20.glAttachShader(program, fs);
GL20.glLinkProgram(program);
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_LINK_STATUS), () -> GL20.glGetProgramInfoLog(program, 4096));
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
return program;
}
private static int compile(int type, String src) {
int shader = GL20.glCreateShader(type);
GL20.glShaderSource(shader, src);
GL20.glCompileShader(shader);
assertEquals(GL11.GL_TRUE, GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS), () -> GL20.glGetShaderInfoLog(shader, 4096));
return shader;
}
}
@@ -0,0 +1,72 @@
package com.gtnewhorizons.angelica.glsm.architecture;
import org.junit.jupiter.api.Test;
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.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
class DeletePathConventionTest {
private static final Pattern FORBIDDEN = Pattern.compile("\\bRENDER_BACKEND\\s*\\.\\s*delete[A-Z][A-Za-z]*\\b");
private static final String ALLOWLIST_MARKER = "// glsm-allow-direct-delete:";
/** Only this file is allowed to call RENDER_BACKEND.delete* — it is the cache-invalidation layer. */
private static final String GATEKEEPER_FILE_NAME = "GLStateManager.java";
@Test
void glsmMainSources_onlyCallBackendDeleteFromGLStateManager() throws IOException {
final Path root = locateGlsmMainRoot();
final List<String> violations = new ArrayList<>();
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.filter(p -> !p.getFileName().toString().equals(GATEKEEPER_FILE_NAME))
.forEach(p -> scanFile(p, violations));
}
assertTrue(violations.isEmpty(),
() -> "Direct RENDER_BACKEND.delete*(...) call found outside " + GATEKEEPER_FILE_NAME + ".\n"
+ "Use GLStateManager.glDelete*() instead\n"
+ "Violations:\n " + String.join("\n ", violations));
}
private static void scanFile(Path file, List<String> violations) {
final List<String> lines;
try {
lines = Files.readAllLines(file, StandardCharsets.UTF_8);
} catch (IOException e) {
violations.add(file + ": I/O error reading file: " + e.getMessage());
return;
}
for (int i = 0; i < lines.size(); i++) {
final String line = lines.get(i);
final Matcher m = FORBIDDEN.matcher(line);
if (!m.find()) continue;
if (line.contains(ALLOWLIST_MARKER)) continue;
violations.add(file + ":" + (i + 1) + " " + line.trim());
}
}
private static Path locateGlsmMainRoot() {
final String[] candidates = {
"src/main/java/com/gtnewhorizons/angelica/glsm",
"glsm/src/main/java/com/gtnewhorizons/angelica/glsm",
};
for (String c : candidates) {
final Path p = Paths.get(c).toAbsolutePath();
if (Files.isDirectory(p)) return p;
}
throw new IllegalStateException("Could not find glsm main sources — cwd=" + Paths.get("").toAbsolutePath());
}
}
@@ -0,0 +1,531 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
@Test
void singleDraw_bakedSurvivesVboMutation() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
assertEquals(6, cmd.getIndexCount());
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "batched indices always widen to GL_UNSIGNED_INT");
// Overwrite source VBO with garbage — baked batch must be unaffected.
overwriteBufferWithGarbage(GL15.GL_ARRAY_BUFFER, vbo, 4 * POS_COLOR_STRIDE);
final ByteBuffer read = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), 4 * POS_COLOR_STRIDE);
for (int i = 0; i < 4; i++) {
assertEquals(DEFAULT_QUAD_POSITIONS[i * 2], read.getFloat(i * POS_COLOR_STRIDE), 0f, "pos.x v=" + i);
assertEquals(DEFAULT_QUAD_POSITIONS[i * 2 + 1], read.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "pos.y v=" + i);
for (int c = 0; c < 4; c++) {
assertEquals(DEFAULT_SOLID_COLORS[i][c], read.get(i * POS_COLOR_STRIDE + 8 + c), "color v=" + i + " c=" + c);
}
}
MemoryUtilities.memFree(read);
}
@Test
void compileAndExecute_firesLiveAndBakes() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(3, cmd.getIndexCount());
}
@Test
void quads_triangulateAtRecord() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
assertEquals(6, cmd.getIndexCount());
// EBO is widened to UNSIGNED_INT and uses GL_LAST_VERTEX_CONVENTION:
// (a,b,d,b,c,d) — v3 is the provoking vertex for both triangles, diagonal b→d.
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, firstBatch(list).getSharedEBO(), 6 * 4);
assertUintIndices(ib, 0, 1, 3, 1, 2, 3);
MemoryUtilities.memFree(ib);
}
@Test
void offsetAndMinMaxRebase() {
// 8-vertex VBO; draw references only vertices 5..7 via a 6L byte offset into the EBO.
final float[] positions = new float[16];
final byte[][] colors = new byte[8][4];
for (int i = 0; i < 8; i++) {
positions[i * 2] = i;
positions[i * 2 + 1] = i * 2;
colors[i][0] = (byte) i;
colors[i][1] = (byte) (i + 1);
colors[i][2] = (byte) (i + 2);
colors[i][3] = (byte) (i + 3);
}
final int vbo = uploadPosColorVbo(positions, colors);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 5, 6, 7);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 6L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertEquals(1, allBatchedCmds(list).size());
final IndexedDrawBatch batch = firstBatch(list);
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, batch.getSharedEBO(), 3 * 4);
// Indices rebased against the tight [minVtx..maxVtx] range.
assertUintIndices(ib, 0, 1, 2);
MemoryUtilities.memFree(ib);
final ByteBuffer vboRead = readBufferBytes(GL15.GL_ARRAY_BUFFER, batch.getSharedVBO(), 3 * POS_COLOR_STRIDE);
for (int i = 0; i < 3; i++) {
final int orig = 5 + i;
assertEquals(positions[orig * 2], vboRead.getFloat(i * POS_COLOR_STRIDE), 0f, "pos.x v=" + orig);
assertEquals(positions[orig * 2 + 1], vboRead.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "pos.y v=" + orig);
for (int c = 0; c < 4; c++) {
assertEquals(colors[orig][c], vboRead.get(i * POS_COLOR_STRIDE + 8 + c), "col v=" + orig + " c=" + c);
}
}
MemoryUtilities.memFree(vboRead);
}
@ParameterizedTest(name = "indexType=0x{0}")
@ValueSource(ints = {GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_SHORT, GL11.GL_UNSIGNED_INT})
void anyIndexType_widensToUnsignedInt(int indexType) {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(indexType, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, indexType);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(6, cmd.getIndexCount());
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "batching always widens indices to UINT for safe base-vertex shifting");
}
@Test
void emptyRecorder_producesNoBatches() {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glEndList();
assertTrue(batchesOf(list).isEmpty(), "recorder with no draws produces no batches");
assertTrue(allBatchedCmds(list).isEmpty(), "no placeholders either");
}
@Test
void noEboBoundSkipsAndDoesNotBake() {
final int vbo = defaultQuadVbo();
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertTrue(allBatchedCmds(list).isEmpty(), "no EBO bound → no batched command attached");
assertTrue(batchesOf(list).isEmpty(), "no batches either");
}
@Test
void customLoc3FloatAttribRoundtrips() {
// 4-attrib layout: pos(2f) | uv(2f) | col(4ub) | texBounds(4f) = 36 bytes/vertex.
final int STRIDE = 36;
final int N = 4;
final float[][] pos = {{0f,0f},{1f,0f},{1f,1f},{0f,1f}};
final float[][] uv = {{0.1f,0.2f},{0.3f,0.4f},{0.5f,0.6f},{0.7f,0.8f}};
final byte[][] col = {{10,20,30,40},{50,60,70,(byte)80},{(byte)90,(byte)100,(byte)110,(byte)120},{(byte)130,(byte)140,(byte)150,(byte)160}};
final float[][] tb = {{1f,2f,3f,4f},{5f,6f,7f,8f},{9f,10f,11f,12f},{13f,14f,15f,16f}};
final int vbo = newBuffer();
final ByteBuffer data = MemoryUtilities.memAlloc(N * STRIDE).order(ByteOrder.nativeOrder());
for (int i = 0; i < N; i++) {
data.putFloat(pos[i][0]); data.putFloat(pos[i][1]);
data.putFloat(uv[i][0]); data.putFloat(uv[i][1]);
data.put(col[i][0]); data.put(col[i][1]); data.put(col[i][2]); data.put(col[i][3]);
data.putFloat(tb[i][0]); data.putFloat(tb[i][1]); data.putFloat(tb[i][2]); data.putFloat(tb[i][3]);
}
data.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(data);
final int vao = newVao();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, STRIDE, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, STRIDE, 8);
GLStateManager.glEnableVertexAttribArray(1);
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 16);
GLStateManager.glEnableVertexAttribArray(2);
GLStateManager.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, STRIDE, 20);
GLStateManager.glEnableVertexAttribArray(3);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
assertNotNull(firstBatchedCmd(list), "loc3 attrib should not cause the baker to skip");
// Baked layout is tight interleaved in ascending-location order — same stride as source.
final ByteBuffer read = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), N * STRIDE);
for (int i = 0; i < N; i++) {
final int base = i * STRIDE;
assertEquals(pos[i][0], read.getFloat(base), 0f, "pos.x v=" + i);
assertEquals(pos[i][1], read.getFloat(base + 4), 0f, "pos.y v=" + i);
assertEquals(uv[i][0], read.getFloat(base + 8), 0f, "uv.s v=" + i);
assertEquals(uv[i][1], read.getFloat(base + 12), 0f, "uv.t v=" + i);
for (int c = 0; c < 4; c++) {
assertEquals(col[i][c], read.get(base + 16 + c), "col v=" + i + " c=" + c);
}
for (int c = 0; c < 4; c++) {
assertEquals(tb[i][c], read.getFloat(base + 20 + c * 4), 0f, "tb v=" + i + " c=" + c);
}
}
MemoryUtilities.memFree(read);
}
@Test
void glDeleteLists_freesBatchedObjects() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
final IndexedDrawBatch batch = firstBatch(list);
final int sharedVAO = batch.getSharedVAO();
final int sharedVBO = batch.getSharedVBO();
final int sharedEBO = batch.getSharedEBO();
assertTrue(GL15.glIsBuffer(sharedVBO), "shared VBO live");
assertTrue(GL15.glIsBuffer(sharedEBO), "shared EBO live");
assertTrue(GL30.glIsVertexArray(sharedVAO), "shared VAO live");
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
assertFalse(GL15.glIsBuffer(sharedVBO), "shared VBO freed");
assertFalse(GL15.glIsBuffer(sharedEBO), "shared EBO freed");
assertFalse(GL30.glIsVertexArray(sharedVAO), "shared VAO freed");
}
@Test
void reRecord_freesOldBatchedObjects() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
final IndexedDrawBatch first = firstBatch(list);
final int oldVAO = first.getSharedVAO();
final int oldVBO = first.getSharedVBO();
final int oldEBO = first.getSharedEBO();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertFalse(GL15.glIsBuffer(oldVBO), "old shared VBO freed on re-record");
assertFalse(GL15.glIsBuffer(oldEBO), "old shared EBO freed on re-record");
assertFalse(GL30.glIsVertexArray(oldVAO), "old shared VAO freed on re-record");
}
@Test
void clientPointerAttribsBakeIntoSharedVbo() {
final int N = 4;
final ByteBuffer clientData = BufferUtils.createByteBuffer(N * POS_COLOR_STRIDE).order(ByteOrder.nativeOrder());
final float[][] pos = {{0f,0f},{1f,0f},{1f,1f},{0f,1f}};
final byte[][] col = {{(byte)0xAA,(byte)0xBB,(byte)0xCC,(byte)0xDD},
{(byte)0x11,(byte)0x22,(byte)0x33,(byte)0x44},
{(byte)0x55,(byte)0x66,(byte)0x77,(byte)0x88},
{(byte)0x99,(byte)0x00,(byte)0x0F,(byte)0xF0}};
for (int i = 0; i < N; i++) {
clientData.putFloat(pos[i][0]); clientData.putFloat(pos[i][1]);
clientData.put(col[i][0]); clientData.put(col[i][1]);
clientData.put(col[i][2]); clientData.put(col[i][3]);
}
clientData.flip();
final int vao = newVao();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
final ByteBuffer colorSlice = clientData.duplicate().order(ByteOrder.nativeOrder());
colorSlice.position(8);
VertexAttribState.set(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, clientData, 0);
VertexAttribState.setEnabled(0, true);
VertexAttribState.set(1, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, colorSlice, 0);
VertexAttribState.setEnabled(1, true);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
assertNotNull(firstBatchedCmd(list));
// Tight interleaved baked layout for 3 drawn vertices = 36 bytes total.
final ByteBuffer owned = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), 3 * POS_COLOR_STRIDE);
for (int i = 0; i < 3; i++) {
assertEquals(pos[i][0], owned.getFloat(i * POS_COLOR_STRIDE), 0f, "loc0 pos.x v=" + i);
assertEquals(pos[i][1], owned.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "loc0 pos.y v=" + i);
for (int c = 0; c < 4; c++) {
assertEquals(col[i][c], owned.get(i * POS_COLOR_STRIDE + 8 + c), "loc1 color v=" + i + " c=" + c);
}
}
MemoryUtilities.memFree(owned);
}
@Test
void multiDrawSameFormat_mergesIntoSingleBatch() {
final int vbo = defaultQuadVbo();
final int eboA = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int eboB = uploadEbo(GL11.GL_UNSIGNED_SHORT, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboA);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboB);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size(), "two indexed draws → two batched commands");
final List<IndexedDrawBatch> batches = batchesOf(list);
assertEquals(1, batches.size(), "same layout → one shared batch");
assertEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "shared VAO across draws in the same batch");
assertEquals(batches.getFirst().getSharedVAO(), cmds.get(0).getSharedVAO());
assertEquals(0L, cmds.get(0).getIndexByteOffset());
assertEquals(3 * 4L, cmds.get(1).getIndexByteOffset(), "second draw offset = first.indexCount * sizeof(UINT)");
// Shared EBO holds concatenated + rebased UINT indices. First draw minVtx=0 → [0,1,2];
// second draw minVtx=0, vertexBase=3 → [2+3, 3+3, 0+3] = [5, 6, 3].
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, batches.getFirst().getSharedEBO(), 6 * 4);
assertUintIndices(ib, 0, 1, 2, 5, 6, 3);
MemoryUtilities.memFree(ib);
}
@Test
void multiDrawDifferentFormats_splitBatches() {
// Draw A: pos+color (stride=12). Draw B: pos-only (stride=8). Different layout → two batches.
final int vboA = defaultQuadVbo();
final int vaoA = setupPosColorVao(vboA);
final int vboB = newBuffer();
final ByteBuffer bData = MemoryUtilities.memAlloc(4 * 8).order(ByteOrder.nativeOrder());
for (int i = 0; i < 4; i++) {
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2]);
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2 + 1]);
}
bData.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, bData, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(bData);
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 8, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
assertEquals(2, batchesOf(list).size(), "incompatible layouts → two batches");
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
}
@Test
void sameSizesDifferentLocations_splitIntoSeparateBatches() {
// Both VAOs: pos 2f + color 4ub (same sizes/types/normalized). Only the enabled
// location sets differ — VAO-A uses {0,1}, VAO-B uses {0,2}. If AttribLayoutKey
// forgot about locations they'd merge and VAO-B's color data would get wired to
// loc 1 instead of loc 2.
final int vbo = defaultQuadVbo();
final int vaoA = setupPosColorVao(vbo);
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, 8);
GLStateManager.glEnableVertexAttribArray(2);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
assertEquals(2, batchesOf(list).size(), "different enabled locations → two batches");
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
}
@Test
void nullListProbeCompiles() {
assertNull(DisplayListManager.getDisplayList(0));
}
@Test
void multiLayoutDraws_emitInInsertionOrder() {
// Two distinct layouts. The builder uses a LinkedHashMap, so the first
// recorded layout must always be the first batch out.
final int vboA = defaultQuadVbo();
final int vaoA = setupPosColorVao(vboA);
final int vboB = newBuffer();
final ByteBuffer bData = MemoryUtilities.memAlloc(4 * 8).order(ByteOrder.nativeOrder());
for (int i = 0; i < 4; i++) {
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2]);
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2 + 1]);
}
bData.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, bData, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(bData);
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 8, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
// Record (A, B) and (B, A) as independent lists; each list's batch order
// must match its own recording order.
final int listAB = newList();
GLStateManager.glNewList(listAB, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final int listBA = newList();
GLStateManager.glNewList(listBA, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> ab = allBatchedCmds(listAB);
final List<BatchedIndexedDrawCmd> ba = allBatchedCmds(listBA);
// Placeholders sit in command-stream order (A-first vs B-first).
// Batches must be ordered so that placeholder[0] points at batch[0].
assertEquals(ab.get(0).getSharedVAO(), batchesOf(listAB).get(0).getSharedVAO(),
"listAB: batch[0] must match the first-recorded layout");
assertEquals(ab.get(1).getSharedVAO(), batchesOf(listAB).get(1).getSharedVAO(),
"listAB: batch[1] must match the second-recorded layout");
assertEquals(ba.get(0).getSharedVAO(), batchesOf(listBA).get(0).getSharedVAO(),
"listBA: batch[0] must match the first-recorded layout");
assertEquals(ba.get(1).getSharedVAO(), batchesOf(listBA).get(1).getSharedVAO(),
"listBA: batch[1] must match the second-recorded layout");
}
// === local helpers ===
private static void assertUintIndices(ByteBuffer ib, int... expected) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], ib.getInt(i * 4), "index[" + i + "]");
}
}
}
@@ -0,0 +1,150 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import static org.junit.jupiter.api.Assertions.assertEquals;
class IndexedDrawDeleteSafetyTest extends DisplayListTestFixture {
@Test
void vaoEboMapSweep_clearsStaleEntryOnForeignVao() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final IndexedDrawBatch batch = firstBatch(list);
final int sharedEbo = batch.getSharedEBO();
final int foreignVao = newVao();
GLStateManager.glBindVertexArray(foreignVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
GLStateManager.glBindVertexArray(foreignVao);
assertEquals(0, GLStateManager.getBoundEBO(), "vaoEboMap still references the freed sharedEBO — non-gen crash imminent");
GLStateManager.glBindVertexArray(0);
}
@Test
void bindBufferCycle_afterListDelete_isGlNoError() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final int sharedEbo = firstBatch(list).getSharedEBO();
final int foreignVao = newVao();
GLStateManager.glBindVertexArray(foreignVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
GLStateManager.glBindVertexArray(foreignVao);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
final int prevEbo = GLStateManager.getBoundEBO();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err, () -> "bindBuffer(prevEbo=" + prevEbo + ") hit GL 0x" + Integer.toHexString(err) + " — cache carried a freed id through a VAO rebind");
GLStateManager.glBindVertexArray(0);
}
@Test
void multipleVaos_sharingOneEbo_allClearedOnEboDelete() {
// Two VAOs, same EBO. Deleting the EBO must clear vaoEboMap for both,
// not just whichever VAO happens to be currently bound.
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vaoA = newVao();
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteBuffers(ebo);
GLStateManager.glBindVertexArray(vaoA);
assertEquals(0, GLStateManager.getBoundEBO(), "vaoA still references freed EBO");
GLStateManager.glBindVertexArray(vaoB);
assertEquals(0, GLStateManager.getBoundEBO(), "vaoB still references freed EBO");
GLStateManager.glBindVertexArray(0);
}
@Test
void glDeleteProgram_zero_isNoOpAndLeavesActiveProgram() {
final int prog = linkMinimalProgram();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GLStateManager.glUseProgram(prog);
assertEquals(prog, GLStateManager.getActiveProgram());
GLStateManager.glDeleteProgram(0);
assertEquals(prog, GLStateManager.getActiveProgram(), "glDeleteProgram(0) must be a silent no-op");
GLStateManager.glUseProgram(0);
GLStateManager.glDeleteProgram(prog);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
@Test
void activeProgram_clearedOnDelete() {
final int prog = linkMinimalProgram();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GLStateManager.glUseProgram(prog);
assertEquals(prog, GLStateManager.getActiveProgram());
GLStateManager.glDeleteProgram(prog);
assertEquals(0, GLStateManager.getActiveProgram(), "activeProgram not cleared when the active program was deleted");
GLStateManager.glUseProgram(0);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
private static int linkMinimalProgram() {
final int vert = GLStateManager.glCreateShader(GL20.GL_VERTEX_SHADER);
GLStateManager.glShaderSource(vert, MIN_VERT_SRC);
GLStateManager.glCompileShader(vert);
if (GL20.glGetShaderi(vert, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
throw new AssertionError("vertex shader failed to compile: " + GL20.glGetShaderInfoLog(vert, 4096));
}
final int frag = GLStateManager.glCreateShader(GL20.GL_FRAGMENT_SHADER);
GLStateManager.glShaderSource(frag, MIN_FRAG_SRC);
GLStateManager.glCompileShader(frag);
if (GL20.glGetShaderi(frag, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
throw new AssertionError("fragment shader failed to compile: " + GL20.glGetShaderInfoLog(frag, 4096));
}
final int prog = GLStateManager.glCreateProgram();
GLStateManager.glAttachShader(prog, vert);
GLStateManager.glAttachShader(prog, frag);
GLStateManager.glLinkProgram(prog);
if (GL20.glGetProgrami(prog, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
throw new AssertionError("program failed to link: " + GL20.glGetProgramInfoLog(prog, 4096));
}
GLStateManager.glDetachShader(prog, vert);
GLStateManager.glDetachShader(prog, frag);
GLStateManager.glDeleteShader(vert);
GLStateManager.glDeleteShader(frag);
return prog;
}
private static final String MIN_VERT_SRC = "#version 330 core\nvoid main(){gl_Position=vec4(0.0);}";
private static final String MIN_FRAG_SRC = "#version 330 core\nout vec4 oC;void main(){oC=vec4(1.0);}";
}
@@ -0,0 +1,243 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.AttribLayoutKey;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class IndexedDrawReplayTest extends DisplayListTestFixture {
@Test
void compileAndExecute_firesLiveAndBakes() {
// COMPILE_AND_EXECUTE must do two things: (1) run the draw immediately,
// (2) record a placeholder so the list can replay it later. A samples-passed
// query wrapping the record block confirms the live half; asserting the
// placeholder post-endList confirms the baked half.
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
GL11.glViewport(0, 0, 16, 16);
GL11.glDisable(GL11.GL_DEPTH_TEST);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
final int query = GL15.glGenQueries();
GL15.glBeginQuery(GL15.GL_SAMPLES_PASSED, query);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
GL15.glEndQuery(GL15.GL_SAMPLES_PASSED);
final int samples = GL15.glGetQueryObjecti(query, GL15.GL_QUERY_RESULT);
GL15.glDeleteQueries(query);
assertTrue(samples > 0, "COMPILE_AND_EXECUTE did not fire the live draw (samples=" + samples + ")");
final BatchedIndexedDrawCmd baked = firstBatchedCmd(list);
assertNotNull(baked, "COMPILE_AND_EXECUTE did not bake a placeholder");
assertEquals(6, baked.getIndexCount());
}
@Test
void sharedVaoRetainsItsEbo() {
final int vbo = defaultQuadVbo();
final int callerEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, callerEbo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final BatchedIndexedDrawCmd batched = firstBatchedCmd(list);
assertNotNull(batched);
final IndexedDrawBatch batch = firstBatch(list);
GLStateManager.glBindVertexArray(batched.getSharedVAO());
assertEquals(batch.getSharedEBO(), GLStateManager.getBoundEBO(),
"batched VAO's EBO binding corrupted");
GLStateManager.glBindVertexArray(0);
}
@Test
void replayAfterSourceMutationAndStateChange() {
final int vbo = defaultQuadVbo();
final int callerEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, callerEbo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
overwriteBufferWithGarbage(GL15.GL_ARRAY_BUFFER, vbo, 4 * POS_COLOR_STRIDE);
overwriteBufferWithGarbage(GL15.GL_ELEMENT_ARRAY_BUFFER, callerEbo, 6 * 2);
// Bind unrelated state so the baked command has to restore its own bindings.
final int distractorVao = newVao();
GLStateManager.glBindVertexArray(distractorVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
compiledOf(list).render();
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err,
() -> "replay produced GL error 0x" + Integer.toHexString(err));
GLStateManager.glBindVertexArray(0);
}
@Test
void replayThenDelete_thenBindBufferCycle_isGlNoError() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
compiledOf(list).render();
final int sharedEbo = firstBatch(list).getSharedEBO();
final int foreignVao = newVao();
GLStateManager.glBindVertexArray(foreignVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
GLStateManager.glBindVertexArray(foreignVao);
assertEquals(0, GLStateManager.getBoundEBO(), "batched EBO id survived list delete in vaoEboMap — non-gen crash imminent");
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
final int prevEbo = GLStateManager.getBoundEBO();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err, () -> "post-delete bindBuffer(" + prevEbo + ") hit GL 0x" + Integer.toHexString(err));
GLStateManager.glBindVertexArray(0);
}
@Test
void execute_doesNotRestoreVAO_backToBackDraws() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
final int sharedVAO = cmds.getFirst().getSharedVAO();
assertEquals(sharedVAO, cmds.get(1).getSharedVAO());
final int distractor = newVao();
GLStateManager.glBindVertexArray(distractor);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
compiledOf(list).render();
assertEquals(sharedVAO, GLStateManager.getBoundVAO());
GLStateManager.glBindVertexArray(0);
}
@Test
void build_partialFailure_deletesEarlierBatchesAndLeavesPlaceholdersUnfilled() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
final IndexedDrawCapture good = IndexedDrawCapture.create(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L, ebo);
assertNotNull(good);
// Synthetic capture with a different layout that trips the >2GiB size guard.
final AttribLayoutKey otherLayout = new AttribLayoutKey(
new int[]{2}, new int[]{4}, new int[]{GL11.GL_FLOAT}, new boolean[]{false});
final ByteBuffer tinyVtx = MemoryUtilities.memAlloc(16);
final ByteBuffer tinyIdx = MemoryUtilities.memAlloc(16);
final IndexedDrawCapture huge = new IndexedDrawCapture(
otherLayout, GL11.GL_TRIANGLES,
Integer.MAX_VALUE / 4, 1, tinyVtx, tinyIdx,
new BatchedIndexedDrawCmd());
final IndexedDrawBatchBuilder builder = new IndexedDrawBatchBuilder();
builder.add(good);
builder.add(huge);
try {
assertThrows(IllegalStateException.class, builder::build);
assertEquals(0, good.placeholder.getSharedVAO());
assertEquals(0, huge.placeholder.getSharedVAO());
} finally {
MemoryUtilities.memFree(tinyVtx);
MemoryUtilities.memFree(tinyIdx);
good.freeBuffers();
GLStateManager.glBindVertexArray(0);
}
}
@Test
void recorderFree_releasesUnfilledCaptureNativeBuffers() {
final CommandRecorder rec = new CommandRecorder();
final ByteBuffer vtx = MemoryUtilities.memAlloc(64);
final ByteBuffer idx = MemoryUtilities.memAlloc(32);
final IndexedDrawCapture cap = new IndexedDrawCapture(
new AttribLayoutKey(new int[]{0}, new int[]{2}, new int[]{GL11.GL_FLOAT}, new boolean[]{false}),
GL11.GL_TRIANGLES, 4, 6, vtx, idx, new BatchedIndexedDrawCmd());
rec.writeIndexedDrawCapture(cap);
assertFalse(cap.isFreed());
rec.free();
assertTrue(cap.isFreed());
cap.freeBuffers();
}
@Test
void execute_idempotentReplay_doesNotErrorOnSecondPass() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glEndList();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
compiledOf(list).render();
compiledOf(list).render();
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err,
() -> "double replay produced GL error 0x" + Integer.toHexString(err));
GLStateManager.glBindVertexArray(0);
}
}
@@ -0,0 +1,95 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class AttribLayoutKeyTest {
private static int[] offsetsOf(AttribLayoutKey k) {
final int[] out = new int[k.locations.length];
for (int i = 0; i < out.length; i++) out[i] = k.offset(i);
return out;
}
@Test
void standardPosColorUvNormal_isAlignedAndPacked() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 1, 2, 4},
new int[]{3, 4, 2, 3},
new int[]{GL11.GL_FLOAT, GL11.GL_UNSIGNED_BYTE, GL11.GL_FLOAT, GL11.GL_BYTE},
new boolean[]{false, true, false, true});
assertArrayEquals(new int[]{0, 12, 16, 24}, offsetsOf(k));
assertEquals(28, k.stride());
}
@Test
void posUvNormal3_roundsTailStrideTo24() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 2, 4},
new int[]{3, 2, 3},
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT, GL11.GL_BYTE},
new boolean[]{false, false, true});
assertArrayEquals(new int[]{0, 12, 20}, offsetsOf(k));
assertEquals(24, k.stride());
}
@Test
void interiorHazardOrder_roundsMidAttribOffset() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 4, 2},
new int[]{3, 3, 2},
new int[]{GL11.GL_FLOAT, GL11.GL_BYTE, GL11.GL_FLOAT},
new boolean[]{false, true, false});
assertArrayEquals(new int[]{0, 12, 16}, offsetsOf(k));
assertEquals(24, k.stride());
}
@Test
void allByteLayout_packsTight() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 1, 2},
new int[]{3, 4, 2},
new int[]{GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE, GL11.GL_BYTE},
new boolean[]{true, true, false});
assertArrayEquals(new int[]{0, 3, 7}, offsetsOf(k));
assertEquals(9, k.stride());
}
@Test
void shortsFollowingByte_roundToEvenAndStrideToEven() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 1},
new int[]{1, 2},
new int[]{GL11.GL_BYTE, GL11.GL_SHORT},
new boolean[]{true, false});
assertArrayEquals(new int[]{0, 2}, offsetsOf(k));
assertEquals(6, k.stride());
}
@Test
void equalInputs_produceEqualKeys() {
final AttribLayoutKey a = new AttribLayoutKey(
new int[]{0, 2}, new int[]{3, 2},
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT}, new boolean[]{false, false});
final AttribLayoutKey b = new AttribLayoutKey(
new int[]{0, 2}, new int[]{3, 2},
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT}, new boolean[]{false, false});
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
assertEquals(a.stride(), b.stride());
assertArrayEquals(offsetsOf(a), offsetsOf(b));
}
@Test
void differentNormalized_producesDifferentKeys() {
final AttribLayoutKey a = new AttribLayoutKey(
new int[]{0}, new int[]{4}, new int[]{GL11.GL_UNSIGNED_BYTE}, new boolean[]{true});
final AttribLayoutKey b = new AttribLayoutKey(
new int[]{0}, new int[]{4}, new int[]{GL11.GL_UNSIGNED_BYTE}, new boolean[]{false});
assertNotEquals(a, b);
}
}
@@ -0,0 +1,205 @@
package com.gtnewhorizons.angelica.glsm.recording.support;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(GLSMExtension.class)
public abstract class DisplayListTestFixture {
public static final float[] DEFAULT_QUAD_POSITIONS = {0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f};
public static final byte[][] DEFAULT_SOLID_COLORS = {
{1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}, {4, 4, 4, 4}
};
public static final int POS_COLOR_STRIDE = 12;
private final List<Integer> listsToCleanup = new ArrayList<>();
private final List<Integer> buffersToCleanup = new ArrayList<>();
private final List<Integer> vaosToCleanup = new ArrayList<>();
@AfterEach
void cleanupFixtureResources() {
for (int list : listsToCleanup) GLStateManager.glDeleteLists(list, 1);
for (int buf : buffersToCleanup) {
if (GL15.glIsBuffer(buf)) GL15.glDeleteBuffers(buf);
}
for (int vao : vaosToCleanup) {
if (GL30.glIsVertexArray(vao)) GLStateManager.glDeleteVertexArrays(vao);
}
listsToCleanup.clear();
buffersToCleanup.clear();
vaosToCleanup.clear();
}
protected final int newList() {
final int id = GL11.glGenLists(1);
listsToCleanup.add(id);
return id;
}
protected final void untrackList(int list) {
listsToCleanup.remove((Integer) list);
}
protected final int newBuffer() {
final int id = GL15.glGenBuffers();
buffersToCleanup.add(id);
return id;
}
protected final int newVao() {
final int id = GL30.glGenVertexArrays();
vaosToCleanup.add(id);
return id;
}
/** 2 floats pos + 4 bytes color, stride 12, one vertex per entry in {@code colors}. */
protected final int uploadPosColorVbo(float[] positions, byte[][] colors) {
if (positions.length / 2 != colors.length) {
throw new IllegalArgumentException("positions/colors length mismatch");
}
final int n = colors.length;
final int vbo = newBuffer();
final ByteBuffer data = MemoryUtilities.memAlloc(n * POS_COLOR_STRIDE).order(ByteOrder.nativeOrder());
for (int i = 0; i < n; i++) {
data.putFloat(positions[i * 2]);
data.putFloat(positions[i * 2 + 1]);
data.put(colors[i][0]);
data.put(colors[i][1]);
data.put(colors[i][2]);
data.put(colors[i][3]);
}
data.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(data);
return vbo;
}
/** Pos+color VBO populated with {@link #DEFAULT_QUAD_POSITIONS} / {@link #DEFAULT_SOLID_COLORS}. */
protected final int defaultQuadVbo() {
return uploadPosColorVbo(DEFAULT_QUAD_POSITIONS, DEFAULT_SOLID_COLORS);
}
protected final int uploadEbo(int indexType, int... values) {
final int bytesPer = switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> 1;
case GL11.GL_UNSIGNED_SHORT -> 2;
case GL11.GL_UNSIGNED_INT -> 4;
default -> throw new IllegalArgumentException("unsupported indexType 0x" + Integer.toHexString(indexType));
};
final int ebo = newBuffer();
final ByteBuffer data = MemoryUtilities.memAlloc(values.length * bytesPer).order(ByteOrder.nativeOrder());
for (int v : values) {
switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> data.put((byte) v);
case GL11.GL_UNSIGNED_SHORT -> data.putShort((short) v);
case GL11.GL_UNSIGNED_INT -> data.putInt(v);
}
}
data.flip();
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(data);
return ebo;
}
/** VAO with loc0 = 2*FLOAT pos, loc1 = 4*UNSIGNED_BYTE norm color, stride 12. */
protected final int setupPosColorVao(int vbo) {
final int vao = newVao();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, 8);
GLStateManager.glEnableVertexAttribArray(1);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
return vao;
}
/**
* Record one {@code glDrawElements} into a {@code GL_COMPILE} list with the given VAO and EBO
* already-bound setup. Returns the list id (tracked for cleanup).
*/
protected final int recordSingleIndexedDraw(int vao, int ebo, int drawMode, int indexCount, int indexType) {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(drawMode, indexCount, indexType, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
return list;
}
protected static CompiledDisplayList compiledOf(int listId) {
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(listId);
assertNotNull(compiled, "compiled display list must exist for id " + listId);
return compiled;
}
protected static List<BatchedIndexedDrawCmd> allBatchedCmds(int listId) {
final List<BatchedIndexedDrawCmd> out = new ArrayList<>();
final Object[] complex = compiledOf(listId).getComplexObjects();
if (complex == null) return out;
for (Object o : complex) {
if (o instanceof BatchedIndexedDrawCmd b) out.add(b);
}
return out;
}
protected static BatchedIndexedDrawCmd firstBatchedCmd(int listId) {
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(listId);
return cmds.isEmpty() ? null : cmds.getFirst();
}
protected static List<IndexedDrawBatch> batchesOf(int listId) {
return compiledOf(listId).getIndexedBatches();
}
protected static IndexedDrawBatch firstBatch(int listId) {
return batchesOf(listId).getFirst();
}
protected static ByteBuffer readBufferBytes(int target, int bufferId, int size) {
final int prev;
if (target == GL15.GL_ARRAY_BUFFER) prev = GLStateManager.getBoundVBO();
else if (target == GL15.GL_ELEMENT_ARRAY_BUFFER) prev = GLStateManager.getBoundEBO();
else prev = 0;
GLStateManager.glBindBuffer(target, bufferId);
final ByteBuffer buf = MemoryUtilities.memAlloc(size).order(ByteOrder.nativeOrder());
GL15.glGetBufferSubData(target, 0, buf);
GLStateManager.glBindBuffer(target, prev);
return buf;
}
protected static void overwriteBufferWithGarbage(int target, int bufferId, int size) {
final ByteBuffer garbage = MemoryUtilities.memAlloc(size);
for (int i = 0; i < garbage.capacity(); i++) garbage.put(i, (byte) 0xAA);
GLStateManager.glBindBuffer(target, bufferId);
GL15.glBufferData(target, garbage, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(target, 0);
MemoryUtilities.memFree(garbage);
}
}
@@ -0,0 +1,142 @@
package com.gtnewhorizons.angelica.glsm.redirect;
import org.junit.jupiter.api.Test;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class GLSMRedirectorTest {
private static final String GLSM = GLSMRedirector.getTargetClassName();
private static final Handle LAMBDA_METAFACTORY = new Handle(
Opcodes.H_INVOKESTATIC,
"java/lang/invoke/LambdaMetafactory",
"metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;"
+ "Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)"
+ "Ljava/lang/invoke/CallSite;",
false);
private static ClassNode makeClassWithInvokeDynamic(Handle implHandle) {
final ClassNode cn = new ClassNode();
cn.version = Opcodes.V1_8;
cn.access = Opcodes.ACC_PUBLIC;
cn.name = "com/example/TestClass";
cn.superName = "java/lang/Object";
cn.methods = new ArrayList<>();
final MethodNode mn = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "test", "()V", null, null);
mn.instructions.add(new InvokeDynamicInsnNode(
"run",
"()Ljava/lang/Runnable;",
LAMBDA_METAFACTORY,
Type.getType("()V"),
implHandle,
Type.getType("()V")));
mn.instructions.add(new InsnNode(Opcodes.RETURN));
cn.methods.add(mn);
return cn;
}
private static Handle getResultHandle(ClassNode cn) {
final InvokeDynamicInsnNode dyn = (InvokeDynamicInsnNode) cn.methods.get(0).instructions.get(0);
return (Handle) dyn.bsmArgs[1];
}
@Test
void invokeDynamicGL11HandleIsRedirected() {
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11", "glPopMatrix", "()V", false);
final ClassNode cn = makeClassWithInvokeDynamic(impl);
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
assertTrue(changed);
final Handle result = getResultHandle(cn);
assertEquals(GLSM, result.getOwner());
assertEquals("glPopMatrix", result.getName());
assertEquals("()V", result.getDesc());
assertEquals(Opcodes.H_INVOKESTATIC, result.getTag());
}
@Test
void invokeDynamicNamedClassHandleIsRedirected() {
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/EXTBlendFuncSeparate", "glBlendFuncSeparateEXT", "(IIII)V", false);
final ClassNode cn = makeClassWithInvokeDynamic(impl);
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
assertTrue(changed);
final Handle result = getResultHandle(cn);
assertEquals(GLSM, result.getOwner());
assertEquals("tryBlendFuncSeparate", result.getName());
assertEquals("(IIII)V", result.getDesc());
}
@Test
void invokeDynamicNonGLHandleIsNotTouched() {
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
final ClassNode cn = makeClassWithInvokeDynamic(impl);
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
assertFalse(changed);
final Handle result = getResultHandle(cn);
assertEquals("java/lang/System", result.getOwner());
assertEquals("currentTimeMillis", result.getName());
}
@Test
void invokeDynamicUnknownGLMethodIsNotTouched() {
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11", "glNonexistent", "()V", false);
final ClassNode cn = makeClassWithInvokeDynamic(impl);
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
assertFalse(changed);
final Handle result = getResultHandle(cn);
assertEquals("org/lwjgl/opengl/GL11", result.getOwner());
}
@Test
void invokeDynamicGLCVariantIsRedirected() {
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11C", "glDrawArrays", "(III)V", false);
final ClassNode cn = makeClassWithInvokeDynamic(impl);
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
assertTrue(changed);
final Handle result = getResultHandle(cn);
assertEquals(GLSM, result.getOwner());
assertEquals("glDrawArrays", result.getName());
assertEquals("(III)V", result.getDesc());
}
@Test
void transformedClassProducesValidBytecode() {
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11", "glPopMatrix", "()V", false);
final ClassNode cn = makeClassWithInvokeDynamic(impl);
new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
assertDoesNotThrow(() -> {
cn.accept(cw);
final byte[] bytes = cw.toByteArray();
assertTrue(bytes.length > 0);
});
}
}
+1
View File
@@ -74,6 +74,7 @@ dependencies {
// LWJGL3
implementation("org.lwjgl:lwjgl:${lwjglVersion}")
implementation("org.lwjgl:lwjgl-opengl:${lwjglVersion}")
compileOnly("org.lwjgl:lwjgl-sdl:${lwjglVersion}")
runtimeOnly("org.lwjgl:lwjgl:$lwjglVersion:$lwjglNatives")
runtimeOnly("org.lwjgl:lwjgl-opengl:$lwjglVersion:$lwjglNatives")
@@ -68,6 +68,16 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
return "OpenGL (LWJGL3)";
}
@Override
public boolean hasContext() {
try {
GL.getCapabilities();
return true;
} catch (IllegalStateException e) {
return false;
}
}
@Override
public int getPriority() { return 50; }
@@ -0,0 +1,40 @@
package com.gtnewhorizons.angelica.lwjgl3;
import static org.lwjgl.sdl.SDLMessageBox.SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
import static org.lwjgl.sdl.SDLMessageBox.SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
import static org.lwjgl.sdl.SDLMessageBox.SDL_MESSAGEBOX_ERROR;
import static org.lwjgl.sdl.SDLMessageBox.SDL_ShowMessageBox;
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
import me.eigenraven.lwjgl3ify.client.MainThreadExec;
import org.lwjgl.sdl.SDL_MessageBoxButtonData;
import org.lwjgl.sdl.SDL_MessageBoxData;
import org.lwjgl.system.MemoryStack;
@Lwjgl3Aware
public final class MissingDependencySdl {
private MissingDependencySdl() {}
public static void showFatal(String title, String message) {
MainThreadExec.runOnMainThread(() -> showFatalOnMainThread(title, message));
}
private static void showFatalOnMainThread(String title, String message) {
try (MemoryStack stack = MemoryStack.stackPush()) {
final SDL_MessageBoxData box = SDL_MessageBoxData.calloc(stack);
box.flags(SDL_MESSAGEBOX_ERROR);
box.title(stack.UTF8(title));
box.message(stack.UTF8(message));
final SDL_MessageBoxButtonData.Buffer buttons = SDL_MessageBoxButtonData.calloc(1, stack);
buttons.get(0).set(
SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT | SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT,
0,
stack.UTF8("OK"));
box.buttons(buttons);
SDL_ShowMessageBox(box, null);
}
}
}
@@ -1,8 +1,10 @@
package com.gtnewhorizons.angelica.api;
import net.minecraft.item.ItemStack;
/**
* To be used for modded items that don't have blocks. Baubles for example.
*/
public interface IDynamicLightProducer {
int getLuminance();
int getLuminance(ItemStack stack);
}
@@ -4,7 +4,14 @@ import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_PAYLOAD;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
@@ -213,12 +220,14 @@ public class BatchingFontRenderer {
pushQuadIdx();
}
private void pushTexRect(float x, float y, float w, float h, float itOff, int rgba, float uStart, float vStart, float uSz, float vSz) {
private void pushTexRect(float x, float y, float w, float h, float itOff, int rgba, float uStart, float vStart, float uSz, float vSz, boolean flipV) {
ensureCapacity();
pushVtx(x + itOff, y, rgba, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff, y + h, rgba, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz);
float vTop = flipV ? vStart + vSz : vStart;
float vBot = flipV ? vStart : vStart + vSz;
pushVtx(x + itOff, y, rgba, uStart, vTop, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff, y + h, rgba, uStart, vBot, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vTop, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vBot, uStart, uStart + uSz, vStart, vStart + vSz);
pushQuadIdx();
}
@@ -442,6 +451,55 @@ public class BatchingFontRenderer {
return (what >= fromInclusive) && (what <= toInclusive);
}
/**
* Count visible characters from {@code start} to {@code end}, skipping format codes.
* Stops at §r, any color code (§0-f, §x), or any color effect (§q, §g).
* Style codes (§k-o) and non-terminating effects (§z wave, §v dinnerbone) are skipped.
*/
private static int countVisibleChars(CharSequence str, int start, int end) {
int count = 0;
for (int i = start; i < end; i++) {
char ch = str.charAt(i);
if (ch == FORMATTING_CHAR && i + 1 < end) {
char code = Character.toLowerCase(str.charAt(i + 1));
if (code == 'r' || (code >= '0' && code <= '9') || (code >= 'a' && code <= 'f')
|| code == 'x' || code == 'q' || code == 'g') {
break;
}
i++;
} else {
count++;
}
}
return Math.max(count, 1);
}
private static int hsvToRgb(float hue, float sat, float val) {
int h = (int)(hue / 60f) % 6;
float f = hue / 60f - h;
float p = val * (1 - sat);
float q = val * (1 - f * sat);
float t = val * (1 - (1 - f) * sat);
float r, g, b;
switch (h) {
case 0: r=val; g=t; b=p; break;
case 1: r=q; g=val; b=p; break;
case 2: r=p; g=val; b=t; break;
case 3: r=p; g=q; b=val; break;
case 4: r=t; g=p; b=val; break;
default: r=val; g=p; b=q; break;
}
return ((int)(r*255) << 16) | ((int)(g*255) << 8) | (int)(b*255);
}
private static final int RAINBOW_LUT_SIZE = 24;
private static final int[] RAINBOW_LUT = new int[RAINBOW_LUT_SIZE];
static {
for (int i = 0; i < RAINBOW_LUT_SIZE; i++) {
RAINBOW_LUT[i] = hsvToRgb(i * 15f, 1f, 1f);
}
}
public boolean forceDefaults() {
return this.bookMode || this.isSGA || this.isSplash;
}
@@ -466,7 +524,8 @@ public class BatchingFontRenderer {
return forceDefaults() ? 1 : FontConfig.fontShadowOffset;
}
private static final char FORMATTING_CHAR = 167; // §
private static final double WAVE_TIME_SCALE = 5e-9;
private static final float WAVE_FREQUENCY = 0.5f;
public float drawString(final float anchorX, final float anchorY, final int color, final boolean enableShadow,
final boolean unicodeFlag, final CharSequence string, int stringOffset, int stringLength) {
@@ -497,6 +556,15 @@ public class BatchingFontRenderer {
boolean curBold = false;
boolean curStrikethrough = false;
boolean curUnderline = false;
boolean curRainbow = false;
boolean curWave = false;
boolean curDinnerbone = false;
boolean curGradient = false;
int gradientStartRgb = 0, gradientEndRgb = 0;
int gradientCharIndex = 0, gradientTotalChars = 0;
float gradientStep = 0f;
int rainbowCharIndex = 0;
int visibleCharIndex = 0;
float glyphScaleY = getGlyphScaleY();
float glyphScaleX = getGlyphScaleX();
@@ -534,6 +602,16 @@ public class BatchingFontRenderer {
strikethroughStartX = strikethroughEndX;
}
if (fmtCode == 'x' && AngelicaConfig.enableRGBColors && charIdx + SECTION_X_PAYLOAD < stringEnd) {
int rgb = ColorCodeUtils.parseHexPairs(string, charIdx + 1, 6);
if (rgb != -1) {
curRainbow = false;
curGradient = false;
curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF);
curShadowColor = (curShadowColor & 0xFF000000) | ((rgb & 0xFCFCFC) >> 2);
charIdx += SECTION_X_PAYLOAD;
}
} else {
final boolean is09 = charInRange(fmtCode, '0', '9');
final boolean isAF = charInRange(fmtCode, 'a', 'f');
if (is09 || isAF) {
@@ -542,6 +620,9 @@ public class BatchingFontRenderer {
curStrikethrough = false;
curUnderline = false;
curItalic = false;
curRainbow = false;
curGradient = false;
// wave/dinnerbone NOT reset — they're positional effects, independent of color
final int colorIdx = is09 ? (fmtCode - '0') : (fmtCode - 'a' + 10);
final int rgb = this.colorCode[colorIdx];
@@ -562,27 +643,51 @@ public class BatchingFontRenderer {
underlineEndX = underlineStartX;
} else if (fmtCode == 'o') {
curItalic = true;
} else if (fmtCode == 'q' && AngelicaConfig.enableRainbow) {
curRainbow = true;
curGradient = false;
rainbowCharIndex = 0;
} else if (fmtCode == 'z' && AngelicaConfig.enableWaveText) {
curWave = !curWave;
} else if (fmtCode == 'v' && AngelicaConfig.enableDinnerboneText) {
curDinnerbone = !curDinnerbone;
} else if (fmtCode == 'g' && AngelicaConfig.enableGradients && charIdx + GRADIENT_PAYLOAD < stringEnd) {
int color1 = ColorCodeUtils.parseSectionXAt(string, charIdx + 1);
int color2 = ColorCodeUtils.parseSectionXAt(string, charIdx + 1 + SECTION_X_LENGTH);
if (color1 != -1 && color2 != -1) {
curGradient = true;
curRainbow = false;
gradientStartRgb = color1;
gradientEndRgb = color2;
gradientCharIndex = 0;
gradientTotalChars = countVisibleChars(string, charIdx + 1 + GRADIENT_PAYLOAD, stringEnd);
gradientStep = gradientTotalChars > 1 ? 1f / (gradientTotalChars - 1) : 0f;
charIdx += GRADIENT_PAYLOAD;
}
} else if (fmtCode == 'r') {
curRandom = false;
curBold = false;
curStrikethrough = false;
curUnderline = false;
curItalic = false;
curRainbow = false;
curWave = false;
curDinnerbone = false;
curGradient = false;
curColor = color;
curShadowColor = shadowColor;
}
} // close else block for non-§x codes
continue;
}
if (FontConfig.enableCustomFont && FontConfig.enableGlyphReplacements) {
String chrReplacement = GlyphReplacements.customGlyphs.get(String.valueOf(chr));
if (chrReplacement != null) {
char replacement = chrReplacement.charAt(0);
boolean isReplacementCharAvailable =
FontProviderCustom.getPrimary().isGlyphAvailable(replacement)
|| FontProviderCustom.getFallback().isGlyphAvailable(replacement);
if (isReplacementCharAvailable) {
final char replacement = GlyphReplacements.getReplacementGlyph(chr);
if (replacement != 0) {
if (FontProviderCustom.getPrimary().isGlyphAvailable(replacement)
|| FontProviderCustom.getFallback().isGlyphAvailable(replacement)
) {
chr = replacement;
}
}
@@ -597,12 +702,31 @@ public class BatchingFontRenderer {
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * fontProvider.getYScaleMultiplier() / 2);
float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY * fontProvider.getYScaleMultiplier();
visibleCharIndex++;
// Check ASCII space, NBSP, NNBSP
if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') {
curX += 4 * this.getWhitespaceScale() + (curBold ? 1 : 0);
continue;
}
if (curRainbow) {
int rgbEffect = RAINBOW_LUT[rainbowCharIndex % RAINBOW_LUT_SIZE];
curColor = (curColor & 0xFF000000) | rgbEffect;
curShadowColor = (curShadowColor & 0xFF000000) | ((rgbEffect & 0xFCFCFC) >> 2);
rainbowCharIndex++;
}
if (curGradient && gradientTotalChars > 0) {
float t = Math.min(gradientCharIndex * gradientStep, 1f);
int gr = (int)((gradientStartRgb >> 16 & 0xFF) * (1-t) + (gradientEndRgb >> 16 & 0xFF) * t);
int gg = (int)((gradientStartRgb >> 8 & 0xFF) * (1-t) + (gradientEndRgb >> 8 & 0xFF) * t);
int gb = (int)((gradientStartRgb & 0xFF) * (1-t) + (gradientEndRgb & 0xFF) * t);
int rgbEffect = (gr << 16) | (gg << 8) | gb;
curColor = (curColor & 0xFF000000) | rgbEffect;
curShadowColor = (curShadowColor & 0xFF000000) | ((rgbEffect & 0xFCFCFC) >> 2);
gradientCharIndex++;
}
final float uStart = fontProvider.getUStart(chr);
final float vStart = fontProvider.getVStart(chr);
final float xAdvance = fontProvider.getXAdvance(chr) * glyphScaleX;
@@ -616,23 +740,30 @@ public class BatchingFontRenderer {
final ResourceLocation texture = fontProvider.getTexture(chr);
final int idxId = idxWriterIndex;
// Wave: Y offset via sine wave
float renderY = heightNorth;
if (curWave) {
float time = HUDCaching.renderingCacheOverride ? 0f : (float)((System.nanoTime() & 0xFFFFFFFFFFFFL) * WAVE_TIME_SCALE);
renderY += (float) Math.sin(visibleCharIndex * WAVE_FREQUENCY + time) * AngelicaConfig.waveAmplitude;
}
if (enableShadow) {
for (int n = 1; n <= shadowCopies; n++) {
final float shadowOffsetPart = shadowOffset * ((float) n / shadowCopies);
pushTexRect(curX + shadowOffsetPart, heightNorth + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz);
pushTexRect(curX + shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
if (curBold) {
pushTexRect(curX + 2.0f * shadowOffsetPart, heightNorth + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz);
pushTexRect(curX + 2.0f * shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
}
}
}
pushTexRect(curX, heightNorth, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz);
pushTexRect(curX, renderY, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz, curDinnerbone);
if (curBold) {
for (int n = 1; n <= boldCopies; n++) {
final float shadowOffsetPart = shadowOffset * ((float) n / boldCopies);
pushTexRect(curX + shadowOffsetPart, heightNorth, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz);
pushTexRect(curX + shadowOffsetPart, renderY, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz, curDinnerbone);
}
}
@@ -653,6 +784,19 @@ public class BatchingFontRenderer {
if (bookMode) { curX = (int) curX; }
underlineEndX = curX;
strikethroughEndX = curX;
if ((curRainbow || curGradient) && curUnderline && underlineStartX != underlineEndX) {
final int ulIdx = idxWriterIndex;
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor);
pushDrawCmd(ulIdx, 6, null, false);
underlineStartX = underlineEndX;
}
if ((curRainbow || curGradient) && curStrikethrough && strikethroughStartX != strikethroughEndX) {
final int stIdx = idxWriterIndex;
pushUntexRect(strikethroughStartX, strikethroughY, strikethroughEndX - strikethroughStartX, glyphScaleY, curColor);
pushDrawCmd(stIdx, 6, null, false);
strikethroughStartX = strikethroughEndX;
}
}
if (curUnderline && underlineStartX != underlineEndX) {
@@ -0,0 +1,144 @@
package com.gtnewhorizons.angelica.client.font;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
/**
* Utility for converting {@code &}-based shorthand into {@code §}-based
* format codes understood by the batched font renderer.
* <p>
* {@code &#RRGGBB} -> {@code §x§R§R§G§G§B§B} (RGB color)<br>
* {@code &g&#RRGGBB&#RRGGBB} -> {@code §g§x§R§R§G§G§B§B§x§R§R§G§G§B§B} (gradient)<br>
* {@code &c}, {@code &l}, {@code &r}, {@code &q}, etc. -> {@code §c}, {@code §l}, {@code §r}, {@code §q}
* <p>
* Effect codes use low-collision letters to avoid false positives on natural text:
* rainbow = {@code &q}, wave = {@code &z}, dinnerbone = {@code &v}, gradient = {@code &g} (only with &#-prefixed colors).
*/
public final class ColorCodeUtils {
private ColorCodeUtils() {}
public static final char FORMATTING_CHAR = '§';
public static final int SECTION_X_PAYLOAD = 12;
public static final int SECTION_X_LENGTH = 14;
public static final int GRADIENT_PAYLOAD = 28;
public static final int GRADIENT_LENGTH = 30;
/** Valid single {@code &} codes. {@code g} is excluded; {@code &g} only converts as part of a gradient. */
public static final String VALID_SINGLE_CODES = "0123456789abcdefklmnorqzv";
private static final String[] SECTION_PREFIX = new String[128];
static {
for (int i = 0; i < VALID_SINGLE_CODES.length(); i++) {
char c = VALID_SINGLE_CODES.charAt(i);
SECTION_PREFIX[c] = String.valueOf(FORMATTING_CHAR) + c;
}
SECTION_PREFIX['x'] = String.valueOf(FORMATTING_CHAR) + 'x';
SECTION_PREFIX['g'] = String.valueOf(FORMATTING_CHAR) + 'g';
}
public static String sectionPrefix(char code) {
char c = (code < 128) ? Character.toLowerCase(code) : code;
return (c < 128) ? SECTION_PREFIX[c] : null;
}
public static int parseHexPairs(CharSequence str, int start, int count) {
int result = 0;
for (int i = 0; i < count; i++) {
int p = start + i * 2;
if (str.charAt(p) != FORMATTING_CHAR) return -1;
int digit = Character.digit(str.charAt(p + 1), 16);
if (digit == -1) return -1;
result = (result << 4) | digit;
}
return result;
}
public static int parseSectionXAt(CharSequence str, int start) {
if (start + SECTION_X_LENGTH > str.length()) return -1;
if (str.charAt(start) != FORMATTING_CHAR) return -1;
if (Character.toLowerCase(str.charAt(start + 1)) != 'x') return -1;
return parseHexPairs(str, start + 2, 6);
}
public static boolean isValidSectionX(CharSequence str, int start) {
return parseSectionXAt(str, start) != -1;
}
private static String lastConversionInput;
private static String lastConversionOutput;
public static String convertAmpersandToSectionX(String text) {
if (text == null || !AngelicaConfig.enableAmpersandConversion) return text;
if (text == lastConversionInput) return lastConversionOutput;
final String out = convertImpl(text);
lastConversionInput = text;
lastConversionOutput = out;
return out;
}
private static String convertImpl(String text) {
int idx = text.indexOf('&');
if (idx == -1) return text;
final int len = text.length();
StringBuilder sb = null;
int last = 0;
while (idx != -1 && idx + 1 < len) {
if (text.charAt(idx + 1) == '#' && idx + 7 < len) {
boolean validHex = true;
for (int i = 2; i <= 7; i++) {
if (Character.digit(text.charAt(idx + i), 16) == -1) {
validHex = false;
break;
}
}
if (validHex) {
if (sb == null) sb = new StringBuilder(len + 16);
sb.append(text, last, idx);
sb.append(FORMATTING_CHAR).append('x');
for (int i = 2; i <= 7; i++) {
sb.append(FORMATTING_CHAR).append(text.charAt(idx + i));
}
last = idx + 8;
idx = text.indexOf('&', last);
continue;
}
}
if (Character.toLowerCase(text.charAt(idx + 1)) == 'g' && idx + 17 < len
&& text.charAt(idx + 2) == '&' && text.charAt(idx + 3) == '#'
&& text.charAt(idx + 10) == '&' && text.charAt(idx + 11) == '#') {
boolean valid1 = true, valid2 = true;
for (int i = 4; i <= 9; i++) {
if (Character.digit(text.charAt(idx + i), 16) == -1) { valid1 = false; break; }
}
if (valid1) {
for (int i = 12; i <= 17; i++) {
if (Character.digit(text.charAt(idx + i), 16) == -1) { valid2 = false; break; }
}
}
if (valid1 && valid2) {
if (sb == null) sb = new StringBuilder(len + 16);
sb.append(text, last, idx);
sb.append(FORMATTING_CHAR).append('g');
last = idx + 2;
idx = text.indexOf('&', last);
continue;
}
}
char code = Character.toLowerCase(text.charAt(idx + 1));
if (VALID_SINGLE_CODES.indexOf(code) != -1) {
if (sb == null) sb = new StringBuilder(len + 16);
sb.append(text, last, idx);
sb.append(FORMATTING_CHAR).append(text.charAt(idx + 1));
last = idx + 2;
}
idx = text.indexOf('&', idx + 1);
}
if (sb == null) return text;
sb.append(text, last, len);
return sb.toString();
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.client.rendering;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import lombok.Getter;
import net.minecraft.client.renderer.Tessellator;
import org.lwjgl.opengl.GL11;
@@ -103,17 +104,36 @@ public class DeferredDrawBatcher {
subEnd++;
// Don't try to merge strips, fans, loops or polygons
if (drawMode == GL11.GL_TRIANGLE_STRIP || drawMode == GL11.GL_TRIANGLE_FAN || drawMode == GL11.GL_LINE_STRIP ||
drawMode == GL11.GL_LINE_LOOP || drawMode == GL11.GL_QUAD_STRIP || drawMode == GL11.GL_POLYGON)
break;
if (GLStateManager.isContinuousDraw(drawMode)) break;
}
TessellatorStreamingDrawer.drawPackedBatch(batchTessellator, ranges, i, subEnd, totalBytes, totalVertices, drawMode, flags);
drawPackedBatch(batchTessellator, ranges, i, subEnd, totalBytes, totalVertices, drawMode, flags);
i = subEnd;
}
}
/**
* Pack ranges from a DeferredBatchTessellator into the streaming drawer's repack buffer
* and issue a single draw call for the merged batch.
*/
private static void drawPackedBatch(DeferredBatchTessellator source, List<DeferredBatchTessellator.DrawRange> ranges, int from, int to, int totalBytes, int totalVertices, int drawMode, int flags) {
if (totalVertices == 0) return;
TessellatorStreamingDrawer.ensureRepackCapacity(totalBytes);
long writePos = TessellatorStreamingDrawer.getRepackAddress();
for (int j = from; j < to; j++) {
final DeferredBatchTessellator.DrawRange r = ranges.get(j);
source.copyRange(r.byteOffset(), r.byteLength(), writePos);
writePos += r.byteLength();
}
final java.nio.ByteBuffer buf = TessellatorStreamingDrawer.getRepackBuffer();
buf.position(0);
buf.limit(totalBytes);
TessellatorStreamingDrawer.drawPacked(buf, drawMode, flags, totalVertices);
}
/**
* Pack current GLSM state into a long key for grouping.
* Layout: [tex1En:1][tex0En:1][textureId:20][srcRgb:12][dstRgb:12][blendEnabled:1][depthMask:1] = 48 bits
@@ -1,10 +1,8 @@
package com.gtnewhorizons.angelica.client.rendering;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.coderbot.iris.gl.state.StateUpdateNotifiers;
import net.minecraft.client.renderer.texture.AbstractTexture;
import org.jetbrains.annotations.Nullable;
@@ -15,11 +13,15 @@ public class TextureTracker {
*/
public static final TextureTracker INSTANCE = new TextureTracker();
private static Runnable bindTextureListener;
static {
StateUpdateNotifiers.bindTextureNotifier = listener -> bindTextureListener = listener;
}
private final Int2ObjectMap<AbstractTexture> textures = new Int2ObjectOpenHashMap<>();
private boolean lockBindCallback;
private TextureTracker() {
}
@@ -31,23 +33,13 @@ public class TextureTracker {
public AbstractTexture getTexture(int id) {
return textures.get(id);
}
public void onBindTexture(int id) {
if (lockBindCallback) {
return;
}
if (GLStateManager.getActiveTextureUnit() == 0) {
lockBindCallback = true;
if (GLSMHooks.TEXTURE_BIND.hasListeners()) {
GLSMHooks.textureBindEvent.textureId = id;
GLSMHooks.TEXTURE_BIND.post(GLSMHooks.textureBindEvent);
}
// Reset texture state
RenderSystem.bindTextureToUnit(0, id);
lockBindCallback = false;
public void onBindTexture() {
if (bindTextureListener != null) {
bindTextureListener.run();
}
}
public void onDeleteTexture(int id) {
textures.remove(id);
}
@@ -0,0 +1,39 @@
package com.gtnewhorizons.angelica.compat;
import java.util.function.Function;
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
import com.gtnewhorizons.angelica.client.font.ColorCodeUtils;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
public class GTNHLibCompat {
public static final boolean HAS_TEXT_PREPROCESSOR;
static {
boolean found = false;
try {
FontRendering.class.getMethod("setTextPreprocessor", Function.class);
found = true;
} catch (NoSuchMethodException ignored) {}
HAS_TEXT_PREPROCESSOR = found;
}
/** Only call when {@link #HAS_TEXT_PREPROCESSOR} is true. */
public static void registerPreprocessor() {
FontRendering.setTextPreprocessor(new FontRendering.TextPreprocessor() {
@Override
public String apply(String text) {
return AngelicaConfig.enableAmpersandConversion
? ColorCodeUtils.convertAmpersandToSectionX(text)
: text;
}
@Override
public boolean handlesAmpCodes() {
return AngelicaConfig.enableAmpersandConversion;
}
});
}
}
@@ -24,7 +24,9 @@ public class ModStatus {
public static boolean isThaumcraftLoaded;
public static boolean isThaumicHorizonsLoaded;
public static boolean isBaublesLoaded;
public static boolean isCosmeticArmorReworkedLoaded;
public static boolean isFluidLoggedLoaded;
public static boolean isBOPLoaded;
public static void preInit() {
if (Loader.isModLoaded("backhand")) {
@@ -42,9 +44,11 @@ public class ModStatus {
isThaumcraftLoaded = Loader.isModLoaded("Thaumcraft");
isThaumicHorizonsLoaded = Loader.isModLoaded("ThaumicHorizons");
isBaublesLoaded = Loader.isModLoaded("Baubles");
isCosmeticArmorReworkedLoaded = Loader.isModLoaded("cosmeticarmorreworked");
isFluidLoggedLoaded = Loader.isModLoaded("fluidlogged");
isHoloInventoryLoaded = Loader.isModLoaded("holoinventory");
isBOPLoaded = Loader.isModLoaded("BiomesOPlenty");
// remove compat with original release of BG2
if (isBattlegearLoaded){
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.IRenderGlobalExt;
import cpw.mods.fml.common.Optional;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.Tessellator;
import vfyjxf.bettercrashes.utils.StateManager;
@Optional.Interface(modid = "angelica", iface = "vfyjxf.bettercrashes.utils.StateManager.IResettable")
@@ -25,6 +26,7 @@ public class BetterCrashesCompat implements StateManager.IResettable {
@Override
public void resetState() {
Tessellator.instance.isDrawing = false;
AngelicaMod.LOGGER.info("Reloading SodiumRenderer");
((IRenderGlobalExt) Minecraft.getMinecraft().renderGlobal).angelica$reload();
AngelicaMod.LOGGER.info("Resetting GLStateManager");
@@ -26,12 +26,11 @@ public class Camera {
private float pitch;
private float yaw;
private EntityLivingBase entity;
private boolean isSleeping;
private boolean thirdPerson;
private float partialTicks;
private Camera() {
}
private Camera() {}
/**
* Updates the camera position and orientation based on the current entity and partial ticks.
@@ -41,7 +40,7 @@ public class Camera {
*/
public void update(EntityLivingBase entity, float partialTicks) {
this.partialTicks = partialTicks;
this.entity = entity;
this.isSleeping = entity.isPlayerSleeping();
// Entity position (interpolated, at eye level)
final double entityX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks;
@@ -63,6 +63,11 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean enableVAO;
@Config.Comment("Enables DSA (Direct State Access) for faster bindings. Disable if you notice terrible performance.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean enableDSA;
@Config.Comment("Enable NotFine features")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
@@ -107,6 +112,35 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean enableFontRenderer;
@Config.Comment("Enable full RGB color support (16.7M colors) using &#RRGGBB syntax in text")
@Config.DefaultBoolean(true)
public static boolean enableRGBColors;
@Config.Comment("Enable gradient text (&g&#start&#end)")
@Config.DefaultBoolean(true)
public static boolean enableGradients;
@Config.Comment("Enable rainbow cycling text (&q)")
@Config.DefaultBoolean(true)
public static boolean enableRainbow;
@Config.Comment("Enable wave/bounce animated text (&z)")
@Config.DefaultBoolean(true)
public static boolean enableWaveText;
@Config.Comment("Enable upside-down text (&v)")
@Config.DefaultBoolean(true)
public static boolean enableDinnerboneText;
@Config.Comment("Wave text amplitude (how far characters bounce)")
@Config.DefaultFloat(2.0f)
@Config.RangeFloat(min = 1.0f, max = 8.0f)
public static float waveAmplitude;
@Config.Comment("Convert &-prefix format codes (&#RRGGBB, &c, &l, etc.) at render time")
@Config.DefaultBoolean(true)
public static boolean enableAmpersandConversion;
@Config.Comment("Enable Dynamic Lights")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
@@ -235,6 +269,11 @@ public class AngelicaConfig {
@Config.DefaultBoolean(false)
public static boolean enableHardcodedCustomUniforms;
@Config.Comment("Modern MC_VERSION to try if shader pack has no 1.7.10 section. 0 = default (260101)")
@Config.DefaultInt(0)
@Config.RangeInt(min = 0)
public static int modernFallbackMcVersion;
@Config.Comment("Define IS_IRIS in shader macros.")
@Config.DefaultBoolean(true)
public static boolean defineIsIris;
@@ -262,6 +301,11 @@ public class AngelicaConfig {
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public boolean narrowAlfheim;
@Config.Comment("Narrow Ears transformer exclusions to allow GL redirection")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public boolean narrowEars;
}
@Config.Comment("Renders chunks before neighbors are ready. Improves loading at render distance edges, useful for low render distance servers.")
@@ -287,4 +331,9 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean disableGLVersionPinning;
@Config.Comment("Disables GL Error checks. Always set to false in dev env or if LWJGL debug is on. Improves performance.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean disableErrorChecks;
}
@@ -45,23 +45,9 @@ public class CompatConfig {
@Config.RequiresMcRestart
public static boolean fixSecurityCraft;
@Config.Comment("Apply tweaks to NTM:Space")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean tweakNTMSpace;
@Config.Comment("Apply fixes to NTM:Space")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean fixNTMSpace;
@Config.Comment("Disables rendering of the planet below the player at high altitudes [Requires tweakNTMSpace]")
@Config.DefaultEnum("ShadersOnly")
public static altitudePlanetRendererState NTMSpace_disableAltitudePlanetRenderer;
public enum altitudePlanetRendererState {
Always,
ShadersOnly,
Never
}
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.dynamiclights;
import baubles.common.lib.PlayerHandler;
import lain.mods.cos.CosmeticArmorReworked;
import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
import com.gtnewhorizon.gtnhlib.util.CoordinatePacker;
@@ -180,6 +181,19 @@ public class DynamicLights {
this.lightSourcesLock.writeLock().unlock();
}
/**
* Removes all tracked light sources, to call when unloading the client world
*/
public void removeAllLightSources() {
this.lightSourcesLock.writeLock().lock();
this.dynamicLightSources.clear();
this.lightSourceCount = 0;
rebuildLightSourceArray();
this.lightSourcesLock.writeLock().unlock();
}
/**
* Removes light sources if the filter matches.
*
@@ -559,13 +573,13 @@ public class DynamicLights {
*/
public static int getLuminanceFromItemStack(@NotNull ItemStack stack) {
final Item item = stack.getItem();
if (item instanceof ItemBlock itemBlock) {
if (item instanceof IDynamicLightProducer lightProducer) {
return lightProducer.getLuminance(stack);
} else if (item instanceof ItemBlock itemBlock) {
final Block block = itemBlock.field_150939_a;
if (block != null) {
return block.getLightValue();
}
} else if (item instanceof IDynamicLightProducer lightProducer){
return lightProducer.getLuminance();
}
if (item == Items.lava_bucket) return Blocks.lava.getLightValue();
@@ -625,6 +639,18 @@ public class DynamicLights {
}
}
}
if (ModStatus.isCosmeticArmorReworkedLoaded) {
final var cosArmor = CosmeticArmorReworked.invMan.getCosArmorInventoryClient(player.getUniqueID());
if (cosArmor != null) {
for (int i = 0; i < cosArmor.getSizeInventory(); i++) {
final var stack = cosArmor.getStackInSlot(i);
if (stack != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(stack));
}
}
}
}
}
return luminance;
Whitespace-only changes.
@@ -76,7 +76,7 @@ public class HUDCaching {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
if (!AngelicaConfig.hudCachingActive || !GLStateManager.isFramebufferEnabled()) {
if (!AngelicaConfig.hudCachingActive || mc.displayWidth < 16 || mc.displayHeight < 16) {
ingame.renderGameOverlay(partialTicks, hasScreen, mouseX, mouseY);
return;
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.iris;
import com.gtnewhorizons.angelica.client.rendering.TextureTracker;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredAlphaHandler;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredDepthColorHandler;
@@ -10,14 +11,32 @@ import net.coderbot.iris.gl.blending.AlphaTestStorage;
import net.coderbot.iris.gl.blending.BlendModeStorage;
import net.coderbot.iris.gl.blending.DepthColorStorage;
import net.coderbot.iris.gl.program.ProgramUniforms;
import net.coderbot.iris.gl.state.StateUpdateNotifiers;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.samplers.IrisSamplers;
import net.coderbot.iris.texture.pbr.PBRTextureManager;
import net.coderbot.iris.vertices.ImmediateState;
public class IrisGLSMBridge {
private static Runnable alphaFuncListener = null;
private static Runnable alphaTestListener = null;
private static Runnable blendFuncListener = null;
private static Runnable fogModeListener = null;
private static Runnable fogStartListener = null;
private static Runnable fogEndListener = null;
private static Runnable fogDensityListener = null;
static {
StateUpdateNotifiers.alphaFuncNotifier = listener -> alphaFuncListener = listener;
StateUpdateNotifiers.alphaTestNotifier = listener -> alphaTestListener = listener;
StateUpdateNotifiers.blendFuncNotifier = listener -> blendFuncListener = listener;
StateUpdateNotifiers.fogModeNotifier = listener -> fogModeListener = listener;
StateUpdateNotifiers.fogStartNotifier = listener -> fogStartListener = listener;
StateUpdateNotifiers.fogEndNotifier = listener -> fogEndListener = listener;
StateUpdateNotifiers.fogDensityNotifier = listener -> fogDensityListener = listener;
}
public static void register() {
IrisSamplers.initRenderer();
GLSMHooks.blendHandler = new DeferredBlendHandler() {
@@ -76,8 +95,31 @@ public class IrisGLSMBridge {
}
};
GLSMHooks.ALPHA_STATE_CHANGE.addListener(event -> {
if (Iris.enabled) {
if (alphaFuncListener != null) alphaFuncListener.run();
if (alphaTestListener != null) alphaTestListener.run();
}
});
GLSMHooks.BLEND_FUNC_CHANGE.addListener(event -> {
if (Iris.enabled) {
if (blendFuncListener != null) blendFuncListener.run();
}
});
GLSMHooks.FOG_STATE_CHANGE.addListener(event -> {
if (Iris.enabled) {
if (fogModeListener != null) fogModeListener.run();
if (fogStartListener != null) fogStartListener.run();
if (fogEndListener != null) fogEndListener.run();
if (fogDensityListener != null) fogDensityListener.run();
}
});
GLSMHooks.TEXTURE_BIND.addListener(event -> {
if (Iris.enabled) {
TextureTracker.INSTANCE.onBindTexture();
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (pipeline != null) {
pipeline.onBindTexture(event.textureId);
@@ -106,20 +148,37 @@ public class IrisGLSMBridge {
}
});
GLSMHooks.PROGRAM_CHANGE.addListener(event -> ProgramUniforms.clearActiveUniforms());
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
if (!Iris.enabled) return;
if (event.postBind) return;
ProgramUniforms.clearActiveUniforms();
});
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
if (!Iris.enabled) return;
if (event.postBind) return;
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (!(pipeline instanceof DeferredWorldRenderingPipeline drp)) return;
if (!drp.shouldOverrideShaders()) return;
if (drp.getActivePassProgramId() == -1) return;
if (!ImmediateState.isRenderingLevel || DepthColorStorage.isOwnedProgram(event.newProgram)) {
if (DepthColorStorage.isOwnedProgram(event.newProgram)) {
DepthColorStorage.unlockDepthColor();
} else {
drp.onModProgramOverride();
}
});
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
if (!Iris.enabled) return;
if (!event.postBind) return;
WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (pipeline instanceof DeferredWorldRenderingPipeline drp) {
DeferredWorldRenderingPipeline.Pass activePass = drp.getActivePassProgram();
if (activePass != null && activePass.getProgram() != null && activePass.getProgram().getProgramId() == event.newProgram) {
activePass.getProgram().getUniforms().update();
}
}
});
}
}
@@ -8,7 +8,10 @@ import com.gtnewhorizon.gtnhmixins.builders.ITransformers;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.CompatConfig;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.loading.DependencyVerifier;
import com.gtnewhorizons.angelica.glsm.loading.EcosystemNarrowRules;
import com.gtnewhorizons.angelica.loading.fml.compat.CompatHandlers;
import com.gtnewhorizons.angelica.lwjgl3.MissingDependencySdl;
import com.gtnewhorizons.angelica.mixins.Mixins;
import com.gtnewhorizons.retrofuturabootstrap.SharedConfig;
import cpw.mods.fml.relauncher.FMLLaunchHandler;
@@ -27,6 +30,8 @@ import org.apache.logging.log4j.core.config.LoggerConfig;
import org.spongepowered.asm.launch.GlobalProperties;
import org.spongepowered.asm.service.mojang.MixinServiceLaunchWrapper;
import javax.swing.JOptionPane;
import java.awt.GraphicsEnvironment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -91,30 +96,45 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
.findFirst()
.orElse(null);
if (handle != null) {
handle.exclusions().add("org.embeddedt.embeddium");
handle.exclusions().add("org.taumc.celeritas");
handle.exclusions().add("com.mitchej123.lwjgl.lwjgl3");
handle.exclusions().add("com.mitchej123.glsm");
handle.exclusions().add("com.gtnewhorizons.angelica.lwjgl3");
for (String exclusion : EcosystemNarrowRules.LWJGL3IFY_EXCLUSIONS_SHARED) {
handle.exclusions().add(exclusion);
}
handle.exclusions().add("com.gtnewhorizons.angelica.vulkan");
}
}
private static void verifyDependencies() {
// Check for fastutil (bundled with GTNHLib since 0.2.1)
if (AngelicaTweaker.class.getResource("/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class") == null) {
throw new RuntimeException("Missing dependency: Angelica requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib");
try {
DependencyVerifier.verify(AngelicaTweaker.class, List.of(
new DependencyVerifier.Check(
"/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class",
"Missing dependency: Angelica requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib"),
new DependencyVerifier.Check(
"/com/gtnewhorizon/gtnhlib/client/renderer/VertexCallbackManager.class",
"GTNHLib is outdated: Angelica requires GTNHLib 0.10.0 or newer! Download: https://modrinth.com/mod/gtnhlib")
));
} catch (RuntimeException ex) {
fatalDependencyError(ex.getMessage());
}
}
// Check for PrimitiveExtractor/cel.model classes (added in GTNHLib 0.8.21)
if (AngelicaTweaker.class.getResource("/com/gtnewhorizon/gtnhlib/client/renderer/PrimitiveExtractor.class") == null) {
throw new RuntimeException("GTNHLib is outdated: Angelica requires GTNHLib 0.8.21 or newer! Download: https://modrinth.com/mod/gtnhlib");
}
private static void fatalDependencyError(String message) {
final String title = "Angelica - Missing Dependency";
System.err.println("FATAL: " + message);
try {
LOGGER.fatal(message);
} catch (Throwable ignored) {}
// Check for jvmdowngrader stubs (bundled with GTNHLib since 0.9.0)
if (AngelicaTweaker.class.getResource("/xyz/wagyourtail/jvmdg/exc/MissingStubError.class") == null) {
throw new RuntimeException("GTNHLib is outdated: Angelica requires GTNHLib 0.9.0 or newer! Download: https://modrinth.com/mod/gtnhlib");
}
final int lwjgl3ifyMajor = ((Integer) Launch.blackboard.getOrDefault("lwjgl3ify:major-version", Integer.MIN_VALUE));
try {
if (lwjgl3ifyMajor >= 3) {
MissingDependencySdl.showFatal(title, message);
} else if (!GraphicsEnvironment.isHeadless()) {
JOptionPane.showMessageDialog(null, message, title, JOptionPane.ERROR_MESSAGE);
}
} catch (Throwable ignored) {}
Runtime.getRuntime().halt(1);
}
@Override
@@ -152,6 +172,7 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
narrowTransformerConfig("Xaeros", AngelicaConfig.transformerCompat.narrowXaeros);
narrowTransformerConfig("AdvancedLightsabers", AngelicaConfig.transformerCompat.narrowAdvancedLightsabers);
narrowTransformerConfig("Alfheim", AngelicaConfig.transformerCompat.narrowAlfheim);
narrowTransformerConfig("Ears", AngelicaConfig.transformerCompat.narrowEars);
tweaks.add("com.gtnewhorizons.angelica.loading.fml.tweakers.IncompatibleModsDisablerTweaker");
if (AngelicaConfig.enableHudCaching) {
@@ -8,7 +8,6 @@ import com.gtnewhorizons.angelica.loading.fml.compat.handlers.ThaumcraftCompatHa
import com.gtnewhorizons.angelica.loading.fml.compat.handlers.ThaumicHorizonsCompatHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
@@ -45,20 +44,18 @@ public enum CompatHandlers {
/**
* Returns extra transformers as well as the main transformer.
* Returns an empty list if no handlers registered.
*/
public static List<String> getTransformers() {
final List<ICompatHandler> handlers = getHandlers();
if (handlers.isEmpty()) {
return Collections.emptyList();
}
List<String> transformers = new ArrayList<>();
List<String> transformers = new ArrayList<>();
for (ICompatHandler handler : handlers) {
if (handler.extraTransformers() != null) {
transformers.addAll(handler.extraTransformers());
}
}
transformers.add("com.gtnewhorizons.angelica.loading.fml.transformers.GenericCompatTransformer");
if (!handlers.isEmpty()) {
transformers.add("com.gtnewhorizons.angelica.loading.fml.transformers.GenericCompatTransformer");
}
return transformers;
}
}
@@ -1,10 +1,8 @@
package com.gtnewhorizons.angelica.loading.fml.transformers;
import com.gtnewhorizons.angelica.loading.shared.transformers.AngelicaRedirector;
import com.gtnewhorizons.angelica.glsm.loading.EarlyRedirectorCore;
import com.gtnewhorizons.angelica.glsm.loading.EcosystemNarrowRules;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
/**
* A scoped redirector that only transforms classes from known-misbehaving (core)mod packages.
@@ -13,52 +11,20 @@ import org.objectweb.asm.tree.ClassNode;
* Those classes get missed by the late-registered {@link AngelicaRedirectorTransformer}, so their
* GL calls permanently bypass GLSM.
* <p>
* This transformer is registered in the {@code AngelicaTweaker} constructor (before any
* coremod's {@code injectData}) and is removed by {@code AngelicaLateTweaker} once the
* full redirector is in its proper post-mixin position.
* <p>
* The mixin service will add this to its delegation list, but it no-ops on game classes
* since it only targets specific mod packages.
* Registered in {@code AngelicaClientTweaker} constructor. Removed by {@code AngelicaLateTweaker}
* once the full redirector is in its proper post-mixin position.
*
* @see EcosystemNarrowRules#EARLY_REDIRECTOR_TARGETS
*/
public class EarlyRedirectorTransformer implements IClassTransformer {
private static final String[] TARGET_PACKAGES = {
"cn.tesseract.mycelium.",
};
private final AngelicaRedirector inner = new AngelicaRedirector();
private final String[] exclusions = inner.getTransformerExclusions();
private final EarlyRedirectorCore impl = new EarlyRedirectorCore(
"com.gtnewhorizons.angelica.lwjgl3.",
"com.gtnewhorizons.angelica.transform"
);
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
boolean targeted = false;
for (String pkg : TARGET_PACKAGES) {
if (transformedName.startsWith(pkg)) {
targeted = true;
break;
}
}
if (!targeted) return basicClass;
for (String exclusion : exclusions) {
if (transformedName.startsWith(exclusion)) return basicClass;
}
if (!inner.shouldTransform(basicClass)) {
return basicClass;
}
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
final boolean changed = inner.transformClassNode(transformedName, cn);
if (changed) {
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
}
return basicClass;
return impl.transform(transformedName, basicClass);
}
}
@@ -0,0 +1,98 @@
package com.gtnewhorizons.angelica.loading.fml.transformers;
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import net.minecraft.launchwrapper.IClassTransformer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
public class IsbrhTessellatorAbuseClassTransformer implements IClassTransformer {
public static final String ISBRH = "cpw/mods/fml/client/registry/ISimpleBlockRenderingHandler";
private static final Logger LOGGER = LogManager.getLogger("IsbrhTessellatorAbuse");
private static final ClassConstantPoolParser CST_POOL_PARSER = new ClassConstantPoolParser(ISBRH);
private static final String TESSELLATOR = "net/minecraft/client/renderer/Tessellator";
private static final String RENDER_WORLD_BLOCK_DESC = "(Lnet/minecraft/world/IBlockAccess;IIILnet/minecraft/block/Block;ILnet/minecraft/client/renderer/RenderBlocks;)Z";
private final String draw;
private final String startDrawingQuads;
private final String startDrawing;
public IsbrhTessellatorAbuseClassTransformer(boolean isObf) {
this.draw = isObf ? "func_78381_a" : "draw";
this.startDrawingQuads = isObf ? "func_78382_b" : "startDrawingQuads";
this.startDrawing = isObf ? "func_78371_b" : "startDrawing";
}
public boolean shouldTransform(byte[] classBytes) {
return CST_POOL_PARSER.find(classBytes);
}
public boolean transformClassNode(String transformedName, ClassNode cn) {
MethodNode target = null;
for (int i = 0, n = cn.methods.size(); i < n; i++) {
final MethodNode mn = cn.methods.get(i);
if ("renderWorldBlock".equals(mn.name) && RENDER_WORLD_BLOCK_DESC.equals(mn.desc)) {
target = mn;
break;
}
}
if (target == null) return false;
boolean changed = false;
AbstractInsnNode insn = target.instructions.getFirst();
while (insn != null) {
final AbstractInsnNode next = insn.getNext();
if (insn instanceof MethodInsnNode min && min.getOpcode() == Opcodes.INVOKEVIRTUAL && TESSELLATOR.equals(min.owner)) {
final int popOp;
final boolean pushIntResult;
if (min.name.equals(draw) && "()I".equals(min.desc)) {
popOp = Opcodes.POP;
pushIntResult = true;
} else if (min.name.equals(startDrawingQuads) && "()V".equals(min.desc)) {
popOp = Opcodes.POP;
pushIntResult = false;
} else if (min.name.equals(startDrawing) && "(I)V".equals(min.desc)) {
popOp = Opcodes.POP2;
pushIntResult = false;
} else {
insn = next;
continue;
}
target.instructions.insertBefore(insn, new InsnNode(popOp));
if (pushIntResult) target.instructions.insertBefore(insn, new InsnNode(Opcodes.ICONST_0));
target.instructions.remove(insn);
LOGGER.info("Stripped Tessellator.{} from {}#renderWorldBlock", min.name, cn.name);
changed = true;
}
insn = next;
}
return changed;
}
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (basicClass == null || !shouldTransform(basicClass)) return basicClass;
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
if (!transformClassNode(transformedName, cn)) return basicClass;
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
final byte[] bytes = cw.toByteArray();
AngelicaClassDump.dumpClass(transformedName, basicClass, bytes, this);
return bytes;
}
}
@@ -1,5 +1,9 @@
package com.gtnewhorizons.angelica.loading.fml.tweakers;
import com.gtnewhorizons.angelica.glsm.loading.EcosystemNarrowRules;
import com.gtnewhorizons.angelica.glsm.loading.TransformerNarrower;
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
import com.gtnewhorizons.angelica.loading.fml.transformers.IsbrhTessellatorAbuseClassTransformer;
import cpw.mods.fml.relauncher.FMLRelaunchLog;
import net.minecraft.launchwrapper.IClassTransformer;
import net.minecraft.launchwrapper.ITweaker;
@@ -27,8 +31,6 @@ public class AngelicaLateTweaker implements ITweaker {
/**
* Reduces overly broad transformer exclusions that prevent AngelicaRedirector from doing its job.
*
* Looking at you, DragonAPI.
*/
@SuppressWarnings("unchecked")
private static void narrowTransformerExclusions(LaunchClassLoader classLoader) {
@@ -37,73 +39,14 @@ public class AngelicaLateTweaker implements ITweaker {
exceptionsField.setAccessible(true);
final Set<String> exceptions = (Set<String>) exceptionsField.get(classLoader);
if (narrowEnabled("DragonAPI") && exceptions.remove("Reika.DragonAPI.ASM")) {
// Re-add non-GL classes; ASMCallsClient and utility classes with GL calls are left exposed
exceptions.add("Reika.DragonAPI.ASM.Patchers");
exceptions.add("Reika.DragonAPI.ASM.Profiling");
exceptions.add("Reika.DragonAPI.ASM.APIStripper");
exceptions.add("Reika.DragonAPI.ASM.ClassReparenter");
exceptions.add("Reika.DragonAPI.ASM.DependentMethodStripper");
exceptions.add("Reika.DragonAPI.ASM.DragonAPIClassTransformer");
exceptions.add("Reika.DragonAPI.ASM.FMLItemBlockPatch");
exceptions.add("Reika.DragonAPI.ASM.FluidNamePatch");
exceptions.add("Reika.DragonAPI.ASM.InterfaceInjector");
exceptions.add("Reika.DragonAPI.ASM.SpecialBiomePlacement");
exceptions.add("Reika.DragonAPI.ASM.StructureLootHooks");
FMLRelaunchLog.info("[Angelica] Narrowed Reika.DragonAPI.ASM transformer exclusion to allow GL redirection");
}
TransformerNarrower.narrow(exceptions, Launch.blackboard, "angelica", "Angelica",
EcosystemNarrowRules.ALL);
if (narrowEnabled("Xaeros")) {
if (exceptions.remove("xaero.common.core")) {
exceptions.add("xaero.common.core.transformer");
FMLRelaunchLog.info("[Angelica] Narrowed xaero.common.core transformer exclusion to allow GL redirection");
}
if (exceptions.remove("xaero.map.core")) {
exceptions.add("xaero.map.core.transformer");
FMLRelaunchLog.info("[Angelica] Narrowed xaero.map.core transformer exclusion to allow GL redirection");
}
}
if (narrowEnabled("AdvancedLightsabers") && exceptions.remove("com.fiskmods.lightsabers.asm")) {
exceptions.add("com.fiskmods.lightsabers.asm.ALLoadingPlugin");
exceptions.add("com.fiskmods.lightsabers.asm.ASMHooks");
exceptions.add("com.fiskmods.lightsabers.asm.transformers");
FMLRelaunchLog.info("[Angelica] Narrowed com.fiskmods.lightsabers.asm transformer exclusion to allow GL redirection");
}
if (narrowEnabled("Alfheim") && exceptions.remove("alfheim.common.core.asm.hook")) {
exceptions.add("alfheim.common.core.asm.hook.AlfheimFieldHookHandler");
exceptions.add("alfheim.common.core.asm.hook.AlfheimHPHooks");
exceptions.add("alfheim.common.core.asm.hook.Botania18AndUpBackport");
exceptions.add("alfheim.common.core.asm.hook.ElementalDamageAdapter");
exceptions.add("alfheim.common.core.asm.hook.integration");
exceptions.add("alfheim.common.core.asm.hook.extender.FurnaceExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.ItemAuraRingExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.ItemLensExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.ItemTwigWandExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.LightRelayExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.PureDaisyExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.QuartzExtender");
exceptions.add("alfheim.common.core.asm.hook.extender.RelicHooks");
exceptions.add("alfheim.common.core.asm.hook.extender.SparkExtender");
exceptions.add("alfheim.common.core.asm.hook.fixes.BotaniaGlowingRenderFixes");
exceptions.add("alfheim.common.core.asm.hook.fixes.CorporeaInputFix");
exceptions.add("alfheim.common.core.asm.hook.fixes.GodAttributesHooks");
exceptions.add("alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix");
FMLRelaunchLog.info("[Angelica] Narrowed alfheim.common.core.asm.hook transformer exclusion to allow GL redirection");
}
} catch (Exception e) {
FMLRelaunchLog.warning("[Angelica] Failed to narrow transformer exclusions: %s", e.getMessage());
}
}
private static boolean narrowEnabled(String mod) {
return !Boolean.FALSE.equals(Launch.blackboard.get("angelica.narrow." + mod));
}
@Override
public String getLaunchTarget() {
return null;
@@ -119,6 +62,10 @@ public class AngelicaLateTweaker implements ITweaker {
final List<IClassTransformer> transformers = (List<IClassTransformer>) transformersField.get(Launch.classLoader);
transformers.removeIf(t -> t.getClass().getName().equals(EARLY_REDIRECTOR_CLASS));
final IClassTransformer isbrhAbuse = new IsbrhTessellatorAbuseClassTransformer(AngelicaClientTweaker.isObfEnv());
FMLRelaunchLog.finer("Registering transformer %s", isbrhAbuse.getClass().getName());
transformers.add(isbrhAbuse);
} catch (Exception e) {
FMLRelaunchLog.warning("[Angelica] Failed to remove EarlyRedirectorTransformer: %s", e.getMessage());
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.loading.rfb;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBAngelicaRedirector;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBCeleritasBlockTransformer;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBIsbrhTessellatorAbuseTransformer;
import com.gtnewhorizons.retrofuturabootstrap.api.PluginContext;
import com.gtnewhorizons.retrofuturabootstrap.api.RetroFuturaBootstrap;
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
@@ -28,7 +29,8 @@ public class AngelicaRfbPlugin implements RfbPlugin {
final boolean isObf = RetroFuturaBootstrap.API.launchClassLoader().findClassMetadata("net.minecraft.world.World") == null;
return new RfbClassTransformer[] {
new RFBAngelicaRedirector(),
new RFBCeleritasBlockTransformer(isObf)
new RFBCeleritasBlockTransformer(isObf),
new RFBIsbrhTessellatorAbuseTransformer(isObf)
};
}
}
@@ -0,0 +1,56 @@
package com.gtnewhorizons.angelica.loading.rfb.transformers;
import com.gtnewhorizons.angelica.loading.fml.transformers.IsbrhTessellatorAbuseClassTransformer;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassHeaderMetadata;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
import org.intellij.lang.annotations.Pattern;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.jar.Manifest;
/** RfbClassTransformer wrapper for {@link IsbrhTessellatorAbuseClassTransformer} */
public class RFBIsbrhTessellatorAbuseTransformer implements RfbClassTransformer {
private final IsbrhTessellatorAbuseClassTransformer inner;
public RFBIsbrhTessellatorAbuseTransformer(boolean isObf) {
inner = new IsbrhTessellatorAbuseClassTransformer(isObf);
}
@Pattern("[a-z0-9-]+")
@Override
public @NotNull String id() {
return "isbrh-tessellator-abuse";
}
@Override
public @NotNull String @Nullable [] sortAfter() {
return new String[] {"*", "mixin:mixin"};
}
@Override
public @NotNull String @Nullable [] sortBefore() {
return new String[] {"lwjgl3ify:redirect"};
}
@Override
public boolean shouldTransformClass(@NotNull ExtensibleClassLoader classLoader,
@NotNull RfbClassTransformer.Context context, @Nullable Manifest manifest, @NotNull String className,
@NotNull ClassNodeHandle classNode) {
if (!classNode.isPresent()) return false;
final ClassHeaderMetadata metadata = classNode.getOriginalMetadata();
return metadata != null && metadata.binaryInterfaceNames.contains(IsbrhTessellatorAbuseClassTransformer.ISBRH);
}
@Override
public void transformClass(@NotNull ExtensibleClassLoader classLoader, @NotNull RfbClassTransformer.Context context,
@Nullable Manifest manifest, @NotNull String className, @NotNull ClassNodeHandle classNode) {
if (inner.transformClassNode(className, classNode.getNode())) {
classNode.computeMaxs();
AngelicaClassDump.dumpRFBClass(className, classNode, this);
}
}
}
@@ -35,11 +35,17 @@ public enum Mixins implements IMixins {
, "angelica.MixinGameSettings"
, "angelica.MixinMinecraft"
, "angelica.MixinMinecraft_FrameHook"
, "angelica.MixinMinecraft_IconifyGuard"
, "angelica.MixinMinecraftServer"
, "angelica.bugfixes.MixinItemRenderer_EdgeDepth"
, "angelica.bugfixes.MixinModelCreeper_AuraBodyInflate"
, "angelica.bugfixes.MixinModelSkeleton_LegPelvisZFight"
, "angelica.bugfixes.MixinModelWither_ArmorCentering"
, "angelica.bugfixes.MixinRenderBlocks_CrossedSquaresNormal"
, "angelica.bugfixes.MixinRenderCreeper_AuraDepth"
, "angelica.bugfixes.MixinRenderGlobal_DeferredEntityOverlay"
, "angelica.bugfixes.MixinRenderGlobal_DestroyBlock"
, "angelica.bugfixes.MixinRendererLivingEntity_DeferredEntityOverlay"
, "angelica.bugfixes.MixinRenderWither_ArmorCentering"
, "angelica.bugfixes.MixinRendererLivingEntity_EyeDepth"
, "angelica.debug.MixinMinecraft_FPSCap"
@@ -66,6 +72,12 @@ public enum Mixins implements IMixins {
.addClientMixins("angelica.gui.MixinGuiMainMenu")
),
ANGELICA_GL_SPLASH_TEXT(
new MixinBuilder("Rewrite 'OpenGL 1.2!' splash to reflect the actual GL context")
.setPhase(Phase.EARLY)
.addClientMixins("angelica.gui.MixinGuiMainMenuSplash")
),
ANGELICA_FONT_RENDERER(new MixinBuilder()
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableFontRenderer)
@@ -118,7 +130,10 @@ public enum Mixins implements IMixins {
ANGELICA_ITEM_RENDERER_OPTIMIZATION(new MixinBuilder("Optimizes in-world item rendering")
.setPhase(Phase.EARLY)
.addClientMixins("angelica.itemrenderer.MixinItemRenderer")
.addClientMixins(
"angelica.itemrenderer.MixinItemRenderer",
"angelica.itemrenderer.MixinRenderBlocks"
)
.setApplyIf(() -> AngelicaConfig.optimizeInWorldItemRendering)),
ANGELICA_OPTIMIZE_GLALLOCATION(new MixinBuilder("Replace HashMap with fastutil Int2IntMap in GLAllocation")
@@ -190,6 +205,7 @@ public enum Mixins implements IMixins {
, "rendering.MixinTileEntity"
, "rendering.MixinTileEntityMobSpawner"
, "rendering.MixinTileEntityRendererDispatcher"
, "rendering.MixinRenderBlocksEmissive"
)
),
@@ -240,6 +256,7 @@ public enum Mixins implements IMixins {
.setApplyIf(() -> AngelicaConfig.enableIris)
.addClientMixins(
"shaders.MixinDroppedItemGlintEdges"
, "shaders.MixinHeldItemGlintEdges"
, "shaders.MixinEntityPickupFX"
, "shaders.MixinEntityRenderer"
, "shaders.MixinGuiIngameForge"
@@ -252,6 +269,7 @@ public enum Mixins implements IMixins {
, "shaders.MixinRenderEntityFlame"
, "shaders.MixinRendererLivingEntity"
, "shaders.MixinRenderGlobal"
, "shaders.AccessorEntityHorse"
, "shaders.MixinRenderHorse"
, "shaders.MixinRenderItem"
, "shaders.MixinRenderNameTag"
@@ -281,6 +299,14 @@ public enum Mixins implements IMixins {
)
),
DRAGONAPI_SHADER_REGISTRY_PARSE_ERROR(new MixinBuilder()
.setPhase(Phase.EARLY)
.addRequiredMod(TargetedMod.DRAGON_API)
.addClientMixins(
"dragonapi.MixinShaderRegistry_ParseError"
)
),
IRIS_RENDERING_NOBACKHAND(new MixinBuilder("Iris Hand Shaders")
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableIris)
@@ -375,16 +401,15 @@ public enum Mixins implements IMixins {
.setApplyIf(() -> CompatConfig.fixMinefactoryReloaded)
.addClientMixins("client.minefactoryreloaded.MixinRedNetCableRenderer")),
NTM_SPACE_TWEAKS(new MixinBuilder("Support for 'Disable Horizon' & 'disableAltitudePlanetRenderer' options in NTM:Space")
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.NTM_SPACE)
.setApplyIf(() -> CompatConfig.tweakNTMSpace)
.addClientMixins("client.ntmSpace.MixinSkyProviderCelestial_Tweaks")),
NTM_SPACE_SHADER_COMPAT(new MixinBuilder("Multiple shader fixes for NTM:Space")
NTM_SPACE_COMPAT(new MixinBuilder("Multiple fixes for NTM:Space")
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.NTM_SPACE)
.setApplyIf(() -> CompatConfig.fixNTMSpace && AngelicaConfig.enableIris)
.addClientMixins("client.ntmSpace.MixinSkyProviderCelestial_ShaderCompat", "client.ntmSpace.MixinSkyProviderLaytheSunset")),
.addClientMixins(
"client.ntmSpace.MixinSkyProviderCelestial",
"client.ntmSpace.MixinSkyProviderOrbit",
"client.ntmSpace.MixinSkyProviderLaytheSunset"
)),
SPEEDUP_CAMPFIRE_BACKPORT_ANIMATIONS(new MixinBuilder("Add animation speedup support to Campfire Backport")
.setPhase(Phase.LATE)
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.proxy;
import biomesoplenty.api.content.BOPCBlocks;
import com.google.common.base.Objects;
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
@@ -21,6 +22,7 @@ import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
import com.gtnewhorizons.angelica.render.CloudRenderer;
import com.gtnewhorizons.angelica.render.EmissiveTextureAutoloader;
import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry;
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasSetup;
@@ -42,6 +44,7 @@ import cpw.mods.fml.relauncher.ReflectionHelper;
import jss.notfine.core.Settings;
import jss.notfine.gui.GuiCustomMenu;
import jss.notfine.gui.NotFineGameOptionPages;
import jss.notfine.gui.options.named.FOVMode;
import me.flashyreese.mods.reeses_sodium_options.client.gui.ReeseSodiumVideoOptionsScreen;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI;
@@ -85,6 +88,7 @@ public final class ClientProxy extends CommonProxy {
private final Minecraft mc = Minecraft.getMinecraft();
private final FrametimeGraph frametimeGraph = new FrametimeGraph();
private final TPSGraph tpsGraph = new TPSGraph();
public static Block bopGrass;
public static SodiumGameOptions options() {
if (CONFIG == null) {
@@ -99,30 +103,11 @@ public final class ClientProxy extends CommonProxy {
super.preInit(event);
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.register(new EmissiveTextureAutoloader());
ModelRegistry.registerModid(MOD_ID);
blockError = new BlockError();
}
@SubscribeEvent
public void worldLoad(WorldEvent.Load event) {
if (GLStateManager.isRunningSplash()) {
GLStateManager.setRunningSplash(false);
LOGGER.info("World loaded - Enabling GLSM Cache");
}
if (AngelicaConfig.enableCeleritas) {
ChunkTaskRegistry.reset();
ChunkTaskRegistry.registerProvider(DefaultChunkTaskProvider.INSTANCE);
ChunkTaskRegistry.registerProvider(ThreadedChunkTaskProvider.INSTANCE);
// Register all blocks. Because blockids are unique to a world, this must be done each load
GameData.getBlockRegistry().typeSafeIterable().forEach(o -> {
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, true, true);
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, false, true);
});
}
}
@Override
public void init(FMLInitializationEvent event) {
super.init(event);
@@ -167,7 +152,6 @@ public final class ClientProxy extends CommonProxy {
}
}
@Override
public void postInit(FMLPostInitializationEvent event) {
super.postInit(event);
@@ -182,13 +166,43 @@ public final class ClientProxy extends CommonProxy {
LOGGER.error("Could not replace LOTR handle render code with thread safe version");
}
}
if(ModStatus.isBOPLoaded) {
bopGrass = BOPCBlocks.newBopGrass;
}
}
@SubscribeEvent
public void worldLoad(WorldEvent.Load event) {
if (!event.world.isRemote) return;
if (GLStateManager.isRunningSplash()) {
GLStateManager.setRunningSplash(false);
LOGGER.info("World loaded - Enabling GLSM Cache");
}
if (AngelicaConfig.enableCeleritas) {
ChunkTaskRegistry.reset();
ChunkTaskRegistry.registerProvider(DefaultChunkTaskProvider.INSTANCE);
ChunkTaskRegistry.registerProvider(ThreadedChunkTaskProvider.INSTANCE);
// Register all blocks. Because blockids are unique to a world, this must be done each load
GameData.getBlockRegistry().typeSafeIterable().forEach(o -> {
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, true, true);
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, false, true);
});
}
}
@SubscribeEvent
public void onWorldUnload(WorldEvent.Unload event) {
if (!event.world.isRemote) return;
DynamicLights.get().removeAllLightSources();
}
float lastIntegratedTickTime;
@SubscribeEvent
public void onTick(TickEvent.ServerTickEvent event) {
if (FMLCommonHandler.instance().getSide().isClient() && event.phase == TickEvent.Phase.END) {
if (event.phase == TickEvent.Phase.END) {
final IntegratedServer srv = Minecraft.getMinecraft().getIntegratedServer();
if (srv != null) {
final long currentTickTime = srv.tickTimeArray[srv.getTickCounter() % 100];
@@ -337,10 +351,19 @@ public final class ClientProxy extends CommonProxy {
}
}
// This is a bit of a hack to prevent the FOV from being modified by other mods
// This only disables FOV changes from vanilla, leaving mod FOV changes untouched
@SubscribeEvent(priority = EventPriority.HIGHEST)
public void onFOVModifierUpdateEarly(FOVUpdateEvent event) {
if(Settings.DYNAMIC_FOV.option.getStore() == FOVMode.MODS) {
event.fov = 1.0F;
event.newfov = 1.0F;
}
}
// This removes all FOV dynamics, both from vanilla (speed/sprinting), and mods
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onFOVModifierUpdate(FOVUpdateEvent event) {
if (!(boolean) Settings.DYNAMIC_FOV.option.getStore()) {
public void onFOVModifierUpdateLate(FOVUpdateEvent event) {
if(Settings.DYNAMIC_FOV.option.getStore() == FOVMode.NONE) {
event.newfov = 1.0F;
}
}
@@ -0,0 +1,60 @@
package com.gtnewhorizons.angelica.render;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.IIcon;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.event.TextureStitchEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@SideOnly(Side.CLIENT)
public class EmissiveTextureAutoloader {
@SubscribeEvent
public void onTextureStitchPre(TextureStitchEvent.Pre event) {
if (event.map.getTextureType() != 0) return;
TextureMap map = event.map;
EmissiveTextureHelper.clearCache();
@SuppressWarnings("unchecked")
Map<String, ?> registeredSprites = map.mapRegisteredSprites;
List<String> toRegister = new ArrayList<>();
for (String baseName : registeredSprites.keySet()) {
if (baseName.endsWith("_emissive")) continue;
String emissiveName = baseName + "_emissive";
if (resourceExists(emissiveName)) {
toRegister.add(baseName);
}
}
for (String baseName : toRegister) {
String emissiveName = baseName + "_emissive";
IIcon emissiveIcon = map.registerIcon(emissiveName);
EmissiveTextureHelper.cacheEmissive(baseName, emissiveIcon);
}
}
private boolean resourceExists(String iconName) {
ResourceLocation loc = new ResourceLocation(iconName);
ResourceLocation full = new ResourceLocation(
loc.getResourceDomain(),
"textures/blocks/" + loc.getResourcePath() + ".png"
);
try {
Minecraft.getMinecraft().getResourceManager().getResource(full);
return true;
} catch (Exception ignored) {
return false;
}
}
}
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.render;
import net.minecraft.block.Block;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.util.IIcon;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import java.util.concurrent.ConcurrentHashMap;
@SideOnly(Side.CLIENT)
public final class EmissiveTextureHelper {
private static final ConcurrentHashMap<String, IIcon> EMISSIVE_ICON_CACHE = new ConcurrentHashMap<>();
public static IIcon getEmissiveIcon(Block block, int side, int metadata) {
IIcon baseIcon = block.getIcon(side, metadata);
if (baseIcon == null) return null;
String baseIconName = baseIcon.getIconName();
if (baseIconName == null) return null;
return EMISSIVE_ICON_CACHE.get(baseIconName);
}
public static void clearCache() {
EMISSIVE_ICON_CACHE.clear();
}
public static void cacheEmissive(String baseIconName, IIcon emissiveIcon) {
EMISSIVE_ICON_CACHE.put(baseIconName, emissiveIcon);
}
public static void renderEmissiveBlockOverlay(RenderBlocks renderer, Block block, int x, int y, int z, int metadata) {
Tessellator tess = Tessellator.instance;
for (int side = 0; side < 6; side++) {
IIcon icon = getEmissiveIcon(block, side, metadata);
if (icon == null) continue;
tess.setColorOpaque_F(1f, 1f, 1f);
switch (side) {
case 0 -> renderer.renderFaceYNeg(block, x, y, z, icon);
case 1 -> renderer.renderFaceYPos(block, x, y, z, icon);
case 2 -> renderer.renderFaceZNeg(block, x, y, z, icon);
case 3 -> renderer.renderFaceZPos(block, x, y, z, icon);
case 4 -> renderer.renderFaceXNeg(block, x, y, z, icon);
case 5 -> renderer.renderFaceXPos(block, x, y, z, icon);
}
}
}
}
@@ -0,0 +1,215 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import lombok.Getter;
import net.coderbot.iris.layer.GbufferPrograms;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.coderbot.iris.uniforms.EntityIdHelper;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.entity.RendererLivingEntity;
import net.minecraft.entity.EntityLivingBase;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Defers entity overlay rendering (auras, armor effects, etc.) to after all entities have been drawn.
* This lets us fix z-fighting between overlay's own coplanar faces by disabling the depth mask,
* and ensures the overlay composites correctly on top of all opaque geometry.
*
* Used by both the charged creeper aura and the Wither armor overlay.
*
* The deferred render replays shouldRenderPass on the original renderer instance so that any
* mod mixins targeting that method still fire during the deferred pass.
*/
public class DeferredEntityOverlay {
@FunctionalInterface
public interface ShouldRenderPassFn {
int invoke(EntityLivingBase entity, int pass, float partialTick);
}
private static final int INITIAL_CAPACITY = 4;
private static final List<DeferredEntry> deferred = new ArrayList<>(INITIAL_CAPACITY);
private static final List<DeferredEntry> pool = new ArrayList<>(INITIAL_CAPACITY);
private static final FloatBuffer MATRIX_BUF = BufferUtils.createFloatBuffer(16);
// Set by the shouldRenderPass inject, consumed by the doRender WrapOperation.
@Getter
private static boolean overlayPassActive = false;
// Set during deferred flush to prevent re-deferring when shouldRenderPass is replayed.
@Getter
private static boolean replaying = false;
// Pending context from the shouldRenderPass inject, consumed by deferRender.
private static ShouldRenderPassFn pendingShouldRenderPass;
private static RendererLivingEntity pendingRenderer;
private static EntityLivingBase pendingEntity;
private static float pendingPartialTick;
/**
* Called from shouldRenderPass HEAD injects. Marks that the upcoming renderPassModel.render()
* should be deferred. If a previous mark was not consumed (shouldRenderPass returned <= 0,
* so render() was never called).
*/
public static void markOverlayPass(ShouldRenderPassFn shouldRenderPass, RendererLivingEntity renderer,
EntityLivingBase entity, float partialTick) {
overlayPassActive = true;
pendingShouldRenderPass = shouldRenderPass;
pendingRenderer = renderer;
pendingEntity = entity;
pendingPartialTick = partialTick;
}
public static void clearStaleOverlayFlag() {
overlayPassActive = false;
}
/**
* Called from the WrapOperation on renderPassModel.render() in doRender.
* Captures the modelview matrix and animation params, skipping the actual render.
*/
public static void deferRender(float limbSwing, float limbSwingAmount, float ageInTicks,
float headYaw, float headPitch, float scale) {
overlayPassActive = false;
DeferredEntry entry = acquireEntry();
entry.set(pendingShouldRenderPass, pendingRenderer, pendingEntity, pendingPartialTick,
limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, scale);
MATRIX_BUF.clear();
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, MATRIX_BUF);
MATRIX_BUF.get(entry.matrix);
deferred.add(entry);
pendingShouldRenderPass = null;
pendingRenderer = null;
pendingEntity = null;
}
private static DeferredEntry acquireEntry() {
int size = pool.size();
if (size > 0) {
return pool.remove(size - 1);
}
return new DeferredEntry();
}
private static void recycle() {
for (DeferredEntry entry : deferred) {
entry.shouldRenderPass = null;
entry.renderer = null;
entry.entity = null;
pool.add(entry);
}
deferred.clear();
}
public static void clear() {
recycle();
overlayPassActive = false;
replaying = false;
}
public static void renderAll() {
if (deferred.isEmpty()) return;
GbufferPrograms.beginEntities();
try {
for (DeferredEntry entry : deferred) {
CapturedRenderingState.INSTANCE.setCurrentEntity(EntityIdHelper.getEntityId(entry.entity));
renderOverlay(entry);
}
} finally {
CapturedRenderingState.INSTANCE.setCurrentEntity(-1);
GbufferPrograms.endEntities();
}
recycle();
}
private static void renderOverlay(DeferredEntry entry) {
EntityLivingBase entity = entry.entity;
// Restore the entity's lightmap coords
int brightness = entity.getBrightnessForRender(entry.partialTick);
OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit,
brightness % 65536, (float) brightness / 65536);
// Restore the saved modelview matrix
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
boolean depthMaskDisabled = false;
try {
MATRIX_BUF.clear();
MATRIX_BUF.put(entry.matrix);
MATRIX_BUF.flip();
GLStateManager.glLoadMatrix(MATRIX_BUF);
// Replay shouldRenderPass pass 1
replaying = true;
int result = entry.shouldRenderPass.invoke(entity, 1, entry.partialTick);
if (result > 0) {
// Disable depth writes so coplanar faces don't z-fight each other
GLStateManager.glDepthMask(false);
depthMaskDisabled = true;
// Disable backface culling so both sides of the overlay shell are visible
GLStateManager.glDisable(GL11.GL_CULL_FACE);
// Render using the model set by shouldRenderPass
entry.renderer.renderPassModel.setLivingAnimations(entity,
entry.limbSwing, entry.limbSwingAmount, entry.partialTick);
entry.renderer.renderPassModel.render(entity,
entry.limbSwing, entry.limbSwingAmount, entry.ageInTicks,
entry.headYaw, entry.headPitch, entry.scale);
}
// Replay shouldRenderPass pass 2
entry.shouldRenderPass.invoke(entity, 2, entry.partialTick);
} finally {
replaying = false;
if (depthMaskDisabled) {
GLStateManager.glDepthMask(true);
GLStateManager.glEnable(GL11.GL_CULL_FACE);
}
GLStateManager.glPopMatrix();
}
}
private static class DeferredEntry {
final float[] matrix = new float[16];
ShouldRenderPassFn shouldRenderPass;
RendererLivingEntity renderer;
EntityLivingBase entity;
float partialTick;
float limbSwing;
float limbSwingAmount;
float ageInTicks;
float headYaw;
float headPitch;
float scale;
void set(ShouldRenderPassFn shouldRenderPass, RendererLivingEntity renderer,
EntityLivingBase entity, float partialTick,
float limbSwing, float limbSwingAmount, float ageInTicks,
float headYaw, float headPitch, float scale) {
this.shouldRenderPass = shouldRenderPass;
this.renderer = renderer;
this.entity = entity;
this.partialTick = partialTick;
this.limbSwing = limbSwing;
this.limbSwingAmount = limbSwingAmount;
this.ageInTicks = ageInTicks;
this.headYaw = headYaw;
this.headPitch = headPitch;
this.scale = scale;
}
}
}
@@ -0,0 +1,31 @@
package com.gtnewhorizons.angelica.rendering;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.ModelHorse;
import net.minecraft.client.model.ModelRenderer;
import java.util.ArrayList;
import java.util.List;
/**
* Horse model with all boxes inflated outward, used as a separate armor layer.
* Extends ModelHorse to inherit all animation and render logic. The constructor
* builds the default geometry, then we rebuild every box with inflate applied.
* This mirrors how modern MC uses a separate ModelLayers.HORSE_ARMOR.
*/
public class ModelHorseArmor extends ModelHorse {
public ModelHorseArmor(float inflate) {
super();
for (ModelRenderer renderer : this.boxList) {
List<ModelBox> originalBoxes = new ArrayList<>(renderer.cubeList);
renderer.cubeList.clear();
for (ModelBox box : originalBoxes) {
int width = Math.round(box.posX2 - box.posX1);
int height = Math.round(box.posY2 - box.posY1);
int depth = Math.round(box.posZ2 - box.posZ1);
renderer.addBox(box.posX1, box.posY1, box.posZ1, width, height, depth, inflate);
}
}
}
}
@@ -16,4 +16,8 @@ public interface StateAwareTessellator {
void angelica$setCeleritasMeshing(boolean active);
int[] angelica$getVertexStates();
int[] angelica$getShaderOverrideBlockIds();
void angelica$setShaderOverrideBlockId(short blockId);
}
@@ -34,11 +34,13 @@ public final class TileEntityRenderBoundsRegistry {
}
private static boolean probeAndCache(TileEntity te, Class<? extends TileEntity> clazz) {
boolean isInfinite = false;
boolean isInfinite;
try {
final AxisAlignedBB aabb = te.getRenderBoundingBox();
isInfinite = isInfiniteExtentsBox(aabb);
} catch (Exception ignored) {}
} catch (Throwable t) {
isInfinite = true;
}
synchronized (classRegistry) {
classRegistry.put(clazz, isInfinite ? INFINITE : FINITE);
@@ -115,6 +115,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
@SuppressWarnings("unchecked")
public void copyRawBuffer(int[] rawBuffer, int vertexCount, int[] vertexStates,
int[] shaderOverrideBlockIds,
ChunkBuildBuffers buffers, Material material, boolean isShaderPackOverride,
boolean blockAllowsSmoothLighting) {
if (vertexCount == 0) {
@@ -145,6 +146,8 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
final int worldY = originY + blockY;
final int worldZ = originZ + blockZ;
final short originalBlockId = blockRenderContext.blockId;
int stateIdx = 0;
int facesAtBaseMaterial = 0;
@@ -239,6 +242,9 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
}
final var builder = buffers.get(correctMaterial);
final int shaderOverrideBlockId = shaderOverrideBlockIds[quadIdx * 4];
blockRenderContext.blockId = shaderOverrideBlockId != -1 ? (short) shaderOverrideBlockId : originalBlockId;
if (correctMaterial != material && builder.getEncoder() instanceof IrisExtendedChunkVertexEncoder iris) {
iris.setContext(blockRenderContext);
}
@@ -8,7 +8,10 @@ import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import com.gtnewhorizons.angelica.rendering.celeritas.iris.BlockRenderContext;
import com.gtnewhorizons.angelica.rendering.celeritas.iris.ContextAwareChunkVertexEncoder;
import com.prupe.mcpatcher.mal.block.RenderBlocksUtils;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
import net.coderbot.iris.block_rendering.BlockMaterialMapping;
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
import net.coderbot.iris.vertices.ExtendedDataHelper;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
@@ -20,6 +23,7 @@ import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.init.Blocks;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.ReportedException;
import net.minecraft.world.IBlockAccess;
import org.embeddedt.embeddium.impl.render.chunk.RenderSection;
@@ -139,7 +143,25 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
if (block.hasTileEntity(meta)) {
final TileEntity tileEntity = region.getTileEntity(x, y, z);
if (tileEntity != null && TileEntityRendererDispatcher.instance.hasSpecialRenderer(tileEntity)) {
(TileEntityRenderBoundsRegistry.isAlwaysInfiniteExtent(tileEntity) ? renderData.globalBlockEntities : renderData.culledBlockEntities).add(tileEntity);
final boolean isGlobal;
if (TileEntityRenderBoundsRegistry.isAlwaysInfiniteExtent(tileEntity)) {
isGlobal = true;
} else {
AxisAlignedBB aabb;
try {
aabb = tileEntity.getRenderBoundingBox();
} catch (Throwable t) {
aabb = null;
}
if (aabb != null) {
final int secMinX = x & ~15, secMinY = y & ~15, secMinZ = z & ~15;
isGlobal = aabb.minX < secMinX || aabb.minY < secMinY || aabb.minZ < secMinZ
|| aabb.maxX > secMinX + 16 || aabb.maxY > secMinY + 16 || aabb.maxZ > secMinZ + 16;
} else {
isGlobal = true;
}
}
(isGlobal ? renderData.globalBlockEntities : renderData.culledBlockEntities).add(tileEntity);
}
}
@@ -242,7 +264,13 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
if (isFluid) {
contextEncoder.prepareToRenderFluid(blockRenderContext, block, lightValue);
} else {
contextEncoder.prepareToRenderBlock(blockRenderContext, block, metadata,
int effectiveMeta = metadata;
if (BlockRenderingSettings.INSTANCE.hasSnowyEntries()
&& BlockRenderingSettings.INSTANCE.getSnowyBlocks().contains(block)
&& RenderBlocksUtils.isSnowCovered(renderBlocks.blockAccess, x, y, z)) {
effectiveMeta |= BlockMaterialMapping.SNOWY_META_BIT;
}
contextEncoder.prepareToRenderBlock(blockRenderContext, block, effectiveMeta,
ExtendedDataHelper.BLOCK_RENDER_TYPE, lightValue);
}
}
@@ -256,6 +284,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
&& block.getLightValue() == 0; // does not emit real block light
buildContext.copyRawBuffer(tessellator.rawBuffer, tessellator.vertexCount,
((StateAwareTessellator)tessellator).angelica$getVertexStates(),
((StateAwareTessellator)tessellator).angelica$getShaderOverrideBlockIds(),
buffers, passMaterial, isShaderPackOverride, blockAllowsSmoothLighting);
tessellator.reset();
tessellator.isDrawing = false;
@@ -6,7 +6,7 @@ import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.client.rendering.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
@@ -52,10 +52,10 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 15;
// For sorting transparent TESRs
private RenderSectionOrderer renderSectionOrderer = new RenderSectionOrderer();
private TileEntityOrderer tileEntityOrderer = new TileEntityOrderer();
private ArrayList<RenderSection> sortedRenderSections = new ArrayList<>();
private ArrayList<TileEntity> sortedTileEntities = new ArrayList<>();
private final RenderSectionOrderer renderSectionOrderer = new RenderSectionOrderer();
private final TileEntityOrderer tileEntityOrderer = new TileEntityOrderer();
private final ArrayList<RenderSection> sortedRenderSections = new ArrayList<>();
private final ArrayList<TileEntity> sortedTileEntities = new ArrayList<>();
private CeleritasWorldRenderer(Minecraft mc) {
// Private constructor for singleton
@@ -92,6 +92,10 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
@Override
protected void unloadWorld() {
DynamicLights.setActiveRenderer(null);
this.sortedRenderSections.clear();
this.sortedTileEntities.clear();
ShadowRenderer.visibleTileEntities.clear();
ShadowRenderer.globalTileEntities.clear();
super.unloadWorld();
}
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more