Compare commits

...
29 Commits
Author SHA1 Message Date
Death Fuel 4c5e5df290 Replace compatibility mode texture formats (#1518) 2026-03-14 18:42:26 -07:00
Jason Mitchell 39ee364418 Improvements (#1516) 2026-03-14 15:06:30 -07:00
sisyphussy 4c9cae5e0b Optimize Streaming (#1515) 2026-03-14 13:13:40 -07:00
DarkShadow44 11b53c9f8f Add glTexImage3D with IntBuffer (#1506) 2026-03-11 11:55:47 -07:00
Jason Mitchell c769a08a4e Fix matching across newlines (#1505) 2026-03-11 09:52:21 -07:00
Jason Mitchell 90dac9f39d Add some missing glGetTexEnv* (#1503) 2026-03-10 19:18:58 -07:00
Jason Mitchell c5c4b290b1 Add missing overload (#1495) 2026-03-10 11:50:58 -07:00
Jason Mitchell 4b46de4566 Botania fix take two (#1501) 2026-03-10 10:22:06 -07:00
Jason Mitchell 9f2a71be4e Add missing overloads (#1500) 2026-03-10 10:06:12 -07:00
Jason Mitchell 79fe08417c Fix overwrite visibility crash (#1499) 2026-03-10 09:45:50 -07:00
Jason Mitchell 5de64abdef Maybe make Xaero work on j8 (#1494) 2026-03-09 19:47:41 -07:00
Death Fuel 2e07465166 UI Fix (#1492) 2026-03-09 16:22:44 -07:00
Death Fuel 2b610b9aea Better font rendering MC parity + book mode (#1489) 2026-03-09 11:49:33 -07:00
MalTeeez ab192ddd2a Split MixinRenderManager into two for DAPI compat (#1480) 2026-03-08 06:07:01 +01:00
Jason Mitchell cb5e2ed7d4 Lazy init entity lighting maps (#1476) 2026-03-07 14:01:55 -08:00
ah-OOG-ah e914ef7563 Actually sleep in FPS limiter (#1471) 2026-03-07 12:20:28 -08:00
Jason Mitchell 748dbd55a1 So secret you can't see them. (#1475) 2026-03-07 11:48:24 -08:00
Jason Mitchell 140c0c2285 Snapshot VAO/VBO data during display list compilation, like mesa does. (#1473) 2026-03-07 11:17:08 -08:00
sisyphussy af26442b98 Fix intel tests (#1442) 2026-03-07 10:12:56 -08:00
danyadevandAlexdoru ef07ee9f77 Refactor CeleritasBlockTransform (#1455)
Co-authored-by: Alexdoru <[email protected]>
2026-03-07 17:26:39 +01:00
Jason Mitchell 1b8087e8a3 Emulate glLineWidth on core using geom shaders (#1468) 2026-03-06 19:05:06 -08:00
Jason Mitchell 9107ea9ae5 Fix load -> enter -> leave: panorama rendering, cleanup state & fix test (#1467) 2026-03-06 19:01:43 -08:00
Jason Mitchell 938a6bda90 Replace panorama with modern opengl equivalent (#1466) 2026-03-06 16:01:27 -08:00
Jason Mitchell e9f6f72741 More core profile followons (#1464) 2026-03-06 15:50:30 -08:00
Jason Mitchell 379330f997 Core followups (#1445) 2026-03-05 11:35:05 -08:00
sisyphussy 2fecb3a05b Add MixinWavefrontObject back (#1452) 2026-03-05 11:07:45 -08:00
Raven Szewczyk c085af35bd Update buildscript with latest lwjgl3ify fixes (#1456) 2026-03-05 11:00:53 -08:00
Caedis a36aec36a2 Invert Horizon and Terrain Fog toggles (#1454) 2026-03-05 17:36:56 +01:00
sisyphussy bdd6259c3f Optimize fontrenderer (#1447) 2026-03-04 18:37:50 -08:00
96 changed files with 3355 additions and 1330 deletions

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ plugins {
minecraft {
extraRunJvmArguments.add("-Dangelica.enableTestBlocks=true")
extraRunJvmArguments.add("-Dangelica.dumpClass=true")
// extraRunJvmArguments.add("-Dorg.lwjgl.util.Debug=true")
// extraRunJvmArguments.addAll("-Dlegacy.debugClassLoadingSave=true")
// extraRunJvmArguments.addAll("-Drfb.dumpLoadedClasses=true", "-Drfb.dumpLoadedClassesPerTransformer=true")
//extraRunJvmArguments.add("-Dangelica.redirectorLogspam=true")
+3 -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.36:dev")
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
@@ -82,8 +82,8 @@ dependencies {
compileOnly "org.apache.ant:ant:1.8.2"
// Celeritas (transitive=false: deps provided by GTNHLib/runtime)
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.2-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-lwjgl2-service:2.5.2-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.3-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-lwjgl2-service:2.5.3-GTNH") { transitive = false }
// Because who doesn't want NEI
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.8.35-GTNH:dev')
+5 -5
View File
@@ -1,12 +1,12 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/c5ee947fbfb70bc347d8d531e3a578c4/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/5ee463876563a46fff651cce1bc53a4c/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/cd495626d2ee49a75447e3fdc6afb287/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/c5ee947fbfb70bc347d8d531e3a578c4/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/5ee463876563a46fff651cce1bc53a4c/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/cd495626d2ee49a75447e3fdc6afb287/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e1118b4c7c2858d28154d086dbff010e/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/59a9771cad43219260d9aac9a8ec4d6a/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/c5ee947fbfb70bc347d8d531e3a578c4/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/5ee463876563a46fff651cce1bc53a4c/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/832229f0f6f0be60ed817c5a5e5848eb/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/b96cb7670c095245220e7d01d5927b58/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/cd495626d2ee49a75447e3fdc6afb287/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/a4c09dd2e2d7079373d30e524bbc2829/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/8e1d9ee5d0f13e442218f6884a306da1/redirect
toolchainVersion=25
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
+1 -1
View File
@@ -16,5 +16,5 @@ pluginManagement {
}
plugins {
id("com.gtnewhorizons.gtnhsettingsconvention") version("2.0.20")
id("com.gtnewhorizons.gtnhsettingsconvention") version("2.0.23")
}
@@ -2,37 +2,35 @@ package com.gtnewhorizons.angelica.client.font;
import com.google.common.collect.ImmutableSet;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
import com.gtnewhorizons.angelica.config.FontConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Setter;
import net.coderbot.iris.gl.program.Program;
import net.coderbot.iris.gl.program.ProgramBuilder;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.IOUtils;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Objects;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
/**
* A batching replacement for {@code FontRenderer}
@@ -44,14 +42,12 @@ public class BatchingFontRenderer {
/** The underlying FontRenderer object that's being accelerated */
protected FontRenderer underlying;
/** Array of width of all the characters in default.png */
protected int[] charWidth = new int[256];
/** Array of the start/end column (in upper/lower nibble) for every glyph in the /font directory. */
protected byte[] glyphWidth;
protected int[] charWidth;
/**
* Array of RGB triplets defining the 16 standard chat colors followed by 16 darker version of the same colors for
* drop shadows.
*/
private int[] colorCode;
private final int[] colorCode;
/** Location of the primary font atlas to bind. */
protected final ResourceLocation locationFontTexture;
@@ -59,28 +55,22 @@ public class BatchingFontRenderer {
private final int AAStrength;
private final int mvpMatrixLocation;
private final int fontShaderId;
private static int fontVAO = 0;
private static int vboPositions, vboColors, vboTexCoords, vboTexBounds, vboIndices;
private static int capPositions, capColors, capTexCoords, capTexBounds, capIndices;
final boolean isSGA;
final boolean isSplash;
/** For use with modded books. Affects calculations and forces some defaults. */
@Setter
boolean bookMode = false;
private static class FontAAShader {
private static Program fontShader = null;
public static Program getProgram() {
if (fontShader == null) {
String vsh, fsh;
try {
fsh = new String(IOUtils.toByteArray(Objects.requireNonNull(FontAAShader.class.getResourceAsStream("/assets/angelica/shaders/fontFilter.fsh"))), StandardCharsets.UTF_8);
vsh = new String(IOUtils.toByteArray(Objects.requireNonNull(FontAAShader.class.getResourceAsStream("/assets/angelica/shaders/fontFilter.vsh"))), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
ProgramBuilder builder = ProgramBuilder.begin("fontFilter", vsh, null, fsh, ImmutableSet.of(0));
fontShader = builder.build();
final String vsh = ShaderLoader.getShaderSource("angelica:fontFilter.vsh");
final String fsh = ShaderLoader.getShaderSource("angelica:fontFilter.fsh");
fontShader = ProgramBuilder.begin("fontFilter", vsh, null, fsh, ImmutableSet.of(0)).build();
}
return fontShader;
}
@@ -107,89 +97,132 @@ public class BatchingFontRenderer {
AAMode = GL20.glGetUniformLocation(fontShaderId, "aaMode");
AAStrength = GL20.glGetUniformLocation(fontShaderId, "strength");
mvpMatrixLocation = GL20.glGetUniformLocation(fontShaderId, "u_MVPMatrix");
if (ebo == null) {
ebo = new IndexBuffer();
vbo = GL15.glGenBuffers();
allocateBuffers();
}
}
// === Batched rendering
private int batchDepth = 0;
private int vtxWriterIndex = 0;
private int idxWriterIndex = 0;
private static final int INITIAL_BATCH_SIZE = 2048;
private static final ResourceLocation DUMMY_RESOURCE_LOCATION = new ResourceLocation("angelica$dummy",
"this is invalid!");
private FloatBuffer batchVtxPositions = memAllocFloat(INITIAL_BATCH_SIZE * 2);
private ByteBuffer batchVtxColors = memAlloc(INITIAL_BATCH_SIZE * 4);
private FloatBuffer batchVtxTexCoords = memAllocFloat(INITIAL_BATCH_SIZE * 2);
private IntBuffer batchIndices = memAllocInt(INITIAL_BATCH_SIZE / 2 * 3);
private FloatBuffer batchVtxTexBounds = memAllocFloat(INITIAL_BATCH_SIZE * 4);
// Layout in data:
// [v, v, t, t, c, c, c, c, tb, tb, tb, tb]
// v, t and tb are floats, c is bytes; 36 bytes total
private static final int VERTEX_SIZE = 36;
private static int rawCapacity = INITIAL_BATCH_SIZE * VERTEX_SIZE;
private static ByteBuffer vertexData = memAlloc(rawCapacity);
private static long vertexDataAddress = memAddress0(vertexData);
private static int vboCapacity;
// OpenGL objects (static, can be used between multiple BatchingFontRenderer)
private static int fontVAO = 0;
private static int vbo;
private static IndexBuffer ebo;
private int batchDepth = 0;
private int vertexDataPos = 0;
private int idxWriterIndex = 0;
private final ObjectArrayList<FontDrawCmd> batchCommands = ObjectArrayList.wrap(new FontDrawCmd[64], 0);
private final ObjectArrayList<FontDrawCmd> batchCommandPool = ObjectArrayList.wrap(new FontDrawCmd[64], 0);
private int blendSrcRGB = GL11.GL_SRC_ALPHA;
private int blendDstRGB = GL11.GL_ONE_MINUS_SRC_ALPHA;
/** */
private void pushVtx(float x, float y, int rgba, float u, float v, float uMin, float uMax, float vMin, float vMax) {
final int oldCap = batchVtxPositions.capacity() / 2;
if (vtxWriterIndex >= oldCap) {
final int newCap = oldCap * 2;
batchVtxPositions = memRealloc(batchVtxPositions, newCap * 2);
batchVtxColors = memRealloc(batchVtxColors, newCap * 4);
batchVtxTexCoords = memRealloc(batchVtxTexCoords, newCap * 2);
batchVtxTexBounds = memRealloc(batchVtxTexBounds, newCap * 4);
final int oldIdxCap = batchIndices.capacity();
final int newIdxCap = oldIdxCap * 2;
batchIndices = memRealloc(batchIndices, newIdxCap);
private void allocateBuffers() {
populateEBO(rawCapacity / VERTEX_SIZE);
}
private void populateEBO(int capacity) {
final int quadCount = capacity * 6;
final ByteBuffer data = memAlloc(quadCount * 6 * 2);
long ptr = memAddress0(data);
for (int i = 0; i < quadCount; i++) {
int base = (i * 4);
// triangle 1
memPutShort(ptr, (short) base);
memPutShort(ptr + 2, (short) (base + 1));
memPutShort(ptr + 4, (short) (base + 2));
// triangle 2
memPutShort(ptr + 6, (short) (base + 2));
memPutShort(ptr + 8, (short) (base + 1));
memPutShort(ptr + 10, (short) (base + 3));
ptr += 12;
}
final int idx = vtxWriterIndex;
final int idx2 = idx * 2;
final int idx4 = idx * 4;
batchVtxPositions.put(idx2, x);
batchVtxPositions.put(idx2 + 1, y);
ebo.upload(data);
memFree(data);
}
private void ensureCapacity() {
if (vertexDataPos + (4 * VERTEX_SIZE) > rawCapacity) {
rawCapacity *= 2;
vertexData = memRealloc(vertexData, rawCapacity);
vertexDataAddress = memAddress0(vertexData);
allocateBuffers();
}
}
private void pushVtx(float x, float y, int rgba, float u, float v, float uMin, float uMax, float vMin, float vMax) {
final long ptr = vertexDataAddress + vertexDataPos;
// v, v
memPutFloat(ptr, x);
memPutFloat(ptr + 4, y);
// t, t
memPutFloat(ptr + 8, u);
memPutFloat(ptr + 12, v);
// c, c, c, c
// 0xAARRGGBB
batchVtxColors.put(idx4, (byte) ((rgba >> 16) & 0xFF));
batchVtxColors.put(idx4 + 1, (byte) ((rgba >> 8) & 0xFF));
batchVtxColors.put(idx4 + 2, (byte) (rgba & 0xFF));
batchVtxColors.put(idx4 + 3, (byte) ((rgba >> 24) & 0xFF));
batchVtxTexCoords.put(idx2, u);
batchVtxTexCoords.put(idx2 + 1, v);
batchVtxTexBounds.put(idx4, uMin);
batchVtxTexBounds.put(idx4 + 1, uMax);
batchVtxTexBounds.put(idx4 + 2, vMin);
batchVtxTexBounds.put(idx4 + 3, vMax);
vtxWriterIndex++;
memPutByte(ptr + 16, (byte) ((rgba >> 16) & 0xFF));
memPutByte(ptr + 17, (byte) ((rgba >> 8) & 0xFF));
memPutByte(ptr + 18, (byte) (rgba & 0xFF));
memPutByte(ptr + 19, (byte) ((rgba >> 24) & 0xFF));
// tb, tb, tb, tb
memPutFloat(ptr + 20, uMin);
memPutFloat(ptr + 24, uMax);
memPutFloat(ptr + 28, vMin);
memPutFloat(ptr + 32, vMax);
vertexDataPos += VERTEX_SIZE;
}
private void pushUntexRect(float x, float y, float w, float h, int rgba) {
final int vtxId = vtxWriterIndex;
ensureCapacity();
pushVtx(x, y, rgba, 0, 0, 0, 0, 0, 0);
pushVtx(x, y + h, rgba, 0, 0, 0, 0, 0, 0);
pushVtx(x + w, y, rgba, 0, 0, 0, 0, 0, 0);
pushVtx(x + w, y + h, rgba, 0, 0, 0, 0, 0, 0);
pushQuadIdx(vtxId);
pushQuadIdx();
}
private void pushTexRect(float x, float y, float w, float h, float itOff, int rgba, float uStart, float vStart, float uSz, float vSz) {
final int vtxId = vtxWriterIndex;
ensureCapacity();
pushVtx(x + itOff, y, rgba, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff, y + h, rgba, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz);
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz);
pushQuadIdx(vtxId);
pushQuadIdx();
}
private int pushQuadIdx(int startV) {
final int idx = idxWriterIndex;
batchIndices.put(idx, startV);
batchIndices.put(idx + 1, startV + 1);
batchIndices.put(idx + 2, startV + 2);
//
batchIndices.put(idx + 3, startV + 2);
batchIndices.put(idx + 4, startV + 1);
batchIndices.put(idx + 5, startV + 3);
private void pushQuadIdx() {
idxWriterIndex += 6;
return idx;
}
private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture, boolean isUnicode) {
@@ -264,9 +297,6 @@ public class BatchingFontRenderer {
* allow for easier optimizing of blocks of font rendering code.
*/
public void beginBatch() {
if (batchDepth == Integer.MAX_VALUE) {
throw new StackOverflowError("More than Integer.MAX_VALUE nested font rendering batch operations");
}
batchDepth++;
}
@@ -282,22 +312,21 @@ public class BatchingFontRenderer {
}
}
private static int streamUpload(int target, int vbo, long address, int bytes, int cap) {
GL15.glBindBuffer(target, vbo);
final ByteBuffer data = memByteBuffer(address, bytes);
if (bytes > cap) {
GL15.glBufferData(target, data, GL15.GL_STREAM_DRAW);
return bytes;
}
GL15.glBufferData(target, cap, GL15.GL_STREAM_DRAW);
GL15.glBufferSubData(target, 0, data);
return cap;
}
private static final Matrix4f scratchMvp = new Matrix4f();
private int fontAAModeLast = -1;
private int fontAAStrengthLast = -1;
private void flushBatch() {
if (vertexDataPos == 0) {
clearBatch();
return;
}
// Upload first (to reduce stalls)
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
vertexData.limit(vertexDataPos);
vboCapacity = StreamingUploader.upload(vertexData, vboCapacity);
final int prevProgram = GLStateManager.glGetInteger(GL20.GL_CURRENT_PROGRAM);
// Sort&Draw
@@ -314,68 +343,49 @@ public class BatchingFontRenderer {
GLStateManager.tryBlendFuncSeparate(blendSrcRGB, blendDstRGB, GL11.GL_ONE, GL11.GL_ZERO);
GLStateManager.glShadeModel(GL11.GL_FLAT);
final boolean useFontShader = prevProgram == 0;
if (useFontShader) {
GLStateManager.glUseProgram(fontShaderId);
if (FontConfig.fontAAMode != fontAAModeLast) {
fontAAModeLast = FontConfig.fontAAMode;
GL20.glUniform1i(AAMode, FontConfig.fontAAMode);
}
if (FontConfig.fontAAStrength != fontAAStrengthLast) {
fontAAStrengthLast = FontConfig.fontAAStrength;
GL20.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
}
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), scratchMvp);
scratchMvp.get(mvpBuf);
GL20.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
}
GLStateManager.glUseProgram(fontShaderId);
if (FontConfig.fontAAMode != fontAAModeLast) {
fontAAModeLast = FontConfig.fontAAMode;
GL20.glUniform1i(AAMode, FontConfig.fontAAMode);
}
if (FontConfig.fontAAStrength != fontAAStrengthLast) {
fontAAStrengthLast = FontConfig.fontAAStrength;
GL20.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
}
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), scratchMvp);
scratchMvp.get(mvpBuf);
GL20.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
}
if (fontVAO == 0) {
fontVAO = org.lwjgl.opengl.GL30.glGenVertexArrays();
vboPositions = GL15.glGenBuffers();
vboColors = GL15.glGenBuffers();
vboTexCoords = GL15.glGenBuffers();
vboTexBounds = GL15.glGenBuffers();
vboIndices = GL15.glGenBuffers();
fontVAO = GL30.glGenVertexArrays();
GLStateManager.glBindVertexArray(fontVAO);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
ebo.bind();
// position
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, VERTEX_SIZE, 0);
GL20.glEnableVertexAttribArray(0);
// texcoords
GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, VERTEX_SIZE, 8);
GL20.glEnableVertexAttribArray(1);
// color
GL20.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, VERTEX_SIZE, 16);
GL20.glEnableVertexAttribArray(2);
// tex bounds
GL20.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, VERTEX_SIZE, 20);
GL20.glEnableVertexAttribArray(3);
}
GLStateManager.glBindVertexArray(fontVAO);
final int vtxCount = vtxWriterIndex;
// Location 0: position (vec2, float)
batchVtxPositions.position(0).limit(vtxCount * 2);
capPositions = streamUpload(GL15.GL_ARRAY_BUFFER, vboPositions, memAddress(batchVtxPositions), batchVtxPositions.remaining() * 4, capPositions);
GL20.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(0);
// Location 1: color (vec4, unsigned byte normalized)
batchVtxColors.position(0).limit(vtxCount * 4);
capColors = streamUpload(GL15.GL_ARRAY_BUFFER, vboColors, memAddress(batchVtxColors), batchVtxColors.remaining(), capColors);
GL20.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, 0, 0L);
GL20.glEnableVertexAttribArray(1);
// Location 2: texcoord0 (vec2, float)
batchVtxTexCoords.position(0).limit(vtxCount * 2);
capTexCoords = streamUpload(GL15.GL_ARRAY_BUFFER, vboTexCoords, memAddress(batchVtxTexCoords), batchVtxTexCoords.remaining() * 4, capTexCoords);
GL20.glVertexAttribPointer(2, 2, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(2);
// Location 3: texbounds (vec4, float)
batchVtxTexBounds.position(0).limit(vtxCount * 4);
capTexBounds = streamUpload(GL15.GL_ARRAY_BUFFER, vboTexBounds, memAddress(batchVtxTexBounds), batchVtxTexBounds.remaining() * 4, capTexBounds);
GL20.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, 0, 0L);
GL20.glEnableVertexAttribArray(3);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
// Stream upload index buffer
batchIndices.position(0).limit(idxWriterIndex);
capIndices = streamUpload(GL15.GL_ELEMENT_ARRAY_BUFFER, vboIndices, memAddress(batchIndices), batchIndices.remaining() * 4, capIndices);
// Use plain for loop to avoid allocations
final FontDrawCmd[] cmdsData = batchCommands.elements();
final int cmdsSize = batchCommands.size();
@@ -393,13 +403,13 @@ public class BatchingFontRenderer {
}
lastTexture = cmd.texture;
}
GL11.glDrawElements(GL11.GL_TRIANGLES, cmd.idxCount, GL11.GL_UNSIGNED_INT, (long) cmd.startVtx * 4L);
}
if (useFontShader) {
GLStateManager.glUseProgram(prevProgram);
GL11.glDrawElements(GL11.GL_TRIANGLES, cmd.idxCount, GL11.GL_UNSIGNED_SHORT, (long) cmd.startVtx * 2L);
}
GLStateManager.glUseProgram(prevProgram);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
if (isTextureEnabledBefore) {
@@ -409,16 +419,15 @@ public class BatchingFontRenderer {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, boundTextureBefore);
}
clearBatch();
}
private void clearBatch() {
// Clear for the next batch
batchCommandPool.addAll(batchCommands);
batchCommands.clear();
vtxWriterIndex = 0;
vertexDataPos = 0;
idxWriterIndex = 0;
batchVtxPositions.limit(batchVtxPositions.capacity());
batchVtxColors.limit(batchVtxColors.capacity());
batchVtxTexCoords.limit(batchVtxTexCoords.capacity());
batchVtxTexBounds.limit(batchVtxTexBounds.capacity());
batchIndices.limit(batchIndices.capacity());
}
// === Actual text mesh generation
@@ -428,7 +437,7 @@ public class BatchingFontRenderer {
}
public boolean forceDefaults() {
return this.isSGA || this.isSplash;
return this.bookMode || this.isSGA || this.isSplash;
}
public float getGlyphScaleX() {
@@ -584,7 +593,7 @@ public class BatchingFontRenderer {
// Check ASCII space, NBSP, NNBSP
if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') {
curX += 4 * this.getWhitespaceScale();
curX += 4 * this.getWhitespaceScale() + (curBold ? 1 : 0);
continue;
}
@@ -634,7 +643,8 @@ public class BatchingFontRenderer {
final int vtxCount = 4 * charCount;
pushDrawCmd(idxId, vtxCount / 2 * 3, texture, chr > 255);
curX += (xAdvance + (curBold ? shadowOffset : 0.0f)) + getGlyphSpacing();
curX += (xAdvance + (curBold ? 1.0f : 0.0f)) + getGlyphSpacing();
if (bookMode) { curX = (int) curX; }
underlineEndX = curX;
strikethroughEndX = curX;
}
@@ -97,7 +97,7 @@ public final class FontProviderUnicode implements FontProvider {
@Override
public float getShadowOffset() {
return FontConfig.fontShadowOffset;
return FontConfig.fontShadowOffset * FontConfig.fontShadowOffsetUC;
}
@Override
@@ -75,13 +75,16 @@ public class FontStrategist {
}
/**
Lets you get a FontProvider per char while respecting font priority and fallbacks, the unicode flag, whether or not
Lets you get a FontProvider per char while respecting font priority and fallbacks, the unicode flag, whether
SGA is on, if we're in a splash screen, if a font can even display a character in the first place, etc.
*/
public static FontProvider getFontProvider(BatchingFontRenderer me, char chr, boolean customFontEnabled, boolean forceUnicode) {
if (me.isSGA && FontProviderMC.get(true).isGlyphAvailable(chr)) {
return FontProviderMC.get(true);
}
if (me.bookMode) {
return FontProviderUnicode.get();
}
if (customFontEnabled && !me.isSplash) {
FontProvider fp;
fp = FontProviderCustom.getPrimary();
@@ -38,7 +38,7 @@ public class FontConfigScreen extends GuiScreen {
private ArrayList<Font> displayedFonts;
private GuiTextField searchBox;
private GuiTextField testArea;
private int sliderPages = 0;
private int lastSliderPageIndex = 0;
private int displayedSliderPage = 0;
private final int sliderWidth = 160;
private final int sliderHeight = 20;
@@ -58,6 +58,7 @@ public class FontConfigScreen extends GuiScreen {
SliderClone.Option optFontAAMode = new SliderClone.Option(0, 2, 1);
SliderClone.Option optFontAAStrength = new SliderClone.Option(1, 24, 1);
SliderClone.Option optCustomFontScale = new SliderClone.Option(0.1f, 3, 0.05f);
SliderClone.Option optShadowOffsetUC = new SliderClone.Option(0, 2, 0.05f);
public FontConfigScreen(GuiScreen parent) {
this.title = I18n.format("options.angelica.fontconfig.title");
@@ -226,6 +227,16 @@ public class FontConfigScreen extends GuiScreen {
.formatString("x%3.2f")
.build()
);
sliders.add(new SliderClone.SliderCloneBuilder()
.width(sliderWidth)
.height(sliderHeight)
.option(optShadowOffsetUC)
.initialValue(FontConfig.fontShadowOffsetUC)
.setter(value -> FontConfig.fontShadowOffsetUC = value)
.langKey("options.angelica.fontconfig.shadow_offset_unicode")
.formatString("x%3.2f")
.build()
);
final int halfWidth = this.width / 2;
final int sliderSpacing = 4;
@@ -236,11 +247,11 @@ public class FontConfigScreen extends GuiScreen {
final int y1 = this.height - 65;
final int y2 = this.height - 65 + sliderHeight + sliderSpacing;
this.sliderPages = sliders.size() / 6;
this.displayedSliderPage = Math.min(this.displayedSliderPage, this.sliderPages);
this.lastSliderPageIndex = (sliders.size() - 1) / 6;
this.displayedSliderPage = Math.min(this.displayedSliderPage, this.lastSliderPageIndex);
this.backButton.enabled = (this.displayedSliderPage != 0);
this.fwdButton.enabled = (this.displayedSliderPage != this.sliderPages);
this.fwdButton.enabled = (this.displayedSliderPage != this.lastSliderPageIndex);
final int start = 6 * this.displayedSliderPage;
final int end = Math.min(6 * (this.displayedSliderPage + 1), sliders.size());
@@ -304,7 +315,7 @@ public class FontConfigScreen extends GuiScreen {
}
private void switchPage(int offset) {
this.displayedSliderPage = MathHelper.clamp_int(this.displayedSliderPage + offset, 0, this.sliderPages);
this.displayedSliderPage = MathHelper.clamp_int(this.displayedSliderPage + offset, 0, this.lastSliderPageIndex);
this.buttonList.removeIf(guiButton -> guiButton instanceof SliderClone);
this.initSliders();
}
@@ -48,6 +48,11 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean enableMCPatcherForgeFeatures;
@Config.Comment("Replace main menu panorama with modern equivalent.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean enablePanoramaBlurShader;
@Config.Comment("Replace cloud renderer with a VBO version.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
@@ -239,4 +244,15 @@ public class AngelicaConfig {
@Config.DefaultBoolean(true)
public static boolean replaceFFPUploads;
@Config.Comment("Pinned OpenGL version an integer (e.g. 46, 41, 33). 0 = auto-detect. [33, 46]. (Disable with disableGLVersionPinning=true)")
@Config.DefaultInt(0)
@Config.RangeInt(min = 0, max = 46)
@Config.RequiresMcRestart
public static int pinnedGLVersion;
@Config.Comment("Disable automatic GL version pinning. When true, always probes from highest on every launch.")
@Config.DefaultBoolean(false)
@Config.RequiresMcRestart
public static boolean disableGLVersionPinning;
}
@@ -71,6 +71,11 @@ public class FontConfig {
@Config.RangeFloat(min = 0.1F, max = 3F)
public static float customFontScale;
@Config.Comment("A shadow offset multiplier applied only to the Unicode font.")
@Config.DefaultFloat(0.5F)
@Config.RangeFloat(min = 0F, max = 2F)
public static float fontShadowOffsetUC;
@Config.Comment({"Mods can provide Angelica with a mapping of special characters to either get replaced with " +
"different characters or rendered using the default Unicode font. Setting this to false disables the " +
"replacement, which is useful in cases where custom fonts have direct support for these special characters. ",
@@ -177,7 +177,7 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public void glDeleteBuffers(int buffer) {
GL15.glDeleteBuffers(buffer);
GLStateManager.glDeleteBuffers(buffer);
}
// ===================== TEXTURE OPERATIONS =====================
@@ -189,7 +189,7 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public void glGenTextures(int[] textures) {
IntBuffer buffer = getIntArrayBuffer(textures.length);
final IntBuffer buffer = getIntArrayBuffer(textures.length);
GL11.glGenTextures(buffer);
buffer.get(textures);
}
@@ -201,7 +201,7 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public void glDeleteTextures(int[] textures) {
IntBuffer buffer = getIntArrayBuffer(textures.length);
final IntBuffer buffer = getIntArrayBuffer(textures.length);
buffer.put(textures);
buffer.flip();
GLStateManager.glDeleteTextures(buffer);
@@ -353,7 +353,7 @@ public class AngelicaGLStateManagerService implements GLStateManagerService {
@Override
public void setBoundTexture(int unit, int texture) {
int currentUnit = GLStateManager.getActiveTextureUnit();
final int currentUnit = GLStateManager.getActiveTextureUnit();
if (currentUnit != unit) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + unit);
}
@@ -40,7 +40,7 @@ import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
public class CompatShaderTransformer {
private static final Pattern VERSION_PATTERN =
Pattern.compile("#version\\s+(\\d+)(?:\\s+(\\w+))?");
Pattern.compile("#version[ \\t]+(\\d+)(?:[ \\t]+(\\w+))?");
private static final Pattern DEFINE_PATTERN =
Pattern.compile("^\\s*#\\s*define\\s+.+$", Pattern.MULTILINE);
@@ -1,12 +1,13 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
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;
@@ -35,7 +36,6 @@ public class FeedbackManager {
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 {
@@ -58,16 +58,11 @@ public class FeedbackManager {
}
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;
}
int result = switch (renderMode) {
case GL11.GL_FEEDBACK -> (feedbackCount > feedbackBufferSize) ? -1 : feedbackCount;
case GL11.GL_RENDER, GL11.GL_SELECT -> 0;
default -> 0;
};
if (mode == GL11.GL_SELECT) {
AngelicaTweaker.LOGGER.warn("glRenderMode(GL_SELECT): selection mode not emulated");
@@ -130,12 +125,19 @@ public class FeedbackManager {
feedbackVertex(winX, winY, winZ, winW);
}
private static int positionStride() {
final int s = VertexAttribState.get(0).effectiveStride();
return (s != 0) ? s : 12;
}
private static long positionOffset() {
return VertexAttribState.get(0).offset;
}
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 stride = positionStride();
final long posOffset = positionOffset();
final int byteCount = count * stride;
ensureReadbackCapacity(byteCount);
readbackBuf.clear().limit(byteCount);
@@ -146,10 +148,9 @@ public class FeedbackManager {
public static void processDrawElements(int mode, int indexCount, int type, long eboOffset) {
computeMVP();
int stride = queryPositionStride();
if (stride == 0) stride = 12;
final int stride = positionStride();
final int elementSize = elementSizeForType(type);
final int elementSize = VertexAttribState.Attrib.glTypeSizeBytes(type);
final int indexByteCount = indexCount * elementSize;
ensureIndexReadbackCapacity(indexByteCount);
indexReadbackBuf.clear().limit(indexByteCount);
@@ -158,12 +159,12 @@ public class FeedbackManager {
int minIdx = Integer.MAX_VALUE, maxIdx = Integer.MIN_VALUE;
for (int i = 0; i < indexCount; i++) {
final int idx = readIndex(indexBuf, type, i);
final int idx = ImmediateModeRecorder.readIndex(indexBuf, type, 0, i);
if (idx < minIdx) minIdx = idx;
if (idx > maxIdx) maxIdx = idx;
}
final long posOffset = queryPositionOffset();
final long posOffset = positionOffset();
final int vertexCount = maxIdx - minIdx + 1;
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
@@ -171,7 +172,7 @@ public class FeedbackManager {
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);
emitAllPrimitives(mode, indexCount, i -> ImmediateModeRecorder.readIndex(indexBuf, type, 0, i) - base, stride);
}
public static void processDrawElements(int mode, ByteBuffer indices) {
@@ -202,8 +203,7 @@ public class FeedbackManager {
private static void processDrawElementsImpl(int mode, int count, IndexReader reader) {
computeMVP();
int stride = queryPositionStride();
if (stride == 0) stride = 12;
final int stride = positionStride();
int minIdx = Integer.MAX_VALUE, maxIdx = Integer.MIN_VALUE;
for (int i = 0; i < count; i++) {
@@ -221,12 +221,6 @@ public class FeedbackManager {
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;
@@ -238,24 +232,6 @@ public class FeedbackManager {
};
}
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());
@@ -268,13 +244,8 @@ public class FeedbackManager {
}
}
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 long posOffset = positionOffset();
final int vertexCount = maxIdx - minIdx + 1;
final int byteCount = vertexCount * stride;
ensureReadbackCapacity(byteCount);
@@ -8,6 +8,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
import com.gtnewhorizons.angelica.render.SelectionBoxRenderer;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
@@ -31,7 +32,7 @@ import com.gtnewhorizons.angelica.glsm.stacks.PointStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.PolygonStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.StencilStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.ViewPortStateStack;
import com.gtnewhorizons.angelica.glsm.states.ClientArrayState;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.Color4;
import com.gtnewhorizons.angelica.glsm.states.TextureBinding;
@@ -285,8 +286,6 @@ public class GLStateManager {
private static int clientActiveTextureUnit = 0;
protected static final IntegerStateStack shadeModelState = new IntegerStateStack(GL11.GL_SMOOTH);
// Client array state (not push/pop tracked, but needed for display list compilation)
@Getter private static final ClientArrayState clientArrayState = new ClientArrayState();
@Getter protected static final TextureUnitArray textures = new TextureUnitArray();
@Getter protected static final BlendStateStack blendState = new BlendStateStack();
@@ -337,6 +336,10 @@ public class GLStateManager {
// Line width range queried at init from GL_ALIASED_LINE_WIDTH_RANGE
static float lineWidthMin = 1.0f;
static float lineWidthMax = 1.0f;
/** True when the driver cannot render wide lines natively (Mesa forward-compat). GS emulation handles widths > 1.0. */
public static boolean wideLineEmulationEnabled = false;
/** Set per draw call: true when the current draw is a line primitive that needs GS expansion. Read by {@link com.gtnewhorizons.angelica.glsm.ffp.VertexKey}. */
public static boolean wideLineEmulationActive = false;
// Point state (GL_POINT_BIT)
@Getter protected static final PointStateStack pointState = new PointStateStack();
@@ -441,6 +444,7 @@ public class GLStateManager {
modelViewMatrix.clear();
projectionMatrix.clear();
vaoEboMap.clear();
VertexAttribState.reset();
}
/**
@@ -498,6 +502,11 @@ public class GLStateManager {
final String glVendor = GL11.glGetString(GL11.GL_VENDOR);
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
// The initial mask value should be defined as all 1's. However, some drivers have it set to 0's.
// To ensure consistency & correctness across all drivers, we're setting them to 0xFF.
GL11.glStencilFunc(stencilState.getFuncFront(), stencilState.getRefFront(), 0xFF);
GL11.glStencilMask(0xFF);
// Compute stencil bit mask — driver clamps stencil masks to buffer depth
// GL_STENCIL_BITS was removed in core profile; query via default FBO attachment
final int stencilBits = GL30.glGetFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE);
@@ -513,6 +522,35 @@ public class GLStateManager {
GL11.glGetFloat(GL12.GL_ALIASED_LINE_WIDTH_RANGE, lwRange);
lineWidthMin = lwRange.get(0);
lineWidthMax = lwRange.get(1);
final float queriedMax = lineWidthMax;
// Probe whether the driver actually accepts the queried max (Mesa forward-compat rejects > 1.0)
if (lineWidthMax > 1.0f) {
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GL11.glLineWidth(lineWidthMax);
if (GL11.glGetError() != GL11.GL_NO_ERROR) {
// Binary search for the actual accepted max between 1.0 and queriedMax
float lo = 1.0f, hi = queriedMax;
while (hi - lo > 0.01f) {
final float mid = (lo + hi) * 0.5f;
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GL11.glLineWidth(mid);
if (GL11.glGetError() == GL11.GL_NO_ERROR) {
lo = mid;
} else {
hi = mid;
}
}
lineWidthMax = lo;
}
GL11.glLineWidth(1.0f);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
wideLineEmulationEnabled = lineWidthMax <= 1.0f;
if (wideLineEmulationEnabled) {
LOGGER.info("GL line width: aliased range [{}, {}], probe at {} rejected, native [{}, {}], GS emulation active for [{}, {}]", lineWidthMin, queriedMax, queriedMax, lineWidthMin, lineWidthMax, lineWidthMax, queriedMax);
} else {
LOGGER.info("GL line width: aliased range [{}, {}], probe at {} accepted, native [{}, {}]", lineWidthMin, queriedMax, queriedMax, lineWidthMin, lineWidthMax);
}
// Sync default vertex attribs with GLSM cache initial values.
GL20.glVertexAttrib4f(1, 1.0f, 1.0f, 1.0f, 1.0f); // COLOR = white
@@ -545,6 +583,8 @@ public class GLStateManager {
defaultVAO = GL30.glGenVertexArrays();
GL30.glBindVertexArray(defaultVAO);
boundVAO = defaultVAO;
VertexAttribState.init(defaultVAO);
SelectionBoxRenderer.init();
// Drain any pending GL errors from initialization. In core profile, some legacy queries may generate GL_INVALID_ENUM. The splash thread inherits the
// DrawableGL context and its error state, so stale errors here would cause SplashProgress.checkGLError() to fail.
@@ -1537,6 +1577,7 @@ public class GLStateManager {
}
}
alphaTest.enable();
fragmentGeneration++;
}
public static void disableAlphaTest() {
@@ -1554,6 +1595,7 @@ public class GLStateManager {
}
}
alphaTest.disable();
fragmentGeneration++;
}
public static void glAlphaFunc(int function, float reference) {
@@ -1673,7 +1715,18 @@ public class GLStateManager {
}
}
private static int changeFormatIfDeprecated(int internalformat) {
switch (internalformat) {
case GL11.GL_ALPHA4 -> internalformat = GL11.GL_RGBA4;
case GL11.GL_ALPHA8 -> internalformat = GL11.GL_RGBA8;
case GL11.GL_ALPHA12 -> internalformat = GL11.GL_RGBA12;
case GL11.GL_ALPHA16 -> internalformat = GL11.GL_RGBA16;
}
return internalformat;
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
internalformat = changeFormatIfDeprecated(internalformat);
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromIntBuffer(target, level, internalformat, width, height, border, format, type, pixels));
@@ -1692,6 +1745,7 @@ public class GLStateManager {
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, FloatBuffer pixels) {
internalformat = changeFormatIfDeprecated(internalformat);
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromFloatBuffer(target, level, internalformat, width, height, border, format, type, pixels));
@@ -1705,6 +1759,7 @@ public class GLStateManager {
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, DoubleBuffer pixels) {
internalformat = changeFormatIfDeprecated(internalformat);
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromDoubleBuffer(target, level, internalformat, width, height, border, format, type, pixels));
@@ -1718,6 +1773,7 @@ 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) {
internalformat = changeFormatIfDeprecated(internalformat);
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordComplexCommand(TexImage2DCmd.fromByteBuffer(target, level, internalformat, width, height, border, format, type, pixels));
@@ -1736,6 +1792,7 @@ public class GLStateManager {
}
public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) {
internalformat = changeFormatIfDeprecated(internalformat);
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glTexImage2D with buffer offset in display lists not yet supported");
}
@@ -1992,6 +2049,12 @@ public class GLStateManager {
}
}
public static void glVertex2i(int x, int y) {
if (DisplayListManager.isRecording() || ImmediateModeRecorder.isDrawing()) {
ImmediateModeRecorder.vertex((float) x, (float) y, 0.0f);
}
}
public static void glVertex2d(double x, double y) {
if (DisplayListManager.isRecording() || ImmediateModeRecorder.isDrawing()) {
ImmediateModeRecorder.vertex((float) x, (float) y, 0.0f);
@@ -2009,6 +2072,13 @@ public class GLStateManager {
ImmediateModeRecorder.vertex((float) x, (float) y, (float) z);
}
}
public static void glVertex3i(int x, int y, int z) {
if (DisplayListManager.isRecording() || ImmediateModeRecorder.isDrawing()) {
ImmediateModeRecorder.vertex((float) x, (float) y, (float) z);
}
}
public static void glDrawElements(int mode, ByteBuffer indices) {
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
@@ -2017,6 +2087,7 @@ public class GLStateManager {
FeedbackManager.processDrawElements(mode, indices);
return;
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
GL11.glDrawElements(mode, indices);
}
@@ -2029,6 +2100,7 @@ public class GLStateManager {
FeedbackManager.processDrawElements(mode, indices);
return;
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
@@ -2045,6 +2117,7 @@ public class GLStateManager {
FeedbackManager.processDrawElements(mode, indices);
return;
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
@@ -2061,6 +2134,7 @@ public class GLStateManager {
FeedbackManager.processDrawElements(mode, count, type, indices);
return;
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
@@ -2074,6 +2148,11 @@ public class GLStateManager {
if (recordMode != RecordMode.NONE) {
if (isVAOBound()) {
DisplayListManager.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
} else if (isVBOBound() || VertexAttribState.hasVBOBoundAttrib()) {
final DirectTessellator result = ImmediateModeRecorder.processDrawElementsFromVBO(mode, indices_count, type, indices_buffer_offset, boundEBO);
if (result != null) {
DisplayListManager.addImmediateModeDraw(result);
}
} else {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
}
@@ -2085,6 +2164,7 @@ public class GLStateManager {
FeedbackManager.processDrawElements(mode, indices_count, type, indices_buffer_offset);
return;
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset);
@@ -2107,11 +2187,7 @@ public class GLStateManager {
public static void glDrawArrays(int mode, int first, int count) {
if (DisplayListManager.isRecording()) {
if (isVAOBound() || isVBOBound()) { // TODO VBO rendering needs to be replaced with proper VAO compilation
DisplayListManager.recordDrawArrays(mode, first, count);
return;
}
final DirectTessellator result = ImmediateModeRecorder.processDrawArrays(mode, first, count);
final DirectTessellator result = ImmediateModeRecorder.processDrawArraysFromAttribs(mode, first, count);
if (result != null) {
DisplayListManager.addImmediateModeDraw(result);
}
@@ -2121,6 +2197,7 @@ public class GLStateManager {
FeedbackManager.processDrawArrays(mode, first, count);
return;
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadsAsTriangles(first, count);
@@ -2129,9 +2206,6 @@ public class GLStateManager {
}
}
// Client array state interception methods (for glDrawArrays conversion)
// State is tracked globally in clientArrayState, which ImmediateModeRecorder reads from directly.
public static void glVertexPointer(int size, int stride, IntBuffer pointer) {
glVertexPointer(size, GL11.GL_INT, stride, MemoryUtilities.memByteBuffer(pointer));
}
@@ -2141,32 +2215,36 @@ public class GLStateManager {
}
public static void glVertexPointer(int size, int stride, FloatBuffer pointer) {
GL20.glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
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) {
GL20.glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
}
public static void glVertexPointer(int size, int type, int stride, long pointer_buffer_offset) {
GL20.glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer_buffer_offset);
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer_buffer_offset);
}
public static void glColorPointer(int size, int stride, FloatBuffer pointer) {
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_FLOAT, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_FLOAT, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glColorPointer(int size, int stride, DoubleBuffer pointer) {
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_DOUBLE, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glColorPointer(int size, int type, int stride, ByteBuffer pointer) {
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
}
public static void glColorPointer(int size, boolean unsigned, int stride, ByteBuffer pointer) {
final int type = unsigned ? GL11.GL_UNSIGNED_BYTE : GL11.GL_BYTE;
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
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) {
GL20.glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer_buffer_offset);
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer_buffer_offset);
}
public static void glNormalPointer(int stride, FloatBuffer pointer) {
@@ -2182,29 +2260,29 @@ public class GLStateManager {
}
public static void glNormalPointer(int type, int stride, ByteBuffer pointer) {
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
}
public static void glNormalPointer(int type, int stride, FloatBuffer pointer) {
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_FLOAT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
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) {
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_SHORT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
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) {
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_INT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
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) {
GL20.glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer_buffer_offset);
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) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, type, false, stride, pointer);
glVertexAttribPointer(loc, size, type, false, stride, pointer);
}
public static void glTexCoordPointer(int size, int stride, FloatBuffer pointer) {
@@ -2222,41 +2300,111 @@ public class GLStateManager {
public static void glTexCoordPointer(int size, int type, int stride, FloatBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
glVertexAttribPointer(loc, size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, ShortBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, GL11.GL_SHORT, false, stride, MemoryUtilities.memByteBuffer(pointer));
glVertexAttribPointer(loc, size, GL11.GL_SHORT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, IntBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, GL11.GL_INT, false, stride, MemoryUtilities.memByteBuffer(pointer));
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) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
GL20.glVertexAttribPointer(loc, size, type, false, stride, pointer_buffer_offset);
glVertexAttribPointer(loc, size, type, false, stride, pointer_buffer_offset);
}
public static void glEnableClientState(int cap) {
final int location = clientStateToAttributeLocation(cap);
if (location >= 0) GL20.glEnableVertexAttribArray(location);
if (location >= 0) glEnableVertexAttribArray(location);
final int flag = clientStateToVertexFlag(cap);
if (flag != 0) ShaderManager.getInstance().enableClientVertexFlag(flag);
}
public static void glDisableClientState(int cap) {
final int location = clientStateToAttributeLocation(cap);
if (location >= 0) GL20.glDisableVertexAttribArray(location);
if (location >= 0) glDisableVertexAttribArray(location);
final int flag = clientStateToVertexFlag(cap);
if (flag != 0) ShaderManager.getInstance().disableClientVertexFlag(flag);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, long offset) {
VertexAttribState.set(index, size, type, normalized, stride, offset, boundVBO);
GL20.glVertexAttribPointer(index, size, type, normalized, stride, offset);
}
public static void glVertexAttribPointer(int index, int size, boolean normalized, int stride, FloatBuffer pointer) {
glVertexAttribPointer(index, size, GL11.GL_FLOAT, normalized, stride, pointer);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, FloatBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
GL20.glVertexAttribPointer(index, size, type, normalized, stride, bb);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, DoubleBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
GL20.glVertexAttribPointer(index, size, type, normalized, stride, bb);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, IntBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
GL20.glVertexAttribPointer(index, size, type, normalized, stride, bb);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ShortBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
GL20.glVertexAttribPointer(index, size, type, normalized, stride, bb);
}
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer) {
VertexAttribState.set(index, size, type, normalized, stride, pointer, boundVBO);
GL20.glVertexAttribPointer(index, size, type, normalized, stride, pointer);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, long offset) {
VertexAttribState.set(index, size, type, false, stride, offset, boundVBO);
GL30.glVertexAttribIPointer(index, size, type, stride, offset);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, ByteBuffer pointer) {
VertexAttribState.set(index, size, type, false, stride, pointer, boundVBO);
GL30.glVertexAttribIPointer(index, size, type, stride, pointer);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, IntBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, false, stride, bb, boundVBO);
GL30.glVertexAttribIPointer(index, size, type, stride, bb);
}
public static void glVertexAttribIPointer(int index, int size, int type, int stride, ShortBuffer pointer) {
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
VertexAttribState.set(index, size, type, false, stride, bb, boundVBO);
GL30.glVertexAttribIPointer(index, size, type, stride, bb);
}
public static void glEnableVertexAttribArray(int index) {
VertexAttribState.setEnabled(index, true);
GL20.glEnableVertexAttribArray(index);
}
public static void glDisableVertexAttribArray(int index) {
VertexAttribState.setEnabled(index, false);
GL20.glDisableVertexAttribArray(index);
}
private static int clientStateToAttributeLocation(int cap) {
return switch (cap) {
case GL11.GL_VERTEX_ARRAY -> Usage.POSITION.getAttributeLocation();
@@ -2908,7 +3056,7 @@ public class GLStateManager {
GL11.glDepthRange(viewportState.depthRangeNear, viewportState.depthRangeFar);
}
if ((mask & GL11.GL_LINE_BIT) != 0) {
GL11.glLineWidth(lineState.getWidth());
GL11.glLineWidth(Math.max(lineWidthMin, Math.min(lineWidthMax, lineState.getWidth())));
}
if ((mask & GL11.GL_POINT_BIT) != 0) {
GL11.glPointSize(pointState.getSize());
@@ -3891,6 +4039,7 @@ public class GLStateManager {
// Missing GL commands from Mesa cross-check
public static void glTexImage1D(int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) {
internalformat = changeFormatIfDeprecated(internalformat);
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glTexImage1D in display lists not yet implemented");
}
@@ -3898,6 +4047,15 @@ public class GLStateManager {
}
public static void glTexImage3D(int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) {
internalformat = changeFormatIfDeprecated(internalformat);
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glTexImage3D in display lists not yet implemented");
}
GL12.glTexImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels);
}
public static void glTexImage3D(int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, IntBuffer pixels) {
internalformat = changeFormatIfDeprecated(internalformat);
if (DisplayListManager.isRecording()) {
throw new UnsupportedOperationException("glTexImage3D in display lists not yet implemented");
}
@@ -3912,7 +4070,6 @@ public class GLStateManager {
}
public static void glLineWidth(float width) {
width = Math.max(lineWidthMin, Math.min(lineWidthMax, width));
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
DisplayListManager.recordLineWidth(width);
@@ -3925,10 +4082,14 @@ public class GLStateManager {
if (caching) {
lineState.setWidth(width);
}
GL11.glLineWidth(width);
GL11.glLineWidth(Math.max(lineWidthMin, Math.min(lineWidthMax, width)));
}
}
public static void prepareWideLineEmulation(int drawMode) {
wideLineEmulationActive = wideLineEmulationEnabled && (drawMode == GL11.GL_LINES || drawMode == GL11.GL_LINE_STRIP || drawMode == GL11.GL_LINE_LOOP) && lineState.getWidth() > 1.0f;
}
public static void glShaderSource(int shader, CharSequence source) {
String src = source.toString();
if (ShaderManager.getInstance().isEnabled()) {
@@ -4454,6 +4615,30 @@ public class GLStateManager {
}
}
public static void glDeleteBuffers(int buffer) {
if (buffer == 0) return;
invalidateDeletedBuffer(buffer);
GL15.glDeleteBuffers(buffer);
}
public static void glDeleteBuffers(IntBuffer buffers) {
for (int i = 0; i < buffers.remaining(); i++) {
invalidateDeletedBuffer(buffers.get(buffers.position() + i));
}
GL15.glDeleteBuffers(buffers);
}
private static void invalidateDeletedBuffer(int buffer) {
if (buffer == 0) return;
if (boundVBO == buffer) {
boundVBO = 0;
}
if (boundEBO == buffer) {
boundEBO = 0;
vaoEboMap.put(boundVAO, 0);
}
}
public static void glBindBuffer(int target, int buffer) {
if (target == GL15.GL_ARRAY_BUFFER) {
// if (boundVBO == buffer) return; TODO figure out why this breaks switching async occlusion mode
@@ -4477,6 +4662,7 @@ public class GLStateManager {
if (boundVAO != array) {
boundVAO = array;
boundEBO = vaoEboMap.get(array);
VertexAttribState.onBindVertexArray(array);
UniversalVAO.bindVertexArray(array);
if (ShaderManager.getInstance().isEnabled()) {
ShaderManager.getInstance().onBindVertexArray(array);
@@ -4486,11 +4672,13 @@ public class GLStateManager {
public static void glDeleteVertexArrays(int array) {
ShaderManager.getInstance().onDeleteVertexArray(array);
VertexAttribState.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);
VertexAttribState.onBindVertexArray(defaultVAO);
UniversalVAO.bindVertexArray(defaultVAO);
}
UniversalVAO.deleteVertexArrays(array);
@@ -4777,6 +4965,62 @@ public class GLStateManager {
}
}
public static float glGetTexEnvf(int target, int pname) {
return (float) glGetTexEnvi(target, pname);
}
public static int glGetTexEnvi(int target, int pname) {
if (target != GL11.GL_TEXTURE_ENV) return 0;
final var envState = textures.getTexEnvState(activeTextureUnit.getValue());
return switch (pname) {
case GL11.GL_TEXTURE_ENV_MODE -> envState.mode;
case GL13.GL_COMBINE_RGB -> envState.combineRgb;
case GL13.GL_COMBINE_ALPHA -> envState.combineAlpha;
case GL13.GL_SOURCE0_RGB -> envState.sourceRgb[0];
case GL13.GL_SOURCE1_RGB -> envState.sourceRgb[1];
case GL13.GL_SOURCE2_RGB -> envState.sourceRgb[2];
case GL13.GL_SOURCE0_ALPHA -> envState.sourceAlpha[0];
case GL13.GL_SOURCE1_ALPHA -> envState.sourceAlpha[1];
case GL13.GL_SOURCE2_ALPHA -> envState.sourceAlpha[2];
case GL13.GL_OPERAND0_RGB -> envState.operandRgb[0];
case GL13.GL_OPERAND1_RGB -> envState.operandRgb[1];
case GL13.GL_OPERAND2_RGB -> envState.operandRgb[2];
case GL13.GL_OPERAND0_ALPHA -> envState.operandAlpha[0];
case GL13.GL_OPERAND1_ALPHA -> envState.operandAlpha[1];
case GL13.GL_OPERAND2_ALPHA -> envState.operandAlpha[2];
case GL13.GL_RGB_SCALE -> (int) envState.scaleRgb;
case GL11.GL_ALPHA_SCALE -> (int) envState.scaleAlpha;
default -> 0;
};
}
public static void glGetTexEnv(int target, int pname, IntBuffer params) {
if (target != GL11.GL_TEXTURE_ENV) return;
if (pname == GL11.GL_TEXTURE_ENV_COLOR) {
final var envState = textures.getTexEnvState(activeTextureUnit.getValue());
params.put(params.position(), (int) (envState.envColorR * 2147483647.0f));
params.put(params.position() + 1, (int) (envState.envColorG * 2147483647.0f));
params.put(params.position() + 2, (int) (envState.envColorB * 2147483647.0f));
params.put(params.position() + 3, (int) (envState.envColorA * 2147483647.0f));
} else {
params.put(params.position(), glGetTexEnvi(target, pname));
}
}
public static void glGetTexEnv(int target, int pname, FloatBuffer params) {
if (target != GL11.GL_TEXTURE_ENV) return;
if (pname == GL11.GL_TEXTURE_ENV_COLOR) {
final var envState = textures.getTexEnvState(activeTextureUnit.getValue());
params.put(params.position(), envState.envColorR);
params.put(params.position() + 1, envState.envColorG);
params.put(params.position() + 2, envState.envColorB);
params.put(params.position() + 3, envState.envColorA);
} else {
params.put(params.position(), (float) glGetTexEnvi(target, pname));
}
}
// TexGen generation counter for FFP uniform dirty tracking
public static int texGenGeneration;
@@ -4863,6 +5107,65 @@ public class GLStateManager {
}
}
public static int glGetTexGeni(int coord, int pname) {
if (pname == GL11.GL_TEXTURE_GEN_MODE) {
return textures.getTexGenState(activeTextureUnit.getValue()).getMode(coord);
}
return 0;
}
public static float glGetTexGenf(int coord, int pname) {
return (float) glGetTexGeni(coord, pname);
}
public static double glGetTexGend(int coord, int pname) {
return glGetTexGeni(coord, pname);
}
public static void glGetTexGen(int coord, int pname, IntBuffer params) {
if (pname == GL11.GL_TEXTURE_GEN_MODE) {
params.put(params.position(), glGetTexGeni(coord, pname));
}
}
public static void glGetTexGen(int coord, int pname, FloatBuffer params) {
final var state = textures.getTexGenState(activeTextureUnit.getValue());
if (pname == GL11.GL_TEXTURE_GEN_MODE) {
params.put(params.position(), (float) state.getMode(coord));
} else if (pname == GL11.GL_OBJECT_PLANE) {
final float[] plane = state.getObjectPlane(coord);
params.put(params.position(), plane[0]);
params.put(params.position() + 1, plane[1]);
params.put(params.position() + 2, plane[2]);
params.put(params.position() + 3, plane[3]);
} else if (pname == GL11.GL_EYE_PLANE) {
final float[] plane = state.getEyePlane(coord);
params.put(params.position(), plane[0]);
params.put(params.position() + 1, plane[1]);
params.put(params.position() + 2, plane[2]);
params.put(params.position() + 3, plane[3]);
}
}
public static void glGetTexGen(int coord, int pname, DoubleBuffer params) {
final var state = textures.getTexGenState(activeTextureUnit.getValue());
if (pname == GL11.GL_TEXTURE_GEN_MODE) {
params.put(params.position(), state.getMode(coord));
} else if (pname == GL11.GL_OBJECT_PLANE) {
final float[] plane = state.getObjectPlane(coord);
params.put(params.position(), plane[0]);
params.put(params.position() + 1, plane[1]);
params.put(params.position() + 2, plane[2]);
params.put(params.position() + 3, plane[3]);
} else if (pname == GL11.GL_EYE_PLANE) {
final float[] plane = state.getEyePlane(coord);
params.put(params.position(), plane[0]);
params.put(params.position() + 1, plane[1]);
params.put(params.position() + 2, plane[2]);
params.put(params.position() + 3, plane[3]);
}
}
public static void glPolygonStipple(ByteBuffer pattern) {
guardUnsupportedFFP("glPolygonStipple", "glPolygonStipple is not available in GL 3.3 core profile.");
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -142,13 +143,7 @@ public final class QuadConverter {
final int quadCount = indexCount / 4;
final int triIndexCount = quadCount * 6;
final int bytesPerIndex;
switch (type) {
case GL11.GL_UNSIGNED_INT: bytesPerIndex = 4; break;
case GL11.GL_UNSIGNED_SHORT: bytesPerIndex = 2; break;
case GL11.GL_UNSIGNED_BYTE: bytesPerIndex = 1; break;
default: return;
}
final int bytesPerIndex = VertexAttribState.Attrib.glTypeSizeBytes(type);
// Read source indices from caller's EBO
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
@@ -132,7 +132,6 @@ public class RenderSystem {
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
LOGGER.info("GL 3.3 core profile detected, enabling FFP shader emulation.");
ShaderManager.getInstance().enable();
GLStateManager.lineWidthMax = 1.0f;
}
}
}
@@ -0,0 +1,142 @@
package com.gtnewhorizons.angelica.glsm.compat.lwjgl;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.util.glu.Cylinder;
import static org.lwjgl.opengl.GL11.GL_LINES;
import static org.lwjgl.opengl.GL11.GL_LINE_LOOP;
import static org.lwjgl.opengl.GL11.GL_POINTS;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.util.glu.GLU.GLU_FILL;
import static org.lwjgl.util.glu.GLU.GLU_INSIDE;
import static org.lwjgl.util.glu.GLU.GLU_LINE;
import static org.lwjgl.util.glu.GLU.GLU_POINT;
import static org.lwjgl.util.glu.GLU.GLU_SILHOUETTE;
/**
* Core-profile compatible replacement for {@link Cylinder}.
*/
@SuppressWarnings("unused")
public class AngelicaCylinder extends Cylinder {
private static final float PI = (float) Math.PI;
private void emitNormal(float x, float y, float z) {
final float mag = (float) Math.sqrt(x * x + y * y + z * z);
if (mag > 0.00001F) {
x /= mag;
y /= mag;
z /= mag;
}
GLStateManager.glNormal3f(x, y, z);
}
private void emitTexCoord(float x, float y) {
if (super.textureFlag) GLStateManager.glTexCoord2f(x, y);
}
@Override
public void draw(float baseRadius, float topRadius, float height, int slices, int stacks) {
final float da, dr, dz, nz, nsign;
float r, x, y, z;
nsign = (super.orientation == GLU_INSIDE) ? -1.0f : 1.0f;
da = 2.0f * PI / slices;
dr = (topRadius - baseRadius) / stacks;
dz = height / stacks;
nz = (baseRadius - topRadius) / height;
if (super.drawStyle == GLU_POINT) {
GLStateManager.glBegin(GL_POINTS);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
z = 0.0f;
r = baseRadius;
for (int j = 0; j <= stacks; j++) {
GLStateManager.glVertex3f(x * r, y * r, z);
z += dz;
r += dr;
}
}
GLStateManager.glEnd();
} else if (super.drawStyle == GLU_LINE || super.drawStyle == GLU_SILHOUETTE) {
// Draw rings
if (super.drawStyle == GLU_LINE) {
z = 0.0f;
r = baseRadius;
for (int j = 0; j <= stacks; j++) {
GLStateManager.glBegin(GL_LINE_LOOP);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * r, y * r, z);
}
GLStateManager.glEnd();
z += dz;
r += dr;
}
} else {
// draw one ring at each end
if (baseRadius != 0.0) {
GLStateManager.glBegin(GL_LINE_LOOP);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * baseRadius, y * baseRadius, 0.0f);
}
GLStateManager.glEnd();
GLStateManager.glBegin(GL_LINE_LOOP);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * topRadius, y * topRadius, height);
}
GLStateManager.glEnd();
}
}
// draw length lines
GLStateManager.glBegin(GL_LINES);
for (int i = 0; i < slices; i++) {
x = cos(i * da);
y = sin(i * da);
emitNormal(x * nsign, y * nsign, nz * nsign);
GLStateManager.glVertex3f(x * baseRadius, y * baseRadius, 0.0f);
GLStateManager.glVertex3f(x * topRadius, y * topRadius, height);
}
GLStateManager.glEnd();
} else if (super.drawStyle == GLU_FILL) {
final float ds = 1.0f / slices;
final float dt = 1.0f / stacks;
float t = 0.0f;
z = 0.0f;
r = baseRadius;
for (int j = 0; j < stacks; j++) {
float s = 0.0f;
GLStateManager.glBegin(GL_TRIANGLE_STRIP);
for (int i = 0; i <= slices; i++) {
final float angle = (i == slices) ? 0.0f : i * da;
x = sin(angle);
y = cos(angle);
emitNormal(x * nsign, y * nsign, nz * nsign);
emitTexCoord(s, t);
GLStateManager.glVertex3f(x * r, y * r, z);
emitNormal(x * nsign, y * nsign, nz * nsign);
emitTexCoord(s, t + dt);
GLStateManager.glVertex3f(x * (r + dr), y * (r + dr), z + dz);
s += ds;
}
GLStateManager.glEnd();
r += dr;
t += dt;
z += dz;
}
}
}
}
@@ -0,0 +1,134 @@
package com.gtnewhorizons.angelica.glsm.compat.lwjgl;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.util.glu.Disk;
import static org.lwjgl.opengl.GL11.GL_LINE_LOOP;
import static org.lwjgl.opengl.GL11.GL_LINE_STRIP;
import static org.lwjgl.opengl.GL11.GL_POINTS;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.util.glu.GLU.GLU_FILL;
import static org.lwjgl.util.glu.GLU.GLU_LINE;
import static org.lwjgl.util.glu.GLU.GLU_NONE;
import static org.lwjgl.util.glu.GLU.GLU_OUTSIDE;
import static org.lwjgl.util.glu.GLU.GLU_POINT;
import static org.lwjgl.util.glu.GLU.GLU_SILHOUETTE;
/**
* Core-profile compatible replacement for {@link Disk}.
*/
@SuppressWarnings("unused")
public class AngelicaDisk extends Disk {
private static final float PI = (float) Math.PI;
private void emitTexCoord(float x, float y) {
if (super.textureFlag) GLStateManager.glTexCoord2f(x, y);
}
@Override
public void draw(float innerRadius, float outerRadius, int slices, int loops) {
final float da, dr;
if (super.normals != GLU_NONE) {
GLStateManager.glNormal3f(0.0f, 0.0f, (super.orientation == GLU_OUTSIDE) ? 1.0f : -1.0f);
}
da = 2.0f * PI / slices;
dr = (outerRadius - innerRadius) / loops;
switch (super.drawStyle) {
case GLU_FILL -> {
final float dtc = 2.0f * outerRadius;
float sa, ca;
float r1 = innerRadius;
for (int l = 0; l < loops; l++) {
final float r2 = r1 + dr;
if (super.orientation == GLU_OUTSIDE) {
GLStateManager.glBegin(GL_TRIANGLE_STRIP);
for (int s = 0; s <= slices; s++) {
final float a = (s == slices) ? 0.0f : s * da;
sa = sin(a);
ca = cos(a);
emitTexCoord(0.5f + sa * r2 / dtc, 0.5f + ca * r2 / dtc);
GLStateManager.glVertex2f(r2 * sa, r2 * ca);
emitTexCoord(0.5f + sa * r1 / dtc, 0.5f + ca * r1 / dtc);
GLStateManager.glVertex2f(r1 * sa, r1 * ca);
}
GLStateManager.glEnd();
} else {
GLStateManager.glBegin(GL_TRIANGLE_STRIP);
for (int s = slices; s >= 0; s--) {
final float a = (s == slices) ? 0.0f : s * da;
sa = sin(a);
ca = cos(a);
emitTexCoord(0.5f - sa * r2 / dtc, 0.5f + ca * r2 / dtc);
GLStateManager.glVertex2f(r2 * sa, r2 * ca);
emitTexCoord(0.5f - sa * r1 / dtc, 0.5f + ca * r1 / dtc);
GLStateManager.glVertex2f(r1 * sa, r1 * ca);
}
GLStateManager.glEnd();
}
r1 = r2;
}
}
case GLU_LINE -> {
/* draw loops */
for (int l = 0; l <= loops; l++) {
final float r = innerRadius + l * dr;
GLStateManager.glBegin(GL_LINE_LOOP);
for (int s = 0; s < slices; s++) {
final float a = s * da;
GLStateManager.glVertex2f(r * sin(a), r * cos(a));
}
GLStateManager.glEnd();
}
/* draw spokes */
for (int s = 0; s < slices; s++) {
final float a = s * da;
final float x = sin(a);
final float y = cos(a);
GLStateManager.glBegin(GL_LINE_STRIP);
for (int l = 0; l <= loops; l++) {
final float r = innerRadius + l * dr;
GLStateManager.glVertex2f(r * x, r * y);
}
GLStateManager.glEnd();
}
}
case GLU_POINT -> {
GLStateManager.glBegin(GL_POINTS);
for (int s = 0; s < slices; s++) {
final float a = s * da;
final float x = sin(a);
final float y = cos(a);
for (int l = 0; l <= loops; l++) {
final float r = innerRadius * l * dr;
GLStateManager.glVertex2f(r * x, r * y);
}
}
GLStateManager.glEnd();
}
case GLU_SILHOUETTE -> {
if (innerRadius != 0.0) {
GLStateManager.glBegin(GL_LINE_LOOP);
for (float a = 0.0f; a < 2.0 * PI; a += da) {
final float x = innerRadius * sin(a);
final float y = innerRadius * cos(a);
GLStateManager.glVertex2f(x, y);
}
GLStateManager.glEnd();
}
GLStateManager.glBegin(GL_LINE_LOOP);
for (float a = 0; a < 2.0f * PI; a += da) {
final float x = outerRadius * sin(a);
final float y = outerRadius * cos(a);
GLStateManager.glVertex2f(x, y);
}
GLStateManager.glEnd();
}
default -> {
}
}
}
}
@@ -0,0 +1,244 @@
package com.gtnewhorizons.angelica.glsm.compat.lwjgl;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.util.glu.PartialDisk;
import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER;
import static org.lwjgl.opengl.GL11.GL_LINE_STRIP;
import static org.lwjgl.opengl.GL11.GL_POINTS;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_FAN;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.util.glu.GLU.GLU_FILL;
import static org.lwjgl.util.glu.GLU.GLU_FLAT;
import static org.lwjgl.util.glu.GLU.GLU_LINE;
import static org.lwjgl.util.glu.GLU.GLU_OUTSIDE;
import static org.lwjgl.util.glu.GLU.GLU_POINT;
import static org.lwjgl.util.glu.GLU.GLU_SILHOUETTE;
import static org.lwjgl.util.glu.GLU.GLU_SMOOTH;
/**
* Core-profile compatible replacement for {@link PartialDisk}.
*/
@SuppressWarnings("unused")
public class AngelicaPartialDisk extends PartialDisk {
private static final float PI = (float) Math.PI;
private static final int CACHE_SIZE = 240;
private final float[] sinCache = new float[CACHE_SIZE];
private final float[] cosCache = new float[CACHE_SIZE];
@Override
public void draw(float innerRadius, float outerRadius, int slices, int loops, float startAngle, float sweepAngle) {
final float deltaRadius, angleOffset;
float angle, sintemp, costemp, radiusLow, radiusHigh, texLow = 0, texHigh = 0;
final int slices2, finish;
if (slices >= CACHE_SIZE) slices = CACHE_SIZE - 1;
if (slices < 2 || loops < 1 || outerRadius <= 0.0f || innerRadius < 0.0f || innerRadius > outerRadius) {
LOGGER.warn("PartialDisk: GLU_INVALID_VALUE (slices={}, loops={}, innerRadius={}, outerRadius={})", slices, loops, innerRadius, outerRadius);
return;
}
if (sweepAngle < -360.0f) sweepAngle = 360.0f;
if (sweepAngle > 360.0f) sweepAngle = 360.0f;
if (sweepAngle < 0) {
startAngle += sweepAngle;
sweepAngle = -sweepAngle;
}
slices2 = (sweepAngle == 360.0f) ? slices : slices + 1;
/* Compute length (needed for normal calculations) */
deltaRadius = outerRadius - innerRadius;
angleOffset = startAngle / 180.0f * PI;
for (int i = 0; i <= slices; i++) {
angle = angleOffset + ((PI * sweepAngle) / 180.0f) * i / slices;
sinCache[i] = sin(angle);
cosCache[i] = cos(angle);
}
if (sweepAngle == 360.0f) {
sinCache[slices] = sinCache[0];
cosCache[slices] = cosCache[0];
}
switch (super.normals) {
case GLU_FLAT, GLU_SMOOTH -> GLStateManager.glNormal3f(0.0f, 0.0f, (super.orientation == GLU_OUTSIDE) ? 1.0f : -1.0f);
default -> {
}
}
switch (super.drawStyle) {
case GLU_FILL -> {
if (innerRadius == .0f) {
finish = loops - 1;
/* Triangle strip for inner polygons */
GLStateManager.glBegin(GL_TRIANGLE_FAN);
if (super.textureFlag) {
GLStateManager.glTexCoord2f(0.5f, 0.5f);
}
GLStateManager.glVertex3f(0.0f, 0.0f, 0.0f);
radiusLow = outerRadius - deltaRadius * ((float) (loops - 1) / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
}
if (super.orientation == GLU_OUTSIDE) {
for (int i = slices; i >= 0; i--) {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sinCache[i], radiusLow * cosCache[i], 0.0f);
}
} else {
for (int i = 0; i <= slices; i++) {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sinCache[i], radiusLow * cosCache[i], 0.0f);
}
}
GLStateManager.glEnd();
} else {
finish = loops;
}
for (int j = 0; j < finish; j++) {
radiusLow = outerRadius - deltaRadius * ((float) j / loops);
radiusHigh = outerRadius - deltaRadius * ((float) (j + 1) / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
texHigh = radiusHigh / outerRadius / 2;
}
GLStateManager.glBegin(GL_TRIANGLE_STRIP);
for (int i = 0; i <= slices; i++) {
if (super.orientation == GLU_OUTSIDE) {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sinCache[i], radiusLow * cosCache[i], 0.0f);
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texHigh * sinCache[i] + 0.5f, texHigh * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusHigh * sinCache[i], radiusHigh * cosCache[i], 0.0f);
} else {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texHigh * sinCache[i] + 0.5f, texHigh * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusHigh * sinCache[i], radiusHigh * cosCache[i], 0.0f);
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sinCache[i], radiusLow * cosCache[i], 0.0f);
}
}
GLStateManager.glEnd();
}
}
case GLU_POINT -> {
GLStateManager.glBegin(GL_POINTS);
for (int i = 0; i < slices2; i++) {
sintemp = sinCache[i];
costemp = cosCache[i];
for (int j = 0; j <= loops; j++) {
radiusLow = outerRadius - deltaRadius * ((float) j / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sintemp, radiusLow * costemp, 0.0f);
}
}
GLStateManager.glEnd();
}
case GLU_LINE -> {
if (innerRadius == outerRadius) {
GLStateManager.glBegin(GL_LINE_STRIP);
for (int i = 0; i <= slices; i++) {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(sinCache[i] / 2 + 0.5f, cosCache[i] / 2 + 0.5f);
}
GLStateManager.glVertex3f(innerRadius * sinCache[i], innerRadius * cosCache[i], 0.0f);
}
GLStateManager.glEnd();
break;
}
for (int j = 0; j <= loops; j++) {
radiusLow = outerRadius - deltaRadius * ((float) j / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
}
GLStateManager.glBegin(GL_LINE_STRIP);
for (int i = 0; i <= slices; i++) {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sinCache[i], radiusLow * cosCache[i], 0.0f);
}
GLStateManager.glEnd();
}
for (int i = 0; i < slices2; i++) {
sintemp = sinCache[i];
costemp = cosCache[i];
GLStateManager.glBegin(GL_LINE_STRIP);
for (int j = 0; j <= loops; j++) {
radiusLow = outerRadius - deltaRadius * ((float) j / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
}
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sintemp, radiusLow * costemp, 0.0f);
}
GLStateManager.glEnd();
}
}
case GLU_SILHOUETTE -> {
if (sweepAngle < 360.0f) {
for (int i = 0; i <= slices; i += slices) {
sintemp = sinCache[i];
costemp = cosCache[i];
GLStateManager.glBegin(GL_LINE_STRIP);
for (int j = 0; j <= loops; j++) {
radiusLow = outerRadius - deltaRadius * ((float) j / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sintemp, radiusLow * costemp, 0.0f);
}
GLStateManager.glEnd();
}
}
for (int j = 0; j <= loops; j += loops) {
radiusLow = outerRadius - deltaRadius * ((float) j / loops);
if (super.textureFlag) {
texLow = radiusLow / outerRadius / 2;
}
GLStateManager.glBegin(GL_LINE_STRIP);
for (int i = 0; i <= slices; i++) {
if (super.textureFlag) {
GLStateManager.glTexCoord2f(texLow * sinCache[i] + 0.5f, texLow * cosCache[i] + 0.5f);
}
GLStateManager.glVertex3f(radiusLow * sinCache[i], radiusLow * cosCache[i], 0.0f);
}
GLStateManager.glEnd();
if (innerRadius == outerRadius) break;
}
}
default -> {
}
}
}
}
@@ -0,0 +1,183 @@
package com.gtnewhorizons.angelica.glsm.compat.lwjgl;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.util.glu.Sphere;
import static org.lwjgl.opengl.GL11.GL_LINE_LOOP;
import static org.lwjgl.opengl.GL11.GL_LINE_STRIP;
import static org.lwjgl.opengl.GL11.GL_POINTS;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_FAN;
import static org.lwjgl.opengl.GL11.GL_TRIANGLE_STRIP;
import static org.lwjgl.util.glu.GLU.GLU_FILL;
import static org.lwjgl.util.glu.GLU.GLU_INSIDE;
import static org.lwjgl.util.glu.GLU.GLU_LINE;
import static org.lwjgl.util.glu.GLU.GLU_NONE;
import static org.lwjgl.util.glu.GLU.GLU_POINT;
import static org.lwjgl.util.glu.GLU.GLU_SILHOUETTE;
/**
* Core-profile compatible replacement for {@link Sphere}.
*/
@SuppressWarnings("unused")
public class AngelicaSphere extends Sphere {
private static final float PI = (float) Math.PI;
private void emitNormal(float x, float y, float z) {
final float mag = (float) Math.sqrt(x * x + y * y + z * z);
if (mag > 0.00001F) {
x /= mag;
y /= mag;
z /= mag;
}
GLStateManager.glNormal3f(x, y, z);
}
private void emitTexCoord(float x, float y) {
if (super.textureFlag) GLStateManager.glTexCoord2f(x, y);
}
@Override
public void draw(float radius, int slices, int stacks) {
final float drho, dtheta, ds, dt, nsign;
float rho, theta, x, y, z, s, t;
final int imin, imax;
final boolean normals;
normals = super.normals != GLU_NONE;
nsign = (super.orientation == GLU_INSIDE) ? -1.0f : 1.0f;
drho = PI / stacks;
dtheta = 2.0f * PI / slices;
if (super.drawStyle == GLU_FILL) {
if (!super.textureFlag) {
// draw +Z end as a triangle fan
GLStateManager.glBegin(GL_TRIANGLE_FAN);
GLStateManager.glNormal3f(0.0f, 0.0f, 1.0f);
GLStateManager.glVertex3f(0.0f, 0.0f, nsign * radius);
for (int j = 0; j <= slices; j++) {
theta = (j == slices) ? 0.0f : j * dtheta;
x = -sin(theta) * sin(drho);
y = cos(theta) * sin(drho);
z = nsign * cos(drho);
if (normals) {
emitNormal(x * nsign, y * nsign, z * nsign);
}
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
}
GLStateManager.glEnd();
}
ds = 1.0f / slices;
dt = 1.0f / stacks;
t = 1.0f;
if (super.textureFlag) {
imin = 0;
imax = stacks;
} else {
imin = 1;
imax = stacks - 1;
}
// draw intermediate stacks as triangle strips (GL_QUAD_STRIP removed in core profile)
for (int i = imin; i < imax; i++) {
rho = i * drho;
GLStateManager.glBegin(GL_TRIANGLE_STRIP);
s = 0.0f;
for (int j = 0; j <= slices; j++) {
theta = (j == slices) ? 0.0f : j * dtheta;
x = -sin(theta) * sin(rho);
y = cos(theta) * sin(rho);
z = nsign * cos(rho);
if (normals) {
emitNormal(x * nsign, y * nsign, z * nsign);
}
emitTexCoord(s, t);
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
x = -sin(theta) * sin(rho + drho);
y = cos(theta) * sin(rho + drho);
z = nsign * cos(rho + drho);
if (normals) {
emitNormal(x * nsign, y * nsign, z * nsign);
}
emitTexCoord(s, t - dt);
s += ds;
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
}
GLStateManager.glEnd();
t -= dt;
}
if (!super.textureFlag) {
// draw -Z end as a triangle fan
GLStateManager.glBegin(GL_TRIANGLE_FAN);
GLStateManager.glNormal3f(0.0f, 0.0f, -1.0f);
GLStateManager.glVertex3f(0.0f, 0.0f, -radius * nsign);
rho = PI - drho;
s = 1.0f;
for (int j = slices; j >= 0; j--) {
theta = (j == slices) ? 0.0f : j * dtheta;
x = -sin(theta) * sin(rho);
y = cos(theta) * sin(rho);
z = nsign * cos(rho);
if (normals) emitNormal(x * nsign, y * nsign, z * nsign);
s -= ds;
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
}
GLStateManager.glEnd();
}
} else if (super.drawStyle == GLU_LINE || super.drawStyle == GLU_SILHOUETTE) {
// draw stack lines
for (int i = 1; i < stacks; i++) {
rho = i * drho;
GLStateManager.glBegin(GL_LINE_LOOP);
for (int j = 0; j < slices; j++) {
theta = j * dtheta;
x = cos(theta) * sin(rho);
y = sin(theta) * sin(rho);
z = cos(rho);
if (normals) emitNormal(x * nsign, y * nsign, z * nsign);
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
}
GLStateManager.glEnd();
}
// draw slice lines
for (int j = 0; j < slices; j++) {
theta = j * dtheta;
GLStateManager.glBegin(GL_LINE_STRIP);
for (int i = 0; i <= stacks; i++) {
rho = i * drho;
x = cos(theta) * sin(rho);
y = sin(theta) * sin(rho);
z = cos(rho);
if (normals) emitNormal(x * nsign, y * nsign, z * nsign);
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
}
GLStateManager.glEnd();
}
} else if (super.drawStyle == GLU_POINT) {
// top and bottom-most points
GLStateManager.glBegin(GL_POINTS);
if (normals) GLStateManager.glNormal3f(0.0f, 0.0f, nsign);
GLStateManager.glVertex3f(0.0f, 0.0f, radius);
if (normals) GLStateManager.glNormal3f(0.0f, 0.0f, -nsign);
GLStateManager.glVertex3f(0.0f, 0.0f, -radius);
// loop over stacks
for (int i = 1; i < stacks - 1; i++) {
rho = i * drho;
for (int j = 0; j < slices; j++) {
theta = j * dtheta;
x = cos(theta) * sin(rho);
y = sin(theta) * sin(rho);
z = cos(rho);
if (normals) emitNormal(x * nsign, y * nsign, z * nsign);
GLStateManager.glVertex3f(x * radius, y * radius, z * radius);
}
}
GLStateManager.glEnd();
}
}
}
@@ -0,0 +1,104 @@
package com.gtnewhorizons.angelica.glsm.ffp;
/**
* Generates GLSL 330 core geometry shaders for wide line emulation.
*/
public final class GeometryShaderGenerator {
private GeometryShaderGenerator() {}
public static String generate(VertexKey key) {
final StringBuilder sb = new StringBuilder(1024);
sb.append("#version 330 core\n\n");
sb.append("layout(lines) in;\n");
sb.append("layout(triangle_strip, max_vertices = 4) out;\n\n");
sb.append("uniform vec2 u_ViewportSize;\n");
sb.append("uniform float u_LineWidth;\n\n");
sb.append("// Pass-through varyings\n");
sb.append("in vec4 v_Color_gs[];\n");
if (key.separateSpecular()) {
sb.append("in vec3 v_SpecularColor_gs[];\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append("in vec4 v_TexCoord0_gs[];\n");
}
if (key.lightmapEnabled()) {
sb.append("in vec4 v_TexCoord1_gs[];\n");
}
if (key.fogEnabled()) {
sb.append("in float v_FogCoord_gs[];\n");
}
sb.append('\n');
sb.append("out vec4 v_Color;\n");
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append("out vec4 v_TexCoord0;\n");
}
if (key.lightmapEnabled()) {
sb.append("out vec4 v_TexCoord1;\n");
}
if (key.fogEnabled()) {
sb.append("out float v_FogCoord;\n");
}
sb.append('\n');
sb.append("void main() {\n");
sb.append(" vec4 c0 = gl_in[0].gl_Position;\n");
sb.append(" vec4 c1 = gl_in[1].gl_Position;\n\n");
sb.append(" // NDC positions\n");
sb.append(" vec2 n0 = c0.xy / c0.w;\n");
sb.append(" vec2 n1 = c1.xy / c1.w;\n\n");
sb.append(" // Screen-space direction and perpendicular\n");
sb.append(" vec2 dir = normalize((n1 - n0) * u_ViewportSize);\n");
sb.append(" vec2 offset = vec2(-dir.y, dir.x) * u_LineWidth / u_ViewportSize;\n\n");
emitVertex(sb, key, 0, "+offset");
emitVertex(sb, key, 0, "-offset");
emitVertex(sb, key, 1, "+offset");
emitVertex(sb, key, 1, "-offset");
sb.append(" EndPrimitive();\n");
sb.append("}\n");
return sb.toString();
}
private static void emitVertex(StringBuilder sb, VertexKey key, int endpointIdx, String offsetExpr) {
final String ci = "c" + endpointIdx;
final String ni = "n" + endpointIdx;
final String idx = "[" + endpointIdx + "]";
sb.append(" // Endpoint ").append(endpointIdx).append(' ').append(offsetExpr).append('\n');
sb.append(" v_Color = v_Color_gs").append(idx).append(";\n");
if (key.separateSpecular()) {
sb.append(" v_SpecularColor = v_SpecularColor_gs").append(idx).append(";\n");
}
if (key.textureEnabled() || key.hasVertexTexCoord() || key.texGenEnabled()) {
sb.append(" v_TexCoord0 = v_TexCoord0_gs").append(idx).append(";\n");
}
if (key.lightmapEnabled()) {
sb.append(" v_TexCoord1 = v_TexCoord1_gs").append(idx).append(";\n");
}
if (key.fogEnabled()) {
sb.append(" v_FogCoord = v_FogCoord_gs").append(idx).append(";\n");
}
if (key.clipPlanesEnabled()) {
for (int i = 0; i < 8; i++) {
sb.append(" gl_ClipDistance[").append(i).append("] = gl_in").append(idx).append(".gl_ClipDistance[").append(i).append("];\n");
}
}
sb.append(" gl_Position = vec4((").append(ni).append(' ').append(offsetExpr).append(") * ").append(ci).append(".w, ").append(ci).append(".z, ").append(ci).append(".w);\n");
sb.append(" EmitVertex();\n\n");
}
}
@@ -8,12 +8,15 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL43;
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.
* A compiled and linked FFP emulation program (vertex + fragment + optional geometry shaders). Owns the GL program handle and caches uniform locations.
*/
public class Program {
@@ -82,6 +85,10 @@ public class Program {
// Clip planes
public int locClipPlanes = -1;
// Wide line emulation (geometry shader)
public int locViewportSize = -1;
public int locLineWidth = -1;
// Fragment uniforms
public final int[] locSampler = { -1, -1, -1, -1 };
public int locAlphaRef = -1;
@@ -151,6 +158,10 @@ public class Program {
// Clip planes
locClipPlanes = loc("u_ClipPlane[0]");
// Wide line emulation
locViewportSize = loc("u_ViewportSize");
locLineWidth = loc("u_LineWidth");
// Fragment
for (int i = 0; i < 4; i++) {
locSampler[i] = loc("u_Sampler" + i);
@@ -172,50 +183,61 @@ public class Program {
}
/**
* Compile a shader, link it into a program, and return the FFPProgram.
* Compile vertex + fragment + optional geometry shaders, link into a program, and return the FFPProgram.
*/
static Program create(VertexKey vk, FragmentKey fk, String vertSrc, String fragSrc) {
static Program create(VertexKey vk, FragmentKey fk, String vertSrc, String fragSrc, String geomSrc) {
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 String vkHex = Long.toHexString(vk.pack());
final boolean hasGeom = geomSrc != null;
final int program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
GL20.glAttachShader(program, fs);
GL20.glLinkProgram(program);
int shaderCount = 0;
final int[] shaders = new int[5]; // vertex, fragment, geometry, tess control, tess evaluation
int program = 0;
try {
shaders[shaderCount++] = compileShader(GL20.GL_VERTEX_SHADER, vertSrc, "ffp_v_" + vkHex);
shaders[shaderCount++] = compileShader(GL20.GL_FRAGMENT_SHADER, fragSrc, "ffp_f_" + id);
if (geomSrc != null) {
shaders[shaderCount++] = compileShader(GL32.GL_GEOMETRY_SHADER, geomSrc, "ffp_g_" + id);
}
final String log = RenderSystem.getProgramInfoLog(program);
if (!log.isEmpty()) {
LOGGER.warn("FFP program link log (vk=0x{}, fk={}): {}", Long.toHexString(vk.pack()), fk, log);
program = GL20.glCreateProgram();
for (int i = 0; i < shaderCount; i++) GL20.glAttachShader(program, shaders[i]);
GL20.glLinkProgram(program);
final String log = RenderSystem.getProgramInfoLog(program);
if (!log.isEmpty()) {
LOGGER.warn("FFP program link log (vk=0x{}, fk={}): {}", vkHex, fk, log);
}
if (GL20.glGetProgrami(program, GL20.GL_LINK_STATUS) != GL11.GL_TRUE) {
throw new RuntimeException("FFP shader link failed (vk=0x" + vkHex + ", fk=" + fk + "): " + log);
}
final String debugName = "FFP(v=0x" + vkHex + ",f=" + id + (hasGeom ? ",g" : "") + ")";
GLDebug.nameObject(KHRDebug.GL_PROGRAM, program, debugName);
// Detach and delete individual shaders — they're linked into the program
for (int i = 0; i < shaderCount; i++) {
GL20.glDetachShader(program, shaders[i]);
GL20.glDeleteShader(shaders[i]);
}
shaderCount = 0;
final Program ffpProgram = new Program(program, vk, fk);
final int previousProgram = GLStateManager.getActiveProgram();
GL20.glUseProgram(program);
for (int i = 0; i < 4; i++) {
if (ffpProgram.locSampler[i] != -1) GL20.glUniform1i(ffpProgram.locSampler[i], i);
}
GL20.glUseProgram(previousProgram);
return ffpProgram;
} catch (RuntimeException e) {
if (program != 0) GL20.glDeleteProgram(program);
for (int i = 0; i < shaderCount; i++) GL20.glDeleteShader(shaders[i]);
throw e;
}
final int linkStatus = GL20.glGetProgrami(program, GL20.GL_LINK_STATUS);
if (linkStatus != GL11.GL_TRUE) {
GL20.glDeleteProgram(program);
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
throw new RuntimeException("FFP shader link failed (vk=0x" + Long.toHexString(vk.pack()) + ", fk=" + fk + "): " + log);
}
final String debugName = "FFP(v=0x" + Long.toHexString(vk.pack()) + ",f=" + id + ")";
GLDebug.nameObject(KHRDebug.GL_PROGRAM, program, debugName);
// Detach and delete individual shaders — they're linked into the program
GL20.glDetachShader(program, vs);
GL20.glDetachShader(program, fs);
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
final Program ffpProgram = new Program(program, vk, fk);
final int previousProgram = GLStateManager.getActiveProgram();
GL20.glUseProgram(program);
for (int i = 0; i < 4; i++) {
if (ffpProgram.locSampler[i] != -1) GL20.glUniform1i(ffpProgram.locSampler[i], i);
}
GL20.glUseProgram(previousProgram);
return ffpProgram;
}
private static int compileShader(int type, String src, String name) {
@@ -223,7 +245,15 @@ public class Program {
GL20.glShaderSource(shader, src);
GL20.glCompileShader(shader);
final String typeName = (type == GL20.GL_VERTEX_SHADER) ? "vertex" : "fragment";
final String typeName = switch (type) {
case GL20.GL_VERTEX_SHADER -> "vertex";
case GL20.GL_FRAGMENT_SHADER -> "fragment";
case GL32.GL_GEOMETRY_SHADER -> "geometry";
case GL40.GL_TESS_CONTROL_SHADER -> "tess_control";
case GL40.GL_TESS_EVALUATION_SHADER -> "tess_evaluation";
case GL43.GL_COMPUTE_SHADER -> "compute";
default -> "unknown(0x" + Integer.toHexString(type) + ")";
};
GLDebug.nameObject(KHRDebug.GL_SHADER, shader, name + "(" + typeName + ")");
final String log = RenderSystem.getShaderInfoLog(shader);
@@ -32,6 +32,7 @@ public class ShaderCache {
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> geometrySourceCache = new Long2ObjectOpenHashMap<>();
// Open-addressed table — parallel arrays
private long[] keys;
@@ -108,7 +109,8 @@ public class ShaderCache {
uniqueFragCount++;
}
final Program program = Program.create(vk, fk, vertSrc, fragSrc);
final String geomSrc = vk.wideLineEmulation() ? getOrGenerateGeometry(vk) : null;
final Program program = Program.create(vk, fk, vertSrc, fragSrc, geomSrc);
final int insertIdx = findEmptySlot(hash);
final int base = insertIdx * SLOT_STRIDE;
@@ -126,6 +128,9 @@ public class ShaderCache {
if (dumpDir != null) {
dumpShader(Long.toHexString(vkPacked), ".vert.glsl", vertSrc);
dumpShader("frag_" + fragmentDumpCounter++, ".frag.glsl", fragSrc);
if (geomSrc != null) {
dumpShader(Long.toHexString(vkPacked), ".geom.glsl", geomSrc);
}
}
return program;
@@ -216,6 +221,10 @@ public class ShaderCache {
return vertexSourceCache.computeIfAbsent(vk.pack(), k -> VertexShaderGenerator.generate(vk));
}
private String getOrGenerateGeometry(VertexKey vk) {
return geometrySourceCache.computeIfAbsent(vk.pack(), k -> GeometryShaderGenerator.generate(vk));
}
private void dumpShader(String name, String suffix, String source) {
if (dumpDir == null) return;
try {
@@ -244,8 +253,11 @@ public class ShaderCache {
dumpShader("frag_" + idx, ".frag.glsl", fragSources[i]);
}
}
LOGGER.info("Dumped {} vertex and {} fragment FFP shader sources to {}",
vertexSourceCache.size(), seen.size(), outputDir);
for (var entry : geometrySourceCache.entrySet()) {
dumpShader(Long.toHexString(entry.getKey()), ".geom.glsl", entry.getValue());
}
LOGGER.info("Dumped {} vertex, {} geometry, and {} fragment FFP shader sources to {}",
vertexSourceCache.size(), geometrySourceCache.size(), seen.size(), outputDir);
} catch (IOException e) {
LOGGER.warn("Failed to dump FFP shaders: {}", e.getMessage());
}
@@ -264,6 +276,7 @@ public class ShaderCache {
count = 0;
uniqueFragCount = 0;
vertexSourceCache.clear();
geometrySourceCache.clear();
lastProgram = null;
lastVK = Long.MIN_VALUE;
LOGGER.debug("FFP shader cache cleared");
@@ -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
@@ -60,12 +57,6 @@ public class TessellatorStreamingDrawer {
if (initialized) return;
initialized = true;
// Select orphan upload strategy from config
if (AngelicaMod.options() != null && AngelicaMod.options().advanced.useMapBufferRange) {
orphanStrategy = UploadStrategy.MAP_BUFFER_RANGE;
}
LOGGER.info("Orphan upload strategy: {}", orphanStrategy);
if (RenderSystem.supportsBufferStorage() && !Boolean.getBoolean("angelica.forceOrphanStreaming")) {
try {
persistentBuffer = new PersistentStreamingBuffer();
@@ -179,11 +170,6 @@ public class TessellatorStreamingDrawer {
if (persistentBuffer != null) {
persistentBuffer.postDraw();
}
for (int i = 0; i < FORMAT_COUNT; i++) {
if (orphanBuffers[i] != null) {
orphanBuffers[i].postDraw();
}
}
}
/**
@@ -231,9 +217,11 @@ public class TessellatorStreamingDrawer {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
firstVertex = orphanBuffers[flags].upload(packed, vertexSize);
orphanBuffers[flags].upload(packed);
firstVertex = 0;
}
GLStateManager.prepareWideLineEmulation(drawMode);
ShaderManager.getInstance().preDraw(flags);
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
@@ -265,7 +253,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]);
@@ -60,6 +60,9 @@ public class Uniforms {
private float lastLightmapY = Float.NaN;
private int lastTexGenGen = -1;
private int lastClipPlaneGen = -1;
private float lastLineWidth = Float.NaN;
private int lastViewportWidth = -1;
private int lastViewportHeight = -1;
/**
* Upload all relevant uniforms to the given FFP program based on current GLSM state.
@@ -144,6 +147,11 @@ public class Uniforms {
uploadFragmentUniforms(program);
lastFragmentGen = fragGen;
}
// Wide line emulation uniforms
if (program.locLineWidth != -1 && program.locViewportSize != -1) {
uploadWideLineUniforms(program, programChanged);
}
}
private void uploadMatrices(Program program, boolean mvChanged, boolean projChanged, boolean texMatChanged) {
@@ -450,6 +458,21 @@ public class Uniforms {
}
}
private void uploadWideLineUniforms(Program program, boolean programChanged) {
final float lineWidth = GLStateManager.getLineState().getWidth();
if (programChanged || lineWidth != lastLineWidth) {
GL20.glUniform1f(program.locLineWidth, lineWidth);
lastLineWidth = lineWidth;
}
final int vw = GLStateManager.getViewportState().width;
final int vh = GLStateManager.getViewportState().height;
if (programChanged || vw != lastViewportWidth || vh != lastViewportHeight) {
GL20.glUniform2f(program.locViewportSize, vw, vh);
lastViewportWidth = vw;
lastViewportHeight = vh;
}
}
private void uploadVec4(int loc, Vector4f v) {
if (loc == -1) return;
vec4Buf.clear();
@@ -42,6 +42,7 @@ public final class VertexKey {
private static final int BIT_TEXGEN_Q = 31;
// bits 31-33: texGenModeQ (3 bits)
private static final int BIT_CLIP_PLANES = 34;
private static final int BIT_WIDE_LINE = 35;
public static final int TG_NONE = 0;
public static final int TG_OBJ_LINEAR = 1;
@@ -92,6 +93,8 @@ public final class VertexKey {
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
/** Whether any clip plane is enabled. */
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
/** Whether wide line GS emulation is active for this variant. */
public boolean wideLineEmulation() { return bit(BIT_WIDE_LINE); }
/** Whether vertex color replaces material ambient in this variant. */
public boolean cmReplacesAmbient() { final int m = colorMaterialMode(); return m == CM_AMBIENT || m == CM_AMBIENT_AND_DIFFUSE; }
@@ -223,6 +226,11 @@ public final class VertexKey {
bits |= (1L << BIT_CLIP_PLANES);
}
// Wide line GS emulation
if (GLStateManager.wideLineEmulationActive) {
bits |= (1L << BIT_WIDE_LINE);
}
return bits;
}
@@ -248,10 +256,10 @@ public final class VertexKey {
@Override
public String toString() {
return String.format("FFPVertexKey[0x%09X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%b lm=%b col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b]",
return String.format("FFPVertexKey[0x%09X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%b lm=%b col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b]",
packed, lightingEnabled(), light0Enabled(), light1Enabled(),
colorMaterialEnabled(), fogEnabled(), textureEnabled(), lightmapEnabled(),
hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(),
texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled());
texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), wideLineEmulation());
}
}
@@ -37,7 +37,15 @@ public final class VertexShaderGenerator {
}
sb.append("}\n");
return sb.toString();
String source = sb.toString();
if (key.wideLineEmulation()) {
source = source.replace("v_SpecularColor", "v_SpecularColor_gs")
.replace("v_TexCoord0", "v_TexCoord0_gs")
.replace("v_TexCoord1", "v_TexCoord1_gs")
.replace("v_FogCoord", "v_FogCoord_gs")
.replace("v_Color", "v_Color_gs");
}
return source;
}
private static void emitAttributes(StringBuilder sb, VertexKey key) {
@@ -2,22 +2,24 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClientArrayState;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.Arrays;
/**
* Records immediate mode GL calls (glBegin/glEnd/glVertex) via {@link DirectTessellator}.
* Used both during display list compilation and for live immediate mode emulation in core profile.
*/
public final class ImmediateModeRecorder {
private static final Logger LOGGER = LogManager.getLogger("ImmediateModeRecorder");
private static final DirectTessellator tessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
/** Separate tessellator for the splash thread. Nulled after splash completes. */
@@ -157,192 +159,292 @@ public final class ImmediateModeRecorder {
tessellator().reset();
}
/**
* Process a glDrawArrays call during display list recording.
* Reads vertex data from client arrays (tracked globally in GLStateManager) and converts to immediate mode geometry.
*
* @param mode Primitive type (GL_QUADS, GL_LINES, etc.)
* @param first Starting index in arrays
* @param count Number of vertices
* @return Result containing converted geometry, or null if no geometry produced
*/
public static DirectTessellator processDrawArrays(int mode, int first, int count) {
final ClientArrayState cas = GLStateManager.getClientArrayState();
final Buffer vertexPointer = cas.isVertexArrayEnabled() ? cas.getVertexPointer() : null;
final Buffer colorPointer = cas.isColorArrayEnabled() ? cas.getColorPointer() : null;
final Buffer normalPointer = cas.isNormalArrayEnabled() ? cas.getNormalPointer() : null;
final Buffer texCoordPointer = cas.isTexCoordArrayEnabled() ? cas.getTexCoordPointer() : null;
private static final int LOC_POSITION = Usage.POSITION.getAttributeLocation();
private static final int LOC_COLOR = Usage.COLOR.getAttributeLocation();
private static final int LOC_UV0 = Usage.PRIMARY_UV.getAttributeLocation();
private static final int LOC_UV1 = Usage.SECONDARY_UV.getAttributeLocation();
private static final int LOC_NORMAL = Usage.NORMAL.getAttributeLocation();
private static final int NUM_ATTRIBS = 1 + Arrays.stream(Usage.values())
.mapToInt(Usage::getAttributeLocation)
.max().orElse(0);
return convertClientArrays(
mode, first, count,
vertexPointer,
cas.getVertexPointerType(), cas.getVertexPointerSize(), cas.getVertexPointerStride(),
colorPointer,
cas.getColorPointerType(), cas.getColorPointerSize(), cas.getColorPointerStride(),
normalPointer,
cas.getNormalPointerType(), cas.getNormalPointerStride(),
texCoordPointer,
cas.getTexCoordPointerType(), cas.getTexCoordPointerSize(), cas.getTexCoordPointerStride()
);
private static final ByteBuffer[] scratchAttribBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchAttribBaseOffsets = new long[NUM_ATTRIBS];
private static final int[] scratchVBOIds = new int[NUM_ATTRIBS];
private static final ByteBuffer[] scratchVBOBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchVBOReadOffsets = new long[NUM_ATTRIBS];
private static final class AttribReader {
ByteBuffer data;
int stride;
int baseOffset;
int size;
int loc;
VertexAttribState.Attrib attrib;
}
private static final AttribReader[] readers = new AttribReader[NUM_ATTRIBS];
private static int nonPositionCount;
private static final int[] nonPositionIndices = new int[NUM_ATTRIBS - 1];
private static int positionReaderIndex = -1;
static {
for (int i = 0; i < NUM_ATTRIBS; i++) {
readers[i] = new AttribReader();
}
}
/**
* Convert client array data to immediate mode.
* Called by glDrawArrays during display list recording.
* Like Mesa's save_DrawArrays, reads vertex data from arrays and emits as immediate mode.
* Process a glDrawArrays call during display list recording.
* Reads vertex data from VBOs or client memory referenced by VertexAttribState and converts to immediate mode geometry.
*/
public static DirectTessellator convertClientArrays(
int mode, int first, int count,
java.nio.Buffer vertexPointer, int vertexType, int vertexSize, int vertexStride,
java.nio.Buffer colorPointer, int colorType, int colorSize, int colorStride,
java.nio.Buffer normalPointer, int normalType, int normalStride,
java.nio.Buffer texCoordPointer, int texCoordType, int texCoordSize, int texCoordStride
) {
public static DirectTessellator processDrawArraysFromAttribs(int mode, int first, int count) {
final int readVBOCount = readbackAttribVBOs(first, count);
try {
prepareReaders();
if (positionReaderIndex < 0) return null;
return readVerticesFromBuffer(mode, first, count);
} finally {
freeReadbackBuffers(readVBOCount);
}
}
/**
* Process a glDrawElements call during display list recording when a VBO is bound.
*/
public static DirectTessellator processDrawElementsFromVBO(int mode, int indicesCount, int indexType, long indicesOffset, int eboId) {
if (eboId == 0) return null;
final int readVBOCount = readbackAttribVBOs(-1, 0);
try {
prepareReaders();
if (positionReaderIndex < 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboSize = GL15.glGetBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboSize <= 0) {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final int indexSize = VertexAttribState.Attrib.glTypeSizeBytes(indexType);
final int eboReadOffset = (int) indicesOffset;
final int eboReadSize = indicesCount * indexSize;
if (eboReadOffset + eboReadSize > eboSize) {
LOGGER.warn("[EBO] Read range {}+{} exceeds buffer size {}", eboReadOffset, eboReadSize, eboSize);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final ByteBuffer eboData = MemoryUtilities.memAlloc(eboReadSize);
GL15.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboReadOffset, eboData);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
try {
return readVerticesFromBufferIndexed(mode, eboData, indexType, 0, indicesCount);
} finally {
MemoryUtilities.memFree(eboData);
}
} finally {
freeReadbackBuffers(readVBOCount);
}
}
/**
* Read data for all enabled attribs into scratchAttribBuffers.
* For VBO-backed attribs, reads VBO data. For client-memory attribs, uses the client pointer directly.
*/
private static int readbackAttribVBOs(int firstVertex, int vertexCount) {
int readVBOCount = 0;
final int prevVBO = GLStateManager.getBoundVBO();
final boolean partialRead = firstVertex >= 0;
for (int i = 0; i < NUM_ATTRIBS; i++) {
scratchAttribBuffers[i] = null;
scratchAttribBaseOffsets[i] = 0;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled) continue;
if (a.vboId != 0) {
// VBO-backed attrib — read from GPU
ByteBuffer existing = null;
int existingIdx = -1;
for (int j = 0; j < readVBOCount; j++) {
if (scratchVBOIds[j] == a.vboId) {
existing = scratchVBOBuffers[j];
existingIdx = j;
break;
}
}
if (existing != null) {
scratchAttribBuffers[i] = existing;
scratchAttribBaseOffsets[i] = scratchVBOReadOffsets[existingIdx];
} else {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = GL15.glGetBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long readOffset;
final int readSize;
if (partialRead) {
final int stride = a.effectiveStride();
long rangeStart = a.offset + (long) firstVertex * stride;
long rangeEnd = a.offset + (long) (firstVertex + vertexCount - 1) * stride + (long) a.size * a.typeSizeBytes();
for (int k = 0; k < NUM_ATTRIBS; k++) {
if (k == i) continue;
final VertexAttribState.Attrib ak = VertexAttribState.get(k);
if (!ak.enabled || ak.vboId != a.vboId) continue;
final int ks = ak.effectiveStride();
rangeStart = Math.min(rangeStart, ak.offset + (long) firstVertex * ks);
rangeEnd = Math.max(rangeEnd, ak.offset + (long) (firstVertex + vertexCount - 1) * ks
+ (long) ak.size * ak.typeSizeBytes());
}
rangeStart = Math.max(0, rangeStart);
rangeEnd = Math.min(bufferSize, rangeEnd);
readOffset = rangeStart;
readSize = (int) (rangeEnd - rangeStart);
} else {
readOffset = 0;
readSize = bufferSize;
}
if (readSize <= 0) continue;
final ByteBuffer buf = MemoryUtilities.memAlloc(readSize);
GL15.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, readOffset, buf);
scratchAttribBuffers[i] = buf;
scratchAttribBaseOffsets[i] = readOffset;
scratchVBOIds[readVBOCount] = a.vboId;
scratchVBOReadOffsets[readVBOCount] = readOffset;
scratchVBOBuffers[readVBOCount] = buf;
readVBOCount++;
}
} else if (a.clientPointer != null) {
// Client-memory attrib — use pointer directly (no allocation needed)
scratchAttribBuffers[i] = a.clientPointer;
}
}
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
return readVBOCount;
}
private static void freeReadbackBuffers(int readVBOCount) {
for (int i = 0; i < readVBOCount; i++) {
MemoryUtilities.memFree(scratchVBOBuffers[i]);
scratchVBOBuffers[i] = null;
}
}
private static void prepareReaders() {
nonPositionCount = 0;
positionReaderIndex = -1;
for (int i = 0; i < NUM_ATTRIBS; i++) {
final ByteBuffer buf = scratchAttribBuffers[i];
if (buf == null) continue;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
final AttribReader r = readers[i];
r.data = buf;
r.stride = a.effectiveStride();
r.baseOffset = (int) (a.offset - scratchAttribBaseOffsets[i]);
r.size = a.size;
r.loc = i;
r.attrib = a;
if (i == LOC_POSITION) {
positionReaderIndex = i;
} else {
nonPositionIndices[nonPositionCount++] = i;
}
}
}
private static DirectTessellator readVerticesFromBuffer(int mode, int first, int count) {
begin(mode);
final DirectTessellator t = tessellator();
// Hoist type checks and stride calculations outside the loop for performance.
// Mesa's algorithm: offset_bytes = index * effective_stride
// effective_stride = (stride == 0) ? (numComponents * sizeof(element)) : stride
// Determine color reader
final ArrayReader colorReader;
if (colorPointer == null) {
colorReader = null;
} else if (colorType == GL11.GL_FLOAT && colorPointer instanceof FloatBuffer fb) {
final int colorEffectiveStride = (colorStride == 0) ? colorSize * 4 : colorStride;
final int strideInFloats = colorEffectiveStride / 4;
colorReader = (idx) -> {
final int offset = idx * strideInFloats;
final float r = fb.get(offset);
final float g = fb.get(offset + 1);
final float b = fb.get(offset + 2);
final float a = (colorSize == 4) ? fb.get(offset + 3) : 1.0f;
GLStateManager.glColor4f(r, g, b, a);
};
} else if ((colorType == GL11.GL_UNSIGNED_BYTE || colorType == GL11.GL_BYTE) && colorPointer instanceof ByteBuffer bb) {
final int colorEffectiveStride = (colorStride == 0) ? colorSize : colorStride;
colorReader = (idx) -> {
final int offset = idx * colorEffectiveStride;
final float r = (bb.get(offset) & 0xFF) / 255.0f;
final float g = (bb.get(offset + 1) & 0xFF) / 255.0f;
final float b = (bb.get(offset + 2) & 0xFF) / 255.0f;
final float a = (colorSize == 4) ? (bb.get(offset + 3) & 0xFF) / 255.0f : 1.0f;
GLStateManager.glColor4f(r, g, b, a);
};
} else {
// Unsupported color type - use white
colorReader = (idx) -> GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}
// Determine normal reader
final ArrayReader normalReader;
if (normalPointer == null) {
normalReader = null;
} else if (normalType == GL11.GL_FLOAT && normalPointer instanceof FloatBuffer fb) {
final int effectiveStride = (normalStride == 0) ? 3 * 4 : normalStride;
final int strideInFloats = effectiveStride / 4;
normalReader = (idx) -> {
final int offset = idx * strideInFloats;
setNormal(fb.get(offset), fb.get(offset + 1), fb.get(offset + 2));
};
} else if (normalType == GL11.GL_BYTE && normalPointer instanceof ByteBuffer bb) {
final int effectiveStride = (normalStride == 0) ? 3 : normalStride;
normalReader = (idx) -> {
final int offset = idx * effectiveStride;
setNormal(bb.get(offset) / 127.0f, bb.get(offset + 1) / 127.0f, bb.get(offset + 2) / 127.0f);
};
} else {
normalReader = null;
}
// Determine texcoord reader
final ArrayReader texCoordReader;
if (texCoordPointer == null) {
texCoordReader = null;
} else if (texCoordType == GL11.GL_FLOAT && texCoordPointer instanceof FloatBuffer fb) {
final int effectiveStride = (texCoordStride == 0) ? texCoordSize * 4 : texCoordStride;
final int strideInFloats = effectiveStride / 4;
texCoordReader = (idx) -> {
final int offset = idx * strideInFloats;
final float s = fb.get(offset);
final float tt = (texCoordSize >= 2) ? fb.get(offset + 1) : 0.0f;
setTexCoord(s, tt);
};
} else {
texCoordReader = null;
}
// Determine vertex reader
final ArrayReader vertexReader;
if (vertexPointer == null) {
vertexReader = null;
} else if (vertexType == GL11.GL_FLOAT && vertexPointer instanceof FloatBuffer fb) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 4 : vertexStride;
final int strideInFloats = effectiveStride / 4;
vertexReader = (idx) -> {
final int offset = idx * strideInFloats;
final float x = fb.get(offset);
final float y = fb.get(offset + 1);
final float z = (vertexSize >= 3) ? fb.get(offset + 2) : 0.0f;
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_DOUBLE && vertexPointer instanceof DoubleBuffer db) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 8 : vertexStride;
final int strideInDoubles = effectiveStride / 8;
vertexReader = (idx) -> {
final int offset = idx * strideInDoubles;
final float x = (float) db.get(offset);
final float y = (float) db.get(offset + 1);
final float z = (vertexSize >= 3) ? (float) db.get(offset + 2) : 0.0f;
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_INT && vertexPointer instanceof IntBuffer ib) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 4 : vertexStride;
final int strideInInts = effectiveStride / 4;
vertexReader = (idx) -> {
final int offset = idx * strideInInts;
final float x = ib.get(offset);
final float y = ib.get(offset + 1);
final float z = (vertexSize >= 3) ? ib.get(offset + 2) : 0.0f;
t.addVertex(x, y, z);
};
} else if (vertexType == GL11.GL_SHORT && vertexPointer instanceof ShortBuffer sb) {
final int effectiveStride = (vertexStride == 0) ? vertexSize * 2 : vertexStride;
final int strideInShorts = effectiveStride / 2;
vertexReader = (idx) -> {
final int offset = idx * strideInShorts;
final float x = sb.get(offset);
final float y = sb.get(offset + 1);
final float z = (vertexSize >= 3) ? sb.get(offset + 2) : 0.0f;
t.addVertex(x, y, z);
};
} else {
// Unsupported vertex type
vertexReader = null;
}
// Main loop - no type checks, just function calls
for (int i = first; i < first + count; i++) {
if (normalReader != null) {
normalReader.read(i);
}
if (texCoordReader != null) {
texCoordReader.read(i);
}
if (colorReader != null) {
colorReader.read(i);
}
if (vertexReader != null) {
vertexReader.read(i);
}
readVertexFromBuffer(t, i);
}
return end();
}
@FunctionalInterface
private interface ArrayReader {
void read(int index);
private static DirectTessellator readVerticesFromBufferIndexed(int mode, ByteBuffer eboData, int indexType, int eboOffset, int indicesCount) {
begin(mode);
final DirectTessellator t = tessellator();
for (int i = 0; i < indicesCount; i++) {
final int index = readIndex(eboData, indexType, eboOffset, i);
readVertexFromBuffer(t, index);
}
return end();
}
public static int readIndex(ByteBuffer ebo, int indexType, int eboOffset, int i) {
return switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> ebo.get(eboOffset + i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> ebo.getShort(eboOffset + i * 2) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> ebo.getInt(eboOffset + i * 4);
default -> i;
};
}
private static void readVertexFromBuffer(DirectTessellator t, int vertexIndex) {
for (int i = 0; i < nonPositionCount; i++) {
final AttribReader r = readers[nonPositionIndices[i]];
final int base = r.baseOffset + vertexIndex * r.stride;
emitAttrib(t, r, base);
}
if (positionReaderIndex >= 0) {
final AttribReader r = readers[positionReaderIndex];
final int base = r.baseOffset + vertexIndex * r.stride;
t.addVertex(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
(r.size >= 3) ? r.attrib.readComponent(r.data, base, 2) : 0.0f);
}
}
private static void emitAttrib(DirectTessellator t, AttribReader r, int base) {
if (r.loc == LOC_NORMAL) {
t.setNormal(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
r.attrib.readComponent(r.data, base, 2));
} else if (r.loc == LOC_UV0) {
t.setTextureUV(
r.attrib.readComponent(r.data, base, 0),
(r.size >= 2) ? r.attrib.readComponent(r.data, base, 1) : 0.0f);
} else if (r.loc == LOC_UV1) {
if (r.attrib.type == GL11.GL_SHORT) {
final int s = r.data.getShort(base) & 0xFFFF;
final int u = (r.size >= 2) ? (r.data.getShort(base + 2) & 0xFFFF) : 0;
t.setBrightness((u << 16) | s);
} else if (r.attrib.type == GL11.GL_FLOAT) {
final int s = (int) r.data.getFloat(base);
final int u = (r.size >= 2) ? (int) r.data.getFloat(base + 4) : 0;
t.setBrightness((u << 16) | (s & 0xFFFF));
}
} else if (r.loc == LOC_COLOR) {
final float cr = r.attrib.readComponent(r.data, base, 0);
final float cg = r.attrib.readComponent(r.data, base, 1);
final float cb = r.attrib.readComponent(r.data, base, 2);
final float ca = (r.size >= 4) ? r.attrib.readComponent(r.data, base, 3) : 1.0f;
t.setColorRGBA((int) (cr * 255.0f), (int) (cg * 255.0f), (int) (cb * 255.0f), (int) (ca * 255.0f));
}
}
}
@@ -1,166 +0,0 @@
package com.gtnewhorizons.angelica.glsm.states;
import org.lwjgl.opengl.GL11;
import java.nio.Buffer;
/**
* Tracks OpenGL client array state (glVertexPointer, glColorPointer, etc.)
* This state is NOT part of the server attribute stack (push/pop attrib),
* but is needed for display list compilation to capture glDrawArrays calls.
*
* When a display list is compiled, glDrawArrays reads from the current
* vertex/color arrays at compile time (not playback time). We need to track
* this state globally so ImmediateModeRecorder can access it.
*/
public class ClientArrayState {
// Vertex array state
private boolean vertexArrayEnabled = false;
private Buffer vertexPointer = null;
private int vertexPointerSize = 4;
private int vertexPointerType = GL11.GL_FLOAT;
private int vertexPointerStride = 0;
// Color array state
private boolean colorArrayEnabled = false;
private Buffer colorPointer = null;
private int colorPointerSize = 4;
private int colorPointerType = GL11.GL_FLOAT;
private int colorPointerStride = 0;
// Vertex array methods
public void setVertexPointer(int size, int type, int stride, Buffer pointer) {
this.vertexPointerSize = size;
this.vertexPointerType = type;
this.vertexPointerStride = stride;
this.vertexPointer = pointer;
}
public boolean isVertexArrayEnabled() {
return vertexArrayEnabled;
}
public void setVertexArrayEnabled(boolean enabled) {
this.vertexArrayEnabled = enabled;
}
public Buffer getVertexPointer() {
return vertexPointer;
}
public int getVertexPointerSize() {
return vertexPointerSize;
}
public int getVertexPointerType() {
return vertexPointerType;
}
public int getVertexPointerStride() {
return vertexPointerStride;
}
// Normal array state
private boolean normalArrayEnabled = false;
private Buffer normalPointer = null;
private int normalPointerType = GL11.GL_FLOAT;
private int normalPointerStride = 0;
// TexCoord array state
private boolean texCoordArrayEnabled = false;
private Buffer texCoordPointer = null;
private int texCoordPointerSize = 4;
private int texCoordPointerType = GL11.GL_FLOAT;
private int texCoordPointerStride = 0;
// Color array methods
public void setColorPointer(int size, int type, int stride, Buffer pointer) {
this.colorPointerSize = size;
this.colorPointerType = type;
this.colorPointerStride = stride;
this.colorPointer = pointer;
}
public boolean isColorArrayEnabled() {
return colorArrayEnabled;
}
public void setColorArrayEnabled(boolean enabled) {
this.colorArrayEnabled = enabled;
}
public Buffer getColorPointer() {
return colorPointer;
}
public int getColorPointerSize() {
return colorPointerSize;
}
public int getColorPointerType() {
return colorPointerType;
}
public int getColorPointerStride() {
return colorPointerStride;
}
// Normal array methods
public void setNormalPointer(int type, int stride, Buffer pointer) {
this.normalPointerType = type;
this.normalPointerStride = stride;
this.normalPointer = pointer;
}
public boolean isNormalArrayEnabled() {
return normalArrayEnabled;
}
public void setNormalArrayEnabled(boolean enabled) {
this.normalArrayEnabled = enabled;
}
public Buffer getNormalPointer() {
return normalPointer;
}
public int getNormalPointerType() {
return normalPointerType;
}
public int getNormalPointerStride() {
return normalPointerStride;
}
// TexCoord array methods
public void setTexCoordPointer(int size, int type, int stride, Buffer pointer) {
this.texCoordPointerSize = size;
this.texCoordPointerType = type;
this.texCoordPointerStride = stride;
this.texCoordPointer = pointer;
}
public boolean isTexCoordArrayEnabled() {
return texCoordArrayEnabled;
}
public void setTexCoordArrayEnabled(boolean enabled) {
this.texCoordArrayEnabled = enabled;
}
public Buffer getTexCoordPointer() {
return texCoordPointer;
}
public int getTexCoordPointerSize() {
return texCoordPointerSize;
}
public int getTexCoordPointerType() {
return texCoordPointerType;
}
public int getTexCoordPointerStride() {
return texCoordPointerStride;
}
}
@@ -0,0 +1,166 @@
package com.gtnewhorizons.angelica.glsm.states;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
/**
* Per-VAO vertex attribute pointer state tracking (Mesa-style).
*/
public class VertexAttribState {
public static final int MAX_ATTRIBS = 16;
private static final Int2ObjectOpenHashMap<Attrib[]> vaoAttribs = new Int2ObjectOpenHashMap<>();
private static Attrib[] current;
private static final ArrayDeque<Attrib[]> pool = new ArrayDeque<>();
public static void init(int defaultVAO) {
vaoAttribs.clear();
pool.clear();
current = allocAttribArray();
vaoAttribs.put(defaultVAO, current);
}
public static void onBindVertexArray(int newVAO) {
current = vaoAttribs.computeIfAbsent(newVAO, k -> allocAttribArray());
}
public static void onDeleteVertexArray(int vaoId) {
final Attrib[] arr = vaoAttribs.remove(vaoId);
if (arr != null) {
pool.addLast(arr);
}
}
public static void reset() {
vaoAttribs.clear();
pool.clear();
current = null;
}
private static Attrib[] allocAttribArray() {
Attrib[] arr = pool.pollFirst();
if (arr != null) {
for (Attrib a : arr) a.reset();
return arr;
}
arr = new Attrib[MAX_ATTRIBS];
for (int i = 0; i < MAX_ATTRIBS; i++) arr[i] = new Attrib();
return arr;
}
public static void set(int index, int size, int type, boolean normalized, int stride, long offset, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
a.size = size;
a.type = type;
a.normalized = normalized;
a.stride = stride;
a.offset = offset;
a.vboId = vboId;
a.clientPointer = null;
}
public static void set(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer, int vboId) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = current[index];
a.size = size;
a.type = type;
a.normalized = normalized;
a.stride = stride;
a.offset = 0;
a.vboId = vboId;
a.clientPointer = (vboId == 0) ? pointer : null;
}
public static void setEnabled(int index, boolean enabled) {
if (index < 0 || index >= MAX_ATTRIBS) return;
current[index].enabled = enabled;
}
public static Attrib get(int index) {
return current[index];
}
public static boolean hasVBOBoundAttrib() {
for (int i = 0; i < MAX_ATTRIBS; i++) {
if (current[i].enabled && current[i].vboId != 0) return true;
}
return false;
}
public static class Attrib {
public boolean enabled;
public int size;
public int type;
public boolean normalized;
public int stride;
public long offset;
public int vboId;
public ByteBuffer clientPointer;
public void reset() {
enabled = false;
size = 0;
type = 0;
normalized = false;
stride = 0;
offset = 0;
vboId = 0;
clientPointer = null;
}
public int effectiveStride() {
return (stride != 0) ? stride : size * typeSizeBytes();
}
public int typeSizeBytes() {
return glTypeSizeBytes(type);
}
public static int glTypeSizeBytes(int glType) {
return switch (glType) {
case GL11.GL_FLOAT, GL11.GL_INT, GL11.GL_UNSIGNED_INT -> 4;
case GL11.GL_DOUBLE -> 8;
case GL11.GL_SHORT, GL11.GL_UNSIGNED_SHORT -> 2;
case GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE -> 1;
default -> 4;
};
}
public float readComponent(ByteBuffer buf, int base, int component) {
return switch (type) {
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
case GL11.GL_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? v / (float) Integer.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_INT -> {
final int v = buf.getInt(base + component * 4);
yield normalized ? (v & 0xFFFFFFFFL) / (float) 0xFFFFFFFFL : (float) (v & 0xFFFFFFFFL);
}
case GL11.GL_SHORT -> {
final short v = buf.getShort(base + component * 2);
yield normalized ? v / (float) Short.MAX_VALUE : (float) v;
}
case GL11.GL_UNSIGNED_SHORT -> {
final int v = buf.getShort(base + component * 2) & 0xFFFF;
yield normalized ? v / (float) 0xFFFF : (float) v;
}
case GL11.GL_BYTE -> {
final byte v = buf.get(base + component);
yield normalized ? v / 127.0f : (float) v;
}
case GL11.GL_UNSIGNED_BYTE -> {
final int v = buf.get(base + component) & 0xFF;
yield normalized ? v / 255.0f : (float) v;
}
default -> 0.0f;
};
}
}
}
@@ -1,142 +1,48 @@
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;
public final class OrphanStreamingBuffer {
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;
public void upload(ByteBuffer data) {
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
boolean freshAllocation = false;
if (alignedPos + dataSize > capacity) {
// Doesn't fit — grow if needed, allocate new storage, reset to offset 0
growIfNeeded(dataSize);
allocateBuffer();
alignedPos = 0;
freshAllocation = true;
} else if (needsOrphan) {
// First upload of the frame — need fresh storage
if (strategy == UploadStrategy.BUFFER_SUB_DATA) {
// SubData strategy: orphan via glBufferData to get new backing store
allocateBuffer();
}
}
if (strategy == UploadStrategy.BUFFER_SUB_DATA) {
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, alignedPos, data);
} else {
// MapRange: choose flags based on whether this is the first upload of the frame
final int flags;
if (freshAllocation) {
// Fresh allocation — nothing to invalidate, just write
flags = MAP_WRITE_INVALIDATE_RANGE;
} else if (needsOrphan) {
// First upload of frame on existing buffer — invalidate entire buffer
flags = MAP_WRITE_INVALIDATE_BUFFER;
} else {
// Subsequent upload — invalidate only our range
flags = MAP_WRITE_INVALIDATE_RANGE;
}
final ByteBuffer mapped = LWJGL.glMapBufferRange(GL15.GL_ARRAY_BUFFER, alignedPos, dataSize, flags);
memCopy(memAddress0(data), memAddress0(mapped), dataSize);
GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER);
}
capacity = StreamingUploader.upload(data, capacity);
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;
}
void upload(StreamingUploader.UploadStrategy strategy, ByteBuffer data) {
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
capacity = StreamingUploader.upload(strategy, data, capacity);
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
}
private void allocateBuffer() {
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, capacity, GL15.GL_STREAM_DRAW);
}
@Override
public int getCapacity() {
return capacity;
}
@Override
public int getBufferId() {
return bufferId;
}
@Override
public void postDraw() {
writePos = 0;
needsOrphan = true;
}
@Override
public void destroy() {
if (bufferId != 0) {
GL15.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
int getWritePos() {
return writePos;
}
UploadStrategy getStrategy() {
return strategy;
}
}
@@ -0,0 +1,68 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.AngelicaMod;
import me.jellysquid.mods.sodium.client.gui.options.named.NamedState;
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;
public final class StreamingUploader {
public enum UploadStrategy implements NamedState {
BUFFER_DATA("sodium.options.upload_method.buffer_data"),
BUFFER_SUB_DATA("sodium.options.upload_method.buffer_subdata"),
MAP_BUFFER_RANGE("sodium.options.upload_method.map_buffer");
private final String key;
UploadStrategy(String key) {
this.key = key;
}
@Override
public String getKey() {
return this.key;
}
}
private static final int MAP_WRITE_INVALIDATE_BUFFER = GL30.GL_MAP_WRITE_BIT | GL30.GL_MAP_INVALIDATE_BUFFER_BIT;
public static int upload(ByteBuffer data, int capacity) {
return upload(AngelicaMod.options().advanced.streamingUploadStrategy, data, capacity);
}
public static int upload(UploadStrategy strategy, ByteBuffer data, int capacity) {
switch (strategy) {
case BUFFER_DATA -> {
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
return data.remaining();
}
case BUFFER_SUB_DATA -> {
if (data.remaining() > capacity) {
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
return data.remaining();
}
GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, 0, data);
return capacity;
}
case MAP_BUFFER_RANGE -> {
if (data.remaining() > capacity) {
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STREAM_DRAW);
return data.remaining();
}
final int dataSize = data.remaining();
final ByteBuffer mapped = LWJGL.glMapBufferRange(GL15.GL_ARRAY_BUFFER, 0, dataSize, MAP_WRITE_INVALIDATE_BUFFER);
memCopy(memAddress0(data), memAddress0(mapped), dataSize);
GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER);
return capacity;
}
default -> throw new UnsupportedOperationException();
}
}
}
@@ -81,6 +81,17 @@ public class AngelicaTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader {
throw new RuntimeException(e);
}
verifyDependencies();
// Register a scoped redirector early so classes prematurely loaded by other coremods during discovery/injectData
// (e.g. Mycelium using HookLoader.class.getName()) still get GL calls redirected to GLSM. Only targets specific mod
// packages to avoid interfering with mixin delegation on game classes. AngelicaLateTweaker removes this registration once
// mixins are setup.
if (FMLLaunchHandler.side().isClient()) {
final boolean rfbLoaded = Launch.blackboard.getOrDefault("angelica.rfbPluginLoaded", Boolean.FALSE) == Boolean.TRUE;
if (!rfbLoaded) {
Launch.classLoader.registerTransformer("com.gtnewhorizons.angelica.loading.fml.transformers.EarlyRedirectorTransformer");
}
}
}
private static void verifyDependencies() {
@@ -139,7 +150,7 @@ public class AngelicaTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader {
// specific IClassTransformers that will run last in the transformer chain.
// If we were to register them normally in getASMTransformerClass(),
// they would be sorted at index 0 which we do not want.
boolean rfbLoaded = Launch.blackboard.getOrDefault("angelica.rfbPluginLoaded", Boolean.FALSE) == Boolean.TRUE;
final boolean rfbLoaded = Launch.blackboard.getOrDefault("angelica.rfbPluginLoaded", Boolean.FALSE) == Boolean.TRUE;
if (!rfbLoaded) {
tweaks.add("com.gtnewhorizons.angelica.loading.fml.tweakers.AngelicaLateTweaker");
if (AngelicaConfig.enableCeleritas) {
@@ -157,7 +168,7 @@ public class AngelicaTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader {
@Override
public String getMixinConfig() {
int v = Runtime.version().feature();
final int v = Runtime.version().feature();
if (v >= 21) return "mixins.angelica.early.j21.json";
if (v >= 17) return "mixins.angelica.early.j17.json";
return "mixins.angelica.early.json";
@@ -174,7 +185,7 @@ public class AngelicaTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader {
}
private static void addLwjgl3ifyExclusions() {
var handle = SharedConfig.getRfbTransformers().stream()
final var handle = SharedConfig.getRfbTransformers().stream()
.filter(transformer -> transformer.id().equals("lwjgl3ify:redirect"))
.findFirst()
.orElse(null);
@@ -30,13 +30,13 @@ 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);
inner.trackBlockSubclasses(cr.getClassName(), cr.getSuperName());
if (!inner.shouldTransform(basicClass)) {
return basicClass;
}
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
final boolean changed = inner.transformClassNode(transformedName, cn);
@@ -0,0 +1,64 @@
package com.gtnewhorizons.angelica.loading.fml.transformers;
import com.gtnewhorizons.angelica.loading.shared.transformers.AngelicaRedirector;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
/**
* A scoped redirector that only transforms classes from known-misbehaving (core)mod packages.
* <p>
* Some mods prematurely load classes that call GL functions during coremod discovery/injectData.
* Those classes get missed by the late-registered {@link AngelicaRedirectorTransformer}, so their
* GL calls permanently bypass GLSM.
* <p>
* This transformer is registered in the {@code AngelicaTweaker} constructor (before any
* coremod's {@code injectData}) and is removed by {@code AngelicaLateTweaker} once the
* full redirector is in its proper post-mixin position.
* <p>
* The mixin service will add this to its delegation list, but it no-ops on game classes
* since it only targets specific mod packages.
*/
public class EarlyRedirectorTransformer implements IClassTransformer {
private static final String[] TARGET_PACKAGES = {
"cn.tesseract.mycelium.",
};
private final AngelicaRedirector inner = new AngelicaRedirector();
private final String[] exclusions = inner.getTransformerExclusions();
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
boolean targeted = false;
for (String pkg : TARGET_PACKAGES) {
if (transformedName.startsWith(pkg)) {
targeted = true;
break;
}
}
if (!targeted) return basicClass;
for (String exclusion : exclusions) {
if (transformedName.startsWith(exclusion)) return basicClass;
}
if (!inner.shouldTransform(basicClass)) {
return basicClass;
}
final ClassReader cr = new ClassReader(basicClass);
final ClassNode cn = new ClassNode();
cr.accept(cn, 0);
final boolean changed = inner.transformClassNode(transformedName, cn);
if (changed) {
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cn.accept(cw);
return cw.toByteArray();
}
return basicClass;
}
}
@@ -1,15 +1,20 @@
package com.gtnewhorizons.angelica.loading.fml.tweakers;
import cpw.mods.fml.relauncher.FMLRelaunchLog;
import net.minecraft.launchwrapper.IClassTransformer;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;
import java.io.File;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Set;
public class AngelicaLateTweaker implements ITweaker {
private static final String EARLY_REDIRECTOR_CLASS = "com.gtnewhorizons.angelica.loading.fml.transformers.EarlyRedirectorTransformer";
@Override
public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) {
// no-op
@@ -17,7 +22,50 @@ public class AngelicaLateTweaker implements ITweaker {
@Override
public void injectIntoClassLoader(LaunchClassLoader classLoader) {
// no-op
narrowTransformerExclusions(classLoader);
}
/**
* Reduces overly broad transformer exclusions that prevent AngelicaRedirector from doing its job.
*
* Looking at you, DragonAPI.
*/
@SuppressWarnings("unchecked")
private static void narrowTransformerExclusions(LaunchClassLoader classLoader) {
try {
final Field exceptionsField = Launch.classLoader.getClass().getDeclaredField("transformerExceptions");
exceptionsField.setAccessible(true);
final Set<String> exceptions = (Set<String>) exceptionsField.get(classLoader);
if (exceptions.remove("Reika.DragonAPI.ASM")) {
// Don't read ASMCallsClient and Patchers/Fixes/WorldRenderAlpha. The rest are fair game.
// Re-add the other non utility classes
exceptions.add("Reika.DragonAPI.ASM.Patchers");
exceptions.add("Reika.DragonAPI.ASM.Profiling");
exceptions.add("Reika.DragonAPI.ASM.APIStripper");
exceptions.add("Reika.DragonAPI.ASM.ClassReparenter");
exceptions.add("Reika.DragonAPI.ASM.DependentMethodStripper");
exceptions.add("Reika.DragonAPI.ASM.DragonAPIClassTransformer");
exceptions.add("Reika.DragonAPI.ASM.FMLItemBlockPatch");
exceptions.add("Reika.DragonAPI.ASM.FluidNamePatch");
exceptions.add("Reika.DragonAPI.ASM.InterfaceInjector");
exceptions.add("Reika.DragonAPI.ASM.SpecialBiomePlacement");
exceptions.add("Reika.DragonAPI.ASM.StructureLootHooks");
FMLRelaunchLog.info("[Angelica] Narrowed Reika.DragonAPI.ASM transformer exclusion to allow GL redirection of utility classes");
}
if (exceptions.remove("xaero.common.core")) {
exceptions.add("xaero.common.core.transformer");
FMLRelaunchLog.info("[Angelica] Narrowed xaero.common.core transformer exclusion to allow GL redirection");
}
if (exceptions.remove("xaero.map.core")) {
exceptions.add("xaero.map.core.transformer");
FMLRelaunchLog.info("[Angelica] Narrowed xaero.map.core transformer exclusion to allow GL redirection");
}
} catch (Exception e) {
FMLRelaunchLog.warning("[Angelica] Failed to narrow transformer exclusions: %s", e.getMessage());
}
}
@Override
@@ -26,9 +74,20 @@ public class AngelicaLateTweaker implements ITweaker {
}
@Override
@SuppressWarnings("unchecked")
public String[] getLaunchArguments() {
// Run after Mixins, but before LWJGl3ify
String transformer = "com.gtnewhorizons.angelica.loading.fml.transformers.AngelicaRedirectorTransformer";
// Remove the scoped early redirector (no longer needed) and register the full one in its proper position: after mixins
try {
final Field transformersField = Launch.classLoader.getClass().getDeclaredField("transformers");
transformersField.setAccessible(true);
final List<IClassTransformer> transformers = (List<IClassTransformer>) transformersField.get(Launch.classLoader);
transformers.removeIf(t -> t.getClass().getName().equals(EARLY_REDIRECTOR_CLASS));
} catch (Exception e) {
FMLRelaunchLog.warning("[Angelica] Failed to remove EarlyRedirectorTransformer: %s", e.getMessage());
}
final String transformer = "com.gtnewhorizons.angelica.loading.fml.transformers.AngelicaRedirectorTransformer";
FMLRelaunchLog.finer("Registering transformer %s", transformer);
Launch.classLoader.registerTransformer(transformer);
return new String[0];
@@ -54,14 +54,8 @@ public class RFBCeleritasBlockTransformer implements RfbClassTransformer {
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;
inner.trackBlockSubclasses(metadata.binaryThisName(), metadata.binarySuperName());
return inner.shouldTransform(classNode.getOriginalBytes());
}
@Override
@@ -12,6 +12,7 @@ import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import java.util.ArrayList;
import java.util.HashMap;
@@ -70,6 +71,7 @@ public final class AngelicaRedirector {
private static final Map<String, Map<String, String>> methodRedirects = new HashMap<>(32);
private static final Map<String, Map<String, String>> interfaceRedirects = new HashMap<>();
private static final Map<Integer, String> glCapRedirects = new HashMap<>();
private static final Map<String, String> gluRedirects = new HashMap<>();
private static final ClassConstantPoolParser cstPoolParser;
static {
@@ -183,10 +185,17 @@ public final class AngelicaRedirector {
.add("glTexEnvi")
.add("glTexEnvf")
.add("glTexEnv")
.add("glGetTexEnv")
.add("glGetTexEnvi")
.add("glGetTexEnvf")
.add("glTexGeni")
.add("glTexGenf")
.add("glTexGend")
.add("glTexGen")
.add("glGetTexGen")
.add("glGetTexGeni")
.add("glGetTexGenf")
.add("glGetTexGend")
.add("glTexImage1D")
.add("glTexImage2D")
.add("glTexImage3D")
@@ -227,8 +236,10 @@ public final class AngelicaRedirector {
.add("glTranslatef")
.add("glVertex2d")
.add("glVertex2f")
.add("glVertex2i")
.add("glVertex3d")
.add("glVertex3f")
.add("glVertex3i")
.add("glVertexPointer")
.add("glColorPointer")
.add("glNormalPointer")
@@ -268,6 +279,7 @@ public final class AngelicaRedirector {
);
methodRedirects.put(GL15, RedirectMap.newMap()
.add("glBindBuffer")
.add("glDeleteBuffers")
);
methodRedirects.put(GL20, RedirectMap.newMap()
.add("glBlendEquationSeparate")
@@ -306,11 +318,15 @@ public final class AngelicaRedirector {
.add("glGetAttachedShaders")
.add("glGetShaderSource")
.add("glGetUniform")
.add("glVertexAttribPointer")
.add("glEnableVertexAttribArray")
.add("glDisableVertexAttribArray")
);
methodRedirects.put(GL30, RedirectMap.newMap()
.add("glBindVertexArray")
.add("glDeleteVertexArrays")
.add("glBindFramebuffer")
.add("glVertexAttribIPointer")
);
// MINECRAFT
@@ -408,9 +424,15 @@ public final class AngelicaRedirector {
.add("glBindVertexArray")
);
gluRedirects.put("org/lwjgl/util/glu/Sphere", "com/gtnewhorizons/angelica/glsm/compat/lwjgl/AngelicaSphere");
gluRedirects.put("org/lwjgl/util/glu/Cylinder", "com/gtnewhorizons/angelica/glsm/compat/lwjgl/AngelicaCylinder");
gluRedirects.put("org/lwjgl/util/glu/Disk", "com/gtnewhorizons/angelica/glsm/compat/lwjgl/AngelicaDisk");
gluRedirects.put("org/lwjgl/util/glu/PartialDisk", "com/gtnewhorizons/angelica/glsm/compat/lwjgl/AngelicaPartialDisk");
final List<String> stringsToSearch = new ArrayList<>(32);
stringsToSearch.add(VaoFunctions);
stringsToSearch.add(LWJGLService);
stringsToSearch.addAll(gluRedirects.keySet());
final String glPrefix = "org/lwjgl/opengl/GL";
for (var entry : new HashMap<>(methodRedirects).entrySet()) {
stringsToSearch.add(entry.getKey());
@@ -425,6 +447,7 @@ public final class AngelicaRedirector {
public String[] getTransformerExclusions() {
return new String[]{
"org.lwjgl",
"com.gtnewhorizon.gtnhlib.asm",
"com.gtnewhorizons.angelica.glsm.",
"com.gtnewhorizons.angelica.transform",
"me.eigenraven.lwjgl3ify"
@@ -446,7 +469,19 @@ public final class AngelicaRedirector {
}
boolean redirectInMethod = false;
for (AbstractInsnNode node : mn.instructions.toArray()) {
if (node instanceof MethodInsnNode mNode) {
if (node instanceof TypeInsnNode tNode) {
// Redirect NEW and CHECKCAST for quadric classes
if (tNode.getOpcode() == Opcodes.NEW || tNode.getOpcode() == Opcodes.CHECKCAST) {
final String redirect = gluRedirects.get(tNode.desc);
if (redirect != null) {
if (LOG_SPAM) {
LOGGER.info("Redirecting {} in {} from {} to {}", tNode.getOpcode() == Opcodes.NEW ? "NEW" : "CHECKCAST", transformedName, tNode.desc, redirect);
}
tNode.desc = redirect;
changed = true;
}
}
} else if (node instanceof MethodInsnNode mNode) {
if (mNode.desc.equals("(I)V") && (mNode.owner.equals(GL11) || mNode.owner.equals(GL11C)) && (mNode.name.equals("glEnable") || mNode.name.equals("glDisable"))) {
final AbstractInsnNode prevNode = node.getPrevious();
String name = null;
@@ -527,6 +562,17 @@ public final class AngelicaRedirector {
}
}
}
// Redirect <init> calls for quadric classes
if (mNode.getOpcode() == Opcodes.INVOKESPECIAL && mNode.name.equals("<init>")) {
final String redirect = gluRedirects.get(mNode.owner);
if (redirect != null) {
if (LOG_SPAM) {
LOGGER.info("Redirecting <init> in {} from {} to {}", transformedName, mNode.owner, redirect);
}
mNode.owner = redirect;
changed = true;
}
}
}
}
if (ASSERT_MAIN_THREAD && redirectInMethod && !transformedName.startsWith(SplashProgress) && !(transformedName.startsWith(MinecraftClient) && ExcludedMinecraftMainThreadChecks.contains(mn.name))) {
@@ -25,10 +25,21 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class CeleritasBlockTransform {
public final class CeleritasBlockTransform implements Opcodes {
private static final boolean LOG_SPAM = Boolean.getBoolean("angelica.redirectorLogspam");
private static final Logger LOGGER = LogManager.getLogger("CeleritasBlockTransformer");
private static final String BlockClass = "net/minecraft/block/Block";
private static final String ThreadedBlockData = "com/gtnewhorizons/angelica/glsm/ThreadedBlockData";
private final Map<String, String> fieldNameToRedirect = new HashMap<>();
private final Set<String> blockSubclasses = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Block subclass owners we shouldn't redirect because they shadow some fields we want to redirect
private final Set<String> blockSubclassExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final ClassConstantPoolParser cstPoolParser;
private final Map<String, String> blockFieldRedirects = new HashMap<>();
private final MethodHandle celeritasEnabledGetter;
private boolean isCeleritasEnabled;
public CeleritasBlockTransform(boolean isObf) {
final List<Pair<String, String>> mappings = ImmutableList.of(
@@ -41,65 +52,47 @@ public final class CeleritasBlockTransform {
);
for (Pair<String, String> pair : mappings) {
final String name = isObf ? pair.getRight() : pair.getLeft();
this.blockFieldRedirects.put(name, pair.getLeft());
this.fieldNameToRedirect.put(name, pair.getLeft());
}
this.cstPoolParser = new ClassConstantPoolParser(this.blockFieldRedirects.keySet().toArray(new String[0]));
}
this.cstPoolParser = new ClassConstantPoolParser(this.fieldNameToRedirect.keySet().toArray(new String[0]));
private static final boolean LOG_SPAM = Boolean.getBoolean("angelica.redirectorLogspam");
private static final Logger LOGGER = LogManager.getLogger("CeleritasBlockTransformer");
private static final String BlockClass = "net/minecraft/block/Block";
private static final String BlockPackage = "net/minecraft/block/Block";
private static final String ThreadedBlockData = "com/gtnewhorizons/angelica/glsm/ThreadedBlockData";
/** All classes in <tt>net.minecraft.block.*</tt> are the block subclasses save for these. */
private static final String[] VanillaBlockExclusions = {
"net/minecraft/block/IGrowable",
"net/minecraft/block/ITileEntityProvider",
"net/minecraft/block/BlockEventData",
"net/minecraft/block/BlockSourceImpl",
"net/minecraft/block/material/"
};
blockSubclasses.add(BlockClass);
private static final Set<String> moddedBlockSubclasses = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Block owners we *shouldn't* redirect because they shadow one of our fields
private static final Set<String> blockOwnerExclusions = Collections.newSetFromMap(new ConcurrentHashMap<>());
// Needed because the config is loaded in LaunchClassLoader, but we need to access it in the parent system loader.
private static final MethodHandle angelicaConfigCeleritasEnabledGetter;
private static boolean isCeleritasEnabled;
static {
try {
// Needed because the config is loaded in LaunchClassLoader, but we need to access it in the parent system loader.
final Class<?> angelicaConfig = Class.forName("com.gtnewhorizons.angelica.config.AngelicaConfig", true, Launch.classLoader);
angelicaConfigCeleritasEnabledGetter = MethodHandles.lookup().findStaticGetter(angelicaConfig, "enableCeleritas", boolean.class);
celeritasEnabledGetter = MethodHandles.lookup().findStaticGetter(angelicaConfig, "enableCeleritas", boolean.class);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
private static boolean isCeleritasEnabled() {
public void setCeleritasSetting() {
isCeleritasEnabled = true;
}
private boolean isCeleritasEnabled() {
if (isCeleritasEnabled) return true;
try {
return (boolean) angelicaConfigCeleritasEnabledGetter.invokeExact();
return (boolean) celeritasEnabledGetter.invokeExact();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private boolean isVanillaBlockSubclass(String className) {
if (className.startsWith(BlockPackage)) {
for (String exclusion : VanillaBlockExclusions) {
if (className.startsWith(exclusion)) {
return false;
}
}
return true;
}
return false;
}
// This method needs to be called for every class, including the ones we don't want to transform
public void trackBlockSubclasses(String className, String superClassName) {
// It works because the deepest subclasses are always transformed first due to
// ForgeEventTransformer recursive superclass loading
if (blockSubclasses.contains(superClassName)) {
blockSubclasses.add(className);
private boolean isBlockSubclass(String className) {
return isVanillaBlockSubclass(className) || moddedBlockSubclasses.contains(className);
if (blockSubclassExclusions.contains(superClassName)) {
LOGGER.info("Class {} extends a class with shadowed block bounds fields and will be skipped from redirecting", className);
blockSubclassExclusions.add(className);
}
}
}
public String[] getTransformerExclusions() {
@@ -111,12 +104,6 @@ public final class CeleritasBlockTransform {
};
}
public void trackBlockSubclasses(String className, String classSuperName) {
if (!isVanillaBlockSubclass(className) && isBlockSubclass(classSuperName)) {
moddedBlockSubclasses.add(className);
}
}
public boolean shouldTransform(byte[] classBytes) {
return cstPoolParser.find(classBytes);
}
@@ -126,80 +113,66 @@ public final class CeleritasBlockTransform {
if (!isCeleritasEnabled()) {
return false;
}
boolean changed = false;
if ("net.minecraft.block.Block".equals(transformedName)) {
changed = cn.fields.removeIf(field -> blockFieldRedirects.containsKey(field.name));
changed = cn.fields.removeIf(field -> fieldNameToRedirect.containsKey(field.name));
} else {
trackBlockShadowingFields(cn);
}
trackBlockSubclasses(cn.name, cn.superName);
// Check if this class shadows any fields of the parent class
if (moddedBlockSubclasses.contains(cn.name)) {
// If a superclass shadows, then so do we, because JVM will resolve a reference on our class to that
// superclass
boolean doWeShadow = false;
if (blockOwnerExclusions.contains(cn.superName)) {
doWeShadow = true;
} else {
// Check if we declare any known field names
for (FieldNode field : cn.fields) {
if (blockFieldRedirects.containsKey(field.name)) {
doWeShadow = true;
break;
}
}
}
if (doWeShadow) {
LOGGER.info("Class '{}' shadows one or more block bounds fields, these accesses won't be redirected!", cn.name);
blockOwnerExclusions.add(cn.name);
}
}
for (MethodNode mn : cn.methods) {
for (AbstractInsnNode node : mn.instructions.toArray()) {
if ((node.getOpcode() == Opcodes.GETFIELD || node.getOpcode() == Opcodes.PUTFIELD) && node instanceof FieldInsnNode fNode) {
if (!blockOwnerExclusions.contains(fNode.owner) && isBlockSubclass(fNode.owner)) {
String fieldRedirect = blockFieldRedirects.get(fNode.name);
if (fieldRedirect != null) {
if (LOG_SPAM) {
LOGGER.info("Redirecting Block.{} in {} to thread-safe wrapper", fNode.name, transformedName);
}
// Perform the redirect
fNode.name = fieldRedirect; // use unobfuscated name
fNode.owner = ThreadedBlockData;
// Inject getter before the field access, to turn Block -> ThreadedBlockData
final MethodInsnNode getter = new MethodInsnNode(Opcodes.INVOKESTATIC, ThreadedBlockData, "get", "(L" + BlockClass + ";)L" + ThreadedBlockData + ";", false);
if (node.getOpcode() == Opcodes.GETFIELD) {
mn.instructions.insertBefore(fNode, getter);
} else if (node.getOpcode() == Opcodes.PUTFIELD) {
// FIXME: this code assumes doubles
// Stack: Block, double
final InsnList beforePut = new InsnList();
beforePut.add(new InsnNode(Opcodes.DUP2_X1));
// Stack: double, Block, double
beforePut.add(new InsnNode(Opcodes.POP2));
// Stack: double, Block
beforePut.add(getter);
// Stack: double, ThreadedBlockData
beforePut.add(new InsnNode(Opcodes.DUP_X2));
// Stack: ThreadedBlockData, double, ThreadedBlockData
beforePut.add(new InsnNode(Opcodes.POP));
// Stack: ThreadedBlockData, double
mn.instructions.insertBefore(fNode, beforePut);
}
changed = true;
}
}
}
}
}
changed |= redirectBlockBoundFields(cn, transformedName);
return changed;
}
public void setCeleritasSetting() {
isCeleritasEnabled = true;
private void trackBlockShadowingFields(ClassNode cn) {
if (blockSubclasses.contains(cn.name)) {
for (FieldNode field : cn.fields) {
if (fieldNameToRedirect.containsKey(field.name)) {
LOGGER.info("Class '{}' shadows one or more block bounds fields, these accesses won't be redirected!", cn.name);
blockSubclassExclusions.add(cn.name);
break;
}
}
}
}
private boolean redirectBlockBoundFields(ClassNode cn, String transformedName) {
boolean changed = false;
for (MethodNode mn : cn.methods) {
for (AbstractInsnNode node = mn.instructions.getFirst(); node != null; node = node.getNext()) {
if ((node.getOpcode() == GETFIELD || node.getOpcode() == PUTFIELD) && node instanceof FieldInsnNode fNode) {
String newFieldName = fieldNameToRedirect.get(fNode.name);
if (newFieldName != null && blockSubclasses.contains(fNode.owner) && !blockSubclassExclusions.contains(fNode.owner)) {
if (LOG_SPAM) {
LOGGER.info("Redirecting Block.{} in {} to thread-safe wrapper", fNode.name, transformedName);
}
// Perform the redirect
fNode.name = newFieldName; // use unobfuscated name
fNode.owner = ThreadedBlockData;
// Inject getter before the field access, to turn Block -> ThreadedBlockData
final MethodInsnNode getter = new MethodInsnNode(INVOKESTATIC, ThreadedBlockData, "get", "(L" + BlockClass + ";)L" + ThreadedBlockData + ";", false);
if (node.getOpcode() == GETFIELD) {
mn.instructions.insertBefore(fNode, getter);
} else if (node.getOpcode() == PUTFIELD) {
// FIXME: this code assumes doubles
// Stack: Block, double
final InsnList beforePut = new InsnList();
beforePut.add(new InsnNode(DUP2_X1));
// Stack: double, Block, double
beforePut.add(new InsnNode(POP2));
// Stack: double, Block
beforePut.add(getter);
// Stack: double, ThreadedBlockData
beforePut.add(new InsnNode(DUP_X2));
// Stack: ThreadedBlockData, double, ThreadedBlockData
beforePut.add(new InsnNode(POP));
// Stack: ThreadedBlockData, double
mn.instructions.insertBefore(fNode, beforePut);
}
changed = true;
}
}
}
}
return changed;
}
}
@@ -29,16 +29,18 @@ public enum Mixins implements IMixins {
.addClientMixins(
"angelica.MixinActiveRenderInfo"
, "angelica.MixinEntityRenderer"
, "angelica.MixinFMLClientHandler"
, "angelica.MixinForgeHooksClient_CoreProfile"
, "angelica.MixinGameSettings"
, "angelica.MixinMinecraft"
, "angelica.MixinMinecraftServer"
, "angelica.optimizations.MixinRendererLivingEntity"
, "angelica.MixinFMLClientHandler"
, "angelica.bugfixes.MixinRenderGlobal_DestroyBlock"
, "angelica.glsm.MixinSplashProgressCaching"
, "angelica.debug.MixinMinecraft_FPSCap"
, "angelica.ffp.MixinTessellator_CoreProfile"
, "angelica.glsm.MixinSplashProgressCaching"
, "angelica.gui.MixinGuiOptions"
, "angelica.optimizations.MixinRendererLivingEntity"
, "angelica.rendering.MixinRenderGlobal_SelectionBox"
)
),
@@ -49,6 +51,13 @@ public enum Mixins implements IMixins {
.addClientMixins("angelica.vbo.MixinRenderGlobal")
),
ANGELICA_PANORAMA_BLUR(
new MixinBuilder("Replace main menu panorama with modern equivalent")
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enablePanoramaBlurShader)
.addClientMixins("angelica.gui.MixinGuiMainMenu")
),
ANGELICA_FONT_RENDERER(new MixinBuilder()
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableFontRenderer)
@@ -221,7 +230,6 @@ public enum Mixins implements IMixins {
, "shaders.MixinRenderGlobal"
, "shaders.MixinRenderHorse"
, "shaders.MixinRenderItem"
, "shaders.MixinRenderManager"
, "shaders.MixinRenderNameTag"
, "shaders.MixinRenderPlayerArmor"
, "shaders.MixinTileEntityBeaconRenderer"
@@ -230,6 +238,24 @@ public enum Mixins implements IMixins {
)
),
IRIS_SHADERS_RENDER_MANAGER(new MixinBuilder()
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableIris)
.addExcludedMod(TargetedMod.DRAGON_API)
.addClientMixins(
"shaders.MixinRenderManager"
)
),
IRIS_SHADERS_RENDER_MANAGER_DAPI(new MixinBuilder()
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableIris)
.addRequiredMod(TargetedMod.DRAGON_API)
.addClientMixins(
"shaders.MixinRenderManagerDAPI"
)
),
IRIS_RENDERING_NOBACKHAND(new MixinBuilder("Iris Hand Shaders")
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableIris)
@@ -354,8 +380,10 @@ public enum Mixins implements IMixins {
REPLACE_FFP(new MixinBuilder()
.setPhase(Phase.EARLY)
.addClientMixins("angelica.vbo.MixinFramebuffer")
.setApplyIf(() -> AngelicaConfig.replaceFFPUploads)),
.setApplyIf(() -> AngelicaConfig.replaceFFPUploads)
.addClientMixins(
"angelica.vbo.MixinFramebuffer",
"angelica.vbo.MixinWavefrontObject")),
//From NotFine
NOTFINE_BASE_MOD(new MixinBuilder()
@@ -16,6 +16,7 @@ import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.debug.OpenGLDebugging;
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
import com.gtnewhorizons.angelica.loading.AngelicaTweaker;
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
import com.gtnewhorizons.angelica.render.CloudRenderer;
@@ -0,0 +1,346 @@
package com.gtnewhorizons.angelica.render;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.uniform.GlUniformFloat2v;
import net.minecraft.client.Minecraft;
import net.minecraft.util.MathHelper;
import net.minecraft.util.ResourceLocation;
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
import org.embeddedt.embeddium.impl.gl.shader.GlShader;
import org.embeddedt.embeddium.impl.gl.shader.ShaderBindingContext;
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
import org.embeddedt.embeddium.impl.gl.shader.ShaderType;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix4f;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import java.util.function.Function;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
/**
* Replaces the vanilla main menu panorama renderer with modern opengl. Significant reduction in draw calls.
*/
public class PanoramaRenderer {
private static final int FBO_SIZE = 256;
private static final int PANORAMA_GRID = 8;
private static final float BLUR_RADIUS = 3.0f;
private static final int VERTEX_STRIDE = 24;
private static final int VERTS_PER_FACE = PANORAMA_GRID * PANORAMA_GRID * 4;
private static final int INDICES_PER_FACE = PANORAMA_GRID * PANORAMA_GRID * 6;
private static PanoramaRenderer instance;
private int fboA, fboB, texA, texB, cubeVao, cubeVbo, cubeIbo, emptyVao;
private GlProgram<CubeUniforms> cubeProgram;
private GlProgram<BlurUniforms> blurProgram;
private GlProgram<BlitUniforms> blitProgram;
private boolean initialized;
private final Matrix4f mvpScratch = new Matrix4f();
private final Matrix4f projScratch = new Matrix4f();
private final Matrix4f baseMVScratch = new Matrix4f();
// Pre-computed face rotation matrices
private static final Matrix4f[] FACE_ROTATIONS = new Matrix4f[6];
static {
FACE_ROTATIONS[0] = new Matrix4f();
FACE_ROTATIONS[1] = new Matrix4f().rotateY((float) Math.toRadians(90.0));
FACE_ROTATIONS[2] = new Matrix4f().rotateY((float) Math.toRadians(180.0));
FACE_ROTATIONS[3] = new Matrix4f().rotateY((float) Math.toRadians(-90.0));
FACE_ROTATIONS[4] = new Matrix4f().rotateX((float) Math.toRadians(90.0));
FACE_ROTATIONS[5] = new Matrix4f().rotateX((float) Math.toRadians(-90.0));
}
public static PanoramaRenderer getInstance() {
if (instance == null) {
instance = new PanoramaRenderer();
}
return instance;
}
private void init() {
if (initialized) return;
initialized = true;
texA = createTexture();
texB = createTexture();
fboA = createFbo(texA);
fboB = createFbo(texB);
buildCubeGeometry();
emptyVao = GL30.glGenVertexArrays();
initShaders();
}
private void initShaders() {
cubeProgram = loadProgram("angelica:panorama_cube", "angelica:panorama_cube", "angelica:panorama_cube", CubeUniforms::new);
blitProgram = loadProgram("angelica:panorama_blit", "angelica:panorama_blur", "angelica:panorama_blit", BlitUniforms::new);
blurProgram = loadProgram("angelica:panorama_blur", "angelica:panorama_blur", "angelica:panorama_blur", BlurUniforms::new);
blurProgram.bind();
blurProgram.getInterface().radius.setFloat(BLUR_RADIUS);
blurProgram.unbind();
}
private static <T> GlProgram<T> loadProgram(String name, String vertBase, String fragBase, Function<ShaderBindingContext, T> factory) {
final GlShader vert = ShaderLoader.loadShader(ShaderType.VERTEX, vertBase + ".vert", ShaderConstants.EMPTY);
final GlShader frag = ShaderLoader.loadShader(ShaderType.FRAGMENT, fragBase + ".frag", ShaderConstants.EMPTY);
try {
final GlProgram<T> program = GlProgram.builder(name).attachShader(vert).attachShader(frag).link(factory);
program.bind();
GL20.glUniform1i(GL20.glGetUniformLocation(program.handle(), "u_Texture"), 0);
program.unbind();
return program;
} finally {
vert.delete();
frag.delete();
}
}
private void buildCubeGeometry() {
final int grid = PANORAMA_GRID;
final int numPasses = grid * grid;
final FloatBuffer vbuf = ByteBuffer.allocateDirect(VERTS_PER_FACE * VERTEX_STRIDE).order(ByteOrder.nativeOrder()).asFloatBuffer();
for (int k = 0; k < numPasses; k++) {
final float tx = ((float) (k % grid) / grid - 0.5f) / 64.0f;
final float ty = ((float) (k / grid) / grid - 0.5f) / 64.0f;
final float alpha = 1.0f / (k + 1);
vbuf.put(-1.0f + tx).put(-1.0f + ty).put(1.0f).put(0.0f).put(0.0f).put(alpha);
vbuf.put(1.0f + tx).put(-1.0f + ty).put(1.0f).put(1.0f).put(0.0f).put(alpha);
vbuf.put(1.0f + tx).put(1.0f + ty).put(1.0f).put(1.0f).put(1.0f).put(alpha);
vbuf.put(-1.0f + tx).put(1.0f + ty).put(1.0f).put(0.0f).put(1.0f).put(alpha);
}
vbuf.flip();
final ShortBuffer ibuf = ByteBuffer.allocateDirect(INDICES_PER_FACE * 2).order(ByteOrder.nativeOrder()).asShortBuffer();
for (int k = 0; k < numPasses; k++) {
final int base = k * 4;
ibuf.put((short) base).put((short) (base + 1)).put((short) (base + 2));
ibuf.put((short) base).put((short) (base + 2)).put((short) (base + 3));
}
ibuf.flip();
cubeVao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(cubeVao);
cubeVbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, cubeVbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vbuf, GL15.GL_STATIC_DRAW);
GL20.glEnableVertexAttribArray(0);
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, VERTEX_STRIDE, 0);
GL20.glEnableVertexAttribArray(1);
GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, VERTEX_STRIDE, 12);
GL20.glEnableVertexAttribArray(2);
GL20.glVertexAttribPointer(2, 1, GL11.GL_FLOAT, false, VERTEX_STRIDE, 20);
cubeIbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, cubeIbo);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, ibuf, GL15.GL_STATIC_DRAW);
GL30.glBindVertexArray(0);
}
private static int createTexture() {
final int tex = GL11.glGenTextures();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, tex);
GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, FBO_SIZE, FBO_SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);
return tex;
}
private static int createFbo(int colorTex) {
final int fbo = GL30.glGenFramebuffers();
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo);
GL30.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, colorTex, 0);
final int status = GL30.glCheckFramebufferStatus(GL30.GL_FRAMEBUFFER);
if (status != GL30.GL_FRAMEBUFFER_COMPLETE) {
throw new RuntimeException("Panorama FBO incomplete: 0x" + Integer.toHexString(status));
}
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
return fbo;
}
public void destroy() {
if (!initialized) return;
initialized = false;
if (fboA != 0) {
GL30.glDeleteFramebuffers(fboA);
fboA = 0;
}
if (fboB != 0) {
GL30.glDeleteFramebuffers(fboB);
fboB = 0;
}
if (texA != 0) {
GL11.glDeleteTextures(texA);
texA = 0;
}
if (texB != 0) {
GL11.glDeleteTextures(texB);
texB = 0;
}
if (cubeVao != 0) {
GL30.glDeleteVertexArrays(cubeVao);
cubeVao = 0;
}
if (cubeVbo != 0) {
GL15.glDeleteBuffers(cubeVbo);
cubeVbo = 0;
}
if (cubeIbo != 0) {
GL15.glDeleteBuffers(cubeIbo);
cubeIbo = 0;
}
if (emptyVao != 0) {
GL30.glDeleteVertexArrays(emptyVao);
emptyVao = 0;
}
if (cubeProgram != null) {
cubeProgram.delete();
cubeProgram = null;
}
if (blurProgram != null) {
blurProgram.delete();
blurProgram = null;
}
if (blitProgram != null) {
blitProgram.delete();
blitProgram = null;
}
}
public void renderSkybox(int panoramaTimer, float partialTicks, ResourceLocation[] panoramaPaths, Minecraft mc, int screenWidth, int screenHeight, float zLevel) {
init();
GLStateManager.glColorMask(true, true, true, true);
GLStateManager.disableCull();
GLStateManager.disableScissorTest();
GLStateManager.disableDepthTest();
mc.getFramebuffer().unbindFramebuffer();
GL11.glViewport(0, 0, FBO_SIZE, FBO_SIZE);
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fboA);
GL11.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
drawCubeFaces(panoramaTimer, partialTicks, panoramaPaths, mc);
GLStateManager.disableBlend();
blurProgram.bind();
GL30.glBindVertexArray(emptyVao);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final BlurUniforms bu = blurProgram.getInterface();
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fboB);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texA);
bu.direction.set(1.0f / FBO_SIZE, 0.0f);
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GL30.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fboA);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texB);
bu.direction.set(0.0f, 1.0f / FBO_SIZE);
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
mc.getFramebuffer().bindFramebuffer(true);
GL11.glViewport(0, 0, mc.displayWidth, mc.displayHeight);
blitProgram.bind();
GL11.glBindTexture(GL11.GL_TEXTURE_2D, texA);
final float f1 = screenWidth > screenHeight ? 120.0F / screenWidth : 120.0F / screenHeight;
blitProgram.getInterface().scaleV.setFloat(screenHeight * f1 / 256.0F);
blitProgram.getInterface().scaleU.setFloat(screenWidth * f1 / 256.0F);
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
GL30.glBindVertexArray(0);
blitProgram.unbind();
GLStateManager.enableDepthTest();
GLStateManager.enableCull();
GLStateManager.enableBlend();
}
private void drawCubeFaces(int panoramaTimer, float partialTicks, ResourceLocation[] panoramaPaths, Minecraft mc) {
final float pitch = MathHelper.sin(((float) panoramaTimer + partialTicks) / 400.0F) * 25.0F + 20.0F;
final float yaw = -((float) panoramaTimer + partialTicks) * 0.1F;
final Matrix4f base = baseMVScratch.identity().rotateX((float) Math.toRadians(180.0)).rotateZ((float) Math.toRadians(90.0))
.rotateX((float) Math.toRadians(pitch)).rotateY((float) Math.toRadians(yaw));
final Matrix4f proj = projScratch.setPerspective((float) Math.toRadians(120.0), 1.0f, 0.05f, 10.0f);
GLStateManager.enableBlend();
GLStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
GLStateManager.glDepthMask(false);
cubeProgram.bind();
GL30.glBindVertexArray(cubeVao);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final CubeUniforms cu = cubeProgram.getInterface();
for (int face = 0; face < 6; face++) {
proj.mul(base, mvpScratch).mul(FACE_ROTATIONS[face]);
cu.mvp.set(mvpScratch);
mc.getTextureManager().bindTexture(panoramaPaths[face]);
GL11.glDrawElements(GL11.GL_TRIANGLES, INDICES_PER_FACE, GL11.GL_UNSIGNED_SHORT, 0);
}
GL30.glBindVertexArray(0);
cubeProgram.unbind();
GLStateManager.glDepthMask(true);
}
private static class CubeUniforms {
final GlUniformMatrix4f mvp;
CubeUniforms(ShaderBindingContext ctx) {
mvp = ctx.bindUniform("u_MVP", GlUniformMatrix4f::new);
}
}
private static class BlurUniforms {
final GlUniformFloat2v direction;
final GlUniformFloat radius;
BlurUniforms(ShaderBindingContext ctx) {
direction = ctx.bindUniform("u_Direction", GlUniformFloat2v::new);
radius = ctx.bindUniform("u_Radius", GlUniformFloat::new);
}
}
private static class BlitUniforms {
final GlUniformFloat scaleV;
final GlUniformFloat scaleU;
BlitUniforms(ShaderBindingContext ctx) {
scaleV = ctx.bindUniform("u_ScaleV", GlUniformFloat::new);
scaleU = ctx.bindUniform("u_ScaleU", GlUniformFloat::new);
}
}
}
@@ -0,0 +1,109 @@
package com.gtnewhorizons.angelica.render;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.Color4;
import com.gtnewhorizons.angelica.glsm.uniform.GlUniformFloat2v;
import net.minecraft.util.AxisAlignedBB;
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
import org.embeddedt.embeddium.impl.gl.shader.GlShader;
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
import org.embeddedt.embeddium.impl.gl.shader.ShaderType;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat3v;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformFloat4v;
import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix4f;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
public final class SelectionBoxRenderer {
private static GlProgram<SelectionBoxUniforms> program;
private static int emptyVao;
private static final Matrix4f mvpMatrix = new Matrix4f();
private static final float[] colorBuf = new float[4];
private SelectionBoxRenderer() {}
public static void init() {
final boolean needsGS = GLStateManager.wideLineEmulationEnabled;
final GlShader vs = ShaderLoader.loadShader(ShaderType.VERTEX, "angelica:selection_box.vert.glsl", ShaderConstants.EMPTY);
final GlShader fs = ShaderLoader.loadShader(ShaderType.FRAGMENT, "angelica:selection_box.frag.glsl", ShaderConstants.EMPTY);
final GlProgram.Builder builder = GlProgram.builder("SelectionBox").attachShader(vs).attachShader(fs);
GlShader gs = null;
if (needsGS) {
gs = ShaderLoader.loadShader(ShaderType.GEOM, "angelica:selection_box.geom.glsl", ShaderConstants.EMPTY);
builder.attachShader(gs);
}
program = builder.link(ctx -> new SelectionBoxUniforms(
ctx.bindUniform("u_MVP", GlUniformMatrix4f::new),
ctx.bindUniform("u_Min", GlUniformFloat3v::new),
ctx.bindUniform("u_Max", GlUniformFloat3v::new),
ctx.bindUniform("u_Color", GlUniformFloat4v::new),
needsGS ? ctx.bindUniform("u_ViewportSize", GlUniformFloat2v::new) : null,
needsGS ? ctx.bindUniform("u_LineWidth", GlUniformFloat::new) : null));
vs.destroy();
fs.destroy();
if (gs != null) gs.destroy();
emptyVao = GL30.glGenVertexArrays();
}
public static void draw(AxisAlignedBB aabb, int color) {
if (program == null) return;
final SelectionBoxUniforms uniforms = program.getInterface();
program.bind();
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), mvpMatrix);
uniforms.mvp.set(mvpMatrix);
uniforms.min.set((float) aabb.minX, (float) aabb.minY, (float) aabb.minZ);
uniforms.max.set((float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ);
if (color != -1) {
colorBuf[0] = ((color >> 16) & 0xFF) / 255.0f;
colorBuf[1] = ((color >> 8) & 0xFF) / 255.0f;
colorBuf[2] = (color & 0xFF) / 255.0f;
colorBuf[3] = 1.0f;
} else {
final Color4 c = GLStateManager.getColor();
colorBuf[0] = c.getRed();
colorBuf[1] = c.getGreen();
colorBuf[2] = c.getBlue();
colorBuf[3] = c.getAlpha();
}
uniforms.color.set(colorBuf);
if (uniforms.viewportSize != null) {
uniforms.viewportSize.set(GLStateManager.getViewportState().width, GLStateManager.getViewportState().height);
uniforms.lineWidth.setFloat(GLStateManager.getLineState().getWidth());
}
GLStateManager.glBindVertexArray(emptyVao);
GL11.glDrawArrays(GL11.GL_LINES, 0, 24);
GLStateManager.glBindVertexArray(0);
program.unbind();
}
public static void destroy() {
if (program != null) {
program.destroy();
program = null;
}
if (emptyVao != 0) {
GL30.glDeleteVertexArrays(emptyVao);
emptyVao = 0;
}
}
private record SelectionBoxUniforms(GlUniformMatrix4f mvp, GlUniformFloat3v min, GlUniformFloat3v max, GlUniformFloat4v color, GlUniformFloat2v viewportSize, GlUniformFloat lineWidth) {
}
}
+2 -2
View File
@@ -99,8 +99,8 @@ public enum Settings {
ChunkRebuildManager.setMaxTicksWaiting((int) this.option.getStore());
}
},
HORIZON_DISABLE(new NotFineOptionTickBox(true, OptionImpact.LOW)),
FOG_DISABLE(new NotFineOptionTickBox(false, OptionImpact.LOW)),
HORIZON(new NotFineOptionTickBox(true, OptionImpact.LOW)),
TERRAIN_FOG(new NotFineOptionTickBox(true, OptionImpact.LOW)),
FOG_NEAR_DISTANCE(new NotFineOptionSliderPercentage(75, 1, 100, 1, OptionImpact.LOW)),
GUI_BACKGROUND(new NotFineOptionCycling<>(BackgroundSelect.DEFAULT, null)) {
@Override
@@ -209,8 +209,8 @@ public class NotFineGameOptionPages {
.add(Settings.MODE_CLOUD_TRANSLUCENCY.option)
.add(Settings.MODE_STARS.option)
.add(Settings.TOTAL_STARS.option)
.add(Settings.HORIZON_DISABLE.option)
.add(Settings.FOG_DISABLE.option)
.add(Settings.HORIZON.option)
.add(Settings.TERRAIN_FOG.option)
.add(Settings.FOG_NEAR_DISTANCE.option)
.build());
return new OptionPage(I18n.format("options.button.sky"), ImmutableList.copyOf(groups));
@@ -283,7 +283,7 @@ public class ReeseSodiumVideoOptionsScreen extends SodiumOptionsGUI {
}
if (keyCode == Keyboard.KEY_P) {
if(isShiftKeyDown()){
this.mc.displayGuiScreen(new GuiCustomMenu(this.prevScreen, SodiumGameOptionPages.general(), SodiumGameOptionPages.quality(), SodiumGameOptionPages.advanced(), SodiumGameOptionPages.performance(), SodiumGameOptionPages.appearance(), SodiumGameOptionPages.debug()));
this.mc.displayGuiScreen(new GuiCustomMenu(this.prevScreen, SodiumGameOptionPages.general(), SodiumGameOptionPages.quality(), SodiumGameOptionPages.advanced(), SodiumGameOptionPages.performance(), SodiumGameOptionPages.appearance()));
} else if (isCtrlKeyDown()) {
this.mc.displayGuiScreen(new SodiumOptionsGUI(this.prevScreen));
}
@@ -3,6 +3,7 @@ package me.jellysquid.mods.sodium.client.gui;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import jss.notfine.core.Settings;
import jss.notfine.core.SettingsManager;
import me.flashyreese.mods.reeses_sodium_options.client.gui.ReeseSodiumVideoOptionsScreen;
@@ -117,15 +118,6 @@ public class SodiumGameOptionPages {
}
}, (opts) -> opts.fullScreen)
.build())
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.sleepbeforeswap"))
.setTooltip(I18n.format("options.angelica.sleepbeforeswap.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> {
AngelicaConfig.sleepBeforeSwap = value;
}, opts -> AngelicaConfig.sleepBeforeSwap)
.setImpact(OptionImpact.VARIES)
.build())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
@@ -281,14 +273,16 @@ 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(StreamingUploader.UploadStrategy.class, sodiumOpts)
.setName(I18n.format("sodium.options.upload_method.name"))
.setTooltip(I18n.format("sodium.options.upload_method.tooltip"))
.setControl(o ->
new CyclingControl<>(o, StreamingUploader.UploadStrategy.class, StreamingUploader.UploadStrategy.values())
)
.setBinding((opts, value) -> opts.advanced.streamingUploadStrategy = value, opts -> opts.advanced.streamingUploadStrategy)
.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"))
@@ -516,12 +510,12 @@ public class SodiumGameOptionPages {
.add(Settings.MODE_CLOUD_TRANSLUCENCY.option)
.add(Settings.MODE_STARS.option)
.add(Settings.TOTAL_STARS.option)
.add(Settings.HORIZON_DISABLE.option)
.add(Settings.HORIZON.option)
.add(Settings.MODE_LIGHT_FLICKER.option)
.build());
groups.add(OptionGroup.createBuilder()
.add(Settings.FOG_DISABLE.option)
.add(Settings.TERRAIN_FOG.option)
.add(Settings.FOG_NEAR_DISTANCE.option)
.add(Settings.VOID_FOG.option)
.build());
@@ -540,20 +534,4 @@ public class SodiumGameOptionPages {
return new OptionPage(I18n.format("sodium.options.pages.appearance"), ImmutableList.copyOf(groups));
}
public static OptionPage debug() {
final List<OptionGroup> groups = new ArrayList<>();
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, sodiumOpts)
.setName(I18n.format("sodium.options.use_gl_state_cache.name"))
.setTooltip(I18n.format("sodium.options.use_gl_state_cache.tooltip"))
.setControl(TickBoxControl::new)
.setImpact(OptionImpact.EXTREME)
.setBinding((opts, value) -> GLStateManager.BYPASS_CACHE = !value, opts -> !GLStateManager.BYPASS_CACHE)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.build())
.build());
return new OptionPage(I18n.format("sodium.options.pages.debug"), ImmutableList.copyOf(groups));
}
}
@@ -6,6 +6,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality;
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
import net.coderbot.iris.Iris;
@@ -33,7 +34,7 @@ public class SodiumGameOptions {
public boolean useParticleCulling = true;
public boolean allowDirectMemoryAccess = true;
public boolean ignoreDriverBlacklist = false;
public boolean useMapBufferRange = false;
public StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.MAP_BUFFER_RANGE;
public boolean enableDeferredBatching = true;
}
@@ -77,7 +77,6 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen {
this.pages.add(shaderPacks);
}
this.pages.add(SodiumGameOptionPages.debug());
}
public void setPage(OptionPage page) {
@@ -302,7 +301,7 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen {
if (keyCode == Keyboard.KEY_P && isShiftKeyDown()) {
this.mc.displayGuiScreen(new GuiCustomMenu(this.prevScreen, SodiumGameOptionPages.general(),
SodiumGameOptionPages.quality(), SodiumGameOptionPages.advanced(), SodiumGameOptionPages.performance(),
SodiumGameOptionPages.appearance(), SodiumGameOptionPages.debug()));
SodiumGameOptionPages.appearance()));
}
}
@@ -1,27 +0,0 @@
package me.jellysquid.mods.sodium.client.gui.utils;
import com.gtnewhorizons.angelica.AngelicaMod;
import net.minecraft.util.Util;
import java.io.IOException;
public class URLUtils {
private static String[] getURLOpenCommand(String url) {
return switch (Util.getOSType()) {
case WINDOWS -> new String[]{"rundll32", "url.dll,FileProtocolHandler", url};
case OSX -> new String[]{"open", url};
case UNKNOWN, LINUX, SOLARIS -> new String[]{"xdg-open", url};
};
}
public static void open(String url) {
try {
Runtime.getRuntime().exec(getURLOpenCommand(url));
} catch (IOException exception) {
AngelicaMod.LOGGER.error("Couldn't open url '{}'", url, exception);
}
}
}
@@ -14,13 +14,10 @@ import net.coderbot.iris.gl.texture.PixelType;
import net.coderbot.iris.gl.uniform.UniformUpdateFrequency;
import net.coderbot.iris.uniforms.SystemTimeUniforms;
import net.minecraft.client.Minecraft;
import org.apache.commons.io.IOUtils;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.function.IntSupplier;
public class CenterDepthSampler {
@@ -45,16 +42,9 @@ public class CenterDepthSampler {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
this.framebuffer.addColorAttachment(0, texture);
ProgramBuilder builder;
try {
String fsh = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/centerDepth.fsh"))), StandardCharsets.UTF_8);
String vsh = new String(IOUtils.toByteArray(Objects.requireNonNull(getClass().getResourceAsStream("/centerDepth.vsh"))), StandardCharsets.UTF_8);
builder = ProgramBuilder.begin("centerDepthSmooth", vsh, null, fsh, ImmutableSet.of(0, 1, 2));
} catch (IOException e) {
throw new RuntimeException(e);
}
String vsh = ShaderLoader.getShaderSource("angelica:centerDepth.vsh");
String fsh = ShaderLoader.getShaderSource("angelica:centerDepth.fsh");
ProgramBuilder builder = ProgramBuilder.begin("centerDepthSmooth", vsh, null, fsh, ImmutableSet.of(0, 1, 2));
builder.addDynamicSampler(depthSupplier, "depth");
builder.addDynamicSampler(() -> altTexture, "altDepth");
@@ -83,8 +83,6 @@ sodium.options.always_defer_chunk_updates.name=Chunk Updates immer herauszögern
sodium.options.always_defer_chunk_updates.tooltip=Wenn angeschaltet, wartet das Rendering nie, bis Chunkupdates vollzogen wurden, selbst wenn sie wichtig sind. Das kann in manchen Szenarios die Bildrate stark verbessern, aber signfikanten Grafiklag in der Welt erzeugen.
sodium.options.use_no_error_context.name=Kein Fehlerkontext
sodium.options.use_no_error_context.tooltip=Aktiviert den OpenGL-Kontext ohne Fehlerüberprüfung. Das kann die Performance leicht verbessern, erhöht aber auch das Crashrisiko bei OpenGL-Fehlern. Schalt es aus, wenn du unerwartete Crashes erlebst.
sodium.options.use_gl_state_cache.name=GL-Zustandscache
sodium.options.use_gl_state_cache.tooltip=Wenn angeschaltet, wird der OpenGL-Zustandscache verwendet um die Anzahl an redundanten Operationen zu senken. Das kann die Performance erhöhen, aber auch Probleme mit einigen Mods auslösen. Diese Option bleibt nicht über Spielstarts hinweg erhalten.
sodium.options.threads.value=%s Thread(s)
sodium.options.default=Standard
sodium.options.buttons.undo=Rückgängig
@@ -143,9 +141,6 @@ options.angelica.mobSpawnerRenderDistance.tooltip=Renderabstand für das rotiere
options.angelica.itemdisplaylistcount=Item Renderer Display List Cache Size
options.angelica.itemdisplaylistcount=Item-Renderer-Anzeigecache-Größe
options.angelica.itemdisplaylistcount.tooltip=Größte Anzahl an Anzeigelisten, die im Itemrenderer gecacht werden. Höhere Werte erhöhen Performance zukosten von VRAM.
options.angelica.sleepbeforeswap=Alternate FPS Limiter
options.angelica.sleepbeforeswap=Alternativer FPS-Begrenzer
options.angelica.sleepbeforeswap.tooltip=Aktiviert einen alternativen FPS-Begrenzer für stabilere Framezeiten, was zu geringfügig höherer Latenz führen kann. Wird nie mehr als einen Frame Latenz erzeugen, und hat bei hoher Framerate weniger Wirkung.
pack.iris.select.title=Auswählen
pack.iris.configure.title=Konfigurieren
label.iris.true=An
@@ -14,7 +14,6 @@ sodium.options.pages.quality=Quality
sodium.options.pages.performance=Performance
sodium.options.pages.advanced=Advanced
sodium.options.pages.appearance=Appearance
sodium.options.pages.debug=Debug
sodium.options.view_distance.tooltip=The render distance controls how far away terrain will be rendered. Shorter distances mean that less terrain will be rendered, improving frame rates.
sodium.options.simulation_distance.tooltip=The simulation distance controls how far away terrain and entities will be loaded and ticked. Shorter distances can reduce the internal server's load and may improve frame rates.
sodium.options.brightness.tooltip=Controls the brightness (gamma) of the game.
@@ -86,8 +85,6 @@ sodium.options.always_defer_chunk_updates.name=Always Defer Chunk Updates
sodium.options.always_defer_chunk_updates.tooltip=If enabled, rendering will never wait for chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag in the world.
sodium.options.use_no_error_context.name=No Error Context
sodium.options.use_no_error_context.tooltip=If enabled, the OpenGL context will be created with error checking disabled. This may slightly improve performance, but it also increases the risk that the game will crash instead of gracefully handling OpenGL errors. You should disable this option if you are experiencing sudden unexplained crashes.
sodium.options.use_gl_state_cache.name=GL State Cache
sodium.options.use_gl_state_cache.tooltip=If enabled, the OpenGL state cache will be used to reduce the number of redundant state changes. This can improve performance, but it may also cause issues with some mods. Does not persist across game restarts.
sodium.options.async_occlusion_mode.name=Async Occlusion
sodium.options.async_occlusion_mode.tooltip=Controls which render passes use asynchronous occlusion culling. ONLY_SHADOW (default) runs async occlusion only for the shadow pass, keeping the main render list synchronous. EVERYTHING runs all passes async, which may cause 1-frame visibility lag during fast camera movement. NONE disables async occlusion entirely.
sodium.options.use_occlusion_culling.name=Occlusion Culling
@@ -190,6 +187,7 @@ options.angelica.fontconfig.aamode.aa_none=Font AA: OFF
options.angelica.fontconfig.aamode.aa_4x=Font AA: 4x MSAA
options.angelica.fontconfig.aamode.aa_16x=Font AA: 16x MSAA
options.angelica.fontconfig.custom_font_scale=Custom Font Scale: %s
options.angelica.fontconfig.shadow_offset_unicode=Unicode Shadow Offset: %s
options.angelica.fontconfig.font_quality.tooltip=The quality at which custom fonts are rendered, making them less pixelated at the expense of some performance and memory.
options.angelica.fontconfig.shadow_offset.tooltip=The distance at which the fonts' shadows are drawn. Also affects the way §lbold§r text is drawn.
options.angelica.fontconfig.shadow_copies.tooltip=The number of shadows to be drawn behind text, all offset so as to provide a fake 3D effect.
@@ -201,6 +199,7 @@ options.angelica.fontconfig.glyph_spacing.tooltip=The spacing between each indiv
options.angelica.fontconfig.font_aa_strength.tooltip=The spacing of samples used in font antialiasing. High values may cause fonts to look blurry, low values may cause aliasing. See what works best with your settings.
options.angelica.fontconfig.aamode.tooltip=The font antialiasing mode. Can significantly improve the appearance and readability of text that's being scaled or viewed at oblique angles, at the expense of some performance.
options.angelica.fontconfig.custom_font_scale.tooltip=A multiplier that affects the scale of custom fonts. Adjust this if text in your font of choice looks under-/over-sized relative to text that does not use custom fonts.
options.angelica.fontconfig.shadow_offset_unicode.tooltip=A shadow offset multiplier applied only to the Unicode font. Set this to 1.0x if, for example, you use a texture pack that replaces portions of the Unicode font with MC lookalikes.
options.angelica.hudCaching=HUD Caching
options.angelica.hudCaching.tooltip=Caches the HUD to a framebuffer and reuses it across frames to reduce rendering overhead. Requires HUD Caching to be enabled at startup. [Experimental]
options.angelica.droppedItemLimit=Dropped Item Render Limit
@@ -209,8 +208,6 @@ options.angelica.mobSpawnerRenderDistance=Mob spawner render distance
options.angelica.mobSpawnerRenderDistance.tooltip=Render distance for the spinning mob inside mod spawners
options.angelica.itemdisplaylistcount=Item Renderer Cache Limit
options.angelica.itemdisplaylistcount.tooltip=The upper limit of VBO's to cache in the optimized item renderer. Higher values will significantly increase performance, but may result in increased VRAM usage.
options.angelica.sleepbeforeswap=Alternate FPS Limiter
options.angelica.sleepbeforeswap.tooltip=Switches to an alternate FPS limiter that gives more stable frametimes, in exchange for slightly more latency. Will never introduce more than one frame of latency, and has a lower impact at higher framerates.
options.angelica.threadedChunkBuilding=Threaded Chunk Building
options.angelica.threadedChunkBuilding.tooltip=If enabled, chunk meshing will be performed on multiple threads in parallel, significantly improving chunk build speed. Disable if you experience rendering issues with mods that have custom block renderers. Requires renderer reload to take effect.
@@ -232,7 +229,10 @@ 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.upload_method.name=Streaming Upload Method
sodium.options.upload_method.tooltip=Changes the way the data gets streamed to the GPU. Some options may lead to better performance for some drivers.
sodium.options.upload_method.buffer_data=BufferData
sodium.options.upload_method.buffer_subdata=BufferSubData
sodium.options.upload_method.map_buffer=MapBufferRange
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.
@@ -79,8 +79,6 @@ sodium.options.always_defer_chunk_updates.name=Zawsze opóźniaj aktualizacje ch
sodium.options.always_defer_chunk_updates.tooltip=Jeśli włączone, renderowanie nigdy nie będzie czekać aż aktualizacje chunków się zakończą, nawet gdy są one ważne. To może znacznie poprawić wydajność klatek w niektórych przypadkach, ale może skutkować widocznym opóźnieniem na świecie.
sodium.options.use_no_error_context.name=Brak kontekstu błędu
sodium.options.use_no_error_context.tooltip=Jeśli włączone, kontekst OpenGL zostanie stworzony z wyłączonym sprawdzaniem błędów. To może minimalnie zwiększyć wydajność, ale również zwiększa ryzyko awarii gry zamiast radzenia sobie z błędami OpenGL. Powinieneś wyłączyć tą opcję, jeśli napotykasz nagłe i niewyjaśnione awarie.
sodium.options.use_gl_state_cache.name=Pamięć podręczna stanu GL
sodium.options.use_gl_state_cache.tooltip=Jeśli włączone, pamięć podręczna stanu OpenGL będzie użyta aby zmniejszyć ilość niepotrzebnych zmian stanu. To może zwiększyć wydajność, ale może też spowodować problemy z modami. Nie utrzymuje się po ponownym uruchomieniu gry.
sodium.options.threads.value=%s wątek(ów)
sodium.options.default=Domyślne
sodium.options.buttons.undo=Cofnij
@@ -81,8 +81,6 @@ sodium.options.always_defer_chunk_updates.name=Всегда Откладыват
sodium.options.always_defer_chunk_updates.tooltip=Если включено, отрисовка никогда не будет ждать завершения обновлений чанков, даже если они важны. Это может значительно улучшить частоту кадров в некоторых сценариях, но это может создать значительную визуальную задержку в мире.
sodium.options.use_no_error_context.name=Без Контекста Ошибок
sodium.options.use_no_error_context.tooltip=Если включено, контекст OpenGL будет создан с отключенной проверкой ошибок. Это может немного улучшить производительность, но также увеличивает риск того, что игра будет вылетать, а не грациозно обрабатывать ошибки OpenGL. Вы должны отключить эту опцию, если у вас возникают внезапные необъяснимые сбои.
sodium.options.use_gl_state_cache.name=Кэш Состояния GL
sodium.options.use_gl_state_cache.tooltip=Если включено, будет использоваться кэш состояния OpenGL, чтобы уменьшить количество избыточных изменений состояния. Это может улучшить производительность, но также может вызвать проблемы с некоторыми модами. Не сохраняется при перезапуске игры.
sodium.options.threads.value=%s поток(ов)
sodium.options.default=По умолчанию
sodium.options.buttons.undo=Отменить
@@ -84,8 +84,6 @@ sodium.options.always_defer_chunk_updates.tooltip=如果启用,渲染将永远
sodium.options.use_no_error_context.name=使用禁用错误检查的上下文
sodium.options.use_no_error_context.tooltip=如果启用,OpenGL日志将不包含详细错误信息。这可能略微提高性能,但也可能因为无法精确处理OpenGL错误而导致游戏崩溃。如遇到不明原因的突然崩溃,则应禁用此选项。
sodium.options.threads.value=%s 线程
sodium.options.use_gl_state_cache.name=启用GL状态缓存
sodium.options.use_gl_state_cache.tooltip=如果启用,OpenGL状态缓存将减少冗余状态更改的次数。这可以提高性能,但也可能导致一些模组出现问题。每次重启游戏时此项将重置。
sodium.options.default=默认
sodium.options.buttons.undo=撤销
sodium.options.buttons.apply=应用
@@ -166,8 +164,6 @@ options.angelica.mobSpawnerRenderDistance=刷怪笼渲染距离
options.angelica.mobSpawnerRenderDistance.tooltip=刷怪笼内旋转实体的渲染距离
options.angelica.itemdisplaylistcount=物品渲染器缓存限制
options.angelica.itemdisplaylistcount.tooltip=优化物品渲染器中的缓存数量上限。较高的值将显著提高性能,但可能导致显存占用增加
options.angelica.sleepbeforeswap=备用FPS限制器
options.angelica.sleepbeforeswap.tooltip=切换到备用FPS限制器,可以降低帧波动,但会略微增加延迟。最多只会造成一帧的延迟,且在较高帧率下影响较小。
pack.iris.select.title=选择
pack.iris.configure.title=配置
label.iris.true=开启
@@ -1,8 +1,8 @@
#version 330 core
layout(location = 0) in vec2 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord0;
layout(location = 1) in vec2 a_TexCoord0;
layout(location = 2) in vec4 a_Color;
layout(location = 3) in vec4 a_TexBounds;
uniform mat4 u_MVPMatrix;
@@ -0,0 +1,15 @@
#version 330 core
in vec2 v_TexCoord;
uniform sampler2D u_Texture;
uniform float u_ScaleV;
uniform float u_ScaleU;
out vec4 fragColor;
void main() {
float u = 0.5 + u_ScaleV * (2.0 * v_TexCoord.y - 1.0);
float v = 0.5 + u_ScaleU * (1.0 - 2.0 * v_TexCoord.x);
fragColor = texture(u_Texture, vec2(u, v));
}
@@ -0,0 +1,24 @@
#version 330 core
in vec2 v_TexCoord;
uniform sampler2D u_Texture;
uniform vec2 u_Direction;
uniform float u_Radius;
out vec4 fragColor;
void main() {
float sigma = max(u_Radius * 0.5, 1.0);
float invTwoSigmaSq = 1.0 / (2.0 * sigma * sigma);
vec3 sum = vec3(0.0);
float weightSum = 0.0;
for (float i = -u_Radius; i <= u_Radius; i += 1.0) {
float w = exp(-i * i * invTwoSigmaSq);
sum += texture(u_Texture, v_TexCoord + u_Direction * i).rgb * w;
weightSum += w;
}
fragColor = vec4(sum / weightSum, 1.0);
}
@@ -0,0 +1,8 @@
#version 330 core
out vec2 v_TexCoord;
void main() {
v_TexCoord = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
gl_Position = vec4(v_TexCoord * 2.0 - 1.0, 0.0, 1.0);
}
@@ -0,0 +1,13 @@
#version 330 core
in vec2 v_TexCoord;
in float v_Alpha;
uniform sampler2D u_Texture;
out vec4 fragColor;
void main() {
vec4 color = texture(u_Texture, v_TexCoord);
fragColor = vec4(color.rgb, v_Alpha);
}
@@ -0,0 +1,16 @@
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec2 a_TexCoord;
layout(location = 2) in float a_Alpha;
uniform mat4 u_MVP;
out vec2 v_TexCoord;
out float v_Alpha;
void main() {
gl_Position = u_MVP * vec4(a_Position, 1.0);
v_TexCoord = a_TexCoord;
v_Alpha = a_Alpha;
}
@@ -0,0 +1,8 @@
#version 330 core
uniform vec4 u_Color;
out vec4 fragColor;
void main() {
fragColor = u_Color;
}
@@ -0,0 +1,28 @@
#version 330 core
layout(lines) in;
layout(triangle_strip, max_vertices = 4) out;
uniform vec2 u_ViewportSize;
uniform float u_LineWidth;
void main() {
vec4 c0 = gl_in[0].gl_Position;
vec4 c1 = gl_in[1].gl_Position;
vec2 n0 = c0.xy / c0.w;
vec2 n1 = c1.xy / c1.w;
vec2 dir = normalize((n1 - n0) * u_ViewportSize);
vec2 offset = vec2(-dir.y, dir.x) * u_LineWidth / u_ViewportSize;
gl_Position = vec4((n0 + offset) * c0.w, c0.z, c0.w);
EmitVertex();
gl_Position = vec4((n0 - offset) * c0.w, c0.z, c0.w);
EmitVertex();
gl_Position = vec4((n1 + offset) * c1.w, c1.z, c1.w);
EmitVertex();
gl_Position = vec4((n1 - offset) * c1.w, c1.z, c1.w);
EmitVertex();
EndPrimitive();
}
@@ -0,0 +1,19 @@
#version 330 core
uniform mat4 u_MVP;
uniform vec3 u_Min;
uniform vec3 u_Max;
const vec3 edges[24] = vec3[24](
vec3(0,0,0), vec3(1,0,0), vec3(1,0,0), vec3(1,0,1),
vec3(1,0,1), vec3(0,0,1), vec3(0,0,1), vec3(0,0,0),
vec3(0,1,0), vec3(1,1,0), vec3(1,1,0), vec3(1,1,1),
vec3(1,1,1), vec3(0,1,1), vec3(0,1,1), vec3(0,1,0),
vec3(0,0,0), vec3(0,1,0), vec3(1,0,0), vec3(1,1,0),
vec3(1,0,1), vec3(1,1,1), vec3(0,0,1), vec3(0,1,1)
);
void main() {
vec3 pos = mix(u_Min, u_Max, edges[gl_VertexID]);
gl_Position = u_MVP * vec4(pos, 1.0);
}
@@ -20,12 +20,12 @@ options.downfall_distance=Weather Effects
options.downfall_distance.tooltip=Weather Effects
options.dynamic_fov=Dynamic FOV
options.dynamic_fov.tooltip=If enabled, the game's field of view will change based on the player's speed.
options.horizon_disable=Disable Horizon
options.horizon_disable.tooltip=Disable Horizon
options.fog_disable=Disable Terrain Fog
options.fog_disable.tooltip=Disable Terrain Fog
options.horizon=Horizon
options.horizon.tooltip=Horizon
options.terrain_fog=Terrain Fog
options.terrain_fog.tooltip=Terrain Fog (aka View Distance Fog)
options.fog_near_distance=Near Fog Distance
options.fog_near_distance.tooltip=Near Fog Distance
options.fog_near_distance.tooltip=Distance away from the player when the view distance fog will start (0%% opacity then linear increase to render distance)
options.mode_cloud_translucency=Cloud Translucency
options.mode_cloud_translucency.tooltip=Cloud Translucency
options.mode_clouds=Clouds
@@ -7,5 +7,8 @@
"injectors":{
"maxShiftBy": 2
},
"compatibilityLevel": "JAVA_17"
"compatibilityLevel": "JAVA_17",
"overwrites": {
"conformVisibility": true
}
}
@@ -7,6 +7,9 @@
"injectors":{
"maxShiftBy": 2
},
"compatibilityLevel": "JAVA_21"
"compatibilityLevel": "JAVA_21",
"overwrites": {
"conformVisibility": true
}
}
@@ -7,6 +7,9 @@
"injectors":{
"maxShiftBy": 2
},
"compatibilityLevel": "JAVA_8"
"compatibilityLevel": "JAVA_8",
"overwrites": {
"conformVisibility": true
}
}
+4 -1
View File
@@ -3,6 +3,9 @@
"minVersion": "0.8.3-GTNH",
"refmap": "mixins.angelica.refmap.json",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_8"
"compatibilityLevel": "JAVA_8",
"overwrites": {
"conformVisibility": true
}
}
@@ -4,5 +4,8 @@
"package": "com.gtnewhorizons.angelica.mixins.late",
"refmap": "mixins.angelica.refmap.json",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_17"
"compatibilityLevel": "JAVA_17",
"overwrites": {
"conformVisibility": true
}
}
@@ -4,6 +4,9 @@
"package": "com.gtnewhorizons.angelica.mixins.late",
"refmap": "mixins.angelica.refmap.json",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_21"
"compatibilityLevel": "JAVA_21",
"overwrites": {
"conformVisibility": true
}
}
+4 -1
View File
@@ -4,6 +4,9 @@
"package": "com.gtnewhorizons.angelica.mixins.late",
"refmap": "mixins.angelica.refmap.json",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_8"
"compatibilityLevel": "JAVA_8",
"overwrites": {
"conformVisibility": true
}
}
@@ -1,6 +1,10 @@
package com.gtnewhorizons.angelica.mixins.early.angelica;
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import net.minecraftforge.client.ForgeHooksClient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
@@ -18,11 +22,13 @@ import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
/**
* Requests highest GL version with core profile context
* Requests highest GL version with core profile context, with optional version pinning.
*/
@Mixin(value = ForgeHooksClient.class, remap = false)
public abstract class MixinForgeHooksClient_CoreProfile {
@Unique private static final Logger LOGGER = LogManager.getLogger("Angelica");
@Shadow static int stencilBits;
/**
@@ -38,7 +44,7 @@ public abstract class MixinForgeHooksClient_CoreProfile {
final PixelFormat format = new PixelFormat().withDepthBits(24).withStencilBits(stencilBits);
final ContextAttribs attribs = new ContextAttribs(4, 5).withProfileCore(true).withForwardCompatible(true);
final ContextAttribs attribs = new ContextAttribs(3, 3).withProfileCore(true).withForwardCompatible(true);
final MethodHandle setMajor, setMinor;
try {
final Field majorField = ContextAttribs.class.getDeclaredField("majorVersion");
@@ -52,42 +58,91 @@ 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;
final int platformMaxMajor = 4;
final int platformMaxMinor = (LWJGLUtil.getPlatform() == LWJGLUtil.PLATFORM_MACOSX) ? 1 : 6;
final int platformMax = platformMaxMajor * 10 + platformMaxMinor;
Exception cur_exception = null;
for (int major = startMajor; major >= 3; --major) {
final int maxMinor = (major == 4) ? startMaxMinor : 3;
// Try pinned version first if configured
final int pinned = angelica$clampPinned(AngelicaConfig.pinnedGLVersion, platformMax);
if (pinned >= 33) {
final Exception e = angelica$tryCreate(attribs, format, setMajor, setMinor, pinned / 10, pinned % 10);
if (e == null) {
LOGGER.info("Created GL {}.{} core profile context (pinned)", pinned / 10, pinned % 10);
return;
}
LOGGER.warn("Pinned GL version {}.{} failed, probing from highest", pinned / 10, pinned % 10);
}
// Probe from highest to lowest
Exception lastException = null;
for (int major = platformMaxMajor; major >= 3; --major) {
final int maxMinor = (major == 4) ? platformMaxMinor : 3;
final int minMinor = (major == 3) ? 3 : 0;
for (int minor = maxMinor; minor >= minMinor; --minor) {
try {
setMajor.invokeExact(attribs, major);
setMinor.invokeExact(attribs, minor);
} catch (Throwable t) {
throw new RuntimeException("Failed to set ContextAttribs version", t);
}
try {
Display.create(format, attribs);
final Exception e = angelica$tryCreate(attribs, format, setMajor, setMinor, major, minor);
if (e == null) {
final int version = major * 10 + minor;
LOGGER.info("Created GL {}.{} core profile context", major, minor);
// Auto-pin if below platform max, or update a stale pin
if (version < platformMax && AngelicaConfig.pinnedGLVersion != version && (AngelicaConfig.pinnedGLVersion != 0 || !AngelicaConfig.disableGLVersionPinning)) {
angelica$savePin(version);
}
return;
} catch (LWJGLException | RuntimeException e) {
try { Display.destroy(); } catch (Exception ignored) {}
cur_exception = e;
}
lastException = e;
}
}
angelica$reportContextFailure(cur_exception);
if (cur_exception instanceof LWJGLException lwjgl) {
angelica$reportContextFailure(lastException);
if (lastException instanceof LWJGLException lwjgl) {
throw lwjgl;
}
throw new LWJGLException("Failed to create OpenGL core profile context", cur_exception);
throw new LWJGLException("Failed to create OpenGL core profile context", lastException);
}
@Unique
private static int angelica$clampPinned(int pinned, int platformMax) {
if (pinned == 0) return 0;
if (pinned >= 33 && pinned <= platformMax) return pinned;
final int clamped = Math.max(33, Math.min(pinned, platformMax));
LOGGER.warn("pinnedGLVersion={} is out of range (33-{}), clamping to {}", pinned, platformMax, clamped);
return clamped;
}
@Unique
private static Exception angelica$tryCreate(ContextAttribs attribs, PixelFormat format, MethodHandle setMajor, MethodHandle setMinor, int major, int minor) throws RuntimeException {
try {
setMajor.invokeExact(attribs, major);
setMinor.invokeExact(attribs, minor);
} catch (Throwable t) {
throw new RuntimeException("Failed to set ContextAttribs version", t);
}
try {
Display.create(format, attribs);
return null;
} catch (LWJGLException | RuntimeException e) {
try { Display.destroy(); } catch (Exception ignored) {}
return e;
}
}
@Unique
private static void angelica$savePin(int version) {
AngelicaConfig.pinnedGLVersion = version;
try {
ConfigurationManager.save(AngelicaConfig.class);
LOGGER.info("Pinned OpenGL version to {}.{} — set pinnedGLVersion=0 to re-probe, or disableGLVersionPinning=true to never pin", version / 10, version % 10);
} catch (Exception e) {
LOGGER.warn("Failed to save pinned GL version: {}", e.getMessage());
}
}
@Unique
private static void angelica$reportContextFailure(Exception e) {
System.err.println("[Angelica] FATAL: Failed to create OpenGL core profile context.");
System.err.println("[Angelica] Error: " + e.getMessage());
LOGGER.error("FATAL: Failed to create OpenGL core profile context.");
LOGGER.error("Error: {}", e != null ? e.getMessage() : "unknown");
try {
System.err.println("[Angelica] GPU: " + GL11.glGetString(GL11.GL_RENDERER) + ", Driver: " + GL11.glGetString(GL11.GL_VERSION));
LOGGER.error("GPU: {}, Driver: {}", GL11.glGetString(GL11.GL_RENDERER), GL11.glGetString(GL11.GL_VERSION));
} catch (Exception ignored) {}
}
}
@@ -1,11 +1,9 @@
package com.gtnewhorizons.angelica.mixins.early.angelica;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.ffp.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.GameSettings;
import org.embeddedt.embeddium.impl.render.frame.RenderAheadManager;
@@ -34,7 +32,10 @@ public abstract class MixinMinecraft {
private static int max_texture_size;
@Unique
private long angelica$lastFrameTime = 0;
private static long angelica$lastFrameTime = 0;
@Unique
private static long angelica$fpsLimitOverhead = 0;
@Unique
private final RenderAheadManager celeritas$renderAheadManager = new RenderAheadManager();
@@ -62,34 +63,41 @@ public abstract class MixinMinecraft {
@Inject(
method = "func_147120_f",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;update()V", shift = At.Shift.BEFORE, remap = false)
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;update()V", remap = false)
)
private void angelica$limitFPS(CallbackInfo ci) {
if (!AngelicaConfig.sleepBeforeSwap) return;
if (isFramerateLimitBelowMax()) {
final long target = angelica$lastFrameTime + (long) (1.0 / getLimitFramerate() * 1_000_000) * 1_000;
while (target - System.nanoTime() > 100) {
Thread.yield();
}
}
}
@Inject(
method = "func_147120_f",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;update()V", shift = At.Shift.AFTER, remap = false)
)
private void angelica$trackFrametimes(CallbackInfo ci) {
if (AngelicaMod.proxy == null) return;
if (isFramerateLimitBelowMax() && !gameSettings.enableVsync) {
final long time = System.nanoTime();
final long lastWorkTime = time - angelica$lastFrameTime;
final long targetNanos = (long) (1.0 / getLimitFramerate() * 1_000_000_000L);
// Account for overhead, so the average FPS remains stable.
final long sleepNanos = targetNanos - lastWorkTime - angelica$fpsLimitOverhead;
if (sleepNanos > 0) {
try {
Thread.sleep(sleepNanos / 1_000_000, (int) sleepNanos % 1_000_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// Record overhead, capping it to prevent outsized spikes from affecting framerates for too long.
// In testing, spikes were followed by 1-frame dips, which I think is acceptable.
long overhead = System.nanoTime() - time - sleepNanos;
if (overhead < 0 || overhead > targetNanos / 2) overhead = 0;
angelica$fpsLimitOverhead = overhead;
}
final long time = System.nanoTime();
AngelicaMod.proxy.putFrametime(time - angelica$lastFrameTime);
angelica$lastFrameTime = time;
}
@WrapOperation(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;sync(I)V", remap = false))
private void angelica$noopFPSLimiter(int fps, Operation<Void> original) {
if (AngelicaConfig.sleepBeforeSwap) return;
original.call(fps);
@WrapWithCondition(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;sync(I)V", remap = false))
private boolean angelica$noopFPSLimiter(int fps) {
return false;
}
@Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiScreen;isShiftKeyDown()Z", shift = At.Shift.AFTER))
@@ -16,7 +16,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
public class MixinTextureManager {
@Inject(method = "loadTexture", at = @At("RETURN"))
private void onTextureLoad(ResourceLocation resource, ITextureObject texture, CallbackInfoReturnable<Boolean> cir) {
if(texture != null && texture != TextureUtil.missingTexture) {
if(texture != null && texture != TextureUtil.missingTexture && !GLStateManager.isRecordingDisplayList()) {
final int curId = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture.getGlTextureId());
GLDebug.nameObject(GL11.GL_TEXTURE, texture.getGlTextureId(), resource.toString());
@@ -50,9 +50,9 @@ public abstract class MixinEntity implements IDynamicLightSource {
@Unique
private double angelica$prevZ;
@Unique
private LongOpenHashSet angelica$trackedLitChunkPos = new LongOpenHashSet();
private LongOpenHashSet angelica$trackedLitChunkPos = null;
@Unique
private LongOpenHashSet angelica$prevTrackedLitChunkPos = new LongOpenHashSet();
private LongOpenHashSet angelica$prevTrackedLitChunkPos = null;
@Override
@@ -117,6 +117,10 @@ public abstract class MixinEntity implements IDynamicLightSource {
this.angelica$prevZ = this.posZ;
this.angelica$lastLuminance = luminance;
if (this.angelica$prevTrackedLitChunkPos == null) {
this.angelica$prevTrackedLitChunkPos = new LongOpenHashSet();
this.angelica$trackedLitChunkPos = new LongOpenHashSet();
}
final LongOpenHashSet newPos = this.angelica$prevTrackedLitChunkPos;
newPos.clear();
@@ -161,7 +165,7 @@ public abstract class MixinEntity implements IDynamicLightSource {
@Override
public void angelica$scheduleTrackedChunksRebuild(@NotNull IDynamicLightWorldRenderer renderer) {
if (this.angelica$trackedLitChunkPos.isEmpty()) {
if (this.angelica$trackedLitChunkPos == null || this.angelica$trackedLitChunkPos.isEmpty()) {
angelica$scheduleRebuildAroundCurrentPosition(renderer);
} else {
var iter = this.angelica$trackedLitChunkPos.iterator();
@@ -0,0 +1,26 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.gui;
import com.gtnewhorizons.angelica.render.PanoramaRenderer;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.util.ResourceLocation;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(GuiMainMenu.class)
public abstract class MixinGuiMainMenu extends GuiScreen {
@Shadow private int panoramaTimer;
@Final @Shadow private static ResourceLocation[] titlePanoramaPaths;
/**
* @author Angelica
* @reason Replace panorama with modern equivalent
*/
@Overwrite
public void renderSkybox(int mouseX, int mouseY, float partialTicks) {
PanoramaRenderer.getInstance().renderSkybox(this.panoramaTimer, partialTicks, titlePanoramaPaths, this.mc, this.width, this.height, this.zLevel);
}
}
@@ -0,0 +1,22 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.gui;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiOptions;
import net.minecraft.client.gui.GuiScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(GuiOptions.class)
public abstract class MixinGuiOptions extends GuiScreen {
@Unique
private static final int SUPER_SECRET_SETTINGS_BUTTON_ID = 8675309;
@Inject(method = "initGui", at = @At("RETURN"))
private void angelica$disableSuperSecretSettings(CallbackInfo ci) {
this.buttonList.removeIf(b -> ((GuiButton) b).id == SUPER_SECRET_SETTINGS_BUTTON_ID);
}
}
@@ -0,0 +1,20 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.rendering;
import com.gtnewhorizons.angelica.render.SelectionBoxRenderer;
import net.minecraft.client.renderer.RenderGlobal;
import net.minecraft.util.AxisAlignedBB;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
@Mixin(RenderGlobal.class)
public class MixinRenderGlobal_SelectionBox {
/**
* @author Angelica
* @reason Optimize selection box rendering
*/
@Overwrite
public static void drawOutlinedBoundingBox(AxisAlignedBB aabb, int color) {
SelectionBoxRenderer.draw(aabb, color);
}
}
@@ -0,0 +1,21 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.vbo;
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.IModelCustomExt;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraftforge.client.model.obj.WavefrontObject;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
@Mixin(value = WavefrontObject.class, remap = false)
public abstract class MixinWavefrontObject {
/**
* @author mitchej123
* @reason Force all models to use VBOs
*/
@SideOnly(Side.CLIENT)
@Overwrite
public void renderAll() {
((IModelCustomExt) (Object)this).renderAllVBO();
}
}
@@ -32,14 +32,14 @@ public abstract class MixinRenderGlobal {
return (boolean)Settings.MODE_SKY.option.getStore();
}
}
@WrapWithCondition(
method = "renderSky(F)V",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", ordinal = 3, remap = false))
private boolean conditionalDisableHorizon(int i) {
return !(boolean)Settings.HORIZON_DISABLE.option.getStore();
private boolean conditionalRenderHorizon(int i) {
return (boolean)Settings.HORIZON.option.getStore();
}
@WrapWithCondition(
method = "renderSky(F)V",
at = @At(value = "INVOKE", target = "Lnet/minecraftforge/client/IRenderHandler;render(FLnet/minecraft/client/multiplayer/WorldClient;Lnet/minecraft/client/Minecraft;)V", remap = false))
@@ -56,7 +56,7 @@ abstract public class MixinEntityRenderer {
)
private float notFine$nearFogDistance(float value) {
// Extremely high values cause issues, but 15 mebimeters out should be practically infinite
if ((Boolean) Settings.FOG_DISABLE.option.getStore()) return 1024 * 1024 * 15;
if (!(Boolean) Settings.TERRAIN_FOG.option.getStore()) return 1024 * 1024 * 15;
return farPlaneDistance * (int) Settings.FOG_NEAR_DISTANCE.option.getStore() * 0.01F - 1F;
}
@@ -70,7 +70,7 @@ abstract public class MixinEntityRenderer {
)
)
private float notFine$replaceFarFogDistance(float value) {
if ((Boolean) Settings.FOG_DISABLE.option.getStore()) return 1024 * 1024 * 16;
if (!(Boolean) Settings.TERRAIN_FOG.option.getStore()) return 1024 * 1024 * 16;
return value;
}
@@ -0,0 +1,31 @@
package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.coderbot.iris.uniforms.EntityIdHelper;
import net.coderbot.iris.uniforms.ItemIdManager;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Dynamic;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(RenderManager.class)
public class MixinRenderManagerDAPI {
@Dynamic
@WrapOperation(
method = "func_147939_a(Lnet/minecraft/entity/Entity;DDDFFZ)Z",
at = @At(remap = false, value = "INVOKE", target = "LReika/DragonAPI/Instantiable/Event/Client/EntityRenderEvent;fire(Lnet/minecraft/client/renderer/entity/Render;Lnet/minecraft/entity/Entity;DDDFF)V"),
require = 1 // Require this if DAPI is present, which should be the case when this mixin is applied.
)
private void iris$wrapDoRenderDragonAPI(Render render, Entity entity, double x, double y, double z, float entityYaw, float partialTicks, Operation<Void> original) {
int entityId = EntityIdHelper.getEntityId(entity);
CapturedRenderingState.INSTANCE.setCurrentEntity(entityId);
ItemIdManager.resetItemId();
original.call(render, entity, x, y, z, entityYaw, partialTicks);
CapturedRenderingState.INSTANCE.setCurrentEntity(-1);
}
}
@@ -17,6 +17,10 @@ public class MixinTileEntityRendererDispatcher {
@Inject(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V", at = @At("HEAD"))
private void iris$setBlockEntityId(TileEntity te, double x, double y, double z, float partialTicks, CallbackInfo ci) {
if (te == null) {
CapturedRenderingState.INSTANCE.setCurrentBlockEntity(0);
return;
}
Block block = te.getBlockType();
if (block == null) {
CapturedRenderingState.INSTANCE.setCurrentBlockEntity(0);
@@ -13,17 +13,17 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = SkyProviderCelestial.class, priority = 100)
public class MixinSkyProviderCelestial_Tweaks {
@WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", ordinal = 2), remap = false)
private boolean angelica$disableHorizon(int i) {
return !(boolean) Settings.HORIZON_DISABLE.option.getStore();
private boolean angelica$renderHorizon(int i) {
return (boolean) Settings.HORIZON.option.getStore();
}
@Inject(method = "render", at = @At(value="INVOKE", target="Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 1))
private void angelica$disableAltitudePlanetRenderer(CallbackInfo ci) {
if(CompatConfig.NTMSpace_disableAltitudePlanetRenderer == CompatConfig.altitudePlanetRendererState.Always || (IrisApi.getInstance().isShaderPackInUse() && CompatConfig.NTMSpace_disableAltitudePlanetRenderer == CompatConfig.altitudePlanetRendererState.ShadersOnly)) {
Tessellator.instance.vertexCount = 0;
}
}
}
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica;
import com.gtnewhorizons.angelica.glsm.GLDebug;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
@@ -45,6 +46,7 @@ public class AngelicaExtension implements BeforeAllCallback, BeforeEachCallback,
setMainThread(Thread.currentThread());
GLStateManager.preInit();
VertexAttribState.init(0);
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete();
GLStateManager.BYPASS_CACHE = false;
@@ -63,13 +65,13 @@ public class AngelicaExtension implements BeforeAllCallback, BeforeEachCallback,
private static void setMainThread(Thread thread) {
try {
// Use Unsafe to set final static field - works on all Java versions
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
Field mainThreadField = GLStateManager.class.getDeclaredField("MainThread");
Object base = unsafe.staticFieldBase(mainThreadField);
long offset = unsafe.staticFieldOffset(mainThreadField);
final Field mainThreadField = GLStateManager.class.getDeclaredField("MainThread");
final Object base = unsafe.staticFieldBase(mainThreadField);
final long offset = unsafe.staticFieldOffset(mainThreadField);
unsafe.putObject(base, offset, thread);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Failed to set MainThread for tests", e);
@@ -104,19 +106,19 @@ public class AngelicaExtension implements BeforeAllCallback, BeforeEachCallback,
@Override
public void afterEach(ExtensionContext context) throws Exception {
int error = GL11.glGetError();
final int error = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, error,
() -> "GL Error: 0x" + Integer.toHexString(error));
int modelviewDepth = GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH);
int projectionDepth = GL11.glGetInteger(GL11.GL_PROJECTION_STACK_DEPTH);
final int modelviewDepth = GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH);
final int projectionDepth = GL11.glGetInteger(GL11.GL_PROJECTION_STACK_DEPTH);
assertEquals(EXPECTED_MODELVIEW_STACK_DEPTH, modelviewDepth,
"MODELVIEW stack depth mismatch - unbalanced glPushMatrix/glPopMatrix");
assertEquals(EXPECTED_PROJECTION_STACK_DEPTH, projectionDepth,
"PROJECTION stack depth mismatch - unbalanced glPushMatrix/glPopMatrix");
int matrixMode = GL11.glGetInteger(GL11.GL_MATRIX_MODE);
final int matrixMode = GL11.glGetInteger(GL11.GL_MATRIX_MODE);
assertEquals(GL11.GL_MODELVIEW, matrixMode,
() -> "Matrix mode not reset to MODELVIEW, was: " + GLDebug.getMatrixModeName(matrixMode));
}
@@ -529,6 +529,41 @@ class CompatShaderTransformerTest {
assertTrue(result.contains("angelica_SceneColor"), "should reference angelica_SceneColor");
}
@Test
void testVersionPatternDoesNotEatNextLine() {
String src = "#version 330\nsomeword\n";
String result = CompatShaderTransformer.transform(src, false);
assertTrue(result.contains("someword"), "Next-line word must not be eaten by version fixup");
assertTrue(result.contains("#version 330 core"), "Version should be upgraded to core");
}
@Test
void testModernShaderWithoutCoreKeyword() {
String src = """
#version 430
in vec3 Position;
uniform mat4 ModelViewMat;
uniform mat4 ProjMat;
out vec4 vertexColor;
out float fogDistance;
void main() {
vec4 finalPos = vec4(Position, 1.0);
vec4 modelPos = ModelViewMat * finalPos;
gl_Position = ProjMat * modelPos;
fogDistance = length(modelPos.xz);
vertexColor = vec4(1.0);
}
""";
String result = CompatShaderTransformer.transform(src, false);
assertTrue(result.contains("#version 430 core"), "Version should get 'core' profile added");
assertTrue(result.contains("in vec3 Position"), "'in' keyword must not be eaten");
assertTrue(result.contains("ModelViewMat"), "Shader content must be preserved");
assertTrue(result.contains("ProjMat"), "Shader content must be preserved");
assertTrue(result.contains("fogDistance"), "Shader content must be preserved");
}
@Test
void testLegacyQualifierConversion() {
String vertSrc = """
@@ -8,7 +8,6 @@ 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;
@@ -60,17 +59,17 @@ class FeedbackManagerTest {
// Upload 3 vertices to a VBO and draw
final int vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
GLStateManager.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);
GLStateManager.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);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLStateManager.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_TRIANGLES, 0, 3);
@@ -78,10 +77,10 @@ class FeedbackManagerTest {
assertEquals(-1, result, "Overflow should return -1");
// Cleanup
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GL30.glDeleteVertexArrays(vao);
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
@@ -112,7 +111,7 @@ class FeedbackManagerTest {
// Upload 3 vertices forming a triangle
final int vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
GLStateManager.glBindVertexArray(vao);
// NDC vertices: (0,0,0), (1,0,0), (0,1,0)
// With identity MVP and viewport 800x600:
@@ -124,10 +123,10 @@ class FeedbackManagerTest {
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.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);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLStateManager.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_TRIANGLES, 0, 3);
@@ -152,10 +151,10 @@ class FeedbackManagerTest {
assertEquals(600.0f, buf.get(7), 0.01f);
// Cleanup
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GL30.glDeleteVertexArrays(vao);
GLStateManager.glDeleteVertexArrays(vao);
}
@Test
@@ -170,7 +169,7 @@ class FeedbackManagerTest {
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
final int vao = GL30.glGenVertexArrays();
GL30.glBindVertexArray(vao);
GLStateManager.glBindVertexArray(vao);
// Single point at NDC origin (0,0,0)
final float[] verts = { 0f, 0f, 0f };
@@ -178,10 +177,10 @@ class FeedbackManagerTest {
vertBuf.put(verts).flip();
final int vbo = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.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);
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLStateManager.glEnableVertexAttribArray(0);
FeedbackManager.processDrawArrays(GL11.GL_POINTS, 0, 1);
@@ -196,9 +195,9 @@ class FeedbackManagerTest {
assertEquals(0.5f, buf.get(3), 0.01f);
// Cleanup
GL20.glDisableVertexAttribArray(0);
GL30.glBindVertexArray(0);
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
GL30.glDeleteVertexArrays(vao);
GLStateManager.glDeleteVertexArrays(vao);
}
}
@@ -100,8 +100,8 @@ public class GLSM_ServerSideState_UnitTest {
backgroundError = t;
}
pendingCommand = null;
completeLatch.countDown();
}
completeLatch.countDown();
}
}
} catch (InterruptedException e) {
@@ -2,11 +2,11 @@ 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 com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader.UploadStrategy;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -20,25 +20,10 @@ 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);
try {
ByteBuffer data = memAlloc(128);
fillPattern(data);
data.flip();
assertEquals(0, buf.upload(data, 32), "First upload should return offset 0");
memFree(data);
} finally {
buf.destroy();
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_dataIntegrity(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
OrphanStreamingBuffer buf = new OrphanStreamingBuffer();
try {
ByteBuffer data = memAlloc(64);
ByteBuffer readBack = null;
@@ -46,7 +31,7 @@ class StreamingBufferTest {
for (int i = 0; i < 64; i++) data.put((byte) i);
data.flip();
buf.upload(data, 16);
buf.upload(strategy, data);
// Read back via glGetBufferSubData
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, buf.getBufferId());
@@ -66,171 +51,6 @@ class StreamingBufferTest {
}
}
@ParameterizedTest
@EnumSource(UploadStrategy.class)
void orphan_multipleUploadsSubAllocate(UploadStrategy strategy) {
OrphanStreamingBuffer buf = new OrphanStreamingBuffer(strategy);
try {
final int stride = 16;
ByteBuffer small = memAlloc(64);
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");
// 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 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);
}
} finally {
buf.destroy();
}
}
@Test
void persistent_uploadAlignment() {
Assumptions.assumeTrue(RenderSystem.supportsBufferStorage(), "GL4.4+ required");