Compare commits

...
18 Commits
Author SHA1 Message Date
Jason Mitchell d7d2228a6c Target OpenGL 3.3 core profile (#1412) 2026-03-02 22:18:11 -08:00
Alexdoru cf9993fce4 change rfb exclusions (#1440) 2026-03-02 15:11:34 -08:00
Alexdoru 3ae57ca594 Fixes for backhand compat (#1437) 2026-03-01 13:04:11 -08:00
sisyphussyandMustafarYasmin 9816cb2148 Replace framebufferRender with a VBO (#1432)
Co-authored-by: MustafarYasmin <[email protected]>
2026-03-01 13:03:27 -08:00
Alexdoru b1e496f56d remove boxing unboxing in RenderPassHelper 2026-03-01 13:02:04 -08:00
danyadevandAlexdoru c8e50227fb Speedup asm redirector transformers and fix missing classes in AngelicaRedirector (#1433)
* add shouldTransform & make it significantly faster

* do not optimize non-FRB CeleritasBlockTransformer

* move closer to usage

---------

Co-authored-by: Alexdoru <[email protected]>
2026-02-28 20:12:29 +01:00
Jason Mitchell 066381a7c0 Stop demoting, per face, once we see a non demoted material 2026-02-27 21:29:20 -08:00
Jason Mitchell 9a0ea1ad65 Workaround apparent JVM 25 bug... 🤣 2026-02-27 21:29:20 -08:00
Alexdoru 8e26a0179b Speedup block transformer by transforming every class directly (#1420)
* speedup block transformer by transforming every class directly

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

* use map for block fields redirect
2026-02-23 20:20:38 +01:00
Jason Mitchell 7dc2625f8c Maybe less leaky (#1414) 2026-02-23 08:57:21 -08:00
Jason Mitchell 3d97977050 Maybe fix holo inventory again - v2 - final.xlsx (#1413) 2026-02-22 20:37:08 -08:00
Eclipse-Sol 5f41c60bc2 Remove force blend as commit 0325a96b fixed this properly. (#1409) 2026-02-22 11:35:35 -08:00
Jason Mitchell 0325a96bc2 Add unknown blend state and BufferBlendOverride handling from modern Iris (#1408) 2026-02-21 15:27:22 -08:00
Caedis ad123d0bd5 Now with 200% more screen shake (#1402) 2026-02-18 09:20:47 -05:00
Caedis 4b89997985 Add slider for hurt screen shake intensity (#1401) 2026-02-18 04:23:57 +01:00
179 changed files with 12354 additions and 1937 deletions

No files matched your search

+15 -2
View File
@@ -12,6 +12,14 @@
* Complementary + Euphoria Patches is the primary compatibility target
* Sildurs, Seus, and any other shaders might work
## Core Profile
Targets GL 3.3+ core profile
* FFP emulation via generated GLSL shaders
* Display list emulation
* Software matrix stacks and state management through GLSM
* GL_QUADS → GL_TRIANGLES conversion
* Automatic transformation of mod shaders that use compat-only GLSL builtins (`gl_ModelViewMatrix`, `texture2D`, etc.)
# Required Dependencies
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.23
@@ -56,9 +64,9 @@
* The [Iris](https://github.com/IrisShaders/Iris) Team: coderbot, IMS212, Justsnoopy30, FoundationGames
* Asek3 for the Iris Forge port [Oculus](https://github.com/Asek3/Oculus/tree/839ce8eca8cf0c4f6b7a1322b906e5c99125ca69)
* Backported code under the LGPL
* Sodium
* Sodium / Celeritas
* JellySquid & the CaffeineMC team, for making Sodium in the first place
* Embeddedt for [Embeddium](https://github.com/embeddedt/embeddium/tree/dc59ca357c25beefd6288f0d1d40b4cd8e670ab8) under the [LGPL 3.0](https://github.com/embeddedt/embeddium/blob/16.x/forge/LICENSE)
* Embeddedt for Celeritas used under the LGPL 3.0
* Reese's Sodium Options
* FlashyReese for [Reese's Sodium Options](https://github.com/FlashyReese/reeses-sodium-options/tree/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b) under the [MIT license](https://github.com/FlashyReese/reeses-sodium-options/blob/aa4ea67047cf53e54976e93c6bb1ab5ba16b142b/LICENSE.txt)
* Neodymium
@@ -85,3 +93,8 @@
* FalsePattern for [FalseTweaks](https://github.com/falsepattern/falsetweaks) - Item Rendering Optimizations taken from FalseTweaks under the LGPL 3.0 license
* Beddium
* Venn and Falsepattern for [Beddium](https://github.com/vfx-dev/Beddium) - MinimapRenderer, SafeChunkTracker, and some debug commands adapted under the LGPL 3.0 License
* [Mesa](https://gitlab.freedesktop.org/mesa/mesa) — Code ported and/or inspired from Mesa - under the MIT licensed
* Display list ID allocator ported from [util_idalloc](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/util/u_idalloc.c) (Copyright 2017 Valve Corporation)
* FFP vertex shader generation inspired by ffvertex_prog.c (Copyright 2007 VMware, Inc.)
* FFP fragment shader generation inspired by ff_fragment_shader.c (Copyright 2007, 2009 VMware, Inc. / 2010-2011 Intel Corporation)
* Fog parameter optimization inspired by st_nir_lower_fog.c (Copyright 2023 Google LLC) and prog_statevars.c (Copyright 1999-2007 Brian Paul)
+14
View File
@@ -1,3 +1,5 @@
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
plugins {
id("com.gtnewhorizons.gtnhconvention")
}
@@ -45,6 +47,18 @@ tasks.register<Copy>("copyDependencies") {
val embedOnly: Configuration by configurations
// Downgrade embedOnly jars for the tests
val downgradeEmbedOnlyForTest by tasks.registering(DowngradeFiles::class) {
inputCollection = embedOnly
}
tasks.test {
dependsOn(downgradeEmbedOnlyForTest)
classpath = classpath
.minus(embedOnly)
.plus(files(downgradeEmbedOnlyForTest.map { it.outputCollection }))
}
tasks.shadowJar {
from(embedOnly.map(::zipTree))
relocate("com.mitchej123", "com.mitchej123")
+4 -4
View File
@@ -53,6 +53,7 @@ configurations {
embedOnly.canBeResolved = true
compileClasspath.extendsFrom(embedOnly)
testCompileClasspath.extendsFrom(embedOnly)
testRuntimeClasspath.extendsFrom(embedOnly)
}
@@ -71,7 +72,7 @@ dependencies {
// Iris Shaders
compileOnly('org.jetbrains:annotations:26.0.2')
api("com.github.GTNewHorizons:GTNHLib:0.9.10:dev")
api("com.github.GTNewHorizons:GTNHLib:0.9.33:dev")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
@@ -117,9 +118,8 @@ dependencies {
compileOnly(rfg.deobf("curse.maven:security-craft-64760:2818228"))
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
// runtimeOnlyNonPublishable("com.github.GTNewHorizons:CoreTweaks:0.3.3.6-GTNH")
// For testing alternative splash screens, can be sourced from https://github.com/MalTeeez/ModernSplash/releases
//runtimeOnly(rfg.deobf(files("dependencies/modernsplash-1.7.10-1.2.2-dev.jar")))
// For testing alternative splash screens — https://github.com/MalTeeez/ModernSplash/releases
// devOnlyNonPublishable("com.github.MalTeeez:modernsplash:1.7.10-1.2.4:dev")
// Hodgepodge
transformedMod("net.industrial-craft:industrialcraft-2:2.2.828-experimental:dev")
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+13
View File
@@ -22,5 +22,18 @@ repositories {
includeGroup 'org.taumc'
}
}
ivy {
name = 'modernsplash-github'
url = 'https://github.com/MalTeeez/ModernSplash/releases/download/'
patternLayout {
artifact '[revision]/[module]-[revision](-[classifier]).[ext]'
}
content {
includeModule 'com.github.MalTeeez', 'modernsplash'
}
metadataSources {
artifact()
}
}
mavenLocal()
}
+1 -1
View File
@@ -16,5 +16,5 @@ pluginManagement {
}
plugins {
id("com.gtnewhorizons.gtnhsettingsconvention") version("2.0.19")
id("com.gtnewhorizons.gtnhsettingsconvention") version("2.0.20")
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
@@ -12,8 +13,9 @@ import net.minecraft.client.gui.FontRenderer;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.IOUtils;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import java.io.IOException;
@@ -24,7 +26,13 @@ import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Objects;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
/**
* A batching replacement for {@code FontRenderer}
@@ -49,8 +57,11 @@ public class BatchingFontRenderer {
private final int AAMode;
private final int AAStrength;
private final int texBoundAttrLocation;
private final int mvpMatrixLocation;
private final int fontShaderId;
private static int fontVAO = 0;
private static int vboPositions, vboColors, vboTexCoords, vboTexBounds, vboIndices;
private static int capPositions, capColors, capTexCoords, capTexBounds, capIndices;
final boolean isSGA;
final boolean isSplash;
@@ -95,7 +106,7 @@ public class BatchingFontRenderer {
fontShaderId = FontAAShader.getProgram().getProgramId();
AAMode = GL20.glGetUniformLocation(fontShaderId, "aaMode");
AAStrength = GL20.glGetUniformLocation(fontShaderId, "strength");
texBoundAttrLocation = GL20.glGetAttribLocation(fontShaderId, "texBounds");
mvpMatrixLocation = GL20.glGetUniformLocation(fontShaderId, "u_MVPMatrix");
}
// === Batched rendering
@@ -104,7 +115,7 @@ public class BatchingFontRenderer {
private int vtxWriterIndex = 0;
private int idxWriterIndex = 0;
private static final int INITIAL_BATCH_SIZE = 256;
private static final int INITIAL_BATCH_SIZE = 2048;
private static final ResourceLocation DUMMY_RESOURCE_LOCATION = new ResourceLocation("angelica$dummy",
"this is invalid!");
private FloatBuffer batchVtxPositions = memAllocFloat(INITIAL_BATCH_SIZE * 2);
@@ -271,9 +282,21 @@ public class BatchingFontRenderer {
}
}
int lastActiveProgram;
int fontAAModeLast = -1;
int fontAAStrengthLast = -1;
private static int streamUpload(int target, int vbo, long address, int bytes, int cap) {
GL15.glBindBuffer(target, vbo);
final ByteBuffer data = memByteBuffer(address, bytes);
if (bytes > cap) {
GL15.glBufferData(target, data, GL15.GL_STREAM_DRAW);
return bytes;
}
GL15.glBufferData(target, cap, GL15.GL_STREAM_DRAW);
GL15.glBufferSubData(target, 0, data);
return cap;
}
private static final Matrix4f scratchMvp = new Matrix4f();
private int fontAAModeLast = -1;
private int fontAAStrengthLast = -1;
private void flushBatch() {
final int prevProgram = GLStateManager.glGetInteger(GL20.GL_CURRENT_PROGRAM);
@@ -291,11 +314,8 @@ public class BatchingFontRenderer {
GLStateManager.tryBlendFuncSeparate(blendSrcRGB, blendDstRGB, GL11.GL_ONE, GL11.GL_ZERO);
GLStateManager.glShadeModel(GL11.GL_FLAT);
final boolean canUseAA = FontConfig.fontAAMode != 0 && prevProgram == 0;
if (canUseAA) {
GL20.glVertexAttribPointer(texBoundAttrLocation, 4, false, 0, batchVtxTexBounds);
GL20.glEnableVertexAttribArray(texBoundAttrLocation);
lastActiveProgram = prevProgram;
final boolean useFontShader = prevProgram == 0;
if (useFontShader) {
GLStateManager.glUseProgram(fontShaderId);
if (FontConfig.fontAAMode != fontAAModeLast) {
fontAAModeLast = FontConfig.fontAAMode;
@@ -305,19 +325,56 @@ public class BatchingFontRenderer {
fontAAStrengthLast = FontConfig.fontAAStrength;
GL20.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
}
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), scratchMvp);
scratchMvp.get(mvpBuf);
GL20.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
}
}
GL13.glClientActiveTexture(GL13.GL_TEXTURE0);
GL11.glTexCoordPointer(2, 0, batchVtxTexCoords);
GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
GL11.glColorPointer(4, GL11.GL_UNSIGNED_BYTE, 0, batchVtxColors);
GL11.glEnableClientState(GL11.GL_COLOR_ARRAY);
GL11.glVertexPointer(2, 0, batchVtxPositions);
GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY);
if (fontVAO == 0) {
fontVAO = org.lwjgl.opengl.GL30.glGenVertexArrays();
vboPositions = GL15.glGenBuffers();
vboColors = GL15.glGenBuffers();
vboTexCoords = GL15.glGenBuffers();
vboTexBounds = GL15.glGenBuffers();
vboIndices = GL15.glGenBuffers();
}
GLStateManager.glBindVertexArray(fontVAO);
final int vtxCount = vtxWriterIndex;
// Location 0: position (vec2, float)
batchVtxPositions.position(0).limit(vtxCount * 2);
capPositions = streamUpload(GL15.GL_ARRAY_BUFFER, vboPositions, memAddress(batchVtxPositions), batchVtxPositions.remaining() * 4, capPositions);
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(0);
// Location 1: color (vec4, unsigned byte normalized)
batchVtxColors.position(0).limit(vtxCount * 4);
capColors = streamUpload(GL15.GL_ARRAY_BUFFER, vboColors, memAddress(batchVtxColors), batchVtxColors.remaining(), capColors);
GL20.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, 0, 0L);
GL20.glEnableVertexAttribArray(1);
// Location 2: texcoord0 (vec2, float)
batchVtxTexCoords.position(0).limit(vtxCount * 2);
capTexCoords = streamUpload(GL15.GL_ARRAY_BUFFER, vboTexCoords, memAddress(batchVtxTexCoords), batchVtxTexCoords.remaining() * 4, capTexCoords);
GL20.glVertexAttribPointer(2, 2, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(2);
// Location 3: texbounds (vec4, float)
batchVtxTexBounds.position(0).limit(vtxCount * 4);
capTexBounds = streamUpload(GL15.GL_ARRAY_BUFFER, vboTexBounds, memAddress(batchVtxTexBounds), batchVtxTexBounds.remaining() * 4, capTexBounds);
GL20.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(3);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
GL11.glDisableClientState(GL11.GL_NORMAL_ARRAY);
GL11.glNormal3f(0.0f, 0.0f, 1.0f);
// Stream upload index buffer
batchIndices.position(0).limit(idxWriterIndex);
capIndices = streamUpload(GL15.GL_ELEMENT_ARRAY_BUFFER, vboIndices, memAddress(batchIndices), batchIndices.remaining() * 4, capIndices);
// Use plain for loop to avoid allocations
final FontDrawCmd[] cmdsData = batchCommands.elements();
@@ -336,19 +393,14 @@ public class BatchingFontRenderer {
}
lastTexture = cmd.texture;
}
batchIndices.limit(cmd.startVtx + cmd.idxCount);
batchIndices.position(cmd.startVtx);
GL11.glDrawElements(GL11.GL_TRIANGLES, batchIndices);
GL11.glDrawElements(GL11.GL_TRIANGLES, cmd.idxCount, GL11.GL_UNSIGNED_INT, (long) cmd.startVtx * 4L);
}
if (canUseAA) {
GLStateManager.glUseProgram(lastActiveProgram);
GL20.glDisableVertexAttribArray(texBoundAttrLocation);
if (useFontShader) {
GLStateManager.glUseProgram(prevProgram);
}
GL11.glDisableClientState(GL11.GL_TEXTURE_COORD_ARRAY);
GL11.glDisableClientState(GL11.GL_COLOR_ARRAY);
GL11.glDisableClientState(GL11.GL_VERTEX_ARRAY);
GLStateManager.glBindVertexArray(0);
if (isTextureEnabledBefore) {
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
@@ -362,8 +414,11 @@ public class BatchingFontRenderer {
batchCommands.clear();
vtxWriterIndex = 0;
idxWriterIndex = 0;
batchVtxPositions.limit(batchVtxPositions.capacity());
batchVtxColors.limit(batchVtxColors.capacity());
batchVtxTexCoords.limit(batchVtxTexCoords.capacity());
batchVtxTexBounds.limit(batchVtxTexBounds.capacity());
batchIndices.limit(batchIndices.capacity());
batchIndices.position(0);
}
// === Actual text mesh generation
@@ -429,6 +484,7 @@ public class BatchingFontRenderer {
boolean curUnderline = false;
float glyphScaleY = getGlyphScaleY();
float glyphScaleX = getGlyphScaleX();
float heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY / 2);
final float underlineY = heightNorth + (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY;
@@ -534,8 +590,8 @@ public class BatchingFontRenderer {
final float uStart = fontProvider.getUStart(chr);
final float vStart = fontProvider.getVStart(chr);
final float xAdvance = fontProvider.getXAdvance(chr) * getGlyphScaleX();
final float glyphW = fontProvider.getGlyphW(chr) * getGlyphScaleX();
final float xAdvance = fontProvider.getXAdvance(chr) * glyphScaleX;
final float glyphW = fontProvider.getGlyphW(chr) * glyphScaleX;
final float uSz = fontProvider.getUSize(chr);
final float vSz = fontProvider.getVSize(chr);
final float itOff = curItalic ? 1.0F : 0.0F; // italic offset
@@ -28,16 +28,14 @@ public final class FontProviderMC implements FontProvider {
public int[] charWidth;
static {
MCFONT_ASCII_MAP.defaultReturnValue((short) -1);
for (short i = 0; i < MCFONT_CHARS.length(); i++) {
MCFONT_ASCII_MAP.put(MCFONT_CHARS.charAt(i), i);
}
}
public int lookupMcFontPosition(char ch) {
if (isGlyphAvailable(ch)) {
return MCFONT_ASCII_MAP.get(ch);
}
return -1;
return MCFONT_ASCII_MAP.get(ch);
}
@Override
@@ -1,12 +1,12 @@
package com.gtnewhorizons.angelica.client.gui;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.resources.I18n;
import org.lwjgl.opengl.GL11;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -123,7 +123,7 @@ public class SliderClone extends GuiButton {
this.displayString = displayStringFormatter.apply(f);
}
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
this.drawTexturedModalRect(this.xPosition + (int)(this.sliderPercentage * (float)(this.width - 8)), this.yPosition, 0, 66, 4, 20);
this.drawTexturedModalRect(this.xPosition + (int)(this.sliderPercentage * (float)(this.width - 8)) + 4, this.yPosition, 196, 66, 4, 20);
}
@@ -11,7 +11,6 @@ import org.apache.logging.log4j.Logger;
public class ModStatus {
public static final Logger LOGGER = LogManager.getLogger("ModCompat");
public static BackhandReflectionCompat backhandCompat;
public static boolean isBetterCrashesLoaded;
public static boolean isNEIDLoaded;
public static boolean isNEIDMetadataExtended;
@@ -27,9 +26,8 @@ public class ModStatus {
public static boolean isBaublesLoaded;
public static boolean isFluidLoggedLoaded;
public static void preInit(){
isBackhandLoaded = Loader.isModLoaded("backhand");
if(isBackhandLoaded) {
public static void preInit() {
if (Loader.isModLoaded("backhand")) {
isBackhandLoaded = BackhandReflectionCompat.isBackhandLoaded();
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.compat.mojang;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.compat.backhand.BackhandReflectionCompat;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
@@ -9,14 +10,10 @@ public enum InteractionHand {
MAIN_HAND,
OFF_HAND;
InteractionHand() {
}
public ItemStack getItemInHand(EntityPlayer player){
if (ModStatus.isBackhandLoaded && this == InteractionHand.OFF_HAND){ // off hand (requires backhand)
return ModStatus.backhandCompat.getOffhandItem(player);
}
else { // main hand
public ItemStack getItemInHand(EntityPlayer player) {
if (ModStatus.isBackhandLoaded && this == InteractionHand.OFF_HAND) { // off hand (requires backhand)
return BackhandReflectionCompat.getOffhandItem(player);
} else { // main hand
return player.getHeldItem();
}
}
@@ -112,7 +112,7 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean optimizeWorldUpdateLight;
@Config.Comment("Speedup Animations. [From Hodgepodge]")
@Config.Comment("Optimize Texture Animations. [From Hodgepodge]")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean speedupAnimations;
@@ -235,4 +235,8 @@ public class AngelicaConfig {
@Config.DefaultBoolean(false)
public static boolean disableF3Additions;
@Config.Comment("Replaces various FFP uploads with statically allocated VBO's.")
@Config.DefaultBoolean(true)
public static boolean replaceFFPUploads;
}
@@ -29,6 +29,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import lombok.Getter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ChunkProviderClient;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.world.chunk.EmptyChunk;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
@@ -97,17 +98,18 @@ public class ChunkDebugMinimap {
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.glBegin(GL11.GL_QUADS);
final Tessellator tess = Tessellator.instance;
tess.startDrawingQuads();
final ChunkProviderClient chunkProvider = (ChunkProviderClient) mc.theWorld.getChunkProvider();
for (int x = playerChunkX - RANGE; x < playerChunkX + RANGE; x++) {
for (int z = playerChunkZ - RANGE; z < playerChunkZ + RANGE; z++) {
drawChunk(x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
drawChunk(tess, x, z, playerChunkX, playerChunkZ, manager, chunkProvider);
}
}
GL11.glEnd();
tess.draw();
// Restore GL state
GLStateManager.enableLighting();
@@ -117,7 +119,7 @@ public class ChunkDebugMinimap {
GLStateManager.glPopAttrib();
}
private void drawChunk(int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
private void drawChunk(Tessellator tess, int chunkX, int chunkZ, int playerChunkX, int playerChunkZ, AngelicaRenderSectionManager manager, ChunkProviderClient chunkProvider) {
boolean initialized = false;
float r = 0, g = 0, b = 0;
@@ -143,16 +145,16 @@ public class ChunkDebugMinimap {
}
}
GL11.glColor4f(r, g, b, 0.8f);
tess.setColorRGBA_F(r, g, b, 0.8f);
final int xStart = chunkX - playerChunkX;
final int zStart = chunkZ - playerChunkZ;
final int xEnd = xStart + 1;
final int zEnd = zStart + 1;
GLStateManager.glVertex3d(xStart, zStart, 0);
GLStateManager.glVertex3d(xStart, zEnd, 0);
GLStateManager.glVertex3d(xEnd, zEnd, 0);
GLStateManager.glVertex3d(xEnd, zStart, 0);
tess.addVertex(xStart, zStart, 0);
tess.addVertex(xStart, zEnd, 0);
tess.addVertex(xEnd, zEnd, 0);
tess.addVertex(xEnd, zStart, 0);
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.debug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.Tessellator;
@@ -10,31 +11,31 @@ import org.lwjgl.opengl.GL11;
public class F3Direction {
public static void renderWorldDirectionsEvent(Minecraft mc, RenderGameOverlayEvent.Pre event) {
if (mc.gameSettings.showDebugInfo && mc.gameSettings.thirdPersonView == 0) {
ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int width = scaledresolution.getScaledWidth();
int height = scaledresolution.getScaledHeight();
final ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
final int width = scaledresolution.getScaledWidth();
final int height = scaledresolution.getScaledHeight();
GL11.glPushMatrix();
GLStateManager.glPushMatrix();
GL11.glTranslatef((float) (width / 2), (float) (height / 2), -90);
GLStateManager.glTranslatef((float) (width / 2), (float) (height / 2), -90);
Entity entity = mc.renderViewEntity;
GL11.glRotatef(entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * event.partialTicks, -1.0F, 0.0F, 0.0F);
GL11.glRotatef(entity.prevRotationYaw + (entity.rotationYaw - entity.prevRotationYaw) * event.partialTicks, 0.0F, 1.0F, 0.0F);
final Entity entity = mc.renderViewEntity;
GLStateManager.glRotatef(entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * event.partialTicks, -1.0F, 0.0F, 0.0F);
GLStateManager.glRotatef(entity.prevRotationYaw + (entity.rotationYaw - entity.prevRotationYaw) * event.partialTicks, 0.0F, 1.0F, 0.0F);
GL11.glScalef(-1.0F, -1.0F, -1.0F);
GLStateManager.glScalef(-1.0F, -1.0F, -1.0F);
renderWorldDirections();
GL11.glPopMatrix();
GLStateManager.glPopMatrix();
}
}
public static void renderWorldDirections() {
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glDepthMask(false);
Tessellator tessellator = Tessellator.instance;
GLStateManager.disableTexture();
GLStateManager.glDepthMask(false);
final Tessellator tessellator = Tessellator.instance;
GL11.glLineWidth(2.0F);
GLStateManager.glLineWidth(2.0F);
tessellator.startDrawing(GL11.GL_LINES);
//X
@@ -54,8 +55,8 @@ public class F3Direction {
tessellator.draw();
GL11.glLineWidth(1.0F);
GL11.glDepthMask(true);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.glLineWidth(1.0F);
GLStateManager.glDepthMask(true);
GLStateManager.enableTexture();
}
}
@@ -1,44 +1,33 @@
package com.gtnewhorizons.angelica.debug;
import static org.lwjgl.opengl.GL11.GL_ALPHA_TEST;
import static org.lwjgl.opengl.GL11.GL_BLEND;
import static org.lwjgl.opengl.GL11.GL_CULL_FACE;
import static org.lwjgl.opengl.GL11.GL_DEPTH_TEST;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.opengl.GL11.GL_VERTEX_ARRAY;
import static org.lwjgl.opengl.GL11.glBlendFunc;
import static org.lwjgl.opengl.GL11.glColor4f;
import static org.lwjgl.opengl.GL11.glDisable;
import static org.lwjgl.opengl.GL11.glDisableClientState;
import static org.lwjgl.opengl.GL11.glDrawArrays;
import static org.lwjgl.opengl.GL11.glEnable;
import static org.lwjgl.opengl.GL11.glEnableClientState;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static com.gtnewhorizons.angelica.glsm.GLStateManager.glBindBuffer;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL20.glDisableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glUniform1;
import static org.lwjgl.opengl.GL20.glUniform1f;
import static org.lwjgl.opengl.GL20.glUniform1i;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.*;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.client.renderer.shader.ShaderProgram;
import java.nio.FloatBuffer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL30;
import java.nio.FloatBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
import static com.gtnewhorizons.angelica.glsm.GLStateManager.glBindBuffer;
import static org.lwjgl.opengl.GL11.GL_FLOAT;
import static org.lwjgl.opengl.GL11.GL_ONE_MINUS_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_SRC_ALPHA;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.GL_STATIC_DRAW;
import static org.lwjgl.opengl.GL15.glBufferData;
import static org.lwjgl.opengl.GL15.glGenBuffers;
import static org.lwjgl.opengl.GL20.glEnableVertexAttribArray;
import static org.lwjgl.opengl.GL20.glUniform1;
import static org.lwjgl.opengl.GL20.glUniform1f;
import static org.lwjgl.opengl.GL20.glUniform1i;
import static org.lwjgl.opengl.GL20.glVertexAttribPointer;
public abstract class F3Graph {
private static final int NUM_SAMPLES = 240;
@@ -63,6 +52,7 @@ public abstract class F3Graph {
private int uHeadIdx;
private int uSamples;
private int vertBuf;
private int vao;
private final ResourceLocation texture;
private final float pxPerNs;
private final boolean left;
@@ -117,8 +107,8 @@ public abstract class F3Graph {
"shaders/debug_graph.frag.glsl");
shader.use();
// Register attributes
aPos = shader.getAttribLocation("pos");
// Attribute location is declared in shader via layout(location = 0)
aPos = 0;
// Register uniforms
uFBWidth = shader.getUniformLocation("fbWidth");
@@ -129,7 +119,9 @@ public abstract class F3Graph {
final int uPxPerNs = shader.getUniformLocation("pxPerNs");
final int uLeft = shader.getUniformLocation("left");
// Load vertex buffer
vao = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
vertBuf = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vertBuf);
try (final MemoryStack stack = stackPush()) {
@@ -146,7 +138,13 @@ public abstract class F3Graph {
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
}
// Configure vertex attribute in the VAO
glVertexAttribPointer(aPos, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
glEnableVertexAttribArray(aPos);
glBindBuffer(GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final Minecraft mc = Minecraft.getMinecraft();
final ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
@@ -186,21 +184,21 @@ public abstract class F3Graph {
// Setup GL state
final Tessellator tess = Tessellator.instance;
glDisable(GL_DEPTH_TEST);
glEnable(GL_ALPHA_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.disableDepthTest();
GLStateManager.enableAlphaTest();
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Draw background
glDisable(GL_TEXTURE_2D);
glColor4f(0x50 / 255.0F, 0x50 / 255.0F, 0x50 / 255.0F, 0x90 / 255.0F);
GLStateManager.disableTexture();
GLStateManager.glColor4f(0x50 / 255.0F, 0x50 / 255.0F, 0x50 / 255.0F, 0x90 / 255.0F);
tess.startDrawingQuads();
for (int i = 0; i < 4; ++i) {
tess.addVertex(getVertX(sr, i), getVertY(sr, i), 0);
}
tess.draw();
glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
glEnable(GL_TEXTURE_2D);
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
GLStateManager.enableTexture();
// Draw samples
@@ -213,21 +211,16 @@ public abstract class F3Graph {
glUniform1i(uHeadIdx, samplesHead);
glUniform1(uSamples, sampleBuf);
glBindBuffer(GL_ARRAY_BUFFER, vertBuf);
glEnableVertexAttribArray(aPos);
glVertexAttribPointer(aPos, VERT_FLOATS, GL_FLOAT, false, VERT_FLOATS * 4, 0);
glEnableClientState(GL_VERTEX_ARRAY);
GLStateManager.glBindVertexArray(vao);
// Left side graphs are inverted, so temporarily disable culling
if (!left) glDisable(GL_CULL_FACE);
if (!left) GLStateManager.disableCull();
glDrawArrays(GL_TRIANGLE_STRIP, 0, VERT_COUNT);
GLStateManager.glDrawArrays(GL_TRIANGLE_STRIP, 0, VERT_COUNT);
if (!left) glEnable(GL_CULL_FACE);
if (!left) GLStateManager.enableCull();
glDisableClientState(GL_VERTEX_ARRAY);
glDisableVertexAttribArray(aPos);
glBindBuffer(GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
ShaderProgram.clear();
@@ -242,9 +235,9 @@ public abstract class F3Graph {
tess.draw();
// Reset GL state
glEnable(GL_DEPTH_TEST);
glDisable(GL_ALPHA_TEST);
glDisable(GL_BLEND);
GLStateManager.enableDepthTest();
GLStateManager.disableAlphaTest();
GLStateManager.disableBlend();
// Draw text
// Ensure running counters are reset on first sample
@@ -6,6 +6,7 @@ import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
import com.gtnewhorizon.gtnhlib.util.CoordinatePacker;
import com.gtnewhorizons.angelica.api.IDynamicLightProducer;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.compat.backhand.BackhandReflectionCompat;
import com.gtnewhorizons.angelica.compat.battlegear2.Battlegear2Compat;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
@@ -552,7 +553,7 @@ public class DynamicLights {
}
}
else if (ModStatus.isBackhandLoaded){
ItemStack offhand = ModStatus.backhandCompat.getOffhandItem(player);
ItemStack offhand = BackhandReflectionCompat.getOffhandItem(player);
if (offhand != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(offhand));
}
@@ -160,7 +160,7 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public void glDeleteVertexArrays(int array) {
UniversalVAO.deleteVertexArrays(array);
GLStateManager.glDeleteVertexArrays(array);
}
// ===================== BUFFER OPERATIONS =====================
@@ -0,0 +1,460 @@
package com.gtnewhorizons.angelica.glsm;
import net.minecraft.launchwrapper.Launch;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.Transformer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
/**
* Compat transformation for mod shaders running under core profile.
*
* <p>Handles:
* <ul>
* <li>Matrix builtin replacement (gl_ModelViewMatrix, gl_ProjectionMatrix, etc.)</li>
* <li>Vertex attribute replacement (gl_Vertex, gl_Color, gl_MultiTexCoord0/1, gl_Normal)</li>
* <li>gl_TexCoord[N] varying array → per-index in/out declarations</li>
* <li>Texture function renames (texture2D → texture, etc.)</li>
* <li>Fragment output handling (gl_FragColor → layout-qualified out declarations)</li>
* <li>Fog builtins (gl_Fog, gl_FogFragCoord)</li>
* <li>gl_FrontColor → local variable in vertex shaders</li>
* <li>shadow2D/shadow2DLod → texture/textureLod with vec4 wrapping</li>
* <li>Version upgrade to 330 core minimum</li>
* <li>Reserved word pre-parse renaming (texture-as-variable, sample, etc.)</li>
* </ul>
*/
public class CompatShaderTransformer {
private static final Pattern VERSION_PATTERN =
Pattern.compile("#version\\s+(\\d+)(?:\\s+(\\w+))?");
private static final Pattern DEFINE_PATTERN =
Pattern.compile("^\\s*#\\s*define\\s+.+$", Pattern.MULTILINE);
/** Compat builtins that trigger AST transformation. */
private static final Set<String> COMPAT_BUILTINS = Set.of(
"gl_ModelView", "gl_Projection", "gl_NormalMatrix", "gl_TextureMatrix",
"gl_FragColor", "gl_Fog", "gl_FrontColor", "gl_Color",
"gl_Vertex", "gl_MultiTexCoord", "gl_TexCoord", "gl_Normal", "ftransform",
"texture2D", "texture3D", "texelFetch2D", "texelFetch3D", "textureSize2D",
"shadow2D", "gl_FrontLightModelProduct",
"gl_LightSource", "gl_FrontMaterial"
);
private static final Pattern NEEDS_TRANSFORM_PATTERN = Pattern.compile(
String.join("|", COMPAT_BUILTINS) + "|\\b(?:attribute|varying)\\b"
);
private static final Map<String, String> MATRIX_RENAMES = Map.of(
"gl_ModelViewMatrix", "angelica_ModelViewMatrix",
"gl_ModelViewMatrixInverse", "angelica_ModelViewMatrixInverse",
"gl_ProjectionMatrix", "angelica_ProjectionMatrix",
"gl_ProjectionMatrixInverse", "angelica_ProjectionMatrixInverse",
"gl_NormalMatrix", "angelica_NormalMatrix"
);
private static final Path DUMP_DIR;
private static final AtomicInteger dumpCounter = new AtomicInteger(0);
static {
boolean isDev = false;
try {
final Object deobfEnv = Launch.blackboard != null ? Launch.blackboard.get("fml.deobfuscatedEnvironment") : null;
isDev = Boolean.TRUE.equals(deobfEnv);
} catch (Exception ignored) {}
DUMP_DIR = (isDev || Boolean.parseBoolean(System.getProperty("angelica.compat.dumpShaders", "false")))
? Paths.get("compat_shaders") : null;
}
private static final int CACHE_SIZE = 32;
private record CacheKey(String source, boolean isFragment) {}
private static final Map<CacheKey, String> cache = Collections.synchronizedMap(
new LinkedHashMap<>(32, 0.75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<CacheKey, String> eldest) {
return size() > CACHE_SIZE;
}
});
public static void clearCache() {
cache.clear();
}
/**
* Transform a mod shader source for core profile compatibility.
*/
public static String transform(String source, boolean isFragment) {
final boolean needsTransform = needsTransformation(source);
String result;
if (!needsTransform) {
result = fixupVersion(source);
} else {
final CacheKey key = new CacheKey(source, isFragment);
final String cached = cache.get(key);
if (cached != null) {
dumpShader(source, cached, isFragment, needsTransform);
return cached;
}
try {
result = transformInternal(source, isFragment);
cache.put(key, result);
} catch (Exception e) {
LOGGER.warn("CompatShaderTransformer: AST transformation failed, falling back to version fixup only", e);
result = fixupVersion(source);
}
}
dumpShader(source, result, isFragment, needsTransform);
return result;
}
private static boolean needsTransformation(String source) {
// Shaders already at 330+ core don't need compat transformation
final Matcher vm = VERSION_PATTERN.matcher(source);
if (vm.find()) {
final int version = Integer.parseInt(vm.group(1));
if (version >= 330 && "core".equals(vm.group(2))) return false;
}
return NEEDS_TRANSFORM_PATTERN.matcher(source).find();
}
private static String transformInternal(String source, boolean isFragment) {
final Matcher versionMatcher = VERSION_PATTERN.matcher(source);
int declaredVersion = 110;
if (versionMatcher.find()) {
declaredVersion = Integer.parseInt(versionMatcher.group(1));
}
final int targetVersion = Math.max(declaredVersion, 330);
// Pre-parse reserved word renaming — prevents ANTLR parse failures
source = GlslTransformUtils.replaceTexture(source);
source = GlslTransformUtils.renameReservedWords(source, targetVersion);
final ShaderParser.ParsedShader parsedShader = ShaderParser.parseShader(source);
final Transformer transformer = new Transformer(parsedShader.full());
injectMatrixUniforms(transformer);
transformFog(transformer, isFragment, source);
// gl_FrontLightModelProduct.sceneColor → angelica_SceneColor uniform
if (source.contains("gl_FrontLightModelProduct")) {
transformer.injectVariable("uniform vec4 angelica_SceneColor;");
transformer.replaceExpression("gl_FrontLightModelProduct.sceneColor", "angelica_SceneColor");
}
// gl_LightSource[i] → struct + uniform array (both via injectFunction to keep struct before uniform)
if (source.contains("gl_LightSource")) {
transformer.injectFunction(
"struct angelica_LightSourceParameters {"
+ "vec4 ambient;vec4 diffuse;vec4 specular;vec4 position;vec4 halfVector;"
+ "vec3 spotDirection;float spotExponent;float spotCutoff;float spotCosCutoff;"
+ "float constantAttenuation;float linearAttenuation;float quadraticAttenuation;"
+ "};");
transformer.injectFunction("uniform angelica_LightSourceParameters angelica_LightSource[2];");
transformer.rename("gl_LightSource", "angelica_LightSource");
}
// gl_FrontMaterial → struct + uniform (both via injectFunction to keep struct before uniform)
if (source.contains("gl_FrontMaterial")) {
transformer.injectFunction(
"struct angelica_MaterialParameters {"
+ "vec4 emission;vec4 ambient;vec4 diffuse;vec4 specular;float shininess;"
+ "};");
transformer.injectFunction("uniform angelica_MaterialParameters angelica_FrontMaterial;");
transformer.rename("gl_FrontMaterial", "angelica_FrontMaterial");
}
// gl_FrontColor (vertex) → gl_Color (fragment) varying chain
// Vertex side is unconditional: fragment may read gl_Color without vertex writing gl_FrontColor.
// Uses angelica_ prefix to avoid colliding with user-declared varyings (e.g. "v_Color").
if (!isFragment) {
transformer.injectVariable("out vec4 angelica_FrontColor;");
transformer.rename("gl_FrontColor", "angelica_FrontColor");
transformer.prependMain("angelica_FrontColor = vec4(1.0);");
// Vertex attributes — replaces removed FFP vertex inputs with explicit in declarations
transformVertexAttributes(transformer, source);
} else {
if (source.contains("gl_Color")) {
transformer.injectVariable("in vec4 angelica_FrontColor;");
transformer.rename("gl_Color", "angelica_FrontColor");
}
}
// gl_TexCoord[N] varying array → per-index in/out declarations
final Set<Integer> texCoordIndices = new HashSet<>();
transformer.renameArray("gl_TexCoord", "angelica_TexCoord", texCoordIndices);
for (Integer i : texCoordIndices) {
final String qualifier = isFragment ? "in" : "out";
transformer.injectVariable(qualifier + " vec4 angelica_TexCoord" + i + ";");
}
// Fragment output handling + alpha test discard
if (isFragment) {
transformFragmentOutputs(transformer);
}
// texture-as-variable collision handling
if (transformer.containsCall("texture") && transformer.hasVariable("texture")) {
transformer.rename("texture", "gtexture");
}
if (transformer.hasVariable("angelica_renamed_texture")) {
transformer.rename("angelica_renamed_texture", "gtexture");
}
transformer.renameFunctionCall(GlslTransformUtils.TEXTURE_RENAMES);
transformer.renameAndWrapShadow("shadow2D", "texture");
transformer.renameAndWrapShadow("shadow2DLod", "textureLod");
final String versionDirective = "#version " + targetVersion + " core\n";
final String extensions = VERSION_PATTERN.matcher(GlslTransformUtils.getFormattedShader(parsedShader.pre(), "")).replaceFirst("").trim();
// Preserve #define directives
final StringBuilder defines = new StringBuilder();
final Matcher defineMatcher = DEFINE_PATTERN.matcher(source);
while (defineMatcher.find()) {
defines.append(defineMatcher.group().trim()).append('\n');
}
final String header = versionDirective + (extensions.isEmpty() ? "" : "\n" + extensions) + (defines.isEmpty() ? "" : "\n" + defines);
final StringBuilder result = new StringBuilder();
transformer.mutateTree(tree -> result.append(GlslTransformUtils.getFormattedShader(tree, header)));
// Restore pre-parse renames
String output = GlslTransformUtils.restoreReservedWords(result.toString());
// Core profile: attribute → in, varying → out (vertex) / in (fragment)
output = fixupQualifiers(output, isFragment);
return output;
}
/**
* Inject matrix uniforms and rename compat builtins.
*/
private static void injectMatrixUniforms(Transformer transformer) {
transformer.injectVariable("uniform mat4 angelica_ModelViewMatrix;");
transformer.injectVariable("uniform mat4 angelica_ModelViewMatrixInverse;");
transformer.injectVariable("uniform mat4 angelica_ProjectionMatrix;");
transformer.injectVariable("uniform mat4 angelica_ProjectionMatrixInverse;");
transformer.injectVariable("uniform mat3 angelica_NormalMatrix;");
transformer.injectVariable("uniform mat4 angelica_LightmapTextureMatrix;");
transformer.rename(MATRIX_RENAMES);
// Expression replacements
transformer.replaceExpression("gl_ModelViewProjectionMatrix", "(angelica_ProjectionMatrix * angelica_ModelViewMatrix)");
transformer.replaceExpression("gl_TextureMatrix[0]", "mat4(1.0)");
transformer.replaceExpression("gl_TextureMatrix[1]", "angelica_LightmapTextureMatrix");
transformer.replaceExpression("gl_TextureMatrix", "mat4[8](mat4(1.0), angelica_LightmapTextureMatrix, mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0), mat4(1.0))");
}
/**
* Transform fragment outputs for core profile.
*/
private static void transformFragmentOutputs(Transformer transformer) {
if (transformer.containsCall("gl_FragColor")) {
transformer.replaceExpression("gl_FragColor", "gl_FragData[0]");
}
final Set<Integer> found = new HashSet<>();
transformer.renameArray("gl_FragData", "angelica_FragData", found);
for (Integer i : found) {
transformer.injectVariable("layout (location = " + i + ") out vec4 angelica_FragData" + i + ";");
}
// Core profile: GL_ALPHA_TEST is removed - inject runtime discard using GLSM-tracked alpha reference (uploaded by CompatUniformManager).
if (found.contains(0)) {
transformer.injectVariable("uniform float angelica_currentAlphaTest;");
transformer.appendMain("if (angelica_FragData0.a <= angelica_currentAlphaTest) discard;");
}
}
private static void transformFog(Transformer transformer, boolean isFragment, String source) {
// Vertex side is unconditional: fragment may read gl_FogFragCoord without vertex writing it
transformer.rename("gl_FogFragCoord", "angelica_FogFragCoord");
if (!isFragment) {
transformer.injectVariable("out float angelica_FogFragCoord;");
transformer.prependMain("angelica_FogFragCoord = 0.0;");
} else {
if (source.contains("gl_FogFragCoord")) {
transformer.injectVariable("in float angelica_FogFragCoord;");
}
}
transformer.rename("gl_Fog", "angelica_Fog");
transformer.injectVariable("uniform float angelica_FogDensity;");
transformer.injectVariable("uniform float angelica_FogStart;");
transformer.injectVariable("uniform float angelica_FogEnd;");
transformer.injectVariable("uniform vec4 angelica_FogColor;");
transformer.injectFunction("struct angelica_FogParameters {vec4 color;float density;float start;float end;float scale;};");
transformer.injectFunction(
"angelica_FogParameters angelica_Fog = angelica_FogParameters("
+ "angelica_FogColor, angelica_FogDensity, angelica_FogStart, angelica_FogEnd, "
+ "1.0 / (angelica_FogEnd - angelica_FogStart));");
}
/**
* Replace removed FFP vertex attributes with explicit {@code in} declarations at core profile attribute locations.
*/
private static void transformVertexAttributes(Transformer transformer, String source) {
if (source.contains("gl_Vertex") || source.contains("ftransform")) {
transformer.injectVariable("layout(location = 0) in vec4 angelica_Vertex;");
transformer.rename("gl_Vertex", "angelica_Vertex");
}
// gl_Color in vertex shaders is the per-vertex color attribute, distinct from the fragment gl_Color (interpolated gl_FrontColor) handled above
if (source.contains("gl_Color")) {
transformer.injectVariable("layout(location = 1) in vec4 angelica_Color;");
transformer.rename("gl_Color", "angelica_Color");
}
if (source.contains("gl_MultiTexCoord0")) {
transformer.injectVariable("layout(location = 2) in vec4 angelica_MultiTexCoord0;");
transformer.rename("gl_MultiTexCoord0", "angelica_MultiTexCoord0");
}
if (source.contains("gl_MultiTexCoord1")) {
transformer.injectVariable("layout(location = 3) in vec4 angelica_MultiTexCoord1;");
transformer.rename("gl_MultiTexCoord1", "angelica_MultiTexCoord1");
}
if (source.contains("gl_Normal")) {
transformer.injectVariable("layout(location = 4) in vec3 angelica_Normal;");
transformer.rename("gl_Normal", "angelica_Normal");
}
if (source.contains("ftransform")) {
transformer.replaceExpression("ftransform()", "(angelica_ProjectionMatrix * angelica_ModelViewMatrix * angelica_Vertex)");
}
}
private static void dumpShader(String original, String transformed, boolean isFragment, boolean wasTransformed) {
if (DUMP_DIR == null) return;
final int id = dumpCounter.getAndIncrement();
final String suffix = isFragment ? ".frag.glsl" : ".vert.glsl";
try {
Files.createDirectories(DUMP_DIR);
// Capture caller info for identification
final String caller = identifyCaller();
final String header = "// Compat shader dump #" + id + " (" + (isFragment ? "fragment" : "vertex") + ")"
+ "\n// Transformed: " + wasTransformed
+ "\n// Caller: " + caller + "\n\n";
Files.writeString(DUMP_DIR.resolve(id + "_original" + suffix), header + original, StandardCharsets.UTF_8);
Files.writeString(DUMP_DIR.resolve(id + "_transformed" + suffix), header + transformed, StandardCharsets.UTF_8);
} catch (IOException e) {
LOGGER.warn("Failed to dump compat shader: {}", e.getMessage());
}
}
private static String identifyCaller() {
for (StackTraceElement frame : Thread.currentThread().getStackTrace()) {
final String cls = frame.getClassName();
if (!cls.startsWith("com.gtnewhorizons.angelica.glsm.")
&& !cls.startsWith("java.")
&& !cls.equals("org.lwjgl.opengl.GL20")) {
return cls + "." + frame.getMethodName() + ":" + frame.getLineNumber();
}
}
return "unknown";
}
private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile("\\battribute\\b");
private static final Pattern VARYING_PATTERN = Pattern.compile("\\bvarying\\b");
/** Replace legacy storage qualifiers removed in core profile. Safe on AST-serialized output (no comments). */
public static String fixupQualifiers(String source, boolean isFragment) {
source = ATTRIBUTE_PATTERN.matcher(source).replaceAll("in");
source = VARYING_PATTERN.matcher(source).replaceAll(isFragment ? "in" : "out");
return source;
}
// Patterns for parsing fragment shader in declarations to generate passthrough vertex shaders
private static final Pattern FRAG_IN_COLOR = Pattern.compile("\\bin\\s+vec4\\s+angelica_FrontColor\\b");
private static final Pattern FRAG_IN_TEXCOORD = Pattern.compile("\\bin\\s+vec4\\s+angelica_TexCoord(\\d+)\\b");
private static final Pattern FRAG_IN_FOGCOORD = Pattern.compile("\\bin\\s+float\\s+angelica_FogFragCoord\\b");
/**
* Generate a passthrough vertex shader for a fragment-only program.
*
* @param fragmentSource the transformed fragment shader source
* @return a complete vertex shader source string
*/
public static String generatePassthroughVertexShader(String fragmentSource) {
final StringBuilder sb = new StringBuilder(512);
sb.append("#version 330 core\n\n");
// Always have position
sb.append("layout(location = 0) in vec4 a_Position;\n");
// Matrix uniforms for position transform
sb.append("uniform mat4 angelica_ModelViewMatrix;\n");
sb.append("uniform mat4 angelica_ProjectionMatrix;\n");
final StringBuilder varyings = new StringBuilder();
final StringBuilder assignments = new StringBuilder();
if (FRAG_IN_COLOR.matcher(fragmentSource).find()) {
sb.append("layout(location = 1) in vec4 a_Color;\n");
varyings.append("out vec4 angelica_FrontColor;\n");
assignments.append(" angelica_FrontColor = a_Color;\n");
}
final Matcher texCoordMatcher = FRAG_IN_TEXCOORD.matcher(fragmentSource);
final Set<Integer> texCoordIndices = new HashSet<>();
while (texCoordMatcher.find()) {
texCoordIndices.add(Integer.parseInt(texCoordMatcher.group(1)));
}
for (int i : texCoordIndices) {
// PRIMARY_UV=2, SECONDARY_UV=3. Only indices 0 and 1 are supported; higher indices collide at location 3.
final int location = i == 0 ? 2 : 3;
sb.append("layout(location = ").append(location).append(") in vec4 a_TexCoord").append(i).append(";\n");
varyings.append("out vec4 angelica_TexCoord").append(i).append(";\n");
assignments.append(" angelica_TexCoord").append(i).append(" = a_TexCoord").append(i).append(";\n");
}
if (FRAG_IN_FOGCOORD.matcher(fragmentSource).find()) {
varyings.append("out float angelica_FogFragCoord;\n");
assignments.append(" angelica_FogFragCoord = abs((angelica_ModelViewMatrix * a_Position).z);\n");
}
sb.append(varyings);
sb.append("\nvoid main() {\n");
sb.append(" gl_Position = angelica_ProjectionMatrix * angelica_ModelViewMatrix * a_Position;\n");
sb.append(assignments);
sb.append("}\n");
return sb.toString();
}
/** Ensure #version is at least 330 core, strip 'compatibility' profile. */
private static String fixupVersion(String source) {
final Matcher m = VERSION_PATTERN.matcher(source);
if (!m.find()) {
return "#version 330 core\n" + source;
}
final int version = Integer.parseInt(m.group(1));
final String profile = m.group(2);
if (version >= 330 && !"compatibility".equals(profile)) {
if ("core".equals(profile)) return source;
return m.replaceFirst("#version " + version + " core");
}
final int targetVersion = Math.max(version, 330);
return m.replaceFirst("#version " + targetVersion + " core");
}
}
@@ -0,0 +1,411 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.LightStateStack;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.LightModelState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
/**
* Per-program compat uniform location cache and upload manager.
*
* <p>Manages {@code angelica_*} uniforms injected by {@link CompatShaderTransformer} and {@code iris_*} matrix uniforms used by Iris/celeritas shader programs.
*/
public class CompatUniformManager {
// Uniform location indices within the cached array (package-private for tests)
static final int LOC_MODELVIEW = 0;
static final int LOC_MODELVIEW_INVERSE = 1;
static final int LOC_PROJECTION = 2;
static final int LOC_PROJECTION_INVERSE = 3;
static final int LOC_NORMAL = 4;
static final int LOC_LIGHTMAP_TEXTURE_MATRIX = 5;
static final int LOC_FOG_DENSITY = 6;
static final int LOC_FOG_START = 7;
static final int LOC_FOG_END = 8;
static final int LOC_FOG_COLOR = 9;
static final int LOC_IRIS_MODELVIEW = 10;
static final int LOC_IRIS_PROJECTION = 11;
static final int LOC_IRIS_NORMAL = 12;
static final int LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX = 13;
static final int LOC_ALPHA_TEST_REF = 14;
static final int LOC_SCENE_COLOR = 15;
static final int LOC_CLIP_PLANES = 16;
static final int LOC_CLIP_PLANES_ENABLED = 17;
// Light source fields: 12 per light × 2 lights = 24 locations
static final int LIGHT_FIELDS = 12;
static final int LF_AMBIENT = 0, LF_DIFFUSE = 1, LF_SPECULAR = 2, LF_POSITION = 3;
static final int LF_HALF_VECTOR = 4, LF_SPOT_DIRECTION = 5, LF_SPOT_EXPONENT = 6;
static final int LF_SPOT_CUTOFF = 7, LF_SPOT_COS_CUTOFF = 8;
static final int LF_CONSTANT_ATTEN = 9, LF_LINEAR_ATTEN = 10, LF_QUADRATIC_ATTEN = 11;
static final int LOC_LIGHT_BASE = 18;
// Material fields: 5 locations
static final int MF_EMISSION = 0, MF_AMBIENT = 1, MF_DIFFUSE = 2, MF_SPECULAR = 3, MF_SHININESS = 4;
static final int MAT_FIELDS = 5;
static final int LOC_MAT_BASE = LOC_LIGHT_BASE + 2 * LIGHT_FIELDS; // 42
static final int LOC_COUNT = LOC_MAT_BASE + MAT_FIELDS; // 47
private static final String[] LIGHT_FIELD_NAMES = {
"ambient", "diffuse", "specular", "position", "halfVector",
"spotDirection", "spotExponent", "spotCutoff", "spotCosCutoff",
"constantAttenuation", "linearAttenuation", "quadraticAttenuation",
};
private static final String[] MAT_FIELD_NAMES = {
"emission", "ambient", "diffuse", "specular", "shininess",
};
private static final String[] UNIFORM_NAMES;
static {
UNIFORM_NAMES = new String[LOC_COUNT];
UNIFORM_NAMES[LOC_MODELVIEW] = "angelica_ModelViewMatrix";
UNIFORM_NAMES[LOC_MODELVIEW_INVERSE] = "angelica_ModelViewMatrixInverse";
UNIFORM_NAMES[LOC_PROJECTION] = "angelica_ProjectionMatrix";
UNIFORM_NAMES[LOC_PROJECTION_INVERSE] = "angelica_ProjectionMatrixInverse";
UNIFORM_NAMES[LOC_NORMAL] = "angelica_NormalMatrix";
UNIFORM_NAMES[LOC_LIGHTMAP_TEXTURE_MATRIX] = "angelica_LightmapTextureMatrix";
UNIFORM_NAMES[LOC_FOG_DENSITY] = "angelica_FogDensity";
UNIFORM_NAMES[LOC_FOG_START] = "angelica_FogStart";
UNIFORM_NAMES[LOC_FOG_END] = "angelica_FogEnd";
UNIFORM_NAMES[LOC_FOG_COLOR] = "angelica_FogColor";
UNIFORM_NAMES[LOC_IRIS_MODELVIEW] = "iris_ModelViewMatrix";
UNIFORM_NAMES[LOC_IRIS_PROJECTION] = "iris_ProjectionMatrix";
UNIFORM_NAMES[LOC_IRIS_NORMAL] = "iris_NormalMatrix";
UNIFORM_NAMES[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX] = "iris_LightmapTextureMatrix";
UNIFORM_NAMES[LOC_ALPHA_TEST_REF] = "angelica_currentAlphaTest";
UNIFORM_NAMES[LOC_SCENE_COLOR] = "angelica_SceneColor";
UNIFORM_NAMES[LOC_CLIP_PLANES] = "angelica_ClipPlane[0]";
UNIFORM_NAMES[LOC_CLIP_PLANES_ENABLED] = "angelica_ClipPlanesEnabled";
for (int li = 0; li < 2; li++) {
for (int fi = 0; fi < LIGHT_FIELDS; fi++) {
UNIFORM_NAMES[LOC_LIGHT_BASE + li * LIGHT_FIELDS + fi] =
"angelica_LightSource[" + li + "]." + LIGHT_FIELD_NAMES[fi];
}
}
for (int fi = 0; fi < MAT_FIELDS; fi++) {
UNIFORM_NAMES[LOC_MAT_BASE + fi] = "angelica_FrontMaterial." + MAT_FIELD_NAMES[fi];
}
}
/** Per-program cached uniform locations. Maps program ID → int[LOC_COUNT]. */
private static final Int2ObjectOpenHashMap<int[]> programLocations = new Int2ObjectOpenHashMap<>();
// Reusable NIO buffers for upload
private static final FloatBuffer mat4Buf = BufferUtils.createFloatBuffer(16);
private static final FloatBuffer mat3Buf = BufferUtils.createFloatBuffer(9);
private static final FloatBuffer vec4Buf = BufferUtils.createFloatBuffer(4);
private static final FloatBuffer clipPlaneBuf = BufferUtils.createFloatBuffer(32); // 8 planes * vec4
private static final Matrix3f normalMatrix = new Matrix3f();
private static final Matrix4f scratchMatrix = new Matrix4f();
private static final float LIGHTMAP_SCALE = 1.0f / 256.0f;
private static final FloatBuffer lightmapMatrixBuf;
static {
lightmapMatrixBuf = BufferUtils.createFloatBuffer(16);
new Matrix4f().scale(LIGHTMAP_SCALE).translate(8.0f, 8.0f, 8.0f).get(lightmapMatrixBuf);
}
// Dirty tracking: skip uploads when state hasn't changed and program is the same
private static int lastProgram = -1;
private static int lastMvGen = -1;
private static int lastProjGen = -1;
private static int lastTexMatGen = -1;
private static int lastFragmentGen = -1;
private static int lastLightingGen = -1;
private static int lastClipPlaneGen = -1;
private CompatUniformManager() {}
public static void onLinkProgram(int program) {
int[] locs = new int[LOC_COUNT];
boolean hasAny = false;
for (int i = 0; i < LOC_COUNT; i++) {
locs[i] = GL20.glGetUniformLocation(program, UNIFORM_NAMES[i]);
if (locs[i] != -1) hasAny = true;
}
if (hasAny) {
programLocations.put(program, locs);
LOGGER.debug("CompatUniformManager: program {} has compat uniforms", program);
}
}
public static void onUseProgram(int program) {
if (program == 0) return;
int[] locs = programLocations.get(program);
if (locs == null) return;
final boolean programChanged = program != lastProgram;
lastProgram = program;
// Matrix uniforms — skip if generation unchanged and same program
final int mvGen = GLStateManager.mvGeneration;
final int projGen = GLStateManager.projGeneration;
final int texMatGen = GLStateManager.texMatrixGeneration;
final boolean mvChanged = programChanged || mvGen != lastMvGen;
final boolean projChanged = programChanged || projGen != lastProjGen;
final boolean texMatChanged = programChanged || texMatGen != lastTexMatGen;
if (mvChanged || projChanged || texMatChanged) {
uploadMatrices(locs, mvChanged, projChanged, texMatChanged);
lastMvGen = mvGen;
lastProjGen = projGen;
lastTexMatGen = texMatGen;
}
// Fragment-category uniforms (fog, alpha) — skip if generation unchanged
final int fragGen = GLStateManager.fragmentGeneration;
if (programChanged || fragGen != lastFragmentGen) {
lastFragmentGen = fragGen;
uploadFragmentUniforms(locs);
}
// Lighting-derived uniforms (scene color, light sources, material)
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
lastLightingGen = litGen;
if (locs[LOC_SCENE_COLOR] != -1) uploadSceneColor(locs);
uploadLightSources(locs);
uploadMaterial(locs);
}
// Clip plane equations + enabled bool — uploaded when enable state or equations change
if (locs[LOC_CLIP_PLANES] != -1 || locs[LOC_CLIP_PLANES_ENABLED] != -1) {
final int cpGen = GLStateManager.clipPlaneGeneration;
if (programChanged || cpGen != lastClipPlaneGen) {
lastClipPlaneGen = cpGen;
uploadClipPlanes(locs);
}
}
}
private static void uploadMatrices(int[] locs, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
if (mvChanged) {
// ModelView
if (locs[LOC_MODELVIEW] != -1 || locs[LOC_IRIS_MODELVIEW] != -1) {
mv.get(mat4Buf);
if (locs[LOC_MODELVIEW] != -1) {
GL20.glUniformMatrix4(locs[LOC_MODELVIEW], false, mat4Buf);
}
if (locs[LOC_IRIS_MODELVIEW] != -1) {
GL20.glUniformMatrix4(locs[LOC_IRIS_MODELVIEW], false, mat4Buf);
}
}
// ModelView Inverse
if (locs[LOC_MODELVIEW_INVERSE] != -1) {
mv.invert(scratchMatrix);
scratchMatrix.get(mat4Buf);
GL20.glUniformMatrix4(locs[LOC_MODELVIEW_INVERSE], false, mat4Buf);
}
// Normal Matrix (inverse-transpose of upper-left 3x3 of ModelView)
if (locs[LOC_NORMAL] != -1 || locs[LOC_IRIS_NORMAL] != -1) {
mv.normal(normalMatrix);
normalMatrix.get(mat3Buf);
if (locs[LOC_NORMAL] != -1) {
GL20.glUniformMatrix3(locs[LOC_NORMAL], false, mat3Buf);
}
if (locs[LOC_IRIS_NORMAL] != -1) {
GL20.glUniformMatrix3(locs[LOC_IRIS_NORMAL], false, mat3Buf);
}
}
}
if (projChanged) {
// Projection
if (locs[LOC_PROJECTION] != -1 || locs[LOC_IRIS_PROJECTION] != -1) {
proj.get(mat4Buf);
if (locs[LOC_PROJECTION] != -1) {
GL20.glUniformMatrix4(locs[LOC_PROJECTION], false, mat4Buf);
}
if (locs[LOC_IRIS_PROJECTION] != -1) {
GL20.glUniformMatrix4(locs[LOC_IRIS_PROJECTION], false, mat4Buf);
}
}
// Projection Inverse
if (locs[LOC_PROJECTION_INVERSE] != -1) {
proj.invert(scratchMatrix);
scratchMatrix.get(mat4Buf);
GL20.glUniformMatrix4(locs[LOC_PROJECTION_INVERSE], false, mat4Buf);
}
}
if (texMatChanged) {
// Iris lightmap texture matrix (constant)
if (locs[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX] != -1) {
GL20.glUniformMatrix4(locs[LOC_IRIS_LIGHTMAP_TEXTURE_MATRIX], false, lightmapMatrixBuf);
}
// angelica_LightmapTextureMatrix — actual GLSM texture unit 1 matrix (set by enableLightmap())
if (locs[LOC_LIGHTMAP_TEXTURE_MATRIX] != -1) {
GLStateManager.getTextures().getTextureUnitMatrix(1).get(mat4Buf);
GL20.glUniformMatrix4(locs[LOC_LIGHTMAP_TEXTURE_MATRIX], false, mat4Buf);
}
}
}
private static void uploadFragmentUniforms(int[] locs) {
// Fog uniforms
FogStateStack fog = GLStateManager.getFogState();
if (locs[LOC_FOG_DENSITY] != -1) {
GL20.glUniform1f(locs[LOC_FOG_DENSITY], Math.max(0.0f, fog.getDensity()));
}
if (locs[LOC_FOG_START] != -1) {
GL20.glUniform1f(locs[LOC_FOG_START], fog.getStart());
}
if (locs[LOC_FOG_END] != -1) {
GL20.glUniform1f(locs[LOC_FOG_END], fog.getEnd());
}
if (locs[LOC_FOG_COLOR] != -1) {
vec4Buf.clear();
vec4Buf.put((float) fog.getFogColor().x).put((float) fog.getFogColor().y)
.put((float) fog.getFogColor().z).put(fog.getFogAlpha());
vec4Buf.flip();
GL20.glUniform4(locs[LOC_FOG_COLOR], vec4Buf);
}
// Alpha test reference — core profile replacement for GL_ALPHA_TEST
// Return -1.0f for ALWAYS or disabled so `alpha <= ref` never discards.
if (locs[LOC_ALPHA_TEST_REF] != -1) {
final float ref;
if (!GLStateManager.getAlphaTest().isEnabled()
|| GLStateManager.getAlphaState().getFunction() == GL11.GL_ALWAYS) {
ref = -1.0f;
} else {
ref = GLStateManager.getAlphaState().getReference();
}
GL20.glUniform1f(locs[LOC_ALPHA_TEST_REF], ref);
}
}
/**
* Scene color = emission + ambient * lightModel.ambient, alpha = diffuse.a
* Same computation as {@code Uniforms.uploadLightingPreComputed()}.
*/
private static void uploadSceneColor(int[] locs) {
final MaterialState mat = GLStateManager.getFrontMaterial();
final LightModelState lm = GLStateManager.getLightModel();
vec4Buf.clear();
vec4Buf.put(mat.emission.x + mat.ambient.x * lm.ambient.x)
.put(mat.emission.y + mat.ambient.y * lm.ambient.y)
.put(mat.emission.z + mat.ambient.z * lm.ambient.z)
.put(mat.diffuse.w);
vec4Buf.flip();
GL20.glUniform4(locs[LOC_SCENE_COLOR], vec4Buf);
}
private static void uploadLightSources(int[] locs) {
// Early exit: if the first light field isn't present, no lighting uniforms exist
if (locs[LOC_LIGHT_BASE] == -1) return;
final LightStateStack[] lights = GLStateManager.getLightDataStates();
for (int li = 0; li < 2; li++) {
final int base = LOC_LIGHT_BASE + li * LIGHT_FIELDS;
final LightState light = lights[li];
if (locs[base + LF_AMBIENT] != -1)
GL20.glUniform4f(locs[base + LF_AMBIENT], light.ambient.x, light.ambient.y, light.ambient.z, light.ambient.w);
if (locs[base + LF_DIFFUSE] != -1)
GL20.glUniform4f(locs[base + LF_DIFFUSE], light.diffuse.x, light.diffuse.y, light.diffuse.z, light.diffuse.w);
if (locs[base + LF_SPECULAR] != -1)
GL20.glUniform4f(locs[base + LF_SPECULAR], light.specular.x, light.specular.y, light.specular.z, light.specular.w);
if (locs[base + LF_POSITION] != -1)
GL20.glUniform4f(locs[base + LF_POSITION], light.position.x, light.position.y, light.position.z, light.position.w);
if (locs[base + LF_HALF_VECTOR] != -1) {
// halfVector = normalize(normalize(position.xyz) + eye), eye=(0,0,1) for infinite viewer
float hx, hy, hz;
if (light.position.w == 0.0f) {
float len = (float) Math.sqrt(light.position.x * light.position.x
+ light.position.y * light.position.y + light.position.z * light.position.z);
if (len > 0) {
hx = light.position.x / len;
hy = light.position.y / len;
hz = light.position.z / len + 1.0f;
} else {
hx = 0; hy = 0; hz = 1;
}
} else {
hx = 0; hy = 0; hz = 1;
}
float hlen = (float) Math.sqrt(hx * hx + hy * hy + hz * hz);
if (hlen > 0) { hx /= hlen; hy /= hlen; hz /= hlen; }
GL20.glUniform4f(locs[base + LF_HALF_VECTOR], hx, hy, hz, 0.0f);
}
if (locs[base + LF_SPOT_DIRECTION] != -1)
GL20.glUniform3f(locs[base + LF_SPOT_DIRECTION], light.spotDirection.x, light.spotDirection.y, light.spotDirection.z);
if (locs[base + LF_SPOT_EXPONENT] != -1)
GL20.glUniform1f(locs[base + LF_SPOT_EXPONENT], light.spotExponent);
if (locs[base + LF_SPOT_CUTOFF] != -1)
GL20.glUniform1f(locs[base + LF_SPOT_CUTOFF], light.spotCutoff);
if (locs[base + LF_SPOT_COS_CUTOFF] != -1)
GL20.glUniform1f(locs[base + LF_SPOT_COS_CUTOFF], light.spotCosCutoff);
if (locs[base + LF_CONSTANT_ATTEN] != -1)
GL20.glUniform1f(locs[base + LF_CONSTANT_ATTEN], light.constantAttenuation);
if (locs[base + LF_LINEAR_ATTEN] != -1)
GL20.glUniform1f(locs[base + LF_LINEAR_ATTEN], light.linearAttenuation);
if (locs[base + LF_QUADRATIC_ATTEN] != -1)
GL20.glUniform1f(locs[base + LF_QUADRATIC_ATTEN], light.quadraticAttenuation);
}
}
private static void uploadMaterial(int[] locs) {
// Early exit: if the first material field isn't present, no material uniforms exist
if (locs[LOC_MAT_BASE] == -1) return;
final MaterialState mat = GLStateManager.getFrontMaterial();
if (locs[LOC_MAT_BASE + MF_EMISSION] != -1)
GL20.glUniform4f(locs[LOC_MAT_BASE + MF_EMISSION], mat.emission.x, mat.emission.y, mat.emission.z, mat.emission.w);
if (locs[LOC_MAT_BASE + MF_AMBIENT] != -1)
GL20.glUniform4f(locs[LOC_MAT_BASE + MF_AMBIENT], mat.ambient.x, mat.ambient.y, mat.ambient.z, mat.ambient.w);
if (locs[LOC_MAT_BASE + MF_DIFFUSE] != -1)
GL20.glUniform4f(locs[LOC_MAT_BASE + MF_DIFFUSE], mat.diffuse.x, mat.diffuse.y, mat.diffuse.z, mat.diffuse.w);
if (locs[LOC_MAT_BASE + MF_SPECULAR] != -1)
GL20.glUniform4f(locs[LOC_MAT_BASE + MF_SPECULAR], mat.specular.x, mat.specular.y, mat.specular.z, mat.specular.w);
if (locs[LOC_MAT_BASE + MF_SHININESS] != -1)
GL20.glUniform1f(locs[LOC_MAT_BASE + MF_SHININESS], mat.shininess);
}
private static void uploadClipPlanes(int[] locs) {
if (locs[LOC_CLIP_PLANES_ENABLED] != -1) {
GL20.glUniform1i(locs[LOC_CLIP_PLANES_ENABLED], GLStateManager.anyClipPlaneEnabled() ? 1 : 0);
}
if (locs[LOC_CLIP_PLANES] != -1) {
final ClipPlaneState cps = GLStateManager.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
clipPlaneBuf.flip();
GL20.glUniform4(locs[LOC_CLIP_PLANES], clipPlaneBuf);
}
}
public static void onDeleteProgram(int program) {
programLocations.remove(program);
}
public static boolean hasProgram(int program) {
return programLocations.containsKey(program);
}
public static int[] getLocations(int program) {
return programLocations.get(program);
}
}
@@ -0,0 +1,187 @@
package com.gtnewhorizons.angelica.glsm;
/*
* Ported from Mesa's util_idalloc (src/util/u_idalloc.c).
*
* Original copyright and license:
*
* Copyright 2017 Valve Corporation
* All Rights Reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sub license, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice (including the
* next paragraph) shall be included in all copies or substantial portions
* of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR
* ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Original author: Samuel Pitoiset <[email protected]>
*/
/**
* Bitmap ID allocator for display list IDs.
*
* <p>Bitmap-based: each bit in the {@code data} array represents one ID.
* Bit set = allocated, bit clear = free. ID 0 is reserved (never returned).
*/
public class DisplayListIDAllocator {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
public DisplayListIDAllocator() {
this(32); // 1024 IDs initially
}
public DisplayListIDAllocator(int initialWords) {
if (initialWords < 1) initialWords = 1;
this.data = new int[initialWords];
this.numElements = initialWords;
this.numSetElements = 0;
this.lowestFreeIdx = 0;
// Reserve ID 0 (never returned), matching Mesa's skip_zero pattern
data[0] = 1;
numSetElements = 1;
}
public int allocRange(int count) {
if (count <= 0) throw new IllegalArgumentException("count must be > 0");
if (count == 1) return allocSingle();
return allocMultiple(count);
}
private int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
private int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
}
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
}
private int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
}
public void free(int id) {
if (id <= 0) return; // ID 0 is reserved
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
}
public void freeRange(int base, int count) {
if (count <= 0 || base <= 0) return;
int startIdx = base / 32;
if (startIdx >= numElements) return;
int end = base + count;
for (int id = base; id < end; id++) {
int idx = id / 32;
if (idx >= numElements) break;
data[idx] &= ~(1 << (id % 32));
}
lowestFreeIdx = Math.min(startIdx, lowestFreeIdx);
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
public boolean isAllocated(int id) {
if (id <= 0) return false;
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
}
}
@@ -27,14 +27,14 @@ import org.joml.Vector3f;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
/**
* VBO-based display list emulation with command recording, transform collapsing, and format-based batching.
@@ -103,9 +103,6 @@ public class DisplayListManager {
private static List<List<String>> multMatrixSources = null; // Source ops for each MULT_MATRIX in raw buffer
private static List<String> drawRangeSources = null; // Source type for each DRAW_RANGE in final buffer
// For flushMatrix() - reusable buffer for executing collapsed transforms
private static final FloatBuffer flushMatrixBuffer = org.lwjgl.BufferUtils.createFloatBuffer(16);
// Nested compilation support - stack of parent contexts
private static final Deque<CompilationContext> compilationStack = new ArrayDeque<>();
@@ -177,11 +174,8 @@ public class DisplayListManager {
stateGeneration++;
}
// COMPILE_AND_EXECUTE: execute now. Bypass GLSM to avoid re-entering recording path.
// COMPILE_AND_EXECUTE: apply to GLSM software stack
if (glListMode == GL11.GL_COMPILE_AND_EXECUTE) {
flushMatrixBuffer.clear();
relativeTransform.get(flushMatrixBuffer);
GL11.glMultMatrix(flushMatrixBuffer);
GLStateManager.getMatrixStack().mul(relativeTransform);
}
@@ -358,6 +352,12 @@ public class DisplayListManager {
currentRecorder.recordViewport(x, y, width, height);
}
public static void recordScissor(int x, int y, int width, int height) {
if (currentRecorder == null) return;
drawBarrier();
currentRecorder.recordScissor(x, y, width, height);
}
public static void recordPointSize(float size) {
if (currentRecorder == null) return;
drawBarrier();
@@ -497,6 +497,12 @@ public class DisplayListManager {
currentRecorder.recordMaterial(face, pname, params);
}
public static void recordClipPlane(int plane, double a, double b, double c, double d) {
if (currentRecorder == null) return;
drawBarrier();
currentRecorder.recordClipPlane(plane, a, b, c, d);
}
public static void recordStencilFunc(int func, int ref, int mask) {
if (currentRecorder == null) return;
drawBarrier();
@@ -619,9 +625,6 @@ public class DisplayListManager {
applyTransformOp(singleTransform, x, y, z, op, rotationAxis);
if (currentRecorder != null) currentRecorder.recordMultMatrix(singleTransform);
if (glListMode == GL11.GL_COMPILE_AND_EXECUTE) {
flushMatrixBuffer.clear();
singleTransform.get(flushMatrixBuffer);
GL11.glMultMatrix(flushMatrixBuffer);
GLStateManager.getMatrixStack().mul(singleTransform);
}
return;
@@ -659,9 +662,6 @@ public class DisplayListManager {
}
if (glListMode == GL11.GL_COMPILE_AND_EXECUTE) {
flushMatrixBuffer.clear();
matrix.get(flushMatrixBuffer);
GL11.glMultMatrix(flushMatrixBuffer);
GLStateManager.getMatrixStack().mul(matrix);
}
return;
@@ -686,9 +686,6 @@ public class DisplayListManager {
if (DEBUG_DISPLAY_LISTS) {
if (currentRecorder != null) currentRecorder.recordMultMatrix(orthoFrustumTemp);
if (glListMode == GL11.GL_COMPILE_AND_EXECUTE) {
flushMatrixBuffer.clear();
orthoFrustumTemp.get(flushMatrixBuffer);
GL11.glMultMatrix(flushMatrixBuffer);
GLStateManager.getMatrixStack().mul(orthoFrustumTemp);
}
return;
@@ -709,9 +706,6 @@ public class DisplayListManager {
if (DEBUG_DISPLAY_LISTS) {
if (currentRecorder != null) currentRecorder.recordMultMatrix(orthoFrustumTemp);
if (glListMode == GL11.GL_COMPILE_AND_EXECUTE) {
flushMatrixBuffer.clear();
orthoFrustumTemp.get(flushMatrixBuffer);
GL11.glMultMatrix(flushMatrixBuffer);
GLStateManager.getMatrixStack().mul(orthoFrustumTemp);
}
return;
@@ -956,11 +950,8 @@ public class DisplayListManager {
vbo.render();
}
// Per OpenGL spec: if list is undefined, glCallList has no effect
} else {
// Positive IDs - fall back to native GL display lists
// This happens for lists allocated but never compiled via glNewList
GL11.glCallList(list);
}
// An uncached positive-ID list = no-op (per OpenGL spec)
}
/**
@@ -971,8 +962,6 @@ public class DisplayListManager {
CompiledDisplayList compiled = displayListCache.remove(i);
if (compiled != null) {
compiled.delete();
} else {
GL11.glDeleteLists(i, 1);
}
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -87,22 +88,20 @@ public class Feature {
GLStateManager.alphaTest // GL_ALPHA_TEST enable bit
, GLStateManager.alphaState // Alpha test function and reference value
, GLStateManager.blendMode // GL_BLEND enable bit
, GLStateManager.blendState // Blending source and destination functions
// Constant blend color
// Blending equation
, GLStateManager.blendState // Blending source/destination functions, equation, blend color
, GLStateManager.colorLogicOpState // GL_COLOR_LOGIC_OP enable bit
, GLStateManager.ditherState // GL_DITHER enable bit
, GLStateManager.drawBuffer // GL_DRAW_BUFFER setting
, GLStateManager.indexLogicOpState // GL_INDEX_LOGIC_OP enable bit
// Logic op function
, GLStateManager.logicOpMode // Logic op function
, GLStateManager.colorMask // Color-mode and index-mode writemasks
, GLStateManager.clearColor // Color-mode and index-mode clear values
));
attribToFeatures.put(GL11.GL_CURRENT_BIT, ImmutableSet.of(
GLStateManager.color // Current RGBA color
, ShaderManager.getNormalStack() // Current normal vector
, ShaderManager.getTexCoordStack() // Current texture coordinates
// Current color index
// Current normal vector
// Current texture coordinates
// Current raster position
// GL_CURRENT_RASTER_POSITION_VALID flag
// RGBA color associated with current raster position
@@ -300,6 +299,7 @@ public class Feature {
));
final Set<IStateStack<?>> textureAttribs = new HashSet<>(ImmutableSet.of(
GLStateManager.activeTextureUnit // Active texture unit
// GL_TEXTURE_ENV_MODE + GL_TEXTURE_ENV_COLOR — now per-unit in TexEnvState (added below)
// Enable bits for the four texture coordinates
// Border color for each texture image
@@ -307,17 +307,16 @@ public class Feature {
// Magnification function for each texture image
// Texture coordinates and wrap mode for each texture image
// Color and mode for each texture environment
// Enable bits GL_TEXTURE_GEN_x, x is S, T, R, and Q
// GL_TEXTURE_GEN_MODE setting for S, T, R, and Q
// glTexGen plane equations for S, T, R, and Q
// Current texture bindings (for example, GL_TEXTURE_BINDING_2D) - Below
));
// Current Texture Bindings - GL_TEXTURE_BINDING_2D
// Current Texture Bindings - GL_TEXTURE_BINDING_2D + per-unit TexEnvState
for(int i = 0 ; i < GLStateManager.MAX_TEXTURE_UNITS; i++) {
textureAttribs.add(GLStateManager.textures.getTextureUnitBindings(i));
// textureAttribs.add(GLStateManager.textures.getInfo(i))
textureAttribs.add(GLStateManager.textures.getTexEnvState(i));
}
// Enable bits GL_TEXTURE_GEN_x where x is S, T, R, or Q
@@ -0,0 +1,388 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
/**
* Software emulation of GL feedback mode
*/
public class FeedbackManager {
private static final int FB_3D = 0x01;
private static final int FB_4D = 0x02;
private static final int FB_COLOR = 0x04;
private static final int FB_TEXTURE = 0x08;
private static int renderMode = GL11.GL_RENDER;
private static FloatBuffer feedbackBuffer;
private static int feedbackBufferSize;
private static int feedbackMask;
private static int feedbackCount;
private static final Matrix4f mvpMatrix = new Matrix4f();
private static final Vector4f clipVec = new Vector4f();
private static ByteBuffer readbackBuf = ByteBuffer.allocateDirect(256 * 1024).order(ByteOrder.nativeOrder());
private static ByteBuffer indexReadbackBuf = ByteBuffer.allocateDirect(4 * 1024).order(ByteOrder.nativeOrder());
private static final IntBuffer attribBuf = ByteBuffer.allocateDirect(16 * 4).order(ByteOrder.nativeOrder()).asIntBuffer();
@FunctionalInterface
private interface IndexReader {
int read(int i);
}
public static boolean isFeedbackMode() {
return renderMode == GL11.GL_FEEDBACK;
}
public static void glFeedbackBuffer(int type, FloatBuffer buffer) {
if (renderMode == GL11.GL_FEEDBACK) {
AngelicaTweaker.LOGGER.error("glFeedbackBuffer called while already in feedback mode");
return;
}
feedbackBuffer = buffer;
feedbackBufferSize = buffer.remaining();
feedbackMask = computeMask(type);
feedbackCount = 0;
}
public static int glRenderMode(int mode) {
int result = 0;
switch (renderMode) {
case GL11.GL_FEEDBACK:
result = (feedbackCount > feedbackBufferSize) ? -1 : feedbackCount;
break;
case GL11.GL_RENDER:
case GL11.GL_SELECT:
result = 0;
break;
}
if (mode == GL11.GL_SELECT) {
AngelicaTweaker.LOGGER.warn("glRenderMode(GL_SELECT): selection mode not emulated");
}
feedbackCount = 0;
renderMode = mode;
return result;
}
public static void glPassThrough(float token) {
if (renderMode == GL11.GL_FEEDBACK) {
feedbackToken(GL11.GL_PASS_THROUGH_TOKEN);
feedbackToken(token);
}
}
private static void feedbackToken(float token) {
if (feedbackCount < feedbackBufferSize) {
feedbackBuffer.put(feedbackBuffer.position() + feedbackCount, token);
}
feedbackCount++;
}
private static void feedbackVertex(float winX, float winY, float winZ, float winW) {
feedbackToken(winX);
feedbackToken(winY);
if ((feedbackMask & FB_3D) != 0) feedbackToken(winZ);
if ((feedbackMask & FB_4D) != 0) feedbackToken(winW);
if ((feedbackMask & FB_COLOR) != 0) {
feedbackToken(1f);
feedbackToken(1f);
feedbackToken(1f);
feedbackToken(1f);
}
if ((feedbackMask & FB_TEXTURE) != 0) {
feedbackToken(0f);
feedbackToken(0f);
feedbackToken(0f);
feedbackToken(1f);
}
}
private static void transformAndEmitVertex(float x, float y, float z) {
clipVec.set(x, y, z, 1.0f);
mvpMatrix.transform(clipVec);
final float w = clipVec.w;
final float ndcX = clipVec.x / w;
final float ndcY = clipVec.y / w;
final float ndcZ = clipVec.z / w;
final ViewportState vp = GLStateManager.getViewportState();
final float winX = vp.x + vp.width * (ndcX + 1.0f) * 0.5f;
final float winY = vp.y + vp.height * (ndcY + 1.0f) * 0.5f;
final float winZ = (float) ((vp.depthRangeFar - vp.depthRangeNear) * ndcZ
+ (vp.depthRangeFar + vp.depthRangeNear)) * 0.5f;
final float winW = 1.0f / w;
feedbackVertex(winX, winY, winZ, winW);
}
public static void processDrawArrays(int mode, int first, int count) {
computeMVP();
int stride = queryPositionStride();
if (stride == 0) stride = 12; // tightly packed 3 floats
final long posOffset = queryPositionOffset();
final int byteCount = count * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, (long) first * stride + posOffset, readbackBuf);
emitAllPrimitives(mode, count, i -> i, stride);
}
public static void processDrawElements(int mode, int indexCount, int type, long eboOffset) {
computeMVP();
int stride = queryPositionStride();
if (stride == 0) stride = 12;
final int elementSize = elementSizeForType(type);
final int indexByteCount = indexCount * elementSize;
ensureIndexReadbackCapacity(indexByteCount);
indexReadbackBuf.clear().limit(indexByteCount);
GL15.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboOffset, indexReadbackBuf);
final ByteBuffer indexBuf = indexReadbackBuf;
int minIdx = Integer.MAX_VALUE, maxIdx = Integer.MIN_VALUE;
for (int i = 0; i < indexCount; i++) {
final int idx = readIndex(indexBuf, type, i);
if (idx < minIdx) minIdx = idx;
if (idx > maxIdx) maxIdx = idx;
}
final long posOffset = queryPositionOffset();
final int vertexCount = maxIdx - minIdx + 1;
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, (long) minIdx * stride + posOffset, readbackBuf);
final int base = minIdx;
emitAllPrimitives(mode, indexCount, i -> readIndex(indexBuf, type, i) - base, stride);
}
public static void processDrawElements(int mode, ByteBuffer indices) {
final int basePos = indices.position();
processDrawElementsImpl(mode, indices.remaining(), i -> indices.get(basePos + i) & 0xFF);
}
public static void processDrawElements(int mode, ShortBuffer indices) {
final int basePos = indices.position();
processDrawElementsImpl(mode, indices.remaining(), i -> indices.get(basePos + i) & 0xFFFF);
}
public static void processDrawElements(int mode, IntBuffer indices) {
final int basePos = indices.position();
processDrawElementsImpl(mode, indices.remaining(), i -> indices.get(basePos + i));
}
public static void processDrawElements(int mode, int count, int type, ByteBuffer indices) {
final int pos = indices.position();
final IndexReader reader = switch (type) {
case GL11.GL_UNSIGNED_BYTE -> i -> indices.get(pos + i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> i -> indices.getShort(pos + i * 2) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> i -> indices.getInt(pos + i * 4);
default -> i -> 0;
};
processDrawElementsImpl(mode, count, reader);
}
private static void processDrawElementsImpl(int mode, int count, IndexReader reader) {
computeMVP();
int stride = queryPositionStride();
if (stride == 0) stride = 12;
int minIdx = Integer.MAX_VALUE, maxIdx = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
final int idx = reader.read(i);
if (idx < minIdx) minIdx = idx;
if (idx > maxIdx) maxIdx = idx;
}
readVerticesFromVBO(minIdx, maxIdx, stride);
final int base = minIdx;
emitAllPrimitives(mode, count, i -> reader.read(i) - base, stride);
}
private static void computeMVP() {
mvpMatrix.set(GLStateManager.getProjectionMatrix()).mul(GLStateManager.getModelViewMatrix());
}
private static int queryPositionStride() {
attribBuf.clear();
GL20.glGetVertexAttrib(0, GL20.GL_VERTEX_ATTRIB_ARRAY_STRIDE, attribBuf);
return attribBuf.get(0);
}
private static int computeMask(int type) {
return switch (type) {
case GL11.GL_2D -> 0;
case GL11.GL_3D -> FB_3D;
case GL11.GL_3D_COLOR -> FB_3D | FB_COLOR;
case GL11.GL_3D_COLOR_TEXTURE -> FB_3D | FB_COLOR | FB_TEXTURE;
case GL11.GL_4D_COLOR_TEXTURE -> FB_3D | FB_4D | FB_COLOR | FB_TEXTURE;
default -> 0;
};
}
private static int elementSizeForType(int type) {
return switch (type) {
case GL11.GL_UNSIGNED_BYTE -> 1;
case GL11.GL_UNSIGNED_SHORT -> 2;
case GL11.GL_UNSIGNED_INT -> 4;
default -> 4;
};
}
private static int readIndex(ByteBuffer buf, int type, int i) {
return switch (type) {
case GL11.GL_UNSIGNED_BYTE -> buf.get(i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> buf.getShort(i * 2) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> buf.getInt(i * 4);
default -> 0;
};
}
private static void ensureReadbackCapacity(int bytes) {
if (readbackBuf.capacity() < bytes) {
readbackBuf = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
}
}
private static void ensureIndexReadbackCapacity(int bytes) {
if (indexReadbackBuf.capacity() < bytes) {
indexReadbackBuf = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder());
}
}
private static long queryPositionOffset() {
final ByteBuffer buf = GL20.glGetVertexAttribPointer(0, GL20.GL_VERTEX_ATTRIB_ARRAY_POINTER, 8);
return buf == null ? 0L : buf.getLong(0);
}
private static void readVerticesFromVBO(int minIdx, int maxIdx, int stride) {
final long posOffset = queryPositionOffset();
final int vertexCount = maxIdx - minIdx + 1;
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, (long) minIdx * stride + posOffset, readbackBuf);
}
private static void emitVertex(int vertexIndex, int stride) {
final float x = readbackBuf.getFloat(vertexIndex * stride);
final float y = readbackBuf.getFloat(vertexIndex * stride + 4);
final float z = readbackBuf.getFloat(vertexIndex * stride + 8);
transformAndEmitVertex(x, y, z);
}
private static void emitAllPrimitives(int mode, int count, IndexReader reader, int stride) {
switch (mode) {
case GL11.GL_POINTS:
for (int i = 0; i < count; i++) {
feedbackToken(GL11.GL_POINT_TOKEN);
emitVertex(reader.read(i), stride);
}
break;
case GL11.GL_LINES:
for (int i = 0; i + 1 < count; i += 2) {
feedbackToken(i == 0 ? GL11.GL_LINE_RESET_TOKEN : GL11.GL_LINE_TOKEN);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
break;
case GL11.GL_LINE_STRIP:
for (int i = 0; i + 1 < count; i++) {
feedbackToken(i == 0 ? GL11.GL_LINE_RESET_TOKEN : GL11.GL_LINE_TOKEN);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
break;
case GL11.GL_LINE_LOOP:
for (int i = 0; i + 1 < count; i++) {
feedbackToken(i == 0 ? GL11.GL_LINE_RESET_TOKEN : GL11.GL_LINE_TOKEN);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
if (count > 2) {
feedbackToken(GL11.GL_LINE_TOKEN);
emitVertex(reader.read(count - 1), stride);
emitVertex(reader.read(0), stride);
}
break;
case GL11.GL_TRIANGLES:
for (int i = 0; i + 2 < count; i += 3) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(3);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i + 2), stride);
}
break;
case GL11.GL_TRIANGLE_STRIP:
for (int i = 0; i + 2 < count; i++) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(3);
if ((i & 1) == 0) {
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
} else {
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i), stride);
}
emitVertex(reader.read(i + 2), stride);
}
break;
case GL11.GL_TRIANGLE_FAN:
for (int i = 1; i + 1 < count; i++) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(3);
emitVertex(reader.read(0), stride);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
}
break;
case GL11.GL_QUADS:
for (int i = 0; i + 3 < count; i += 4) {
feedbackToken(GL11.GL_POLYGON_TOKEN);
feedbackToken(4);
emitVertex(reader.read(i), stride);
emitVertex(reader.read(i + 1), stride);
emitVertex(reader.read(i + 2), stride);
emitVertex(reader.read(i + 3), stride);
}
break;
default:
throw new UnsupportedOperationException("Feedback mode: unsupported primitive mode 0x" + Integer.toHexString(mode));
}
}
static void reset() {
renderMode = GL11.GL_RENDER;
feedbackBuffer = null;
feedbackBufferSize = 0;
feedbackMask = 0;
feedbackCount = 0;
}
}
@@ -620,6 +620,7 @@ public final class GLDebug {
private int depth = 0;
private static final int maxDepth = GL11.glGetInteger(KHRDebug.GL_MAX_DEBUG_GROUP_STACK_DEPTH);
private static final int maxNameLength = GL11.glGetInteger(KHRDebug.GL_MAX_LABEL_LENGTH);
private final String[] groupStack = new String[maxDepth + 1];
@Override
public void nameObject(int id, int object, String name) {
@@ -628,20 +629,28 @@ public final class GLDebug {
@Override
public void pushGroup(String name) {
depth++;
if (depth > maxDepth) {
throw new RuntimeException("Stack overflow");
if (depth + 1 > maxDepth) {
final StringBuilder sb = new StringBuilder();
sb.append("Debug group stack overflow (maxDepth=").append(maxDepth).append("), current stack:\n");
for (int i = 0; i < depth; i++) {
sb.append(" [").append(i).append("] ").append(groupStack[i]).append('\n');
}
sb.append(" -> ").append(name).append(" (rejected)");
throw new RuntimeException(sb.toString());
}
groupStack[depth] = name;
KHRDebug.glPushDebugGroup(KHRDebug.GL_DEBUG_SOURCE_APPLICATION, ID, name);
depth++;
}
@Override
public void popGroup() {
depth--;
if (depth < 0) {
if (depth - 1 < 0) {
throw new RuntimeException("Stack underflow");
}
KHRDebug.glPopDebugGroup();
depth--;
groupStack[depth] = null;
}
@Override
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,111 @@
package com.gtnewhorizons.angelica.glsm;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Shared GLSL shader transformation utilities.
*/
public class GlslTransformUtils {
private static final String RENAMED_PREFIX = "angelica_renamed_";
/** Texture function renames */
public static final Map<String, String> TEXTURE_RENAMES = Map.ofEntries(
Map.entry("texture2D", "texture"),
Map.entry("texture3D", "texture"),
Map.entry("texture2DLod", "textureLod"),
Map.entry("texture3DLod", "textureLod"),
Map.entry("texture2DProj", "textureProj"),
Map.entry("texture3DProj", "textureProj"),
Map.entry("texture2DGrad", "textureGrad"),
Map.entry("texture2DGradARB", "textureGrad"),
Map.entry("texture3DGrad", "textureGrad"),
Map.entry("texelFetch2D", "texelFetch"),
Map.entry("texelFetch3D", "texelFetch"),
Map.entry("textureSize2D", "textureSize")
);
private static final Pattern TEXTURE_PATTERN = Pattern.compile("\\btexture\\s*\\(|(\\btexture\\b)");
/** Reserved words added in later GLSL versions that may appear as identifiers in older shaders. */
private record ReservedWordRename(Pattern pattern, String replacement) {}
private static final Map<Integer, List<ReservedWordRename>> VERSIONED_RESERVED_WORDS = Map.of(
400, List.of(new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"))
);
public static String replaceTexture(String input) {
final Matcher matcher = TEXTURE_PATTERN.matcher(input);
final StringBuilder builder = new StringBuilder();
while (matcher.find()) {
if (matcher.group(1) != null) {
matcher.appendReplacement(builder, RENAMED_PREFIX + "texture");
} else {
matcher.appendReplacement(builder, Matcher.quoteReplacement(matcher.group(0)));
}
}
matcher.appendTail(builder);
return builder.toString();
}
public static String renameReservedWords(String source, int targetVersion) {
for (var entry : VERSIONED_RESERVED_WORDS.entrySet()) {
if (targetVersion < entry.getKey()) {
for (var rename : entry.getValue()) {
source = rename.pattern().matcher(source).replaceAll(rename.replacement());
}
}
}
return source;
}
public static String restoreReservedWords(String source) {
source = source.replace(RENAMED_PREFIX + "texture", "texture");
source = source.replace(RENAMED_PREFIX + "sample", "sample");
return source;
}
public static String getFormattedShader(ParseTree tree, String header) {
final StringBuilder sb = new StringBuilder(header + "\n");
final String[] tabHolder = {""};
getFormattedShader(tree, sb, tabHolder);
return sb.toString();
}
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder) {
if (tree instanceof TerminalNode) {
final String text = tree.getText();
if (text.equals("<EOF>")) {
return;
}
if (text.equals("#")) {
stringBuilder.append("\n#");
return;
}
stringBuilder.append(text);
if (text.equals("{")) {
stringBuilder.append(" \n\t");
tabHolder[0] = "\t";
}
if (text.equals("}")) {
if (stringBuilder.length() >= 2) {
stringBuilder.deleteCharAt(stringBuilder.length() - 2);
}
tabHolder[0] = "";
stringBuilder.append(" \n");
} else {
stringBuilder.append(text.equals(";") ? " \n" + tabHolder[0] : " ");
}
} else {
for (int i = 0; i < tree.getChildCount(); ++i) {
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder);
}
}
}
}
@@ -0,0 +1,299 @@
package com.gtnewhorizons.angelica.glsm;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetByte;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetShort;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutShort;
/**
* Shared quad-to-triangle EBO for core profile GL_QUADS emulation.
*
* <p>Maintains a single growable element buffer with pre-computed indices mapping
* GL_QUADS vertex order to GL_TRIANGLES: {@code (0,1,3, 1,2,3)} per quad.
* Uses {@code GL_LAST_VERTEX_CONVENTION} winding (Mesa {@code do_quad} LAST path)
* so both emitted triangles share the quad's provoking vertex (v3).
*/
public final class QuadConverter {
public static final int INDEX_TYPE = GL11.GL_UNSIGNED_INT;
private static int eboId;
private static int maxQuads;
/** Scratch EBO for converted quad-element draws (drawQuadElementsAsTriangles). */
private static int scratchEboId;
private static int scratchEboCapacity; // in bytes
private QuadConverter() {}
private static void ensureCapacity(int quadCount) {
if (quadCount <= maxQuads) return;
// Round up to power of 2 for amortized growth
int newMaxQuads = Math.max(256, maxQuads);
while (newMaxQuads < quadCount) {
newMaxQuads *= 2;
}
if (eboId == 0) {
eboId = GL15.glGenBuffers();
}
// Generate index data: 6 ints per quad
final ByteBuffer indexData = memAlloc(newMaxQuads * 6 * 4);
long ptr = memAddress0(indexData);
for (int i = 0; i < newMaxQuads; i++) {
final int base = i * 4;
// GL_LAST_VERTEX_CONVENTION: both triangles share v3 as provoking vertex
// Triangle 1: 0, 1, 3
memPutInt(ptr, base);
memPutInt(ptr + 4, base + 1);
memPutInt(ptr + 8, base + 3);
// Triangle 2: 1, 2, 3
memPutInt(ptr + 12, base + 1);
memPutInt(ptr + 16, base + 2);
memPutInt(ptr + 20, base + 3);
ptr += 24;
}
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indexData, GL15.GL_STATIC_DRAW);
memFree(indexData);
maxQuads = newMaxQuads;
}
/**
* Convert a GL_QUADS glDrawArrays call to indexed GL_TRIANGLES.
* Binds the shared EBO to the current VAO and issues glDrawElements.
*
* @param first first vertex index (must be aligned to quad boundary, i.e. multiple of 4)
* @param vertexCount number of vertices (must be multiple of 4)
*/
public static void drawQuadsAsTriangles(int first, int vertexCount) {
assert first % 4 == 0 : "QuadConverter: first (" + first + ") must be a multiple of 4";
assert vertexCount % 4 == 0 : "QuadConverter: vertexCount (" + vertexCount + ") must be a multiple of 4";
final int quadCount = vertexCount / 4;
final int prevEbo = GLStateManager.getBoundEBO();
ensureCapacity(first / 4 + quadCount);
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
// Index offset: first vertex / 4 quads * 6 indices * 4 bytes per int
final long indexOffset = (long) (first / 4) * 6 * 4;
GL11.glDrawElements(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset);
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
}
/**
* Upload converted triangle indices to the scratch EBO and draw.
* Saves/restores the caller's EBO binding so subsequent indexed draws
* (or glGetBufferSubData reads) still see the original EBO.
*
* @param dst off-heap buffer with triangle index data (freed by this method)
* @param triIndexCount number of triangle indices
* @param indexType GL_UNSIGNED_INT or GL_UNSIGNED_SHORT
* @param bytesPerIndex bytes per index element
*/
private static void uploadAndDraw(ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex) {
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = GLStateManager.getBoundEBO();
if (scratchEboId == 0) {
scratchEboId = GL15.glGenBuffers();
}
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, scratchEboId);
if (needed > scratchEboCapacity) {
// Power-of-2 growth — allocate full capacity, upload actual data
int newCap = Math.max(4096, scratchEboCapacity);
while (newCap < needed) newCap *= 2;
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, newCap, GL15.GL_STREAM_DRAW);
scratchEboCapacity = newCap;
}
GL15.glBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, 0, dst);
GL11.glDrawElements(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L);
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
memFree(dst);
}
/**
* Convert an indexed GL_QUADS draw to GL_TRIANGLES by reading the source EBO,
* building a triangle index buffer, and issuing the converted draw.
*
* @param indexCount number of quad indices (must be multiple of 4)
* @param type GL_UNSIGNED_INT, GL_UNSIGNED_SHORT, or GL_UNSIGNED_BYTE
* @param offset byte offset into the currently bound EBO
*/
public static void drawQuadElementsAsTriangles(int indexCount, int type, long offset) {
if (indexCount == 0) return;
assert indexCount % 4 == 0 : "QuadConverter: indexCount must be multiple of 4";
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final int bytesPerIndex;
switch (type) {
case GL11.GL_UNSIGNED_INT: bytesPerIndex = 4; break;
case GL11.GL_UNSIGNED_SHORT: bytesPerIndex = 2; break;
case GL11.GL_UNSIGNED_BYTE: bytesPerIndex = 1; break;
default: return;
}
// Read source indices from caller's EBO
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
GL15.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, offset, src);
final long srcAddr = memAddress0(src);
// Allocate output as GL_UNSIGNED_INT
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
default /* GL_UNSIGNED_BYTE */ -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
}
// GL_LAST_VERTEX_CONVENTION: (a,b,d), (b,c,d)
final long o = (long) i * 24;
memPutInt(dstAddr + o, a);
memPutInt(dstAddr + o + 4, b);
memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b);
memPutInt(dstAddr + o + 16, c);
memPutInt(dstAddr + o + 20, d);
}
memFree(src);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Convert a client-side IntBuffer of quad indices to triangles.
*/
public static void drawQuadElementsAsTriangles(IntBuffer indices) {
final int indexCount = indices.remaining();
if (indexCount == 0) return;
assert indexCount % 4 == 0;
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final int a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Convert a client-side ShortBuffer of quad indices to triangles.
*/
public static void drawQuadElementsAsTriangles(ShortBuffer indices) {
final int indexCount = indices.remaining();
if (indexCount == 0) return;
assert indexCount % 4 == 0;
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 2);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final short a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 12;
memPutShort(dstAddr + o, a); memPutShort(dstAddr + o + 2, b); memPutShort(dstAddr + o + 4, d);
memPutShort(dstAddr + o + 6, b); memPutShort(dstAddr + o + 8, c); memPutShort(dstAddr + o + 10, d);
}
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
}
/**
* Convert a client-side ByteBuffer of quad indices (type-agnostic) to triangles.
*/
public static void drawQuadElementsAsTriangles(int count, int type, ByteBuffer indices) {
if (count == 0) return;
assert count % 4 == 0;
final int quadCount = count / 4;
final int triIndexCount = quadCount * 6;
final long srcAddr = memAddress0(indices) + indices.position();
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
case GL11.GL_UNSIGNED_BYTE -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
default -> { memFree(dst); return; }
}
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
/**
* Clean up the shared EBO.
*/
public static void destroy() {
if (eboId != 0) {
GL15.glDeleteBuffers(eboId);
eboId = 0;
maxQuads = 0;
}
if (scratchEboId != 0) {
GL15.glDeleteBuffers(scratchEboId);
scratchEboId = 0;
scratchEboCapacity = 0;
}
}
}
@@ -1,10 +1,13 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSAAccess;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSACore;
import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import net.coderbot.iris.gl.shader.StandardMacros;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -22,6 +25,7 @@ import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL33;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL42;
@@ -29,8 +33,6 @@ import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.GL44;
import org.lwjgl.opengl.NVXGpuMemoryInfo;
import net.coderbot.iris.gl.shader.StandardMacros;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -62,7 +64,7 @@ public class RenderSystem {
public static void initRenderer() {
try {
if (GLStateManager.capabilities.OpenGL45) {
dsaState = new DSACore();
dsaState = (Runtime.version().feature() > 8 && GLStateManager.capabilities.GL_EXT_direct_state_access) ? new DSAEXT() : new DSACore();
LOGGER.info("OpenGL 4.5 detected, enabling DSA.");
}
@@ -124,6 +126,15 @@ public class RenderSystem {
LOGGER.info("Image Load/Store: {}, Max Image Units: {}", supportsImageLoadStore, maxImageUnits);
LOGGER.info("SSBO: {}, Max SSBO Bindings: {}", supportsSSBO, maxSSBOBindings);
LOGGER.info("Buffer Storage: {}, Clear Texture: {}, Sampler Objects: {}", supportsBufferStorage, supportsClearTexture, supportsSamplerObjects);
if (GLStateManager.capabilities.OpenGL32) {
final int profileMask = GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
LOGGER.info("GL 3.3 core profile detected, enabling FFP shader emulation.");
ShaderManager.getInstance().enable();
GLStateManager.lineWidthMax = 1.0f;
}
}
}
public static void generateMipmaps(int texture, int mipmapTarget) {
@@ -347,10 +358,12 @@ public class RenderSystem {
public static void disableBufferBlend(int buffer) {
GL30.glDisablei(GL11.GL_BLEND, buffer);
GLStateManager.getBlendMode().setUnknownState();
}
public static void enableBufferBlend(int buffer) {
GL30.glEnablei(GL11.GL_BLEND, buffer);
GLStateManager.getBlendMode().setUnknownState();
}
public static void blendFuncSeparatei(int buffer, int srcRGB, int dstRGB, int srcAlpha, int dstAlpha) {
@@ -40,6 +40,7 @@ public class DSAARB extends DSAUnsupported {
@Override
public void texParameteri(int texture, int target, int pname, int param) {
param = GLStateManager.remapTexClamp(pname, param);
if(!GLStateManager.updateTexParameteriCache(target, texture, pname, param)) return;
ARBDirectStateAccess.glTextureParameteri(texture, pname, param);
@@ -47,6 +48,7 @@ public class DSAARB extends DSAUnsupported {
@Override
public void texParameterf(int texture, int target, int pname, float param) {
param = GLStateManager.remapTexClamp(pname, param);
if(!GLStateManager.updateTexParameterfCache(target, texture, pname, param)) return;
ARBDirectStateAccess.glTextureParameterf(texture, pname, param);
@@ -54,6 +56,7 @@ public class DSAARB extends DSAUnsupported {
@Override
public void texParameteriv(int texture, int target, int pname, IntBuffer params) {
GLStateManager.remapTexClampBuffer(pname, params);
ARBDirectStateAccess.glTextureParameter(texture, pname, params);
}
@@ -0,0 +1,18 @@
package com.gtnewhorizons.angelica.glsm.dsa;
import org.lwjgl.opengl.EXTDirectStateAccess;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
public class DSAEXT extends DSACore {
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
EXTDirectStateAccess.glTextureImage2DEXT(texture, target, level, internalformat, width, height, border, format, type, pixels);
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
EXTDirectStateAccess.glTextureImage2DEXT(texture, target, level, internalformat, width, height, border, format, type, pixels);
}
}
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraft.client.renderer.Tessellator;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.joml.Vector3f;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
/**
* DirectTessellator subclass for deferred particle batching. Accumulates vertex data from
* multiple interceptDraw() calls, recording per-draw metadata as {@link DrawRange} entries.
* Computes a delta modelview matrix at each intercept to pre-transform vertices.
*/
class DeferredBatchTessellator extends DirectTessellator {
@Desugar
record DrawRange(long stateKey, int byteOffset, int byteLength, int vertexCount, int drawMode, int flags) {}
private final List<DrawRange> ranges = new ArrayList<>();
private final Matrix4f defaultModelview = new Matrix4f();
private final Matrix4f defaultModelviewInverse = new Matrix4f();
private final Matrix4f deltaMatrix = new Matrix4f(); // reusable scratch
private final Vector3f scratch = new Vector3f();
private Matrix4fc currentTransform;
DeferredBatchTessellator(ByteBuffer buffer) {
super(buffer, false); // deleteAfter=false: keep buffer across resets for reuse
}
@Override
protected long writeVertexData(VertexFormat format, int[] rawBuffer, int rawBufferIndex) {
if (currentTransform != null) {
return format.writeToBuffer0(writePtr, rawBuffer, rawBufferIndex, currentTransform, scratch);
}
return super.writeVertexData(format, rawBuffer, rawBufferIndex);
}
/** Snapshot the current modelview as the "default" for this bracket. */
void snapshotDefaultModelview() {
defaultModelview.set(GLStateManager.getModelViewMatrix());
defaultModelview.invert(defaultModelviewInverse);
}
@Override
protected int interceptDraw(Tessellator tess) {
// Compute delta: if current MV differs from default, pre-transform vertices
final Matrix4f currentMV = GLStateManager.getModelViewMatrix();
if (!currentMV.equals(defaultModelview)) {
defaultModelviewInverse.mul(currentMV, deltaMatrix);
this.currentTransform = deltaMatrix;
} else {
this.currentTransform = null;
}
final int byteOffsetBefore = bufferLimit();
final int vertCount = tess.vertexCount;
final int drawMode = tess.drawMode;
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
final long stateKey = DeferredDrawBatcher.captureStateKey();
final int result = super.interceptDraw(tess); // repacks int[] → ByteBuffer + transforms if vertexTransform set
final int byteOffsetAfter = bufferLimit();
ranges.add(new DrawRange(stateKey, byteOffsetBefore, byteOffsetAfter - byteOffsetBefore, vertCount, drawMode, flags));
this.currentTransform = null; // clear for next draw
return result;
}
List<DrawRange> getRanges() { return ranges; }
void clearRanges() { ranges.clear(); }
void copyRange(int byteOffset, int byteLength, long destAddress) {
memCopy(startPtr + byteOffset, destAddress, byteLength);
}
@Override
protected void onRemovedFromStack() {
clearRanges();
super.onRemovedFromStack(); // calls reset(), keeps baseBuffer (deleteAfter=false)
}
}
@@ -0,0 +1,173 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import lombok.Getter;
import org.lwjgl.opengl.GL11;
import java.util.Comparator;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
/**
* Deferred draw batching coordinator. During rendering regions (e.g. particles),
* intercepts tessellator.draw() calls via DirectTessellator, captures pre-packed vertex data
* + GL state, then flushes all captured batches sorted by state key.
* <p>
* Vertices are pre-transformed at capture time.
*/
public class DeferredDrawBatcher {
private static final int INITIAL_BUFFER_SIZE = 64 * 1024;
@Getter private static boolean active = false;
private static DeferredBatchTessellator batchTessellator;
private DeferredDrawBatcher() {}
/**
* Begin deferred capture mode. Called after startDrawingQuads().
* Pushes a DeferredBatchTessellator onto the DirectTessellator stack so that
* subsequent tessellator.draw() calls are intercepted and accumulated.
*/
public static void enter() {
active = true;
if (batchTessellator == null) {
batchTessellator = new DeferredBatchTessellator(memAlloc(INITIAL_BUFFER_SIZE));
}
batchTessellator.clearRanges();
batchTessellator.snapshotDefaultModelview();
TessellatorManager.startCapturingDirect(batchTessellator);
}
/**
* Exit deferred mode and flush all captured batches. Groups entries by state key
* and vertex format, issues one draw per unique group. Restores the GL state that
* was active for the last group so vanilla sees consistent state.
*/
public static void exitAndFlush() {
active = false;
final List<DeferredBatchTessellator.DrawRange> ranges = batchTessellator.getRanges();
if (!ranges.isEmpty()) {
ranges.sort(Comparator.comparingLong(DeferredBatchTessellator.DrawRange::stateKey));
flushSorted(ranges);
}
TessellatorManager.stopCapturingDirect();
}
private static void flushSorted(List<DeferredBatchTessellator.DrawRange> ranges) {
long currentKey = ranges.get(0).stateKey();
int groupStart = 0;
for (int i = 0; i <= ranges.size(); i++) {
final boolean endOfList = (i == ranges.size());
final boolean keyChanged = !endOfList && ranges.get(i).stateKey() != currentKey;
if (endOfList || keyChanged) {
applyStateKey(currentKey);
flushGroup(ranges, groupStart, i);
if (!endOfList) {
currentKey = ranges.get(i).stateKey();
groupStart = i;
}
}
}
}
/**
* Flush a group of ranges sharing the same state key.
* Subgroups by (drawMode, format flags) since those must match for merged draws.
*/
private static void flushGroup(List<DeferredBatchTessellator.DrawRange> ranges, int from, int to) {
int i = from;
while (i < to) {
final DeferredBatchTessellator.DrawRange first = ranges.get(i);
final int drawMode = first.drawMode();
final int flags = first.flags();
int totalBytes = 0;
int totalVertices = 0;
int subEnd = i;
while (subEnd < to) {
final DeferredBatchTessellator.DrawRange e = ranges.get(subEnd);
if (e.drawMode() != drawMode || e.flags() != flags) break;
totalBytes += e.byteLength();
totalVertices += e.vertexCount();
subEnd++;
}
TessellatorStreamingDrawer.drawPackedBatch(
batchTessellator, ranges, i, subEnd, totalBytes, totalVertices, drawMode, flags);
i = subEnd;
}
}
/**
* 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
* GL blend constants (e.g. GL_SRC_ALPHA=0x0302) need 12 bits.
*
* NOT captured (uniform within a particle layer bracket — set once before startDrawingQuads,
* not changed by individual particles):
* - depth test enable (always GL_LEQUAL for particles)
* - alpha test func/ref (set per-layer, not per-particle)
* - separate blend alpha factors (srcAlpha/dstAlpha) — particles use glBlendFunc not glBlendFuncSeparate
* - fog state (mode, color, start/end — constant for the frame)
*/
static long captureStateKey() {
final int textureId = GLStateManager.getTextures().getTextureUnitBindings(0).getBinding();
final int srcRgb = GLStateManager.getBlendState().getSrcRgb();
final int dstRgb = GLStateManager.getBlendState().getDstRgb();
final boolean blendEnabled = GLStateManager.getBlendMode().isEnabled();
final boolean depthMask = GLStateManager.getDepthState().isEnabled();
final boolean tex0Enabled = GLStateManager.getTextures().getTextureUnitStates(0).isEnabled();
final boolean tex1Enabled = GLStateManager.getTextures().getTextureUnitStates(1).isEnabled();
return packStateKey(textureId, srcRgb, dstRgb, blendEnabled, depthMask, tex0Enabled, tex1Enabled);
}
static long packStateKey(int textureId, int srcRgb, int dstRgb, boolean blendEnabled, boolean depthMask, boolean tex0Enabled, boolean tex1Enabled) {
return ((long) (textureId & 0xFFFFF) << 26)
| ((long) (srcRgb & 0xFFF) << 14)
| ((long) (dstRgb & 0xFFF) << 2)
| (blendEnabled ? 2L : 0L)
| (depthMask ? 1L : 0L)
| (tex0Enabled ? (1L << 46) : 0L)
| (tex1Enabled ? (1L << 47) : 0L);
}
static int unpackTextureId(long key) { return (int) ((key >> 26) & 0xFFFFF); }
static int unpackSrcRgb(long key) { return (int) ((key >> 14) & 0xFFF); }
static int unpackDstRgb(long key) { return (int) ((key >> 2) & 0xFFF); }
static boolean unpackBlendEnabled(long key) { return (key & 2L) != 0; }
static boolean unpackDepthMask(long key) { return (key & 1L) != 0; }
static boolean unpackTex0Enabled(long key) { return (key & (1L << 46)) != 0; }
static boolean unpackTex1Enabled(long key) { return (key & (1L << 47)) != 0; }
static void applyStateKey(long key) {
final int textureId = unpackTextureId(key);
final int srcRgb = unpackSrcRgb(key);
final int dstRgb = unpackDstRgb(key);
final boolean blendEnabled = unpackBlendEnabled(key);
final boolean depthMask = unpackDepthMask(key);
final boolean tex0Enabled = unpackTex0Enabled(key);
final boolean tex1Enabled = unpackTex1Enabled(key);
GLStateManager.getTextures().getTextureUnitStates(0).setEnabled(tex0Enabled);
GLStateManager.getTextures().getTextureUnitStates(1).setEnabled(tex1Enabled);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
GLStateManager.glBlendFunc(srcRgb, dstRgb);
if (blendEnabled) {
GLStateManager.enableBlend();
} else {
GLStateManager.disableBlend();
}
GLStateManager.glDepthMask(depthMask);
}
}
@@ -0,0 +1,368 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import java.util.Arrays;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
/**
* Packed-long fragment shader permutation key for FFP emulation.
*
* Layout:
* long[0]: global bits (10) + unit 0 (47 bits at offset 10)
* long[1..3]: unit 1..3 (47 bits each, low-aligned)
* Only max(1, nrEnabledUnits) longs are significant.
*
* Per-unit packing (47 bits):
* 0: enabled
* 1-3: mode (MODULATE=0..COMBINE=5)
* 4: needsEnvColor
* 5-8: combineRgb (encoded 0-7)
* 9-12: combineAlpha (encoded 0-7)
* 13-14: scaleShiftRgb
* 15-16: scaleShiftAlpha
* 17-21: arg0 RGB (source:3 + operand:2)
* 22-26: arg1 RGB
* 27-31: arg2 RGB
* 32-36: arg0 Alpha
* 37-41: arg1 Alpha
* 42-46: arg2 Alpha
*/
public final class FragmentKey {
private static final Logger LOGGER = LogManager.getLogger("FragmentKey");
public static final int TEX_ENV_MODULATE = 0;
public static final int TEX_ENV_REPLACE = 1;
public static final int TEX_ENV_ADD = 2;
public static final int TEX_ENV_DECAL = 3;
public static final int TEX_ENV_BLEND = 4;
public static final int TEX_ENV_COMBINE = 5;
public static final int COMBINE_REPLACE = 0;
public static final int COMBINE_MODULATE = 1;
public static final int COMBINE_ADD = 2;
public static final int COMBINE_ADD_SIGNED = 3;
public static final int COMBINE_INTERPOLATE = 4;
public static final int COMBINE_SUBTRACT = 5;
public static final int COMBINE_DOT3_RGB = 6;
public static final int COMBINE_DOT3_RGBA = 7;
public static final int SRC_TEXTURE = 0;
public static final int SRC_CONSTANT = 1;
public static final int SRC_PRIMARY_COLOR = 2;
public static final int SRC_PREVIOUS = 3;
public static final int OP_SRC_COLOR = 0;
public static final int OP_ONE_MINUS_SRC_COLOR = 1;
public static final int OP_SRC_ALPHA = 2;
public static final int OP_ONE_MINUS_SRC_ALPHA = 3;
public static final int FOG_NONE = 0;
public static final int FOG_LINEAR = 1;
public static final int FOG_EXP = 2;
public static final int FOG_EXP2 = 3;
private static final int GLOBAL_BITS = 10;
private static final int BIT_FOG_MODE = 0; // 2 bits
private static final int BIT_ALPHA_TEST = 2; // 1 bit
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
private static final int BIT_SEPARATE_SPECULAR = 6; // 1 bit
private static final int BIT_NR_ENABLED_UNITS = 7; // 3 bits
private static final int U_ENABLED = 0;
private static final int U_MODE = 1; // 3 bits
private static final int U_NEEDS_ENV_COLOR = 4; // 1 bit
private static final int U_COMBINE_RGB = 5; // 4 bits
private static final int U_COMBINE_ALPHA = 9; // 4 bits
private static final int U_SCALE_RGB = 13; // 2 bits
private static final int U_SCALE_ALPHA = 15; // 2 bits
private static final int U_ARG0_RGB = 17; // 5 bits (source:3 + operand:2)
private static final int U_ARG1_RGB = 22; // 5 bits
private static final int U_ARG2_RGB = 27; // 5 bits
private static final int U_ARG0_ALPHA = 32; // 5 bits
private static final int U_ARG1_ALPHA = 37; // 5 bits
private static final int U_ARG2_ALPHA = 42; // 5 bits
static final int MAX_UNITS = 4;
private final long[] packed;
private final int hash;
private FragmentKey(long[] packed, int hash) {
this.packed = packed;
this.hash = hash;
}
/**
* Pack current GLSM fragment state into pre-allocated output array.
* @param out must be at least MAX_UNITS (4) longs
* @return number of significant longs (1..4)
*/
public static int packFromState(long[] out) {
long global = 0;
// Fog
if (GLStateManager.getFogMode().isEnabled()) {
final int fogMode = switch (GLStateManager.getFogState().getFogMode()) {
case GL11.GL_LINEAR -> FOG_LINEAR;
case GL11.GL_EXP -> FOG_EXP;
case GL11.GL_EXP2 -> FOG_EXP2;
default -> FOG_NONE;
};
global |= (long) fogMode;
}
// Alpha test
if (GLStateManager.getAlphaTest().isEnabled()) {
global |= (1L << BIT_ALPHA_TEST);
global |= ((long) (encodeAlphaFunc(GLStateManager.getAlphaState().getFunction()) & 0x7)) << BIT_ALPHA_FUNC;
}
// Separate specular
if (GLStateManager.getLightingState().isEnabled()
&& GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
global |= (1L << BIT_SEPARATE_SPECULAR);
}
// Per-unit state
int highestEnabled = -1;
long unit0Bits = 0;
for (int i = 0; i < MAX_UNITS; i++) {
final boolean texEnabled = GLStateManager.getTextures().getTextureUnitStates(i).isEnabled();
if (texEnabled) highestEnabled = i;
final long unitBits = packUnit(i, texEnabled);
if (i == 0) {
unit0Bits = unitBits;
} else {
out[i] = unitBits;
}
}
final int nrEnabled = highestEnabled + 1;
global |= ((long) (nrEnabled & 0x7)) << BIT_NR_ENABLED_UNITS;
global |= (unit0Bits << GLOBAL_BITS);
out[0] = global;
return Math.max(1, nrEnabled);
}
private static long packUnit(int unitIndex, boolean texEnabled) {
if (!texEnabled) return 0;
final TexEnvState envState = GLStateManager.getTextures().getTexEnvState(unitIndex);
final int mode = encodeTexEnvMode(envState.mode);
long bits = 1L; // enabled
bits |= ((long) (mode & 0x7)) << U_MODE;
boolean needsEnvColor = (mode == TEX_ENV_BLEND);
if (mode == TEX_ENV_COMBINE) {
final int combRgb = encodeCombineFunc(envState.combineRgb);
final int combAlpha = encodeCombineFunc(envState.combineAlpha);
bits |= ((long) (combRgb & 0xF)) << U_COMBINE_RGB;
bits |= ((long) (combAlpha & 0xF)) << U_COMBINE_ALPHA;
bits |= ((long) (encodeScale(envState.scaleRgb) & 0x3)) << U_SCALE_RGB;
bits |= ((long) (encodeScale(envState.scaleAlpha) & 0x3)) << U_SCALE_ALPHA;
// Only pack args actually used by each channel's combine function.
// Unused arg slots stay 0, preventing TexEnvState defaults from inflating the cache.
final int numArgsRgb = combineNumArgs(combRgb);
final int numArgsAlpha = combineNumArgs(combAlpha);
final int maxArgs = Math.max(numArgsRgb, numArgsAlpha);
for (int a = 0; a < maxArgs; a++) {
if (a < numArgsRgb) {
final int srcR = encodeSource(envState.sourceRgb[a]);
final int opR = encodeOperand(envState.operandRgb[a]);
bits |= ((long) ((srcR & 0x7) | ((opR & 0x3) << 3))) << (U_ARG0_RGB + a * 5);
if (srcR == SRC_CONSTANT) needsEnvColor = true;
}
if (a < numArgsAlpha) {
final int srcA = encodeSource(envState.sourceAlpha[a]);
final int opA = encodeOperand(envState.operandAlpha[a]);
bits |= ((long) ((srcA & 0x7) | ((opA & 0x3) << 3))) << (U_ARG0_ALPHA + a * 5);
if (srcA == SRC_CONSTANT) needsEnvColor = true;
}
}
}
if (needsEnvColor) {
bits |= (1L << U_NEEDS_ENV_COLOR);
}
return bits;
}
/** Create FragmentKey from packed scratch data. Only called on cache miss. */
public static FragmentKey fromPacked(long[] scratch, int len) {
final long[] copy = Arrays.copyOf(scratch, len);
return new FragmentKey(copy, Arrays.hashCode(copy));
}
/** Convenience: pack + create. Allocates. For infrequent paths. */
public static FragmentKey fromState() {
final long[] scratch = new long[MAX_UNITS];
final int len = packFromState(scratch);
return fromPacked(scratch, len);
}
@Override
public int hashCode() {
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof FragmentKey other)) return false;
if (hash != other.hash) return false;
return Arrays.equals(packed, other.packed);
}
public int fogMode() { return (int) (packed[0] & 0x3); }
public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; }
public int alphaTestFunc() { return (int) ((packed[0] >> BIT_ALPHA_FUNC) & 0x7); }
public boolean separateSpecular() { return ((packed[0] >> BIT_SEPARATE_SPECULAR) & 1) != 0; }
public int nrEnabledUnits() { return (int) ((packed[0] >> BIT_NR_ENABLED_UNITS) & 0x7); }
private long unitBits(int i) {
return (i == 0) ? (packed[0] >>> GLOBAL_BITS) : packed[i];
}
public boolean unitEnabled(int i) { return (unitBits(i) & 1) != 0; }
public int unitMode(int i) { return (int) ((unitBits(i) >> U_MODE) & 0x7); }
public boolean unitNeedsEnvColor(int i) { return ((unitBits(i) >> U_NEEDS_ENV_COLOR) & 1) != 0; }
public int unitCombineRgb(int i) { return (int) ((unitBits(i) >> U_COMBINE_RGB) & 0xF); }
public int unitCombineAlpha(int i) { return (int) ((unitBits(i) >> U_COMBINE_ALPHA) & 0xF); }
public int unitScaleShiftRgb(int i) { return (int) ((unitBits(i) >> U_SCALE_RGB) & 0x3); }
public int unitScaleShiftAlpha(int i) { return (int) ((unitBits(i) >> U_SCALE_ALPHA) & 0x3); }
public int unitSourceRgb(int i, int arg) { return (int) ((unitBits(i) >> (U_ARG0_RGB + arg * 5)) & 0x7); }
public int unitOperandRgb(int i, int arg) { return (int) ((unitBits(i) >> (U_ARG0_RGB + arg * 5 + 3)) & 0x3); }
public int unitSourceAlpha(int i, int arg) { return (int) ((unitBits(i) >> (U_ARG0_ALPHA + arg * 5)) & 0x7); }
public int unitOperandAlpha(int i, int arg) { return (int) ((unitBits(i) >> (U_ARG0_ALPHA + arg * 5 + 3)) & 0x3); }
public boolean textureEnabled() {
for (int i = 0; i < nrEnabledUnits(); i++) {
if (unitEnabled(i)) return true;
}
return false;
}
public boolean lightmapEnabled() {
return nrEnabledUnits() > 1 && unitEnabled(1);
}
private static int encodeAlphaFunc(int glFunc) {
return glFunc & 0x7;
}
static int encodeTexEnvMode(int glMode) {
return switch (glMode) {
case GL11.GL_MODULATE -> TEX_ENV_MODULATE;
case GL11.GL_REPLACE -> TEX_ENV_REPLACE;
case GL11.GL_ADD -> TEX_ENV_ADD;
case GL11.GL_DECAL -> TEX_ENV_DECAL;
case GL11.GL_BLEND -> TEX_ENV_BLEND;
case GL13.GL_COMBINE -> TEX_ENV_COMBINE;
default -> TEX_ENV_MODULATE;
};
}
static int encodeCombineFunc(int glFunc) {
return switch (glFunc) {
case GL11.GL_REPLACE -> COMBINE_REPLACE;
case GL11.GL_MODULATE -> COMBINE_MODULATE;
case GL11.GL_ADD -> COMBINE_ADD;
case GL13.GL_ADD_SIGNED -> COMBINE_ADD_SIGNED;
case GL13.GL_INTERPOLATE -> COMBINE_INTERPOLATE;
case GL13.GL_SUBTRACT -> COMBINE_SUBTRACT;
case GL13.GL_DOT3_RGB -> COMBINE_DOT3_RGB;
case GL13.GL_DOT3_RGBA -> COMBINE_DOT3_RGBA;
default -> COMBINE_REPLACE;
};
}
static int encodeSource(int glSource) {
return switch (glSource) {
case GL11.GL_TEXTURE -> SRC_TEXTURE;
case GL13.GL_CONSTANT -> SRC_CONSTANT;
case GL13.GL_PRIMARY_COLOR -> SRC_PRIMARY_COLOR;
case GL13.GL_PREVIOUS -> SRC_PREVIOUS;
default -> {
LOGGER.debug("Unknown TexEnv source 0x{}, falling back to SRC_PREVIOUS", Integer.toHexString(glSource));
yield SRC_PREVIOUS;
}
};
}
static int encodeOperand(int glOperand) {
return switch (glOperand) {
case GL11.GL_SRC_COLOR -> OP_SRC_COLOR;
case GL11.GL_ONE_MINUS_SRC_COLOR -> OP_ONE_MINUS_SRC_COLOR;
case GL11.GL_SRC_ALPHA -> OP_SRC_ALPHA;
case GL11.GL_ONE_MINUS_SRC_ALPHA -> OP_ONE_MINUS_SRC_ALPHA;
default -> OP_SRC_COLOR;
};
}
/** Number of source/operand args used by the given (encoded) combine function. */
public static int combineNumArgs(int func) {
return switch (func) {
case COMBINE_REPLACE -> 1;
case COMBINE_INTERPOLATE -> 3;
default -> 2; // MODULATE, ADD, ADD_SIGNED, SUBTRACT, DOT3_*
};
}
private static int encodeScale(float scale) {
if (scale >= 4.0f) return 2;
if (scale >= 2.0f) return 1;
return 0;
}
public static int decodeAlphaFunc(int encoded) {
return 0x0200 | encoded;
}
@Override
public String toString() {
final String fogName = switch (fogMode()) {
case FOG_NONE -> "NONE";
case FOG_LINEAR -> "LINEAR";
case FOG_EXP -> "EXP";
case FOG_EXP2 -> "EXP2";
default -> "?";
};
final StringBuilder sb = new StringBuilder();
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b units=%d",
fogName, alphaTestEnabled(),
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-",
separateSpecular(), nrEnabledUnits()));
for (int i = 0; i < nrEnabledUnits(); i++) {
if (!unitEnabled(i)) {
sb.append(String.format(" u%d=OFF", i));
} else {
final String modeName = switch (unitMode(i)) {
case TEX_ENV_MODULATE -> "MOD";
case TEX_ENV_REPLACE -> "REPL";
case TEX_ENV_ADD -> "ADD";
case TEX_ENV_DECAL -> "DEC";
case TEX_ENV_BLEND -> "BL";
case TEX_ENV_COMBINE -> "COMB";
default -> "?";
};
sb.append(String.format(" u%d=%s", i, modeName));
}
}
sb.append(']');
return sb.toString();
}
}
@@ -0,0 +1,316 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import org.lwjgl.opengl.GL11;
/**
* Generates GLSL 330 core fragment shaders for FFP emulation.
* Supports per-unit texenv chain including GL_COMBINE mode.
* Follows Mesa's ff_fragment_shader.c and st_nir_lower_fog.c.
*/
public final class FragmentShaderGenerator {
private FragmentShaderGenerator() {}
public static String generate(FragmentKey key) {
final StringBuilder sb = new StringBuilder(2048);
sb.append("#version 330 core\n\n");
emitInputs(sb, key);
emitUniforms(sb, key);
sb.append("out vec4 fragColor;\n\n");
sb.append("void main() {\n");
emitTextureSampling(sb, key);
emitTexEnvChain(sb, key);
emitSpecularAdd(sb, key);
emitAlphaTest(sb, key);
emitFog(sb, key);
sb.append(" fragColor = color;\n");
sb.append("}\n");
return sb.toString();
}
private static void emitInputs(StringBuilder sb, FragmentKey key) {
sb.append("// Inputs from vertex shader\n");
sb.append("in vec4 v_Color;\n");
if (key.separateSpecular()) {
sb.append("in vec3 v_SpecularColor;\n");
}
if (key.textureEnabled()) {
sb.append("in vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("in vec4 v_TexCoord1;\n");
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("in float v_FogCoord;\n");
}
sb.append('\n');
}
private static void emitUniforms(StringBuilder sb, FragmentKey key) {
// Samplers for each enabled unit
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (key.unitEnabled(i)) {
sb.append("uniform sampler2D u_Sampler").append(i).append(";\n");
}
}
if (key.alphaTestEnabled()) {
sb.append("uniform float u_AlphaRef;\n");
}
// Per-unit texEnvColor uniforms
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (key.unitEnabled(i) && key.unitNeedsEnvColor(i)) {
sb.append("uniform vec4 u_TexEnvColor").append(i).append(";\n");
}
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("uniform vec4 u_FogParams;\n");
sb.append("uniform vec4 u_FogColor;\n");
}
sb.append('\n');
}
private static void emitTextureSampling(StringBuilder sb, FragmentKey key) {
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (!key.unitEnabled(i)) continue;
// Unit 1 is the lightmap — uses v_TexCoord1.
// Units 2-3 intentionally share unit 0's texture coordinates since
// the vertex shader only provides 2 tex coord varyings (unit 0 and unit 1/lightmap).
final String texCoord = (i == 1) ? "v_TexCoord1.st" : "v_TexCoord0.st";
sb.append(" vec4 tex").append(i).append("Color = texture(u_Sampler").append(i).append(", ").append(texCoord).append(");\n");
}
}
private static void emitTexEnvChain(StringBuilder sb, FragmentKey key) {
if (!key.textureEnabled()) {
sb.append(" // No texture — vertex color only\n");
sb.append(" vec4 color = v_Color;\n");
return;
}
// Process units in order; "previous" starts as vertex color
boolean firstUnit = true;
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (!key.unitEnabled(i)) continue;
final String texVar = "tex" + i + "Color";
final String envColorVar = "u_TexEnvColor" + i;
final String prevVar = firstUnit ? "v_Color" : "color";
final String assign = firstUnit ? " vec4 color = " : " color = ";
if (key.unitMode(i) == FragmentKey.TEX_ENV_COMBINE) {
emitCombineUnit(sb, key, i, texVar, envColorVar, prevVar, assign);
} else {
emitSimpleUnit(sb, key, i, texVar, envColorVar, prevVar, assign);
}
firstUnit = false;
}
}
private static void emitSimpleUnit(StringBuilder sb, FragmentKey key, int unit, String texVar, String envColorVar, String prevVar, String assign) {
switch (key.unitMode(unit)) {
case FragmentKey.TEX_ENV_REPLACE -> {
sb.append(" // Unit ").append(unit).append(" (REPLACE)\n");
sb.append(assign).append(texVar).append(";\n");
}
case FragmentKey.TEX_ENV_ADD -> {
sb.append(" // Unit ").append(unit).append(" (ADD)\n");
sb.append(assign).append("vec4(clamp(").append(prevVar).append(".rgb + ").append(texVar).append(".rgb, 0.0, 1.0), ")
.append(prevVar).append(".a * ").append(texVar).append(".a);\n");
}
case FragmentKey.TEX_ENV_DECAL -> {
sb.append(" // Unit ").append(unit).append(" (DECAL)\n");
sb.append(assign).append("vec4(mix(").append(prevVar).append(".rgb, ").append(texVar).append(".rgb, ").append(texVar).append(".a), ")
.append(prevVar).append(".a);\n");
}
case FragmentKey.TEX_ENV_BLEND -> {
sb.append(" // Unit ").append(unit).append(" (BLEND)\n");
sb.append(assign).append("vec4(").append(prevVar).append(".rgb * (vec3(1.0) - ").append(texVar).append(".rgb) + ")
.append(envColorVar).append(".rgb * ").append(texVar).append(".rgb, ")
.append(prevVar).append(".a * ").append(texVar).append(".a);\n");
}
default -> { // MODULATE
sb.append(" // Unit ").append(unit).append(" (MODULATE)\n");
sb.append(assign).append(prevVar).append(" * ").append(texVar).append(";\n");
}
}
}
/** Emit a GL_COMBINE texenv unit */
private static void emitCombineUnit(StringBuilder sb, FragmentKey key, int unit, String texVar, String envColorVar, String prevVar, String assign) {
sb.append(" // Unit ").append(unit).append(" (COMBINE)\n");
final String rgbResult = "combRgb" + unit;
final String alphaResult = "combAlpha" + unit;
if (key.unitCombineRgb(unit) == FragmentKey.COMBINE_DOT3_RGBA) {
sb.append(" vec3 ").append(rgbResult).append(";\n");
sb.append(" {\n");
emitCombineChannel(sb, key, unit, texVar, envColorVar, prevVar, true);
sb.append(" }\n");
sb.append(assign).append("vec4(").append(rgbResult).append(", ").append(rgbResult).append(".x);\n");
} else {
sb.append(" vec3 ").append(rgbResult).append(";\n");
sb.append(" float ").append(alphaResult).append(";\n");
sb.append(" {\n");
emitCombineChannel(sb, key, unit, texVar, envColorVar, prevVar, true);
emitCombineChannel(sb, key, unit, texVar, envColorVar, prevVar, false);
sb.append(" }\n");
sb.append(assign).append("vec4(").append(rgbResult).append(", ").append(alphaResult).append(");\n");
}
}
/**
* Emit one channel (RGB or Alpha) of a GL_COMBINE unit.
*/
private static void emitCombineChannel(StringBuilder sb, FragmentKey key, int unit, String texVar, String envColorVar, String prevVar, boolean isRgb) {
final String suffix = isRgb ? "Rgb" : "Alpha";
final String type = isRgb ? "vec3" : "float";
final int combineFunc = isRgb ? key.unitCombineRgb(unit) : key.unitCombineAlpha(unit);
final int scaleShift = isRgb ? key.unitScaleShiftRgb(unit) : key.unitScaleShiftAlpha(unit);
// Guard: DOT3 is invalid for alpha channel — fallback to REPLACE
final int effectiveFunc;
if (!isRgb && (combineFunc == FragmentKey.COMBINE_DOT3_RGB || combineFunc == FragmentKey.COMBINE_DOT3_RGBA)) {
effectiveFunc = FragmentKey.COMBINE_REPLACE;
} else {
effectiveFunc = combineFunc;
}
// Determine how many args we need
final int numArgs = combineNumArgs(effectiveFunc);
// Emit argument resolution
for (int a = 0; a < numArgs; a++) {
final int source = isRgb ? key.unitSourceRgb(unit, a) : key.unitSourceAlpha(unit, a);
final int operand = isRgb ? key.unitOperandRgb(unit, a) : key.unitOperandAlpha(unit, a);
final String argName = "arg" + suffix + a + "_" + unit;
final String sourceExpr = resolveSource(source, texVar, envColorVar, prevVar);
final String operandExpr = applyOperand(sourceExpr, operand, isRgb);
sb.append(" ").append(type).append(" ").append(argName).append(" = ").append(operandExpr).append(";\n");
}
// Emit combine operation
final String resultName = "comb" + suffix + unit;
final String a0 = "arg" + suffix + "0_" + unit;
final String a1 = "arg" + suffix + "1_" + unit;
final String a2 = "arg" + suffix + "2_" + unit;
sb.append(" ").append(resultName).append(" = ");
switch (effectiveFunc) {
case FragmentKey.COMBINE_REPLACE -> sb.append(a0);
case FragmentKey.COMBINE_MODULATE -> sb.append(a0).append(" * ").append(a1);
case FragmentKey.COMBINE_ADD -> sb.append(a0).append(" + ").append(a1);
case FragmentKey.COMBINE_ADD_SIGNED ->
sb.append(a0).append(" + ").append(a1).append(" - ").append(isRgb ? "vec3(0.5)" : "0.5");
case FragmentKey.COMBINE_SUBTRACT -> sb.append(a0).append(" - ").append(a1);
case FragmentKey.COMBINE_INTERPOLATE ->
sb.append("mix(").append(a1).append(", ").append(a0).append(", ").append(a2).append(")");
case FragmentKey.COMBINE_DOT3_RGB, FragmentKey.COMBINE_DOT3_RGBA ->
sb.append("vec3(4.0 * dot(").append(a0).append(" - vec3(0.5), ").append(a1).append(" - vec3(0.5)))");
default -> sb.append(a0); // fallback to REPLACE
}
sb.append(";\n");
// Apply scale
if (scaleShift > 0) {
final String scaleFactor = scaleShift == 1 ? "2.0" : "4.0";
sb.append(" ").append(resultName).append(" *= ").append(scaleFactor).append(";\n");
}
// Clamp
sb.append(" ").append(resultName).append(" = clamp(").append(resultName).append(", ")
.append(isRgb ? "vec3(0.0), vec3(1.0)" : "0.0, 1.0").append(");\n");
}
private static String resolveSource(int source, String texVar, String envColorVar, String prevVar) {
return switch (source) {
case FragmentKey.SRC_TEXTURE -> texVar;
case FragmentKey.SRC_CONSTANT -> envColorVar;
case FragmentKey.SRC_PRIMARY_COLOR -> "v_Color";
case FragmentKey.SRC_PREVIOUS -> prevVar;
default -> prevVar;
};
}
private static String applyOperand(String sourceExpr, int operand, boolean isRgb) {
if (isRgb) {
return switch (operand) {
case FragmentKey.OP_SRC_COLOR -> sourceExpr + ".rgb";
case FragmentKey.OP_ONE_MINUS_SRC_COLOR -> "(vec3(1.0) - " + sourceExpr + ".rgb)";
case FragmentKey.OP_SRC_ALPHA -> "vec3(" + sourceExpr + ".a)";
case FragmentKey.OP_ONE_MINUS_SRC_ALPHA -> "vec3(1.0 - " + sourceExpr + ".a)";
default -> sourceExpr + ".rgb";
};
} else {
return switch (operand) {
case FragmentKey.OP_SRC_ALPHA -> sourceExpr + ".a";
case FragmentKey.OP_ONE_MINUS_SRC_ALPHA -> "(1.0 - " + sourceExpr + ".a)";
// Per GL spec, alpha channel operands are SRC_ALPHA or ONE_MINUS_SRC_ALPHA only, but handle color operands gracefully
case FragmentKey.OP_SRC_COLOR -> sourceExpr + ".a";
case FragmentKey.OP_ONE_MINUS_SRC_COLOR -> "(1.0 - " + sourceExpr + ".a)";
default -> sourceExpr + ".a";
};
}
}
private static int combineNumArgs(int func) {
return FragmentKey.combineNumArgs(func);
}
private static void emitSpecularAdd(StringBuilder sb, FragmentKey key) {
if (key.separateSpecular()) {
sb.append(" // Add separate specular\n");
sb.append(" color.rgb += v_SpecularColor;\n");
}
}
private static void emitAlphaTest(StringBuilder sb, FragmentKey key) {
if (!key.alphaTestEnabled()) return;
sb.append(" // Alpha test\n");
final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc());
switch (func) {
case GL11.GL_NEVER ->
sb.append(" discard;\n");
case GL11.GL_LESS ->
sb.append(" if (color.a >= u_AlphaRef) discard;\n");
case GL11.GL_EQUAL ->
sb.append(" if (color.a != u_AlphaRef) discard;\n");
case GL11.GL_LEQUAL ->
sb.append(" if (color.a > u_AlphaRef) discard;\n");
case GL11.GL_GREATER ->
sb.append(" if (color.a <= u_AlphaRef) discard;\n");
case GL11.GL_NOTEQUAL ->
sb.append(" if (color.a == u_AlphaRef) discard;\n");
case GL11.GL_GEQUAL ->
sb.append(" if (color.a < u_AlphaRef) discard;\n");
case GL11.GL_ALWAYS -> {}
}
}
private static void emitFog(StringBuilder sb, FragmentKey key) {
final int fogMode = key.fogMode();
if (fogMode == FragmentKey.FOG_NONE) return;
sb.append(" // Fog\n");
sb.append(" float f;\n");
switch (fogMode) {
case FragmentKey.FOG_LINEAR ->
sb.append(" f = v_FogCoord * u_FogParams.x + u_FogParams.y;\n");
case FragmentKey.FOG_EXP ->
sb.append(" f = exp2(-(v_FogCoord * u_FogParams.z));\n");
case FragmentKey.FOG_EXP2 -> {
sb.append(" float fogTmp = v_FogCoord * u_FogParams.w;\n");
sb.append(" f = exp2(-(fogTmp * fogTmp));\n");
}
}
sb.append(" f = clamp(f, 0.0, 1.0);\n");
sb.append(" color.rgb = mix(u_FogColor.rgb, color.rgb, f);\n");
}
}
@@ -0,0 +1,243 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import lombok.Getter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.KHRDebug;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A compiled and linked FFP emulation program (vertex + fragment shader pair). Owns the GL program handle and caches uniform locations.
*/
public class Program {
private static final Logger LOGGER = LogManager.getLogger("FFPProgram");
private static final AtomicInteger PROGRAM_COUNTER = new AtomicInteger();
@Getter private final int programId;
@Getter private final VertexKey vertexKey;
@Getter private final FragmentKey fragmentKey;
// Uniform locations (-1 = not present in this variant)
// Matrices
public int locModelViewMatrix = -1;
public int locProjectionMatrix = -1;
public int locMVPMatrix = -1;
public int locNormalMatrix = -1;
public int locTextureMatrix0 = -1;
// Current state defaults
public int locCurrentNormal = -1;
public int locCurrentColor = -1;
public int locCurrentTexCoord = -1;
public int locCurrentLightmapCoord = -1;
public int locLightmapTextureMatrix = -1;
// Lighting - color material path (raw values)
public int locLightModelAmbient = -1;
public int locMaterialEmission = -1;
public int locMaterialAmbient = -1;
public int locMaterialDiffuse = -1;
public int locMaterialSpecular = -1;
public int locMaterialShininess = -1;
public int locLight0Ambient = -1;
public int locLight0Diffuse = -1;
public int locLight0Specular = -1;
public int locLight1Ambient = -1;
public int locLight1Diffuse = -1;
public int locLight1Specular = -1;
// Lighting - pre-computed path
public int locSceneColor = -1;
public int locLightProd0Ambient = -1;
public int locLightProd0Diffuse = -1;
public int locLightProd0Specular = -1;
public int locLightProd1Ambient = -1;
public int locLightProd1Diffuse = -1;
public int locLightProd1Specular = -1;
// Light positions
public int locLight0Position = -1;
public int locLight1Position = -1;
// Normal scale (for rescale normals)
public int locNormalScale = -1;
// TexGen plane uniforms
public int locTexGenObjPlaneS = -1;
public int locTexGenObjPlaneT = -1;
public int locTexGenObjPlaneR = -1;
public int locTexGenObjPlaneQ = -1;
public int locTexGenEyePlaneS = -1;
public int locTexGenEyePlaneT = -1;
public int locTexGenEyePlaneR = -1;
public int locTexGenEyePlaneQ = -1;
// Clip planes
public int locClipPlanes = -1;
// Fragment uniforms
public final int[] locSampler = { -1, -1, -1, -1 };
public int locAlphaRef = -1;
public final int[] locTexEnvColor = { -1, -1, -1, -1 };
public int locFogParams = -1;
public int locFogColor = -1;
Program(int programId, VertexKey vertexKey, FragmentKey fragmentKey) {
this.programId = programId;
this.vertexKey = vertexKey;
this.fragmentKey = fragmentKey;
resolveLocations();
}
private void resolveLocations() {
locModelViewMatrix = loc("u_ModelViewMatrix");
locProjectionMatrix = loc("u_ProjectionMatrix");
locMVPMatrix = loc("u_MVPMatrix");
locNormalMatrix = loc("u_NormalMatrix");
locTextureMatrix0 = loc("u_TextureMatrix0");
locCurrentNormal = loc("u_CurrentNormal");
locCurrentColor = loc("u_CurrentColor");
locCurrentTexCoord = loc("u_CurrentTexCoord");
locCurrentLightmapCoord = loc("u_CurrentLightmapCoord");
locLightmapTextureMatrix = loc("u_LightmapTextureMatrix");
// Color material path
locLightModelAmbient = loc("u_LightModelAmbient");
locMaterialEmission = loc("u_MaterialEmission");
locMaterialAmbient = loc("u_MaterialAmbient");
locMaterialDiffuse = loc("u_MaterialDiffuse");
locMaterialSpecular = loc("u_MaterialSpecular");
locMaterialShininess = loc("u_MaterialShininess");
locLight0Ambient = loc("u_Light0Ambient");
locLight0Diffuse = loc("u_Light0Diffuse");
locLight0Specular = loc("u_Light0Specular");
locLight1Ambient = loc("u_Light1Ambient");
locLight1Diffuse = loc("u_Light1Diffuse");
locLight1Specular = loc("u_Light1Specular");
// Pre-computed path
locSceneColor = loc("u_SceneColor");
locLightProd0Ambient = loc("u_LightProd0Ambient");
locLightProd0Diffuse = loc("u_LightProd0Diffuse");
locLightProd0Specular = loc("u_LightProd0Specular");
locLightProd1Ambient = loc("u_LightProd1Ambient");
locLightProd1Diffuse = loc("u_LightProd1Diffuse");
locLightProd1Specular = loc("u_LightProd1Specular");
// Light positions
locLight0Position = loc("u_Light0Position");
locLight1Position = loc("u_Light1Position");
locNormalScale = loc("u_NormalScale");
// TexGen
locTexGenObjPlaneS = loc("u_TexGenObjPlaneS");
locTexGenObjPlaneT = loc("u_TexGenObjPlaneT");
locTexGenObjPlaneR = loc("u_TexGenObjPlaneR");
locTexGenObjPlaneQ = loc("u_TexGenObjPlaneQ");
locTexGenEyePlaneS = loc("u_TexGenEyePlaneS");
locTexGenEyePlaneT = loc("u_TexGenEyePlaneT");
locTexGenEyePlaneR = loc("u_TexGenEyePlaneR");
locTexGenEyePlaneQ = loc("u_TexGenEyePlaneQ");
// Clip planes
locClipPlanes = loc("u_ClipPlane[0]");
// Fragment
for (int i = 0; i < 4; i++) {
locSampler[i] = loc("u_Sampler" + i);
}
locAlphaRef = loc("u_AlphaRef");
for (int i = 0; i < 4; i++) {
locTexEnvColor[i] = loc("u_TexEnvColor" + i);
}
locFogParams = loc("u_FogParams");
locFogColor = loc("u_FogColor");
}
private int loc(String name) {
return GL20.glGetUniformLocation(programId, name);
}
public void destroy() {
GL20.glDeleteProgram(programId);
}
/**
* Compile a shader, link it into a program, and return the FFPProgram.
*/
static Program create(VertexKey vk, FragmentKey fk, String vertSrc, String fragSrc) {
final int id = PROGRAM_COUNTER.getAndIncrement();
final int vs = compileShader(GL20.GL_VERTEX_SHADER, vertSrc, "ffp_v_" + Long.toHexString(vk.pack()));
final int fs = compileShader(GL20.GL_FRAGMENT_SHADER, fragSrc, "ffp_f_" + id);
final int program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
GL20.glAttachShader(program, fs);
GL20.glLinkProgram(program);
final String log = RenderSystem.getProgramInfoLog(program);
if (!log.isEmpty()) {
LOGGER.warn("FFP program link log (vk=0x{}, fk={}): {}", Long.toHexString(vk.pack()), fk, log);
}
final int linkStatus = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
if (linkStatus != GL11.GL_TRUE) {
GL20.glDeleteProgram(program);
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
throw new RuntimeException("FFP shader link failed (vk=0x" + Long.toHexString(vk.pack()) + ", fk=" + fk + "): " + log);
}
final String debugName = "FFP(v=0x" + Long.toHexString(vk.pack()) + ",f=" + id + ")";
GLDebug.nameObject(KHRDebug.GL_PROGRAM, program, debugName);
// Detach and delete individual shaders — they're linked into the program
GL20.glDetachShader(program, vs);
GL20.glDetachShader(program, fs);
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
final Program ffpProgram = new Program(program, vk, fk);
final int previousProgram = GLStateManager.getActiveProgram();
GL20.glUseProgram(program);
for (int i = 0; i < 4; i++) {
if (ffpProgram.locSampler[i] != -1) GL20.glUniform1i(ffpProgram.locSampler[i], i);
}
GL20.glUseProgram(previousProgram);
return ffpProgram;
}
private static int compileShader(int type, String src, String name) {
final int shader = GL20.glCreateShader(type);
GL20.glShaderSource(shader, src);
GL20.glCompileShader(shader);
final String typeName = (type == GL20.GL_VERTEX_SHADER) ? "vertex" : "fragment";
GLDebug.nameObject(KHRDebug.GL_SHADER, shader, name + "(" + typeName + ")");
final String log = RenderSystem.getShaderInfoLog(shader);
if (!log.isEmpty()) {
LOGGER.warn("FFP {} shader compilation log for {}: {}", typeName, name, log);
}
final int result = GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS);
if (result != GL11.GL_TRUE) {
LOGGER.error("FFP {} shader source:\n{}", typeName, src);
GL20.glDeleteShader(shader);
throw new RuntimeException("FFP " + typeName + " shader compilation failed for " + name + ": " + log);
}
return shader;
}
}
@@ -0,0 +1,283 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import lombok.Setter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.IdentityHashMap;
/**
* Open-addressed cache for FFP shader program variants.
* Keyed on (vertexKey, fragmentKey) pairs stored inline as raw longs — zero allocation on cache hits.
* Converges on Mesa's prog_cache.c: hash+memcmp over packed bytes, open addressing.
*
* <p>Layout per slot in {@code keys[]}: {@link #SLOT_STRIDE}=6 longs
* <pre>
* [0]: vertexKeyPacked
* [1]: (fkLen &lt;&lt; 48) | (hash &amp; 0x0000_FFFF_FFFF_FFFF) — never 0 for valid entries (fkLen &gt;= 1)
* [2..5]: fk0..fk3
* </pre>
*/
public class ShaderCache {
private static final Logger LOGGER = LogManager.getLogger("FFPShaderCache");
private static final int SLOT_STRIDE = 6;
private static final int INITIAL_CAPACITY = 64; // power of 2
private static final long EMPTY_MARKER = 0L; // slot [1] == 0 means empty (fkLen >= 1 guarantees nonzero)
private final Long2ObjectOpenHashMap<String> vertexSourceCache = new Long2ObjectOpenHashMap<>();
// Open-addressed table — parallel arrays
private long[] keys;
private Program[] programs;
private String[] fragSources; // fragment source per slot; reused across VK variants via findFragSource
private int capacity;
private int count;
private int uniqueFragCount;
// Fast-path: last-used key (raw longs, no objects)
private long lastVK = Long.MIN_VALUE;
private int lastFKLen;
private long lastFK0, lastFK1, lastFK2, lastFK3;
private Program lastProgram;
private int fragmentDumpCounter;
@Setter private Path dumpDir = null;
public ShaderCache() {
capacity = INITIAL_CAPACITY;
keys = new long[capacity * SLOT_STRIDE];
programs = new Program[capacity];
fragSources = new String[capacity];
}
/**
* Get or create an FFP program for the given state keys.
*
* @param vkPacked packed vertex key
* @param fkPacked pre-allocated scratch array with packed fragment key longs
* @param fkLen number of significant longs in fkPacked (1..4)
*/
public Program getOrCreate(long vkPacked, long[] fkPacked, int fkLen) {
// Fast-path: compare raw longs inline
if (vkPacked == lastVK && fkLen == lastFKLen && lastProgram != null
&& fkPacked[0] == lastFK0
&& (fkLen < 2 || fkPacked[1] == lastFK1)
&& (fkLen < 3 || fkPacked[2] == lastFK2)
&& (fkLen < 4 || fkPacked[3] == lastFK3)) {
return lastProgram;
}
final long hash = hashKey(vkPacked, fkPacked, fkLen);
final long storedWord = ((long) fkLen << 48) | (hash & 0x0000_FFFF_FFFF_FFFFL);
final int mask = capacity - 1;
int idx = (int) (hash & mask);
while (true) {
final int base = idx * SLOT_STRIDE;
final long slot1 = keys[base + 1];
if (slot1 == EMPTY_MARKER) {
break;
}
if (slot1 == storedWord && keys[base] == vkPacked && keysMatch(base + 2, fkPacked, fkLen)) {
final Program p = programs[idx];
setLastUsed(vkPacked, fkPacked, fkLen, p);
return p;
}
idx = (idx + 1) & mask;
}
if (count >= (capacity * 3) / 4) {
grow();
return getOrCreate(vkPacked, fkPacked, fkLen);
}
final VertexKey vk = VertexKey.fromPacked(vkPacked);
final String vertSrc = getOrGenerateVertex(vk);
final FragmentKey fk = FragmentKey.fromPacked(fkPacked, fkLen);
String fragSrc = findFragSource(fkPacked, fkLen);
if (fragSrc == null) {
fragSrc = FragmentShaderGenerator.generate(fk);
uniqueFragCount++;
}
final Program program = Program.create(vk, fk, vertSrc, fragSrc);
final int insertIdx = findEmptySlot(hash);
final int base = insertIdx * SLOT_STRIDE;
keys[base] = vkPacked;
keys[base + 1] = storedWord;
for (int i = 0; i < fkLen; i++) keys[base + 2 + i] = fkPacked[i];
programs[insertIdx] = program;
fragSources[insertIdx] = fragSrc;
count++;
setLastUsed(vkPacked, fkPacked, fkLen, program);
LOGGER.debug("FFP variant compiled: {} + {} (total: {} programs)", vk, fk, count);
if (dumpDir != null) {
dumpShader(Long.toHexString(vkPacked), ".vert.glsl", vertSrc);
dumpShader("frag_" + fragmentDumpCounter++, ".frag.glsl", fragSrc);
}
return program;
}
private String findFragSource(long[] fkPacked, int fkLen) {
for (int i = 0; i < capacity; i++) {
final int base = i * SLOT_STRIDE;
if (keys[base + 1] == EMPTY_MARKER) continue;
final int storedLen = (int) (keys[base + 1] >>> 48);
if (storedLen != fkLen) continue;
if (keysMatch(base + 2, fkPacked, fkLen)) {
return fragSources[i];
}
}
return null;
}
private boolean keysMatch(int keysOffset, long[] fkPacked, int fkLen) {
for (int i = 0; i < fkLen; i++) {
if (keys[keysOffset + i] != fkPacked[i]) return false;
}
return true;
}
private void setLastUsed(long vkPacked, long[] fkPacked, int fkLen, Program program) {
lastVK = vkPacked;
lastFKLen = fkLen;
lastFK0 = fkPacked[0];
lastFK1 = fkLen >= 2 ? fkPacked[1] : 0;
lastFK2 = fkLen >= 3 ? fkPacked[2] : 0;
lastFK3 = fkLen >= 4 ? fkPacked[3] : 0;
lastProgram = program;
}
private int findEmptySlot(long hash) {
final int mask = capacity - 1;
int idx = (int) (hash & mask);
while (keys[idx * SLOT_STRIDE + 1] != EMPTY_MARKER) {
idx = (idx + 1) & mask;
}
return idx;
}
/**
* Hash over (vk + fk longs). Adapted from Mesa's hash_key — mix-rotate-multiply.
*/
private static long hashKey(long vkPacked, long[] fkPacked, int fkLen) {
long h = vkPacked * 0x9E3779B97F4A7C15L;
for (int i = 0; i < fkLen; i++) {
h ^= fkPacked[i] * 0x517CC1B727220A95L;
h = Long.rotateLeft(h, 31) * 0x9E3779B97F4A7C15L;
}
return h | 1L; // ensure nonzero lower bits
}
private void grow() {
final int oldCap = capacity;
final long[] oldKeys = keys;
final Program[] oldPrograms = programs;
final String[] oldFragSources = fragSources;
capacity = oldCap * 2;
keys = new long[capacity * SLOT_STRIDE];
programs = new Program[capacity];
fragSources = new String[capacity];
count = 0;
final int mask = capacity - 1;
for (int i = 0; i < oldCap; i++) {
final int oldBase = i * SLOT_STRIDE;
if (oldKeys[oldBase + 1] == EMPTY_MARKER) continue;
final long storedHash = oldKeys[oldBase + 1] & 0x0000_FFFF_FFFF_FFFFL;
int idx = (int) (storedHash & mask);
while (keys[idx * SLOT_STRIDE + 1] != EMPTY_MARKER) {
idx = (idx + 1) & mask;
}
final int newBase = idx * SLOT_STRIDE;
System.arraycopy(oldKeys, oldBase, keys, newBase, SLOT_STRIDE);
programs[idx] = oldPrograms[i];
fragSources[idx] = oldFragSources[i];
count++;
}
}
private String getOrGenerateVertex(VertexKey vk) {
return vertexSourceCache.computeIfAbsent(vk.pack(), k -> VertexShaderGenerator.generate(vk));
}
private void dumpShader(String name, String suffix, String source) {
if (dumpDir == null) return;
try {
Files.createDirectories(dumpDir);
final Path file = dumpDir.resolve("ffp_" + name + suffix);
if (!Files.exists(file)) {
Files.writeString(file, source, StandardCharsets.UTF_8);
}
} catch (IOException e) {
LOGGER.warn("Failed to dump FFP shader to {}: {}", dumpDir, e.getMessage());
}
}
/** Dump all cached shaders to disk. */
public void dumpAll(Path outputDir) {
try {
Files.createDirectories(outputDir);
for (var entry : vertexSourceCache.entrySet()) {
dumpShader(Long.toHexString(entry.getKey()), ".vert.glsl", entry.getValue());
}
final IdentityHashMap<String, Integer> seen = new IdentityHashMap<>();
for (int i = 0; i < capacity; i++) {
if (fragSources[i] != null && !seen.containsKey(fragSources[i])) {
final int idx = seen.size();
seen.put(fragSources[i], idx);
dumpShader("frag_" + idx, ".frag.glsl", fragSources[i]);
}
}
LOGGER.info("Dumped {} vertex and {} fragment FFP shader sources to {}",
vertexSourceCache.size(), seen.size(), outputDir);
} catch (IOException e) {
LOGGER.warn("Failed to dump FFP shaders: {}", e.getMessage());
}
}
/** Destroy all cached programs and clear caches. */
public void destroy() {
for (int i = 0; i < capacity; i++) {
if (programs[i] != null) {
programs[i].destroy();
}
}
Arrays.fill(keys, 0);
Arrays.fill(programs, null);
Arrays.fill(fragSources, null);
count = 0;
uniqueFragCount = 0;
vertexSourceCache.clear();
lastProgram = null;
lastVK = Long.MIN_VALUE;
LOGGER.debug("FFP shader cache cleared");
}
public int getProgramCount() {
return count;
}
public int getVertexVariantCount() {
return vertexSourceCache.size();
}
public int getFragmentVariantCount() {
return uniqueFragCount;
}
}
@@ -0,0 +1,203 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import java.util.Arrays;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.CompatUniformManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.stacks.Vec3fStack;
import com.gtnewhorizons.angelica.glsm.stacks.Vec4fStack;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
import net.coderbot.iris.gl.blending.BlendModeStorage;
import net.minecraft.launchwrapper.Launch;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL20;
import java.nio.file.Paths;
/**
* FFP shader manager
*/
public class ShaderManager {
private static final Logger LOGGER = LogManager.getLogger("FFPShaderManager");
private static final class Holder { static final ShaderManager INSTANCE = new ShaderManager(); }
private final ShaderCache cache = new ShaderCache();
private final Uniforms uniforms = new Uniforms();
private final Int2IntOpenHashMap vaoVertexFlags = new Int2IntOpenHashMap();
@Getter
private boolean active = false;
private Program currentProgram = null;
private long currentVertexKeyPacked = Long.MIN_VALUE;
private final long[] currentFKScratch = new long[FragmentKey.MAX_UNITS];
private final long[] currentFKPacked = new long[FragmentKey.MAX_UNITS];
private int currentFKLen = 0;
@Getter private static final Vector3f currentNormal = new Vector3f(0.0f, 0.0f, 1.0f);
@Getter private static final Vector4f currentTexCoord = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
@Getter private static final Vec3fStack normalStack = new Vec3fStack(currentNormal);
@Getter private static final Vec4fStack texCoordStack = new Vec4fStack(currentTexCoord);
@Getter private static int normalGeneration;
@Getter private static int texCoordGeneration;
@Getter private boolean enabled = false;
private int currentVertexFlags = VertexFlags.TEXTURE_BIT | VertexFlags.COLOR_BIT | VertexFlags.NORMAL_BIT;
private ShaderManager() {
vaoVertexFlags.defaultReturnValue(-1);
boolean isDev = false;
try {
final Object deobfEnv = Launch.blackboard != null ? Launch.blackboard.get("fml.deobfuscatedEnvironment") : null;
isDev = Boolean.TRUE.equals(deobfEnv);
} catch (Exception ignored) {}
if (isDev || Boolean.parseBoolean(System.getProperty("angelica.ffp.dumpShaders", "false"))) {
cache.setDumpDir(Paths.get("ffp_shaders"));
}
}
public static ShaderManager getInstance() {
return Holder.INSTANCE;
}
public void enable() {
enabled = true;
VertexFormat.registerSetupBufferStateOverride((format, offset) -> {
currentVertexFlags = format.getVertexFlags();
vaoVertexFlags.put(GLStateManager.getBoundVAO(), currentVertexFlags);
return false;
});
LOGGER.info("FFP shader emulation enabled");
}
public void disable() {
enabled = false;
}
public void activate() {
active = true;
updateVariant(true, true, true, true);
uploadUniforms();
}
public void deactivate() {
active = false;
currentProgram = null;
currentVertexKeyPacked = Long.MIN_VALUE;
currentFKLen = 0;
}
public void preDraw(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
GLStateManager.flushDeferredVertexAttribs();
BlendModeStorage.flushDeferredBlend();
if (!active) {
if (enabled) {
final int currentProgramId = GLStateManager.getActiveProgram();
if (currentProgramId != 0) {
CompatUniformManager.onUseProgram(currentProgramId);
return;
}
active = true;
} else {
return;
}
}
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
final int fkLen = FragmentKey.packFromState(currentFKScratch);
if (vkPacked != currentVertexKeyPacked || !Arrays.equals(currentFKScratch, 0, fkLen, currentFKPacked, 0, currentFKLen)) {
commitVariant(vkPacked, fkLen);
}
uploadUniforms();
}
public void preDraw(int vertexFlags) {
currentVertexFlags = vertexFlags;
preDraw(
(vertexFlags & VertexFlags.COLOR_BIT) != 0,
(vertexFlags & VertexFlags.NORMAL_BIT) != 0,
(vertexFlags & VertexFlags.TEXTURE_BIT) != 0,
(vertexFlags & VertexFlags.BRIGHTNESS_BIT) != 0
);
}
public void preDraw() {
preDraw(currentVertexFlags);
}
private void updateVariant(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
final int fkLen = FragmentKey.packFromState(currentFKScratch);
commitVariant(vkPacked, fkLen);
}
private void commitVariant(long vkPacked, int fkLen) {
currentVertexKeyPacked = vkPacked;
System.arraycopy(currentFKScratch, 0, currentFKPacked, 0, fkLen);
currentFKLen = fkLen;
currentProgram = cache.getOrCreate(vkPacked, currentFKPacked, currentFKLen);
GL20.glUseProgram(currentProgram.getProgramId());
}
private void uploadUniforms() {
if (currentProgram != null) {
uniforms.upload(currentProgram);
}
}
public static void setCurrentNormal(float x, float y, float z) {
currentNormal.set(x, y, z);
normalGeneration++;
}
public static void setCurrentTexCoord(float s, float t, float r, float q) {
currentTexCoord.set(s, t, r, q);
texCoordGeneration++;
}
public static void bumpNormalGeneration() { normalGeneration++; }
public static void bumpTexCoordGeneration() { texCoordGeneration++; }
public void enableClientVertexFlag(int flag) { currentVertexFlags |= flag; }
public void disableClientVertexFlag(int flag) { currentVertexFlags &= ~flag; }
public int getCurrentVertexFlags() { return currentVertexFlags; }
public void setCurrentVertexFlags(int flags) { currentVertexFlags = flags; }
public void onBindVertexArray(int vaoId) {
final int flags = vaoVertexFlags.get(vaoId);
if (flags != -1) {
currentVertexFlags = flags;
}
}
public void onDeleteVertexArray(int vaoId) {
vaoVertexFlags.remove(vaoId);
}
public void destroy() {
cache.destroy();
uniforms.destroy();
TessellatorStreamingDrawer.destroy();
com.gtnewhorizons.angelica.glsm.QuadConverter.destroy();
active = false;
currentProgram = null;
}
public String getDebugInfo() {
return String.format("FFP: %d programs (%d vert, %d frag variants)",
cache.getProgramCount(), cache.getVertexVariantCount(), cache.getFragmentVariantCount());
}
}
@@ -0,0 +1,309 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.streaming.OrphanStreamingBuffer;
import com.gtnewhorizons.angelica.glsm.streaming.OrphanStreamingBuffer.UploadStrategy;
import com.gtnewhorizons.angelica.glsm.streaming.PersistentStreamingBuffer;
import net.minecraft.client.renderer.Tessellator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Replaces the vanilla Tessellator's FFP client-array draw path with a streaming VBO+VAO approach for GL 3.3 core profile compatibility.
* <p>
* Uses a persistent-mapped ring buffer on GL4.4+ hardware (zero-copy uploads with fence sync),
* falling back to the classic orphan pattern on older hardware or on overflow.
* Maintains two VAO sets (persistent + orphan) per vertex format (up to 16 combinations).
*/
public class TessellatorStreamingDrawer {
private static final Logger LOGGER = LogManager.getLogger("TessellatorStreamingDrawer");
private static final int FORMAT_COUNT = VertexFlags.BITSET_SIZE; // 16
private static PersistentStreamingBuffer persistentBuffer;
private static final OrphanStreamingBuffer[] orphanBuffers = new OrphanStreamingBuffer[FORMAT_COUNT];
private static final int[] persistentVAOs = new int[FORMAT_COUNT];
private static final int[] orphanVAOs = new int[FORMAT_COUNT];
private static ByteBuffer repackBuffer;
private static long repackAddress;
private static int repackCapacity;
private static boolean initialized = false;
private static UploadStrategy orphanStrategy = UploadStrategy.BUFFER_SUB_DATA;
static {
// Initial repack buffer: 64KB
repackCapacity = 0x10000;
repackBuffer = memAlloc(repackCapacity);
repackAddress = memAddress0(repackBuffer);
}
private static void init() {
if (initialized) return;
initialized = true;
// Select orphan upload strategy from config
if (AngelicaMod.options() != null && AngelicaMod.options().advanced.useMapBufferRange) {
orphanStrategy = UploadStrategy.MAP_BUFFER_RANGE;
}
LOGGER.info("Orphan upload strategy: {}", orphanStrategy);
if (RenderSystem.supportsBufferStorage() && !Boolean.getBoolean("angelica.forceOrphanStreaming")) {
try {
persistentBuffer = new PersistentStreamingBuffer();
LOGGER.info("Persistent streaming buffer created ({}MB)", PersistentStreamingBuffer.DEFAULT_CAPACITY / (1024 * 1024));
} catch (Exception e) {
LOGGER.warn("Failed to create persistent streaming buffer, using orphan fallback", e);
persistentBuffer = null;
}
}
}
/**
* Draw the vanilla Tessellator's data using streaming VBO+VAO instead of FFP client arrays.
*/
public static int draw(Tessellator tess) {
if (!tess.isDrawing) {
throw new IllegalStateException("Not tesselating!");
}
tess.isDrawing = false;
final int vertexCount = tess.vertexCount;
if (vertexCount == 0) {
final int result = tess.rawBufferIndex * 4;
tess.reset();
return result;
}
// Determine the optimal vertex format from the tessellator's flags
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
final int vertexSize = format.getVertexSize();
final int requiredBytes = vertexCount * vertexSize;
ensureRepackCapacity(requiredBytes);
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
// Shrink rawBuffer if oversized
if (tess.rawBufferSize > 0x20000 && tess.rawBufferIndex < (tess.rawBufferSize << 3)) {
tess.rawBufferSize = 0x10000;
tess.rawBuffer = new int[tess.rawBufferSize];
}
final int result = tess.rawBufferIndex * 4;
tess.reset();
return result;
}
/**
* Draw DirectTessellator data via streaming VBO+VAO. Used for live immediate mode emulation.
*/
public static void drawDirect(DirectTessellator dt) {
final VertexFormat format = dt.getVertexFormat();
if (format == null) return;
final int vertexCount = dt.getVertexCount();
if (vertexCount == 0) return;
final int drawMode = dt.getDrawMode();
final int flags = format.getVertexFlags();
final ByteBuffer buffer = dt.getWriteBuffer();
final int vertexSize = format.getVertexSize();
uploadAndDraw(buffer, 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
public static String getDebugInfo() {
if (!initialized) return "Stream: not initialized";
final long now = System.nanoTime();
if (now - lastDebugUpdateNanos < DEBUG_UPDATE_INTERVAL_NS) {
return cachedDebugInfo;
}
lastDebugUpdateNanos = now;
int orphanCount = 0;
int orphanBytes = 0;
for (int i = 0; i < FORMAT_COUNT; i++) {
if (orphanBuffers[i] != null) {
orphanCount++;
orphanBytes += orphanBuffers[i].getCapacity();
}
}
if (persistentBuffer != null) {
cachedDebugInfo = String.format("Stream: Persistent %s (%s free) + %d orphan (%s)",
formatBytes(persistentBuffer.getCapacity()), formatBytes(persistentBuffer.getRemaining()),
orphanCount, formatBytes(orphanBytes));
} else {
cachedDebugInfo = String.format("Stream: Orphan (%d bufs, %s)", orphanCount, formatBytes(orphanBytes));
}
return cachedDebugInfo;
}
private static String formatBytes(int bytes) {
if (bytes >= 1024 * 1024) return String.format("%5.1fMB", bytes / (1024.0 * 1024.0));
if (bytes >= 1024) return String.format("%5.1fKB", bytes / 1024.0);
return String.format("%5dB", bytes);
}
public static void endFrame() {
if (persistentBuffer != null) {
persistentBuffer.postDraw();
}
for (int i = 0; i < FORMAT_COUNT; i++) {
if (orphanBuffers[i] != null) {
orphanBuffers[i].postDraw();
}
}
}
/**
* 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.
*/
private static void uploadAndDraw(ByteBuffer packed, int flags, VertexFormat format, int vertexSize, int drawMode, int vertexCount) {
ensureVAO(flags, format);
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(packed, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
firstVertex = orphanBuffers[flags].upload(packed, vertexSize);
}
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
}
private static void drawWithQuadConversion(int drawMode, int firstVertex, int vertexCount) {
if (drawMode == GL11.GL_QUADS) {
QuadConverter.drawQuadsAsTriangles(firstVertex, vertexCount);
} else {
GL11.glDrawArrays(drawMode, firstVertex, vertexCount);
}
}
private static void ensureRepackCapacity(int requiredBytes) {
if (requiredBytes <= repackCapacity) return;
int newCapacity = repackCapacity;
while (newCapacity < requiredBytes) {
newCapacity *= 2;
}
memFree(repackBuffer);
repackBuffer = memAlloc(newCapacity);
repackAddress = memAddress0(repackBuffer);
repackCapacity = newCapacity;
}
private static void ensureVAO(int flags, VertexFormat format) {
init();
if (orphanVAOs[flags] == 0) {
orphanBuffers[flags] = new OrphanStreamingBuffer(orphanStrategy);
orphanVAOs[flags] = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, orphanBuffers[flags].getBufferId());
format.setupBufferState(0L);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
}
if (persistentBuffer != null && persistentVAOs[flags] == 0) {
persistentVAOs[flags] = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, persistentBuffer.getBufferId());
format.setupBufferState(0L);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
}
}
/**
* Clean up all VAOs, streaming buffers, and the repack buffer.
*/
public static void destroy() {
for (int i = 0; i < FORMAT_COUNT; i++) {
if (persistentVAOs[i] != 0) { GLStateManager.glDeleteVertexArrays(persistentVAOs[i]); persistentVAOs[i] = 0; }
if (orphanVAOs[i] != 0) { GLStateManager.glDeleteVertexArrays(orphanVAOs[i]); orphanVAOs[i] = 0; }
if (orphanBuffers[i] != null) { orphanBuffers[i].destroy(); orphanBuffers[i] = null; }
}
if (persistentBuffer != null) {
persistentBuffer.destroy();
persistentBuffer = null;
}
if (repackBuffer != null) {
memFree(repackBuffer);
repackBuffer = null;
repackAddress = 0;
repackCapacity = 0;
}
initialized = false;
}
}
@@ -0,0 +1,471 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import net.minecraft.client.renderer.OpenGlHelper;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Uploads GLSM cached state to the active FFP program's uniforms.
*
* Follows Mesa's prog_statevars.c patterns:
* - Pre-computed derived state (light products, scene color) when material is static
* - Raw light/material values when color material is active
* - Optimized fog params (pre-divided for single MAD in shader)
*/
public class Uniforms {
// Reusable temp buffers for uniform upload
private final FloatBuffer mat4Buf = memAllocFloat(16);
private final FloatBuffer mat3Buf = memAllocFloat(9);
private final FloatBuffer vec4Buf = memAllocFloat(4);
private final FloatBuffer vec3Buf = memAllocFloat(3);
private final FloatBuffer clipPlaneBuf = memAllocFloat(32); // 8 planes * vec4
// Derived matrices (computed on CPU)
private final Matrix4f mvpMatrix = new Matrix4f();
private final Matrix3f normalMatrix = new Matrix3f();
// Temp vectors for light product and normal scale computation
private final Vector4f tempVec4 = new Vector4f();
private final org.joml.Vector3f tempVec3 = new org.joml.Vector3f();
// Pre-computed constants for fog params (Mesa STATE_FOG_PARAMS_OPTIMIZED)
private static final double LN2 = Math.log(2.0);
private static final double SQRT_LN2 = Math.sqrt(LN2);
// Dirty tracking: last-uploaded generation per category + program ID.
// Program change forces full re-upload since uniform locations differ.
private int lastProgramId = -1;
private int lastMvGen = -1;
private int lastProjGen = -1;
private int lastTexMatGen = -1;
private int lastLightingGen = -1;
private int lastFragmentGen = -1;
private int lastColorGen = -1;
private int lastNormalGen = -1;
private int lastTexCoordGen = -1;
private float lastLightmapX = Float.NaN;
private float lastLightmapY = Float.NaN;
private int lastTexGenGen = -1;
private int lastClipPlaneGen = -1;
/**
* Upload all relevant uniforms to the given FFP program based on current GLSM state.
* Uses generation counters to skip categories that haven't changed since last upload.
*/
public void upload(Program program) {
final boolean programChanged = program.getProgramId() != lastProgramId;
lastProgramId = program.getProgramId();
final int mvGen = GLStateManager.mvGeneration;
final int projGen = GLStateManager.projGeneration;
final int texMatGen = GLStateManager.texMatrixGeneration;
final boolean mvChanged = programChanged || mvGen != lastMvGen;
final boolean projChanged = programChanged || projGen != lastProjGen;
final boolean texMatChanged = programChanged || texMatGen != lastTexMatGen;
if (mvChanged || projChanged || texMatChanged) {
uploadMatrices(program, mvChanged, projChanged, texMatChanged);
lastMvGen = mvGen;
lastProjGen = projGen;
lastTexMatGen = texMatGen;
}
if (program.getVertexKey().lightingEnabled()) {
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
uploadLighting(program);
lastLightingGen = litGen;
}
}
// Current color/normal/texcoord — skip if generation unchanged
if (!program.getVertexKey().hasVertexColor()) {
final int colorGen = GLStateManager.colorGeneration;
if (programChanged || colorGen != lastColorGen) {
uploadCurrentColor(program);
lastColorGen = colorGen;
}
}
if (!program.getVertexKey().hasVertexNormal() && program.getVertexKey().lightingEnabled()) {
final int normalGen = ShaderManager.getNormalGeneration();
if (programChanged || normalGen != lastNormalGen) {
uploadCurrentNormal(program);
lastNormalGen = normalGen;
}
}
if (!program.getVertexKey().hasVertexTexCoord() && program.getVertexKey().textureEnabled()) {
final int texGen = ShaderManager.getTexCoordGeneration();
if (programChanged || texGen != lastTexCoordGen) {
uploadCurrentTexCoord(program);
lastTexCoordGen = texGen;
}
}
if (program.getVertexKey().lightmapEnabled() && !program.getVertexKey().hasVertexLightmap()) {
if (programChanged || OpenGlHelper.lastBrightnessX != lastLightmapX || OpenGlHelper.lastBrightnessY != lastLightmapY) {
uploadCurrentLightmapCoord(program);
lastLightmapX = OpenGlHelper.lastBrightnessX;
lastLightmapY = OpenGlHelper.lastBrightnessY;
}
}
if (program.getVertexKey().texGenEnabled()) {
final int tgGen = GLStateManager.texGenGeneration;
if (programChanged || tgGen != lastTexGenGen) {
uploadTexGen(program);
lastTexGenGen = tgGen;
}
}
if (program.getVertexKey().clipPlanesEnabled()) {
final int cpGen = GLStateManager.clipPlaneGeneration;
if (programChanged || cpGen != lastClipPlaneGen) {
uploadClipPlanes(program);
lastClipPlaneGen = cpGen;
}
}
final int fragGen = GLStateManager.fragmentGeneration;
if (programChanged || fragGen != lastFragmentGen) {
uploadFragmentUniforms(program);
lastFragmentGen = fragGen;
}
}
private void uploadMatrices(Program program, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
final Matrix4f mv = GLStateManager.getModelViewMatrix();
final Matrix4f proj = GLStateManager.getProjectionMatrix();
// ModelView + derived (normal matrix, normal scale)
if (mvChanged) {
if (program.locModelViewMatrix != -1) {
mv.get(mat4Buf);
GL20.glUniformMatrix4(program.locModelViewMatrix, false, mat4Buf);
}
// Normal matrix = inverse transpose of upper-left 3x3 of ModelView
if (program.locNormalMatrix != -1 || program.locNormalScale != -1) {
mv.normal(normalMatrix);
}
if (program.locNormalMatrix != -1) {
normalMatrix.get(mat3Buf);
GL20.glUniformMatrix3(program.locNormalMatrix, false, mat3Buf);
}
// Normal scale (for GL_RESCALE_NORMAL without GL_NORMALIZE)
if (program.locNormalScale != -1) {
// Scale factor = 1/length of first column of normal matrix
final float scale = 1.0f / normalMatrix.getColumn(0, tempVec3).length();
GL20.glUniform1f(program.locNormalScale, scale);
}
}
// Projection
if (projChanged) {
if (program.locProjectionMatrix != -1) {
proj.get(mat4Buf);
GL20.glUniformMatrix4(program.locProjectionMatrix, false, mat4Buf);
}
}
// MVP = Projection * ModelView — needs recompute if either input changed
if ((mvChanged || projChanged) && program.locMVPMatrix != -1) {
proj.mul(mv, mvpMatrix);
mvpMatrix.get(mat4Buf);
GL20.glUniformMatrix4(program.locMVPMatrix, false, mat4Buf);
}
// Texture matrices
if (texMatChanged) {
// Texture matrix unit 0
if (program.locTextureMatrix0 != -1) {
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
texMat.get(mat4Buf);
GL20.glUniformMatrix4(program.locTextureMatrix0, false, mat4Buf);
}
// Texture matrix unit 1 (lightmap)
if (program.locLightmapTextureMatrix != -1) {
final Matrix4f lmTexMat = GLStateManager.getTextures().getTextureUnitMatrix(1);
lmTexMat.get(mat4Buf);
GL20.glUniformMatrix4(program.locLightmapTextureMatrix, false, mat4Buf);
}
}
}
private void uploadLighting(Program program) {
final VertexKey vk = program.getVertexKey();
if (vk.colorMaterialEnabled()) {
uploadLightingColorMaterial(program, vk);
} else {
uploadLightingPreComputed(program, vk);
}
}
/**
* Color material active: upload raw light and material values.
* The shader multiplies them with vertex color at runtime.
*/
private void uploadLightingColorMaterial(Program program, VertexKey vk) {
final MaterialState mat = GLStateManager.getFrontMaterial();
uploadVec4(program.locLightModelAmbient, GLStateManager.getLightModel().ambient);
uploadVec4(program.locMaterialEmission, mat.emission);
uploadVec4(program.locMaterialAmbient, mat.ambient);
uploadVec4(program.locMaterialDiffuse, mat.diffuse);
uploadVec4(program.locMaterialSpecular, mat.specular);
if (program.locMaterialShininess != -1) {
GL20.glUniform1f(program.locMaterialShininess, mat.shininess);
}
if (vk.light0Enabled()) {
final LightState light0 = GLStateManager.getLightDataStates()[0];
uploadVec4(program.locLight0Ambient, light0.ambient);
uploadVec4(program.locLight0Diffuse, light0.diffuse);
uploadVec4(program.locLight0Specular, light0.specular);
uploadVec4(program.locLight0Position, light0.position);
}
if (vk.light1Enabled()) {
final LightState light1 = GLStateManager.getLightDataStates()[1];
uploadVec4(program.locLight1Ambient, light1.ambient);
uploadVec4(program.locLight1Diffuse, light1.diffuse);
uploadVec4(program.locLight1Specular, light1.specular);
uploadVec4(program.locLight1Position, light1.position);
}
}
/**
* No color material: upload pre-computed light products and scene color.
* Fewer shader multiplies since material is static.
*/
private void uploadLightingPreComputed(Program program, VertexKey vk) {
final MaterialState mat = GLStateManager.getFrontMaterial();
// Scene color = emission + ambient * lightModel.ambient
// Alpha = material.diffuse.a
if (program.locSceneColor != -1) {
tempVec4.set(
mat.emission.x + mat.ambient.x * GLStateManager.getLightModel().ambient.x,
mat.emission.y + mat.ambient.y * GLStateManager.getLightModel().ambient.y,
mat.emission.z + mat.ambient.z * GLStateManager.getLightModel().ambient.z,
mat.diffuse.w // alpha from diffuse
);
uploadVec4(program.locSceneColor, tempVec4);
}
if (program.locMaterialShininess != -1) {
GL20.glUniform1f(program.locMaterialShininess, mat.shininess);
}
// Per-light products
if (vk.light0Enabled()) {
final LightState light0 = GLStateManager.getLightDataStates()[0];
uploadVec4(program.locLight0Position, light0.position);
uploadLightProduct(program.locLightProd0Ambient, light0.ambient, mat.ambient);
uploadLightProduct(program.locLightProd0Diffuse, light0.diffuse, mat.diffuse);
uploadLightProduct(program.locLightProd0Specular, light0.specular, mat.specular);
}
if (vk.light1Enabled()) {
final LightState light1 = GLStateManager.getLightDataStates()[1];
uploadVec4(program.locLight1Position, light1.position);
uploadLightProduct(program.locLightProd1Ambient, light1.ambient, mat.ambient);
uploadLightProduct(program.locLightProd1Diffuse, light1.diffuse, mat.diffuse);
uploadLightProduct(program.locLightProd1Specular, light1.specular, mat.specular);
}
}
/**
* Upload pre-multiplied light product: light.X * material.X (RGB only).
*/
private void uploadLightProduct(int loc, Vector4f lightVal, Vector4f materialVal) {
if (loc == -1) return;
vec3Buf.clear();
vec3Buf.put(lightVal.x * materialVal.x);
vec3Buf.put(lightVal.y * materialVal.y);
vec3Buf.put(lightVal.z * materialVal.z);
vec3Buf.flip();
GL20.glUniform3(loc, vec3Buf);
}
private void uploadCurrentColor(Program program) {
if (program.locCurrentColor == -1) return;
// Upload the current color from GLSM
final var color = GLStateManager.getColor();
vec4Buf.clear();
vec4Buf.put(color.getRed());
vec4Buf.put(color.getGreen());
vec4Buf.put(color.getBlue());
vec4Buf.put(color.getAlpha());
vec4Buf.flip();
GL20.glUniform4(program.locCurrentColor, vec4Buf);
}
private void uploadCurrentTexCoord(Program program) {
if (program.locCurrentTexCoord == -1) return;
final var tc = ShaderManager.getCurrentTexCoord();
uploadVec4(program.locCurrentTexCoord, tc);
}
private void uploadCurrentLightmapCoord(Program program) {
if (program.locCurrentLightmapCoord == -1) return;
GL20.glUniform2f(program.locCurrentLightmapCoord,
OpenGlHelper.lastBrightnessX, OpenGlHelper.lastBrightnessY);
}
private void uploadCurrentNormal(Program program) {
if (program.locCurrentNormal == -1) return;
// Upload current normal from FFPShaderManager's tracked normal
final var normal = ShaderManager.getCurrentNormal();
vec3Buf.clear();
vec3Buf.put(normal.x);
vec3Buf.put(normal.y);
vec3Buf.put(normal.z);
vec3Buf.flip();
GL20.glUniform3(program.locCurrentNormal, vec3Buf);
}
private void uploadTexGen(Program program) {
final VertexKey vk = program.getVertexKey();
final TexGenState tg = GLStateManager.getTextures().getTexGenState(0);
if (vk.texGenModeS() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneS, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_S));
} else if (vk.texGenModeS() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneS, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_S));
}
if (vk.texGenModeT() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneT, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_T));
} else if (vk.texGenModeT() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneT, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_T));
}
if (vk.texGenModeR() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneR, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_R));
} else if (vk.texGenModeR() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneR, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_R));
}
if (vk.texGenModeQ() == VertexKey.TG_OBJ_LINEAR) {
uploadPlane(program.locTexGenObjPlaneQ, tg.getObjectPlane(org.lwjgl.opengl.GL11.GL_Q));
} else if (vk.texGenModeQ() == VertexKey.TG_EYE_LINEAR) {
uploadPlane(program.locTexGenEyePlaneQ, tg.getEyePlane(org.lwjgl.opengl.GL11.GL_Q));
}
}
private void uploadPlane(int loc, float[] plane) {
if (loc == -1) return;
vec4Buf.clear();
vec4Buf.put(plane[0]);
vec4Buf.put(plane[1]);
vec4Buf.put(plane[2]);
vec4Buf.put(plane[3]);
vec4Buf.flip();
GL20.glUniform4(loc, vec4Buf);
}
private void uploadClipPlanes(Program program) {
if (program.locClipPlanes == -1) return;
final ClipPlaneState cps = GLStateManager.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
clipPlaneBuf.flip();
GL20.glUniform4(program.locClipPlanes, clipPlaneBuf);
}
private void uploadFragmentUniforms(Program program) {
final FragmentKey fk = program.getFragmentKey();
// Alpha test reference
if (fk.alphaTestEnabled() && program.locAlphaRef != -1) {
GL20.glUniform1f(program.locAlphaRef, GLStateManager.getAlphaState().getReference());
}
// Per-unit tex env color
for (int i = 0; i < 4; i++) {
if (program.locTexEnvColor[i] != -1) {
final var envState = GLStateManager.getTextures().getTexEnvState(i);
vec4Buf.clear();
vec4Buf.put(envState.envColorR);
vec4Buf.put(envState.envColorG);
vec4Buf.put(envState.envColorB);
vec4Buf.put(envState.envColorA);
vec4Buf.flip();
GL20.glUniform4(program.locTexEnvColor[i], vec4Buf);
}
}
// Fog
if (fk.fogMode() != FragmentKey.FOG_NONE) {
uploadFog(program);
}
}
/**
* Upload optimized fog params (following Mesa's STATE_FOG_PARAMS_OPTIMIZED):
*/
private void uploadFog(Program program) {
final FogState fog = GLStateManager.getFogState();
if (program.locFogParams != -1) {
final float start = fog.getStart();
final float end = fog.getEnd();
final float density = fog.getDensity();
final float range = end - start;
vec4Buf.clear();
vec4Buf.put(range != 0.0f ? -1.0f / range : 0.0f); // [0]: -1/(end-start)
vec4Buf.put(range != 0.0f ? end / range : 1.0f); // [1]: end/(end-start)
vec4Buf.put((float)(density / LN2)); // [2]: density/ln(2)
vec4Buf.put((float)(density / SQRT_LN2)); // [3]: density/sqrt(ln(2))
vec4Buf.flip();
GL20.glUniform4(program.locFogParams, vec4Buf);
}
if (program.locFogColor != -1) {
vec4Buf.clear();
vec4Buf.put((float) fog.getFogColor().x);
vec4Buf.put((float) fog.getFogColor().y);
vec4Buf.put((float) fog.getFogColor().z);
vec4Buf.put(fog.getFogAlpha());
vec4Buf.flip();
GL20.glUniform4(program.locFogColor, vec4Buf);
}
}
private void uploadVec4(int loc, Vector4f v) {
if (loc == -1) return;
vec4Buf.clear();
vec4Buf.put(v.x);
vec4Buf.put(v.y);
vec4Buf.put(v.z);
vec4Buf.put(v.w);
vec4Buf.flip();
GL20.glUniform4(loc, vec4Buf);
}
public void destroy() {
memFree(mat4Buf);
memFree(mat3Buf);
memFree(vec4Buf);
memFree(vec3Buf);
memFree(clipPlaneBuf);
}
}
@@ -0,0 +1,257 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
/**
* Compact key capturing the vertex shader permutation axes for FFP emulation.
* Packed into a single long for fast hash/compare.
*/
public final class VertexKey {
private final long packed;
private static final int BIT_LIGHTING = 0;
private static final int BIT_LIGHT0 = 1;
private static final int BIT_LIGHT1 = 2;
private static final int BIT_LIGHT0_DIR = 3;
private static final int BIT_LIGHT1_DIR = 4;
private static final int BIT_COLOR_MATERIAL = 5;
private static final int BIT_SEPARATE_SPECULAR = 6;
private static final int BIT_FOG = 7;
private static final int BIT_FOG_DIST_MODE = 8;
private static final int BIT_NORMALIZE = 10;
private static final int BIT_RESCALE_NORMAL = 11;
private static final int BIT_TEXTURE = 12;
private static final int BIT_LIGHTMAP = 13;
private static final int BIT_TEX_MATRIX = 14;
private static final int BIT_HAS_VERTEX_COLOR = 15;
private static final int BIT_HAS_VERTEX_NORMAL = 16;
private static final int BIT_HAS_VERTEX_TEX = 17;
private static final int BIT_HAS_VERTEX_LIGHTMAP = 18;
private static final int BIT_COLOR_MAT_MODE = 19;
// bits 19-21: colorMaterialMode (3 bits)
// TexGen modes: 3 bits each (0=NONE, 1=OBJ_LINEAR, 2=EYE_LINEAR)
private static final int BIT_TEXGEN_S = 22;
private static final int BIT_TEXGEN_T = 25;
private static final int BIT_TEXGEN_R = 28;
private static final int BIT_TEXGEN_Q = 31;
// bits 31-33: texGenModeQ (3 bits)
private static final int BIT_CLIP_PLANES = 34;
public static final int TG_NONE = 0;
public static final int TG_OBJ_LINEAR = 1;
public static final int TG_EYE_LINEAR = 2;
public static final int CM_AMBIENT = 0;
public static final int CM_DIFFUSE = 1;
public static final int CM_SPECULAR = 2;
public static final int CM_EMISSION = 3;
public static final int CM_AMBIENT_AND_DIFFUSE = 4;
private VertexKey(long packed) {
this.packed = packed;
}
public long pack() { return packed; }
public boolean lightingEnabled() { return bit(BIT_LIGHTING); }
public boolean light0Enabled() { return bit(BIT_LIGHT0); }
public boolean light1Enabled() { return bit(BIT_LIGHT1); }
public boolean light0Directional() { return bit(BIT_LIGHT0_DIR); }
public boolean light1Directional() { return bit(BIT_LIGHT1_DIR); }
public boolean colorMaterialEnabled() { return bit(BIT_COLOR_MATERIAL); }
public boolean separateSpecular() { return bit(BIT_SEPARATE_SPECULAR); }
public boolean fogEnabled() { return bit(BIT_FOG); }
public int fogDistanceMode() { return (int)((packed >> BIT_FOG_DIST_MODE) & 0x3); }
public boolean normalizeEnabled() { return bit(BIT_NORMALIZE); }
public boolean rescaleNormalsEnabled() { return bit(BIT_RESCALE_NORMAL); }
public boolean textureEnabled() { return bit(BIT_TEXTURE); }
public boolean lightmapEnabled() { return bit(BIT_LIGHTMAP); }
public boolean textureMatrixEnabled() { return bit(BIT_TEX_MATRIX); }
public boolean hasVertexColor() { return bit(BIT_HAS_VERTEX_COLOR); }
public boolean hasVertexNormal() { return bit(BIT_HAS_VERTEX_NORMAL); }
public boolean hasVertexTexCoord() { return bit(BIT_HAS_VERTEX_TEX); }
public boolean hasVertexLightmap() { return bit(BIT_HAS_VERTEX_LIGHTMAP); }
/** Color material mode (CM_AMBIENT, CM_DIFFUSE, CM_SPECULAR, CM_EMISSION, CM_AMBIENT_AND_DIFFUSE). Only meaningful when colorMaterialEnabled(). */
public int colorMaterialMode() { return (int)((packed >> BIT_COLOR_MAT_MODE) & 0x7); }
/** TexGen mode for S coordinate (TG_NONE, TG_OBJ_LINEAR, TG_EYE_LINEAR). */
public int texGenModeS() { return (int)((packed >> BIT_TEXGEN_S) & 0x7); }
/** TexGen mode for T coordinate. */
public int texGenModeT() { return (int)((packed >> BIT_TEXGEN_T) & 0x7); }
/** TexGen mode for R coordinate. */
public int texGenModeR() { return (int)((packed >> BIT_TEXGEN_R) & 0x7); }
/** TexGen mode for Q coordinate. */
public int texGenModeQ() { return (int)((packed >> BIT_TEXGEN_Q) & 0x7); }
/** Whether any texgen coordinate is enabled. */
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
/** Whether any clip plane is enabled. */
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
/** Whether vertex color replaces material ambient in this variant. */
public boolean cmReplacesAmbient() { final int m = colorMaterialMode(); return m == CM_AMBIENT || m == CM_AMBIENT_AND_DIFFUSE; }
/** Whether vertex color replaces material diffuse in this variant. */
public boolean cmReplacesDiffuse() { final int m = colorMaterialMode(); return m == CM_DIFFUSE || m == CM_AMBIENT_AND_DIFFUSE; }
/** Whether vertex color replaces material specular in this variant. */
public boolean cmReplacesSpecular() { return colorMaterialMode() == CM_SPECULAR; }
/** Whether vertex color replaces material emission in this variant. */
public boolean cmReplacesEmission() { return colorMaterialMode() == CM_EMISSION; }
private boolean bit(int pos) { return ((packed >> pos) & 1) != 0; }
static int encodeTexGenMode(int glMode) {
return switch (glMode) {
case GL11.GL_OBJECT_LINEAR -> TG_OBJ_LINEAR;
case GL11.GL_EYE_LINEAR -> TG_EYE_LINEAR;
default -> TG_NONE;
};
}
static int encodeColorMaterialMode(int glMode) {
return switch (glMode) {
case GL11.GL_AMBIENT -> CM_AMBIENT;
case GL11.GL_DIFFUSE -> CM_DIFFUSE;
case GL11.GL_SPECULAR -> CM_SPECULAR;
case GL11.GL_EMISSION -> CM_EMISSION;
case GL11.GL_AMBIENT_AND_DIFFUSE -> CM_AMBIENT_AND_DIFFUSE;
default -> CM_AMBIENT_AND_DIFFUSE;
};
}
public static long packFromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
long bits = 0;
final boolean lighting = GLStateManager.getLightingState().isEnabled();
if (lighting) {
bits |= (1L << BIT_LIGHTING);
if (GLStateManager.getLightStates()[0].isEnabled()) {
bits |= (1L << BIT_LIGHT0);
// position.w == 0 means directional
if (GLStateManager.getLightDataStates()[0].position.w == 0.0f) {
bits |= (1L << BIT_LIGHT0_DIR);
}
}
if (GLStateManager.getLightStates()[1].isEnabled()) {
bits |= (1L << BIT_LIGHT1);
if (GLStateManager.getLightDataStates()[1].position.w == 0.0f) {
bits |= (1L << BIT_LIGHT1_DIR);
}
}
if (GLStateManager.getColorMaterial().isEnabled()) {
bits |= (1L << BIT_COLOR_MATERIAL);
final int cmMode = encodeColorMaterialMode(GLStateManager.getColorMaterialParameter().getValue());
bits |= ((long) cmMode & 0x7) << BIT_COLOR_MAT_MODE;
}
if (GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
bits |= (1L << BIT_SEPARATE_SPECULAR);
}
}
if (GLStateManager.getFogMode().isEnabled()) {
bits |= (1L << BIT_FOG);
final int fogDistMode = GLStateManager.getFogState().getFogDistanceMode();
bits |= ((long) fogDistMode & 0x3) << BIT_FOG_DIST_MODE;
}
if (GLStateManager.getNormalizeState().isEnabled()) {
bits |= (1L << BIT_NORMALIZE);
}
if (GLStateManager.getRescaleNormalState().isEnabled()) {
bits |= (1L << BIT_RESCALE_NORMAL);
}
// Texture unit 0 (base texture)
if (GLStateManager.getTextures().getTextureUnitStates(0).isEnabled()) {
bits |= (1L << BIT_TEXTURE);
}
// Texture unit 1 (lightmap)
if (GLStateManager.getTextures().getTextureUnitStates(1).isEnabled()) {
bits |= (1L << BIT_LIGHTMAP);
}
// Check if texture matrix unit 0 is non-identity
final Matrix4f texMat = GLStateManager.getTextures().getTextureUnitMatrix(0);
if (!DisplayListManager.isIdentity(texMat)) {
bits |= (1L << BIT_TEX_MATRIX);
}
// TexGen (unit 0 only) — encode per-coordinate mode if enabled
final var texUnit = GLStateManager.getTextures();
final TexGenState tg = texUnit.getTexGenState(0);
boolean anyTexGen = false;
if (texUnit.getTexGenSStates(0).isEnabled() && tg.getMode(GL11.GL_S) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_S));
bits |= ((long) m & 0x7) << BIT_TEXGEN_S;
if (m != TG_NONE) anyTexGen = true;
}
if (texUnit.getTexGenTStates(0).isEnabled() && tg.getMode(GL11.GL_T) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_T));
bits |= ((long) m & 0x7) << BIT_TEXGEN_T;
if (m != TG_NONE) anyTexGen = true;
}
if (texUnit.getTexGenRStates(0).isEnabled() && tg.getMode(GL11.GL_R) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_R));
bits |= ((long) m & 0x7) << BIT_TEXGEN_R;
if (m != TG_NONE) anyTexGen = true;
}
if (texUnit.getTexGenQStates(0).isEnabled() && tg.getMode(GL11.GL_Q) != 0) {
final int m = encodeTexGenMode(tg.getMode(GL11.GL_Q));
bits |= ((long) m & 0x7) << BIT_TEXGEN_Q;
if (m != TG_NONE) anyTexGen = true;
}
// Force texture matrix on when texgen is active (end portal pattern always uses it)
if (anyTexGen) {
bits |= (1L << BIT_TEX_MATRIX);
}
// Vertex format flags
if (hasColor) bits |= (1L << BIT_HAS_VERTEX_COLOR);
if (hasNormal) bits |= (1L << BIT_HAS_VERTEX_NORMAL);
if (hasTexCoord) bits |= (1L << BIT_HAS_VERTEX_TEX);
if (hasLightmap) bits |= (1L << BIT_HAS_VERTEX_LIGHTMAP);
// Clip planes
if (GLStateManager.anyClipPlaneEnabled()) {
bits |= (1L << BIT_CLIP_PLANES);
}
return bits;
}
public static VertexKey fromState(boolean hasColor, boolean hasNormal, boolean hasTexCoord, boolean hasLightmap) {
return new VertexKey(packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap));
}
static VertexKey fromPacked(long packed) {
return new VertexKey(packed);
}
@Override
public int hashCode() {
return Long.hashCode(packed);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof VertexKey other)) return false;
return packed == other.packed;
}
@Override
public String toString() {
return String.format("FFPVertexKey[0x%09X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%b lm=%b col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b]",
packed, lightingEnabled(), light0Enabled(), light1Enabled(),
colorMaterialEnabled(), fogEnabled(), textureEnabled(), lightmapEnabled(),
hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(),
texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled());
}
}
@@ -0,0 +1,408 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
/**
* Generates GLSL 330 core vertex shaders for FFP emulation.
* Follows Mesa's ffvertex_prog.c build order.
*
* Attribute locations are sourced from {@link VertexFormatElement.Usage} to stay in sync with GTNHLib's vertex format setup.
*/
public final class VertexShaderGenerator {
private VertexShaderGenerator() {}
public static String generate(VertexKey key) {
final StringBuilder sb = new StringBuilder(2048);
sb.append("#version 330 core\n\n");
emitAttributes(sb, key);
emitUniforms(sb, key);
emitOutputs(sb, key);
sb.append("void main() {\n");
emitPositionTransform(sb, key);
if (key.lightingEnabled()) {
emitNormalTransform(sb, key);
emitLighting(sb, key);
} else {
emitColorPassthrough(sb, key);
}
emitTexCoordPassthrough(sb, key);
if (key.fogEnabled()) {
emitFogDistance(sb, key);
}
if (key.clipPlanesEnabled()) {
emitClipDistances(sb);
}
sb.append("}\n");
return sb.toString();
}
private static void emitAttributes(StringBuilder sb, VertexKey key) {
sb.append("// Vertex attributes\n");
sb.append("layout(location = ").append(VertexFormatElement.Usage.POSITION.getAttributeLocation()).append(") in vec3 a_Position;\n");
if (key.hasVertexColor()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.COLOR.getAttributeLocation()).append(") in vec4 a_Color;\n");
}
if (key.hasVertexTexCoord()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.PRIMARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord0;\n");
}
if (key.hasVertexLightmap()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.SECONDARY_UV.getAttributeLocation()).append(") in vec2 a_TexCoord1;\n");
}
if (key.hasVertexNormal()) {
sb.append("layout(location = ").append(VertexFormatElement.Usage.NORMAL.getAttributeLocation()).append(") in vec3 a_Normal;\n");
}
sb.append('\n');
}
private static void emitUniforms(StringBuilder sb, VertexKey key) {
sb.append("// Matrices\n");
sb.append("uniform mat4 u_ModelViewMatrix;\n");
sb.append("uniform mat4 u_ProjectionMatrix;\n");
sb.append("uniform mat4 u_MVPMatrix;\n");
if (key.lightingEnabled()) {
sb.append("uniform mat3 u_NormalMatrix;\n");
}
if (key.textureMatrixEnabled()) {
sb.append("uniform mat4 u_TextureMatrix0;\n");
}
// TexGen plane uniforms
if (key.texGenModeS() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneS;\n");
if (key.texGenModeS() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneS;\n");
if (key.texGenModeT() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneT;\n");
if (key.texGenModeT() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneT;\n");
if (key.texGenModeR() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneR;\n");
if (key.texGenModeR() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneR;\n");
if (key.texGenModeQ() == VertexKey.TG_OBJ_LINEAR) sb.append("uniform vec4 u_TexGenObjPlaneQ;\n");
if (key.texGenModeQ() == VertexKey.TG_EYE_LINEAR) sb.append("uniform vec4 u_TexGenEyePlaneQ;\n");
if (!key.hasVertexNormal() && key.lightingEnabled()) {
sb.append("uniform vec3 u_CurrentNormal;\n");
}
if (!key.hasVertexColor()) {
sb.append("uniform vec4 u_CurrentColor;\n");
}
if (!key.hasVertexTexCoord() && key.textureEnabled()) {
sb.append("uniform vec4 u_CurrentTexCoord;\n");
}
if (key.lightmapEnabled() && !key.hasVertexLightmap()) {
sb.append("uniform vec2 u_CurrentLightmapCoord;\n");
}
if (key.lightmapEnabled()) {
sb.append("uniform mat4 u_LightmapTextureMatrix;\n");
}
if (key.lightingEnabled()) {
emitLightingUniforms(sb, key);
}
if (key.lightingEnabled() && (key.normalizeEnabled() || key.rescaleNormalsEnabled())) {
// normalize is done in shader; rescale uses normalScale
if (key.rescaleNormalsEnabled() && !key.normalizeEnabled()) {
sb.append("uniform float u_NormalScale;\n");
}
}
if (key.clipPlanesEnabled()) {
sb.append("uniform vec4 u_ClipPlane[8];\n");
}
sb.append('\n');
}
private static void emitLightingUniforms(StringBuilder sb, VertexKey key) {
sb.append("\n// Lighting\n");
if (key.colorMaterialEnabled()) {
// When color material is active, we can't pre-compute light products so we upload raw light and material values
sb.append("uniform vec4 u_LightModelAmbient;\n");
sb.append("uniform vec4 u_MaterialEmission;\n");
sb.append("uniform vec4 u_MaterialAmbient;\n");
sb.append("uniform vec4 u_MaterialDiffuse;\n");
sb.append("uniform vec4 u_MaterialSpecular;\n");
sb.append("uniform float u_MaterialShininess;\n");
if (key.light0Enabled()) {
sb.append("uniform vec4 u_Light0Ambient;\n");
sb.append("uniform vec4 u_Light0Diffuse;\n");
sb.append("uniform vec4 u_Light0Specular;\n");
sb.append("uniform vec4 u_Light0Position;\n");
}
if (key.light1Enabled()) {
sb.append("uniform vec4 u_Light1Ambient;\n");
sb.append("uniform vec4 u_Light1Diffuse;\n");
sb.append("uniform vec4 u_Light1Specular;\n");
sb.append("uniform vec4 u_Light1Position;\n");
}
} else {
// Pre-computed: sceneColor + lightProducts
sb.append("uniform vec4 u_SceneColor;\n");
if (key.light0Enabled()) {
sb.append("uniform vec4 u_Light0Position;\n");
sb.append("uniform vec3 u_LightProd0Ambient;\n");
sb.append("uniform vec3 u_LightProd0Diffuse;\n");
sb.append("uniform vec3 u_LightProd0Specular;\n");
}
if (key.light1Enabled()) {
sb.append("uniform vec4 u_Light1Position;\n");
sb.append("uniform vec3 u_LightProd1Ambient;\n");
sb.append("uniform vec3 u_LightProd1Diffuse;\n");
sb.append("uniform vec3 u_LightProd1Specular;\n");
}
sb.append("uniform float u_MaterialShininess;\n");
}
}
private static void emitOutputs(StringBuilder sb, VertexKey key) {
sb.append("// Outputs\n");
sb.append("out vec4 v_Color;\n");
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append("out vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("out vec4 v_TexCoord1;\n");
}
if (key.fogEnabled()) {
sb.append("out float v_FogCoord;\n");
}
sb.append('\n');
}
private static void emitPositionTransform(StringBuilder sb, VertexKey key) {
sb.append(" // Position transform\n");
sb.append(" vec4 pos4 = vec4(a_Position, 1.0);\n");
sb.append(" gl_Position = u_MVPMatrix * pos4;\n");
// Eye position needed for lighting, fog, EYE_LINEAR texgen, and clip planes
if (key.lightingEnabled() || key.fogEnabled() || texGenNeedsEyePos(key) || key.clipPlanesEnabled()) {
sb.append(" vec4 eyePos = u_ModelViewMatrix * pos4;\n");
}
sb.append('\n');
}
private static void emitNormalTransform(StringBuilder sb, VertexKey key) {
sb.append(" // Normal transform\n");
if (key.hasVertexNormal()) {
sb.append(" vec3 normal = u_NormalMatrix * a_Normal;\n");
} else {
sb.append(" vec3 normal = u_NormalMatrix * u_CurrentNormal;\n");
}
if (key.normalizeEnabled()) {
sb.append(" normal = normalize(normal);\n");
} else if (key.rescaleNormalsEnabled()) {
sb.append(" normal *= u_NormalScale;\n");
}
sb.append('\n');
}
private static void emitLighting(StringBuilder sb, VertexKey key) {
sb.append(" // Gouraud lighting\n");
if (key.colorMaterialEnabled()) {
// Color material: vertex color replaces specific material properties per glColorMaterial mode (Mesa ffvertex_prog.c get_material() bitmask approach)
sb.append(" vec4 matColor = ");
sb.append(key.hasVertexColor() ? "a_Color" : "u_CurrentColor");
sb.append(";\n");
// Emission source: vertex color for CM_EMISSION, else material uniform
final String emSrc = key.cmReplacesEmission() ? "matColor.rgb" : "u_MaterialEmission.rgb";
// Ambient source for scene color: vertex color for CM_AMBIENT / CM_AMBIENT_AND_DIFFUSE
final String amSrc = key.cmReplacesAmbient() ? "matColor.rgb" : "u_MaterialAmbient.rgb";
sb.append(" vec3 sceneColor = ").append(emSrc).append(" + ").append(amSrc).append(" * u_LightModelAmbient.rgb;\n");
// Alpha from diffuse source (per GL spec)
sb.append(" float outAlpha = ").append(key.cmReplacesDiffuse() ? "matColor.a" : "u_MaterialDiffuse.a").append(";\n");
} else {
// Pre-computed scene color
sb.append(" vec3 sceneColor = u_SceneColor.rgb;\n");
sb.append(" float outAlpha = u_SceneColor.a;\n");
}
sb.append(" vec3 color0 = sceneColor;\n");
if (key.separateSpecular()) {
sb.append(" vec3 specAccum = vec3(0.0);\n");
}
// Per-light accumulation
if (key.light0Enabled()) {
emitLightContribution(sb, key, 0);
}
if (key.light1Enabled()) {
emitLightContribution(sb, key, 1);
}
sb.append(" v_Color = vec4(clamp(color0, 0.0, 1.0), outAlpha);\n");
if (key.separateSpecular()) {
sb.append(" v_SpecularColor = clamp(specAccum, 0.0, 1.0);\n");
}
sb.append('\n');
}
private static void emitLightContribution(StringBuilder sb, VertexKey key, int lightIndex) {
final String li = String.valueOf(lightIndex);
final boolean directional = (lightIndex == 0) ? key.light0Directional() : key.light1Directional();
final String posUniform = "u_Light" + li + "Position";
sb.append(" { // Light ").append(li).append('\n');
// Light vector
if (directional) {
// Directional: position is already normalized direction
sb.append(" vec3 VPpli = normalize(").append(posUniform).append(".xyz);\n");
} else {
// Positional light
sb.append(" vec3 VPpli = ").append(posUniform).append(".xyz - eyePos.xyz;\n");
sb.append(" VPpli = normalize(VPpli);\n");
}
// Dot products
sb.append(" float NdotVP = max(dot(normal, VPpli), 0.0);\n");
// Half vector for specular (infinite viewer: H = normalize(L + (0,0,1)))
sb.append(" vec3 halfVec = normalize(VPpli + vec3(0.0, 0.0, 1.0));\n");
sb.append(" float NdotHV = max(dot(normal, halfVec), 0.0);\n");
// Specular power
sb.append(" float spec = (NdotVP > 0.0 && u_MaterialShininess > 0.0) ? pow(NdotHV, u_MaterialShininess) : 0.0;\n");
// Accumulate — per-property source depends on color material mode
if (key.colorMaterialEnabled()) {
final String ambU = "u_Light" + li + "Ambient";
final String difU = "u_Light" + li + "Diffuse";
final String speU = "u_Light" + li + "Specular";
// Ambient product: light.ambient * (vertex color or material)
final String ambMat = key.cmReplacesAmbient() ? "matColor.rgb" : "u_MaterialAmbient.rgb";
sb.append(" vec3 ambient = ").append(ambU).append(".rgb * ").append(ambMat).append(";\n");
// Diffuse product
final String difMat = key.cmReplacesDiffuse() ? "matColor.rgb" : "u_MaterialDiffuse.rgb";
sb.append(" vec3 diffuse = ").append(difU).append(".rgb * ").append(difMat).append(";\n");
// Specular product
final String speMat = key.cmReplacesSpecular() ? "matColor.rgb" : "u_MaterialSpecular.rgb";
sb.append(" vec3 specular = ").append(speU).append(".rgb * ").append(speMat).append(";\n");
} else {
sb.append(" vec3 ambient = u_LightProd").append(li).append("Ambient;\n");
sb.append(" vec3 diffuse = u_LightProd").append(li).append("Diffuse;\n");
sb.append(" vec3 specular = u_LightProd").append(li).append("Specular;\n");
}
sb.append(" color0 += ambient + NdotVP * diffuse;\n");
if (key.separateSpecular()) {
sb.append(" specAccum += spec * specular;\n");
} else {
sb.append(" color0 += spec * specular;\n");
}
sb.append(" }\n");
}
private static void emitColorPassthrough(StringBuilder sb, VertexKey key) {
sb.append(" // Color passthrough (no lighting)\n");
if (key.hasVertexColor()) {
sb.append(" v_Color = a_Color;\n");
} else {
sb.append(" v_Color = u_CurrentColor;\n");
}
sb.append('\n');
}
private static void emitTexCoordPassthrough(StringBuilder sb, VertexKey key) {
if (key.texGenEnabled()) {
emitTexGenCoordGeneration(sb, key);
} else if (key.textureEnabled() || key.hasVertexTexCoord()) {
sb.append(" // Texture coordinates\n");
if (key.hasVertexTexCoord()) {
if (key.textureMatrixEnabled()) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * vec4(a_TexCoord0, 0.0, 1.0);\n");
} else {
sb.append(" v_TexCoord0 = vec4(a_TexCoord0, 0.0, 1.0);\n");
}
} else {
if (key.textureMatrixEnabled()) {
sb.append(" v_TexCoord0 = u_TextureMatrix0 * u_CurrentTexCoord;\n");
} else {
sb.append(" v_TexCoord0 = u_CurrentTexCoord;\n");
}
}
}
if (key.lightmapEnabled()) {
if (key.hasVertexLightmap()) {
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(a_TexCoord1, 0.0, 1.0);\n");
} else {
sb.append(" v_TexCoord1 = u_LightmapTextureMatrix * vec4(u_CurrentLightmapCoord, 0.0, 1.0);\n");
}
}
}
/**
* Emit per-coordinate texgen code following Mesa's ffvertex_prog.c build order.
* OBJ_LINEAR: dot(pos4, objPlane), EYE_LINEAR: dot(eyePos, eyePlane).
* Coordinates without texgen use vertex attrib or default (0,0,0,1).
*/
private static void emitTexGenCoordGeneration(StringBuilder sb, VertexKey key) {
sb.append(" // TexGen coordinate generation\n");
sb.append(" vec4 texGenCoord = vec4(0.0, 0.0, 0.0, 1.0);\n");
emitTexGenComponent(sb, key.texGenModeS(), "s", "S", key);
emitTexGenComponent(sb, key.texGenModeT(), "t", "T", key);
emitTexGenComponent(sb, key.texGenModeR(), "r", "R", key);
emitTexGenComponent(sb, key.texGenModeQ(), "q", "Q", key);
// Always apply texture matrix when texgen is active (forced on in VertexKey)
sb.append(" v_TexCoord0 = u_TextureMatrix0 * texGenCoord;\n");
}
private static void emitTexGenComponent(StringBuilder sb, int mode, String swizzle, String coordName, VertexKey key) {
switch (mode) {
case VertexKey.TG_OBJ_LINEAR ->
sb.append(" texGenCoord.").append(swizzle).append(" = dot(pos4, u_TexGenObjPlane").append(coordName).append(");\n");
case VertexKey.TG_EYE_LINEAR ->
sb.append(" texGenCoord.").append(swizzle).append(" = dot(eyePos, u_TexGenEyePlane").append(coordName).append(");\n");
// TG_NONE: keep default (0 for s/t/r, 1 for q) — already set in texGenCoord init
}
}
private static boolean texGenNeedsEyePos(VertexKey key) {
return key.texGenModeS() == VertexKey.TG_EYE_LINEAR
|| key.texGenModeT() == VertexKey.TG_EYE_LINEAR
|| key.texGenModeR() == VertexKey.TG_EYE_LINEAR
|| key.texGenModeQ() == VertexKey.TG_EYE_LINEAR;
}
private static void emitFogDistance(StringBuilder sb, VertexKey key) {
sb.append(" // Fog distance\n");
// Mesa ffvertex_prog.c build_fog(): FDM_EYE_RADIAL/EYE_PLANE/EYE_PLANE_ABS
final int fogDistMode = key.fogDistanceMode();
switch (fogDistMode) {
case 0 -> // FDM_EYE_RADIAL: Euclidean distance
sb.append(" v_FogCoord = length(eyePos.xyz);\n");
case 1 -> // FDM_EYE_PLANE: raw Z (can be negative, clamped by fog factor)
sb.append(" v_FogCoord = eyePos.z;\n");
case 2 -> // FDM_EYE_PLANE_ABS: absolute Z
sb.append(" v_FogCoord = abs(eyePos.z);\n");
default ->
sb.append(" v_FogCoord = abs(eyePos.z);\n");
}
}
private static void emitClipDistances(StringBuilder sb) {
sb.append(" // Clip distances\n");
for (int i = 0; i < 8; i++) {
sb.append(" gl_ClipDistance[").append(i).append("] = dot(u_ClipPlane[").append(i).append("], eyePos);\n");
}
}
}
@@ -263,6 +263,15 @@ public final class CommandBuffer {
writeInt(height);
}
public void writeScissor(int x, int y, int width, int height) {
ensureCapacity(20);
writeInt(GLCommand.SCISSOR);
writeInt(x);
writeInt(y);
writeInt(width);
writeInt(height);
}
public void writeBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) {
ensureCapacity(20);
writeInt(GLCommand.BLEND_FUNC);
@@ -545,6 +554,16 @@ public final class CommandBuffer {
}
}
public void writeClipPlane(int plane, double a, double b, double c, double d) {
ensureCapacity(40);
writeInt(GLCommand.CLIP_PLANE);
writeInt(plane);
writeDouble(a);
writeDouble(b);
writeDouble(c);
writeDouble(d);
}
// === Draw commands ===
public void writeDrawRange(int vboIndex) {
@@ -10,6 +10,7 @@ import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -25,6 +26,8 @@ public final class CommandBufferExecutor {
private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer(16);
// Static reusable buffer for fog/light/material params
private static final FloatBuffer PARAMS_BUFFER = BufferUtils.createFloatBuffer(4);
// Static reusable buffer for clip plane equation
private static final DoubleBuffer PARAMS_DOUBLE_BUFFER = BufferUtils.createDoubleBuffer(4);
// Static reusable buffer for draw buffers
private static final int MAX_DRAW_BUFFERS = 8;
private static final IntBuffer DRAW_BUFFERS_BUFFER = BufferUtils.createIntBuffer(MAX_DRAW_BUFFERS);
@@ -196,6 +199,14 @@ public final class CommandBufferExecutor {
ptr += 16;
GLStateManager.glViewport(x, y, width, height);
}
case GLCommand.SCISSOR -> {
final int x = memGetInt(ptr);
final int y = memGetInt(ptr + 4);
final int width = memGetInt(ptr + 8);
final int height = memGetInt(ptr + 12);
ptr += 16;
GLStateManager.glScissor(x, y, width, height);
}
case GLCommand.BLEND_FUNC -> {
final int srcRgb = memGetInt(ptr);
final int dstRgb = memGetInt(ptr + 4);
@@ -449,6 +460,19 @@ public final class CommandBufferExecutor {
GLStateManager.glMaterial(face, pname, PARAMS_BUFFER);
}
case GLCommand.CLIP_PLANE -> {
final int plane = memGetInt(ptr);
final double a = memGetDouble(ptr + 4);
final double b = memGetDouble(ptr + 12);
final double c = memGetDouble(ptr + 20);
final double d = memGetDouble(ptr + 28);
ptr += 36;
PARAMS_DOUBLE_BUFFER.clear();
PARAMS_DOUBLE_BUFFER.put(a).put(b).put(c).put(d);
PARAMS_DOUBLE_BUFFER.flip();
GLStateManager.glClipPlane(plane, PARAMS_DOUBLE_BUFFER);
}
// === Draw commands ===
case GLCommand.DRAW_RANGE -> {
final int vboIndex = memGetInt(ptr);
@@ -519,7 +543,7 @@ public final class CommandBufferExecutor {
ptr += 4;
}
case GLCommand.BIND_VAO -> {
UniversalVAO.bindVertexArray(memGetInt(ptr));
GLStateManager.glBindVertexArray(memGetInt(ptr));
ptr += 4;
}
@@ -67,6 +67,7 @@ public final class CommandBufferProcessor {
// Four int commands
case GLCommand.VIEWPORT -> out.writeViewport(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.SCISSOR -> out.writeScissor(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.BLEND_FUNC -> out.writeBlendFunc(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.COLOR_MASK -> out.writeColorMask(raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0);
case GLCommand.STENCIL_FUNC_SEPARATE -> out.writeStencilFuncSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt());
@@ -142,6 +143,16 @@ public final class CommandBufferProcessor {
out.writeMaterial(face, pname, fb);
}
// Clip plane command (4 doubles)
case GLCommand.CLIP_PLANE -> {
final int plane = raw.readInt();
final double a = raw.readDouble();
final double b = raw.readDouble();
final double c = raw.readDouble();
final double d = raw.readDouble();
out.writeClipPlane(plane, a, b, c, d);
}
// Draw/call commands
case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt());
case GLCommand.CALL_LIST -> out.writeCallList(raw.readInt());
@@ -172,6 +172,11 @@ public final class CommandRecorder {
commandCount++;
}
public void recordScissor(int x, int y, int width, int height) {
buffer.writeScissor(x, y, width, height);
commandCount++;
}
public void recordPointSize(float size) {
buffer.writePointSize(size);
commandCount++;
@@ -287,6 +292,11 @@ public final class CommandRecorder {
commandCount++;
}
public void recordClipPlane(int plane, double a, double b, double c, double d) {
buffer.writeClipPlane(plane, a, b, c, d);
commandCount++;
}
public void recordStencilFunc(int func, int ref, int mask) {
buffer.writeStencilFunc(func, ref, mask);
commandCount++;
@@ -1,8 +1,6 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.IVertexBuffer;
import org.lwjgl.opengl.GL15;
/**
* A class that stores multiple vertex formats & their corresponding buffers.
@@ -14,7 +14,11 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.nmemFree;
/**
* Compiles multiple ByteBuffers of the same Vertex format into a VBO. Any buffer added to the builder will be freed afterward.
@@ -69,7 +73,7 @@ public final class DisplayListVBOBuilder {
vertexCount = format.getVertexCount(size);
}
if (data.drawMode == GL11.GL_QUADS) {
IVertexArrayObject indexedVAO = new IndexedVAO(vbo, IndexBuffer.convertQuadsToTrigs(start, start + vertexCount));
final IVertexArrayObject indexedVAO = new IndexedVAO(vbo, IndexBuffer.convertQuadsToTrigs(start, start + vertexCount));
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(indexedVAO, GL11.GL_TRIANGLES, 0, vertexCount / 4 * 6);
} else {
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, data.drawMode, start, vertexCount);
@@ -51,6 +51,7 @@ public final class GLCommand {
public static final int COLOR_MASK = 42; // [cmd:4][r:4][g:4][b:4][a:4] (0 or 1 each)
public static final int STENCIL_FUNC_SEPARATE = 43; // [cmd:4][face:4][func:4][ref:4][mask:4]
public static final int STENCIL_OP_SEPARATE = 44; // [cmd:4][face:4][sfail:4][dpfail:4][dppass:4]
public static final int SCISSOR = 45; // [cmd:4][x:4][y:4][width:4][height:4]
// === Float commands ===
public static final int POINT_SIZE = 50; // [cmd:4][size:4f]
@@ -88,6 +89,7 @@ public final class GLCommand {
public static final int LIGHT = 91; // [cmd:4][light:4][pname:4][count:4][params:16f] = 32 bytes
public static final int LIGHT_MODEL = 92; // [cmd:4][pname:4][count:4][params:16f] = 28 bytes
public static final int MATERIAL = 93; // [cmd:4][face:4][pname:4][count:4][params:16f] = 32 bytes
public static final int CLIP_PLANE = 94; // [cmd:4][plane:4][a:8d][b:8d][c:8d][d:8d] = 40 bytes
// === Draw commands ===
public static final int DRAW_RANGE = 100; // [cmd:4][vboIndex:4] = 8 bytes
@@ -147,6 +149,7 @@ public final class GLCommand {
case STENCIL_OP -> "STENCIL_OP";
case TEX_PARAMETERI -> "TEX_PARAMETERI";
case VIEWPORT -> "VIEWPORT";
case SCISSOR -> "SCISSOR";
case BLEND_FUNC -> "BLEND_FUNC";
case COLOR_MASK -> "COLOR_MASK";
case STENCIL_FUNC_SEPARATE -> "STENCIL_FUNC_SEPARATE";
@@ -177,6 +180,7 @@ public final class GLCommand {
case LIGHT -> "LIGHT";
case LIGHT_MODEL -> "LIGHT_MODEL";
case MATERIAL -> "MATERIAL";
case CLIP_PLANE -> "CLIP_PLANE";
case DRAW_RANGE -> "DRAW_RANGE";
case CALL_LIST -> "CALL_LIST";
case DRAW_BUFFER -> "DRAW_BUFFER";
@@ -220,6 +224,7 @@ public final class GLCommand {
// Four int commands (20 bytes)
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
GLCommand.STENCIL_FUNC_SEPARATE, GLCommand.STENCIL_OP_SEPARATE,
GLCommand.SCISSOR,
GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.BLEND_COLOR -> 20;
case GLCommand.DRAW_ELEMENTS -> 24;
@@ -251,6 +256,7 @@ public final class GLCommand {
final int count = memGetInt(ptr + 12); // light/face at +4, pname at +8, count at +12
yield 16 + count * 4;
}
case GLCommand.CLIP_PLANE -> 40; // cmd + plane + 4 doubles
default -> throw new IllegalStateException("Unknown command: " + cmd);
};
@@ -4,8 +4,6 @@ import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClientArrayState;
import net.minecraft.client.renderer.Tessellator;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.Buffer;
@@ -16,28 +14,57 @@ import java.nio.IntBuffer;
import java.nio.ShortBuffer;
/**
* Records immediate mode GL calls (glBegin/glEnd/glVertex) during display list compilation.
* Converts immediate mode geometry to ByteBuffer directly via {@link DirectTessellator}
* <p>
* Currently only used for display list compilation, should be perfectly usable outside of display lists too though.
* 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);
/** Separate tessellator for the splash thread. Nulled after splash completes. */
private static DirectTessellator splashTessellator;
private ImmediateModeRecorder() {
}
public static DirectTessellator getInternalTessellator() {
public static void initSplashTessellator() {
splashTessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
}
public static void destroySplashTessellator() {
splashTessellator = null;
}
private static DirectTessellator tessellator() {
final DirectTessellator splash = splashTessellator;
if (splash != null && Thread.currentThread() != GLStateManager.getMainThread()) {
return splash;
}
return tessellator;
}
public static DirectTessellator getInternalTessellator() {
return tessellator();
}
public static boolean isDrawing() {
return tessellator().isDrawing;
}
/**
* Set the current texture coordinate.
* Called by GLStateManager.glTexCoord*.
*/
public static void setTexCoord(float s, float t) {
tessellator.setTextureUV(s, t);
tessellator().setTextureUV(s, t);
}
/**
* Set the current lightmap coordinate as packed brightness.
* Called by GLStateManager.glMultiTexCoord2f for unit 1 during recording/drawing.
*/
public static void setLightmapCoord(float s, float t) {
tessellator().setBrightness(((int) t << 16) | ((int) s & 0xFFFF));
}
/**
@@ -45,14 +72,39 @@ public final class ImmediateModeRecorder {
* Called by GLStateManager.glNormal*.
*/
public static void setNormal(float x, float y, float z) {
tessellator.setNormal(x, y, z);
tessellator().setNormal(x, y, z);
}
/**
* Set the current color on the internal tessellator.
* Called by GLStateManager.glColor* when inside a glBegin/glEnd block.
*/
public static void setColor(float r, float g, float b, float a) {
tessellator().setColorRGBA((int) (r * 255.0f), (int) (g * 255.0f), (int) (b * 255.0f), (int) (a * 255.0f));
}
/**
* Start recording a new primitive.
* Resets the tessellator to clear any data from the previous draw.
*
* @param primitiveType GL primitive type (GL_QUADS, GL_TRIANGLES, etc.)
*/
public static void begin(int primitiveType) {
tessellator.startDrawing(primitiveType);
final DirectTessellator t = tessellator();
t.reset();
t.startDrawing(primitiveType);
}
/**
* Start recording a new primitive with initial color sync from GLSM cache.
* Used for live immediate mode emulation only (not display list recording).
*/
public static void beginLive(int primitiveType) {
final DirectTessellator t = tessellator();
t.reset();
t.startDrawing(primitiveType);
final var c = GLStateManager.getColor();
t.setColorRGBA((int) (c.getRed() * 255.0f), (int) (c.getGreen() * 255.0f), (int) (c.getBlue() * 255.0f), (int) (c.getAlpha() * 255.0f));
}
/**
@@ -62,15 +114,16 @@ public final class ImmediateModeRecorder {
* @return Result containing quads, lines, and flags, or null if no geometry was produced
*/
public static DirectTessellator end() {
if (!tessellator.isDrawing) {
final DirectTessellator t = tessellator();
if (!t.isDrawing) {
throw new IllegalStateException("glEnd called without glBegin");
}
tessellator.isDrawing = false;
t.isDrawing = false;
if (tessellator.isEmpty()) return null;
if (t.isEmpty()) return null;
return tessellator;
return t;
}
/**
@@ -78,10 +131,11 @@ public final class ImmediateModeRecorder {
* Color is read from GLStateManager at call time.
*/
public static void vertex(float x, float y, float z) {
if (!tessellator.isDrawing) {
final DirectTessellator t = tessellator();
if (!t.isDrawing) {
throw new IllegalStateException("glVertex called outside glBegin/glEnd");
}
tessellator.addVertex(x, y, z);
t.addVertex(x, y, z);
}
/**
@@ -92,7 +146,7 @@ public final class ImmediateModeRecorder {
* where primitives might fail to convert (e.g., incomplete quads).
*/
public static boolean hasGeometry() {
return tessellator.vertexCount != 0;
return tessellator().vertexCount != 0;
}
/**
@@ -100,7 +154,7 @@ public final class ImmediateModeRecorder {
* Called when starting a new display list.
*/
public static void reset() {
tessellator.reset();
tessellator().reset();
}
/**
@@ -116,13 +170,19 @@ public final class ImmediateModeRecorder {
final ClientArrayState cas = GLStateManager.getClientArrayState();
final Buffer vertexPointer = cas.isVertexArrayEnabled() ? cas.getVertexPointer() : null;
final Buffer colorPointer = cas.isColorArrayEnabled() ? cas.getColorPointer() : null;
final Buffer normalPointer = cas.isNormalArrayEnabled() ? cas.getNormalPointer() : null;
final Buffer texCoordPointer = cas.isTexCoordArrayEnabled() ? cas.getTexCoordPointer() : null;
return convertClientArrays(
mode, first, count,
vertexPointer,
cas.getVertexPointerType(), cas.getVertexPointerSize(), cas.getVertexPointerStride(),
colorPointer,
cas.getColorPointerType(), cas.getColorPointerSize(), cas.getColorPointerStride()
cas.getColorPointerType(), cas.getColorPointerSize(), cas.getColorPointerStride(),
normalPointer,
cas.getNormalPointerType(), cas.getNormalPointerStride(),
texCoordPointer,
cas.getTexCoordPointerType(), cas.getTexCoordPointerSize(), cas.getTexCoordPointerStride()
);
}
@@ -130,33 +190,23 @@ public final class ImmediateModeRecorder {
* Convert client array data to immediate mode.
* Called by glDrawArrays during display list recording.
* Like Mesa's save_DrawArrays, reads vertex data from arrays and emits as immediate mode.
*
* @param mode Primitive type (GL_QUADS, GL_LINES, etc.)
* @param first Starting index in arrays
* @param count Number of vertices
* @param vertexPointer Vertex position buffer (or null if not enabled)
* @param vertexType GL type (GL_FLOAT, GL_DOUBLE, etc.)
* @param vertexSize Size of vertex (2, 3, or 4)
* @param vertexStride Stride in bytes (0 for tightly packed)
* @param colorPointer Color buffer (or null if not enabled)
* @param colorType GL type (GL_FLOAT, GL_UNSIGNED_BYTE, etc.)
* @param colorSize Size of color (3 or 4)
* @param colorStride Stride in bytes (0 for tightly packed)
* @return Result containing converted geometry, or null if no geometry produced
*/
public static DirectTessellator convertClientArrays(
int mode, int first, int count,
java.nio.Buffer vertexPointer, int vertexType, int vertexSize, int vertexStride,
java.nio.Buffer colorPointer, int colorType, int colorSize, int colorStride
java.nio.Buffer colorPointer, int colorType, int colorSize, int colorStride,
java.nio.Buffer normalPointer, int normalType, int normalStride,
java.nio.Buffer texCoordPointer, int texCoordType, int texCoordSize, int texCoordStride
) {
begin(mode);
final DirectTessellator t = tessellator();
// Hoist type checks and stride calculations outside the loop for performance.
// Mesa's algorithm: offset_bytes = index * effective_stride
// effective_stride = (stride == 0) ? (numComponents * sizeof(element)) : stride
// Determine color reader (hoisted out of loop)
final ColorReader colorReader;
// Determine color reader
final ArrayReader colorReader;
if (colorPointer == null) {
colorReader = null;
} else if (colorType == GL11.GL_FLOAT && colorPointer instanceof FloatBuffer fb) {
@@ -185,8 +235,46 @@ public final class ImmediateModeRecorder {
colorReader = (idx) -> GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
// Determine vertex reader (hoisted out of loop)
final VertexReader vertexReader;
// Determine normal reader
final ArrayReader normalReader;
if (normalPointer == null) {
normalReader = null;
} else if (normalType == GL11.GL_FLOAT && normalPointer instanceof FloatBuffer fb) {
final int effectiveStride = (normalStride == 0) ? 3 * 4 : normalStride;
final int strideInFloats = effectiveStride / 4;
normalReader = (idx) -> {
final int offset = idx * strideInFloats;
setNormal(fb.get(offset), fb.get(offset + 1), fb.get(offset + 2));
};
} else if (normalType == GL11.GL_BYTE && normalPointer instanceof ByteBuffer bb) {
final int effectiveStride = (normalStride == 0) ? 3 : normalStride;
normalReader = (idx) -> {
final int offset = idx * effectiveStride;
setNormal(bb.get(offset) / 127.0f, bb.get(offset + 1) / 127.0f, bb.get(offset + 2) / 127.0f);
};
} else {
normalReader = null;
}
// Determine texcoord reader
final ArrayReader texCoordReader;
if (texCoordPointer == null) {
texCoordReader = null;
} else if (texCoordType == GL11.GL_FLOAT && texCoordPointer instanceof FloatBuffer fb) {
final int effectiveStride = (texCoordStride == 0) ? texCoordSize * 4 : texCoordStride;
final int strideInFloats = effectiveStride / 4;
texCoordReader = (idx) -> {
final int offset = idx * strideInFloats;
final float s = fb.get(offset);
final float tt = (texCoordSize >= 2) ? fb.get(offset + 1) : 0.0f;
setTexCoord(s, tt);
};
} else {
texCoordReader = null;
}
// Determine vertex reader
final ArrayReader vertexReader;
if (vertexPointer == null) {
vertexReader = null;
} else if (vertexType == GL11.GL_FLOAT && vertexPointer instanceof FloatBuffer fb) {
@@ -197,7 +285,7 @@ public final class ImmediateModeRecorder {
final float x = fb.get(offset);
final float y = fb.get(offset + 1);
final float z = (vertexSize >= 3) ? fb.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_DOUBLE && vertexPointer instanceof DoubleBuffer db) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 8 : vertexStride;
@@ -207,7 +295,7 @@ public final class ImmediateModeRecorder {
final float x = (float) db.get(offset);
final float y = (float) db.get(offset + 1);
final float z = (vertexSize >= 3) ? (float) db.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_INT && vertexPointer instanceof IntBuffer ib) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 4 : vertexStride;
@@ -217,7 +305,7 @@ public final class ImmediateModeRecorder {
final float x = ib.get(offset);
final float y = ib.get(offset + 1);
final float z = (vertexSize >= 3) ? ib.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_SHORT && vertexPointer instanceof ShortBuffer sb) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 2 : vertexStride;
@@ -227,7 +315,7 @@ public final class ImmediateModeRecorder {
final float x = sb.get(offset);
final float y = sb.get(offset + 1);
final float z = (vertexSize >= 3) ? sb.get(offset + 2) : 0.0f;
vertex(x, y, z);
t.addVertex(x, y, z);
};
} else {
// Unsupported vertex type
@@ -236,6 +324,12 @@ public final class ImmediateModeRecorder {
// Main loop - no type checks, just function calls
for (int i = first; i < first + count; i++) {
if (normalReader != null) {
normalReader.read(i);
}
if (texCoordReader != null) {
texCoordReader.read(i);
}
if (colorReader != null) {
colorReader.read(i);
}
@@ -247,15 +341,8 @@ public final class ImmediateModeRecorder {
return end();
}
@FunctionalInterface
private interface ColorReader {
void read(int index);
}
@FunctionalInterface
private interface VertexReader {
private interface ArrayReader {
void read(int index);
}
}
@@ -22,8 +22,7 @@ public class BooleanStateStack extends BooleanState implements IStateStack<Boole
protected int savedDepth;
public BooleanStateStack(int glCap) {
// Most booleans default to false
this(glCap, false);
this(glCap, false, false);
}
/**
@@ -34,7 +33,18 @@ public class BooleanStateStack extends BooleanState implements IStateStack<Boole
* @param initialState initial enabled state
*/
public BooleanStateStack(int glCap, boolean initialState) {
super(glCap);
this(glCap, initialState, false);
}
/**
* Create a BooleanStateStack with custom initial state and FFP flag.
*
* @param glCap GL capability constant
* @param initialState initial enabled state
* @param ffpStateOnly when true, GL calls are skipped when FFP emulation is active
*/
public BooleanStateStack(int glCap, boolean initialState, boolean ffpStateOnly) {
super(glCap, ffpStateOnly);
this.enabled = initialState;
stack = new boolean[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
}
@@ -12,12 +12,12 @@ public class TextureUnitBooleanStateStack extends BooleanStateStack {
private final int unitIndex;
public TextureUnitBooleanStateStack(int glCap, int unitIndex) {
super(glCap);
super(glCap, false, true); // All texture enable/disable states are FFP-only
this.unitIndex = unitIndex;
}
public TextureUnitBooleanStateStack(int glCap, int unitIndex, boolean initialState) {
super(glCap, initialState);
super(glCap, initialState, true);
this.unitIndex = unitIndex;
}
@@ -41,12 +41,14 @@ public class TextureUnitBooleanStateStack extends BooleanStateStack {
if (bypass || enabled != this.enabled) {
if (!bypass) this.enabled = enabled;
int currentUnit = GLStateManager.getActiveTextureUnit();
boolean needsSwitch = currentUnit != unitIndex;
if (!ffpStateOnly) {
final int currentUnit = GLStateManager.getActiveTextureUnit();
final boolean needsSwitch = currentUnit != unitIndex;
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + unitIndex);
if (enabled) GL11.glEnable(glCap); else GL11.glDisable(glCap);
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + currentUnit);
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + unitIndex);
if (enabled) GL11.glEnable(glCap); else GL11.glDisable(glCap);
if (needsSwitch) GL13.glActiveTexture(GL13.GL_TEXTURE0 + currentUnit);
}
}
}
@@ -0,0 +1,47 @@
package com.gtnewhorizons.angelica.glsm.stacks;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Vector3f;
/**
* Stack for a Vector3f value (e.g. current normal).
*/
public class Vec3fStack implements IStateStack<Vec3fStack> {
private final Vector3f value;
private final float[][] stack;
private int pointer;
public Vec3fStack(Vector3f value) {
this.value = value;
stack = new float[GLStateManager.MAX_ATTRIB_STACK_DEPTH][3];
}
@Override
public Vec3fStack push() {
if (pointer == stack.length) {
throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached");
}
stack[pointer][0] = value.x;
stack[pointer][1] = value.y;
stack[pointer][2] = value.z;
pointer++;
return this;
}
@Override
public Vec3fStack pop() {
if (pointer == 0) {
throw new IllegalStateException("Stack underflow");
}
pointer--;
value.set(stack[pointer][0], stack[pointer][1], stack[pointer][2]);
return this;
}
@Override
public boolean isEmpty() {
return pointer == 0;
}
}
@@ -0,0 +1,48 @@
package com.gtnewhorizons.angelica.glsm.stacks;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Vector4f;
/**
* Stack for a Vector4f value (e.g. current texture coordinates).
*/
public class Vec4fStack implements IStateStack<Vec4fStack> {
private final Vector4f value;
private final float[][] stack;
private int pointer;
public Vec4fStack(Vector4f value) {
this.value = value;
stack = new float[GLStateManager.MAX_ATTRIB_STACK_DEPTH][4];
}
@Override
public Vec4fStack push() {
if (pointer == stack.length) {
throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached");
}
stack[pointer][0] = value.x;
stack[pointer][1] = value.y;
stack[pointer][2] = value.z;
stack[pointer][3] = value.w;
pointer++;
return this;
}
@Override
public Vec4fStack pop() {
if (pointer == 0) {
throw new IllegalStateException("Stack underflow");
}
pointer--;
value.set(stack[pointer][0], stack[pointer][1], stack[pointer][2], stack[pointer][3]);
return this;
}
@Override
public boolean isEmpty() {
return pointer == 0;
}
}
@@ -7,7 +7,7 @@ import org.lwjgl.opengl.GL11;
@Getter
public class AlphaState implements ISettableState<AlphaState> {
@Setter protected int function = GL11.GL_ALWAYS;
@Setter protected float reference = -1.0F;
@Setter protected float reference = 0.0F;
@Override
public AlphaState set(AlphaState state) {
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.states;
import lombok.Getter;
import lombok.Setter;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL14;
@Getter @Setter
@@ -11,6 +12,12 @@ public class BlendState implements ISettableState<BlendState> {
protected int dstRgb = GL11.GL_ZERO;
protected int srcAlpha = GL11.GL_ONE;
protected int dstAlpha = GL11.GL_ZERO;
protected int equationRgb = GL14.GL_FUNC_ADD;
protected int equationAlpha = GL14.GL_FUNC_ADD;
protected float blendColorR = 0.0f;
protected float blendColorG = 0.0f;
protected float blendColorB = 0.0f;
protected float blendColorA = 0.0f;
public BlendState() {}
@@ -39,6 +46,12 @@ public class BlendState implements ISettableState<BlendState> {
this.dstRgb = state.dstRgb;
this.srcAlpha = state.srcAlpha;
this.dstAlpha = state.dstAlpha;
this.equationRgb = state.equationRgb;
this.equationAlpha = state.equationAlpha;
this.blendColorR = state.blendColorR;
this.blendColorG = state.blendColorG;
this.blendColorB = state.blendColorB;
this.blendColorA = state.blendColorA;
return this;
}
@@ -46,7 +59,13 @@ public class BlendState implements ISettableState<BlendState> {
public boolean sameAs(Object state) {
if (this == state) return true;
if (!(state instanceof BlendState blendState)) return false;
return srcRgb == blendState.srcRgb && dstRgb == blendState.dstRgb && srcAlpha == blendState.srcAlpha && dstAlpha == blendState.dstAlpha;
return srcRgb == blendState.srcRgb && dstRgb == blendState.dstRgb
&& srcAlpha == blendState.srcAlpha && dstAlpha == blendState.dstAlpha
&& equationRgb == blendState.equationRgb && equationAlpha == blendState.equationAlpha
&& Float.compare(blendColorR, blendState.blendColorR) == 0
&& Float.compare(blendColorG, blendState.blendColorG) == 0
&& Float.compare(blendColorB, blendState.blendColorB) == 0
&& Float.compare(blendColorA, blendState.blendColorA) == 0;
}
@Override
@@ -6,11 +6,22 @@ import org.lwjgl.opengl.GL11;
public class BooleanState implements ISettableState<BooleanState> {
protected final int glCap;
protected final boolean ffpStateOnly;
@Getter protected boolean enabled;
protected boolean stateUnknown;
public BooleanState(int glCap) {
this(glCap, false);
}
public BooleanState(int glCap, boolean ffpStateOnly) {
this.glCap = glCap;
this.ffpStateOnly = ffpStateOnly;
}
public void setUnknownState() {
stateUnknown = true;
}
public void disable() {
@@ -23,15 +34,17 @@ public class BooleanState implements ISettableState<BooleanState> {
public void setEnabled(boolean enabled) {
final boolean bypass = GLStateManager.shouldBypassCache();
if (bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) {
if (stateUnknown || bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) {
stateUnknown = false;
if (!bypass) {
this.enabled = enabled;
}
// Always call GL - the calling method controls whether we reach here based on recording mode
if (enabled) {
GL11.glEnable(this.glCap);
} else {
GL11.glDisable(this.glCap);
if (!ffpStateOnly) {
if (enabled) {
GL11.glEnable(this.glCap);
} else {
GL11.glDisable(this.glCap);
}
}
}
}
@@ -60,6 +60,19 @@ public class ClientArrayState {
return vertexPointerStride;
}
// Normal array state
private boolean normalArrayEnabled = false;
private Buffer normalPointer = null;
private int normalPointerType = GL11.GL_FLOAT;
private int normalPointerStride = 0;
// TexCoord array state
private boolean texCoordArrayEnabled = false;
private Buffer texCoordPointer = null;
private int texCoordPointerSize = 4;
private int texCoordPointerType = GL11.GL_FLOAT;
private int texCoordPointerStride = 0;
// Color array methods
public void setColorPointer(int size, int type, int stride, Buffer pointer) {
this.colorPointerSize = size;
@@ -91,4 +104,63 @@ public class ClientArrayState {
public int getColorPointerStride() {
return colorPointerStride;
}
// Normal array methods
public void setNormalPointer(int type, int stride, Buffer pointer) {
this.normalPointerType = type;
this.normalPointerStride = stride;
this.normalPointer = pointer;
}
public boolean isNormalArrayEnabled() {
return normalArrayEnabled;
}
public void setNormalArrayEnabled(boolean enabled) {
this.normalArrayEnabled = enabled;
}
public Buffer getNormalPointer() {
return normalPointer;
}
public int getNormalPointerType() {
return normalPointerType;
}
public int getNormalPointerStride() {
return normalPointerStride;
}
// TexCoord array methods
public void setTexCoordPointer(int size, int type, int stride, Buffer pointer) {
this.texCoordPointerSize = size;
this.texCoordPointerType = type;
this.texCoordPointerStride = stride;
this.texCoordPointer = pointer;
}
public boolean isTexCoordArrayEnabled() {
return texCoordArrayEnabled;
}
public void setTexCoordArrayEnabled(boolean enabled) {
this.texCoordArrayEnabled = enabled;
}
public Buffer getTexCoordPointer() {
return texCoordPointer;
}
public int getTexCoordPointerSize() {
return texCoordPointerSize;
}
public int getTexCoordPointerType() {
return texCoordPointerType;
}
public int getTexCoordPointerStride() {
return texCoordPointerStride;
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm.states;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import java.nio.FloatBuffer;
/**
* Stores eye-space clip plane equations for core profile emulation.
*/
public class ClipPlaneState {
private final float[][] eyePlanes = new float[GLStateManager.MAX_CLIP_PLANES][4];
private static final Vector4f tempPlane = new Vector4f();
private static final Matrix4f tempInverse = new Matrix4f();
public void setPlane(int index, double a, double b, double c, double d, Matrix4f modelView) {
final float[] dest = eyePlanes[index];
modelView.invert(tempInverse);
tempPlane.set((float) a, (float) b, (float) c, (float) d);
tempInverse.transformTranspose(tempPlane);
dest[0] = tempPlane.x;
dest[1] = tempPlane.y;
dest[2] = tempPlane.z;
dest[3] = tempPlane.w;
}
/** Writes the 4-component eye-space plane equation into {@code buf} at its current position. */
public void putEyePlane(int index, FloatBuffer buf) {
final float[] p = eyePlanes[index];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
}
}
@@ -8,11 +8,13 @@ import org.lwjgl.opengl.GL11;
public class DepthState implements ISettableState<DepthState> {
protected boolean enabled = true;
protected int func = GL11.GL_LESS;
protected double clearValue = 1.0;
@Override
public DepthState set(DepthState state) {
this.enabled = state.enabled;
this.func = state.func;
this.clearValue = state.clearValue;
return this;
}
@@ -20,7 +22,7 @@ public class DepthState implements ISettableState<DepthState> {
public boolean sameAs(Object state) {
if (this == state) return true;
if (!(state instanceof DepthState depthState)) return false;
return enabled == depthState.enabled && func == depthState.func;
return enabled == depthState.enabled && func == depthState.func && Double.compare(clearValue, depthState.clearValue) == 0;
}
@Override
public DepthState copy() {
@@ -16,6 +16,8 @@ public class FogState implements ISettableState<FogState> {
protected float density = 1.0F;
protected float start;
protected float end = 1.0F;
/** Fog distance computation mode: 0=EYE_RADIAL, 1=EYE_PLANE, 2=EYE_PLANE_ABS */
protected int fogDistanceMode = 2;
@Override
public FogState set(FogState state) {
@@ -23,16 +25,20 @@ public class FogState implements ISettableState<FogState> {
this.fogColor.set(state.fogColor);
this.fogAlpha = state.fogAlpha;
this.fogColorBuffer.put(0, state.fogColorBuffer.get(0));
this.fogColorBuffer.put(1, state.fogColorBuffer.get(1));
this.fogColorBuffer.put(2, state.fogColorBuffer.get(2));
this.fogColorBuffer.put(3, state.fogColorBuffer.get(3));
this.density = state.density;
this.start = state.start;
this.end = state.end;
this.fogDistanceMode = state.fogDistanceMode;
return this;
}
@Override
public boolean sameAs(Object state) {
if (this == state) return true;
if (!(state instanceof FogState fogState)) return false;
return fogMode == fogState.fogMode && Float.compare(fogState.fogAlpha, fogAlpha) == 0 && Float.compare(fogState.density, density) == 0 && Float.compare(fogState.start, start) == 0 && Float.compare(fogState.end, end) == 0 && fogColor.equals(fogState.fogColor);
return fogMode == fogState.fogMode && fogDistanceMode == fogState.fogDistanceMode && Float.compare(fogState.fogAlpha, fogAlpha) == 0 && Float.compare(fogState.density, density) == 0 && Float.compare(fogState.start, start) == 0 && Float.compare(fogState.end, end) == 0 && fogColor.equals(fogState.fogColor);
}
@Override
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm.states;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Vector4f;
import org.joml.Vector4i;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import java.nio.FloatBuffer;
@@ -32,7 +31,6 @@ public class LightModelState implements ISettableState<LightModelState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
GL11.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, newBuffer);
}
}
@@ -41,14 +39,12 @@ public class LightModelState implements ISettableState<LightModelState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
GL11.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, newBuffer);
}
}
public void setColorControl(int val) {
if (GLStateManager.shouldBypassCache() || this.colorControl != val) {
this.colorControl = val;
GL11.glLightModeli(GL12.GL_LIGHT_MODEL_COLOR_CONTROL, val);
}
}
@@ -59,7 +55,6 @@ public class LightModelState implements ISettableState<LightModelState> {
public void setLocalViewer(float val) {
if (GLStateManager.shouldBypassCache() || Float.compare(this.localViewer, val) != 0) {
this.localViewer = val;
GL11.glLightModelf(GL11.GL_LIGHT_MODEL_LOCAL_VIEWER, val);
}
}
@@ -78,7 +73,6 @@ public class LightModelState implements ISettableState<LightModelState> {
public void setTwoSide(float val) {
if (GLStateManager.shouldBypassCache() || Float.compare(this.twoSide, val) != 0) {
this.twoSide = val;
GL11.glLightModelf(GL11.GL_LIGHT_MODEL_TWO_SIDE, val);
}
}
@@ -29,6 +29,7 @@ public class LightState implements ISettableState<LightState> {
public final Vector3f spotDirection;
public float spotExponent;
public float spotCutoff;
public float spotCosCutoff;
public float constantAttenuation;
public float linearAttenuation;
public float quadraticAttenuation;
@@ -60,6 +61,7 @@ public class LightState implements ISettableState<LightState> {
this.specular = specular;
this.spotExponent = spotExponent;
this.spotCutoff = spotCutoff;
this.spotCosCutoff = (float) Math.cos(Math.toRadians(spotCutoff));
this.constantAttenuation = constantAttenuation;
this.linearAttenuation = linearAttenuation;
this.quadraticAttenuation = quadraticAttenuation;
@@ -76,7 +78,6 @@ public class LightState implements ISettableState<LightState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
GL11.glLight(this.light, GL11.GL_AMBIENT, newBuffer);
}
}
@@ -85,7 +86,6 @@ public class LightState implements ISettableState<LightState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
GL11.glLight(this.light, GL11.GL_AMBIENT, newBuffer);
}
}
@@ -93,7 +93,6 @@ public class LightState implements ISettableState<LightState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
GL11.glLight(this.light, GL11.GL_DIFFUSE, newBuffer);
}
}
@@ -102,7 +101,6 @@ public class LightState implements ISettableState<LightState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
GL11.glLight(this.light, GL11.GL_DIFFUSE, newBuffer);
}
}
@@ -110,7 +108,6 @@ public class LightState implements ISettableState<LightState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
GL11.glLight(this.light, GL11.GL_SPECULAR, newBuffer);
}
}
@@ -119,7 +116,6 @@ public class LightState implements ISettableState<LightState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
GL11.glLight(this.light, GL11.GL_SPECULAR, newBuffer);
}
}
@@ -129,14 +125,12 @@ public class LightState implements ISettableState<LightState> {
// with and the untransformed coordinates in addition to the final transformation.
this.position.set(newBuffer);
this.position.mul(GLStateManager.getModelViewMatrix());
GL11.glLight(this.light, GL11.GL_POSITION, newBuffer);
}
public void setPosition(IntBuffer newBuffer) {
vector4i.set(newBuffer);
this.position.set((float) vector4i.x, (float) vector4i.y, (float) vector4i.z, (float) vector4i.w);
this.position.mul(GLStateManager.getModelViewMatrix());
GL11.glLight(this.light, GL11.GL_POSITION, newBuffer);
}
public void setSpotDirection(FloatBuffer newBuffer) {
@@ -146,7 +140,6 @@ public class LightState implements ISettableState<LightState> {
GLStateManager.getModelViewMatrix().get3x3(matrix3f);
this.spotDirection.set(newBuffer);
this.spotDirection.mul(matrix3f);
GL11.glLight(this.light, GL11.GL_SPOT_DIRECTION, newBuffer);
}
public void setSpotDirection(IntBuffer newBuffer) {
@@ -154,7 +147,6 @@ public class LightState implements ISettableState<LightState> {
GLStateManager.getModelViewMatrix().get3x3(matrix3f);
this.spotDirection.set((float) vector3i.x, (float) vector3i.y, (float) vector3i.z);
this.spotDirection.mul(matrix3f);
GL11.glLight(this.light, GL11.GL_SPOT_DIRECTION, newBuffer);
}
public void setSpotExponent(FloatBuffer newBuffer) {
@@ -172,7 +164,6 @@ public class LightState implements ISettableState<LightState> {
public void setSpotExponent(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.spotExponent) != 0) {
this.spotExponent = f;
GL11.glLightf(this.light, GL11.GL_SPOT_EXPONENT, f);
}
}
@@ -191,7 +182,7 @@ public class LightState implements ISettableState<LightState> {
public void setSpotCutoff(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.spotCutoff) != 0) {
this.spotCutoff = f;
GL11.glLightf(this.light, GL11.GL_SPOT_CUTOFF, f);
this.spotCosCutoff = (float) Math.cos(Math.toRadians(f));
}
}
@@ -210,7 +201,6 @@ public class LightState implements ISettableState<LightState> {
public void setConstantAttenuation(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.constantAttenuation) != 0) {
this.constantAttenuation = f;
GL11.glLightf(this.light, GL11.GL_CONSTANT_ATTENUATION, f);
}
}
@@ -229,7 +219,6 @@ public class LightState implements ISettableState<LightState> {
public void setLinearAttenuation(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.linearAttenuation) != 0) {
this.linearAttenuation = f;
GL11.glLightf(this.light, GL11.GL_LINEAR_ATTENUATION, f);
}
}
@@ -248,7 +237,6 @@ public class LightState implements ISettableState<LightState> {
public void setQuadraticAttenuation(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.quadraticAttenuation) != 0) {
this.quadraticAttenuation = f;
GL11.glLightf(this.light, GL11.GL_QUADRATIC_ATTENUATION, f);
}
}
@@ -261,6 +249,7 @@ public class LightState implements ISettableState<LightState> {
this.spotDirection.set(state.spotDirection);
this.spotExponent = state.spotExponent;
this.spotCutoff = state.spotCutoff;
this.spotCosCutoff = state.spotCosCutoff;
this.constantAttenuation = state.constantAttenuation;
this.linearAttenuation = state.linearAttenuation;
this.quadraticAttenuation = state.quadraticAttenuation;
@@ -5,7 +5,6 @@ import org.joml.Vector3f;
import org.joml.Vector3i;
import org.joml.Vector4f;
import org.joml.Vector4i;
import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -42,7 +41,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
GL11.glMaterial(face, GL11.GL_AMBIENT, newBuffer);
}
}
@@ -51,7 +49,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.ambient.equals(vector4f)) {
this.ambient.set(vector4f);
GL11.glMaterial(face, GL11.GL_AMBIENT, newBuffer);
}
}
@@ -59,7 +56,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
GL11.glMaterial(face, GL11.GL_DIFFUSE, newBuffer);
}
}
@@ -68,7 +64,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.diffuse.equals(vector4f)) {
this.diffuse.set(vector4f);
GL11.glMaterial(face, GL11.GL_DIFFUSE, newBuffer);
}
}
@@ -76,7 +71,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
GL11.glMaterial(face, GL11.GL_SPECULAR, newBuffer);
}
}
@@ -85,7 +79,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.specular.equals(vector4f)) {
this.specular.set(vector4f);
GL11.glMaterial(face, GL11.GL_SPECULAR, newBuffer);
}
}
@@ -93,7 +86,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(newBuffer);
if (GLStateManager.shouldBypassCache() || !this.emission.equals(vector4f)) {
this.emission.set(vector4f);
GL11.glMaterial(face, GL11.GL_EMISSION, newBuffer);
}
}
@@ -102,14 +94,12 @@ public class MaterialState implements ISettableState<MaterialState> {
vector4f.set(i2f(vector4i.x), i2f(vector4i.y), i2f(vector4i.z), i2f(vector4i.w));
if (GLStateManager.shouldBypassCache() || !this.emission.equals(vector4f)) {
this.emission.set(vector4f);
GL11.glMaterial(face, GL11.GL_EMISSION, newBuffer);
}
}
public void setShininess(float val) {
if (GLStateManager.shouldBypassCache() || Float.compare(this.shininess, val) != 0) {
this.shininess = val;
GL11.glMaterialf(face, GL11.GL_SHININESS, val);
}
}
@@ -132,7 +122,6 @@ public class MaterialState implements ISettableState<MaterialState> {
if (GLStateManager.shouldBypassCache() || !this.colorIndexes.equals(vector3f)) {
this.colorIndexes.set(vector3f);
GL11.glMaterial(face, GL11.GL_COLOR_INDEXES, newBuffer);
}
}
@@ -141,7 +130,6 @@ public class MaterialState implements ISettableState<MaterialState> {
vector3f.set((float) vector3i.x, (float) vector3i.y, (float) vector3i.z);
if (GLStateManager.shouldBypassCache() || !this.colorIndexes.equals(vector3f)) {
this.colorIndexes.set(vector3f);
GL11.glMaterial(face, GL11.GL_COLOR_INDEXES, newBuffer);
}
}
@@ -10,8 +10,7 @@ public class MatrixMode implements ISettableState<MatrixMode> {
public void setMode(int mode) {
if(mode != GL11.GL_MODELVIEW && mode != GL11.GL_PROJECTION && mode != GL11.GL_TEXTURE && mode != GL11.GL_COLOR) {
// Invalid mode, do nothing on the cache, but pass it along to OGL
GL11.glMatrixMode(mode);
// Invalid mode, do nothing on the cache
return;
}
@@ -20,7 +19,6 @@ public class MatrixMode implements ISettableState<MatrixMode> {
if (!bypass) {
this.mode = mode;
}
GL11.glMatrixMode(mode);
}
}
@@ -0,0 +1,110 @@
package com.gtnewhorizons.angelica.glsm.states;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
/**
* Per-texture-unit GL_TEXTURE_ENV state for FFP emulation. Tracks the simple texenv mode and all GL_COMBINE sub-parameters.
*/
public class TexEnvState implements IStateStack<TexEnvState> {
public int mode;
public int combineRgb;
public int combineAlpha;
public final int[] sourceRgb = new int[3];
public final int[] sourceAlpha = new int[3];
public final int[] operandRgb = new int[3];
public final int[] operandAlpha = new int[3];
public float scaleRgb;
public float scaleAlpha;
public float envColorR;
public float envColorG;
public float envColorB;
public float envColorA;
private final TexEnvState[] stack;
private int pointer;
private final boolean isStackEntry;
public TexEnvState() {
this(false);
}
private TexEnvState(boolean isStackEntry) {
this.isStackEntry = isStackEntry;
if (isStackEntry) {
stack = null;
} else {
stack = new TexEnvState[GLStateManager.MAX_ATTRIB_STACK_DEPTH];
for (int i = 0; i < GLStateManager.MAX_ATTRIB_STACK_DEPTH; i++) {
stack[i] = new TexEnvState(true);
}
}
reset();
}
public void copyFrom(TexEnvState other) {
this.mode = other.mode;
this.combineRgb = other.combineRgb;
this.combineAlpha = other.combineAlpha;
System.arraycopy(other.sourceRgb, 0, this.sourceRgb, 0, 3);
System.arraycopy(other.sourceAlpha, 0, this.sourceAlpha, 0, 3);
System.arraycopy(other.operandRgb, 0, this.operandRgb, 0, 3);
System.arraycopy(other.operandAlpha, 0, this.operandAlpha, 0, 3);
this.scaleRgb = other.scaleRgb;
this.scaleAlpha = other.scaleAlpha;
this.envColorR = other.envColorR;
this.envColorG = other.envColorG;
this.envColorB = other.envColorB;
this.envColorA = other.envColorA;
}
public void reset() {
mode = GL11.GL_MODULATE;
combineRgb = GL11.GL_MODULATE;
combineAlpha = GL11.GL_MODULATE;
sourceRgb[0] = GL11.GL_TEXTURE; sourceRgb[1] = GL13.GL_PREVIOUS; sourceRgb[2] = GL13.GL_CONSTANT;
sourceAlpha[0] = GL11.GL_TEXTURE; sourceAlpha[1] = GL13.GL_PREVIOUS; sourceAlpha[2] = GL13.GL_CONSTANT;
operandRgb[0] = GL11.GL_SRC_COLOR; operandRgb[1] = GL11.GL_SRC_COLOR; operandRgb[2] = GL11.GL_SRC_ALPHA;
operandAlpha[0] = GL11.GL_SRC_ALPHA; operandAlpha[1] = GL11.GL_SRC_ALPHA; operandAlpha[2] = GL11.GL_SRC_ALPHA;
scaleRgb = 1.0f;
scaleAlpha = 1.0f;
envColorR = 0.0f; envColorG = 0.0f; envColorB = 0.0f; envColorA = 0.0f;
}
@Override
public TexEnvState push() {
if (stack == null) throw new IllegalStateException("Cannot push stack entry");
if (pointer >= stack.length) {
throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached");
}
stack[pointer++].copyFrom(this);
return this;
}
@Override
public TexEnvState pop() {
if (stack == null) throw new IllegalStateException("Cannot pop stack entry");
if (pointer == 0) {
throw new IllegalStateException("Stack underflow");
}
copyFrom(stack[--pointer]);
return this;
}
@Override
public boolean isEmpty() {
return pointer == 0;
}
public boolean isCombineMode() {
return mode == GL13.GL_COMBINE;
}
}
@@ -0,0 +1,83 @@
package com.gtnewhorizons.angelica.glsm.states;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import org.lwjgl.opengl.GL11;
/**
* Per-texture-unit storage for texgen mode and plane coefficients. Based on Mesa's texgen.c storage model.
*/
public class TexGenState {
private int modeS, modeT, modeR, modeQ;
private final float[] objectPlaneS = {1, 0, 0, 0};
private final float[] objectPlaneT = {0, 1, 0, 0};
private final float[] objectPlaneR = {0, 0, 0, 0};
private final float[] objectPlaneQ = {0, 0, 0, 0};
private final float[] eyePlaneS = {1, 0, 0, 0};
private final float[] eyePlaneT = {0, 1, 0, 0};
private final float[] eyePlaneR = {0, 0, 0, 0};
private final float[] eyePlaneQ = {0, 0, 0, 0};
private static final Vector4f tempPlane = new Vector4f();
private static final Matrix4f tempInverse = new Matrix4f();
public int getMode(int coord) {
return switch (coord) {
case GL11.GL_S -> modeS;
case GL11.GL_T -> modeT;
case GL11.GL_R -> modeR;
case GL11.GL_Q -> modeQ;
default -> 0;
};
}
public void setMode(int coord, int mode) {
switch (coord) {
case GL11.GL_S -> modeS = mode;
case GL11.GL_T -> modeT = mode;
case GL11.GL_R -> modeR = mode;
case GL11.GL_Q -> modeQ = mode;
}
}
public float[] getObjectPlane(int coord) {
return switch (coord) {
case GL11.GL_S -> objectPlaneS;
case GL11.GL_T -> objectPlaneT;
case GL11.GL_R -> objectPlaneR;
case GL11.GL_Q -> objectPlaneQ;
default -> objectPlaneS;
};
}
public float[] getEyePlane(int coord) {
return switch (coord) {
case GL11.GL_S -> eyePlaneS;
case GL11.GL_T -> eyePlaneT;
case GL11.GL_R -> eyePlaneR;
case GL11.GL_Q -> eyePlaneQ;
default -> eyePlaneS;
};
}
public void setObjectPlane(int coord, float[] plane) {
final float[] dest = getObjectPlane(coord);
System.arraycopy(plane, 0, dest, 0, 4);
}
/**
* Set eye plane coefficients. Per GL spec (Mesa texgen.c:154), the plane is transformed by the inverse of the current modelview matrix at the time of the call.
*/
public void setEyePlane(int coord, float[] plane, Matrix4f modelView) {
final float[] dest = getEyePlane(coord);
modelView.invert(tempInverse);
tempPlane.set(plane[0], plane[1], plane[2], plane[3]);
tempInverse.transformTranspose(tempPlane);
dest[0] = tempPlane.x;
dest[1] = tempPlane.y;
dest[2] = tempPlane.z;
dest[3] = tempPlane.w;
}
}
@@ -17,6 +17,8 @@ public class TextureUnitArray {
private final TextureUnitBooleanStateStack[] texGenTStates;
private final TextureUnitBooleanStateStack[] texGenRStates;
private final TextureUnitBooleanStateStack[] texGenQStates;
private final TexGenState[] texGenStates;
private final TexEnvState[] texEnvStates;
@Getter
public final Matrix4fStack[] textureMatricies;
@@ -29,6 +31,8 @@ public class TextureUnitArray {
texGenTStates = new TextureUnitBooleanStateStack[GLStateManager.MAX_TEXTURE_UNITS];
texGenRStates = new TextureUnitBooleanStateStack[GLStateManager.MAX_TEXTURE_UNITS];
texGenQStates = new TextureUnitBooleanStateStack[GLStateManager.MAX_TEXTURE_UNITS];
texGenStates = new TexGenState[GLStateManager.MAX_TEXTURE_UNITS];
texEnvStates = new TexEnvState[GLStateManager.MAX_TEXTURE_UNITS];
textureMatricies = new Matrix4fStack[GLStateManager.MAX_TEXTURE_UNITS];
for (int i = 0; i < GLStateManager.MAX_TEXTURE_UNITS; i++) {
@@ -40,6 +44,8 @@ public class TextureUnitArray {
texGenTStates[i] = new TextureUnitBooleanStateStack(GL11.GL_TEXTURE_GEN_T, i);
texGenRStates[i] = new TextureUnitBooleanStateStack(GL11.GL_TEXTURE_GEN_R, i);
texGenQStates[i] = new TextureUnitBooleanStateStack(GL11.GL_TEXTURE_GEN_Q, i);
texGenStates[i] = new TexGenState();
texEnvStates[i] = new TexEnvState();
textureMatricies[i] = new Matrix4fStack(GLStateManager.MAX_TEXTURE_STACK_DEPTH);
}
}
@@ -76,6 +82,14 @@ public class TextureUnitArray {
return texGenQStates[index];
}
public TexGenState getTexGenState(int index) {
return texGenStates[index];
}
public TexEnvState getTexEnvState(int index) {
return texEnvStates[index];
}
public Matrix4fStack getTextureUnitMatrix(int index) {
return textureMatricies[index];
}
@@ -7,6 +7,8 @@ public class ViewportState implements ISettableState<ViewportState> {
public int y;
public int width;
public int height;
public double depthRangeNear = 0.0;
public double depthRangeFar = 1.0;
public void setViewPort(int x, int y, int width, int height) {
this.x = x;
@@ -15,6 +17,11 @@ public class ViewportState implements ISettableState<ViewportState> {
this.height = height;
}
public void setDepthRange(double near, double far) {
this.depthRangeNear = near;
this.depthRangeFar = far;
}
public void get(IntBuffer params) {
params.put(0, x);
params.put(1, y);
@@ -28,6 +35,8 @@ public class ViewportState implements ISettableState<ViewportState> {
this.y = state.y;
this.width = state.width;
this.height = state.height;
this.depthRangeNear = state.depthRangeNear;
this.depthRangeFar = state.depthRangeFar;
return this;
}
@@ -36,7 +45,9 @@ public class ViewportState implements ISettableState<ViewportState> {
public boolean sameAs(Object state) {
if (this == state) return true;
if (!(state instanceof ViewportState viewportState)) return false;
return x == viewportState.x && y == viewportState.y && width == viewportState.width && height == viewportState.height;
return x == viewportState.x && y == viewportState.y && width == viewportState.width && height == viewportState.height
&& Double.compare(depthRangeNear, viewportState.depthRangeNear) == 0
&& Double.compare(depthRangeFar, viewportState.depthRangeFar) == 0;
}
@Override
@@ -0,0 +1,142 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
import static com.mitchej123.lwjgl.LWJGLServiceProvider.LWJGL;
/**
* Streaming buffer using the classic orphan pattern with sub-allocation.
* <p>
* Supports two upload strategies:
* <ul>
* <li>{@link UploadStrategy#BUFFER_SUB_DATA} — glBufferData orphan + glBufferSubData (default)</li>
* <li>{@link UploadStrategy#MAP_BUFFER_RANGE} — glMapBufferRange with INVALIDATE hints </li>
* </ul>
*/
public class OrphanStreamingBuffer implements StreamingBuffer {
public enum UploadStrategy {
BUFFER_SUB_DATA,
MAP_BUFFER_RANGE
}
private static final int MAP_WRITE_INVALIDATE_BUFFER = GL30.GL_MAP_WRITE_BIT | GL30.GL_MAP_INVALIDATE_BUFFER_BIT | GL30.GL_MAP_UNSYNCHRONIZED_BIT;
private static final int MAP_WRITE_INVALIDATE_RANGE = GL30.GL_MAP_WRITE_BIT | GL30.GL_MAP_INVALIDATE_RANGE_BIT | GL30.GL_MAP_UNSYNCHRONIZED_BIT;
private int bufferId;
private int capacity;
private int writePos;
private boolean needsOrphan = true;
private final UploadStrategy strategy;
public OrphanStreamingBuffer() {
this(UploadStrategy.BUFFER_SUB_DATA);
}
public OrphanStreamingBuffer(UploadStrategy strategy) {
this.strategy = strategy;
this.bufferId = GL15.glGenBuffers();
}
@Override
public int upload(ByteBuffer data, int vertexStride) {
final int dataSize = data.remaining();
final int alignUnit = vertexStride * 4;
int alignedPos = ((writePos + alignUnit - 1) / alignUnit) * alignUnit;
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
boolean freshAllocation = false;
if (alignedPos + dataSize > capacity) {
// Doesn't fit — grow if needed, allocate new storage, reset to offset 0
growIfNeeded(dataSize);
allocateBuffer();
alignedPos = 0;
freshAllocation = true;
} else if (needsOrphan) {
// First upload of the frame — need fresh storage
if (strategy == UploadStrategy.BUFFER_SUB_DATA) {
// SubData strategy: orphan via glBufferData to get new backing store
allocateBuffer();
}
}
if (strategy == UploadStrategy.BUFFER_SUB_DATA) {
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, alignedPos, data);
} else {
// MapRange: choose flags based on whether this is the first upload of the frame
final int flags;
if (freshAllocation) {
// Fresh allocation — nothing to invalidate, just write
flags = MAP_WRITE_INVALIDATE_RANGE;
} else if (needsOrphan) {
// First upload of frame on existing buffer — invalidate entire buffer
flags = MAP_WRITE_INVALIDATE_BUFFER;
} else {
// Subsequent upload — invalidate only our range
flags = MAP_WRITE_INVALIDATE_RANGE;
}
final ByteBuffer mapped = LWJGL.glMapBufferRange(GL15.GL_ARRAY_BUFFER, alignedPos, dataSize, flags);
memCopy(memAddress0(data), memAddress0(mapped), dataSize);
GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER);
}
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
needsOrphan = false;
writePos = alignedPos + dataSize;
return alignedPos / vertexStride;
}
private static final int MIN_CAPACITY = 64 * 1024;
private void growIfNeeded(int needed) {
if (needed > capacity) {
int newCapacity = Math.max(capacity, MIN_CAPACITY);
while (newCapacity < needed) {
newCapacity *= 2;
}
capacity = newCapacity;
}
}
private void allocateBuffer() {
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, capacity, GL15.GL_STREAM_DRAW);
}
@Override
public int getCapacity() {
return capacity;
}
@Override
public int getBufferId() {
return bufferId;
}
@Override
public void postDraw() {
writePos = 0;
needsOrphan = true;
}
@Override
public void destroy() {
if (bufferId != 0) {
GL15.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
int getWritePos() {
return writePos;
}
UploadStrategy getStrategy() {
return strategy;
}
}
@@ -0,0 +1,180 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import org.embeddedt.embeddium.impl.gl.sync.GlFence;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL44;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
import static com.mitchej123.lwjgl.LWJGLServiceProvider.LWJGL;
/**
* Persistent-mapped ring buffer for streaming vertex data on GL4.4+
*/
public class PersistentStreamingBuffer implements StreamingBuffer {
public static final int DEFAULT_CAPACITY = 16 * 1024 * 1024; // 16 MB
private int bufferId;
private final int capacity;
private final ByteBuffer mappedBuffer;
private final long mappedAddress;
private final ObjectArrayFIFOQueue<FencedRegion> fenceQueue = new ObjectArrayFIFOQueue<>();
private int writePos;
private int remaining;
private int pendingBytes;
public PersistentStreamingBuffer() {
this(DEFAULT_CAPACITY);
}
public PersistentStreamingBuffer(int capacity) {
this.capacity = capacity;
this.remaining = capacity;
this.bufferId = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
final int storageFlags = GL44.GL_MAP_PERSISTENT_BIT | GL30.GL_MAP_WRITE_BIT | GL44.GL_MAP_COHERENT_BIT | GL44.GL_CLIENT_STORAGE_BIT;
RenderSystem.bufferStorage(GL15.GL_ARRAY_BUFFER, capacity, storageFlags);
final int mapFlags = GL44.GL_MAP_PERSISTENT_BIT | GL30.GL_MAP_WRITE_BIT | GL44.GL_MAP_COHERENT_BIT;
this.mappedBuffer = LWJGL.glMapBufferRange(GL15.GL_ARRAY_BUFFER, 0, capacity, mapFlags);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (this.mappedBuffer == null) {
GL15.glDeleteBuffers(bufferId);
bufferId = 0;
throw new RuntimeException("Failed to persistently map streaming buffer");
}
this.mappedAddress = memAddress0(mappedBuffer);
}
@Override
public int upload(ByteBuffer data, int vertexStride) {
final int dataSize = data.remaining();
if (dataSize > capacity) {
return -1;
}
reclaim();
final int alignUnit = vertexStride * 4;
final int alignedPos = ((writePos + alignUnit - 1) / alignUnit) * alignUnit;
final int alignmentWaste = alignedPos - writePos;
final int tailSpace = capacity - alignedPos;
if (dataSize <= tailSpace) {
// Fits at tail — just need enough remaining budget
if (ensureRemaining(alignmentWaste + dataSize)) {
return writeAt(alignedPos, data, dataSize, alignmentWaste, vertexStride);
}
} else {
// Must wrap to start — burn remaining tail space
final int wrapWaste = capacity - writePos;
if (ensureRemaining(wrapWaste + dataSize)) {
return writeAtStart(data, dataSize, wrapWaste);
}
}
return -1;
}
/** Sync fences until {@code needed} bytes are available, or no fences remain. */
private boolean ensureRemaining(int needed) {
while (remaining < needed) {
if (!syncOldest()) return false;
}
return true;
}
private int writeAt(int offset, ByteBuffer data, int dataSize, int waste, int vertexStride) {
memCopy(memAddress0(data) + data.position(), mappedAddress + offset, dataSize);
final int totalCost = waste + dataSize;
writePos = offset + dataSize;
remaining -= totalCost;
pendingBytes += totalCost;
return offset / vertexStride;
}
private int writeAtStart(ByteBuffer data, int dataSize, int wrapWaste) {
memCopy(memAddress0(data) + data.position(), mappedAddress, dataSize);
final int totalCost = wrapWaste + dataSize;
writePos = dataSize;
remaining -= totalCost;
pendingBytes += totalCost;
return 0;
}
@Override
public int getCapacity() {
return capacity;
}
public int getRemaining() { return remaining; }
@Override
public int getBufferId() {
return bufferId;
}
@Override
public void postDraw() {
if (pendingBytes > 0) {
final long fenceId = LWJGL.glFenceSync(GL32.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
fenceQueue.enqueue(new FencedRegion(new GlFence(fenceId), pendingBytes));
pendingBytes = 0;
}
}
@Override
public void destroy() {
while (!fenceQueue.isEmpty()) {
fenceQueue.dequeue().fence.delete();
}
if (bufferId != 0) {
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GL15.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
private void reclaim() {
while (!fenceQueue.isEmpty()) {
final FencedRegion region = fenceQueue.first();
if (!region.fence.isCompleted()) break;
region.fence.delete();
fenceQueue.dequeue();
remaining += region.bytes;
}
}
/** Sync on oldest fence. Returns true if bytes were reclaimed. */
private boolean syncOldest() {
if (fenceQueue.isEmpty()) return false;
final FencedRegion region = fenceQueue.first();
region.fence.sync();
region.fence.delete();
fenceQueue.dequeue();
remaining += region.bytes;
reclaim(); // opportunistically reclaim any additional completed fences
return true;
}
// Package-private accessors for testing
ByteBuffer getMappedBuffer() { return mappedBuffer; }
private record FencedRegion(GlFence fence, int bytes) {}
}
@@ -0,0 +1,12 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import java.nio.ByteBuffer;
public interface StreamingBuffer {
int upload(ByteBuffer data, int vertexStride);
int getBufferId();
void postDraw();
int getCapacity();
void destroy();
}
@@ -0,0 +1,23 @@
package com.gtnewhorizons.angelica.glsm.uniform;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniform;
import org.lwjgl.opengl.GL20;
public class GlUniformFloat2v extends GlUniform<float[]> {
public GlUniformFloat2v(int index) {
super(index);
}
@Override
public void set(float[] value) {
if (value.length != 2) {
throw new IllegalArgumentException("value.length != 2");
}
GL20.glUniform2f(this.index, value[0], value[1]);
}
public void set(float x, float y) {
GL20.glUniform2f(this.index, x, y);
}
}
@@ -142,6 +142,9 @@ public class AngelicaTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader {
boolean rfbLoaded = Launch.blackboard.getOrDefault("angelica.rfbPluginLoaded", Boolean.FALSE) == Boolean.TRUE;
if (!rfbLoaded) {
tweaks.add("com.gtnewhorizons.angelica.loading.fml.tweakers.AngelicaLateTweaker");
if (AngelicaConfig.enableCeleritas) {
tweaks.add("com.gtnewhorizons.angelica.loading.fml.tweakers.CeleritasLateTweaker");
}
}
}
}
@@ -2,26 +2,25 @@ package com.gtnewhorizons.angelica.loading.fml.transformers;
import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.AngelicaRedirector;
import com.gtnewhorizons.angelica.loading.shared.transformers.SodiumBlockTransform;
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
public class SodiumBlockTransformer implements IClassTransformer {
public class CeleritasBlockTransformer implements IClassTransformer {
private final SodiumBlockTransform inner;
private final CeleritasBlockTransform inner;
private final String[] exclusions;
public SodiumBlockTransformer() {
this.inner = new SodiumBlockTransform(AngelicaTweaker.isObfEnv());
public CeleritasBlockTransformer() {
this.inner = new CeleritasBlockTransform(AngelicaTweaker.isObfEnv());
this.exclusions = inner.getTransformerExclusions();
this.inner.setCeleritasSetting();
}
/**
* Delete the global vanilla bounding box fields off the Block object. {@link AngelicaRedirector}
* Delete the global vanilla bounding box fields off the Block object. {@link CeleritasBlockTransform}
* replaces these with a thread-safe alternative.
*/
@Override
@@ -31,9 +30,11 @@ public class SodiumBlockTransformer implements IClassTransformer {
if (transformedName.startsWith(exclusion)) return basicClass;
}
if (!inner.shouldTransform(basicClass)) {
return basicClass;
}
// Here could be a `inner.shouldTransform(basicClass)` check to exit early from transforming,
// but we also need to call `inner.trackBlockSubclasses` in this case and somehow pass
// the name of a class and a superclass, which we can do only after parsing the whole constant pool.
// This transformer is merely a fallback when FRB / lwjgl3ify is not present, so we can afford
// it being not fully optimized
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
@@ -0,0 +1,35 @@
package com.gtnewhorizons.angelica.loading.fml.tweakers;
import cpw.mods.fml.relauncher.FMLRelaunchLog;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;
import java.io.File;
import java.util.List;
public class CeleritasLateTweaker implements ITweaker {
@Override
public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) {
// no-op
}
@Override
public void injectIntoClassLoader(LaunchClassLoader classLoader) {
// no-op
}
@Override
public String getLaunchTarget() {
return null;
}
@Override
public String[] getLaunchArguments() {
String transformer = "com.gtnewhorizons.angelica.loading.fml.transformers.CeleritasBlockTransformer";
FMLRelaunchLog.finer("Registering transformer %s", transformer);
Launch.classLoader.registerTransformer(transformer);
return new String[0];
}
}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.loading.rfb;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBAngelicaRedirector;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBSodiumBlockTransformer;
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBCeleritasBlockTransformer;
import com.gtnewhorizons.retrofuturabootstrap.api.PluginContext;
import com.gtnewhorizons.retrofuturabootstrap.api.RetroFuturaBootstrap;
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
@@ -27,7 +27,7 @@ public class AngelicaRfbPlugin implements RfbPlugin {
final boolean isObf = RetroFuturaBootstrap.API.launchClassLoader().findClassMetadata("net.minecraft.world.World") == null;
return new RfbClassTransformer[] {
new RFBAngelicaRedirector(),
new RFBSodiumBlockTransformer(isObf)
new RFBCeleritasBlockTransformer(isObf)
};
}
}
@@ -48,10 +48,6 @@ public class RFBAngelicaRedirector implements RfbClassTransformer {
if (!classNode.isPresent()) {
return false;
}
if (!classNode.isOriginal()) {
// If a class is already a transformed ClassNode, conservatively continue processing.
return true;
}
return inner.shouldTransform(classNode.getOriginalBytes());
}
@@ -1,7 +1,8 @@
package com.gtnewhorizons.angelica.loading.rfb.transformers;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
import com.gtnewhorizons.angelica.loading.shared.transformers.SodiumBlockTransform;
import com.gtnewhorizons.angelica.loading.shared.transformers.CeleritasBlockTransform;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassHeaderMetadata;
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
@@ -11,12 +12,12 @@ import org.jetbrains.annotations.Nullable;
import java.util.jar.Manifest;
public class RFBSodiumBlockTransformer implements RfbClassTransformer {
public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
private final SodiumBlockTransform inner;
private final CeleritasBlockTransform inner;
public RFBSodiumBlockTransformer(boolean isObf) {
inner = new SodiumBlockTransform(isObf);
public RFBCeleritasBlockTransformer(boolean isObf) {
inner = new CeleritasBlockTransform(isObf);
}
@Pattern("[a-z0-9-]+")
@@ -47,11 +48,20 @@ public class RFBSodiumBlockTransformer implements RfbClassTransformer {
if (!classNode.isPresent()) {
return false;
}
if (!classNode.isOriginal()) {
// If a class is already a transformed ClassNode, conservatively continue processing.
ClassHeaderMetadata metadata = classNode.getOriginalMetadata();
if (metadata == null) {
return false;
}
if (inner.shouldTransform(classNode.getOriginalBytes())) {
return true;
}
return inner.shouldTransform(classNode.getOriginalBytes());
// Track possible block subclasses even if we don't need to transform this class
inner.trackBlockSubclasses(metadata.binaryThisName, metadata.binarySuperName);
return false;
}
@Override
@@ -1,31 +1,23 @@
package com.gtnewhorizons.angelica.loading.shared.transformers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
import net.minecraft.launchwrapper.Launch;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* This transformer redirects many GL calls to our custom GLStateManager
@@ -51,9 +43,11 @@ public final class AngelicaRedirector {
private static final String ARBVertexArrayObject = "org/lwjgl/opengl/ARBVertexArrayObject";
private static final String APPLEVertexArrayObject = "org/lwjgl/opengl/APPLEVertexArrayObject";
private static final String Project = "org/lwjgl/util/glu/Project";
private static final String GLU = "org/lwjgl/util/glu/GLU";
private static final String OpenGlHelper = "net/minecraft/client/renderer/OpenGlHelper";
private static final String EXTBlendFunc = "org/lwjgl/opengl/EXTBlendFuncSeparate";
private static final String ARBMultiTexture = "org/lwjgl/opengl/ARBMultitexture";
private static final String ARBShaderObjects = "org/lwjgl/opengl/ARBShaderObjects";
// Redirect VAO related calls from NHLib and LWJGLService
private static final String UniversalVAO = "com/gtnewhorizon/gtnhlib/client/opengl/UniversalVAO";
@@ -73,10 +67,10 @@ public final class AngelicaRedirector {
"initializeTextures", "func_77474_a"
);
private static final ClassConstantPoolParser cstPoolParser = new ClassConstantPoolParser(GL11, GL12, GL13, GL14, GL20, OpenGlHelper, EXTBlendFunc, ARBMultiTexture, Project);
private static final Map<String, Map<String, String>> methodRedirects = new HashMap<>();
private static final Map<String, Map<String, String>> methodRedirects = new HashMap<>(32);
private static final Map<String, Map<String, String>> interfaceRedirects = new HashMap<>();
private static final Map<Integer, String> glCapRedirects = new HashMap<>();
private static final ClassConstantPoolParser cstPoolParser;
static {
glCapRedirects.put(org.lwjgl.opengl.GL11.GL_ALPHA_TEST, "AlphaTest");
@@ -111,6 +105,9 @@ public final class AngelicaRedirector {
.add("glColorMask")
.add("glColorMaterial")
.add("glDeleteLists")
.add("glGenLists")
.add("glGenTextures")
.add("glIsList")
.add("glDeleteTextures")
.add("glDepthFunc")
.add("glDepthMask")
@@ -183,6 +180,13 @@ public final class AngelicaRedirector {
.add("glTexCoord3f")
.add("glTexCoord4d")
.add("glTexCoord4f")
.add("glTexEnvi")
.add("glTexEnvf")
.add("glTexEnv")
.add("glTexGeni")
.add("glTexGenf")
.add("glTexGend")
.add("glTexGen")
.add("glTexImage1D")
.add("glTexImage2D")
.add("glTexImage3D")
@@ -194,6 +198,8 @@ public final class AngelicaRedirector {
.add("glCopyTexSubImage2D")
.add("glPixelStoref")
.add("glPixelStorei")
.add("glPixelTransferf")
.add("glPixelTransferi")
.add("glTexParameter")
.add("glTexParameterf")
.add("glTexParameteri")
@@ -206,6 +212,8 @@ public final class AngelicaRedirector {
.add("glPointSize")
.add("glPolygonMode")
.add("glPolygonOffset")
.add("glPolygonStipple")
.add("glAccum")
.add("glReadBuffer")
.add("glSampleCoverage")
.add("glScissor")
@@ -223,9 +231,22 @@ public final class AngelicaRedirector {
.add("glVertex3f")
.add("glVertexPointer")
.add("glColorPointer")
.add("glNormalPointer")
.add("glTexCoordPointer")
.add("glEnableClientState")
.add("glDisableClientState")
.add("glInterleavedArrays")
.add("glPushClientAttrib")
.add("glPopClientAttrib")
.add("glViewport")
.add("glFeedbackBuffer")
.add("glRenderMode")
.add("glPassThrough")
.add("glSelectBuffer")
.add("glInitNames")
.add("glPushName")
.add("glPopName")
.add("glLoadName")
);
methodRedirects.put(GL12, RedirectMap.newMap()
.add("glTexImage3D")
@@ -235,6 +256,10 @@ public final class AngelicaRedirector {
methodRedirects.put(GL13, RedirectMap.newMap()
.add("glActiveTexture")
.add("glSampleCoverage")
.add("glClientActiveTexture")
.add("glMultiTexCoord2f")
.add("glMultiTexCoord2d")
.add("glMultiTexCoord2s")
);
methodRedirects.put(GL14, RedirectMap.newMap()
.add("glBlendFuncSeparate", "tryBlendFuncSeparate")
@@ -251,9 +276,40 @@ public final class AngelicaRedirector {
.add("glStencilMaskSeparate")
.add("glStencilOpSeparate")
.add("glUseProgram")
.add("glShaderSource")
.add("glLinkProgram")
.add("glDeleteProgram")
.add("glCreateShader")
.add("glCompileShader")
.add("glCreateProgram")
.add("glAttachShader")
.add("glDetachShader")
.add("glValidateProgram")
.add("glGetUniformLocation")
.add("glGetAttribLocation")
.add("glUniform1f")
.add("glUniform2f")
.add("glUniform3f")
.add("glUniform4f")
.add("glUniform1i")
.add("glUniform2i")
.add("glUniform3i")
.add("glUniform4i")
.add("glUniform1")
.add("glUniform2")
.add("glUniform3")
.add("glUniform4")
.add("glUniformMatrix2")
.add("glUniformMatrix3")
.add("glUniformMatrix4")
.add("glGetActiveUniform")
.add("glGetAttachedShaders")
.add("glGetShaderSource")
.add("glGetUniform")
);
methodRedirects.put(GL30, RedirectMap.newMap()
.add("glBindVertexArray")
.add("glDeleteVertexArrays")
.add("glBindFramebuffer")
);
@@ -276,8 +332,46 @@ public final class AngelicaRedirector {
methodRedirects.put(ARBMultiTexture, RedirectMap.newMap()
.add("glActiveTextureARB")
);
methodRedirects.put(ARBShaderObjects, RedirectMap.newMap()
.add("glUseProgramObjectARB", "glUseProgram")
.add("glShaderSourceARB", "glShaderSource")
.add("glLinkProgramARB", "glLinkProgram")
.add("glCreateShaderObjectARB", "glCreateShader")
.add("glCompileShaderARB", "glCompileShader")
.add("glCreateProgramObjectARB", "glCreateProgram")
.add("glAttachObjectARB", "glAttachShader")
.add("glDetachObjectARB", "glDetachShader")
.add("glValidateProgramARB", "glValidateProgram")
.add("glGetUniformLocationARB", "glGetUniformLocation")
.add("glGetAttribLocationARB", "glGetAttribLocation")
.add("glDeleteObjectARB")
.add("glGetObjectParameterARB")
.add("glGetObjectParameteriARB")
.add("glGetInfoLogARB")
.add("glGetHandleARB")
.add("glUniform1fARB", "glUniform1f")
.add("glUniform2fARB", "glUniform2f")
.add("glUniform3fARB", "glUniform3f")
.add("glUniform4fARB", "glUniform4f")
.add("glUniform1iARB", "glUniform1i")
.add("glUniform2iARB", "glUniform2i")
.add("glUniform3iARB", "glUniform3i")
.add("glUniform4iARB", "glUniform4i")
.add("glUniform1ARB", "glUniform1")
.add("glUniform2ARB", "glUniform2")
.add("glUniform3ARB", "glUniform3")
.add("glUniform4ARB", "glUniform4")
.add("glUniformMatrix2ARB", "glUniformMatrix2")
.add("glUniformMatrix3ARB", "glUniformMatrix3")
.add("glUniformMatrix4ARB", "glUniformMatrix4")
.add("glGetActiveUniformARB", "glGetActiveUniform")
.add("glGetAttachedObjectsARB", "glGetAttachedShaders")
.add("glGetShaderSourceARB", "glGetShaderSource")
.add("glGetUniformARB", "glGetUniform")
);
methodRedirects.put(ARBVertexArrayObject, RedirectMap.newMap()
.add("glBindVertexArray")
.add("glDeleteVertexArrays")
);
@@ -289,30 +383,43 @@ public final class AngelicaRedirector {
// GTNHLib VAO
methodRedirects.put(UniversalVAO, RedirectMap.newMap()
.add("bindVertexArray", "glBindVertexArray")
.add("deleteVertexArrays", "glDeleteVertexArrays")
);
// OTHER
methodRedirects.put(Project, RedirectMap.newMap().add("gluPerspective"));
methodRedirects.put(Project, RedirectMap.newMap()
.add("gluPerspective")
.add("gluLookAt")
.add("gluPickMatrix")
);
methodRedirects.put(GLU, RedirectMap.newMap()
.add("gluPerspective")
.add("gluLookAt")
.add("gluOrtho2D")
.add("gluPickMatrix")
);
// Interface/virtual redirects — callers invoke these on a receiver object
interfaceRedirects.put(VaoFunctions, RedirectMap.newMap()
.add("glBindVertexArray")
.add("glDeleteVertexArrays")
);
interfaceRedirects.put(LWJGLService, RedirectMap.newMap()
.add("glBindVertexArray")
);
cstPoolParser.addString(UniversalVAO);
cstPoolParser.addString(VaoFunctions);
cstPoolParser.addString(LWJGLService);
final List<String> stringsToSearch = new ArrayList<>(32);
stringsToSearch.add(VaoFunctions);
stringsToSearch.add(LWJGLService);
final String glPrefix = "org/lwjgl/opengl/GL";
for (var entry : new HashMap<>(methodRedirects).entrySet()) {
stringsToSearch.add(entry.getKey());
if (entry.getKey().startsWith(glPrefix)) {
methodRedirects.put(entry.getKey() + "C", entry.getValue());
cstPoolParser.addString(entry.getKey() + "C");
stringsToSearch.add(entry.getKey() + "C");
}
}
cstPoolParser = new ClassConstantPoolParser(stringsToSearch.toArray(new String[0]));
}
public String[] getTransformerExclusions() {
@@ -320,30 +427,27 @@ public final class AngelicaRedirector {
"org.lwjgl",
"com.gtnewhorizons.angelica.glsm.",
"com.gtnewhorizons.angelica.transform",
"com.gtnewhorizon.gtnhlib.asm.",
"me.eigenraven.lwjgl3ify"
};
}
public boolean shouldTransform(byte[] basicClass) {
return cstPoolParser.find(basicClass, true);
return cstPoolParser.find(basicClass);
}
/** @return Was the class changed? */
public boolean transformClassNode(String transformedName, ClassNode cn) {
if (cn == null) {
return false;
}
boolean changed = false;
final boolean isOpenGlHelper = transformedName.equals("net.minecraft.client.renderer.OpenGlHelper");
final boolean isUniversalVAO = transformedName.startsWith(UniversalVAODot);
for (MethodNode mn : cn.methods) {
if (transformedName.equals("net.minecraft.client.renderer.OpenGlHelper") && (mn.name.equals("glBlendFunc") || mn.name.equals("func_148821_a"))) {
if (isOpenGlHelper && (mn.name.equals("glBlendFunc") || mn.name.equals("func_148821_a"))) {
continue;
}
boolean redirectInMethod = false;
for (AbstractInsnNode node : mn.instructions.toArray()) {
if (node instanceof MethodInsnNode mNode) {
if ((mNode.owner.equals(GL11) || mNode.owner.equals(GL11C)) && (mNode.name.equals("glEnable") || mNode.name.equals("glDisable")) && mNode.desc.equals("(I)V")) {
if (mNode.desc.equals("(I)V") && (mNode.owner.equals(GL11) || mNode.owner.equals(GL11C)) && (mNode.name.equals("glEnable") || mNode.name.equals("glDisable"))) {
final AbstractInsnNode prevNode = node.getPrevious();
String name = null;
if (prevNode instanceof LdcInsnNode ldcNode) {
@@ -373,7 +477,7 @@ public final class AngelicaRedirector {
}
changed = true;
redirectInMethod = true;
} else if (mNode.owner.startsWith(Drawable) && mNode.name.equals("makeCurrent")) {
} else if (mNode.name.equals("makeCurrent") && mNode.owner.startsWith(Drawable)) {
mNode.setOpcode(Opcodes.INVOKESTATIC);
mNode.owner = GLStateManager;
mNode.desc = "(L" + Drawable + ";)V";
@@ -387,7 +491,7 @@ public final class AngelicaRedirector {
if (redirects != null) {
final String glsmName = redirects.get(mNode.name);
// Skip VAO method redirects inside UniversalVAO
if (glsmName != null && !(transformedName.startsWith(UniversalVAODot) && UniversalVAOSkippedMethods.contains(glsmName))) {
if (glsmName != null && !(isUniversalVAO && UniversalVAOSkippedMethods.contains(glsmName))) {
if (LOG_SPAM) {
final String shortOwner = mNode.owner.substring(mNode.owner.lastIndexOf("/") + 1);
LOGGER.info("Redirecting call in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, mNode.name, mNode.desc, glsmName, mNode.desc);
@@ -403,7 +507,7 @@ public final class AngelicaRedirector {
final Map<String, String> ifaceRedirects = interfaceRedirects.get(mNode.owner);
if (ifaceRedirects != null) {
final String glsmName = ifaceRedirects.get(mNode.name);
if (glsmName != null && !(transformedName.startsWith(UniversalVAODot) && UniversalVAOSkippedMethods.contains(glsmName))) {
if (glsmName != null && !(isUniversalVAO && UniversalVAOSkippedMethods.contains(glsmName))) {
if (LOG_SPAM) {
final String shortOwner = mNode.owner.substring(mNode.owner.lastIndexOf("/") + 1);
LOGGER.info("Redirecting interface call in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, mNode.name, mNode.desc, glsmName, "(I)V");
@@ -19,31 +19,39 @@ import org.objectweb.asm.tree.MethodNode;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class SodiumBlockTransform {
public final class CeleritasBlockTransform {
public SodiumBlockTransform(boolean isObf) {
this.IS_OBF = isObf;
private final ClassConstantPoolParser cstPoolParser;
private final Map<String, String> blockFieldRedirects = new HashMap<>();
public CeleritasBlockTransform(boolean isObf) {
final List<Pair<String, String>> mappings = ImmutableList.of(
Pair.of("minX", "field_149759_B"),
Pair.of("minY", "field_149760_C"),
Pair.of("minZ", "field_149754_D"),
Pair.of("maxX", "field_149755_E"),
Pair.of("maxY", "field_149756_F"),
Pair.of("maxZ", "field_149757_G")
);
for (Pair<String, String> pair : mappings) {
final String name = isObf ? pair.getRight() : pair.getLeft();
this.blockFieldRedirects.put(name, pair.getLeft());
}
this.cstPoolParser = new ClassConstantPoolParser(this.blockFieldRedirects.keySet().toArray(new String[0]));
}
private final boolean IS_OBF;
private static final boolean LOG_SPAM = Boolean.getBoolean("angelica.redirectorLogspam");
private static final Logger LOGGER = LogManager.getLogger("SodiumBlockTransformer");
private static final Logger LOGGER = LogManager.getLogger("CeleritasBlockTransformer");
private static final String BlockClass = "net/minecraft/block/Block";
private static final String BlockPackage = "net/minecraft/block/Block";
private static final String ThreadedBlockData = "com/gtnewhorizons/angelica/glsm/ThreadedBlockData";
private static final List<Pair<String, String>> BlockBoundsFields = ImmutableList.of(
Pair.of("minX", "field_149759_B"),
Pair.of("minY", "field_149760_C"),
Pair.of("minZ", "field_149754_D"),
Pair.of("maxX", "field_149755_E"),
Pair.of("maxY", "field_149756_F"),
Pair.of("maxZ", "field_149757_G")
);
/** All classes in <tt>net.minecraft.block.*</tt> are the block subclasses save for these. */
private static final String[] VanillaBlockExclusions = {
"net/minecraft/block/IGrowable",
@@ -53,7 +61,6 @@ public class SodiumBlockTransform {
"net/minecraft/block/material/"
};
private static final ClassConstantPoolParser cstPoolParser = new ClassConstantPoolParser(BlockPackage);
private static final Set<String> moddedBlockSubclasses = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Block owners we *shouldn't* redirect because they shadow one of our fields
private static final Set<String> blockOwnerExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>());
@@ -95,64 +102,54 @@ public class SodiumBlockTransform {
return isVanillaBlockSubclass(className) || moddedBlockSubclasses.contains(className);
}
private String getFieldName(Pair<String, String> fieldPair) {
return IS_OBF ? fieldPair.getRight() : fieldPair.getLeft();
}
public String[] getTransformerExclusions() {
return new String[]{
"org.lwjgl",
"com.gtnewhorizons.angelica.glsm.",
"com.gtnewhorizons.angelica.transform",
"com.gtnewhorizon.gtnhlib.asm.",
"me.eigenraven.lwjgl3ify"
};
}
public boolean shouldTransform(byte[] basicClass) {
return cstPoolParser.find(basicClass, true);
public void trackBlockSubclasses(String className, String classSuperName) {
if (!isVanillaBlockSubclass(className) && isBlockSubclass(classSuperName)) {
moddedBlockSubclasses.add(className);
}
}
public boolean shouldTransform(byte[] classBytes) {
return cstPoolParser.find(classBytes);
}
/** @return Was the class changed? */
public boolean transformClassNode(String transformedName, ClassNode cn) {
if (cn == null) {
if (!isCeleritasEnabled()) {
return false;
}
if ("net.minecraft.block.Block".equals(transformedName) && isCeleritasEnabled()) {
cn.fields.removeIf(field -> BlockBoundsFields.stream().anyMatch(pair -> field.name.equals(pair.getLeft()) || field.name.equals(pair.getRight())));
boolean changed = false;
if ("net.minecraft.block.Block".equals(transformedName)) {
changed = cn.fields.removeIf(field -> blockFieldRedirects.containsKey(field.name));
}
// Track subclasses of Block
if (!isVanillaBlockSubclass(cn.name) && isBlockSubclass(cn.superName)) {
moddedBlockSubclasses.add(cn.name);
cstPoolParser.addString(cn.name);
}
trackBlockSubclasses(cn.name, cn.superName);
// Check if this class shadows any fields of the parent class
if (moddedBlockSubclasses.contains(cn.name)) {
// If a superclass shadows, then so do we, because JVM will resolve a reference on our class to that
// superclass
boolean doWeShadow;
boolean doWeShadow = false;
if (blockOwnerExclusions.contains(cn.superName)) {
doWeShadow = true;
} else {
// Check if we declare any known field names
boolean b = false;
for (Pair<String, String> pair : BlockBoundsFields) {
boolean result = false;
for (FieldNode field : cn.fields) {
if (field.name.equals(getFieldName(pair))) {
result = true;
break;
}
}
if (result) {
b = true;
for (FieldNode field : cn.fields) {
if (blockFieldRedirects.containsKey(field.name)) {
doWeShadow = true;
break;
}
}
doWeShadow = b;
}
if (doWeShadow) {
LOGGER.info("Class '{}' shadows one or more block bounds fields, these accesses won't be redirected!", cn.name);
@@ -160,21 +157,17 @@ public class SodiumBlockTransform {
}
}
boolean changed = false;
for (MethodNode mn : cn.methods) {
for (AbstractInsnNode node : mn.instructions.toArray()) {
if ((node.getOpcode() == Opcodes.GETFIELD || node.getOpcode() == Opcodes.PUTFIELD) && node instanceof FieldInsnNode fNode) {
if (!blockOwnerExclusions.contains(fNode.owner) && isBlockSubclass(fNode.owner) && isCeleritasEnabled()) {
Pair<String, String> fieldToRedirect = BlockBoundsFields.stream()
.filter(pair -> fNode.name.equals(getFieldName(pair)))
.findFirst()
.orElse(null);
if (fieldToRedirect != null) {
if (!blockOwnerExclusions.contains(fNode.owner) && isBlockSubclass(fNode.owner)) {
String fieldRedirect = blockFieldRedirects.get(fNode.name);
if (fieldRedirect != null) {
if (LOG_SPAM) {
LOGGER.info("Redirecting Block.{} in {} to thread-safe wrapper", fNode.name, transformedName);
}
// Perform the redirect
fNode.name = fieldToRedirect.getLeft(); // use unobfuscated name
fNode.name = fieldRedirect; // use unobfuscated name
fNode.owner = ThreadedBlockData;
// Inject getter before the field access, to turn Block -> ThreadedBlockData
final MethodInsnNode getter = new MethodInsnNode(Opcodes.INVOKESTATIC, ThreadedBlockData, "get", "(L" + BlockClass + ";)L" + ThreadedBlockData + ";", false);
@@ -29,6 +29,7 @@ public enum Mixins implements IMixins {
.addClientMixins(
"angelica.MixinActiveRenderInfo"
, "angelica.MixinEntityRenderer"
, "angelica.MixinForgeHooksClient_CoreProfile"
, "angelica.MixinGameSettings"
, "angelica.MixinMinecraft"
, "angelica.MixinMinecraftServer"
@@ -37,6 +38,7 @@ public enum Mixins implements IMixins {
, "angelica.bugfixes.MixinRenderGlobal_DestroyBlock"
, "angelica.glsm.MixinSplashProgressCaching"
, "angelica.debug.MixinMinecraft_FPSCap"
, "angelica.ffp.MixinTessellator_CoreProfile"
)
),
@@ -106,6 +108,11 @@ public enum Mixins implements IMixins {
.setPhase(Phase.EARLY)
.addClientMixins("angelica.optimizations.MixinGLAllocation")),
ANGELICA_DEFERRED_TESSELLATOR_BATCH(new MixinBuilder("Deferred tessellator batching for particles to reduce draw calls")
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableCeleritas)
.addClientMixins("angelica.particles.MixinEffectRenderer_DeferredBatch")),
// Not compatible with the lwjgl debug callbacks, so disable if that's enabled
ARCHAIC_SPLASH(new MixinBuilder()
.setPhase(Phase.EARLY)
@@ -275,6 +282,7 @@ public enum Mixins implements IMixins {
OPTIMIZE_WORLD_UPDATE_LIGHT(new MixinBuilder()
.setPhase(Phase.EARLY)
.addExcludedMod(TargetedMod.ARCHAICFIX)
.addExcludedMod(TargetedMod.SUPERNOVA)
.setApplyIf(() -> AngelicaConfig.optimizeWorldUpdateLight)
.addCommonMixins("angelica.lighting.MixinWorld_FixLightUpdateLag")),
@@ -344,6 +352,11 @@ public enum Mixins implements IMixins {
.addClientMixins("angelica.textures.MixinTextureUtil_OptimizeMipmap")
.setApplyIf(() -> AngelicaConfig.optimizeTextureLoading)),
REPLACE_FFP(new MixinBuilder()
.setPhase(Phase.EARLY)
.addClientMixins("angelica.vbo.MixinFramebuffer")
.setApplyIf(() -> AngelicaConfig.replaceFFPUploads)),
//From NotFine
NOTFINE_BASE_MOD(new MixinBuilder()
.setPhase(Phase.EARLY)
@@ -22,6 +22,7 @@ public enum TargetedMod implements ITargetMod {
IC2("ic2.core.coremod.IC2core", "IC2"),
NATURA(null, "Natura"),
SECURITYCRAFT(null, "securitycraft"),
SUPERNOVA("com.mitchej123.supernova.core.SupernovaCore", "supernova"),
THAUMCRAFT(null, "Thaumcraft"),
TINKERS_CONSTRUCT(null, "TConstruct"),
TWILIGHT_FOREST(null, "TwilightForest"),
@@ -25,38 +25,44 @@ import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VertexBufferType;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.IVertexBuffer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.uniform.GlUniformFloat2v;
import jss.notfine.core.Settings;
import jss.notfine.gui.options.named.GraphicsQualityOff;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.resources.IReloadableResourceManager;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourceManagerReloadListener;
import net.minecraft.entity.Entity;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Vec3;
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
import org.embeddedt.embeddium.impl.gl.shader.GlShader;
import org.embeddedt.embeddium.impl.gl.shader.ShaderBindingContext;
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
import org.embeddedt.embeddium.impl.gl.shader.ShaderType;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat3v;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat4v;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformInt;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix4f;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.joml.Matrix4f;
import org.joml.Matrix4fStack;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
public class CloudRenderer implements IResourceManagerReloadListener {
// Shared constants.
private static final float PX_SIZE = 1 / 256F;
// Building constants.
private static final int TOP_SECTIONS = 12; // Number of slices a top face will span.
private static final int TOP_SECTIONS = 12;
private static final int HEIGHT = 4;
private static final float INSET = 0.001F;
private static final float ALPHA = 0.8F;
// Debug
private static final boolean WIREFRAME = false;
// Instance fields
private final Minecraft mc = Minecraft.getMinecraft();
private final ResourceLocation texture = new ResourceLocation("textures/environment/clouds.png");
@@ -66,14 +72,13 @@ public class CloudRenderer implements IResourceManagerReloadListener {
private int cloudElevation = -1;
private int scaleMult = -1;
private DynamicTexture COLOR_TEX = null;
private int texColor;
private int texW;
private int texH;
private GlProgram<CloudUniforms> program;
private final Matrix4f mvpScratch = new Matrix4f();
public CloudRenderer() {
// Resource manager should always be reloadable.
((IReloadableResourceManager) mc.getResourceManager()).registerReloadListener(this);
}
@@ -89,24 +94,28 @@ public class CloudRenderer implements IResourceManagerReloadListener {
}
private float ceilToScale(float value) {
float scale = getScale();
final float scale = getScale();
return (float)Math.ceil(value / scale) * scale;
}
private float meshExtent() {
return ceilToScale(renderDistance * 4 * 16);
}
private void vertices(Tessellator tessellator) {
GraphicsQualityOff cloudGraphicsQuality = (GraphicsQualityOff)Settings.MODE_CLOUDS.option.getStore();
boolean fancy = cloudGraphicsQuality == GraphicsQualityOff.FANCY || cloudGraphicsQuality == GraphicsQualityOff.DEFAULT && mc.gameSettings.fancyGraphics;
final GraphicsQualityOff cloudGraphicsQuality = (GraphicsQualityOff)Settings.MODE_CLOUDS.option.getStore();
final boolean fancy = cloudGraphicsQuality == GraphicsQualityOff.FANCY || cloudGraphicsQuality == GraphicsQualityOff.DEFAULT && mc.gameSettings.fancyGraphics;
float scale = getScale();
float CULL_DIST = 2 * scale;
final float scale = getScale();
final float CULL_DIST = 2 * scale;
float bCol = fancy ? 0.7F : 1F;
final float bCol = fancy ? 0.7F : 1F;
float sectEnd = ceilToScale((renderDistance * 2) * 16);
float sectStart = -sectEnd;
final float sectEnd = meshExtent();
final float sectStart = -sectEnd;
float sectStep = ceilToScale(sectEnd * 2 / TOP_SECTIONS);
float sectPx = PX_SIZE / scale;
final float sectStep = ceilToScale(sectEnd * 2 / TOP_SECTIONS);
final float sectPx = PX_SIZE / scale;
tessellator.startDrawingQuads();
@@ -128,10 +137,10 @@ public class CloudRenderer implements IResourceManagerReloadListener {
if (sectZ1 > sectEnd)
sectZ1 = sectEnd;
float u0 = sectX0 * sectPx;
float u1 = sectX1 * sectPx;
float v0 = sectZ0 * sectPx;
float v1 = sectZ1 * sectPx;
final float u0 = sectX0 * sectPx;
final float u1 = sectX1 * sectPx;
final float v0 = sectZ0 * sectPx;
final float v1 = sectZ1 * sectPx;
// Bottom
tessellator.setColorRGBA_F(bCol, bCol, bCol, ALPHA);
@@ -222,6 +231,10 @@ public class CloudRenderer implements IResourceManagerReloadListener {
vao.delete();
vao = null;
}
if (program != null) {
program.delete();
program = null;
}
}
private void build() {
@@ -239,7 +252,7 @@ public class CloudRenderer implements IResourceManagerReloadListener {
}
public void checkSettings() {
GraphicsQualityOff cloudGraphicsQuality = (GraphicsQualityOff)Settings.MODE_CLOUDS.option.getStore();
final GraphicsQualityOff cloudGraphicsQuality = (GraphicsQualityOff)Settings.MODE_CLOUDS.option.getStore();
final int cloudQualitySetting = cloudGraphicsQuality == GraphicsQualityOff.FANCY
|| cloudGraphicsQuality == GraphicsQualityOff.DEFAULT && mc.gameSettings.fancyGraphics ? 2 : 1;
final boolean newEnabled = cloudGraphicsQuality != GraphicsQualityOff.OFF
@@ -273,91 +286,170 @@ public class CloudRenderer implements IResourceManagerReloadListener {
}
}
private void initProgram() {
if (program != null) return;
final GlShader vert = ShaderLoader.loadShader(ShaderType.VERTEX, "angelica:cloud.vert", ShaderConstants.EMPTY);
final GlShader frag = ShaderLoader.loadShader(ShaderType.FRAGMENT, "angelica:cloud.frag", ShaderConstants.EMPTY);
try {
program = GlProgram.builder("angelica:cloud")
.attachShader(vert)
.attachShader(frag)
.link(CloudUniforms::new);
} finally {
vert.delete();
frag.delete();
}
final CloudUniforms u = program.getInterface();
final float start = renderDistance * 16.0f;
final float end = meshExtent();
final float range = end - start;
program.bind();
u.cloudTex.setInt(0);
u.fogParams.set(new float[] {
range != 0.0f ? -1.0f / range : 0.0f,
range != 0.0f ? end / range : 1.0f,
0.0f, 0.0f
});
program.unbind();
}
private void uploadFogUniforms(float partialTicks) {
final CloudUniforms u = program.getInterface();
final boolean fogEnabled = GLStateManager.getFogMode().isEnabled();
u.setFogEnabled(fogEnabled);
if (fogEnabled) {
final Vec3 sky = mc.theWorld.getSkyColor(mc.renderViewEntity, partialTicks);
u.setFogColor((float) sky.xCoord, (float) sky.yCoord, (float) sky.zCoord);
}
}
private static class CloudUniforms {
final GlUniformMatrix4f mvp;
final GlUniformFloat3v colorMult;
final GlUniformFloat2v texOffset;
final GlUniformFloat2v fogOrigin;
final GlUniformFloat4v fogParams;
final GlUniformFloat4v fogColor;
final GlUniformInt fogEnabled;
final GlUniformInt cloudTex;
private int lastFogEnabled = -1;
private float lastFogR = Float.NaN, lastFogG = Float.NaN, lastFogB = Float.NaN;
private float lastColorR = Float.NaN, lastColorG = Float.NaN, lastColorB = Float.NaN;
private final float[] fogColorBuf = new float[4];
CloudUniforms(ShaderBindingContext ctx) {
mvp = ctx.bindUniform("u_MVPMatrix", GlUniformMatrix4f::new);
colorMult = ctx.bindUniform("u_ColorMultiplier", GlUniformFloat3v::new);
texOffset = ctx.bindUniform("u_TexOffset", GlUniformFloat2v::new);
fogOrigin = ctx.bindUniform("u_FogOrigin", GlUniformFloat2v::new);
fogParams = ctx.bindUniform("u_FogParams", GlUniformFloat4v::new);
fogColor = ctx.bindUniform("u_FogColor", GlUniformFloat4v::new);
fogEnabled = ctx.bindUniform("u_FogEnabled", GlUniformInt::new);
cloudTex = ctx.bindUniform("u_CloudTexture", GlUniformInt::new);
}
void setFogEnabled(boolean enabled) {
final int val = enabled ? 1 : 0;
if (val != lastFogEnabled) {
lastFogEnabled = val;
fogEnabled.setInt(val);
}
}
void setFogColor(float r, float g, float b) {
if (r != lastFogR || g != lastFogG || b != lastFogB) {
lastFogR = r; lastFogG = g; lastFogB = b;
fogColorBuf[0] = r; fogColorBuf[1] = g; fogColorBuf[2] = b; fogColorBuf[3] = 1.0f;
fogColor.set(fogColorBuf);
}
}
void setColorMult(float r, float g, float b) {
if (r != lastColorR || g != lastColorG || b != lastColorB) {
lastColorR = r; lastColorG = g; lastColorB = b;
colorMult.set(r, g, b);
}
}
}
public boolean render(int cloudTicks, float partialTicks) {
if (!isBuilt())
return false;
Entity entity = mc.renderViewEntity;
if (program == null) {
initProgram();
if (program == null) return false;
}
double totalOffset = cloudTicks + partialTicks;
final Entity entity = mc.renderViewEntity;
double x = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks + totalOffset * 0.03;
double y = cloudElevation - (entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks) + 0.33;
final double totalOffset = cloudTicks + partialTicks;
final double x = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks + totalOffset * 0.03;
final double y = cloudElevation - (entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks) + 0.33;
double z = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks;
int scale = getScale();
final int scale = getScale();
if (cloudMode == 2)
z += 0.33 * scale;
// Integer UVs to translate the texture matrix by.
int offU = fullCoord(x, scale);
int offV = fullCoord(z, scale);
GLStateManager.glPushMatrix();
// Translate by the remainder after the UV offset.
GLStateManager.glTranslated((offU * scale) - x, y, (offV * scale) - z);
// Modulo to prevent texture samples becoming inaccurate at extreme offsets.
final float fogOriginX = (float)(x - offU * scale);
final float fogOriginZ = (float)(z - offV * scale);
offU = offU % texW;
offV = offV % texH;
// Translate the texture.
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glTranslatef(offU * PX_SIZE, offV * PX_SIZE, 0);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.disableCull();
GLStateManager.enableBlend();
GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
// Color multiplier.
Vec3 color = mc.theWorld.getCloudColour(partialTicks);
final Vec3 color = mc.theWorld.getCloudColour(partialTicks);
float r = (float) color.xCoord;
float g = (float) color.yCoord;
float b = (float) color.zCoord;
if (mc.gameSettings.anaglyph) {
float tempR = r * 0.3F + g * 0.59F + b * 0.11F;
float tempG = r * 0.3F + g * 0.7F;
float tempB = r * 0.3F + b * 0.7F;
final float tempR = r * 0.3F + g * 0.59F + b * 0.11F;
final float tempG = r * 0.3F + g * 0.7F;
final float tempB = r * 0.3F + b * 0.7F;
r = tempR;
g = tempG;
b = tempB;
}
// Apply a color multiplier through a texture upload if shaders aren't supported.
final int newColor = 0xFF000000
| ((int) (r * 255)) << 16
| ((int) (g * 255)) << 8
| (int) (b * 255);
if (texColor != newColor) {
if (COLOR_TEX == null) {
COLOR_TEX = new DynamicTexture(1, 1);
}
COLOR_TEX.getTextureData()[0] = newColor;
COLOR_TEX.updateDynamicTexture();
texColor = newColor;
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, COLOR_TEX.getGlTextureId());
GLStateManager.enableTexture();
// Bind the clouds texture last so the shader's sampler2D is correct.
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
mc.renderEngine.bindTexture(texture);
program.bind();
final CloudUniforms u = program.getInterface();
final Matrix4fStack mv = GLStateManager.getModelViewMatrix();
GLStateManager.getProjectionMatrix().mul(mv, mvpScratch);
u.mvp.set(mvpScratch);
u.texOffset.set(offU * PX_SIZE, offV * PX_SIZE);
u.setColorMult(r, g, b);
u.fogOrigin.set(fogOriginX, fogOriginZ);
uploadFogUniforms(partialTicks);
vao.bind();
// Depth pass to prevent insides rendering from the outside.
GLStateManager.glColorMask(false, false, false, false);
vao.draw();
// Full render.
if (!mc.gameSettings.anaglyph) {
GLStateManager.glColorMask(true, true, true, true);
} else {
@@ -371,32 +463,19 @@ public class CloudRenderer implements IResourceManagerReloadListener {
}
}
// Wireframe for debug.
if (WIREFRAME) {
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
GL11.glLineWidth(2.0F);
GLStateManager.disableTexture();
GLStateManager.glLineWidth(2.0F);
GLStateManager.glDepthMask(false);
GLStateManager.disableFog();
vao.draw();
GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
GLStateManager.glDepthMask(true);
GLStateManager.enableTexture();
GLStateManager.enableFog();
}
vao.draw();
vao.unbind(); // Unbind buffer.
// Disable our coloring.
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.disableTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
// Reset texture matrix.
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glLoadIdentity();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
program.unbind();
GLStateManager.disableBlend();
GLStateManager.enableCull();
@@ -417,5 +496,9 @@ public class CloudRenderer implements IResourceManagerReloadListener {
@Override
public void onResourceManagerReload(IResourceManager resourceManager) {
reloadTextures();
if (program != null) {
program.delete();
program = null;
}
}
}
@@ -29,11 +29,10 @@ import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexedVAO;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -8,5 +8,12 @@ public interface StateAwareTessellator {
void angelica$setAppliedAo(boolean flag);
/**
* Sets whether we're doing terrain meshing as part of celeritas -- collects additional information.
*
* Enables per-vertex AO state collection into {@link #angelica$getVertexStates()}.
*/
void angelica$setCeleritasMeshing(boolean active);
int[] angelica$getVertexStates();
}
@@ -142,6 +142,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
final int worldZ = originZ + blockZ;
int stateIdx = 0;
int facesAtBaseMaterial = 0;
for (int quadIdx = 0; quadIdx < numQuads; quadIdx++) {
float uSum = 0, vSum = 0;
@@ -218,7 +219,17 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
}
}
final Material correctMaterial = selectMaterial(material, sprite, isShaderPackOverride);
final int faceBit = 1 << facing.ordinal();
final Material correctMaterial;
if ((facesAtBaseMaterial & faceBit) != 0) {
// Face already has a non-demoted quad — skip selectMaterial entirely
correctMaterial = material;
} else {
correctMaterial = selectMaterial(material, sprite, isShaderPackOverride);
if (correctMaterial == material) {
facesAtBaseMaterial |= faceBit;
}
}
final var builder = buffers.get(correctMaterial);
if (correctMaterial != material && builder.getEncoder() instanceof IrisExtendedChunkVertexEncoder iris) {
@@ -104,6 +104,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
onEnterExecute();
final Tessellator tessellator = getTessellator();
((StateAwareTessellator)tessellator).angelica$setCeleritasMeshing(true);
try {
tessellator.setTranslation(-minX, -minY, -minZ);
@@ -207,6 +208,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
} catch (Throwable ex) {
throw fillCrashInfo(CrashReport.makeCrashReport(ex, "Encountered exception while building chunk meshes"), region, blockPos);
} finally {
((StateAwareTessellator)tessellator).angelica$setCeleritasMeshing(false);
SmoothBiomeColorCache.clearActiveCache();
onExitExecute();
}
@@ -270,15 +272,19 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
final CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
final RenderBlocks mainThreadRenderBlocks = new RenderBlocks(region);
final Tessellator mainTessellator = Tessellator.instance;
((StateAwareTessellator)mainTessellator).angelica$setCeleritasMeshing(true);
mainTessellator.setTranslation(-minX, -minY, -minZ);
for (DeferredBlock deferred : deferredBlocks) {
renderBlock(deferred.block(), deferred.meta(), deferred.x(), deferred.y(), deferred.z(), deferred.pass(),
mainTessellator, mainThreadRenderBlocks, buffers, buildContext,
blockRenderContext, minX, minY, minZ, deferred.materialOverride(), deferred.isShaderPackOverride());
try {
for (DeferredBlock deferred : deferredBlocks) {
renderBlock(deferred.block(), deferred.meta(), deferred.x(), deferred.y(), deferred.z(), deferred.pass(),
mainTessellator, mainThreadRenderBlocks, buffers, buildContext,
blockRenderContext, minX, minY, minZ, deferred.materialOverride(), deferred.isShaderPackOverride());
}
} finally {
((StateAwareTessellator)mainTessellator).angelica$setCeleritasMeshing(false);
mainTessellator.setTranslation(0, 0, 0);
}
mainTessellator.setTranslation(0, 0, 0);
}, AngelicaRenderQueue.executor());
if (Thread.currentThread().isInterrupted()) {
@@ -4,9 +4,9 @@ import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
import org.embeddedt.embeddium.impl.gl.device.RenderDevice;
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
import org.embeddedt.embeddium.impl.render.chunk.DefaultChunkRenderer;
import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
import org.embeddedt.embeddium.impl.render.chunk.multidraw.DirectMultiDrawEmitter;
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.rendering.celeritas;
import com.google.common.collect.ImmutableListMultimap;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
import org.embeddedt.embeddium.impl.render.chunk.compile.sorting.QuadPrimitiveType;
@@ -21,17 +22,17 @@ public class AngelicaRenderPassConfiguration {
@Override
public void setup() {
if (pass == 0) {
GL11.glAlphaFunc(GL11.GL_GREATER, 0.1F);
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1F);
}
if (disableAlphaTest) {
GL11.glDisable(GL11.GL_ALPHA_TEST);
GLStateManager.disableAlphaTest();
}
}
@Override
public void clear() {
if (disableAlphaTest) {
GL11.glEnable(GL11.GL_ALPHA_TEST);
GLStateManager.enableAlphaTest();
}
}
}
@@ -40,7 +41,7 @@ public class AngelicaRenderPassConfiguration {
return TerrainRenderPass.builder()
.pipelineState(new AngelicaPipelineState(pass, disableAlphaTest))
.vertexType(vertexType)
.primitiveType(QuadPrimitiveType.DIRECT);
.primitiveType(QuadPrimitiveType.TRIANGULATED);
}
public static RenderPassConfiguration<BlockRenderLayer> build(ChunkVertexType vertexType) {
@@ -4,6 +4,8 @@ import com.gtnewhorizons.angelica.Tags;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
@@ -42,6 +44,11 @@ public class CeleritasDebugScreenHandler {
event.right.add("");
event.right.add(EnumChatFormatting.GREEN + "Angelica " + Tags.VERSION + " [Celeritas Renderer]");
event.right.addAll(renderer.getDebugStrings());
final ShaderManager sm = ShaderManager.getInstance();
if (sm != null) {
event.right.add(sm.getDebugInfo());
}
event.right.add(TessellatorStreamingDrawer.getDebugInfo());
event.right.add("");
}
@@ -230,6 +230,7 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
private int renderCulledTileEntities(TileEntityRenderContext renderContext) {
int count = 0;
sortedTileEntities.clear();
SortedRenderLists renderLists = renderSectionManager.getRenderLists();
Iterator<ChunkRenderList> renderListIterator = renderLists.iterator();
@@ -4,19 +4,24 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraftforge.client.ForgeHooksClient;
public final class RenderPassHelper {
private static final ThreadLocal<Integer> threadLocalPass = ThreadLocal.withInitial(() -> -1);
private static final class PassIntHolder {
int pass = -1;
}
private static final ThreadLocal<PassIntHolder> threadLocalPass = ThreadLocal.withInitial(PassIntHolder::new);
private RenderPassHelper() {}
public static int getWorldRenderPass() {
return GLStateManager.isMainThread() ? ForgeHooksClient.worldRenderPass : threadLocalPass.get();
return GLStateManager.isMainThread() ? ForgeHooksClient.worldRenderPass : threadLocalPass.get().pass;
}
public static void setWorldRenderPass(int pass) {
if (GLStateManager.isMainThread()) {
ForgeHooksClient.worldRenderPass = pass;
} else {
threadLocalPass.set(pass);
threadLocalPass.get().pass = pass;
}
}
@@ -7,8 +7,8 @@ import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.StampedLock;
import net.minecraft.block.Block;
import net.minecraft.util.IIcon;
@@ -33,19 +33,36 @@ public class CTMUtils {
private static final MCLogger logger = MCLogger.getLogger(MCLogger.Category.CONNECTED_TEXTURES, "CTM");
private static final List<TileOverride> allOverrides = new ArrayList<>();
private static final Map<Block, List<BlockStateMatcher>> blockOverrides = new IdentityHashMap<>();
private static final Map<String, List<TileOverride>> tileOverrides = new HashMap<>();
private static class Overrides {
private final List<TileOverride> all = new ArrayList<>();
private final Map<Block, List<BlockStateMatcher>> block = new IdentityHashMap<>();
private final Map<String, List<TileOverride>> tile = new HashMap<>();
}
private static Overrides overrides = new Overrides();
private static Overrides newOverrides = null;
private static TileLoader tileLoader;
private static TileOverride lastOverride;
private static final Lock lock = new ReentrantLock();
private static final StampedLock lock = new StampedLock();
private static final TileOverrideIterator.IJK ijkIterator = newIJKIterator();
private static final TileOverrideIterator.Metadata metadataIterator = newMetadataIterator();
private static class StateAndIterator {
private final TileOverrideIterator.IJK ijkIterator = CTMUtils.newIJKIterator();
private final TileOverrideIterator.Metadata metadataIterator = CTMUtils.newMetadataIterator();
private final BlockOrientation renderBlockState = new BlockOrientation();
private final int texturePackChangeCounter;
private static boolean haveBlockFace;
private static final BlockOrientation renderBlockState = new BlockOrientation();
public StateAndIterator() {
this.texturePackChangeCounter = 0;
}
public StateAndIterator(int texturePackChangeCounter) {
this.texturePackChangeCounter = texturePackChangeCounter;
}
}
private static final ThreadLocal<StateAndIterator> stateAndIterator = ThreadLocal.withInitial(StateAndIterator::new);
private static final AtomicInteger texturePackChangeCounter = new AtomicInteger(0);
static {
try {
@@ -61,7 +78,7 @@ public class CTMUtils {
@Override
public void beforeChange() {
lock.lock();
long stamp = lock.writeLock();
try {
RenderPassAPI.instance.clear();
try {
@@ -71,39 +88,39 @@ public class CTMUtils {
} catch (Exception e) {
e.printStackTrace();
}
renderBlockState.clear();
ijkIterator.clear();
metadataIterator.clear();
allOverrides.clear();
blockOverrides.clear();
tileOverrides.clear();
lastOverride = null;
RenderBlocksUtils.blankIcon = null;
tileLoader = new TileLoader("textures/blocks", logger);
RenderPassAPI.instance.refreshBlendingOptions();
logger.info("loading CTM overrides");
newOverrides = new Overrides();
if (MCPatcherForgeConfig.ConnectedTextures.standard || MCPatcherForgeConfig.ConnectedTextures.nonStandard) {
for (ResourceLocation resource : ResourceList.getInstance()
.listResources(TexturePackAPI.MCPATCHER_SUBDIR + "ctm", ".properties", true)) {
registerOverride(TileOverride.create(resource, tileLoader));
registerOverrideWithoutLock(newOverrides, TileOverride.create(resource, tileLoader));
}
}
for (ResourceLocation resource : BlendMethod.getAllBlankResources()) {
tileLoader.preloadTile(resource, false);
}
} finally {
lock.unlock();
lock.unlockWrite(stamp);
}
}
@Override
public void afterChange() {
lock.lock();
if (newOverrides == null) {
logger.error("CTM overrides were not initialized during texture pack change");
logger.error("Maybe beforeChange was not called, or threw an exception?");
return;
}
long stamp = lock.writeLock();
try {
for (TileOverride override : allOverrides) {
for (TileOverride override : newOverrides.all) {
override.registerIcons();
}
for (Map.Entry<Block, List<BlockStateMatcher>> entry : blockOverrides.entrySet()) {
for (Map.Entry<Block, List<BlockStateMatcher>> entry : newOverrides.block.entrySet()) {
for (BlockStateMatcher matcher : entry.getValue()) {
TileOverride override = (TileOverride) matcher.getData();
if (override.getRenderPass() >= 0) {
@@ -111,67 +128,90 @@ public class CTMUtils {
}
}
}
for (List<BlockStateMatcher> overrides : blockOverrides.values()) {
for (List<BlockStateMatcher> overrides : newOverrides.block.values()) {
overrides.sort((m1, m2) -> {
TileOverride o1 = (TileOverride) m1.getData();
TileOverride o2 = (TileOverride) m2.getData();
return o1.compareTo(o2);
});
}
for (List<TileOverride> overrides : tileOverrides.values()) {
for (List<TileOverride> overrides : newOverrides.tile.values()) {
Collections.sort(overrides);
}
setBlankResource();
setBlankResourceWithoutLock();
overrides = newOverrides;
newOverrides = null;
texturePackChangeCounter.getAndIncrement();
} finally {
lock.unlock();
lock.unlockWrite(stamp);
}
}
});
}
private static void clearBlockFace() {
haveBlockFace = false;
private static StateAndIterator getStateAndIterator() {
int globalTexturePackChangeCounter = texturePackChangeCounter.get();
StateAndIterator local = stateAndIterator.get();
if (local.texturePackChangeCounter < globalTexturePackChangeCounter) {
local = new StateAndIterator(globalTexturePackChangeCounter);
stateAndIterator.set(local);
}
return local;
}
private static IIcon getBlockIconWithoutLock(IIcon icon, Block block, IBlockAccess blockAccess,
int x, int y, int z, int face) {
StateAndIterator local = getStateAndIterator();
if (blockAccess != null && checkFace(face)) {
local.renderBlockState.setBlock(block, blockAccess, x, y, z);
local.renderBlockState.setFace(face);
TileOverride lastOverride = local.ijkIterator.go(local.renderBlockState, icon);
if (lastOverride != null) {
return skipDefaultRendering(block) ? RenderBlocksUtils.blankIcon : local.ijkIterator.getIcon();
}
}
return skipDefaultRendering(block) ? RenderBlocksUtils.blankIcon : icon;
}
private static IIcon getBlockIconWithoutLock(IIcon icon, Block block, int face, int metadata) {
StateAndIterator local = getStateAndIterator();
if (checkFace(face) && checkRenderType(block)) {
local.renderBlockState.setBlockMetadata(block, metadata, face);
TileOverride lastOverride = local.metadataIterator.go(local.renderBlockState, icon);
if (lastOverride != null) {
return local.metadataIterator.getIcon();
}
}
return icon;
}
public static IIcon getBlockIcon(IIcon icon, Block block, IBlockAccess blockAccess,
int x, int y, int z, int face) {
lock.lock();
try {
lastOverride = null;
if (blockAccess != null && checkFace(face)) {
if (!haveBlockFace) {
renderBlockState.setBlock(block, blockAccess, x, y, z);
renderBlockState.setFace(face);
}
lastOverride = ijkIterator.go(renderBlockState, icon);
if (lastOverride != null) {
icon = ijkIterator.getIcon();
}
long stamp = lock.tryOptimisticRead();
IIcon value = getBlockIconWithoutLock(icon, block, blockAccess, x, y, z, face);
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
value = getBlockIconWithoutLock(icon, block, blockAccess, x, y, z, face);
} finally {
lock.unlockRead(stamp);
}
clearBlockFace();
IIcon result = lastOverride == null && skipDefaultRendering(block) ? RenderBlocksUtils.blankIcon : icon;
return result;
} finally {
lock.unlock();
}
return value;
}
public static IIcon getBlockIcon(IIcon icon, Block block, int face, int metadata) {
lock.lock();
try {
lastOverride = null;
if (checkFace(face) && checkRenderType(block)) {
renderBlockState.setBlockMetadata(block, metadata, face);
lastOverride = metadataIterator.go(renderBlockState, icon);
if (lastOverride != null) {
icon = metadataIterator.getIcon();
}
long stamp = lock.tryOptimisticRead();
IIcon value = getBlockIconWithoutLock(icon, block, face, metadata);
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
value = getBlockIconWithoutLock(icon, block, face, metadata);
} finally {
lock.unlockRead(stamp);
}
return icon;
} finally {
lock.unlock();
}
return value;
}
public static IIcon getBlockIcon(IIcon icon, Block block, int face) {
@@ -197,56 +237,55 @@ public class CTMUtils {
return RenderPassAPI.instance.skipDefaultRendering(block);
}
private static void registerOverride(TileOverride override) {
lock.lock();
try {
if (override != null && !override.isDisabled()) {
boolean registered = false;
List<BlockStateMatcher> matchingBlocks = override.getMatchingBlocks();
if (!MCPatcherUtils.isNullOrEmpty(matchingBlocks)) {
for (BlockStateMatcher matcher : matchingBlocks) {
if (matcher == null) {
continue;
}
Block block = matcher.getBlock();
List<BlockStateMatcher> list = blockOverrides.computeIfAbsent(block, k -> new ArrayList<>());
list.add(matcher);
logger.fine("using %s for block %s", override, BlockAPI.getBlockName(block));
registered = true;
private static void registerOverrideWithoutLock(Overrides overrides, TileOverride override) {
if (override != null && !override.isDisabled()) {
boolean registered = false;
List<BlockStateMatcher> matchingBlocks = override.getMatchingBlocks();
if (!MCPatcherUtils.isNullOrEmpty(matchingBlocks)) {
for (BlockStateMatcher matcher : matchingBlocks) {
if (matcher == null) {
continue;
}
}
Set<String> matchingTiles = override.getMatchingTiles();
if (!MCPatcherUtils.isNullOrEmpty(matchingTiles)) {
for (String name : matchingTiles) {
List<TileOverride> list = tileOverrides.computeIfAbsent(name, k -> new ArrayList<>());
list.add(override);
logger.fine("using %s for tile %s", override, name);
registered = true;
}
}
if (registered) {
allOverrides.add(override);
Block block = matcher.getBlock();
List<BlockStateMatcher> list = overrides.block.computeIfAbsent(block, k -> new ArrayList<>());
list.add(matcher);
logger.fine("using %s for block %s", override, BlockAPI.getBlockName(block));
registered = true;
}
}
} finally {
lock.unlock();
Set<String> matchingTiles = override.getMatchingTiles();
if (!MCPatcherUtils.isNullOrEmpty(matchingTiles)) {
for (String name : matchingTiles) {
List<TileOverride> list = overrides.tile.computeIfAbsent(name, k -> new ArrayList<>());
list.add(override);
logger.fine("using %s for tile %s", override, name);
registered = true;
}
}
if (registered) {
overrides.all.add(override);
}
}
}
private static void setBlankResourceWithoutLock() {
RenderBlocksUtils.blankIcon = tileLoader.getIcon(RenderPassAPI.instance.getBlankResource());
}
public static void setBlankResource() {
lock.lock();
long stamp = lock.writeLock();
try {
RenderBlocksUtils.blankIcon = tileLoader.getIcon(RenderPassAPI.instance.getBlankResource());
setBlankResourceWithoutLock();
} finally {
lock.unlock();
lock.unlockWrite(stamp);
}
}
private static TileOverrideIterator.IJK newIJKIterator() {
return new TileOverrideIterator.IJK(blockOverrides, tileOverrides);
return new TileOverrideIterator.IJK(overrides.block, overrides.tile);
}
private static TileOverrideIterator.Metadata newMetadataIterator() {
return new TileOverrideIterator.Metadata(blockOverrides, tileOverrides);
return new TileOverrideIterator.Metadata(overrides.block, overrides.tile);
}
}
@@ -71,13 +71,13 @@ public class MipmapHelper {
}
static void setupTexture(int width, int height, boolean blur, boolean clamp, String textureName) {
int mipmaps = useMipmapsForTexture(textureName) ? getMipmapLevels(width, height, 1) : 0;
final int mipmaps = useMipmapsForTexture(textureName) ? getMipmapLevels(width, height, 1) : 0;
logger.finer("setupTexture(%s) %dx%d %d mipmaps", textureName, width, height, mipmaps);
int magFilter = blur ? GL11.GL_LINEAR : GL11.GL_NEAREST;
int minFilter = mipmaps > 0 ? GL11.GL_LINEAR_MIPMAP_LINEAR : magFilter;
int wrap = clamp ? GL11.GL_CLAMP : GL11.GL_REPEAT;
final int magFilter = blur ? GL11.GL_LINEAR : GL11.GL_NEAREST;
final int minFilter = mipmaps > 0 ? GL11.GL_LINEAR_MIPMAP_LINEAR : magFilter;
final int wrap = clamp ? GL11.GL_CLAMP : GL11.GL_REPEAT;
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, mipmaps);
if (mipmaps > 0) {
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, mipmaps);
checkGLError("%s: set GL_TEXTURE_MAX_LEVEL = %d", textureName, mipmaps);
if (anisoSupported && anisoLevel > 1) {
GL11.glTexParameterf(
@@ -125,12 +125,12 @@ public class MipmapHelper {
mipmapType.clear();
mipmapType.put("terrain", true);
mipmapType.put("items", false);
PropertiesFile properties = PropertiesFile.get(logger, MIPMAP_PROPERTIES);
final PropertiesFile properties = PropertiesFile.get(logger, MIPMAP_PROPERTIES);
if (properties != null) {
for (Map.Entry<String, String> entry : properties.entrySet()) {
String key = entry.getKey()
final String key = entry.getKey()
.trim();
boolean value = Boolean.parseBoolean(
final boolean value = Boolean.parseBoolean(
entry.getValue()
.trim()
.toLowerCase());
@@ -172,7 +172,7 @@ public class MipmapHelper {
}
static int getMipmapLevelsForCurrentTexture() {
int filter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER);
final int filter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER);
if (filter != GL11.GL_NEAREST_MIPMAP_LINEAR && filter != GL11.GL_NEAREST_MIPMAP_NEAREST) {
return 0;
}
@@ -195,7 +195,7 @@ public class MipmapHelper {
static void scaleHalf(IntBuffer in, int w, int h, IntBuffer out, int rotate) {
for (int i = 0; i < w / 2; i++) {
for (int j = 0; j < h / 2; j++) {
int k = w * 2 * j + 2 * i;
final int k = w * 2 * j + 2 * i;
int pixel00 = in.get(k);
int pixel01 = in.get(k + 1);
int pixel10 = in.get(k + w);
@@ -216,10 +216,10 @@ public class MipmapHelper {
}
private static int average4RGBA(int pixel00, int pixel01, int pixel10, int pixel11) {
int a00 = pixel00 & 0xff;
int a01 = pixel01 & 0xff;
int a10 = pixel10 & 0xff;
int a11 = pixel11 & 0xff;
final int a00 = pixel00 & 0xff;
final int a01 = pixel01 & 0xff;
final int a10 = pixel10 & 0xff;
final int a11 = pixel11 & 0xff;
switch ((a00 << 24) | (a01 << 16) | (a10 << 8) | a11) {
case 0xff000000:
return pixel00;
@@ -256,10 +256,10 @@ public class MipmapHelper {
return average2RGBA(average2RGBA(pixel00, pixel11), average2RGBA(pixel01, pixel10));
default:
int a = a00 + a01 + a10 + a11;
final int a = a00 + a01 + a10 + a11;
int pixel = a >> 2;
for (int i = 8; i < 32; i += 8) {
int average = (a00 * ((pixel00 >> i) & 0xff) + a01 * ((pixel01 >> i) & 0xff)
final int average = (a00 * ((pixel00 >> i) & 0xff) + a01 * ((pixel01 >> i) & 0xff)
+ a10 * ((pixel10 >> i) & 0xff)
+ a11 * ((pixel11 >> i) & 0xff)) / a;
pixel |= (average << i);
@@ -273,9 +273,9 @@ public class MipmapHelper {
}
private static void checkGLError(String format, Object... params) {
int error = GL11.glGetError();
final int error = GL11.glGetError();
if (error != 0) {
String message = GLU.gluErrorString(error) + ": " + String.format(format, params);
final String message = GLU.gluErrorString(error) + ": " + String.format(format, params);
new RuntimeException(message).printStackTrace();
}
}
@@ -50,6 +50,7 @@ public enum Settings {
}
},
DYNAMIC_FOV(new NotFineOptionTickBox(true, null)),
HURT_SHAKE(new NotFineOptionSliderPercentage(100, 0, 300, 5, OptionImpact.LOW)),
DYNAMIC_LIGHTS(new NotFineOptionCycling<>(DynamicLightsMode.FANCY, OptionImpact.VARIES)){
@Override
public void applyChanges() {
@@ -152,6 +152,7 @@ public class SodiumGameOptionPages {
.setBinding((opts, value) -> opts.viewBobbing = value, opts -> opts.viewBobbing)
.build())
.add(Settings.DYNAMIC_FOV.option)
.add(Settings.HURT_SHAKE.option)
.add(Settings.MODE_WATER.option)
.add(Settings.MODE_DROPPED_ITEMS.option)
.build());
@@ -280,6 +281,21 @@ public class SodiumGameOptionPages {
.setImpact(OptionImpact.VARIES)
.setEnabled(GLStateManager.capabilities != null)
.build())
.add(OptionImpl.createBuilder(boolean.class, sodiumOpts)
.setName(I18n.format("sodium.options.use_map_buffer_range.name"))
.setTooltip(I18n.format("sodium.options.use_map_buffer_range.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> opts.advanced.useMapBufferRange = value, opts -> opts.advanced.useMapBufferRange)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.setImpact(OptionImpact.VARIES)
.build())
.add(OptionImpl.createBuilder(boolean.class, sodiumOpts)
.setName(I18n.format("sodium.options.enable_deferred_batching.name"))
.setTooltip(I18n.format("sodium.options.enable_deferred_batching.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> opts.advanced.enableDeferredBatching = value, opts -> opts.advanced.enableDeferredBatching)
.setImpact(OptionImpact.MEDIUM)
.build())
.build());
groups.add(OptionGroup.createBuilder()
@@ -33,6 +33,8 @@ public class SodiumGameOptions {
public boolean useParticleCulling = true;
public boolean allowDirectMemoryAccess = true;
public boolean ignoreDriverBlacklist = false;
public boolean useMapBufferRange = false;
public boolean enableDeferredBatching = true;
}
public static class PerformanceSettings {
@@ -89,6 +91,10 @@ public class SodiumGameOptions {
}
} else {
config = new SodiumGameOptions();
// Lwjgl2 doesn't expose multidraw and actually does individual, so try indirect if available
if (isLwjgl2() && !isMacOS()) {
config.advanced.multiDrawMode = MultiDrawMode.INDIRECT;
}
}
config.configPath = path;
@@ -98,6 +104,7 @@ public class SodiumGameOptions {
config.performance.cpuRenderAheadLimit = 0;
}
// Downgrade INDIRECT if hardware doesn't support it
if (config.advanced.multiDrawMode == MultiDrawMode.INDIRECT && (GLStateManager.capabilities == null || (!GLStateManager.capabilities.OpenGL43 && !GLStateManager.capabilities.GL_ARB_multi_draw_indirect))) {
config.advanced.multiDrawMode = MultiDrawMode.DIRECT;
}
@@ -112,6 +119,14 @@ public class SodiumGameOptions {
return config;
}
private static boolean isLwjgl2() {
return SodiumGameOptions.class.getClassLoader().getResource("org/lwjgl/Sys.class") != null;
}
private static boolean isMacOS() {
return System.getProperty("os.name", "").toLowerCase().contains("mac");
}
public void writeChanges() throws IOException {
final Path dir = this.configPath.getParent();
@@ -1,11 +1,14 @@
package net.coderbot.iris.celeritas;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.BlendModeOverride;
import net.coderbot.iris.gl.blending.BufferBlendOverride;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
import net.coderbot.iris.shaderpack.loading.ProgramId;
import net.coderbot.iris.gl.program.ProgramImages;
import net.coderbot.iris.gl.program.ProgramSamplers;
import net.coderbot.iris.gl.program.ProgramUniforms;
@@ -19,7 +22,10 @@ import net.coderbot.iris.shaderpack.ProgramSource;
import net.coderbot.iris.uniforms.custom.CustomUniforms;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -43,6 +49,7 @@ public class CeleritasTerrainPipeline {
private final EnumMap<PatchShaderType, Optional<String>> sources = new EnumMap<>(PatchShaderType.class);
private GlFramebuffer framebuffer;
private BlendModeOverride blendModeOverride;
private List<BufferBlendOverride> bufferBlendOverrides;
private float alphaReference;
private PassInfo() {
@@ -91,21 +98,41 @@ public class CeleritasTerrainPipeline {
PassInfo passInfo = new PassInfo();
passInfoMap.put(pass, passInfo);
// Set up framebuffer
// Set up framebuffer, blend mode, and buffer blend overrides
final ProgramId programId = switch (pass) {
case GBUFFER_TRANSLUCENT -> ProgramId.Water;
case SHADOW, SHADOW_CUTOUT -> ProgramId.Shadow;
default -> ProgramId.Terrain;
};
if (pass == IrisTerrainPass.SHADOW || pass == IrisTerrainPass.SHADOW_CUTOUT) {
passInfo.framebuffer = shadowFramebuffer;
passInfo.blendModeOverride = programId.getBlendModeOverride();
passInfo.bufferBlendOverrides = Collections.emptyList();
} else if (renderTargets != null) {
ImmutableSet<Integer> flipped = pass == IrisTerrainPass.GBUFFER_TRANSLUCENT ? flippedAfterTranslucent : flippedAfterPrepare;
Optional<ProgramSource> programSource = gbufferProgramSource.get(pass);
programSource.ifPresentOrElse(source -> passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, source.getDirectives().getDrawBuffers()), () -> passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, new int[] {0}));
}
programSource.ifPresentOrElse(source -> {
passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, source.getDirectives().getDrawBuffers());
passInfo.blendModeOverride = source.getDirectives().getBlendModeOverride().orElse(programId.getBlendModeOverride());
// Set blend mode override
passInfo.blendModeOverride = switch (pass) {
case GBUFFER_TRANSLUCENT -> null; // Default blending
default -> BlendModeOverride.OFF;
};
passInfo.bufferBlendOverrides = new ArrayList<>();
source.getDirectives().getBufferBlendOverrides().forEach(information -> {
int index = Ints.indexOf(source.getDirectives().getDrawBuffers(), information.getIndex());
if (index > -1) {
passInfo.bufferBlendOverrides.add(new BufferBlendOverride(index, information.getBlendMode()));
}
});
}, () -> {
passInfo.framebuffer = renderTargets.createGbufferFramebuffer(flipped, new int[] {0});
passInfo.blendModeOverride = programId.getBlendModeOverride();
passInfo.bufferBlendOverrides = Collections.emptyList();
});
} else {
passInfo.blendModeOverride = programId.getBlendModeOverride();
passInfo.bufferBlendOverrides = Collections.emptyList();
}
// Set alpha reference
passInfo.alphaReference = switch (pass) {
@@ -2,6 +2,7 @@ package net.coderbot.iris.celeritas;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.BlendModeOverride;
import net.coderbot.iris.gl.blending.BufferBlendOverride;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.pipeline.transform.PatchShaderType;
import net.coderbot.iris.shadows.ShadowRenderingState;
@@ -15,6 +16,7 @@ import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
import org.jetbrains.annotations.Nullable;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -82,11 +84,11 @@ public class IrisCeleritasChunkProgramOverrides {
builder.bindAttribute(attr.getName(), attrIndex++);
}
// Get blend from PassInfo
final CeleritasTerrainPipeline.PassInfo passInfo = pipeline.getPassInfo(pass);
final BlendModeOverride blendOverride = passInfo.blendModeOverride();
final List<BufferBlendOverride> bufferOverrides = passInfo.bufferBlendOverrides();
return builder.link(context -> new IrisCeleritasChunkShaderInterface(((GlObject) context).handle(), context, pipeline, pass.isShadow(), blendOverride, pipeline.getCustomUniforms()));
return builder.link(context -> new IrisCeleritasChunkShaderInterface(((GlObject) context).handle(), context, pipeline, pass.isShadow(), blendOverride, bufferOverrides, pipeline.getCustomUniforms()));
} finally {
vertShader.delete();
if (geomShader != null) geomShader.delete();
@@ -4,18 +4,17 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.BlendModeOverride;
import net.coderbot.iris.gl.blending.BufferBlendOverride;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.samplers.IrisSamplers;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import net.coderbot.iris.gl.program.ProgramImages;
import net.coderbot.iris.gl.program.ProgramSamplers;
import net.coderbot.iris.gl.program.ProgramUniforms;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.samplers.IrisSamplers;
import net.coderbot.iris.shadows.ShadowRenderingState;
import net.coderbot.iris.uniforms.custom.CustomUniforms;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture;
import org.embeddedt.embeddium.impl.gl.shader.ShaderBindingContext;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat3v;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix3f;
@@ -28,6 +27,10 @@ import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import java.util.List;
public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
@Nullable
@@ -51,13 +54,15 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
// Rendering state
private final BlendModeOverride blendModeOverride;
private final List<BufferBlendOverride> bufferBlendOverrides;
private final boolean hasOverrides;
// Stored matrices for inverse and normal matrix computation
private final Matrix4f projectionMatrixInverse = new Matrix4f();
private final Matrix4f modelViewMatrixInverse = new Matrix4f();
private final Matrix3f normalMatrix = new Matrix3f();
public IrisCeleritasChunkShaderInterface(int handle, ShaderBindingContext context, CeleritasTerrainPipeline pipeline, boolean isShadowPass, BlendModeOverride blendModeOverride, CustomUniforms customUniforms) {
public IrisCeleritasChunkShaderInterface(int handle, ShaderBindingContext context, CeleritasTerrainPipeline pipeline, boolean isShadowPass, BlendModeOverride blendModeOverride, List<BufferBlendOverride> bufferBlendOverrides, CustomUniforms customUniforms) {
this.uniformModelViewMatrix = context.bindUniformIfPresent("iris_ModelViewMatrix", GlUniformMatrix4f::new);
this.uniformModelViewMatrixInverse = context.bindUniformIfPresent("iris_ModelViewMatrixInverse", GlUniformMatrix4f::new);
this.uniformProjectionMatrix = context.bindUniformIfPresent("iris_ProjectionMatrix", GlUniformMatrix4f::new);
@@ -66,6 +71,8 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
this.uniformRegionOffset = context.bindUniformIfPresent("u_RegionOffset", GlUniformFloat3v::new);
this.blendModeOverride = blendModeOverride;
this.bufferBlendOverrides = bufferBlendOverrides;
this.hasOverrides = bufferBlendOverrides != null && !bufferBlendOverrides.isEmpty();
this.customUniforms = customUniforms;
final ProgramUniforms.Builder builder = pipeline.initUniforms(handle);
@@ -94,6 +101,10 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
blendModeOverride.apply();
}
if (hasOverrides) {
bufferBlendOverrides.forEach(BufferBlendOverride::apply);
}
if (irisProgramUniforms != null) {
irisProgramUniforms.update();
}
@@ -109,7 +120,7 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
@Override
public void restoreState() {
if (blendModeOverride != null) {
if (blendModeOverride != null || hasOverrides) {
BlendModeOverride.restore();
}
@@ -119,7 +130,7 @@ public class IrisCeleritasChunkShaderInterface implements ChunkShaderInterface {
@Override
public GlPrimitiveType getPrimitiveType() {
return GlPrimitiveType.QUADS;
return GlPrimitiveType.TRIANGLES;
}
@Override
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff. Show more