Compare commits

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

No files matched your search

+4 -3
View File
@@ -72,7 +72,7 @@ dependencies {
// Iris Shaders
compileOnly('org.jetbrains:annotations:26.0.2')
api("com.github.GTNewHorizons:GTNHLib:0.9.33:dev")
api("com.github.GTNewHorizons:GTNHLib:0.9.10:dev")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
@@ -118,8 +118,9 @@ dependencies {
compileOnly(rfg.deobf("curse.maven:security-craft-64760:2818228"))
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
// For testing alternative splash screens — https://github.com/MalTeeez/ModernSplash/releases
// devOnlyNonPublishable("com.github.MalTeeez:modernsplash:1.7.10-1.2.4:dev")
// 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")))
// Hodgepodge
transformedMod("net.industrial-craft:industrialcraft-2:2.2.828-experimental:dev")
-13
View File
@@ -22,18 +22,5 @@ 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()
}
@@ -11,6 +11,7 @@ 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;
@@ -26,8 +27,9 @@ public class ModStatus {
public static boolean isBaublesLoaded;
public static boolean isFluidLoggedLoaded;
public static void preInit() {
if (Loader.isModLoaded("backhand")) {
public static void preInit(){
isBackhandLoaded = Loader.isModLoaded("backhand");
if(isBackhandLoaded) {
isBackhandLoaded = BackhandReflectionCompat.isBackhandLoaded();
}
@@ -2,7 +2,6 @@ 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;
@@ -10,10 +9,14 @@ public enum InteractionHand {
MAIN_HAND,
OFF_HAND;
public ItemStack getItemInHand(EntityPlayer player) {
if (ModStatus.isBackhandLoaded && this == InteractionHand.OFF_HAND) { // off hand (requires backhand)
return BackhandReflectionCompat.getOffhandItem(player);
} else { // main 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
return player.getHeldItem();
}
}
@@ -112,7 +112,7 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean optimizeWorldUpdateLight;
@Config.Comment("Optimize Texture Animations. [From Hodgepodge]")
@Config.Comment("Speedup Animations. [From Hodgepodge]")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean speedupAnimations;
@@ -235,8 +235,4 @@ 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;
}
@@ -6,7 +6,6 @@ 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;
@@ -553,7 +552,7 @@ public class DynamicLights {
}
}
else if (ModStatus.isBackhandLoaded){
ItemStack offhand = BackhandReflectionCompat.getOffhandItem(player);
ItemStack offhand = ModStatus.backhandCompat.getOffhandItem(player);
if (offhand != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(offhand));
}
@@ -51,8 +51,7 @@ public class CompatShaderTransformer {
"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"
"shadow2D", "gl_FrontLightModelProduct"
);
private static final Pattern NEEDS_TRANSFORM_PATTERN = Pattern.compile(
@@ -158,28 +157,6 @@ public class CompatShaderTransformer {
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").
@@ -1,10 +1,8 @@
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;
@@ -43,62 +41,28 @@ public class CompatUniformManager {
static final int LOC_SCENE_COLOR = 15;
static final int LOC_CLIP_PLANES = 16;
static final int LOC_CLIP_PLANES_ENABLED = 17;
static final int LOC_COUNT = 18;
// 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[] UNIFORM_NAMES = {
"angelica_ModelViewMatrix",
"angelica_ModelViewMatrixInverse",
"angelica_ProjectionMatrix",
"angelica_ProjectionMatrixInverse",
"angelica_NormalMatrix",
"angelica_LightmapTextureMatrix",
"angelica_FogDensity",
"angelica_FogStart",
"angelica_FogEnd",
"angelica_FogColor",
"iris_ModelViewMatrix",
"iris_ProjectionMatrix",
"iris_NormalMatrix",
"iris_LightmapTextureMatrix",
"angelica_currentAlphaTest",
"angelica_SceneColor",
"angelica_ClipPlane[0]",
"angelica_ClipPlanesEnabled",
};
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<>();
@@ -174,13 +138,13 @@ public class CompatUniformManager {
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);
// Lighting-derived uniforms (scene color) — skip if lighting state unchanged
if (locs[LOC_SCENE_COLOR] != -1) {
final int litGen = GLStateManager.lightingGeneration;
if (programChanged || litGen != lastLightingGen) {
lastLightingGen = litGen;
uploadSceneColor(locs);
}
}
// Clip plane equations + enabled bool — uploaded when enable state or equations change
@@ -313,75 +277,6 @@ public class CompatUniformManager {
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);
@@ -299,7 +299,8 @@ 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)
, GLStateManager.texEnvMode // GL_TEXTURE_ENV_MODE
, GLStateManager.texEnvColor // GL_TEXTURE_ENV_COLOR
// Enable bits for the four texture coordinates
// Border color for each texture image
@@ -313,10 +314,10 @@ public class Feature {
// Current texture bindings (for example, GL_TEXTURE_BINDING_2D) - Below
));
// Current Texture Bindings - GL_TEXTURE_BINDING_2D + per-unit TexEnvState
// Current Texture Bindings - GL_TEXTURE_BINDING_2D
for(int i = 0 ; i < GLStateManager.MAX_TEXTURE_UNITS; i++) {
textureAttribs.add(GLStateManager.textures.getTextureUnitBindings(i));
textureAttribs.add(GLStateManager.textures.getTexEnvState(i));
// textureAttribs.add(GLStateManager.textures.getInfo(i))
}
// Enable bits GL_TEXTURE_GEN_x where x is S, T, R, or Q
@@ -1,388 +0,0 @@
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,7 +620,6 @@ 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) {
@@ -629,28 +628,20 @@ public final class GLDebug {
@Override
public void pushGroup(String name) {
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++;
if (depth > maxDepth) {
throw new RuntimeException("Stack overflow");
}
KHRDebug.glPushDebugGroup(KHRDebug.GL_DEBUG_SOURCE_APPLICATION, ID, name);
}
@Override
public void popGroup() {
if (depth - 1 < 0) {
depth--;
if (depth < 0) {
throw new RuntimeException("Stack underflow");
}
KHRDebug.glPopDebugGroup();
depth--;
groupStack[depth] = null;
}
@Override
@@ -42,7 +42,6 @@ import com.gtnewhorizons.angelica.glsm.texture.TextureTracker;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
import cpw.mods.fml.relauncher.ReflectionHelper;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntStack;
@@ -180,10 +179,6 @@ public class GLStateManager {
// Highest texture unit index that has ever had a non-zero binding; limits onDeleteTexture scan range
private static int maxBoundTextureUnit = 0;
// Deferred texture deletion: names kept valid until glGenTextures recycles them.
// Emulates Mesa/compat-profile behavior where delete-then-bind doesn't error.
private static final IntOpenHashSet deferredDeleteTextures = new IntOpenHashSet();
@Getter private static Vendor VENDOR;
// This setting varies depending on driver, so it gets queried at runtime
@@ -306,6 +301,10 @@ public class GLStateManager {
@Getter protected static final AlphaStateStack alphaState = new AlphaStateStack();
@Getter protected static final BooleanStateStack alphaTest = new BooleanStateStack(GL11.GL_ALPHA_TEST, false, true);
// Texture environment mode (GL_MODULATE default per OpenGL spec)
@Getter protected static final IntegerStateStack texEnvMode = new IntegerStateStack(GL11.GL_MODULATE);
// Texture environment color (used by GL_BLEND mode)
@Getter protected static final Color4Stack texEnvColor = new Color4Stack(new Color4(0.0f, 0.0f, 0.0f, 0.0f));
@Getter protected static final BooleanStateStack lightingState = new BooleanStateStack(GL11.GL_LIGHTING, false, true);
@Getter protected static final BooleanStateStack rescaleNormalState = new BooleanStateStack(GL12.GL_RESCALE_NORMAL, false, true);
@@ -415,10 +414,6 @@ public class GLStateManager {
@Getter protected static int boundVBO;
@Getter protected static int boundEBO;
@Getter protected static int boundVAO;
private static final Int2IntOpenHashMap vaoEboMap = new Int2IntOpenHashMap();
static {
vaoEboMap.defaultReturnValue(0);
}
@Getter private static int defaultVAO; // Non-zero on core profile
public static void reset() {
@@ -440,7 +435,6 @@ public class GLStateManager {
modelViewMatrix.clear();
projectionMatrix.clear();
vaoEboMap.clear();
}
/**
@@ -969,12 +963,14 @@ public class GLStateManager {
case GL30.GL_DRAW_FRAMEBUFFER_BINDING -> drawFramebuffer;
case GL30.GL_READ_FRAMEBUFFER_BINDING -> readFramebuffer;
default -> switch (pname) {
case GL11.GL_FOG_MODE -> fogState.getFogMode();
case GL11.GL_LINE_STIPPLE_PATTERN -> lineState.getStipplePattern() & 0xFFFF;
case GL11.GL_LINE_STIPPLE_REPEAT -> lineState.getStippleFactor();
default -> GL11.glGetInteger(pname);
};
default -> {
yield switch (pname) {
case GL11.GL_FOG_MODE -> fogState.getFogMode();
case GL11.GL_LINE_STIPPLE_PATTERN -> lineState.getStipplePattern() & 0xFFFF;
case GL11.GL_LINE_STIPPLE_REPEAT -> lineState.getStippleFactor();
default -> GL11.glGetInteger(pname);
};
}
};
}
@@ -1050,7 +1046,10 @@ public class GLStateManager {
params.put((float) viewportState.depthRangeNear);
params.put((float) viewportState.depthRangeFar);
}
case GL14.GL_BLEND_COLOR -> params.put(blendState.getBlendColorR()).put(blendState.getBlendColorG()).put(blendState.getBlendColorB()).put(blendState.getBlendColorA());
case GL14.GL_BLEND_COLOR -> {
params.put(blendState.getBlendColorR()).put(blendState.getBlendColorG())
.put(blendState.getBlendColorB()).put(blendState.getBlendColorA());
}
case GL11.GL_CURRENT_NORMAL -> {
final Vector3f normal = ShaderManager.getCurrentNormal();
params.put(normal.x).put(normal.y).put(normal.z);
@@ -1642,11 +1641,6 @@ public class GLStateManager {
return;
}
}
// Rebinding a deferred-delete texture rescues it (Mesa/compat-profile behavior)
if (texture != 0) {
deferredDeleteTextures.remove(texture);
}
if (target != GL11.GL_TEXTURE_2D) {
// We're only supporting 2D textures for now
GL11.glBindTexture(target, texture);
@@ -1673,6 +1667,46 @@ public class GLStateManager {
}
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromByteBuffer(target, level, internalformat, width, height, border, format, type, pixels));
if (mode == RecordMode.COMPILE) {
return;
}
}
final int texture = getBoundTextureForServerState();
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
TextureInfoCache.INSTANCE.onTexImage2D(texture, target, level, internalformat, width, height);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, ShortBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromShortBuffer(target, level, internalformat, width, height, border, format, type, pixels));
if (mode == RecordMode.COMPILE) {
return;
}
}
final int texture = getBoundTextureForServerState();
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
TextureInfoCache.INSTANCE.onTexImage2D(texture, target, level, internalformat, width, height);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
@@ -1681,13 +1715,15 @@ public class GLStateManager {
return;
}
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
final int texture = getBoundTextureForServerState();
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
TextureInfoCache.INSTANCE.onTexImage2D(texture, target, level, internalformat, width, height);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
RenderSystem.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
@@ -1699,9 +1735,16 @@ public class GLStateManager {
return;
}
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
// FloatBuffer not in DSA interface - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
final int texture = getBoundTextureForServerState();
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
TextureInfoCache.INSTANCE.onTexImage2D(texture, target, level, internalformat, width, height);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, DoubleBuffer pixels) {
@@ -1712,26 +1755,15 @@ public class GLStateManager {
return;
}
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
// DoubleBuffer not in DSA interface - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromByteBuffer(target, level, internalformat, width, height, border, format, type, pixels));
if (mode == RecordMode.COMPILE) {
return;
}
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : null);
final int texture = getBoundTextureForServerState();
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
TextureInfoCache.INSTANCE.onTexImage2D(texture, target, level, internalformat, width, height);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
@@ -1739,8 +1771,15 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glTexImage2D with buffer offset in display lists not yet supported");
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
final int texture = getBoundTextureForServerState();
TextureInfoCache.INSTANCE.onTexImage2D(texture, target, level, internalformat, width, height);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
public static void glTexCoord1f(float s) {
@@ -1809,69 +1848,17 @@ public class GLStateManager {
}
public static void glDeleteTextures(int id) {
deferDeleteTexture(id);
onDeleteTexture(id);
GL11.glDeleteTextures(id);
}
public static void glDeleteTextures(IntBuffer ids) {
for(int i = 0; i < ids.remaining(); i++) {
final int id = ids.get(ids.position() + i);
deferDeleteTexture(id);
onDeleteTexture(id);
}
}
/** Shrink texture to 1x1 to free GPU memory, unbind from all units, but keep the name valid. */
private static void deferDeleteTexture(int id) {
if (id == 0) return;
final int savedUnit = GLStateManager.activeTextureUnit.getValue();
final int savedBinding = textures.getTextureUnitBindings(savedUnit).getBinding();
boolean changedUnit = false;
// Unbind from all units that have this texture
for (int i = 0; i <= maxBoundTextureUnit; i++) {
if (textures.getTextureUnitBindings(i).getBinding() == id) {
if (i != savedUnit) {
GL13.glActiveTexture(GL13.GL_TEXTURE0 + i);
changedUnit = true;
}
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
}
onDeleteTexture(ids.get(i));
}
// Restore active unit, then shrink the texture via a temporary bind
if (changedUnit) {
GL13.glActiveTexture(GL13.GL_TEXTURE0 + savedUnit);
}
GL11.glBindTexture(GL11.GL_TEXTURE_2D, id);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, 1, 1, 0, GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
// Restore previous binding on active unit
GL11.glBindTexture(GL11.GL_TEXTURE_2D, savedBinding == id ? 0 : savedBinding);
deferredDeleteTextures.add(id);
}
/** Flush deferred deletes so the driver can recycle names. */
private static void flushDeferredTextureDeletes() {
if (deferredDeleteTextures.isEmpty()) return;
final var it = deferredDeleteTextures.iterator();
while (it.hasNext()) {
GL11.glDeleteTextures(it.nextInt());
}
deferredDeleteTextures.clear();
}
public static int glGenTextures() {
flushDeferredTextureDeletes();
return GL11.glGenTextures();
}
public static void glGenTextures(IntBuffer textures) {
flushDeferredTextureDeletes();
GL11.glGenTextures(textures);
GL11.glDeleteTextures(ids);
}
public static void enableTexture() {
@@ -2013,10 +2000,6 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices);
return;
}
ShaderManager.getInstance().preDraw();
GL11.glDrawElements(mode, indices);
}
@@ -2025,10 +2008,6 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices);
return;
}
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
@@ -2041,10 +2020,6 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices);
return;
}
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
@@ -2057,10 +2032,6 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, count, type, indices);
return;
}
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
@@ -2081,10 +2052,6 @@ public class GLStateManager {
return;
}
}
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices_count, type, indices_buffer_offset);
return;
}
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset);
@@ -2117,10 +2084,6 @@ public class GLStateManager {
}
return;
}
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawArrays(mode, first, count);
return;
}
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadsAsTriangles(first, count);
@@ -2141,31 +2104,38 @@ public class GLStateManager {
}
public static void glVertexPointer(int size, int stride, FloatBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glVertexPointer(int size, int type, int stride, ByteBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
}
public static void glVertexPointer(int size, int type, int stride, long pointer_buffer_offset) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer_buffer_offset);
}
public static void glColorPointer(int size, int stride, FloatBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_FLOAT, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glColorPointer(int size, int type, int stride, ByteBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
}
public static void glColorPointer(int size, boolean unsigned, int stride, ByteBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
final int type = unsigned ? GL11.GL_UNSIGNED_BYTE : GL11.GL_BYTE;
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
}
public static void glColorPointer(int size, int type, int stride, long pointer_buffer_offset) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer_buffer_offset);
}
@@ -2182,26 +2152,32 @@ public class GLStateManager {
}
public static void glNormalPointer(int type, int stride, ByteBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
}
public static void glNormalPointer(int type, int stride, FloatBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_FLOAT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, ShortBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_SHORT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, IntBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_INT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, long pointer_buffer_offset) {
if (!ShaderManager.getInstance().isActive()) return;
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer_buffer_offset);
}
public static void glTexCoordPointer(int size, int type, int stride, ByteBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, type, false, stride, pointer);
@@ -2220,30 +2196,35 @@ public class GLStateManager {
}
public static void glTexCoordPointer(int size, int type, int stride, FloatBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, ShortBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, GL11.GL_SHORT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, IntBuffer pointer) {
if (!ShaderManager.getInstance().isActive()) return;
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, GL11.GL_INT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, long pointer_buffer_offset) {
if (!ShaderManager.getInstance().isActive()) return;
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, type, false, stride, pointer_buffer_offset);
}
public static void glEnableClientState(int cap) {
if (!ShaderManager.getInstance().isActive()) return;
final int location = clientStateToAttributeLocation(cap);
if (location >= 0) GL20.glEnableVertexAttribArray(location);
final int flag = clientStateToVertexFlag(cap);
@@ -2251,6 +2232,7 @@ public class GLStateManager {
}
public static void glDisableClientState(int cap) {
if (!ShaderManager.getInstance().isActive()) return;
final int location = clientStateToAttributeLocation(cap);
if (location >= 0) GL20.glDisableVertexAttribArray(location);
final int flag = clientStateToVertexFlag(cap);
@@ -2717,10 +2699,12 @@ public class GLStateManager {
PBRTextureManager.INSTANCE.onDeleteTexture(id);
}
// Always delete
for (int i = 0; i <= maxBoundTextureUnit; i++) {
if (textures.getTextureUnitBindings(i).getBinding() == id) {
textures.getTextureUnitBindings(i).setBinding(0);
// Only update cached texture bindings on main thread
if (isCachingEnabled()) {
for (int i = 0; i <= maxBoundTextureUnit; i++) {
if (textures.getTextureUnitBindings(i).getBinding() == id) {
textures.getTextureUnitBindings(i).setBinding(0);
}
}
}
}
@@ -2815,11 +2799,8 @@ public class GLStateManager {
public static void glCallList(int list) {
GLDebug.pushGroup("glCallList " + list);
try {
DisplayListManager.glCallList(list);
} finally {
GLDebug.popGroup();
}
DisplayListManager.glCallList(list);
GLDebug.popGroup();
}
public static void pushState(int mask) {
@@ -2935,7 +2916,6 @@ public class GLStateManager {
if ((mask & GL11.GL_LIGHTING_BIT) != 0 && lightingGeneration != savedLightingGen[depth]) lightingGeneration++;
if ((mask & GL11.GL_FOG_BIT) != 0 && fragmentGeneration != savedFragmentGen[depth]) fragmentGeneration++;
if ((mask & GL11.GL_COLOR_BUFFER_BIT) != 0 && fragmentGeneration != savedFragmentGen[depth]) fragmentGeneration++; // alpha ref
if ((mask & GL11.GL_TEXTURE_BIT) != 0 && fragmentGeneration != savedFragmentGen[depth]) fragmentGeneration++; // texenv state
if ((mask & GL11.GL_TRANSFORM_BIT) != 0) {
if (mvGeneration != savedMvGen[depth]) mvGeneration++;
if (projGeneration != savedProjGen[depth]) projGeneration++;
@@ -2981,12 +2961,9 @@ public class GLStateManager {
GLDebug.popGroup();
poppingAttributes = true;
GLDebug.pushGroup("popState");
try {
popState();
} finally {
GLDebug.popGroup();
poppingAttributes = false;
}
popState();
GLDebug.popGroup();
poppingAttributes = false;
}
// Matrix Operations
@@ -3971,12 +3948,31 @@ public class GLStateManager {
return;
}
}
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
}
public static void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ShortBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexSubImage2DCmd.fromShortBuffer(target, level, xoffset, yoffset, width, height, format, type, pixels));
if (mode == RecordMode.COMPILE) {
return;
}
}
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
}
@@ -3988,12 +3984,49 @@ public class GLStateManager {
return;
}
}
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
}
public static void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, FloatBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexSubImage2DCmd.fromFloatBuffer(target, level, xoffset, yoffset, width, height, format, type, pixels));
if (mode == RecordMode.COMPILE) {
return;
}
}
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
}
public static void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, DoubleBuffer pixels) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexSubImage2DCmd.fromDoubleBuffer(target, level, xoffset, yoffset, width, height, format, type, pixels));
if (mode == RecordMode.COMPILE) {
return;
}
}
final long pixels_buffer_offset = pixels != null ? MemoryUtilities.memAddress(pixels) : 0L;
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
}
@@ -4001,7 +4034,13 @@ public class GLStateManager {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glTexSubImage2D with buffer offset in display lists not yet supported");
}
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
} else {
// Non-main thread or proxy texture - use direct GL call
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
}
public static void glTexSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int width, int height, int depth, int format, int type, ByteBuffer pixels) {
@@ -4460,7 +4499,6 @@ public class GLStateManager {
boundVBO = buffer;
} else if (target == GL15.GL_ELEMENT_ARRAY_BUFFER) {
boundEBO = buffer;
vaoEboMap.put(boundVAO, buffer);
}
GL15.glBindBuffer(target, buffer);
}
@@ -4476,7 +4514,6 @@ public class GLStateManager {
}
if (boundVAO != array) {
boundVAO = array;
boundEBO = vaoEboMap.get(array);
UniversalVAO.bindVertexArray(array);
if (ShaderManager.getInstance().isEnabled()) {
ShaderManager.getInstance().onBindVertexArray(array);
@@ -4486,11 +4523,9 @@ public class GLStateManager {
public static void glDeleteVertexArrays(int array) {
ShaderManager.getInstance().onDeleteVertexArray(array);
vaoEboMap.remove(array);
if (array == boundVAO) {
// Deleting the bound VAO implicitly unbinds it. Rebind the default VAO.
boundVAO = defaultVAO;
boundEBO = vaoEboMap.get(defaultVAO);
UniversalVAO.bindVertexArray(defaultVAO);
}
UniversalVAO.deleteVertexArrays(array);
@@ -4566,24 +4601,11 @@ public class GLStateManager {
public static void glMultiTexCoord2f(int target, float s, float t) {
if (target == GL13.GL_TEXTURE0) {
if (DisplayListManager.isRecording() || ImmediateModeRecorder.isDrawing()) {
ImmediateModeRecorder.setTexCoord(s, t);
return;
}
ShaderManager.setCurrentTexCoord(s, t, 0.0f, 1.0f);
dirtyTexCoordAttrib = true;
} else if (target == GL13.GL_TEXTURE1) {
if (DisplayListManager.isRecording() || ImmediateModeRecorder.isDrawing()) {
ImmediateModeRecorder.setLightmapCoord(s, t);
return;
}
setLightmapTextureCoords(target, s, t);
}
// Units 2+ silently ignored — shader uses v_TexCoord0 for all non-lightmap units
}
public static void glMultiTexCoord2d(int target, double s, double t) {
glMultiTexCoord2f(target, (float) s, (float) t);
}
public static void glMultiTexCoord2s(int target, short s, short t) {
@@ -4656,6 +4678,8 @@ public class GLStateManager {
}
}
private static final String TEXENV_COMBINE_MSG = "Texture environment combine mode is not supported in FFP emulation.";
public static void glTexEnvi(int target, int pname, int param) {
handleTexEnvScalar(target, pname, param);
}
@@ -4667,31 +4691,24 @@ public class GLStateManager {
public static void glTexEnv(int target, int pname, FloatBuffer params) {
if (target == GL11.GL_TEXTURE_ENV && pname == GL11.GL_TEXTURE_ENV_COLOR && params.remaining() >= 4) {
final int pos = params.position();
final var envState = textures.getTexEnvState(activeTextureUnit.getValue());
envState.envColorR = params.get(pos);
envState.envColorG = params.get(pos + 1);
envState.envColorB = params.get(pos + 2);
envState.envColorA = params.get(pos + 3);
texEnvColor.setRed(params.get(pos));
texEnvColor.setGreen(params.get(pos + 1));
texEnvColor.setBlue(params.get(pos + 2));
texEnvColor.setAlpha(params.get(pos + 3));
fragmentGeneration++;
return;
}
if (target == GL14.GL_TEXTURE_FILTER_CONTROL) {
guardUnsupportedFFP("glTexEnv", "LOD bias via buffer glTexEnv is not supported; use glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, value) instead.");
return;
}
// Silently ignore other buffer glTexEnv calls (some mods pass combine params via buffers)
guardTexEnvTarget(target, "glTexEnv");
}
public static void glTexEnv(int target, int pname, IntBuffer params) {
if (target == GL11.GL_TEXTURE_ENV) {
handleTexEnvScalar(target, pname, (float) params.get(params.position()));
}
guardTexEnvTarget(target, "glTexEnv");
}
/**
* Scalar glTexEnvi/glTexEnvf dispatch. glTexEnv is removed in core profile.
* GL_TEXTURE_FILTER_CONTROL (LOD bias) is remapped to glTexParameterf.
* All GL_TEXTURE_ENV parameters including GL_COMBINE sub-params are tracked in per-unit TexEnvState.
* GL_TEXTURE_ENV_MODE values (MODULATE, REPLACE, ADD, DECAL, BLEND) are tracked in cache.
*/
private static void handleTexEnvScalar(int target, int pname, float param) {
if (target == GL14.GL_TEXTURE_FILTER_CONTROL) {
@@ -4699,82 +4716,27 @@ public class GLStateManager {
glTexParameterf(GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_LOD_BIAS, param);
return;
}
if (target != GL11.GL_TEXTURE_ENV) return;
final var envState = textures.getTexEnvState(activeTextureUnit.getValue());
final int iparam = (int) param;
switch (pname) {
case GL11.GL_TEXTURE_ENV_MODE -> {
envState.mode = iparam;
if (target == GL11.GL_TEXTURE_ENV && pname == GL11.GL_TEXTURE_ENV_MODE) {
final int mode = (int) param;
if (mode == GL11.GL_MODULATE || mode == GL11.GL_REPLACE
|| mode == GL11.GL_ADD || mode == GL11.GL_DECAL || mode == GL11.GL_BLEND) {
texEnvMode.setValue(mode);
fragmentGeneration++;
return;
}
case GL13.GL_COMBINE_RGB -> {
envState.combineRgb = iparam;
fragmentGeneration++;
}
case GL13.GL_COMBINE_ALPHA -> {
envState.combineAlpha = iparam;
fragmentGeneration++;
}
case GL13.GL_SOURCE0_RGB -> {
envState.sourceRgb[0] = iparam;
fragmentGeneration++;
}
case GL13.GL_SOURCE1_RGB -> {
envState.sourceRgb[1] = iparam;
fragmentGeneration++;
}
case GL13.GL_SOURCE2_RGB -> {
envState.sourceRgb[2] = iparam;
fragmentGeneration++;
}
case GL13.GL_SOURCE0_ALPHA -> {
envState.sourceAlpha[0] = iparam;
fragmentGeneration++;
}
case GL13.GL_SOURCE1_ALPHA -> {
envState.sourceAlpha[1] = iparam;
fragmentGeneration++;
}
case GL13.GL_SOURCE2_ALPHA -> {
envState.sourceAlpha[2] = iparam;
fragmentGeneration++;
}
case GL13.GL_OPERAND0_RGB -> {
envState.operandRgb[0] = iparam;
fragmentGeneration++;
}
case GL13.GL_OPERAND1_RGB -> {
envState.operandRgb[1] = iparam;
fragmentGeneration++;
}
case GL13.GL_OPERAND2_RGB -> {
envState.operandRgb[2] = iparam;
fragmentGeneration++;
}
case GL13.GL_OPERAND0_ALPHA -> {
envState.operandAlpha[0] = iparam;
fragmentGeneration++;
}
case GL13.GL_OPERAND1_ALPHA -> {
envState.operandAlpha[1] = iparam;
fragmentGeneration++;
}
case GL13.GL_OPERAND2_ALPHA -> {
envState.operandAlpha[2] = iparam;
fragmentGeneration++;
}
case GL13.GL_RGB_SCALE -> {
envState.scaleRgb = param;
fragmentGeneration++;
}
case GL11.GL_ALPHA_SCALE -> {
envState.scaleAlpha = param;
fragmentGeneration++;
}
// Silently ignore unknown pname — other mods may pass unrecognized params
}
guardUnsupportedFFP("glTexEnv", TEXENV_COMBINE_MSG);
}
/**
* Guard for buffer glTexEnv variants. glTexEnv is removed in core profile.
*/
private static void guardTexEnvTarget(int target, String function) {
if (target == GL14.GL_TEXTURE_FILTER_CONTROL) {
guardUnsupportedFFP(function, "LOD bias via buffer glTexEnv is not supported; use glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, value) instead.");
return;
}
guardUnsupportedFFP(function, TEXENV_COMBINE_MSG);
}
// TexGen generation counter for FFP uniform dirty tracking
@@ -4871,31 +4833,6 @@ public class GLStateManager {
guardUnsupportedFFP("glAccum", "The accumulation buffer is not available in GL 3.3 core profile.");
}
public static void glFeedbackBuffer(int type, FloatBuffer buffer) {
FeedbackManager.glFeedbackBuffer(type, buffer);
}
public static int glRenderMode(int mode) {
return FeedbackManager.glRenderMode(mode);
}
public static void glPassThrough(float token) {
FeedbackManager.glPassThrough(token);
}
private static boolean selectWarned = false;
public static void glSelectBuffer(IntBuffer buffer) {
if (!selectWarned) {
LOGGER.warn("glSelectBuffer: selection mode not emulated");
selectWarned = true;
}
}
public static void glInitNames() { /* no-op: ignored outside GL_SELECT */ }
public static void glPushName(int name) { /* no-op */ }
public static void glPopName() { /* no-op */ }
public static void glLoadName(int name) { /* no-op */ }
public static void glLinkProgram(int program) {
if (ShaderManager.getInstance().isEnabled()) {
generateVertexShaderIfNeeded(program);
@@ -84,13 +84,11 @@ public final class QuadConverter {
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);
}
/**
@@ -147,10 +147,7 @@ public class RenderSystem {
public static void texImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
GLStateManager.glBindTexture(target, texture);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
if (target == GL11.GL_TEXTURE_2D && level == 0) {
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
public static void uniformMatrix4fv(int location, boolean transpose, FloatBuffer matrix) {
@@ -215,20 +212,12 @@ public class RenderSystem {
dsaState.texParameterf(texture, target, pname, param);
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
}
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) {
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
@@ -16,26 +16,16 @@ public class DSAARB extends DSAUnsupported {
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
// Note: DSA glTextureImage2D doesn't exist in ARB_direct_state_access
// We need to use glTextureStorage2D + glTextureSubImage2D, or fall back to bind-based approach
// For simplicity, use the fallback which binds, uploads, and restores
super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels);
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) {
ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
}
@Override
@@ -8,10 +8,8 @@ public interface DSAAccess {
void generateMipmaps(int texture, int target);
// Texture image upload - DSA allows specifying texture directly without binding
void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels);
void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels);
void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels);
void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels);
void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset);
void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset);
void texParameteri(int texture, int target, int pname, int param);
void texParameterf(int texture, int target, int pname, float param);
@@ -2,17 +2,9 @@ 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);
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
EXTDirectStateAccess.glTextureImage2DEXT(texture, target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
}
}
@@ -11,7 +11,6 @@ import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL42;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -24,36 +23,19 @@ public class DSAUnsupported implements DSAAccess {
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
// Get what cache thinks is bound - we'll restore to this to keep cache/GL in sync
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GL11.glBindTexture(target, cachedBinding); // Restore to cache value
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset);
if(cachedBinding != texture) GL11.glBindTexture(target, cachedBinding);
}
@Override
public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
GL11.glBindTexture(target, cachedBinding);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
GL11.glBindTexture(target, cachedBinding);
}
@Override
public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
final int cachedBinding = GLStateManager.getBoundTextureForServerState();
GL11.glBindTexture(target, texture);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
GL11.glBindTexture(target, cachedBinding);
GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
if(cachedBinding != texture) GL11.glBindTexture(target, cachedBinding);
}
@Override
@@ -1,92 +0,0 @@
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)
}
}
@@ -1,173 +0,0 @@
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);
}
}
@@ -1,337 +1,133 @@
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
* Compact key capturing the fragment shader permutation axes for FFP emulation.
* Packed into a single long for fast hash/compare.
*/
public final class FragmentKey {
private 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;
private final long packed;
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;
private static final int BIT_FOG_MODE = 0;
private static final int BIT_ALPHA_TEST = 2;
private static final int BIT_ALPHA_FUNC = 3;
private static final int BIT_SEPARATE_SPECULAR = 6;
private static final int BIT_TEXTURE = 7;
private static final int BIT_LIGHTMAP = 8;
private static final int BIT_TEX_ENV_MODE = 9; // 3 bits: 0=MODULATE, 1=REPLACE, 2=ADD, 3=DECAL, 4=BLEND
public static final int FOG_NONE = 0;
public static final int FOG_LINEAR = 1;
public static final int FOG_EXP = 2;
public static final int FOG_EXP2 = 3;
private 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) {
private FragmentKey(long packed) {
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;
public long pack() { return packed; }
public int fogMode() { return (int)(packed & 0x3); }
public boolean alphaTestEnabled() { return ((packed >> BIT_ALPHA_TEST) & 1) != 0; }
public int alphaTestFunc() { return (int)((packed >> BIT_ALPHA_FUNC) & 0x7); }
public boolean separateSpecular() { return ((packed >> BIT_SEPARATE_SPECULAR) & 1) != 0; }
public boolean textureEnabled() { return ((packed >> BIT_TEXTURE) & 1) != 0; }
public boolean lightmapEnabled() { return ((packed >> BIT_LIGHTMAP) & 1) != 0; }
public int texEnvMode() { return (int)((packed >> BIT_TEX_ENV_MODE) & 0x7); }
public static final int TEX_ENV_MODULATE = 0;
public static final int TEX_ENV_REPLACE = 1;
public static final int TEX_ENV_ADD = 2;
public static final int TEX_ENV_DECAL = 3;
public static final int TEX_ENV_BLEND = 4;
public static long packFromState() {
long bits = 0;
// Fog
if (GLStateManager.getFogMode().isEnabled()) {
final int fogMode = switch (GLStateManager.getFogState().getFogMode()) {
final int glFogMode = GLStateManager.getFogState().getFogMode();
final int fogMode = switch (glFogMode) {
case GL11.GL_LINEAR -> FOG_LINEAR;
case GL11.GL_EXP -> FOG_EXP;
case GL11.GL_EXP2 -> FOG_EXP2;
default -> FOG_NONE;
};
global |= (long) fogMode;
bits |= ((long) fogMode & 0x3);
}
// Alpha test
if (GLStateManager.getAlphaTest().isEnabled()) {
global |= (1L << BIT_ALPHA_TEST);
global |= ((long) (encodeAlphaFunc(GLStateManager.getAlphaState().getFunction()) & 0x7)) << BIT_ALPHA_FUNC;
bits |= (1L << BIT_ALPHA_TEST);
final int func = encodeAlphaFunc(GLStateManager.getAlphaState().getFunction());
bits |= ((long) func & 0x7) << BIT_ALPHA_FUNC;
}
// Separate specular
if (GLStateManager.getLightingState().isEnabled()
&& GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
global |= (1L << BIT_SEPARATE_SPECULAR);
bits |= (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;
// Texture unit 0
if (GLStateManager.getTextures().getTextureUnitStates(0).isEnabled()) {
bits |= (1L << BIT_TEXTURE);
final long unitBits = packUnit(i, texEnabled);
if (i == 0) {
unit0Bits = unitBits;
} else {
out[i] = unitBits;
}
// Tex env mode only matters when texture is enabled
final int envMode = encodeTexEnvMode(GLStateManager.getTexEnvMode().getValue());
bits |= ((long) envMode & 0x7) << BIT_TEX_ENV_MODE;
}
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);
// Texture unit 1 (lightmap)
if (GLStateManager.getTextures().getTextureUnitStates(1).isEnabled()) {
bits |= (1L << BIT_LIGHTMAP);
}
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);
return new FragmentKey(packFromState());
}
@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);
static FragmentKey fromPacked(long packed) {
return new FragmentKey(packed);
}
private static int encodeAlphaFunc(int glFunc) {
return glFunc & 0x7;
}
static int encodeTexEnvMode(int glMode) {
private static int encodeTexEnvMode(int glMode) {
return switch (glMode) {
case GL11.GL_MODULATE -> TEX_ENV_MODULATE;
case GL11.GL_REPLACE -> TEX_ENV_REPLACE;
case GL11.GL_ADD -> TEX_ENV_ADD;
case GL11.GL_DECAL -> TEX_ENV_DECAL;
case GL11.GL_BLEND -> TEX_ENV_BLEND;
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 int hashCode() {
return Long.hashCode(packed);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof FragmentKey other)) return false;
return packed == other.packed;
}
@Override
public String toString() {
final String fogName = switch (fogMode()) {
@@ -341,28 +137,17 @@ public final class FragmentKey {
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(),
final String envName = switch (texEnvMode()) {
case TEX_ENV_MODULATE -> "MOD";
case TEX_ENV_REPLACE -> "REPL";
case TEX_ENV_ADD -> "ADD";
case TEX_ENV_DECAL -> "DEC";
case TEX_ENV_BLEND -> "BL";
default -> "?";
};
return String.format("FFPFragmentKey[0x%03X: fog=%s alpha=%b(%s) specSep=%b tex=%b(env=%s) lm=%b]",
packed, fogName, alphaTestEnabled(),
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-",
separateSpecular(), 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();
separateSpecular(), textureEnabled(), envName, lightmapEnabled());
}
}
@@ -4,7 +4,6 @@ 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 {
@@ -12,7 +11,7 @@ public final class FragmentShaderGenerator {
private FragmentShaderGenerator() {}
public static String generate(FragmentKey key) {
final StringBuilder sb = new StringBuilder(2048);
final StringBuilder sb = new StringBuilder(1024);
sb.append("#version 330 core\n\n");
emitInputs(sb, key);
@@ -21,7 +20,7 @@ public final class FragmentShaderGenerator {
sb.append("void main() {\n");
emitTextureSampling(sb, key);
emitTexEnvChain(sb, key);
emitColorCombine(sb, key);
emitSpecularAdd(sb, key);
emitAlphaTest(sb, key);
emitFog(sb, key);
@@ -50,20 +49,17 @@ public final class FragmentShaderGenerator {
}
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.textureEnabled()) {
sb.append("uniform sampler2D u_Sampler0;\n");
}
if (key.lightmapEnabled()) {
sb.append("uniform sampler2D u_Sampler1;\n");
}
if (key.alphaTestEnabled()) {
sb.append("uniform float u_AlphaRef;\n");
}
// 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.textureEnabled() && key.texEnvMode() == FragmentKey.TEX_ENV_BLEND) {
sb.append("uniform vec4 u_TexEnvColor;\n");
}
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("uniform vec4 u_FogParams;\n");
@@ -73,191 +69,46 @@ public final class FragmentShaderGenerator {
}
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");
if (key.textureEnabled()) {
sb.append(" vec4 texColor = texture(u_Sampler0, v_TexCoord0.st);\n");
}
if (key.lightmapEnabled()) {
sb.append(" vec4 lightmapColor = texture(u_Sampler1, v_TexCoord1.st);\n");
}
}
private static void emitTexEnvChain(StringBuilder sb, FragmentKey key) {
private static void emitColorCombine(StringBuilder sb, FragmentKey key) {
if (!key.textureEnabled()) {
sb.append(" // No texture — vertex color only\n");
sb.append(" vec4 color = v_Color;\n");
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");
final int envMode = key.texEnvMode();
switch (envMode) {
case FragmentKey.TEX_ENV_REPLACE -> {
sb.append(" // Color combine (REPLACE)\n");
sb.append(" vec4 color = texColor;\n");
}
case FragmentKey.TEX_ENV_ADD -> {
sb.append(" // Color combine (ADD)\n");
sb.append(" vec4 color = vec4(clamp(v_Color.rgb + texColor.rgb, 0.0, 1.0), v_Color.a * texColor.a);\n");
}
case FragmentKey.TEX_ENV_DECAL -> {
sb.append(" // Color combine (DECAL)\n");
sb.append(" vec4 color = vec4(mix(v_Color.rgb, texColor.rgb, texColor.a), v_Color.a);\n");
}
case FragmentKey.TEX_ENV_BLEND -> {
sb.append(" // Color combine (BLEND)\n");
sb.append(" vec4 color = vec4(v_Color.rgb * (vec3(1.0) - texColor.rgb) + u_TexEnvColor.rgb * texColor.rgb, v_Color.a * texColor.a);\n");
}
default -> {
sb.append(" // Color combine (MODULATE)\n");
sb.append(" vec4 color = v_Color * texColor;\n");
}
}
}
}
/**
* 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;
if (key.lightmapEnabled()) {
sb.append(" color *= lightmapColor;\n");
}
// 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) {
@@ -10,15 +10,12 @@ 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;
@@ -83,9 +80,10 @@ public class Program {
public int locClipPlanes = -1;
// Fragment uniforms
public final int[] locSampler = { -1, -1, -1, -1 };
public int locSampler0 = -1;
public int locSampler1 = -1;
public int locAlphaRef = -1;
public final int[] locTexEnvColor = { -1, -1, -1, -1 };
public int locTexEnvColor = -1;
public int locFogParams = -1;
public int locFogColor = -1;
@@ -152,13 +150,10 @@ public class Program {
locClipPlanes = loc("u_ClipPlane[0]");
// Fragment
for (int i = 0; i < 4; i++) {
locSampler[i] = loc("u_Sampler" + i);
}
locSampler0 = loc("u_Sampler0");
locSampler1 = loc("u_Sampler1");
locAlphaRef = loc("u_AlphaRef");
for (int i = 0; i < 4; i++) {
locTexEnvColor[i] = loc("u_TexEnvColor" + i);
}
locTexEnvColor = loc("u_TexEnvColor");
locFogParams = loc("u_FogParams");
locFogColor = loc("u_FogColor");
}
@@ -175,9 +170,8 @@ public class Program {
* 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 fs = compileShader(GL20.GL_FRAGMENT_SHADER, fragSrc, "ffp_f_" + Long.toHexString(fk.pack()));
final int program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
@@ -186,7 +180,7 @@ public class 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);
LOGGER.warn("FFP program link log (vk=0x{}, fk=0x{}): {}", Long.toHexString(vk.pack()), Long.toHexString(fk.pack()), log);
}
final int linkStatus = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
@@ -194,10 +188,10 @@ public class Program {
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);
throw new RuntimeException("FFP shader link failed (vk=0x" + Long.toHexString(vk.pack()) + ", fk=0x" + Long.toHexString(fk.pack()) + "): " + log);
}
final String debugName = "FFP(v=0x" + Long.toHexString(vk.pack()) + ",f=" + id + ")";
final String debugName = "FFP(v=0x" + Long.toHexString(vk.pack()) + ",f=0x" + Long.toHexString(fk.pack()) + ")";
GLDebug.nameObject(KHRDebug.GL_PROGRAM, program, debugName);
// Detach and delete individual shaders — they're linked into the program
@@ -210,9 +204,8 @@ public class Program {
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);
}
if (ffpProgram.locSampler0 != -1) GL20.glUniform1i(ffpProgram.locSampler0, 0);
if (ffpProgram.locSampler1 != -1) GL20.glUniform1i(ffpProgram.locSampler1, 1);
GL20.glUseProgram(previousProgram);
return ffpProgram;
@@ -9,218 +9,75 @@ 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>
* Cache for FFP shader program variants. Keyed on (vertexKey, fragmentKey) pairs.
*/
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<>();
private final Long2ObjectOpenHashMap<String> fragmentSourceCache = new Long2ObjectOpenHashMap<>();
private final Long2ObjectOpenHashMap<Program> programCache = 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;
private long lastCombinedKey = Long.MIN_VALUE;
private Program lastProgram = null;
@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)
* Get or create an FFP program for the given packed state keys.
*/
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)) {
public Program getOrCreate(long vkPacked, long fkPacked) {
final long combinedKey = (fkPacked << 32) | (vkPacked & 0xFFFFFFFFL);
if (combinedKey == lastCombinedKey && lastProgram != null) {
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;
}
Program program = programCache.get(combinedKey);
if (program != null) {
lastCombinedKey = combinedKey;
lastProgram = program;
if (count >= (capacity * 3) / 4) {
grow();
return getOrCreate(vkPacked, fkPacked, fkLen);
return program;
}
final VertexKey vk = VertexKey.fromPacked(vkPacked);
final FragmentKey fk = FragmentKey.fromPacked(fkPacked);
final String vertSrc = getOrGenerateVertex(vk);
final String fragSrc = getOrGenerateFragment(fk);
final FragmentKey fk = FragmentKey.fromPacked(fkPacked, fkLen);
String fragSrc = findFragSource(fkPacked, fkLen);
if (fragSrc == null) {
fragSrc = FragmentShaderGenerator.generate(fk);
uniqueFragCount++;
}
program = Program.create(vk, fk, vertSrc, fragSrc);
programCache.put(combinedKey, program);
lastCombinedKey = combinedKey;
lastProgram = program;
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);
LOGGER.debug("FFP variant compiled: {} + {} (total: {} programs)", vk, fk, programCache.size());
// Debug dump
if (dumpDir != null) {
dumpShader(Long.toHexString(vkPacked), ".vert.glsl", vertSrc);
dumpShader("frag_" + fragmentDumpCounter++, ".frag.glsl", fragSrc);
dumpShader(vkPacked, ".vert.glsl", vertSrc);
dumpShader(fkPacked, ".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) {
private String getOrGenerateFragment(FragmentKey fk) {
return fragmentSourceCache.computeIfAbsent(fk.pack(), k -> FragmentShaderGenerator.generate(fk));
}
private void dumpShader(long key, String suffix, String source) {
if (dumpDir == null) return;
try {
Files.createDirectories(dumpDir);
final Path file = dumpDir.resolve("ffp_" + name + suffix);
final Path file = dumpDir.resolve("ffp_" + Long.toHexString(key) + suffix);
if (!Files.exists(file)) {
Files.writeString(file, source, StandardCharsets.UTF_8);
}
@@ -229,48 +86,41 @@ public class ShaderCache {
}
}
/** Dump all cached shaders to disk. */
/**
* Dump all cached shaders to disk. Useful for debugging.
*/
public void dumpAll(Path outputDir) {
try {
Files.createDirectories(outputDir);
for (var entry : vertexSourceCache.entrySet()) {
dumpShader(Long.toHexString(entry.getKey()), ".vert.glsl", entry.getValue());
dumpShader(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]);
}
for (var entry : fragmentSourceCache.entrySet()) {
dumpShader(entry.getKey(), ".frag.glsl", entry.getValue());
}
LOGGER.info("Dumped {} vertex and {} fragment FFP shader sources to {}",
vertexSourceCache.size(), seen.size(), outputDir);
LOGGER.info("Dumped {} vertex and {} fragment FFP shader sources to {}", vertexSourceCache.size(), fragmentSourceCache.size(), outputDir);
} catch (IOException e) {
LOGGER.warn("Failed to dump FFP shaders: {}", e.getMessage());
}
}
/** Destroy all cached programs and clear caches. */
/**
* Destroy all cached programs and clear caches.
*/
public void destroy() {
for (int i = 0; i < capacity; i++) {
if (programs[i] != null) {
programs[i].destroy();
}
for (Program program : programCache.values()) {
program.destroy();
}
Arrays.fill(keys, 0);
Arrays.fill(programs, null);
Arrays.fill(fragSources, null);
count = 0;
uniqueFragCount = 0;
programCache.clear();
vertexSourceCache.clear();
fragmentSourceCache.clear();
lastProgram = null;
lastVK = Long.MIN_VALUE;
lastCombinedKey = Long.MIN_VALUE;
LOGGER.debug("FFP shader cache cleared");
}
public int getProgramCount() {
return count;
return programCache.size();
}
public int getVertexVariantCount() {
@@ -278,6 +128,6 @@ public class ShaderCache {
}
public int getFragmentVariantCount() {
return uniqueFragCount;
return fragmentSourceCache.size();
}
}
@@ -1,7 +1,5 @@
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;
@@ -37,9 +35,7 @@ public class ShaderManager {
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;
private long currentFragmentKeyPacked = Long.MIN_VALUE;
@Getter private static final Vector3f currentNormal = new Vector3f(0.0f, 0.0f, 1.0f);
@Getter private static final Vector4f currentTexCoord = new Vector4f(0.0f, 0.0f, 0.0f, 1.0f);
@@ -71,7 +67,10 @@ public class ShaderManager {
VertexFormat.registerSetupBufferStateOverride((format, offset) -> {
currentVertexFlags = format.getVertexFlags();
vaoVertexFlags.put(GLStateManager.getBoundVAO(), currentVertexFlags);
final int boundVao = GLStateManager.getBoundVAO();
if (boundVao != 0) {
vaoVertexFlags.put(boundVao, currentVertexFlags);
}
return false;
});
@@ -92,7 +91,7 @@ public class ShaderManager {
active = false;
currentProgram = null;
currentVertexKeyPacked = Long.MIN_VALUE;
currentFKLen = 0;
currentFragmentKeyPacked = Long.MIN_VALUE;
}
@@ -115,10 +114,14 @@ public class ShaderManager {
}
final long vkPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
final int fkLen = FragmentKey.packFromState(currentFKScratch);
final long fkPacked = FragmentKey.packFromState();
if (vkPacked != currentVertexKeyPacked || !Arrays.equals(currentFKScratch, 0, fkLen, currentFKPacked, 0, currentFKLen)) {
commitVariant(vkPacked, fkLen);
if (vkPacked != currentVertexKeyPacked || fkPacked != currentFragmentKeyPacked) {
currentVertexKeyPacked = vkPacked;
currentFragmentKeyPacked = fkPacked;
currentProgram = cache.getOrCreate(vkPacked, fkPacked);
GL20.glUseProgram(currentProgram.getProgramId());
}
uploadUniforms();
@@ -139,16 +142,10 @@ public class ShaderManager {
}
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);
}
currentVertexKeyPacked = VertexKey.packFromState(hasColor, hasNormal, hasTexCoord, hasLightmap);
currentFragmentKeyPacked = FragmentKey.packFromState();
private void commitVariant(long vkPacked, int fkLen) {
currentVertexKeyPacked = vkPacked;
System.arraycopy(currentFKScratch, 0, currentFKPacked, 0, fkLen);
currentFKLen = fkLen;
currentProgram = cache.getOrCreate(vkPacked, currentFKPacked, currentFKLen);
currentProgram = cache.getOrCreate(currentVertexKeyPacked, currentFragmentKeyPacked);
GL20.glUseProgram(currentProgram.getProgramId());
}
@@ -4,12 +4,10 @@ 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;
@@ -47,7 +45,6 @@ public class TessellatorStreamingDrawer {
private static int repackCapacity;
private static boolean initialized = false;
private static UploadStrategy orphanStrategy = UploadStrategy.BUFFER_SUB_DATA;
static {
// Initial repack buffer: 64KB
@@ -59,14 +56,7 @@ public class TessellatorStreamingDrawer {
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")) {
if (RenderSystem.supportsBufferStorage()) {
try {
persistentBuffer = new PersistentStreamingBuffer();
LOGGER.info("Persistent streaming buffer created ({}MB)", PersistentStreamingBuffer.DEFAULT_CAPACITY / (1024 * 1024));
@@ -106,7 +96,26 @@ public class TessellatorStreamingDrawer {
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
ensureVAO(flags, format);
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(repackBuffer, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(repackBuffer, vertexSize);
firstVertex = 0;
}
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(tess.drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
// Shrink rawBuffer if oversized
if (tess.rawBufferSize > 0x20000 && tess.rawBufferIndex < (tess.rawBufferSize << 3)) {
@@ -134,22 +143,31 @@ public class TessellatorStreamingDrawer {
final ByteBuffer buffer = dt.getWriteBuffer();
final int vertexSize = format.getVertexSize();
uploadAndDraw(buffer, flags, format, vertexSize, drawMode, vertexCount);
}
ensureVAO(flags, format);
private static String cachedDebugInfo = "Stream: not initialized";
private static long lastDebugUpdateNanos;
private static final long DEBUG_UPDATE_INTERVAL_NS = 500_000_000L; // 500ms
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(buffer, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(buffer, vertexSize);
firstVertex = 0;
}
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
}
public static String getDebugInfo() {
if (!initialized) return "Stream: not initialized";
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++) {
@@ -160,83 +178,23 @@ public class TessellatorStreamingDrawer {
}
if (persistentBuffer != null) {
cachedDebugInfo = String.format("Stream: Persistent %s (%s free) + %d orphan (%s)",
return 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;
return String.format("Stream: Orphan (%d bufs, %s)", orphanCount, formatBytes(orphanBytes));
}
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);
if (bytes >= 1024 * 1024) return String.format("%dMB", bytes / (1024 * 1024));
if (bytes >= 1024) return String.format("%dKB", bytes / 1024);
return bytes + "B";
}
public static void endFrame() {
if (persistentBuffer != null) {
persistentBuffer.postDraw();
}
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) {
@@ -265,7 +223,7 @@ public class TessellatorStreamingDrawer {
init();
if (orphanVAOs[flags] == 0) {
orphanBuffers[flags] = new OrphanStreamingBuffer(orphanStrategy);
orphanBuffers[flags] = new OrphanStreamingBuffer();
orphanVAOs[flags] = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
@@ -398,18 +398,16 @@ public class Uniforms {
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);
}
// Tex env color (only used by GL_BLEND mode)
if (program.locTexEnvColor != -1) {
final var envColor = GLStateManager.getTexEnvColor();
vec4Buf.clear();
vec4Buf.put(envColor.getRed());
vec4Buf.put(envColor.getGreen());
vec4Buf.put(envColor.getBlue());
vec4Buf.put(envColor.getAlpha());
vec4Buf.flip();
GL20.glUniform4(program.locTexEnvColor, vec4Buf);
}
// Fog
@@ -59,14 +59,6 @@ public final class ImmediateModeRecorder {
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));
}
/**
* Set the current normal vector.
* Called by GLStateManager.glNormal*.
@@ -11,6 +11,7 @@ import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
/**
* Command: glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels)
@@ -26,18 +27,35 @@ public record TexImage2DCmd(
int border,
int format,
int type,
@Nullable byte[] pixelData
byte @Nullable [] pixelData
) implements DisplayListCommand {
/**
* Create command from ShortBuffer
*/
public static TexImage2DCmd fromShortBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ShortBuffer pixels) {
byte[] data = null;
if (pixels != null) {
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 2; // 2 bytes per short
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asShortBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data);
}
/**
* Create command from IntBuffer
*/
public static TexImage2DCmd fromIntBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per int
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per int
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asIntBuffer().put(pixels);
@@ -52,9 +70,9 @@ public record TexImage2DCmd(
public static TexImage2DCmd fromFloatBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable FloatBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per float
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per float
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asFloatBuffer().put(pixels);
@@ -69,9 +87,9 @@ public record TexImage2DCmd(
public static TexImage2DCmd fromDoubleBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable DoubleBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 8; // 8 bytes per double
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 8; // 8 bytes per double
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asDoubleBuffer().put(pixels);
@@ -86,9 +104,9 @@ public record TexImage2DCmd(
public static TexImage2DCmd fromByteBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = limit - pos;
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = limit - pos;
data = new byte[size];
pixels.get(data);
pixels.position(pos); // Restore position
@@ -109,7 +127,7 @@ public record TexImage2DCmd(
@Override
public @NotNull String toString() {
String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
final String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
return String.format("TexImage2D(target=%s, level=%d, internalformat=%s, %dx%d, border=%d, format=%s, type=%s, data=%s)",
GLDebug.getTextureTargetName(target), level, GLDebug.getTextureFormatName(internalformat),
width, height, border, GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr);
@@ -11,6 +11,7 @@ import java.nio.ByteOrder;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
/**
* Command: glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels)
@@ -26,18 +27,35 @@ public record TexSubImage2DCmd(
int height,
int format,
int type,
@Nullable byte[] pixelData
byte @Nullable [] pixelData
) implements DisplayListCommand {
/**
* Create command from ShortBuffer
*/
public static TexSubImage2DCmd fromShortBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ShortBuffer pixels) {
byte[] data = null;
if (pixels != null) {
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 2; // 2 bytes per short
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asShortBuffer().put(pixels);
pixels.position(pos); // Restore position
}
return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data);
}
/**
* Create command from IntBuffer
*/
public static TexSubImage2DCmd fromIntBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable IntBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per int
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per int
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asIntBuffer().put(pixels);
@@ -52,9 +70,9 @@ public record TexSubImage2DCmd(
public static TexSubImage2DCmd fromFloatBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable FloatBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 4; // 4 bytes per float
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 4; // 4 bytes per float
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asFloatBuffer().put(pixels);
@@ -69,9 +87,9 @@ public record TexSubImage2DCmd(
public static TexSubImage2DCmd fromDoubleBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable DoubleBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = (limit - pos) * 8; // 8 bytes per double
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = (limit - pos) * 8; // 8 bytes per double
data = new byte[size];
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder());
bb.asDoubleBuffer().put(pixels);
@@ -86,9 +104,9 @@ public record TexSubImage2DCmd(
public static TexSubImage2DCmd fromByteBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ByteBuffer pixels) {
byte[] data = null;
if (pixels != null) {
int pos = pixels.position();
int limit = pixels.limit();
int size = limit - pos;
final int pos = pixels.position();
final int limit = pixels.limit();
final int size = limit - pos;
data = new byte[size];
pixels.get(data);
pixels.position(pos); // Restore position
@@ -109,7 +127,7 @@ public record TexSubImage2DCmd(
@Override
public @NotNull String toString() {
String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
final String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null";
return String.format("TexSubImage2D(target=%s, level=%d, offset=(%d,%d), %dx%d, format=%s, type=%s, data=%s)",
GLDebug.getTextureTargetName(target), level, xoffset, yoffset, width, height,
GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr);
@@ -29,7 +29,6 @@ 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;
@@ -61,7 +60,6 @@ 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;
@@ -182,7 +180,6 @@ public class LightState implements ISettableState<LightState> {
public void setSpotCutoff(float f) {
if (GLStateManager.shouldBypassCache() || Float.compare(f, this.spotCutoff) != 0) {
this.spotCutoff = f;
this.spotCosCutoff = (float) Math.cos(Math.toRadians(f));
}
}
@@ -249,7 +246,6 @@ 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;
@@ -1,110 +0,0 @@
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;
}
}
@@ -18,7 +18,6 @@ public class TextureUnitArray {
private final TextureUnitBooleanStateStack[] texGenRStates;
private final TextureUnitBooleanStateStack[] texGenQStates;
private final TexGenState[] texGenStates;
private final TexEnvState[] texEnvStates;
@Getter
public final Matrix4fStack[] textureMatricies;
@@ -32,7 +31,6 @@ public class TextureUnitArray {
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++) {
@@ -45,7 +43,6 @@ public class TextureUnitArray {
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);
}
}
@@ -86,10 +83,6 @@ public class TextureUnitArray {
return texGenStates[index];
}
public TexEnvState getTexEnvState(int index) {
return texEnvStates[index];
}
public Matrix4fStack getTextureUnitMatrix(int index) {
return textureMatricies[index];
}
@@ -1,111 +1,34 @@
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>
* Streaming buffer using the classic orphan pattern
*/
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;
final int requiredBytes = data.remaining();
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);
if (requiredBytes > capacity) {
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
capacity = requiredBytes;
} 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.glBufferData(GL15.GL_ARRAY_BUFFER, capacity, GL15.GL_STREAM_DRAW);
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0, data);
}
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);
return 0;
}
@Override
@@ -120,8 +43,7 @@ public class OrphanStreamingBuffer implements StreamingBuffer {
@Override
public void postDraw() {
writePos = 0;
needsOrphan = true;
// No-op
}
@Override
@@ -131,12 +53,4 @@ public class OrphanStreamingBuffer implements StreamingBuffer {
bufferId = 0;
}
}
int getWritePos() {
return writePos;
}
UploadStrategy getStrategy() {
return strategy;
}
}
@@ -3,10 +3,8 @@ package com.gtnewhorizons.angelica.glsm.texture;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.opengl.GL11;
import java.nio.Buffer;
import java.util.concurrent.locks.ReentrantLock;
public class TextureInfoCache {
@@ -40,19 +38,9 @@ public class TextureInfoCache {
}
}
public void onTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable Buffer pixels) {
public void onTexImage2D(int texture, int target, int level, int internalformat, int width, int height) {
if (target == GL11.GL_TEXTURE_2D && level == 0) {
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.internalFormat = internalformat;
info.width = width;
info.height = height;
}
}
public void onTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
if (target == GL11.GL_TEXTURE_2D && level == 0) {
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
final TextureInfo info = getInfo(texture);
if (info == null) return;
info.internalFormat = internalformat;
info.width = width;
@@ -30,12 +30,6 @@ public class CeleritasBlockTransformer implements IClassTransformer {
if (transformedName.startsWith(exclusion)) 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();
cr.accept(cn, 0);
@@ -48,6 +48,10 @@ 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());
}
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.loading.rfb.transformers;
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
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;
@@ -45,23 +44,7 @@ public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
public boolean shouldTransformClass(@NotNull ExtensibleClassLoader classLoader,
@NotNull RfbClassTransformer.Context context, @Nullable Manifest manifest, @NotNull String className,
@NotNull ClassNodeHandle classNode) {
if (!classNode.isPresent()) {
return false;
}
ClassHeaderMetadata metadata = classNode.getOriginalMetadata();
if (metadata == null) {
return false;
}
if (inner.shouldTransform(classNode.getOriginalBytes())) {
return true;
}
// Track possible block subclasses even if we don't need to transform this class
inner.trackBlockSubclasses(metadata.binaryThisName, metadata.binarySuperName);
return false;
return classNode.isPresent();
}
@Override
@@ -67,7 +67,7 @@ public final class AngelicaRedirector {
"initializeTextures", "func_77474_a"
);
private static final Map<String, Map<String, String>> methodRedirects = new HashMap<>(32);
private static final Map<String, Map<String, String>> methodRedirects = new HashMap<>();
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;
@@ -106,7 +106,6 @@ public final class AngelicaRedirector {
.add("glColorMaterial")
.add("glDeleteLists")
.add("glGenLists")
.add("glGenTextures")
.add("glIsList")
.add("glDeleteTextures")
.add("glDepthFunc")
@@ -239,14 +238,6 @@ public final class AngelicaRedirector {
.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")
@@ -258,7 +249,6 @@ public final class AngelicaRedirector {
.add("glSampleCoverage")
.add("glClientActiveTexture")
.add("glMultiTexCoord2f")
.add("glMultiTexCoord2d")
.add("glMultiTexCoord2s")
);
methodRedirects.put(GL14, RedirectMap.newMap()
@@ -408,12 +398,22 @@ public final class AngelicaRedirector {
.add("glBindVertexArray")
);
final List<String> stringsToSearch = new ArrayList<>(32);
final List<String> stringsToSearch = new ArrayList<>();
stringsToSearch.add(GL11);
stringsToSearch.add(GL12);
stringsToSearch.add(GL13);
stringsToSearch.add(GL14);
stringsToSearch.add(GL20);
stringsToSearch.add(OpenGlHelper);
stringsToSearch.add(EXTBlendFunc);
stringsToSearch.add(ARBMultiTexture);
stringsToSearch.add(ARBShaderObjects);
stringsToSearch.add(Project);
stringsToSearch.add(UniversalVAO);
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());
stringsToSearch.add(entry.getKey() + "C");
@@ -427,12 +427,13 @@ 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);
return cstPoolParser.find(basicClass, true);
}
/** @return Was the class changed? */
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.loading.shared.transformers;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
import net.minecraft.launchwrapper.Launch;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
@@ -18,6 +17,7 @@ import org.objectweb.asm.tree.MethodNode;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
public final class CeleritasBlockTransform {
private final ClassConstantPoolParser cstPoolParser;
private final List<String> blockFieldNames = new ArrayList<>();
private final Map<String, String> blockFieldRedirects = new HashMap<>();
public CeleritasBlockTransform(boolean isObf) {
@@ -41,10 +41,9 @@ public final class CeleritasBlockTransform {
);
for (Pair<String, String> pair : mappings) {
final String name = isObf ? pair.getRight() : pair.getLeft();
this.blockFieldNames.add(name);
this.blockFieldRedirects.put(name, pair.getLeft());
}
this.cstPoolParser = new ClassConstantPoolParser(this.blockFieldRedirects.keySet().toArray(new String[0]));
}
private static final boolean LOG_SPAM = Boolean.getBoolean("angelica.redirectorLogspam");
@@ -107,34 +106,22 @@ public final class CeleritasBlockTransform {
"org.lwjgl",
"com.gtnewhorizons.angelica.glsm.",
"com.gtnewhorizons.angelica.transform",
"com.gtnewhorizon.gtnhlib.asm.",
"me.eigenraven.lwjgl3ify"
};
}
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 (!isCeleritasEnabled()) {
return false;
if ("net.minecraft.block.Block".equals(transformedName) && isCeleritasEnabled()) {
cn.fields.removeIf(field -> blockFieldNames.stream().anyMatch(name -> field.name.equals(name)));
}
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);
}
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
@@ -145,7 +132,7 @@ public final class CeleritasBlockTransform {
} else {
// Check if we declare any known field names
for (FieldNode field : cn.fields) {
if (blockFieldRedirects.containsKey(field.name)) {
if (blockFieldNames.contains(field.name)) {
doWeShadow = true;
break;
}
@@ -157,6 +144,11 @@ public final class CeleritasBlockTransform {
}
}
if (!isCeleritasEnabled()) {
return false;
}
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) {
@@ -108,11 +108,6 @@ 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)
@@ -352,11 +347,6 @@ 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)
@@ -142,7 +142,6 @@ 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;
@@ -219,17 +218,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
}
}
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 Material correctMaterial = selectMaterial(material, sprite, isShaderPackOverride);
final var builder = buffers.get(correctMaterial);
if (correctMaterial != material && builder.getEncoder() instanceof IrisExtendedChunkVertexEncoder iris) {
@@ -4,24 +4,19 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraftforge.client.ForgeHooksClient;
public final class RenderPassHelper {
private static final class PassIntHolder {
int pass = -1;
}
private static final ThreadLocal<PassIntHolder> threadLocalPass = ThreadLocal.withInitial(PassIntHolder::new);
private static final ThreadLocal<Integer> threadLocalPass = ThreadLocal.withInitial(() -> -1);
private RenderPassHelper() {}
public static int getWorldRenderPass() {
return GLStateManager.isMainThread() ? ForgeHooksClient.worldRenderPass : threadLocalPass.get().pass;
return GLStateManager.isMainThread() ? ForgeHooksClient.worldRenderPass : threadLocalPass.get();
}
public static void setWorldRenderPass(int pass) {
if (GLStateManager.isMainThread()) {
ForgeHooksClient.worldRenderPass = pass;
} else {
threadLocalPass.get().pass = pass;
threadLocalPass.set(pass);
}
}
@@ -71,13 +71,13 @@ public class MipmapHelper {
}
static void setupTexture(int width, int height, boolean blur, boolean clamp, String textureName) {
final int mipmaps = useMipmapsForTexture(textureName) ? getMipmapLevels(width, height, 1) : 0;
int mipmaps = useMipmapsForTexture(textureName) ? getMipmapLevels(width, height, 1) : 0;
logger.finer("setupTexture(%s) %dx%d %d mipmaps", textureName, width, height, mipmaps);
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);
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;
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);
final PropertiesFile properties = PropertiesFile.get(logger, MIPMAP_PROPERTIES);
PropertiesFile properties = PropertiesFile.get(logger, MIPMAP_PROPERTIES);
if (properties != null) {
for (Map.Entry<String, String> entry : properties.entrySet()) {
final String key = entry.getKey()
String key = entry.getKey()
.trim();
final boolean value = Boolean.parseBoolean(
boolean value = Boolean.parseBoolean(
entry.getValue()
.trim()
.toLowerCase());
@@ -172,7 +172,7 @@ public class MipmapHelper {
}
static int getMipmapLevelsForCurrentTexture() {
final int filter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER);
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++) {
final int k = w * 2 * j + 2 * i;
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) {
final int a00 = pixel00 & 0xff;
final int a01 = pixel01 & 0xff;
final int a10 = pixel10 & 0xff;
final int a11 = pixel11 & 0xff;
int a00 = pixel00 & 0xff;
int a01 = pixel01 & 0xff;
int a10 = pixel10 & 0xff;
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:
final int a = a00 + a01 + a10 + a11;
int a = a00 + a01 + a10 + a11;
int pixel = a >> 2;
for (int i = 8; i < 32; i += 8) {
final int average = (a00 * ((pixel00 >> i) & 0xff) + a01 * ((pixel01 >> i) & 0xff)
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) {
final int error = GL11.glGetError();
int error = GL11.glGetError();
if (error != 0) {
final String message = GLU.gluErrorString(error) + ": " + String.format(format, params);
String message = GLU.gluErrorString(error) + ": " + String.format(format, params);
new RuntimeException(message).printStackTrace();
}
}
@@ -281,21 +281,6 @@ 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,8 +33,6 @@ 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 {
@@ -91,10 +89,6 @@ 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;
@@ -104,7 +98,6 @@ 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;
}
@@ -119,14 +112,6 @@ 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();
@@ -6,7 +6,6 @@ import com.gtnewhorizons.angelica.compat.mojang.InteractionHand;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.rendering.RenderingState;
import com.gtnewhorizons.angelica.rendering.celeritas.BlockRenderLayer;
import lombok.Getter;
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
import net.coderbot.iris.layer.GbufferPrograms;
import net.irisshaders.iris.api.v0.IrisApi;
@@ -27,7 +26,7 @@ public class HandRenderer {
public static final HandRenderer INSTANCE = new HandRenderer();
private boolean ACTIVE;
private @Getter boolean renderingSolid;
private boolean renderingSolid;
public static final float DEPTH = 0.125F;
private void setupGlState(RenderGlobal gameRenderer, Camera camera, float tickDelta) {
@@ -183,4 +182,8 @@ public class HandRenderer {
public boolean isActive() {
return ACTIVE;
}
public boolean isRenderingSolid() {
return renderingSolid;
}
}
@@ -10,15 +10,10 @@ public class ExternallyManagedUniforms {
public static void addExternallyManagedUniforms(UniformHolder uniformHolder) {
addMat4(uniformHolder, "iris_ModelViewMatrix");
addMat4(uniformHolder, "iris_ModelViewMatrixInverse");
addMat4(uniformHolder, "iris_ProjectionMatrix");
addMat4(uniformHolder, "iris_ProjectionMatrixInverse");
addMat4(uniformHolder, "iris_LightmapTextureMatrix");
addMat4(uniformHolder, "u_ModelViewProjectionMatrix");
addMat3(uniformHolder, "iris_NormalMatrix");
uniformHolder.externallyManagedUniform("heavyFog", UniformType.BOOL);
uniformHolder.externallyManagedUniform("angelica_ClipPlanesEnabled", UniformType.BOOL);
uniformHolder.externallyManagedUniform("angelica_ClipPlane[0]", UniformType.VEC4);
}
public static void addExternallyManagedUniforms116(UniformHolder uniformHolder) {
@@ -26,7 +21,6 @@ public class ExternallyManagedUniforms {
uniformHolder.externallyManagedUniform("u_ModelScale", UniformType.VEC3);
uniformHolder.externallyManagedUniform("u_TextureScale", UniformType.VEC2);
uniformHolder.externallyManagedUniform("u_RegionOffset", UniformType.VEC3);
}
private static void addMat3(UniformHolder uniformHolder, String name) {
@@ -18,8 +18,8 @@ public class BuiltinReplacementUniforms {
public static void addBuiltinReplacementUniforms(UniformHolder uniforms) {
uniforms.uniformMatrix(UniformUpdateFrequency.ONCE, "iris_LightmapTextureMatrix", () -> {
Iris.logger.debug("A shader appears to require the lightmap texture matrix even after transformations have occurred");
Iris.logger.debug("Iris handles this correctly but it indicates that the shader is doing weird things with lightmap coordinates");
Iris.logger.warn("A shader appears to require the lightmap texture matrix even after transformations have occurred");
Iris.logger.warn("Iris handles this correctly but it indicates that the shader is doing weird things with lightmap coordinates");
return lightmapTextureMatrix;
});
@@ -2,7 +2,7 @@ name=Angelica
version=${version}
additionalVersions=
className=com.gtnewhorizons.angelica.loading.rfb.AngelicaRfbPlugin
transformerExclusions=com.gtnewhorizons.angelica.loading.shared;com.gtnewhorizon.gtnhlib.asm
transformerExclusions=com.gtnewhorizons.angelica.loading.shared
versionConstraints=
loadBefore=
loadAfter=mixin; lwjgl3ify
@@ -232,7 +232,3 @@ sodium.options.multidraw_mode.tooltip=Controls how draw commands are dispatched
sodium.options.multidraw_mode.direct=Direct
sodium.options.multidraw_mode.indirect=Indirect
sodium.options.multidraw_mode.individual=Individual
sodium.options.use_map_buffer_range.name=Use MapBufferRange
sodium.options.use_map_buffer_range.tooltip=Uses glMapBufferRange instead of glBufferSubData for streaming vertex uploads. May improve performance on some drivers.
sodium.options.enable_deferred_batching.name=Deferred Particle Batching
sodium.options.enable_deferred_batching.tooltip=Batches particle draw calls into fewer merged draws per layer. Disable if particles render incorrectly.
@@ -6,7 +6,6 @@ import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.LWJGLUtil;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@@ -52,12 +51,9 @@ public abstract class MixinForgeHooksClient_CoreProfile {
throw new RuntimeException("Failed to obtain ContextAttribs version setters", e);
}
final int startMajor = 4;
final int startMaxMinor = (LWJGLUtil.getPlatform() == LWJGLUtil.PLATFORM_MACOSX) ? 1 : 6;
Exception cur_exception = null;
for (int major = startMajor; major >= 3; --major) {
final int maxMinor = (major == 4) ? startMaxMinor : 3;
for (int major = 4; major >= 3; --major) {
final int maxMinor = (major == 4) ? 6 : 3;
final int minMinor = (major == 3) ? 3 : 0;
for (int minor = maxMinor; minor >= minMinor; --minor) {
try {
@@ -70,7 +66,6 @@ public abstract class MixinForgeHooksClient_CoreProfile {
Display.create(format, attribs);
return;
} catch (LWJGLException | RuntimeException e) {
try { Display.destroy(); } catch (Exception ignored) {}
cur_exception = e;
}
}
@@ -1,37 +0,0 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.particles;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.ffp.DeferredDrawBatcher;
import net.minecraft.client.particle.EffectRenderer;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Captures individual particle draw() calls and merges them into a single draw call per unique GL state.
*/
@Mixin(EffectRenderer.class)
public class MixinEffectRenderer_DeferredBatch {
@Inject(method = "renderParticles",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V",
shift = At.Shift.AFTER))
private void angelica$enterDeferredMode(Entity player, float partialTickTime, CallbackInfo ci) {
if (AngelicaMod.options() != null && AngelicaMod.options().advanced.enableDeferredBatching) {
DeferredDrawBatcher.enter();
}
}
@Inject(method = "renderParticles",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/renderer/Tessellator;draw()I",
shift = At.Shift.BEFORE))
private void angelica$exitDeferredAndFlush(Entity player, float partialTickTime, CallbackInfo ci) {
if (DeferredDrawBatcher.isActive()) {
DeferredDrawBatcher.exitAndFlush();
}
}
}
@@ -1,13 +1,9 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
import net.minecraft.client.renderer.texture.TextureUtil;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(TextureUtil.class)
public class MixinTextureUtil {
@@ -16,9 +12,4 @@ public class MixinTextureUtil {
private static void angelica$dontDeleteTexture(int textureId) {
// NO-OP - Not sure why it's deleting a texture that was just generated and subsequently being bound...
}
@Inject(method = "allocateTextureImpl", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/TextureUtil;bindTexture(I)V", shift = At.Shift.AFTER))
private static void angelica$setMaxLevel(int textureId, int mipmapLevels, int width, int height, float anisotropicFiltering, CallbackInfo ci) {
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, mipmapLevels);
}
}
@@ -1,45 +0,0 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.vbo;
import com.gtnewhorizon.gtnhlib.client.renderer.postprocessing.PostProcessingHelper;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.minecraft.client.shader.Framebuffer;
import org.lwjgl.opengl.GL11;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(Framebuffer.class)
public abstract class MixinFramebuffer {
@Shadow
public int framebufferTexture;
/**
* @author Sisyphus
* @reason Replace FFP with statically allocated Buffers.
*/
@Overwrite
public void framebufferRender(int width, int height) {
GLStateManager.glColorMask(true, true, true, false);
GLStateManager.disableDepthTest();
GLStateManager.glDepthMask(false);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glLoadIdentity();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glLoadIdentity();
GLStateManager.glViewport(0, 0, width, height);
GLStateManager.enableTexture();
GLStateManager.disableLighting();
GLStateManager.disableAlphaTest();
GLStateManager.disableBlend();
GLStateManager.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, framebufferTexture);
PostProcessingHelper.bindFullscreenVAO();
PostProcessingHelper.drawFullscreenQuad();
PostProcessingHelper.unbindVAO();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glDepthMask(true);
GLStateManager.glColorMask(true, true, true, true);
}
}
@@ -32,41 +32,36 @@ public abstract class MixinTextureAtlasSprite implements SpriteExtension {
celeritas$processTransparentImages(this.framesTextureData.get(0)[0], level > 0 && !iconName.contains("leaves"));
}
// Ordinals: OPAQUE=0, TRANSPARENT=1, TRANSLUCENT=2
@Unique private static final int ORDINAL_OPAQUE = SpriteTransparencyLevel.OPAQUE.ordinal();
@Unique private static final int ORDINAL_TRANSPARENT = SpriteTransparencyLevel.TRANSPARENT.ordinal();
@Unique private static final int ORDINAL_TRANSLUCENT = SpriteTransparencyLevel.TRANSLUCENT.ordinal();
@Unique private static final SpriteTransparencyLevel[] LEVELS_BY_ORDINAL = SpriteTransparencyLevel.values();
@Unique
private void celeritas$processTransparentImages(int[] nativeImage, boolean shouldRewriteColors) {
float r = 0.0f, g = 0.0f, b = 0.0f;
float totalWeight = 0.0f;
// Use primitive int instead of loop-carried enum reference to avoid JDK 25 C2 SuperWord non-convergence bug
int maxLevel = ORDINAL_OPAQUE;
SpriteTransparencyLevel level = SpriteTransparencyLevel.OPAQUE;
for (int y = 0; y < nativeImage.length; y++) {
final int color = nativeImage[y];
final int alpha = ColorABGR.unpackAlpha(color);
int color = nativeImage[y];
int alpha = ColorABGR.unpackAlpha(color);
if (alpha > 0) {
if (alpha < 255) {
maxLevel = Math.max(maxLevel, ORDINAL_TRANSLUCENT);
level = level.chooseNextLevel(SpriteTransparencyLevel.TRANSLUCENT);
} else {
level = level.chooseNextLevel(SpriteTransparencyLevel.OPAQUE);
}
if (shouldRewriteColors) {
final float weight = (float) alpha;
float weight = (float) alpha;
r += ColorSRGB.srgbToLinear(ColorABGR.unpackRed(color)) * weight;
g += ColorSRGB.srgbToLinear(ColorABGR.unpackGreen(color)) * weight;
b += ColorSRGB.srgbToLinear(ColorABGR.unpackBlue(color)) * weight;
totalWeight += weight;
}
} else {
maxLevel = Math.max(maxLevel, ORDINAL_TRANSPARENT);
level = level.chooseNextLevel(SpriteTransparencyLevel.TRANSPARENT);
}
}
this.celeritas$transparencyLevel = LEVELS_BY_ORDINAL[maxLevel];
this.celeritas$transparencyLevel = level;
if (!shouldRewriteColors || totalWeight == 0.0f) {
return;
@@ -76,11 +71,11 @@ public abstract class MixinTextureAtlasSprite implements SpriteExtension {
g /= totalWeight;
b /= totalWeight;
final int averageColor = ColorSRGB.linearToSrgb(r, g, b, 0);
int averageColor = ColorSRGB.linearToSrgb(r, g, b, 0);
for (int y = 0; y < nativeImage.length; y++) {
final int color = nativeImage[y];
final int alpha = ColorABGR.unpackAlpha(color);
int color = nativeImage[y];
int alpha = ColorABGR.unpackAlpha(color);
if (alpha == 0) {
nativeImage[y] = averageColor;
}
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.compat.backhand.BackhandReflectionCompat;
import com.gtnewhorizons.angelica.compat.mojang.InteractionHand;
import net.coderbot.iris.pipeline.HandRenderer;
import net.irisshaders.iris.api.v0.IrisApi;
@@ -16,13 +15,19 @@ public class MixinItemRendererBackhand {
@Inject(method = "renderItemInFirstPerson", at = @At("HEAD"), cancellable = true)
private void iris$skipTranslucentHands(float partialTicks, CallbackInfo ci) {
if (IrisApi.getInstance().isShaderPackInUse()) {
final boolean isHandTranslucent = HandRenderer.INSTANCE.isHandTranslucent(InteractionHand.MAIN_HAND);
boolean isHandTranslucent = HandRenderer.INSTANCE.isHandTranslucent(InteractionHand.MAIN_HAND);
if (HandRenderer.INSTANCE.isRenderingSolid() && isHandTranslucent) {
ci.cancel();
BackhandReflectionCompat.renderOffhand(partialTicks);
if (ModStatus.isBackhandLoaded){
iris$skipTranslucentHandsBackhand(partialTicks, ci);
ModStatus.backhandCompat.renderOffhand(partialTicks);
}
} else if (!HandRenderer.INSTANCE.isRenderingSolid() && !isHandTranslucent) {
ci.cancel();
BackhandReflectionCompat.renderOffhand(partialTicks);
if (ModStatus.isBackhandLoaded){
iris$skipTranslucentHandsBackhand(partialTicks, ci);
ModStatus.backhandCompat.renderOffhand(partialTicks);
}
}
}
}
@@ -31,7 +36,7 @@ public class MixinItemRendererBackhand {
@Inject(method = "renderItemInFirstPerson", at = @At("RETURN"), cancellable = true, order = 900)
private void iris$skipTranslucentHandsBackhand(float partialTicks, CallbackInfo ci) {
if (ModStatus.isBackhandLoaded && IrisApi.getInstance().isShaderPackInUse()) {
final boolean isHandTranslucent = HandRenderer.INSTANCE.isHandTranslucent(InteractionHand.OFF_HAND);
boolean isHandTranslucent = HandRenderer.INSTANCE.isHandTranslucent(InteractionHand.OFF_HAND);
if (HandRenderer.INSTANCE.isRenderingSolid() && isHandTranslucent) {
ci.cancel();
} else if (!HandRenderer.INSTANCE.isRenderingSolid() && !isHandTranslucent) {
@@ -1,204 +0,0 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.AngelicaExtension;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(AngelicaExtension.class)
class FeedbackManagerTest {
@BeforeEach
void setUp() {
FeedbackManager.reset();
}
@AfterEach
void tearDown() {
// Ensure we're back in render mode
if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.glRenderMode(GL11.GL_RENDER);
}
FeedbackManager.reset();
}
@Test
void testStateMachine_noDraws_returnsZero() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(64);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
assertTrue(FeedbackManager.isFeedbackMode());
final int result = FeedbackManager.glRenderMode(GL11.GL_RENDER);
assertEquals(0, result, "No draws should produce 0 tokens");
assertFalse(FeedbackManager.isFeedbackMode());
}
@Test
void testOverflow_returnsNegative() {
// Buffer with only 2 floats — any triangle will overflow
final FloatBuffer buf = BufferUtils.createFloatBuffer(2);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
// Set up identity matrices and a viewport
GLStateManager.getModelViewMatrix().identity();
GLStateManager.getProjectionMatrix().identity();
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
// Upload 3 vertices to a VBO and draw
final int vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
final float[] verts = { 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f, 0f };
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(9);
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GL20.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_TRIANGLES, 0, 3);
final int result = FeedbackManager.glRenderMode(GL11.GL_RENDER);
assertEquals(-1, result, "Overflow should return -1");
// Cleanup
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GL30.glDeleteVertexArrays(vao);
}
@Test
void testPassThrough() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(64);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
FeedbackManager.glPassThrough(42.0f);
final int result = FeedbackManager.glRenderMode(GL11.GL_RENDER);
assertEquals(2, result, "PassThrough should write 2 floats");
assertEquals(GL11.GL_PASS_THROUGH_TOKEN, buf.get(0), "First token should be PASS_THROUGH_TOKEN");
assertEquals(42.0f, buf.get(1), "Second token should be the pass-through value");
}
@Test
void testTriangleFeedback_GL2D() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(128);
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
// Identity MV/proj, viewport (0,0,800,600)
GLStateManager.getModelViewMatrix().identity();
GLStateManager.getProjectionMatrix().identity();
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
// Upload 3 vertices forming a triangle
final int vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
// NDC vertices: (0,0,0), (1,0,0), (0,1,0)
// With identity MVP and viewport 800x600:
// NDC(0,0) → window(400, 300)
// NDC(1,0) → window(800, 300)
// NDC(0,1) → window(400, 600)
final float[] verts = { 0f, 0f, 0f, 1f, 0f, 0f, 0f, 1f, 0f };
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(9);
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GL20.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_TRIANGLES, 0, 3);
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
// GL_2D: POLYGON_TOKEN, 3, x0, y0, x1, y1, x2, y2 = 8 floats
assertEquals(8, count, "Triangle in GL_2D should produce 8 floats");
assertEquals(GL11.GL_POLYGON_TOKEN, buf.get(0), 0.001f);
assertEquals(3.0f, buf.get(1), 0.001f);
// Vertex 0: NDC(0,0) → window(400, 300)
assertEquals(400.0f, buf.get(2), 0.01f);
assertEquals(300.0f, buf.get(3), 0.01f);
// Vertex 1: NDC(1,0) → window(800, 300)
assertEquals(800.0f, buf.get(4), 0.01f);
assertEquals(300.0f, buf.get(5), 0.01f);
// Vertex 2: NDC(0,1) → window(400, 600)
assertEquals(400.0f, buf.get(6), 0.01f);
assertEquals(600.0f, buf.get(7), 0.01f);
// Cleanup
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GL30.glDeleteVertexArrays(vao);
}
@Test
void testIdentityTransform_viewportMapping() {
final FloatBuffer buf = BufferUtils.createFloatBuffer(64);
FeedbackManager.glFeedbackBuffer(GL11.GL_3D, buf);
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
GLStateManager.getModelViewMatrix().identity();
GLStateManager.getProjectionMatrix().identity();
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
final int vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
// Single point at NDC origin (0,0,0)
final float[] verts = { 0f, 0f, 0f };
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(3);
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GL20.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_POINTS, 0, 1);
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
// GL_3D point: POINT_TOKEN, x, y, z = 4 floats
assertEquals(4, count);
assertEquals(GL11.GL_POINT_TOKEN, buf.get(0), 0.001f);
// NDC(0,0,0) → window center (400, 300, 0.5)
assertEquals(400.0f, buf.get(1), 0.01f);
assertEquals(300.0f, buf.get(2), 0.01f);
assertEquals(0.5f, buf.get(3), 0.01f);
// Cleanup
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GL30.glDeleteVertexArrays(vao);
}
}
@@ -457,23 +457,18 @@ public class GLSM_ServerSideState_UnitTest {
assertTrue(GL11.glIsTexture(texId), "Texture should exist before delete");
// SharedDrawable: delete the texture (deferred — name stays valid)
// SharedDrawable: delete the texture
executeOnSharedDrawable(() -> {
GLStateManager.glDeleteTextures(texId);
assertTrue(GL11.glIsTexture(texId), "Texture name should still be valid (deferred delete)");
assertFalse(GL11.glIsTexture(texId), "Texture should not exist after SharedDrawable delete");
});
assertTrue(GL11.glIsTexture(texId), "Texture name should still be valid on main thread (deferred delete)");
// glGenTextures flushes deferred deletes
int newTexId = GLStateManager.glGenTextures();
assertFalse(GL11.glIsTexture(texId), "Texture should be gone after flush via glGenTextures");
GL11.glDeleteTextures(newTexId);
assertFalse(GL11.glIsTexture(texId), "Texture should not exist on main thread after SharedDrawable delete");
} finally {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
if (GL11.glIsTexture(texId)) {
GL11.glDeleteTextures(texId);
GLStateManager.glDeleteTextures(texId);
}
}
}
@@ -1,215 +0,0 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import static org.junit.jupiter.api.Assertions.*;
class DeferredDrawBatcherTest {
@Test
void stateKey_roundtrip_basic() {
long key = DeferredDrawBatcher.packStateKey(42, GL11.GL_SRC_ALPHA, GL11.GL_ONE, true, false, true, true);
assertEquals(42, DeferredDrawBatcher.unpackTextureId(key));
assertEquals(GL11.GL_SRC_ALPHA, DeferredDrawBatcher.unpackSrcRgb(key));
assertEquals(GL11.GL_ONE, DeferredDrawBatcher.unpackDstRgb(key));
assertTrue(DeferredDrawBatcher.unpackBlendEnabled(key));
assertFalse(DeferredDrawBatcher.unpackDepthMask(key));
}
@Test
void stateKey_roundtrip_allFlags() {
long key = DeferredDrawBatcher.packStateKey(0, 0, 0, false, true, true, true);
assertEquals(0, DeferredDrawBatcher.unpackTextureId(key));
assertEquals(0, DeferredDrawBatcher.unpackSrcRgb(key));
assertEquals(0, DeferredDrawBatcher.unpackDstRgb(key));
assertFalse(DeferredDrawBatcher.unpackBlendEnabled(key));
assertTrue(DeferredDrawBatcher.unpackDepthMask(key));
}
@Test
void stateKey_roundtrip_largeTextureId() {
// 20-bit texture ID = max 1048575
int texId = (1 << 20) - 1;
long key = DeferredDrawBatcher.packStateKey(texId, 0x0302, 0x0303, true, true, true, true);
assertEquals(texId, DeferredDrawBatcher.unpackTextureId(key));
assertEquals(0x0302, DeferredDrawBatcher.unpackSrcRgb(key));
assertEquals(0x0303, DeferredDrawBatcher.unpackDstRgb(key));
assertTrue(DeferredDrawBatcher.unpackBlendEnabled(key));
assertTrue(DeferredDrawBatcher.unpackDepthMask(key));
}
@Test
void stateKey_differentKeysNotEqual() {
long keyA = DeferredDrawBatcher.packStateKey(42, GL11.GL_SRC_ALPHA, GL11.GL_ONE, true, false, true, true);
long keyB = DeferredDrawBatcher.packStateKey(43, GL11.GL_SRC_ALPHA, GL11.GL_ONE, true, false, true, true);
long keyC = DeferredDrawBatcher.packStateKey(42, GL11.GL_ONE, GL11.GL_ONE, true, false, true, true);
assertNotEquals(keyA, keyB, "Different texture IDs should produce different keys");
assertNotEquals(keyA, keyC, "Different blend funcs should produce different keys");
}
@Test
void grouping_sameKeyEntriesMerged() {
// Verify that entries with the same state key sort together
long keyA = DeferredDrawBatcher.packStateKey(1, 0x0302, 0x0001, true, true, true, true);
long keyB = DeferredDrawBatcher.packStateKey(2, 0x0302, 0x0001, true, true, true, true);
// Create entries with interleaved keys: A, B, A, B, A
final int flags = VertexFlags.convertToFlags(true, true, false, true);
var entries = new java.util.ArrayList<DeferredBatchTessellator.DrawRange>();
entries.add(new DeferredBatchTessellator.DrawRange(keyA, 0, 128, 4, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(keyB, 128, 128, 4, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(keyA, 256, 128, 4, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(keyB, 384, 128, 4, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(keyA, 512, 128, 4, GL11.GL_QUADS, flags));
// Sort by state key (same as exitAndFlush)
entries.sort((a, b) -> Long.compare(a.stateKey(), b.stateKey()));
// Count groups
int groupCount = 0;
int[] groupSizes = new int[2];
long currentKey = entries.get(0).stateKey();
int groupSize = 0;
for (int i = 0; i <= entries.size(); i++) {
if (i == entries.size() || entries.get(i).stateKey() != currentKey) {
groupSizes[groupCount] = groupSize;
groupCount++;
if (i < entries.size()) {
currentKey = entries.get(i).stateKey();
groupSize = 1;
}
} else {
groupSize++;
}
}
assertEquals(2, groupCount, "Should have exactly 2 groups");
// keyA < keyB (texture ID 1 vs 2), so keyA group comes first
assertEquals(3, groupSizes[0], "Key A group should have 3 entries");
assertEquals(2, groupSizes[1], "Key B group should have 2 entries");
}
@Test
void grouping_mergedVertexCountCorrect() {
long key = DeferredDrawBatcher.packStateKey(1, 0x0302, 0x0001, true, true, true, true);
final int flags = VertexFlags.convertToFlags(true, true, false, true);
var entries = new java.util.ArrayList<DeferredBatchTessellator.DrawRange>();
entries.add(new DeferredBatchTessellator.DrawRange(key, 0, 128, 4, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(key, 128, 256, 8, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(key, 384, 128, 4, GL11.GL_QUADS, flags));
// Simulate merge within a group: sum vertices and byte lengths
int totalVertices = 0;
int totalBytes = 0;
for (var e : entries) {
totalVertices += e.vertexCount();
totalBytes += e.byteLength();
}
assertEquals(16, totalVertices, "Merged group should have 4+8+4=16 vertices");
assertEquals(512, totalBytes, "Merged group should have 128+256+128=512 bytes");
}
@Test
void grouping_differentDrawModesFormSubgroups() {
long key = DeferredDrawBatcher.packStateKey(1, 0x0302, 0x0001, true, true, true, true);
final int flags = VertexFlags.convertToFlags(true, true, false, true);
var entries = new java.util.ArrayList<DeferredBatchTessellator.DrawRange>();
entries.add(new DeferredBatchTessellator.DrawRange(key, 0, 128, 4, GL11.GL_QUADS, flags));
entries.add(new DeferredBatchTessellator.DrawRange(key, 128, 128, 4, GL11.GL_TRIANGLES, flags));
entries.add(new DeferredBatchTessellator.DrawRange(key, 256, 128, 4, GL11.GL_QUADS, flags));
// Same state key, but different draw modes can't be merged.
// The flush logic subgroups by (drawMode, format flags).
// Count subgroups: QUADS(entry0), TRIANGLES(entry1), QUADS(entry2)
// Since entries aren't further sorted by drawMode, this produces 3 subgroups.
int subgroupCount = 0;
int i = 0;
while (i < entries.size()) {
int drawMode = entries.get(i).drawMode();
while (i < entries.size() && entries.get(i).drawMode() == drawMode) i++;
subgroupCount++;
}
// Two QUADS entries are not contiguous (TRIANGLES between them), so 3 subgroups
assertEquals(3, subgroupCount, "Different draw modes should form separate subgroups");
}
@Test
void stateKey_blendConstants() {
// Verify all common GL blend constants roundtrip correctly
int[] blendConstants = {
GL11.GL_ZERO, // 0x0000
GL11.GL_ONE, // 0x0001
GL11.GL_SRC_ALPHA, // 0x0302
GL11.GL_ONE_MINUS_SRC_ALPHA, // 0x0303
GL11.GL_DST_ALPHA, // 0x0304
GL11.GL_ONE_MINUS_DST_ALPHA, // 0x0305
};
for (int src : blendConstants) {
for (int dst : blendConstants) {
long key = DeferredDrawBatcher.packStateKey(1, src, dst, true, true, true, true);
assertEquals(src, DeferredDrawBatcher.unpackSrcRgb(key), "srcRgb roundtrip failed for 0x" + Integer.toHexString(src));
assertEquals(dst, DeferredDrawBatcher.unpackDstRgb(key), "dstRgb roundtrip failed for 0x" + Integer.toHexString(dst));
}
}
}
@Test
void stateKey_textureEnableRoundtrip() {
// tex0=true, tex1=false
long key1 = DeferredDrawBatcher.packStateKey(42, 0x0302, 0x0001, true, true, true, false);
assertTrue(DeferredDrawBatcher.unpackTex0Enabled(key1));
assertFalse(DeferredDrawBatcher.unpackTex1Enabled(key1));
assertEquals(42, DeferredDrawBatcher.unpackTextureId(key1));
// tex0=false, tex1=true
long key2 = DeferredDrawBatcher.packStateKey(42, 0x0302, 0x0001, true, true, false, true);
assertFalse(DeferredDrawBatcher.unpackTex0Enabled(key2));
assertTrue(DeferredDrawBatcher.unpackTex1Enabled(key2));
assertEquals(42, DeferredDrawBatcher.unpackTextureId(key2));
// Enable bits don't interfere with existing fields
long keyBothOff = DeferredDrawBatcher.packStateKey(42, 0x0302, 0x0001, true, true, false, false);
long keyBothOn = DeferredDrawBatcher.packStateKey(42, 0x0302, 0x0001, true, true, true, true);
assertEquals(DeferredDrawBatcher.unpackTextureId(keyBothOff), DeferredDrawBatcher.unpackTextureId(keyBothOn));
assertEquals(DeferredDrawBatcher.unpackSrcRgb(keyBothOff), DeferredDrawBatcher.unpackSrcRgb(keyBothOn));
assertEquals(DeferredDrawBatcher.unpackDstRgb(keyBothOff), DeferredDrawBatcher.unpackDstRgb(keyBothOn));
assertEquals(DeferredDrawBatcher.unpackBlendEnabled(keyBothOff), DeferredDrawBatcher.unpackBlendEnabled(keyBothOn));
assertEquals(DeferredDrawBatcher.unpackDepthMask(keyBothOff), DeferredDrawBatcher.unpackDepthMask(keyBothOn));
// Different tex enable states produce different keys
assertNotEquals(key1, key2, "Different texture enable states should produce different keys");
}
@Test
void drawRange_byteOffsetsNonOverlapping() {
long key = DeferredDrawBatcher.packStateKey(1, 0x0302, 0x0001, true, true, true, true);
final int flags = VertexFlags.convertToFlags(true, true, false, true);
// Simulate sequential byte offsets as would be produced by interceptDraw
var ranges = new java.util.ArrayList<DeferredBatchTessellator.DrawRange>();
int offset = 0;
for (int i = 0; i < 5; i++) {
int byteLen = (i + 1) * 128; // 128, 256, 384, 512, 640
ranges.add(new DeferredBatchTessellator.DrawRange(key, offset, byteLen, (i + 1) * 4, GL11.GL_QUADS, flags));
offset += byteLen;
}
// Verify ranges are contiguous and non-overlapping
for (int i = 1; i < ranges.size(); i++) {
var prev = ranges.get(i - 1);
var curr = ranges.get(i);
assertEquals(prev.byteOffset() + prev.byteLength(), curr.byteOffset(), "Range " + i + " should start where range " + (i - 1) + " ends");
}
// Total bytes = 128+256+384+512+640 = 1920
assertEquals(1920, offset, "Total accumulated bytes");
}
}
@@ -1,307 +0,0 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.AngelicaExtension;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for FFP TexEnv COMBINE / multi-unit features:
* TexEnvState tracking, FragmentKey packing, FragmentShaderGenerator COMBINE codegen, and glMultiTexCoord routing.
*/
@ExtendWith(AngelicaExtension.class)
class FFPCombineShaderTest {
private final List<Integer> shadersToDelete = new ArrayList<>();
private final List<Integer> programsToDelete = new ArrayList<>();
@AfterEach
void cleanup() {
// Delete GL resources
for (int p : programsToDelete) GL20.glDeleteProgram(p);
for (int s : shadersToDelete) GL20.glDeleteShader(s);
programsToDelete.clear();
shadersToDelete.clear();
// Reset texture units to defaults
for (int i = 3; i >= 0; i--) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i);
GLStateManager.getTextures().getTexEnvState(i).reset();
if (i > 0) {
GLStateManager.getTextures().getTextureUnitStates(i).disable();
}
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
@Test
void testTexEnvStateTracking() {
// Unit 0: COMBINE mode, SUBTRACT RGB, REPLACE alpha
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL13.GL_SUBTRACT);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE1_RGB, GL13.GL_PRIMARY_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_RGB, GL11.GL_ONE_MINUS_SRC_COLOR);
TexEnvState env0 = GLStateManager.getTextures().getTexEnvState(0);
assertEquals(GL13.GL_COMBINE, env0.mode);
assertEquals(GL13.GL_SUBTRACT, env0.combineRgb);
assertEquals(GL11.GL_REPLACE, env0.combineAlpha);
assertEquals(GL11.GL_TEXTURE, env0.sourceRgb[0]);
assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, env0.operandRgb[0]);
assertEquals(GL13.GL_PRIMARY_COLOR, env0.sourceRgb[1]);
assertEquals(GL11.GL_ONE_MINUS_SRC_COLOR, env0.operandRgb[1]);
// Unit 2: independent state
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_ADD);
TexEnvState env2 = GLStateManager.getTextures().getTexEnvState(2);
assertEquals(GL13.GL_COMBINE, env2.mode);
assertEquals(GL11.GL_ADD, env2.combineRgb);
// Verify unit 0 unchanged
assertEquals(GL13.GL_SUBTRACT, env0.combineRgb, "Unit 0 should be independent of unit 2");
}
@Test
void testFragmentKeyMultiUnitCombine() {
setupXaeroState();
FragmentKey fk = FragmentKey.fromState();
assertEquals(4, fk.nrEnabledUnits(), "highestEnabled is unit 3 → nrEnabledUnits=4");
assertTrue(fk.unitEnabled(0));
assertFalse(fk.unitEnabled(1), "unit 1 should be disabled");
assertTrue(fk.unitEnabled(2));
assertTrue(fk.unitEnabled(3));
assertEquals(FragmentKey.TEX_ENV_COMBINE, fk.unitMode(0));
assertEquals(FragmentKey.COMBINE_SUBTRACT, fk.unitCombineRgb(0));
assertEquals(FragmentKey.TEX_ENV_COMBINE, fk.unitMode(2));
assertEquals(FragmentKey.COMBINE_ADD, fk.unitCombineRgb(2));
assertEquals(FragmentKey.TEX_ENV_MODULATE, fk.unitMode(3));
// Verify source/operand on unit 0
assertEquals(FragmentKey.SRC_TEXTURE, fk.unitSourceRgb(0, 0));
assertEquals(FragmentKey.OP_SRC_COLOR, fk.unitOperandRgb(0, 0));
assertEquals(FragmentKey.SRC_PRIMARY_COLOR, fk.unitSourceRgb(0, 1));
assertEquals(FragmentKey.OP_SRC_COLOR, fk.unitOperandRgb(0, 1));
}
@Test
void testSingleUnitCombineCompiles() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE);
FragmentKey fk = FragmentKey.fromState();
String fragSource = FragmentShaderGenerator.generate(fk);
int shader = compileShader(fragSource, GL20.GL_FRAGMENT_SHADER);
assertTrue(shader > 0, "Single-unit COMBINE fragment shader should compile");
}
@Test
void testXaeroMultiUnitCombineCompiles() {
setupXaeroState();
FragmentKey fk = FragmentKey.fromState();
VertexKey vk = VertexKey.fromState(true, false, true, false);
String fragSource = FragmentShaderGenerator.generate(fk);
String vertSource = VertexShaderGenerator.generate(vk);
int vs = compileShader(vertSource, GL20.GL_VERTEX_SHADER);
int fs = compileShader(fragSource, GL20.GL_FRAGMENT_SHADER);
int prog = GL20.glCreateProgram();
programsToDelete.add(prog);
GL20.glAttachShader(prog, vs);
GL20.glAttachShader(prog, fs);
GL20.glLinkProgram(prog);
int linkStatus = GL20.glGetProgrami(prog, GL20.GL_LINK_STATUS);
if (linkStatus == 0) {
String log = GL20.glGetProgramInfoLog(prog, 4096);
fail("Xaero multi-unit program link failed:\n" + log
+ "\n\n--- Vertex Shader ---\n" + vertSource
+ "\n\n--- Fragment Shader ---\n" + fragSource);
}
}
static Stream<Arguments> allCombineFunctions() {
return Stream.of(
Arguments.of("REPLACE", GL11.GL_REPLACE, 1),
Arguments.of("MODULATE", GL11.GL_MODULATE, 2),
Arguments.of("ADD", GL11.GL_ADD, 2),
Arguments.of("ADD_SIGNED", GL13.GL_ADD_SIGNED, 2),
Arguments.of("SUBTRACT", GL13.GL_SUBTRACT, 2),
Arguments.of("INTERPOLATE", GL13.GL_INTERPOLATE, 3),
Arguments.of("DOT3_RGB", GL13.GL_DOT3_RGB, 2),
Arguments.of("DOT3_RGBA", GL13.GL_DOT3_RGBA, 2)
);
}
@ParameterizedTest(name = "COMBINE function {0}")
@MethodSource("allCombineFunctions")
void testAllCombineFunctionsCompile(String name, int glFunc, int numArgs) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, glFunc);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
// Set up appropriate sources
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA);
if (numArgs >= 2) {
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE1_RGB, GL13.GL_PRIMARY_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE1_ALPHA, GL13.GL_PREVIOUS);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);
}
if (numArgs >= 3) {
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE2_RGB, GL13.GL_PREVIOUS);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE2_ALPHA, GL13.GL_PREVIOUS);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);
}
FragmentKey fk = FragmentKey.fromState();
String fragSource = FragmentShaderGenerator.generate(fk);
int shader = compileShader(fragSource, GL20.GL_FRAGMENT_SHADER);
assertTrue(shader > 0, "COMBINE function " + name + " should compile");
}
@Test
void testCombineScopeDeclaresColorOutsideBlock() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE);
FragmentKey fk = FragmentKey.fromState();
String src = FragmentShaderGenerator.generate(fk);
// Result variables declared outside the scope block
assertTrue(src.contains("vec3 combRgb0;"), "combRgb0 should be declared outside scope block");
assertTrue(src.contains("float combAlpha0;"), "combAlpha0 should be declared outside scope block");
// color assignment uses the result variables outside the block
assertTrue(src.contains("vec4(combRgb0, combAlpha0)"), "color should be assigned from combine results outside block");
// The scope block uses { } to contain arg declarations
assertTrue(src.contains(" {\n"), "Should have scope block for arg declarations");
// Bonus: verify it compiles
int shader = compileShader(src, GL20.GL_FRAGMENT_SHADER);
assertTrue(shader > 0, "Scope-correct COMBINE shader should compile");
}
// ---- Test 7: glMultiTexCoord2f routes to ShaderManager.currentTexCoord ----
@Test
void testMultiTexCoordRoutesToShaderManager() {
// Not in immediate mode or display list recording, so should go to ShaderManager.currentTexCoord
ShaderManager.setCurrentTexCoord(0.0f, 0.0f, 0.0f, 1.0f); // reset
GLStateManager.glMultiTexCoord2f(GL13.GL_TEXTURE0, 0.5f, 0.75f);
var texCoord = ShaderManager.getCurrentTexCoord();
assertEquals(0.5f, texCoord.x, 1e-6f, "s coordinate should be 0.5");
assertEquals(0.75f, texCoord.y, 1e-6f, "t coordinate should be 0.75");
}
@Test
void testMultiTexCoord2dDelegatesToFloat() {
ShaderManager.setCurrentTexCoord(0.0f, 0.0f, 0.0f, 1.0f);
GLStateManager.glMultiTexCoord2d(GL13.GL_TEXTURE0, 0.5, 0.75);
var texCoord = ShaderManager.getCurrentTexCoord();
assertEquals(0.5f, texCoord.x, 1e-6f, "double 0.5 should arrive as float 0.5");
assertEquals(0.75f, texCoord.y, 1e-6f, "double 0.75 should arrive as float 0.75");
}
/**
* Configure the 4-unit Xaero-like combine chain:
* Unit 0: COMBINE, RGB=SUBTRACT, SRC0=TEXTURE/SRC_COLOR, SRC1=PRIMARY_COLOR/SRC_COLOR
* Unit 1: disabled
* Unit 2: COMBINE, RGB=ADD, SRC0=TEXTURE/SRC_COLOR, SRC1=PREVIOUS/SRC_COLOR
* Unit 3: MODULATE (simple)
*/
private void setupXaeroState() {
// Unit 0: COMBINE / SUBTRACT
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL13.GL_SUBTRACT);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE1_RGB, GL13.GL_PRIMARY_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA);
// Unit 1: disabled (skip — default is disabled)
// Unit 2: COMBINE / ADD
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.enableTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_ADD);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE1_RGB, GL13.GL_PREVIOUS);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA);
// Unit 3: simple MODULATE
GLStateManager.glActiveTexture(GL13.GL_TEXTURE3);
GLStateManager.enableTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
// Restore active unit
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
private int compileShader(String source, int type) {
int shader = GL20.glCreateShader(type);
shadersToDelete.add(shader);
GL20.glShaderSource(shader, source);
GL20.glCompileShader(shader);
int status = GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS);
if (status == 0) {
String log = GL20.glGetShaderInfoLog(shader, 4096);
fail("Shader compilation failed:\n" + log + "\n\n--- Source ---\n" + source);
}
return shader;
}
}
@@ -2,12 +2,9 @@ package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.AngelicaExtension;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.streaming.OrphanStreamingBuffer.UploadStrategy;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -20,212 +17,73 @@ import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(AngelicaExtension.class)
class StreamingBufferTest {
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_firstUploadReturnsZero(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
@Test
void orphan_uploadReturnsZero() {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer();
try {
ByteBuffer data = memAlloc(128);
fillPattern(data);
data.flip();
assertEquals(0, buf.upload(data, 32), "First upload should return offset 0");
assertEquals(0, buf.upload(data, 32));
memFree(data);
} finally {
buf.destroy();
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_dataIntegrity(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
@Test
void orphan_dataIntegrity() {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer();
try {
ByteBuffer data = memAlloc(64);
ByteBuffer readBack = null;
try {
for (int i = 0; i < 64; i++) data.put((byte) i);
data.flip();
for (int i = 0; i < 64; i++) data.put((byte) i);
data.flip();
buf.upload(data, 16);
buf.upload(data, 16);
// Read back via glGetBufferSubData
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buf.getBufferId());
readBack = memAlloc(64);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, 0, readBack);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
// Read back via glGetBufferSubData
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buf.getBufferId());
ByteBuffer readBack = memAlloc(64);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, 0, readBack);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
for (int i = 0; i < 64; i++) {
assertEquals((byte) i, readBack.get(i), "Mismatch at byte " + i);
}
} finally {
memFree(data);
if (readBack != null) memFree(readBack);
for (int i = 0; i < 64; i++) {
assertEquals((byte) i, readBack.get(i), "Mismatch at byte " + i);
}
memFree(readBack);
} finally {
buf.destroy();
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_multipleUploadsSubAllocate(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
@Test
void orphan_multipleUploadsGrowCapacity() {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer();
try {
final int stride = 16;
// First upload: small
ByteBuffer small = memAlloc(64);
fillPattern(small);
small.flip();
buf.upload(small, 16);
memFree(small);
// Second upload: larger
ByteBuffer big = memAlloc(256);
ByteBuffer readBack = null;
try {
// First upload: small
fillPattern(small);
small.flip();
int v1 = buf.upload(small, stride);
assertEquals(0, v1, "First upload should be at offset 0");
fillPattern(big);
big.flip();
buf.upload(big, 16);
memFree(big);
// Second upload: larger — sub-allocates after first (64KB min capacity)
fillPattern(big);
big.flip();
int v2 = buf.upload(big, stride);
assertTrue(v2 > 0, "Second upload should sub-allocate at non-zero offset");
// Verify larger data is present
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buf.getBufferId());
ByteBuffer readBack = memAlloc(256);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, 0, readBack);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
// Verify second upload's data at its offset
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buf.getBufferId());
readBack = memAlloc(buf.getCapacity());
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, 0, readBack);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
int byteOffset = v2 * stride;
for (int i = 0; i < 256; i++) {
assertEquals((byte) (i & 0xFF), readBack.get(byteOffset + i), "Mismatch at byte " + i);
}
} finally {
memFree(small);
memFree(big);
if (readBack != null) memFree(readBack);
}
} finally {
buf.destroy();
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_subAllocationReturnsIncreasingOffsets(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
try {
final int stride = 16;
final int dataSize = 64; // 4 vertices worth
ByteBuffer data1 = memAlloc(dataSize);
ByteBuffer data2 = memAlloc(dataSize);
ByteBuffer data3 = memAlloc(dataSize);
ByteBuffer readBack = null;
try {
// First upload: offset 0
for (int i = 0; i < dataSize; i++) data1.put((byte) (i + 10));
data1.flip();
int v1 = buf.upload(data1, stride);
assertEquals(0, v1, "First upload should be at vertex 0");
// Second upload: should be at aligned offset after first
for (int i = 0; i < dataSize; i++) data2.put((byte) (i + 20));
data2.flip();
int v2 = buf.upload(data2, stride);
assertTrue(v2 > 0, "Second upload should be at a non-zero vertex offset");
assertEquals(0, v2 % 4, "Vertex offset must be quad-aligned (multiple of 4)");
// Third upload
for (int i = 0; i < dataSize; i++) data3.put((byte) (i + 30));
data3.flip();
int v3 = buf.upload(data3, stride);
assertTrue(v3 > v2, "Third upload should be after second");
assertEquals(0, v3 % 4, "Vertex offset must be quad-aligned");
// Verify all three regions have correct data
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buf.getBufferId());
readBack = memAlloc(buf.getCapacity());
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, 0, readBack);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
for (int i = 0; i < dataSize; i++) {
assertEquals((byte) (i + 10), readBack.get(v1 * stride + i), "Upload 1 mismatch at byte " + i);
assertEquals((byte) (i + 20), readBack.get(v2 * stride + i), "Upload 2 mismatch at byte " + i);
assertEquals((byte) (i + 30), readBack.get(v3 * stride + i), "Upload 3 mismatch at byte " + i);
}
} finally {
memFree(data1);
memFree(data2);
memFree(data3);
if (readBack != null) memFree(readBack);
}
} finally {
buf.destroy();
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_overflowResetsToZero(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
try {
final int stride = 16;
// First upload: establishes capacity
ByteBuffer data1 = memAlloc(64);
try {
fillPattern(data1);
data1.flip();
buf.upload(data1, stride);
} finally {
memFree(data1);
}
int capacityAfterFirst = buf.getCapacity();
// Second upload: fills remaining space or overflows
ByteBuffer data2 = memAlloc(capacityAfterFirst); // exactly fills or overflows
try {
fillPattern(data2);
data2.flip();
int v2 = buf.upload(data2, stride);
// Either sub-allocated (if it fit after alignment) or reset to 0 (overflow)
assertEquals(0, v2 % 4, "Offset must be quad-aligned");
} finally {
memFree(data2);
}
} finally {
buf.destroy();
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_postDrawResetsForNextFrame(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
try {
final int stride = 16;
ByteBuffer data = memAlloc(64);
try {
// Frame 1: two uploads
fillPattern(data);
data.flip();
buf.upload(data, stride);
data.position(0).limit(64);
int v2 = buf.upload(data, stride);
assertTrue(v2 > 0, "Second upload in frame should be at non-zero offset");
// End of frame
buf.postDraw();
assertEquals(0, buf.getWritePos(), "writePos should reset after postDraw");
// Frame 2: first upload should be at offset 0 again (after orphan)
data.position(0).limit(64);
int v3 = buf.upload(data, stride);
assertEquals(0, v3, "First upload after postDraw should return 0");
} finally {
memFree(data);
for (int i = 0; i < 256; i++) {
assertEquals((byte) (i & 0xFF), readBack.get(i), "Mismatch at byte " + i);
}
memFree(readBack);
} finally {
buf.destroy();
}