Compare commits

...
10 Commits
Author SHA1 Message Date
Yoshy f43fbd4d9f Fix CTM to not spread to nearby TEs (#1695) 2026-04-25 13:09:38 -07:00
Jason Mitchell dbabc4b3dc Maybe fix DragonAPI on OSX - take two (#1689)
* Maybe fix DragonAPI on OSX - take two
* Bind a default VAO on the SharedDrawable
* Mixin that tries to swap out glGetProgramInfoLog when appropriate

* no direct gl calls
2026-04-21 15:12:02 -07:00
Jason Mitchell e01097264b Fix infinite extent AABB crash w/ kotlin, and ensure proper sizing/padding on VAO/VBO snapshots. (#1687) 2026-04-20 10:11:46 -07:00
Jason Mitchell 0a922f3129 AE2 Crash Fix (#1684)
* Snapshot all relevant data into display lists (Avoids referencing stale/recycled data and a crash on Nvidia)
* Route deletes through GLStateManager
2026-04-19 08:19:02 -07:00
Eclipse-Sol 4a2dcd0b22 Automate the collection of legacy IDs for very old shaderpacks. (#1631) 2026-04-18 09:53:06 -07:00
Eclipse-Sol 0db691db0b Add FlatteningMap and EntityFlatteningMap (#1635) 2026-04-18 09:20:24 -07:00
danyadev e5cd1372e7 always request 0.9.0+ GTNHLib version when a player has an outdated version (#1672) 2026-04-18 09:18:28 -07:00
Kotmatross fbd07cc575 NTM:Space compat rework and update (#1675) 2026-04-18 09:14:49 -07:00
George Paton 91f2cf9dfb Add "Mods Only" Dynamic FOV option (#1680) 2026-04-18 09:13:39 -07:00
Kaiz 34a6967c71 fix(iris): implement full GL alpha test (#1673) 2026-04-18 09:12:46 -07:00
67 changed files with 4505 additions and 889 deletions

No files matched your search

+1 -1
View File
@@ -121,7 +121,7 @@ dependencies {
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev")
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5634_H261"))
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5663_H261"))
compileOnly(rfg.deobf("curse.maven:campfirebackport-387444:4611675"))
compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684"))
+4
View File
@@ -146,6 +146,10 @@ tasks.test {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
if (System.getProperty("os.name").lowercase().contains("mac")
&& System.getProperty("os.arch") == "aarch64") {
vendor = JvmVendorSpec.AZUL
}
}
dependsOn(downgradeMainClasses, downgradeTestClasses, downgradeDepsForTest)
@@ -15,6 +15,9 @@ import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBO;
import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBOBuilder;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@@ -566,12 +569,16 @@ public class DisplayListManager {
currentRecorder.recordDrawBuffers(count, bufs);
}
// Complex commands are texture uploads - not draw barriers
public static void recordComplexCommand(DisplayListCommand cmd) {
if (currentRecorder == null) return;
currentRecorder.recordComplexCommand(cmd);
}
public static void recordIndexedDrawCapture(IndexedDrawCapture capture) {
if (currentRecorder == null) return;
currentRecorder.recordIndexedDrawCapture(capture);
}
public static void recordLoadMatrix(Matrix4f matrix) {
if (currentRecorder != null) currentRecorder.recordLoadMatrix(matrix);
}
@@ -580,14 +587,6 @@ public class DisplayListManager {
if (currentRecorder != null) currentRecorder.recordLoadIdentity();
}
public static void recordDrawArrays(int mode, int start, int count) {
if (currentRecorder != null) currentRecorder.recordDrawArrays(mode, start, count);
}
public static void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
if (currentRecorder != null) currentRecorder.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
}
public static void recordBindVBO(int vbo) { if (currentRecorder != null) currentRecorder.recordBindVBO(vbo); }
public static void recordBindVAO(int vao) { if (currentRecorder != null) currentRecorder.recordBindVAO(vao); }
@@ -822,17 +821,34 @@ public class DisplayListManager {
final boolean hasDraws = accumulatedDraws != null && !accumulatedDraws.isEmpty();
if (hasCommands || hasDraws) {
// Phase 1: Compile format-based VBOs (shared by both optimized and unoptimized paths)
final DisplayListVBO compiledBuffers = new DisplayListVBOBuilder().addDraws(accumulatedDraws).build();
try {
final CommandBuffer finalBuffer = new CommandBuffer();
CommandBufferBuilder.buildFromRawBuffer(rawCommandBuffer, accumulatedDraws, finalBuffer);
// Build to CommandBuffer - optimize raw buffer to final buffer
final CommandBuffer finalBuffer = new CommandBuffer();
CommandBufferBuilder.buildFromRawBuffer(rawCommandBuffer, accumulatedDraws, finalBuffer);
final IndexedDrawBatchBuilder indexedBuilder = currentRecorder.getIndexedDraws();
final List<IndexedDrawBatch> indexedBatches;
if (indexedBuilder.isEmpty()) {
indexedBatches = Collections.emptyList();
} else {
final CommandRecorder paused = pauseRecording();
try {
indexedBatches = indexedBuilder.build();
} finally {
try {
for (IndexedDrawCapture c : indexedBuilder.getCaptures()) {
c.freeBuffers();
}
} finally {
resumeRecording(paused);
}
}
}
// Free the recorder (and its buffer) after optimization
currentRecorder.free();
compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), compiledBuffers);
compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), compiledBuffers, indexedBatches);
} finally {
currentRecorder.free();
}
} else {
// Free the recorder even if empty
if (currentRecorder != null) {
@@ -841,8 +857,10 @@ public class DisplayListManager {
// Empty display list - per OpenGL spec, still valid after glNewList/glEndList
compiled = CompiledDisplayList.EMPTY;
}
// Store the compiled list (even if empty - an empty list is still a valid list)
displayListCache.put(glListId, compiled);
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
}
// Log compilation details if enabled (before context restoration changes glListId)
if (LOG_DISPLAY_LIST_COMPILATION) {
@@ -895,7 +913,7 @@ public class DisplayListManager {
}
// Merge the previous draw call if possible
final AccumulatedDraw previous = accumulatedDraws.get(accumulatedDraws.size() - 1);
final AccumulatedDraw previous = accumulatedDraws.getLast();
if (previous.format == tessellator.getVertexFormat() && previous.stateGeneration == stateGeneration) {
previous.mergeDraw(tessellator, copyLast);
return;
@@ -17,6 +17,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.recording.CommandRecorder;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import com.gtnewhorizons.angelica.glsm.recording.commands.TexImage2DCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.TexSubImage2DCmd;
import com.gtnewhorizons.angelica.glsm.stacks.AlphaStateStack;
@@ -43,6 +44,7 @@ import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
@@ -1636,6 +1638,9 @@ public class GLStateManager {
}
alphaTest.enable();
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
public static void disableAlphaTest() {
@@ -1653,6 +1658,9 @@ public class GLStateManager {
}
alphaTest.disable();
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
public static void glAlphaFunc(int function, float reference) {
@@ -1672,6 +1680,9 @@ public class GLStateManager {
alphaState.setFunction(function);
alphaState.setReference(reference);
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
}
@@ -2229,15 +2240,12 @@ public class GLStateManager {
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
if (isVAOBound()) {
DisplayListManager.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
} else if (isVBOBound() || VertexAttribState.hasVBOBoundAttrib()) {
final DirectTessellator result = ImmediateModeRecorder.processDrawElementsFromVBO(mode, indices_count, type, indices_buffer_offset, boundEBO);
if (result != null) {
DisplayListManager.addImmediateModeDraw(result);
}
} else {
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
// Core profile: a default VAO is generated at init and glBindVertexArray(0)
// is redirected to it, so a VAO is always bound here — no fallback branches.
DisplayListManager.flushMatrix();
final IndexedDrawCapture capture = IndexedDrawCapture.create(mode, indices_count, type, indices_buffer_offset, boundEBO);
if (capture != null) {
DisplayListManager.recordIndexedDrawCapture(capture);
}
if (recordMode == RecordMode.COMPILE) {
return;
@@ -4948,12 +4956,14 @@ public class GLStateManager {
private static void invalidateDeletedBuffer(int buffer) {
if (buffer == 0) return;
if (boundVBO == buffer) {
boundVBO = 0;
}
if (boundEBO == buffer) {
boundEBO = 0;
vaoEboMap.put(boundVAO, 0);
if (boundVBO == buffer) boundVBO = 0;
if (boundEBO == buffer) boundEBO = 0;
if (boundPixelUnpackBuffer == buffer) boundPixelUnpackBuffer = 0;
if (boundPixelPackBuffer == buffer) boundPixelPackBuffer = 0;
// Sweep all VAOs: an unbound VAO may later rebind with a dangling EBO id.
for (Int2IntMap.Entry e : vaoEboMap.int2IntEntrySet()) {
if (e.getIntValue() == buffer) e.setValue(0);
}
}
@@ -5070,14 +5080,6 @@ public class GLStateManager {
glBindVertexArray(array);
}
public static boolean isVBOBound() {
return boundVBO != 0;
}
public static boolean isVAOBound() {
return boundVAO != 0;
}
public static boolean vendorIsAMD() {
return VENDOR == AMD;
}
@@ -5661,7 +5663,10 @@ public class GLStateManager {
}
public static void glDeleteProgram(int program) {
if (program == 0) return;
CompatUniformManager.onDeleteProgram(program);
if (activeProgram == program) activeProgram = 0;
RENDER_BACKEND.deleteProgram(program);
}
@@ -5721,6 +5726,14 @@ public class GLStateManager {
return RENDER_BACKEND.getProgramInfoLog(program);
}
public static boolean glIsProgram(int obj) {
return RENDER_BACKEND.isProgram(obj);
}
public static boolean glIsShader(int obj) {
return RENDER_BACKEND.isShader(obj);
}
public static void glBindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) { RENDER_BACKEND.bindImageTexture(unit, texture, level, layered, layer, access, format); }
public static void glMemoryBarrier(int barriers) { RENDER_BACKEND.memoryBarrier(barriers); }
public static void glCopyImageSubData(int srcName, int srcTarget, int srcLevel, int srcX, int srcY, int srcZ,
@@ -146,42 +146,11 @@ public final class QuadConverter {
final int bytesPerIndex = VertexAttribState.Attrib.glTypeSizeBytes(type);
// Read source indices from caller's EBO
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, offset, src);
final long srcAddr = memAddress0(src);
// Allocate output as GL_UNSIGNED_INT
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
default /* GL_UNSIGNED_BYTE */ -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
}
// GL_LAST_VERTEX_CONVENTION: (a,b,d), (b,c,d)
final long o = (long) i * 24;
memPutInt(dstAddr + o, a);
memPutInt(dstAddr + o + 4, b);
memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b);
memPutInt(dstAddr + o + 16, c);
memPutInt(dstAddr + o + 20, d);
}
triangulateQuads(memAddress0(src), type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
memFree(src);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
@@ -198,16 +167,8 @@ public final class QuadConverter {
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final int a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
final long srcAddr = memAddress0(indices) + (long) indices.position() * 4;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
@@ -223,16 +184,8 @@ public final class QuadConverter {
final int triIndexCount = quadCount * 6;
final ByteBuffer dst = memAlloc(triIndexCount * 2);
final long dstAddr = memAddress0(dst);
final int basePos = indices.position();
for (int i = 0; i < quadCount; i++) {
final int base = basePos + i * 4;
final short a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
final long o = (long) i * 12;
memPutShort(dstAddr + o, a); memPutShort(dstAddr + o + 2, b); memPutShort(dstAddr + o + 4, d);
memPutShort(dstAddr + o + 6, b); memPutShort(dstAddr + o + 8, c); memPutShort(dstAddr + o + 10, d);
}
final long srcAddr = memAddress0(indices) + (long) indices.position() * 2;
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
}
@@ -248,31 +201,7 @@ public final class QuadConverter {
final long srcAddr = memAddress0(indices) + indices.position();
final ByteBuffer dst = memAlloc(triIndexCount * 4);
final long dstAddr = memAddress0(dst);
for (int i = 0; i < quadCount; i++) {
final int a, b, c, d;
switch (type) {
case GL11.GL_UNSIGNED_INT -> {
final long p = srcAddr + (long) i * 16;
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
}
case GL11.GL_UNSIGNED_SHORT -> {
final long p = srcAddr + (long) i * 8;
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
}
case GL11.GL_UNSIGNED_BYTE -> {
final long p = srcAddr + (long) i * 4;
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
}
default -> { memFree(dst); return; }
}
final long o = (long) i * 24;
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
}
triangulateQuads(srcAddr, type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
}
@@ -282,14 +211,140 @@ public final class QuadConverter {
*/
public static void destroy() {
if (eboId != 0) {
RENDER_BACKEND.deleteBuffers(eboId);
GLStateManager.glDeleteBuffers(eboId);
eboId = 0;
maxQuads = 0;
}
if (scratchEboId != 0) {
RENDER_BACKEND.deleteBuffers(scratchEboId);
GLStateManager.glDeleteBuffers(scratchEboId);
scratchEboId = 0;
scratchEboCapacity = 0;
}
}
/**
* Read one index at {@code srcAddr + i * sizeof(srcType)} and return its
* zero-extended unsigned value.
*/
private static int readIndex(long srcAddr, int srcType, int i) {
return switch (srcType) {
case GL11.GL_UNSIGNED_BYTE -> memGetByte(srcAddr + i) & 0xFF;
case GL11.GL_UNSIGNED_SHORT -> memGetShort(srcAddr + i * 2L) & 0xFFFF;
case GL11.GL_UNSIGNED_INT -> memGetInt(srcAddr + i * 4L);
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(srcType));
};
}
/**
* Write one index at {@code dstAddr + i * sizeof(dstType)}.
*/
private static void writeIndex(long dstAddr, int dstType, int i, int value) {
switch (dstType) {
case GL11.GL_UNSIGNED_SHORT -> memPutShort(dstAddr + i * 2L, (short) value);
case GL11.GL_UNSIGNED_INT -> memPutInt(dstAddr + i * 4L, value);
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(dstType));
}
}
/**
* Read {@code quadCount} quads from {@code srcAddr} and emit 6 triangle indices
* per quad at {@code dstAddr} using {@code GL_LAST_VERTEX_CONVENTION}:
* {@code (a,b,d), (b,c,d)}. v3 is the provoking vertex for both triangles;
* diagonal runs b→d. Matches Mesa's {@code do_quad} LAST path.
*
* <p>Caller allocates dst of size {@code quadCount * 6 * sizeof(dstType)}.
*
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT}
* / {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Widening only.
*/
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount) {
triangulateQuads(srcAddr, srcType, dstAddr, dstType, quadCount, 0);
}
/**
* Same as {@link #triangulateQuads(long, int, long, int, int)} but subtracts
* {@code minVtx} from every emitted index, rebasing to a 0-based buffer in
* one pass.
*/
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount, int minVtx) {
for (int i = 0; i < quadCount; i++) {
final int a = readIndex(srcAddr, srcType, i * 4) - minVtx;
final int b = readIndex(srcAddr, srcType, i * 4 + 1) - minVtx;
final int c = readIndex(srcAddr, srcType, i * 4 + 2) - minVtx;
final int d = readIndex(srcAddr, srcType, i * 4 + 3) - minVtx;
final int o = i * 6;
writeIndex(dstAddr, dstType, o, a);
writeIndex(dstAddr, dstType, o + 1, b);
writeIndex(dstAddr, dstType, o + 2, d);
writeIndex(dstAddr, dstType, o + 3, b);
writeIndex(dstAddr, dstType, o + 4, c);
writeIndex(dstAddr, dstType, o + 5, d);
}
}
/**
* Copy {@code count} indices from {@code srcAddr} to {@code dstAddr}, converting
* from {@code srcType} to {@code dstType}.
*
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
* {@code GL_UNSIGNED_INT}. Widening only; narrowing is rejected by
* {@link #writeIndex(long, int, int, int) writeIndex}.
*/
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count) {
widenIndices(srcAddr, srcType, dstAddr, dstType, count, 0);
}
/**
* Same as {@link #widenIndices(long, int, long, int, int)} but subtracts
* {@code minVtx} from every written index in one pass.
*/
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count, int minVtx) {
for (int i = 0; i < count; i++) {
writeIndex(dstAddr, dstType, i, readIndex(srcAddr, srcType, i) - minVtx);
}
}
/**
* Scan {@code count} indices at {@code srcAddr} of {@code srcType}. Returns
* {@code (max << 32) | (min & 0xFFFFFFFFL)} or {@code -1L} for an unsupported
* type or empty range.
*/
public static long scanMinMaxIndex(long srcAddr, int srcType, int count) {
if (count <= 0) return -1L;
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
switch (srcType) {
case GL11.GL_UNSIGNED_BYTE -> {
for (int i = 0; i < count; i++) {
final int v = memGetByte(srcAddr + i) & 0xFF;
if (v < min) min = v;
if (v > max) max = v;
}
}
case GL11.GL_UNSIGNED_SHORT -> {
for (int i = 0; i < count; i++) {
final int v = memGetShort(srcAddr + i * 2L) & 0xFFFF;
if (v < min) min = v;
if (v > max) max = v;
}
}
case GL11.GL_UNSIGNED_INT -> {
for (int i = 0; i < count; i++) {
final long v = memGetInt(srcAddr + i * 4L) & 0xFFFFFFFFL;
if (v < min) min = v;
if (v > max) max = v;
}
// readIndex / writeIndex / rebase all route UINT through signed int.
// Reject the > 2^31 slice so the caller bails rather than silently
// emitting negative indices. MC never hits this in practice.
if (max > Integer.MAX_VALUE) return -1L;
}
default -> {
return -1L;
}
}
return (max << 32) | (min & 0xFFFFFFFFL);
}
}
@@ -432,7 +432,7 @@ public class RenderSystem {
}
public static void deleteBuffers(int buffer) {
RENDER_BACKEND.deleteBuffers(buffer);
GLStateManager.glDeleteBuffers(buffer);
}
public static long getVRAM() {
@@ -491,7 +491,7 @@ public class RenderSystem {
public static void destroySampler(int sampler) {
if (!supportsSamplerObjects || sampler == 0) return;
RENDER_BACKEND.deleteSamplers(sampler);
GLStateManager.glDeleteSamplers(sampler);
}
public static void samplerParameteri(int sampler, int pname, int param) {
@@ -178,7 +178,7 @@ public class Program {
}
public void destroy() {
RENDER_BACKEND.deleteProgram(programId);
GLStateManager.glDeleteProgram(programId);
}
/**
@@ -1,12 +1,6 @@
package com.gtnewhorizons.angelica.glsm.hooks;
import com.gtnewhorizons.angelica.glsm.hooks.events.BlendFuncChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.FogStateChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.LightmapCoordsEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.ProgramChangeEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureBindEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureDeleteEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureUnitStateEvent;
import com.gtnewhorizons.angelica.glsm.hooks.events.*;
import net.minecraftforge.eventbus.api.bus.EventBus;
public final class GLSMHooks {
@@ -23,6 +17,7 @@ public final class GLSMHooks {
public static final EventBus<BlendFuncChangeEvent> BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class);
public static final EventBus<FogStateChangeEvent> FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class);
public static final EventBus<LightmapCoordsEvent> LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class);
public static final EventBus<AlphaStateChangeEvent> ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class);
// Reusable event instances
public static final TextureBindEvent textureBindEvent = new TextureBindEvent();
@@ -32,6 +27,7 @@ public final class GLSMHooks {
public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent();
public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent();
public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent();
public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent();
private GLSMHooks() {}
}
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
/**
* AlphaTest changed - Listeners should query GLSM alpha state directly if they care what changed.
*/
public final class AlphaStateChangeEvent extends MutableEvent {
}
@@ -0,0 +1,178 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public final class AttribSnapshot {
public record AttribDesc(int location, int size, int type, boolean normalized, int stride, long offset, int sourceVboId, ByteBuffer readBuffer, long readBufferBaseOffset) {
public int effectiveStride() {
return stride != 0 ? stride : size * VertexAttribState.Attrib.glTypeSizeBytes(type);
}
public int typeSizeBytes() {
return VertexAttribState.Attrib.glTypeSizeBytes(type);
}
}
private final AttribDesc[] perLocation;
private final List<ByteBuffer> allocatedBuffers;
private boolean freed;
private AttribSnapshot(AttribDesc[] perLocation, List<ByteBuffer> allocatedBuffers) {
this.perLocation = perLocation;
this.allocatedBuffers = allocatedBuffers;
}
public AttribDesc get(int location) {
return (location >= 0 && location < perLocation.length) ? perLocation[location] : null;
}
public int maxLocations() {
return perLocation.length;
}
public void free() {
if (freed) return;
freed = true;
for (ByteBuffer buf : allocatedBuffers) {
MemoryUtilities.memFree(buf);
}
allocatedBuffers.clear();
}
/**
* Snapshot attribs over a tight vertex window {@code [firstVertex, firstVertex+vertexCount)}.
*
* <p>Hot path (single VBO backing every enabled attrib — the typical draw) runs without
* allocating any hashmaps. A second distinct VBO promotes to the map-backed fallback.
*/
public static AttribSnapshot snapshot(int firstVertex, int vertexCount) {
final AttribDesc[] out = new AttribDesc[VertexAttribState.MAX_ATTRIBS];
final List<ByteBuffer> allocated = new ArrayList<>();
final int prevVBO = GLStateManager.getBoundVBO();
// Pass 1 — unioned [start,end] per vboId. Hot path: one source VBO, zero allocations.
int rangeSoleVboId = 0;
long rangeSoleStart = Long.MAX_VALUE;
long rangeSoleEnd = Long.MIN_VALUE;
Int2ObjectOpenHashMap<long[]> vboRanges = null;
final long lastVtx = (long) firstVertex + vertexCount - 1;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled || a.vboId == 0) continue;
final int stride = a.effectiveStride();
final long typeBytes = (long) a.size * VertexAttribState.Attrib.glTypeSizeBytes(a.type);
final long start = a.offset + (long) firstVertex * stride;
final long end = a.offset + lastVtx * stride + typeBytes;
if (vboRanges != null) {
long[] r = vboRanges.get(a.vboId);
if (r == null) {
vboRanges.put(a.vboId, new long[] { start, end });
} else {
if (start < r[0]) r[0] = start;
if (end > r[1]) r[1] = end;
}
} else if (rangeSoleVboId == 0 || rangeSoleVboId == a.vboId) {
rangeSoleVboId = a.vboId;
if (start < rangeSoleStart) rangeSoleStart = start;
if (end > rangeSoleEnd) rangeSoleEnd = end;
} else {
// Spill: second distinct vboId. Promote the accumulated range to the map.
vboRanges = new Int2ObjectOpenHashMap<>();
vboRanges.put(rangeSoleVboId, new long[] { rangeSoleStart, rangeSoleEnd });
vboRanges.put(a.vboId, new long[] { start, end });
}
}
// Pass 2 — readback cache. Same spill pattern.
int readSoleVboId = 0;
ByteBuffer readSoleBuf = null;
long readSoleOffset = 0;
Int2ObjectOpenHashMap<ByteBuffer> vboBuffers = null;
Int2LongOpenHashMap vboReadOffsets = null;
try {
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled) continue;
if (a.vboId != 0) {
ByteBuffer buf;
long readOffset;
if (vboBuffers != null) {
buf = vboBuffers.get(a.vboId);
readOffset = buf != null ? vboReadOffsets.get(a.vboId) : 0;
} else if (readSoleVboId == a.vboId) {
buf = readSoleBuf;
readOffset = readSoleOffset;
} else {
buf = null;
readOffset = 0;
}
if (buf == null) {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long rStart;
final long rEnd;
if (vboRanges != null) {
final long[] range = vboRanges.get(a.vboId);
rStart = range[0];
rEnd = range[1];
} else {
rStart = rangeSoleStart;
rEnd = rangeSoleEnd;
}
final long ro = Math.max(0, rStart);
final int rs = (int) (Math.min(bufferSize, rEnd) - ro);
if (rs <= 0) continue;
buf = MemoryUtilities.memAlloc(rs);
GLStateManager.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, ro, buf);
allocated.add(buf);
readOffset = ro;
if (vboBuffers != null) {
vboBuffers.put(a.vboId, buf);
vboReadOffsets.put(a.vboId, ro);
} else if (readSoleVboId == 0) {
readSoleVboId = a.vboId;
readSoleBuf = buf;
readSoleOffset = ro;
} else {
vboBuffers = new Int2ObjectOpenHashMap<>();
vboReadOffsets = new Int2LongOpenHashMap();
vboBuffers.put(readSoleVboId, readSoleBuf);
vboReadOffsets.put(readSoleVboId, readSoleOffset);
vboBuffers.put(a.vboId, buf);
vboReadOffsets.put(a.vboId, ro);
}
}
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.offset, a.vboId, buf, readOffset);
} else if (a.clientPointer != null) {
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.clientPointer.position(), 0, a.clientPointer, 0L);
}
}
} finally {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
return new AttribSnapshot(out, allocated);
}
}
@@ -600,23 +600,6 @@ public final class CommandBuffer {
writeInt(listId);
}
public void writeDrawArrays(int mode, int start, int count) {
ensureCapacity(16);
writeInt(GLCommand.DRAW_ARRAYS);
writeInt(mode);
writeInt(start);
writeInt(count);
}
public void writeDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
ensureCapacity(24);
writeInt(GLCommand.DRAW_ELEMENTS);
writeInt(mode);
writeInt(indices_count);
writeInt(type);
writeLong(indices_buffer_offset);
}
public void writeBindVBO(int vbo) {
ensureCapacity(8);
writeInt(GLCommand.BIND_VBO);
@@ -530,14 +530,6 @@ public final class CommandBufferExecutor {
ptr += 4 * MAX_DRAW_BUFFERS;
GLStateManager.glDrawBuffers(DRAW_BUFFERS_BUFFER);
}
case GLCommand.DRAW_ARRAYS -> {
GLStateManager.glDrawArrays(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8));
ptr += 12;
}
case GLCommand.DRAW_ELEMENTS -> {
GLStateManager.glDrawElements(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8), memGetLong(ptr + 12));
ptr += 20;
}
case GLCommand.BIND_VBO -> {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, memGetInt(ptr));
ptr += 4;
@@ -150,8 +150,6 @@ public final class CommandBufferProcessor {
// Draw/call commands
case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt());
case GLCommand.CALL_LIST -> out.writeCallList(raw.readInt());
case GLCommand.DRAW_ARRAYS -> out.writeDrawArrays(raw.readInt(), raw.readInt(), raw.readInt());
case GLCommand.DRAW_ELEMENTS -> out.writeDrawElements(raw.readInt(), raw.readInt(), raw.readInt(), raw.readLong());
case GLCommand.BIND_VBO -> out.writeBindVBO(raw.readInt());
case GLCommand.BIND_VAO -> out.writeBindVAO(raw.readInt());
@@ -1,6 +1,8 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import org.joml.Matrix4f;
import java.nio.FloatBuffer;
@@ -11,6 +13,7 @@ import java.nio.IntBuffer;
*/
public final class CommandRecorder {
private final CommandBuffer buffer;
private final IndexedDrawBatchBuilder indexedDraws = new IndexedDrawBatchBuilder();
private int commandCount;
public CommandRecorder() {
@@ -26,7 +29,19 @@ public final class CommandRecorder {
return commandCount;
}
public IndexedDrawBatchBuilder getIndexedDraws() {
return indexedDraws;
}
public void recordIndexedDrawCapture(IndexedDrawCapture capture) {
indexedDraws.add(capture);
recordComplexCommand(capture.placeholder);
}
public void free() {
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
c.freeBuffers();
}
buffer.free();
}
@@ -347,16 +362,6 @@ public final class CommandRecorder {
commandCount++;
}
public void recordDrawArrays(int mode, int start, int count) {
buffer.writeDrawArrays(mode, start, count);
commandCount++;
}
public void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
buffer.writeDrawElements(mode, indices_count, type, indices_buffer_offset);
commandCount++;
}
public void recordBindVBO(int vbo) {
buffer.writeBindVBO(vbo);
commandCount++;
@@ -2,12 +2,15 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
@@ -26,16 +29,18 @@ public final class CompiledDisplayList {
* Empty display list singleton. Per OpenGL spec, an empty list is still valid.
* This instance does nothing when rendered and is never deleted (shared singleton).
*/
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null);
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null, Collections.emptyList());
private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed
private final Object[] complexObjects; // Complex commands (TexImage2D, etc.)
private final DisplayListVBO ownedVbos; // GPU resources referenced by index
private final List<IndexedDrawBatch> indexedBatches; // Shared VAO/VBO/EBO triples per layout group
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos, List<IndexedDrawBatch> indexedBatches) {
this.commandBuffer = commandBuffer;
this.complexObjects = complexObjects;
this.ownedVbos = ownedVbos;
this.indexedBatches = indexedBatches;
}
/**
@@ -51,7 +56,6 @@ public final class CompiledDisplayList {
* Delete all resources held by this display list.
*/
public void delete() {
// Delete complex objects that hold resources
if (complexObjects != null) {
for (Object obj : complexObjects) {
if (obj instanceof DisplayListCommand cmd) {
@@ -60,11 +64,18 @@ public final class CompiledDisplayList {
}
}
// Close VBOs
// Close VBOs from immediate-mode DisplayListVBOBuilder.
if (ownedVbos != null) {
ownedVbos.delete();
}
// Release shared (VAO, VBO, EBO) triples from IndexedDrawBatchBuilder.
if (indexedBatches != null) {
for (IndexedDrawBatch batch : indexedBatches) {
batch.delete();
}
}
// Free the command buffer (off-heap memory)
if (commandBuffer != null) {
memFree(commandBuffer);
@@ -89,6 +100,10 @@ public final class CompiledDisplayList {
return ownedVbos;
}
public List<IndexedDrawBatch> getIndexedBatches() {
return indexedBatches;
}
// === Test inspection methods ===
/**
@@ -97,8 +97,6 @@ public final class GLCommand {
public static final int CALL_LIST = 102; // [cmd:4][listId:4] = 8 bytes
public static final int DRAW_ARRAYS = 110; // [cmd:4][drawMode:4][start:4][count:4] = 20 bytes
public static final int DRAW_ELEMENTS = 111; // [cmd:4][mode:4][indices_count:4][type:4][indices_buffer_offset:8] = 28 bytes
public static final int DRAW_BUFFER = 112; // [cmd:4][mode:4] = 8 bytes
public static final int DRAW_BUFFERS = 113; // [cmd:4][count:4][bufs:4*8] = 40 bytes (up to 8 buffers)
/**
@@ -219,7 +217,7 @@ public final class GLCommand {
// Three int commands (16 bytes)
case GLCommand.STENCIL_FUNC, GLCommand.STENCIL_OP, GLCommand.TEX_PARAMETERI,
GLCommand.LIGHTF, GLCommand.LIGHTI,
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF, GLCommand.DRAW_ARRAYS -> 16;
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF -> 16;
// Four int commands (20 bytes)
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
@@ -227,8 +225,6 @@ public final class GLCommand {
GLCommand.SCISSOR,
GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.BLEND_COLOR -> 20;
case GLCommand.DRAW_ELEMENTS -> 24;
// DRAW_RANGE_RESTORE: 28 bytes
case GLCommand.DRAW_RANGE_RESTORE -> 28;
@@ -5,15 +5,11 @@ import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
/**
* Records immediate mode GL calls (glBegin/glEnd/glVertex) via {@link DirectTessellator}.
* Used both during display list compilation and for live immediate mode emulation in core profile.
@@ -167,19 +163,14 @@ public final class ImmediateModeRecorder {
.mapToInt(Usage::getAttributeLocation)
.max().orElse(0);
private static final ByteBuffer[] scratchAttribBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchAttribBaseOffsets = new long[NUM_ATTRIBS];
private static final int[] scratchVBOIds = new int[NUM_ATTRIBS];
private static final ByteBuffer[] scratchVBOBuffers = new ByteBuffer[NUM_ATTRIBS];
private static final long[] scratchVBOReadOffsets = new long[NUM_ATTRIBS];
private static final class AttribReader {
ByteBuffer data;
int stride;
int baseOffset;
int size;
int loc;
VertexAttribState.Attrib attrib;
int type;
boolean normalized;
}
private static final AttribReader[] readers = new AttribReader[NUM_ATTRIBS];
@@ -198,167 +189,32 @@ public final class ImmediateModeRecorder {
* Reads vertex data from VBOs or client memory referenced by VertexAttribState and converts to immediate mode geometry.
*/
public static DirectTessellator processDrawArraysFromAttribs(int mode, int first, int count) {
final int readVBOCount = readbackAttribVBOs(first, count);
final AttribSnapshot snap = AttribSnapshot.snapshot(first, count);
try {
prepareReaders();
prepareReaders(snap);
if (positionReaderIndex < 0) return null;
return readVerticesFromBuffer(mode, first, count);
} finally {
freeReadbackBuffers(readVBOCount);
snap.free();
}
}
/**
* Process a glDrawElements call during display list recording when a VBO is bound.
*/
public static DirectTessellator processDrawElementsFromVBO(int mode, int indicesCount, int indexType, long indicesOffset, int eboId) {
if (eboId == 0) return null;
final int readVBOCount = readbackAttribVBOs(-1, 0);
try {
prepareReaders();
if (positionReaderIndex < 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboSize <= 0) {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final int indexSize = VertexAttribState.Attrib.glTypeSizeBytes(indexType);
final int eboReadOffset = (int) indicesOffset;
final int eboReadSize = indicesCount * indexSize;
if (eboReadOffset + eboReadSize > eboSize) {
GLStateManager.LOGGER.warn("[EBO] Read range {}+{} exceeds buffer size {}", eboReadOffset, eboReadSize, eboSize);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
return null;
}
final ByteBuffer eboData = MemoryUtilities.memAlloc(eboReadSize);
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboReadOffset, eboData);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
try {
return readVerticesFromBufferIndexed(mode, eboData, indexType, 0, indicesCount);
} finally {
MemoryUtilities.memFree(eboData);
}
} finally {
freeReadbackBuffers(readVBOCount);
}
}
/**
* Read data for all enabled attribs into scratchAttribBuffers.
* For VBO-backed attribs, reads VBO data. For client-memory attribs, uses the client pointer directly.
*/
private static int readbackAttribVBOs(int firstVertex, int vertexCount) {
int readVBOCount = 0;
final int prevVBO = GLStateManager.getBoundVBO();
final boolean partialRead = firstVertex >= 0;
for (int i = 0; i < NUM_ATTRIBS; i++) {
scratchAttribBuffers[i] = null;
scratchAttribBaseOffsets[i] = 0;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
if (!a.enabled) continue;
if (a.vboId != 0) {
// VBO-backed attrib — read from GPU
ByteBuffer existing = null;
int existingIdx = -1;
for (int j = 0; j < readVBOCount; j++) {
if (scratchVBOIds[j] == a.vboId) {
existing = scratchVBOBuffers[j];
existingIdx = j;
break;
}
}
if (existing != null) {
scratchAttribBuffers[i] = existing;
scratchAttribBaseOffsets[i] = scratchVBOReadOffsets[existingIdx];
} else {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
final int bufferSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (bufferSize <= 0) {
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
continue;
}
final long readOffset;
final int readSize;
if (partialRead) {
final int stride = a.effectiveStride();
long rangeStart = a.offset + (long) firstVertex * stride;
long rangeEnd = a.offset + (long) (firstVertex + vertexCount - 1) * stride + (long) a.size * a.typeSizeBytes();
for (int k = 0; k < NUM_ATTRIBS; k++) {
if (k == i) continue;
final VertexAttribState.Attrib ak = VertexAttribState.get(k);
if (!ak.enabled || ak.vboId != a.vboId) continue;
final int ks = ak.effectiveStride();
rangeStart = Math.min(rangeStart, ak.offset + (long) firstVertex * ks);
rangeEnd = Math.max(rangeEnd, ak.offset + (long) (firstVertex + vertexCount - 1) * ks
+ (long) ak.size * ak.typeSizeBytes());
}
rangeStart = Math.max(0, rangeStart);
rangeEnd = Math.min(bufferSize, rangeEnd);
readOffset = rangeStart;
readSize = (int) (rangeEnd - rangeStart);
} else {
readOffset = 0;
readSize = bufferSize;
}
if (readSize <= 0) continue;
final ByteBuffer buf = MemoryUtilities.memAlloc(readSize);
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, readOffset, buf);
scratchAttribBuffers[i] = buf;
scratchAttribBaseOffsets[i] = readOffset;
scratchVBOIds[readVBOCount] = a.vboId;
scratchVBOReadOffsets[readVBOCount] = readOffset;
scratchVBOBuffers[readVBOCount] = buf;
readVBOCount++;
}
} else if (a.clientPointer != null) {
// Client-memory attrib — use pointer directly (no allocation needed)
scratchAttribBuffers[i] = a.clientPointer;
}
}
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
return readVBOCount;
}
private static void freeReadbackBuffers(int readVBOCount) {
for (int i = 0; i < readVBOCount; i++) {
MemoryUtilities.memFree(scratchVBOBuffers[i]);
scratchVBOBuffers[i] = null;
}
}
private static void prepareReaders() {
private static void prepareReaders(AttribSnapshot snap) {
nonPositionCount = 0;
positionReaderIndex = -1;
for (int i = 0; i < NUM_ATTRIBS; i++) {
final ByteBuffer buf = scratchAttribBuffers[i];
if (buf == null) continue;
final VertexAttribState.Attrib a = VertexAttribState.get(i);
final AttribSnapshot.AttribDesc d = snap.get(i);
if (d == null) continue;
final AttribReader r = readers[i];
r.data = buf;
r.stride = a.effectiveStride();
r.baseOffset = (int) (a.offset - scratchAttribBaseOffsets[i]);
r.size = a.size;
r.data = d.readBuffer();
r.stride = d.effectiveStride();
r.baseOffset = (int) (d.offset() - d.readBufferBaseOffset());
r.size = d.size();
r.loc = i;
r.attrib = a;
r.type = d.type();
r.normalized = d.normalized();
if (i == LOC_POSITION) {
positionReaderIndex = i;
@@ -379,18 +235,6 @@ public final class ImmediateModeRecorder {
return end();
}
private static DirectTessellator readVerticesFromBufferIndexed(int mode, ByteBuffer eboData, int indexType, int eboOffset, int indicesCount) {
begin(mode);
final DirectTessellator t = tessellator();
for (int i = 0; i < indicesCount; i++) {
final int index = readIndex(eboData, indexType, eboOffset, i);
readVertexFromBuffer(t, index);
}
return end();
}
public static int readIndex(ByteBuffer ebo, int indexType, int eboOffset, int i) {
return switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> ebo.get(eboOffset + i) & 0xFF;
@@ -411,39 +255,43 @@ public final class ImmediateModeRecorder {
final AttribReader r = readers[positionReaderIndex];
final int base = r.baseOffset + vertexIndex * r.stride;
t.addVertex(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
(r.size >= 3) ? r.attrib.readComponent(r.data, base, 2) : 0.0f);
readComponent(r, base, 0),
readComponent(r, base, 1),
(r.size >= 3) ? readComponent(r, base, 2) : 0.0f);
}
}
private static void emitAttrib(DirectTessellator t, AttribReader r, int base) {
if (r.loc == LOC_NORMAL) {
t.setNormal(
r.attrib.readComponent(r.data, base, 0),
r.attrib.readComponent(r.data, base, 1),
r.attrib.readComponent(r.data, base, 2));
readComponent(r, base, 0),
readComponent(r, base, 1),
readComponent(r, base, 2));
} else if (r.loc == LOC_UV0) {
t.setTextureUV(
r.attrib.readComponent(r.data, base, 0),
(r.size >= 2) ? r.attrib.readComponent(r.data, base, 1) : 0.0f);
readComponent(r, base, 0),
(r.size >= 2) ? readComponent(r, base, 1) : 0.0f);
} else if (r.loc == LOC_UV1) {
if (r.attrib.type == GL11.GL_SHORT) {
if (r.type == GL11.GL_SHORT) {
final int s = r.data.getShort(base) & 0xFFFF;
final int u = (r.size >= 2) ? (r.data.getShort(base + 2) & 0xFFFF) : 0;
t.setBrightness((u << 16) | s);
} else if (r.attrib.type == GL11.GL_FLOAT) {
} else if (r.type == GL11.GL_FLOAT) {
final int s = (int) r.data.getFloat(base);
final int u = (r.size >= 2) ? (int) r.data.getFloat(base + 4) : 0;
t.setBrightness((u << 16) | (s & 0xFFFF));
}
} else if (r.loc == LOC_COLOR) {
final float cr = r.attrib.readComponent(r.data, base, 0);
final float cg = r.attrib.readComponent(r.data, base, 1);
final float cb = r.attrib.readComponent(r.data, base, 2);
final float ca = (r.size >= 4) ? r.attrib.readComponent(r.data, base, 3) : 1.0f;
final float cr = readComponent(r, base, 0);
final float cg = readComponent(r, base, 1);
final float cb = readComponent(r, base, 2);
final float ca = (r.size >= 4) ? readComponent(r, base, 3) : 1.0f;
t.setColorRGBA((int) (cr * 255.0f), (int) (cg * 255.0f), (int) (cb * 255.0f), (int) (ca * 255.0f));
}
}
private static float readComponent(AttribReader r, int base, int component) {
return VertexAttribState.Attrib.readComponent(r.type, r.normalized, r.data, base, component);
}
}
@@ -0,0 +1,64 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import java.util.Arrays;
public final class AttribLayoutKey {
final int[] locations;
final int[] sizes;
final int[] types;
final boolean[] normalized;
private final int[] offsets;
private final int stride;
private final int hash;
public AttribLayoutKey(int[] locations, int[] sizes, int[] types, boolean[] normalized) {
this.locations = locations;
this.sizes = sizes;
this.types = types;
this.normalized = normalized;
final int n = locations.length;
final int[] off = new int[n];
int base = 0;
int maxAlign = 1;
for (int i = 0; i < n; i++) {
final int typeSize = VertexAttribState.Attrib.glTypeSizeBytes(types[i]);
if (typeSize > maxAlign) maxAlign = typeSize;
base = (base + typeSize - 1) & -typeSize;
off[i] = base;
base += sizes[i] * typeSize;
}
this.offsets = off;
this.stride = (base + maxAlign - 1) & -maxAlign;
int h = Arrays.hashCode(locations);
h = 31 * h + Arrays.hashCode(sizes);
h = 31 * h + Arrays.hashCode(types);
h = 31 * h + Arrays.hashCode(normalized);
this.hash = h;
}
public int offset(int i) {
return offsets[i];
}
public int stride() {
return stride;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof AttribLayoutKey k)) return false;
return Arrays.equals(locations, k.locations)
&& Arrays.equals(sizes, k.sizes)
&& Arrays.equals(types, k.types)
&& Arrays.equals(normalized, k.normalized);
}
@Override
public int hashCode() {
return hash;
}
}
@@ -0,0 +1,37 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
private int sharedVAO;
private int drawMode;
private int indexCount;
private int indexType;
private long indexByteOffset;
public void fill(int sharedVAO, int drawMode, int indexCount, int indexType, long indexByteOffset) {
this.sharedVAO = sharedVAO;
this.drawMode = drawMode;
this.indexCount = indexCount;
this.indexType = indexType;
this.indexByteOffset = indexByteOffset;
}
public int getSharedVAO() { return sharedVAO; }
public int getDrawMode() { return drawMode; }
public int getIndexCount() { return indexCount; }
public int getIndexType() { return indexType; }
public long getIndexByteOffset() { return indexByteOffset; }
@Override
public void execute() {
if (sharedVAO == 0) return;
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
}
@Override
public String toString() {
return "BatchedIndexedDrawCmd(vao=" + sharedVAO + ", mode=" + drawMode + ", count=" + indexCount + ", type=" + indexType + ", offset=" + indexByteOffset + ")";
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
public final class IndexedDrawBatch {
private int sharedVAO;
private int sharedVBO;
private int sharedEBO;
private boolean deleted;
public IndexedDrawBatch(int sharedVAO, int sharedVBO, int sharedEBO) {
this.sharedVAO = sharedVAO;
this.sharedVBO = sharedVBO;
this.sharedEBO = sharedEBO;
}
public int getSharedVAO() { return sharedVAO; }
public int getSharedVBO() { return sharedVBO; }
public int getSharedEBO() { return sharedEBO; }
public void delete() {
if (deleted) return;
deleted = true;
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
sharedVAO = sharedVBO = sharedEBO = 0;
}
@Override
public String toString() {
return "IndexedDrawBatch(vao=" + sharedVAO + ", vbo=" + sharedVBO + ", ebo=" + sharedEBO + ")";
}
}
@@ -0,0 +1,171 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
public final class IndexedDrawBatchBuilder {
private final List<IndexedDrawCapture> captures = new ArrayList<>();
public boolean isEmpty() {
return captures.isEmpty();
}
public void add(IndexedDrawCapture capture) {
captures.add(capture);
}
public List<IndexedDrawCapture> getCaptures() {
return captures;
}
public List<IndexedDrawBatch> build() {
final List<IndexedDrawBatch> out = new ArrayList<>();
if (captures.isEmpty()) return out;
final int prevVAO = GLStateManager.getBoundVAO();
final int prevVBO = GLStateManager.getBoundVBO();
final int prevEBO = GLStateManager.getBoundEBO();
final List<GroupResult> results = new ArrayList<>();
boolean success = false;
try {
final Object2ObjectLinkedOpenHashMap<AttribLayoutKey, List<IndexedDrawCapture>> groups = new Object2ObjectLinkedOpenHashMap<>();
for (IndexedDrawCapture c : captures) {
groups.computeIfAbsent(c.layoutKey, k -> new ArrayList<>()).add(c);
}
for (List<IndexedDrawCapture> group : groups.values()) {
results.add(buildGroup(group));
}
// Placeholder fill is deferred until every group's GL work has succeeded.
for (GroupResult r : results) r.commit();
for (GroupResult r : results) out.add(r.batch);
success = true;
} finally {
if (!success) {
for (GroupResult r : results) r.batch.delete();
}
GLStateManager.glBindVertexArray(prevVAO);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
return out;
}
private record GroupResult(IndexedDrawBatch batch, List<IndexedDrawCapture> captures,
int[] drawModes, int[] indexCounts, long[] indexOffsets) {
void commit() {
final int sharedVAO = batch.getSharedVAO();
for (int i = 0; i < captures.size(); i++) {
captures.get(i).placeholder.fill(sharedVAO, drawModes[i], indexCounts[i], GL11.GL_UNSIGNED_INT, indexOffsets[i]);
}
}
}
private static GroupResult buildGroup(List<IndexedDrawCapture> group) {
long totalVertexBytes = 0;
long totalIndexBytes = 0;
for (IndexedDrawCapture c : group) {
totalVertexBytes += (long) c.vertexCount * c.layoutKey.stride();
totalIndexBytes += (long) c.indexCount * 4; // always GL_UNSIGNED_INT
}
if (totalVertexBytes > Integer.MAX_VALUE || totalIndexBytes > Integer.MAX_VALUE) {
throw new IllegalStateException("batch exceeds 2GiB — split needed");
}
int sharedVAO = 0;
int sharedVBO = 0;
int sharedEBO = 0;
ByteBuffer eboData = null;
boolean success = false;
final int n = group.size();
final int[] drawModes = new int[n];
final int[] indexCounts = new int[n];
final long[] indexOffsets = new long[n];
try {
sharedVAO = GLStateManager.glGenVertexArrays();
sharedVBO = GLStateManager.glGenBuffers();
sharedEBO = GLStateManager.glGenBuffers();
eboData = MemoryUtilities.memAlloc((int) totalIndexBytes);
GLStateManager.glBindVertexArray(sharedVAO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, totalVertexBytes, GL15.GL_STATIC_DRAW);
int vboBytePos = 0;
int eboBytePos = 0;
int vertexBase = 0;
final long eboAddr = memAddress0(eboData);
for (int g = 0; g < n; g++) {
final IndexedDrawCapture c = group.get(g);
final int vBytes = c.vertexCount * c.layoutKey.stride();
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, vboBytePos, c.vertexData);
final long srcIdxAddr = memAddress0(c.indexData);
final int idxBytes = c.indexCount * 4;
if (vertexBase == 0) {
// First capture (or a group that starts at 0) — indices are already
// rebased to [0..vertexCount) in IndexedDrawCapture, so a raw bulk copy
// is correct and strictly faster than the per-index loop.
memCopy(srcIdxAddr, eboAddr + eboBytePos, idxBytes);
} else {
final long dstBase = eboAddr + eboBytePos;
for (int i = 0; i < c.indexCount; i++) {
memPutInt(dstBase + i * 4L, memGetInt(srcIdxAddr + i * 4L) + vertexBase);
}
}
drawModes[g] = c.drawMode;
indexCounts[g] = c.indexCount;
indexOffsets[g] = eboBytePos;
vboBytePos += vBytes;
eboBytePos += idxBytes;
vertexBase += c.vertexCount;
}
eboData.position(0).limit((int) totalIndexBytes);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboData, GL15.GL_STATIC_DRAW);
final AttribLayoutKey layout = group.get(0).layoutKey;
final int stride = layout.stride();
for (int i = 0; i < layout.locations.length; i++) {
final int loc = layout.locations[i];
final int size = layout.sizes[i];
final int type = layout.types[i];
GLStateManager.glVertexAttribPointer(loc, size, type, layout.normalized[i], stride, layout.offset(i));
GLStateManager.glEnableVertexAttribArray(loc);
}
success = true;
return new GroupResult(new IndexedDrawBatch(sharedVAO, sharedVBO, sharedEBO),
group, drawModes, indexCounts, indexOffsets);
} finally {
if (eboData != null) MemoryUtilities.memFree(eboData);
if (!success) {
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
}
}
}
}
@@ -0,0 +1,242 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.recording.AttribSnapshot;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Set;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetLong;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutLong;
/** {@link #create} returns null on any recoverable validation failure; callers must not record the placeholder. */
public final class IndexedDrawCapture {
public final AttribLayoutKey layoutKey;
public final int drawMode;
public final int vertexCount;
public final int indexCount;
public final ByteBuffer vertexData;
public final ByteBuffer indexData;
public final BatchedIndexedDrawCmd placeholder;
private boolean freed;
public IndexedDrawCapture(AttribLayoutKey layoutKey, int drawMode, int vertexCount, int indexCount,
ByteBuffer vertexData, ByteBuffer indexData,
BatchedIndexedDrawCmd placeholder) {
this.layoutKey = layoutKey;
this.drawMode = drawMode;
this.vertexCount = vertexCount;
this.indexCount = indexCount;
this.vertexData = vertexData;
this.indexData = indexData;
this.placeholder = placeholder;
}
public void freeBuffers() {
if (freed) return;
freed = true;
if (vertexData != null) MemoryUtilities.memFree(vertexData);
if (indexData != null) MemoryUtilities.memFree(indexData);
}
public boolean isFreed() {
return freed;
}
private static final Set<String> WARN_ONCE = ConcurrentHashMap.newKeySet();
private static void warnOnce(String key, String fmt, Object... args) {
if (WARN_ONCE.add(key)) GLStateManager.LOGGER.warn(fmt, args);
}
public static IndexedDrawCapture create(int mode, int indicesCount, int srcIndexType, long indicesOffset, int eboId) {
if (indicesCount == 0) return null;
if (eboId == 0) {
warnOnce("no-ebo", "[IndexedDrawCapture] glDrawElements in display list with no EBO bound — skipping (mode={} count={} offset={})", mode, indicesCount, indicesOffset);
return null;
}
if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) {
warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 — skipping", indicesCount);
return null;
}
final int srcIndexSize = VertexAttribState.Attrib.glTypeSizeBytes(srcIndexType);
if (srcIndexSize <= 0) return null;
final int prevEBO = GLStateManager.getBoundEBO();
final int prevVBO = GLStateManager.getBoundVBO();
ByteBuffer eboSrc = null;
AttribSnapshot snap = null;
ByteBuffer vertexData = null;
ByteBuffer indexData = null;
boolean success = false;
try {
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
final int eboBufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
if (eboBufferSize <= 0) {
warnOnce("ebo-size", "[IndexedDrawCapture] GL_BUFFER_SIZE={} for EBO {}", eboBufferSize, eboId);
return null;
}
final long eboReadSize = (long) indicesCount * srcIndexSize;
if (indicesOffset < 0 || indicesOffset + eboReadSize > eboBufferSize) {
warnOnce("ebo-range", "[IndexedDrawCapture] EBO range {}+{} exceeds buffer size {} — skipping", indicesOffset, eboReadSize, eboBufferSize);
return null;
}
eboSrc = MemoryUtilities.memAlloc((int) eboReadSize);
GLStateManager.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesOffset, eboSrc);
final long eboSrcAddr = MemoryUtilities.memAddress0(eboSrc);
final long minMax = QuadConverter.scanMinMaxIndex(eboSrcAddr, srcIndexType, indicesCount);
if (minMax == -1L) {
warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType);
return null;
}
final int minVtx = (int) (minMax & 0xFFFFFFFFL);
final int maxVtx = (int) (minMax >>> 32);
final int vertexCount = maxVtx - minVtx + 1;
snap = AttribSnapshot.snapshot(minVtx, vertexCount);
int enabledCount = 0;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
if (snap.get(i) != null) enabledCount++;
}
if (enabledCount == 0) {
warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping");
return null;
}
final int[] locations = new int[enabledCount];
final int[] sizes = new int[enabledCount];
final int[] types = new int[enabledCount];
final boolean[] normalized = new boolean[enabledCount];
int k = 0;
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
final AttribSnapshot.AttribDesc d = snap.get(i);
if (d == null) continue;
locations[k] = i;
sizes[k] = d.size();
types[k] = d.type();
normalized[k] = d.normalized();
k++;
}
final AttribLayoutKey layoutKey = new AttribLayoutKey(locations, sizes, types, normalized);
final int tightStride = layoutKey.stride();
ByteBuffer sharedSrcBuffer = null;
long sharedSrcBase = 0;
int sharedSrcStride = -1;
boolean tightSrc = true;
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int relOffset = (int) (d.offset() - d.readBufferBaseOffset());
if (a == 0) {
sharedSrcBuffer = d.readBuffer();
sharedSrcStride = d.effectiveStride();
sharedSrcBase = MemoryUtilities.memAddress0(sharedSrcBuffer) + relOffset + (long) minVtx * sharedSrcStride;
if (relOffset != layoutKey.offset(0)) tightSrc = false;
} else if (d.readBuffer() != sharedSrcBuffer || d.effectiveStride() != sharedSrcStride || relOffset != layoutKey.offset(a)) {
tightSrc = false;
}
}
if (tightSrc && sharedSrcStride != tightStride) tightSrc = false;
vertexData = MemoryUtilities.memAlloc(vertexCount * tightStride);
final long dstAddr = MemoryUtilities.memAddress0(vertexData);
if (tightSrc) {
MemoryUtilities.memCopy(sharedSrcBase, dstAddr, (long) vertexCount * tightStride);
} else {
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int attribBytes = d.size() * d.typeSizeBytes();
final int srcStride = d.effectiveStride();
final long srcStart = MemoryUtilities.memAddress0(d.readBuffer()) + (d.offset() - d.readBufferBaseOffset()) + (long) minVtx * srcStride;
final long dstStart = dstAddr + layoutKey.offset(a);
copyStrided(srcStart, srcStride, dstStart, tightStride, attribBytes, vertexCount);
}
}
final int bakedDrawMode;
final int bakedIndexCount;
if (mode == GL11.GL_QUADS) {
final int quadCount = indicesCount / 4;
bakedIndexCount = quadCount * 6;
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
QuadConverter.triangulateQuads(eboSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, quadCount, minVtx);
bakedDrawMode = GL11.GL_TRIANGLES;
} else {
bakedIndexCount = indicesCount;
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
QuadConverter.widenIndices(eboSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, indicesCount, minVtx);
bakedDrawMode = mode;
}
final BatchedIndexedDrawCmd placeholder = new BatchedIndexedDrawCmd();
final IndexedDrawCapture out = new IndexedDrawCapture(layoutKey, bakedDrawMode, vertexCount, bakedIndexCount, vertexData, indexData, placeholder);
success = true;
return out;
} finally {
if (eboSrc != null) MemoryUtilities.memFree(eboSrc);
if (snap != null) snap.free();
if (!success) {
if (vertexData != null) MemoryUtilities.memFree(vertexData);
if (indexData != null) MemoryUtilities.memFree(indexData);
}
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
}
/**
* Copy {@code attribBytes} per vertex from {@code src} (stride {@code srcStride}) to
* {@code dst} (stride {@code dstStride}) for {@code vertexCount} vertices.
*
* <p>Specializes on the common attribute sizes (4 / 8 / 12 / 16) so the hot path is
* one or two {@code memPut} calls per vertex instead of a JNI-ish {@code memCopy}
* dispatch. Falls through to {@code memCopy} for exotic sizes.
*/
private static void copyStrided(long src, int srcStride, long dst, int dstStride, int attribBytes, int vertexCount) {
switch (attribBytes) {
case 4 -> {
for (int v = 0; v < vertexCount; v++) {
memPutInt(dst, memGetInt(src));
src += srcStride; dst += dstStride;
}
}
case 8 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
src += srcStride; dst += dstStride;
}
}
case 12 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
memPutInt(dst + 8, memGetInt(src + 8));
src += srcStride; dst += dstStride;
}
}
case 16 -> {
for (int v = 0; v < vertexCount; v++) {
memPutLong(dst, memGetLong(src));
memPutLong(dst + 8, memGetLong(src + 8));
src += srcStride; dst += dstStride;
}
}
default -> {
for (int v = 0; v < vertexCount; v++) {
MemoryUtilities.memCopy(src, dst, attribBytes);
src += srcStride; dst += dstStride;
}
}
}
}
}
@@ -108,13 +108,6 @@ public class VertexAttribState {
clientSideEnabledCount = count;
}
public static boolean hasVBOBoundAttrib() {
for (int i = 0; i < MAX_ATTRIBS; i++) {
if (current[i].enabled && current[i].vboId != 0) return true;
}
return false;
}
/**
* Returns true if any currently enabled vertex attribute was registered without a VBO
* (i.e. as a client-side pointer). In core profile, such attribs are treated as null
@@ -167,6 +160,10 @@ public class VertexAttribState {
}
public float readComponent(ByteBuffer buf, int base, int component) {
return readComponent(type, normalized, buf, base, component);
}
public static float readComponent(int type, boolean normalized, ByteBuffer buf, int base, int component) {
return switch (type) {
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
@@ -40,7 +41,7 @@ public final class OrphanStreamingBuffer {
public void destroy() {
if (bufferId != 0) {
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
import org.embeddedt.embeddium.impl.gl.sync.GlFence;
@@ -52,7 +53,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (this.mappedBuffer == null) {
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
throw new RuntimeException("Failed to persistently map streaming buffer");
}
@@ -147,7 +148,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
RENDER_BACKEND.unmapBuffer(GL15.GL_ARRAY_BUFFER);
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
RENDER_BACKEND.deleteBuffers(bufferId);
GLStateManager.glDeleteBuffers(bufferId);
bufferId = 0;
}
}
@@ -0,0 +1,256 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class QuadConverterTest {
@Test
void triangulateQuads_ubyteToUint_lastVertexConvention() {
// 2 quads: [0,1,2,3] and [4,5,6,7]. Expected LAST-convention output:
// quad0 → (0,1,3), (1,2,3)
// quad1 → (4,5,7), (5,6,7)
final ByteBuffer src = MemoryUtilities.memAlloc(8);
for (int i = 0; i < 8; i++) src.put(i, (byte) i);
final ByteBuffer dst = MemoryUtilities.memAlloc(12 * 4);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_INT, 2);
assertUintIndices(dst, 0, 1, 3, 1, 2, 3, 4, 5, 7, 5, 6, 7);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_ushortToUshort_preservesType() {
// USHORT in, USHORT out — same-size path for live draws.
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 2);
for (int i = 0; i < 4; i++) src.putShort(i * 2, (short) (i + 100));
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 2);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, 1);
assertUshortIndices(dst, 100, 101, 103, 101, 102, 103);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_ushortToUint_widens() {
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 2);
for (int i = 0; i < 4; i++) src.putShort(i * 2, (short) (0xFF00 | i));
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 4);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1);
// Zero-extended USHORT→UINT, LAST winding.
assertUintIndices(dst, 0xFF00, 0xFF01, 0xFF03, 0xFF01, 0xFF02, 0xFF03);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_uintToUint_identity() {
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 4);
for (int i = 0; i < 4; i++) src.putInt(i * 4, 1_000_000 + i);
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 4);
try {
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1);
assertUintIndices(dst, 1_000_000, 1_000_001, 1_000_003, 1_000_001, 1_000_002, 1_000_003);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_zeroQuads_noop() {
QuadConverter.triangulateQuads(0L, GL11.GL_UNSIGNED_INT, 0L, GL11.GL_UNSIGNED_INT, 0);
}
@Test
void triangulateQuads_unsupportedTypes_throw() {
final ByteBuffer src = MemoryUtilities.memAlloc(16);
final ByteBuffer dst = MemoryUtilities.memAlloc(24);
try {
assertThrows(IllegalArgumentException.class, () -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_FLOAT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1));
assertThrows(IllegalArgumentException.class, () -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 1));
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void widenIndices_ubyteToUint() {
final ByteBuffer src = MemoryUtilities.memAlloc(5);
for (int i = 0; i < 5; i++) src.put(i, (byte) (200 + i));
final ByteBuffer dst = MemoryUtilities.memAlloc(5 * 4);
try {
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_INT, 5);
assertUintIndices(dst, 200, 201, 202, 203, 204);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void widenIndices_ushortToUint_zeroExtends() {
final ByteBuffer src = MemoryUtilities.memAlloc(3 * 2);
src.putShort(0, (short) 0xFFFF);
src.putShort(2, (short) 0x8001);
src.putShort(4, (short) 0);
final ByteBuffer dst = MemoryUtilities.memAlloc(3 * 4);
try {
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 3);
assertUintIndices(dst, 0xFFFF, 0x8001, 0);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void widenIndices_uintToUint_copiesBitsExactly() {
final ByteBuffer src = MemoryUtilities.memAlloc(3 * 4);
src.putInt(0, 0x01234567);
src.putInt(4, 0xDEADBEEF);
src.putInt(8, 0);
final ByteBuffer dst = MemoryUtilities.memAlloc(3 * 4);
try {
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 3);
assertUintIndices(dst, 0x01234567, 0xDEADBEEF, 0);
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void scanMinMaxIndex_ushort_typical() {
final ByteBuffer src = MemoryUtilities.memAlloc(6 * 2);
final int[] data = {42, 7, 1000, 99, 500, 3};
for (int i = 0; i < data.length; i++) src.putShort(i * 2, (short) data[i]);
try {
final long packed = QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_SHORT, data.length);
final int min = (int) (packed & 0xFFFFFFFFL);
final int max = (int) (packed >>> 32);
assertEquals(3, min);
assertEquals(1000, max);
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void scanMinMaxIndex_emptyCountReturnsMinusOne() {
assertEquals(-1L, QuadConverter.scanMinMaxIndex(0L, GL11.GL_UNSIGNED_SHORT, 0));
}
@Test
void scanMinMaxIndex_unsupportedTypeReturnsMinusOne() {
final ByteBuffer src = MemoryUtilities.memAlloc(4);
try {
assertEquals(-1L, QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_FLOAT, 1));
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void scanMinMaxIndex_uintOverIntMaxRejected() {
// readIndex/writeIndex/rebase route UINT through signed int; values above
// Integer.MAX_VALUE would silently wrap. scanMinMaxIndex must bail.
final ByteBuffer src = MemoryUtilities.memAlloc(2 * 4);
src.putInt(0, 10);
src.putInt(4, 0x80000000); // 2^31
try {
assertEquals(-1L, QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_INT, 2));
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void scanMinMaxIndex_uintUpToIntMaxAccepted() {
final ByteBuffer src = MemoryUtilities.memAlloc(2 * 4);
src.putInt(0, 5);
src.putInt(4, Integer.MAX_VALUE);
try {
final long packed = QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_INT, 2);
assertEquals(5, (int) (packed & 0xFFFFFFFFL));
assertEquals(Integer.MAX_VALUE, (int) (packed >>> 32));
} finally {
MemoryUtilities.memFree(src);
}
}
@Test
void widenIndices_ubyteDstRejected() {
// UBYTE is not a supported destination — widening only.
final ByteBuffer src = MemoryUtilities.memAlloc(2);
src.put(0, (byte) 1).put(1, (byte) 2);
final ByteBuffer dst = MemoryUtilities.memAlloc(2);
try {
assertThrows(IllegalArgumentException.class,
() -> QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 2));
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
@Test
void triangulateQuads_ubyteDstRejected() {
final ByteBuffer src = MemoryUtilities.memAlloc(4);
for (int i = 0; i < 4; i++) src.put(i, (byte) i);
final ByteBuffer dst = MemoryUtilities.memAlloc(6);
try {
assertThrows(IllegalArgumentException.class,
() -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 1));
} finally {
MemoryUtilities.memFree(src);
MemoryUtilities.memFree(dst);
}
}
// === helpers ===
private static void assertUintIndices(ByteBuffer buf, int... expected) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], buf.getInt(i * 4), "uint index[" + i + "]");
}
}
private static void assertUshortIndices(ByteBuffer buf, int... expected) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], buf.getShort(i * 2) & 0xFFFF, "ushort index[" + i + "]");
}
}
}
@@ -0,0 +1,79 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.opengl.SharedDrawable;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
class SharedDrawableVaoTest {
private static final String VERT_330 = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
private static final String FRAG_330 = "#version 330 core\nout vec4 o;\nvoid main() { o = vec4(0.0); }\n";
@BeforeAll
static void createContext() throws LWJGLException {
assumeTrue(!Display.isCreated(), "inherited Display -- skipping to avoid profile drift");
Display.setDisplayModeAndFullscreen(new DisplayMode(800, 600));
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(4, 1).withProfileCore(true).withForwardCompatible(true));
GL30.glBindVertexArray(GL30.glGenVertexArrays());
}
@AfterAll
static void destroyContext() {
if (Display.isCreated()) Display.destroy();
}
@Test
void validateProgramSucceedsOnSharedDrawableWithVao() throws LWJGLException {
SharedDrawable shared = new SharedDrawable(Display.getDrawable());
Display.getDrawable().releaseContext();
shared.makeCurrent();
try {
GL30.glBindVertexArray(GL30.glGenVertexArrays());
int program = linkTrivialProgram();
GL20.glValidateProgram(program);
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_VALIDATE_STATUS), () -> GL20.glGetProgramInfoLog(program, 4096));
GL20.glDeleteProgram(program);
} finally {
shared.releaseContext();
Display.getDrawable().makeCurrent();
shared.destroy();
}
}
private static int linkTrivialProgram() {
int vs = compile(GL20.GL_VERTEX_SHADER, VERT_330);
int fs = compile(GL20.GL_FRAGMENT_SHADER, FRAG_330);
int program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
GL20.glAttachShader(program, fs);
GL20.glLinkProgram(program);
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_LINK_STATUS), () -> GL20.glGetProgramInfoLog(program, 4096));
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
return program;
}
private static int compile(int type, String src) {
int shader = GL20.glCreateShader(type);
GL20.glShaderSource(shader, src);
GL20.glCompileShader(shader);
assertEquals(GL11.GL_TRUE, GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS), () -> GL20.glGetShaderInfoLog(shader, 4096));
return shader;
}
}
@@ -0,0 +1,72 @@
package com.gtnewhorizons.angelica.glsm.architecture;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
class DeletePathConventionTest {
private static final Pattern FORBIDDEN = Pattern.compile("\\bRENDER_BACKEND\\s*\\.\\s*delete[A-Z][A-Za-z]*\\b");
private static final String ALLOWLIST_MARKER = "// glsm-allow-direct-delete:";
/** Only this file is allowed to call RENDER_BACKEND.delete* — it is the cache-invalidation layer. */
private static final String GATEKEEPER_FILE_NAME = "GLStateManager.java";
@Test
void glsmMainSources_onlyCallBackendDeleteFromGLStateManager() throws IOException {
final Path root = locateGlsmMainRoot();
final List<String> violations = new ArrayList<>();
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.filter(p -> !p.getFileName().toString().equals(GATEKEEPER_FILE_NAME))
.forEach(p -> scanFile(p, violations));
}
assertTrue(violations.isEmpty(),
() -> "Direct RENDER_BACKEND.delete*(...) call found outside " + GATEKEEPER_FILE_NAME + ".\n"
+ "Use GLStateManager.glDelete*() instead\n"
+ "Violations:\n " + String.join("\n ", violations));
}
private static void scanFile(Path file, List<String> violations) {
final List<String> lines;
try {
lines = Files.readAllLines(file, StandardCharsets.UTF_8);
} catch (IOException e) {
violations.add(file + ": I/O error reading file: " + e.getMessage());
return;
}
for (int i = 0; i < lines.size(); i++) {
final String line = lines.get(i);
final Matcher m = FORBIDDEN.matcher(line);
if (!m.find()) continue;
if (line.contains(ALLOWLIST_MARKER)) continue;
violations.add(file + ":" + (i + 1) + " " + line.trim());
}
}
private static Path locateGlsmMainRoot() {
final String[] candidates = {
"src/main/java/com/gtnewhorizons/angelica/glsm",
"glsm/src/main/java/com/gtnewhorizons/angelica/glsm",
};
for (String c : candidates) {
final Path p = Paths.get(c).toAbsolutePath();
if (Files.isDirectory(p)) return p;
}
throw new IllegalStateException("Could not find glsm main sources — cwd=" + Paths.get("").toAbsolutePath());
}
}
@@ -0,0 +1,531 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
@Test
void singleDraw_bakedSurvivesVboMutation() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
assertEquals(6, cmd.getIndexCount());
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "batched indices always widen to GL_UNSIGNED_INT");
// Overwrite source VBO with garbage — baked batch must be unaffected.
overwriteBufferWithGarbage(GL15.GL_ARRAY_BUFFER, vbo, 4 * POS_COLOR_STRIDE);
final ByteBuffer read = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), 4 * POS_COLOR_STRIDE);
for (int i = 0; i < 4; i++) {
assertEquals(DEFAULT_QUAD_POSITIONS[i * 2], read.getFloat(i * POS_COLOR_STRIDE), 0f, "pos.x v=" + i);
assertEquals(DEFAULT_QUAD_POSITIONS[i * 2 + 1], read.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "pos.y v=" + i);
for (int c = 0; c < 4; c++) {
assertEquals(DEFAULT_SOLID_COLORS[i][c], read.get(i * POS_COLOR_STRIDE + 8 + c), "color v=" + i + " c=" + c);
}
}
MemoryUtilities.memFree(read);
}
@Test
void compileAndExecute_firesLiveAndBakes() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(3, cmd.getIndexCount());
}
@Test
void quads_triangulateAtRecord() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
assertEquals(6, cmd.getIndexCount());
// EBO is widened to UNSIGNED_INT and uses GL_LAST_VERTEX_CONVENTION:
// (a,b,d,b,c,d) — v3 is the provoking vertex for both triangles, diagonal b→d.
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, firstBatch(list).getSharedEBO(), 6 * 4);
assertUintIndices(ib, 0, 1, 3, 1, 2, 3);
MemoryUtilities.memFree(ib);
}
@Test
void offsetAndMinMaxRebase() {
// 8-vertex VBO; draw references only vertices 5..7 via a 6L byte offset into the EBO.
final float[] positions = new float[16];
final byte[][] colors = new byte[8][4];
for (int i = 0; i < 8; i++) {
positions[i * 2] = i;
positions[i * 2 + 1] = i * 2;
colors[i][0] = (byte) i;
colors[i][1] = (byte) (i + 1);
colors[i][2] = (byte) (i + 2);
colors[i][3] = (byte) (i + 3);
}
final int vbo = uploadPosColorVbo(positions, colors);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 5, 6, 7);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 6L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertEquals(1, allBatchedCmds(list).size());
final IndexedDrawBatch batch = firstBatch(list);
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, batch.getSharedEBO(), 3 * 4);
// Indices rebased against the tight [minVtx..maxVtx] range.
assertUintIndices(ib, 0, 1, 2);
MemoryUtilities.memFree(ib);
final ByteBuffer vboRead = readBufferBytes(GL15.GL_ARRAY_BUFFER, batch.getSharedVBO(), 3 * POS_COLOR_STRIDE);
for (int i = 0; i < 3; i++) {
final int orig = 5 + i;
assertEquals(positions[orig * 2], vboRead.getFloat(i * POS_COLOR_STRIDE), 0f, "pos.x v=" + orig);
assertEquals(positions[orig * 2 + 1], vboRead.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "pos.y v=" + orig);
for (int c = 0; c < 4; c++) {
assertEquals(colors[orig][c], vboRead.get(i * POS_COLOR_STRIDE + 8 + c), "col v=" + orig + " c=" + c);
}
}
MemoryUtilities.memFree(vboRead);
}
@ParameterizedTest(name = "indexType=0x{0}")
@ValueSource(ints = {GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_SHORT, GL11.GL_UNSIGNED_INT})
void anyIndexType_widensToUnsignedInt(int indexType) {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(indexType, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, indexType);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(6, cmd.getIndexCount());
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "batching always widens indices to UINT for safe base-vertex shifting");
}
@Test
void emptyRecorder_producesNoBatches() {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glEndList();
assertTrue(batchesOf(list).isEmpty(), "recorder with no draws produces no batches");
assertTrue(allBatchedCmds(list).isEmpty(), "no placeholders either");
}
@Test
void noEboBoundSkipsAndDoesNotBake() {
final int vbo = defaultQuadVbo();
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertTrue(allBatchedCmds(list).isEmpty(), "no EBO bound → no batched command attached");
assertTrue(batchesOf(list).isEmpty(), "no batches either");
}
@Test
void customLoc3FloatAttribRoundtrips() {
// 4-attrib layout: pos(2f) | uv(2f) | col(4ub) | texBounds(4f) = 36 bytes/vertex.
final int STRIDE = 36;
final int N = 4;
final float[][] pos = {{0f,0f},{1f,0f},{1f,1f},{0f,1f}};
final float[][] uv = {{0.1f,0.2f},{0.3f,0.4f},{0.5f,0.6f},{0.7f,0.8f}};
final byte[][] col = {{10,20,30,40},{50,60,70,(byte)80},{(byte)90,(byte)100,(byte)110,(byte)120},{(byte)130,(byte)140,(byte)150,(byte)160}};
final float[][] tb = {{1f,2f,3f,4f},{5f,6f,7f,8f},{9f,10f,11f,12f},{13f,14f,15f,16f}};
final int vbo = newBuffer();
final ByteBuffer data = MemoryUtilities.memAlloc(N * STRIDE).order(ByteOrder.nativeOrder());
for (int i = 0; i < N; i++) {
data.putFloat(pos[i][0]); data.putFloat(pos[i][1]);
data.putFloat(uv[i][0]); data.putFloat(uv[i][1]);
data.put(col[i][0]); data.put(col[i][1]); data.put(col[i][2]); data.put(col[i][3]);
data.putFloat(tb[i][0]); data.putFloat(tb[i][1]); data.putFloat(tb[i][2]); data.putFloat(tb[i][3]);
}
data.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(data);
final int vao = newVao();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, STRIDE, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, STRIDE, 8);
GLStateManager.glEnableVertexAttribArray(1);
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 16);
GLStateManager.glEnableVertexAttribArray(2);
GLStateManager.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, STRIDE, 20);
GLStateManager.glEnableVertexAttribArray(3);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
assertNotNull(firstBatchedCmd(list), "loc3 attrib should not cause the baker to skip");
// Baked layout is tight interleaved in ascending-location order — same stride as source.
final ByteBuffer read = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), N * STRIDE);
for (int i = 0; i < N; i++) {
final int base = i * STRIDE;
assertEquals(pos[i][0], read.getFloat(base), 0f, "pos.x v=" + i);
assertEquals(pos[i][1], read.getFloat(base + 4), 0f, "pos.y v=" + i);
assertEquals(uv[i][0], read.getFloat(base + 8), 0f, "uv.s v=" + i);
assertEquals(uv[i][1], read.getFloat(base + 12), 0f, "uv.t v=" + i);
for (int c = 0; c < 4; c++) {
assertEquals(col[i][c], read.get(base + 16 + c), "col v=" + i + " c=" + c);
}
for (int c = 0; c < 4; c++) {
assertEquals(tb[i][c], read.getFloat(base + 20 + c * 4), 0f, "tb v=" + i + " c=" + c);
}
}
MemoryUtilities.memFree(read);
}
@Test
void glDeleteLists_freesBatchedObjects() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
final IndexedDrawBatch batch = firstBatch(list);
final int sharedVAO = batch.getSharedVAO();
final int sharedVBO = batch.getSharedVBO();
final int sharedEBO = batch.getSharedEBO();
assertTrue(GL15.glIsBuffer(sharedVBO), "shared VBO live");
assertTrue(GL15.glIsBuffer(sharedEBO), "shared EBO live");
assertTrue(GL30.glIsVertexArray(sharedVAO), "shared VAO live");
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
assertFalse(GL15.glIsBuffer(sharedVBO), "shared VBO freed");
assertFalse(GL15.glIsBuffer(sharedEBO), "shared EBO freed");
assertFalse(GL30.glIsVertexArray(sharedVAO), "shared VAO freed");
}
@Test
void reRecord_freesOldBatchedObjects() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
final IndexedDrawBatch first = firstBatch(list);
final int oldVAO = first.getSharedVAO();
final int oldVBO = first.getSharedVBO();
final int oldEBO = first.getSharedEBO();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertFalse(GL15.glIsBuffer(oldVBO), "old shared VBO freed on re-record");
assertFalse(GL15.glIsBuffer(oldEBO), "old shared EBO freed on re-record");
assertFalse(GL30.glIsVertexArray(oldVAO), "old shared VAO freed on re-record");
}
@Test
void clientPointerAttribsBakeIntoSharedVbo() {
final int N = 4;
final ByteBuffer clientData = BufferUtils.createByteBuffer(N * POS_COLOR_STRIDE).order(ByteOrder.nativeOrder());
final float[][] pos = {{0f,0f},{1f,0f},{1f,1f},{0f,1f}};
final byte[][] col = {{(byte)0xAA,(byte)0xBB,(byte)0xCC,(byte)0xDD},
{(byte)0x11,(byte)0x22,(byte)0x33,(byte)0x44},
{(byte)0x55,(byte)0x66,(byte)0x77,(byte)0x88},
{(byte)0x99,(byte)0x00,(byte)0x0F,(byte)0xF0}};
for (int i = 0; i < N; i++) {
clientData.putFloat(pos[i][0]); clientData.putFloat(pos[i][1]);
clientData.put(col[i][0]); clientData.put(col[i][1]);
clientData.put(col[i][2]); clientData.put(col[i][3]);
}
clientData.flip();
final int vao = newVao();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
final ByteBuffer colorSlice = clientData.duplicate().order(ByteOrder.nativeOrder());
colorSlice.position(8);
VertexAttribState.set(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, clientData, 0);
VertexAttribState.setEnabled(0, true);
VertexAttribState.set(1, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, colorSlice, 0);
VertexAttribState.setEnabled(1, true);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
assertNotNull(firstBatchedCmd(list));
// Tight interleaved baked layout for 3 drawn vertices = 36 bytes total.
final ByteBuffer owned = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), 3 * POS_COLOR_STRIDE);
for (int i = 0; i < 3; i++) {
assertEquals(pos[i][0], owned.getFloat(i * POS_COLOR_STRIDE), 0f, "loc0 pos.x v=" + i);
assertEquals(pos[i][1], owned.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "loc0 pos.y v=" + i);
for (int c = 0; c < 4; c++) {
assertEquals(col[i][c], owned.get(i * POS_COLOR_STRIDE + 8 + c), "loc1 color v=" + i + " c=" + c);
}
}
MemoryUtilities.memFree(owned);
}
@Test
void multiDrawSameFormat_mergesIntoSingleBatch() {
final int vbo = defaultQuadVbo();
final int eboA = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int eboB = uploadEbo(GL11.GL_UNSIGNED_SHORT, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboA);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboB);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size(), "two indexed draws → two batched commands");
final List<IndexedDrawBatch> batches = batchesOf(list);
assertEquals(1, batches.size(), "same layout → one shared batch");
assertEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "shared VAO across draws in the same batch");
assertEquals(batches.getFirst().getSharedVAO(), cmds.get(0).getSharedVAO());
assertEquals(0L, cmds.get(0).getIndexByteOffset());
assertEquals(3 * 4L, cmds.get(1).getIndexByteOffset(), "second draw offset = first.indexCount * sizeof(UINT)");
// Shared EBO holds concatenated + rebased UINT indices. First draw minVtx=0 → [0,1,2];
// second draw minVtx=0, vertexBase=3 → [2+3, 3+3, 0+3] = [5, 6, 3].
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, batches.getFirst().getSharedEBO(), 6 * 4);
assertUintIndices(ib, 0, 1, 2, 5, 6, 3);
MemoryUtilities.memFree(ib);
}
@Test
void multiDrawDifferentFormats_splitBatches() {
// Draw A: pos+color (stride=12). Draw B: pos-only (stride=8). Different layout → two batches.
final int vboA = defaultQuadVbo();
final int vaoA = setupPosColorVao(vboA);
final int vboB = newBuffer();
final ByteBuffer bData = MemoryUtilities.memAlloc(4 * 8).order(ByteOrder.nativeOrder());
for (int i = 0; i < 4; i++) {
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2]);
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2 + 1]);
}
bData.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, bData, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(bData);
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 8, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
assertEquals(2, batchesOf(list).size(), "incompatible layouts → two batches");
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
}
@Test
void sameSizesDifferentLocations_splitIntoSeparateBatches() {
// Both VAOs: pos 2f + color 4ub (same sizes/types/normalized). Only the enabled
// location sets differ — VAO-A uses {0,1}, VAO-B uses {0,2}. If AttribLayoutKey
// forgot about locations they'd merge and VAO-B's color data would get wired to
// loc 1 instead of loc 2.
final int vbo = defaultQuadVbo();
final int vaoA = setupPosColorVao(vbo);
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, 8);
GLStateManager.glEnableVertexAttribArray(2);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
assertEquals(2, batchesOf(list).size(), "different enabled locations → two batches");
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
}
@Test
void nullListProbeCompiles() {
assertNull(DisplayListManager.getDisplayList(0));
}
@Test
void multiLayoutDraws_emitInInsertionOrder() {
// Two distinct layouts. The builder uses a LinkedHashMap, so the first
// recorded layout must always be the first batch out.
final int vboA = defaultQuadVbo();
final int vaoA = setupPosColorVao(vboA);
final int vboB = newBuffer();
final ByteBuffer bData = MemoryUtilities.memAlloc(4 * 8).order(ByteOrder.nativeOrder());
for (int i = 0; i < 4; i++) {
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2]);
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2 + 1]);
}
bData.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, bData, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(bData);
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 8, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
// Record (A, B) and (B, A) as independent lists; each list's batch order
// must match its own recording order.
final int listAB = newList();
GLStateManager.glNewList(listAB, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final int listBA = newList();
GLStateManager.glNewList(listBA, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> ab = allBatchedCmds(listAB);
final List<BatchedIndexedDrawCmd> ba = allBatchedCmds(listBA);
// Placeholders sit in command-stream order (A-first vs B-first).
// Batches must be ordered so that placeholder[0] points at batch[0].
assertEquals(ab.get(0).getSharedVAO(), batchesOf(listAB).get(0).getSharedVAO(),
"listAB: batch[0] must match the first-recorded layout");
assertEquals(ab.get(1).getSharedVAO(), batchesOf(listAB).get(1).getSharedVAO(),
"listAB: batch[1] must match the second-recorded layout");
assertEquals(ba.get(0).getSharedVAO(), batchesOf(listBA).get(0).getSharedVAO(),
"listBA: batch[0] must match the first-recorded layout");
assertEquals(ba.get(1).getSharedVAO(), batchesOf(listBA).get(1).getSharedVAO(),
"listBA: batch[1] must match the second-recorded layout");
}
// === local helpers ===
private static void assertUintIndices(ByteBuffer ib, int... expected) {
for (int i = 0; i < expected.length; i++) {
assertEquals(expected[i], ib.getInt(i * 4), "index[" + i + "]");
}
}
}
@@ -0,0 +1,150 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import static org.junit.jupiter.api.Assertions.assertEquals;
class IndexedDrawDeleteSafetyTest extends DisplayListTestFixture {
@Test
void vaoEboMapSweep_clearsStaleEntryOnForeignVao() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final IndexedDrawBatch batch = firstBatch(list);
final int sharedEbo = batch.getSharedEBO();
final int foreignVao = newVao();
GLStateManager.glBindVertexArray(foreignVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
GLStateManager.glBindVertexArray(foreignVao);
assertEquals(0, GLStateManager.getBoundEBO(), "vaoEboMap still references the freed sharedEBO — non-gen crash imminent");
GLStateManager.glBindVertexArray(0);
}
@Test
void bindBufferCycle_afterListDelete_isGlNoError() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final int sharedEbo = firstBatch(list).getSharedEBO();
final int foreignVao = newVao();
GLStateManager.glBindVertexArray(foreignVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
GLStateManager.glBindVertexArray(foreignVao);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
final int prevEbo = GLStateManager.getBoundEBO();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err, () -> "bindBuffer(prevEbo=" + prevEbo + ") hit GL 0x" + Integer.toHexString(err) + " — cache carried a freed id through a VAO rebind");
GLStateManager.glBindVertexArray(0);
}
@Test
void multipleVaos_sharingOneEbo_allClearedOnEboDelete() {
// Two VAOs, same EBO. Deleting the EBO must clear vaoEboMap for both,
// not just whichever VAO happens to be currently bound.
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vaoA = newVao();
final int vaoB = newVao();
GLStateManager.glBindVertexArray(vaoA);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glBindVertexArray(vaoB);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteBuffers(ebo);
GLStateManager.glBindVertexArray(vaoA);
assertEquals(0, GLStateManager.getBoundEBO(), "vaoA still references freed EBO");
GLStateManager.glBindVertexArray(vaoB);
assertEquals(0, GLStateManager.getBoundEBO(), "vaoB still references freed EBO");
GLStateManager.glBindVertexArray(0);
}
@Test
void glDeleteProgram_zero_isNoOpAndLeavesActiveProgram() {
final int prog = linkMinimalProgram();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GLStateManager.glUseProgram(prog);
assertEquals(prog, GLStateManager.getActiveProgram());
GLStateManager.glDeleteProgram(0);
assertEquals(prog, GLStateManager.getActiveProgram(), "glDeleteProgram(0) must be a silent no-op");
GLStateManager.glUseProgram(0);
GLStateManager.glDeleteProgram(prog);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
@Test
void activeProgram_clearedOnDelete() {
final int prog = linkMinimalProgram();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GLStateManager.glUseProgram(prog);
assertEquals(prog, GLStateManager.getActiveProgram());
GLStateManager.glDeleteProgram(prog);
assertEquals(0, GLStateManager.getActiveProgram(), "activeProgram not cleared when the active program was deleted");
GLStateManager.glUseProgram(0);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
private static int linkMinimalProgram() {
final int vert = GLStateManager.glCreateShader(GL20.GL_VERTEX_SHADER);
GLStateManager.glShaderSource(vert, MIN_VERT_SRC);
GLStateManager.glCompileShader(vert);
if (GL20.glGetShaderi(vert, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
throw new AssertionError("vertex shader failed to compile: " + GL20.glGetShaderInfoLog(vert, 4096));
}
final int frag = GLStateManager.glCreateShader(GL20.GL_FRAGMENT_SHADER);
GLStateManager.glShaderSource(frag, MIN_FRAG_SRC);
GLStateManager.glCompileShader(frag);
if (GL20.glGetShaderi(frag, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
throw new AssertionError("fragment shader failed to compile: " + GL20.glGetShaderInfoLog(frag, 4096));
}
final int prog = GLStateManager.glCreateProgram();
GLStateManager.glAttachShader(prog, vert);
GLStateManager.glAttachShader(prog, frag);
GLStateManager.glLinkProgram(prog);
if (GL20.glGetProgrami(prog, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
throw new AssertionError("program failed to link: " + GL20.glGetProgramInfoLog(prog, 4096));
}
GLStateManager.glDetachShader(prog, vert);
GLStateManager.glDetachShader(prog, frag);
GLStateManager.glDeleteShader(vert);
GLStateManager.glDeleteShader(frag);
return prog;
}
private static final String MIN_VERT_SRC = "#version 330 core\nvoid main(){gl_Position=vec4(0.0);}";
private static final String MIN_FRAG_SRC = "#version 330 core\nout vec4 oC;void main(){oC=vec4(1.0);}";
}
@@ -0,0 +1,243 @@
package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.commands.AttribLayoutKey;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class IndexedDrawReplayTest extends DisplayListTestFixture {
@Test
void compileAndExecute_firesLiveAndBakes() {
// COMPILE_AND_EXECUTE must do two things: (1) run the draw immediately,
// (2) record a placeholder so the list can replay it later. A samples-passed
// query wrapping the record block confirms the live half; asserting the
// placeholder post-endList confirms the baked half.
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
GL11.glViewport(0, 0, 16, 16);
GL11.glDisable(GL11.GL_DEPTH_TEST);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
final int query = GL15.glGenQueries();
GL15.glBeginQuery(GL15.GL_SAMPLES_PASSED, query);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
GL15.glEndQuery(GL15.GL_SAMPLES_PASSED);
final int samples = GL15.glGetQueryObjecti(query, GL15.GL_QUERY_RESULT);
GL15.glDeleteQueries(query);
assertTrue(samples > 0, "COMPILE_AND_EXECUTE did not fire the live draw (samples=" + samples + ")");
final BatchedIndexedDrawCmd baked = firstBatchedCmd(list);
assertNotNull(baked, "COMPILE_AND_EXECUTE did not bake a placeholder");
assertEquals(6, baked.getIndexCount());
}
@Test
void sharedVaoRetainsItsEbo() {
final int vbo = defaultQuadVbo();
final int callerEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, callerEbo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
final BatchedIndexedDrawCmd batched = firstBatchedCmd(list);
assertNotNull(batched);
final IndexedDrawBatch batch = firstBatch(list);
GLStateManager.glBindVertexArray(batched.getSharedVAO());
assertEquals(batch.getSharedEBO(), GLStateManager.getBoundEBO(),
"batched VAO's EBO binding corrupted");
GLStateManager.glBindVertexArray(0);
}
@Test
void replayAfterSourceMutationAndStateChange() {
final int vbo = defaultQuadVbo();
final int callerEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, callerEbo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
overwriteBufferWithGarbage(GL15.GL_ARRAY_BUFFER, vbo, 4 * POS_COLOR_STRIDE);
overwriteBufferWithGarbage(GL15.GL_ELEMENT_ARRAY_BUFFER, callerEbo, 6 * 2);
// Bind unrelated state so the baked command has to restore its own bindings.
final int distractorVao = newVao();
GLStateManager.glBindVertexArray(distractorVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
compiledOf(list).render();
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err,
() -> "replay produced GL error 0x" + Integer.toHexString(err));
GLStateManager.glBindVertexArray(0);
}
@Test
void replayThenDelete_thenBindBufferCycle_isGlNoError() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
compiledOf(list).render();
final int sharedEbo = firstBatch(list).getSharedEBO();
final int foreignVao = newVao();
GLStateManager.glBindVertexArray(foreignVao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
GLStateManager.glBindVertexArray(0);
GLStateManager.glDeleteLists(list, 1);
untrackList(list);
GLStateManager.glBindVertexArray(foreignVao);
assertEquals(0, GLStateManager.getBoundEBO(), "batched EBO id survived list delete in vaoEboMap — non-gen crash imminent");
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
final int prevEbo = GLStateManager.getBoundEBO();
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err, () -> "post-delete bindBuffer(" + prevEbo + ") hit GL 0x" + Integer.toHexString(err));
GLStateManager.glBindVertexArray(0);
}
@Test
void execute_doesNotRestoreVAO_backToBackDraws() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glEndList();
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
assertEquals(2, cmds.size());
final int sharedVAO = cmds.getFirst().getSharedVAO();
assertEquals(sharedVAO, cmds.get(1).getSharedVAO());
final int distractor = newVao();
GLStateManager.glBindVertexArray(distractor);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
compiledOf(list).render();
assertEquals(sharedVAO, GLStateManager.getBoundVAO());
GLStateManager.glBindVertexArray(0);
}
@Test
void build_partialFailure_deletesEarlierBatchesAndLeavesPlaceholdersUnfilled() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
final IndexedDrawCapture good = IndexedDrawCapture.create(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L, ebo);
assertNotNull(good);
// Synthetic capture with a different layout that trips the >2GiB size guard.
final AttribLayoutKey otherLayout = new AttribLayoutKey(
new int[]{2}, new int[]{4}, new int[]{GL11.GL_FLOAT}, new boolean[]{false});
final ByteBuffer tinyVtx = MemoryUtilities.memAlloc(16);
final ByteBuffer tinyIdx = MemoryUtilities.memAlloc(16);
final IndexedDrawCapture huge = new IndexedDrawCapture(
otherLayout, GL11.GL_TRIANGLES,
Integer.MAX_VALUE / 4, 1, tinyVtx, tinyIdx,
new BatchedIndexedDrawCmd());
final IndexedDrawBatchBuilder builder = new IndexedDrawBatchBuilder();
builder.add(good);
builder.add(huge);
try {
assertThrows(IllegalStateException.class, builder::build);
assertEquals(0, good.placeholder.getSharedVAO());
assertEquals(0, huge.placeholder.getSharedVAO());
} finally {
MemoryUtilities.memFree(tinyVtx);
MemoryUtilities.memFree(tinyIdx);
good.freeBuffers();
GLStateManager.glBindVertexArray(0);
}
}
@Test
void recorderFree_releasesUnfilledCaptureNativeBuffers() {
final CommandRecorder rec = new CommandRecorder();
final ByteBuffer vtx = MemoryUtilities.memAlloc(64);
final ByteBuffer idx = MemoryUtilities.memAlloc(32);
final IndexedDrawCapture cap = new IndexedDrawCapture(
new AttribLayoutKey(new int[]{0}, new int[]{2}, new int[]{GL11.GL_FLOAT}, new boolean[]{false}),
GL11.GL_TRIANGLES, 4, 6, vtx, idx, new BatchedIndexedDrawCmd());
rec.recordIndexedDrawCapture(cap);
assertFalse(cap.isFreed());
rec.free();
assertTrue(cap.isFreed());
cap.freeBuffers();
}
@Test
void execute_idempotentReplay_doesNotErrorOnSecondPass() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glEndList();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
compiledOf(list).render();
compiledOf(list).render();
final int err = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, err,
() -> "double replay produced GL error 0x" + Integer.toHexString(err));
GLStateManager.glBindVertexArray(0);
}
}
@@ -0,0 +1,95 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class AttribLayoutKeyTest {
private static int[] offsetsOf(AttribLayoutKey k) {
final int[] out = new int[k.locations.length];
for (int i = 0; i < out.length; i++) out[i] = k.offset(i);
return out;
}
@Test
void standardPosColorUvNormal_isAlignedAndPacked() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 1, 2, 4},
new int[]{3, 4, 2, 3},
new int[]{GL11.GL_FLOAT, GL11.GL_UNSIGNED_BYTE, GL11.GL_FLOAT, GL11.GL_BYTE},
new boolean[]{false, true, false, true});
assertArrayEquals(new int[]{0, 12, 16, 24}, offsetsOf(k));
assertEquals(28, k.stride());
}
@Test
void posUvNormal3_roundsTailStrideTo24() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 2, 4},
new int[]{3, 2, 3},
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT, GL11.GL_BYTE},
new boolean[]{false, false, true});
assertArrayEquals(new int[]{0, 12, 20}, offsetsOf(k));
assertEquals(24, k.stride());
}
@Test
void interiorHazardOrder_roundsMidAttribOffset() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 4, 2},
new int[]{3, 3, 2},
new int[]{GL11.GL_FLOAT, GL11.GL_BYTE, GL11.GL_FLOAT},
new boolean[]{false, true, false});
assertArrayEquals(new int[]{0, 12, 16}, offsetsOf(k));
assertEquals(24, k.stride());
}
@Test
void allByteLayout_packsTight() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 1, 2},
new int[]{3, 4, 2},
new int[]{GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE, GL11.GL_BYTE},
new boolean[]{true, true, false});
assertArrayEquals(new int[]{0, 3, 7}, offsetsOf(k));
assertEquals(9, k.stride());
}
@Test
void shortsFollowingByte_roundToEvenAndStrideToEven() {
final AttribLayoutKey k = new AttribLayoutKey(
new int[]{0, 1},
new int[]{1, 2},
new int[]{GL11.GL_BYTE, GL11.GL_SHORT},
new boolean[]{true, false});
assertArrayEquals(new int[]{0, 2}, offsetsOf(k));
assertEquals(6, k.stride());
}
@Test
void equalInputs_produceEqualKeys() {
final AttribLayoutKey a = new AttribLayoutKey(
new int[]{0, 2}, new int[]{3, 2},
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT}, new boolean[]{false, false});
final AttribLayoutKey b = new AttribLayoutKey(
new int[]{0, 2}, new int[]{3, 2},
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT}, new boolean[]{false, false});
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
assertEquals(a.stride(), b.stride());
assertArrayEquals(offsetsOf(a), offsetsOf(b));
}
@Test
void differentNormalized_producesDifferentKeys() {
final AttribLayoutKey a = new AttribLayoutKey(
new int[]{0}, new int[]{4}, new int[]{GL11.GL_UNSIGNED_BYTE}, new boolean[]{true});
final AttribLayoutKey b = new AttribLayoutKey(
new int[]{0}, new int[]{4}, new int[]{GL11.GL_UNSIGNED_BYTE}, new boolean[]{false});
assertNotEquals(a, b);
}
}
@@ -0,0 +1,205 @@
package com.gtnewhorizons.angelica.glsm.recording.support;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(GLSMExtension.class)
public abstract class DisplayListTestFixture {
public static final float[] DEFAULT_QUAD_POSITIONS = {0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f};
public static final byte[][] DEFAULT_SOLID_COLORS = {
{1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}, {4, 4, 4, 4}
};
public static final int POS_COLOR_STRIDE = 12;
private final List<Integer> listsToCleanup = new ArrayList<>();
private final List<Integer> buffersToCleanup = new ArrayList<>();
private final List<Integer> vaosToCleanup = new ArrayList<>();
@AfterEach
void cleanupFixtureResources() {
for (int list : listsToCleanup) GLStateManager.glDeleteLists(list, 1);
for (int buf : buffersToCleanup) {
if (GL15.glIsBuffer(buf)) GL15.glDeleteBuffers(buf);
}
for (int vao : vaosToCleanup) {
if (GL30.glIsVertexArray(vao)) GLStateManager.glDeleteVertexArrays(vao);
}
listsToCleanup.clear();
buffersToCleanup.clear();
vaosToCleanup.clear();
}
protected final int newList() {
final int id = GL11.glGenLists(1);
listsToCleanup.add(id);
return id;
}
protected final void untrackList(int list) {
listsToCleanup.remove((Integer) list);
}
protected final int newBuffer() {
final int id = GL15.glGenBuffers();
buffersToCleanup.add(id);
return id;
}
protected final int newVao() {
final int id = GL30.glGenVertexArrays();
vaosToCleanup.add(id);
return id;
}
/** 2 floats pos + 4 bytes color, stride 12, one vertex per entry in {@code colors}. */
protected final int uploadPosColorVbo(float[] positions, byte[][] colors) {
if (positions.length / 2 != colors.length) {
throw new IllegalArgumentException("positions/colors length mismatch");
}
final int n = colors.length;
final int vbo = newBuffer();
final ByteBuffer data = MemoryUtilities.memAlloc(n * POS_COLOR_STRIDE).order(ByteOrder.nativeOrder());
for (int i = 0; i < n; i++) {
data.putFloat(positions[i * 2]);
data.putFloat(positions[i * 2 + 1]);
data.put(colors[i][0]);
data.put(colors[i][1]);
data.put(colors[i][2]);
data.put(colors[i][3]);
}
data.flip();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(data);
return vbo;
}
/** Pos+color VBO populated with {@link #DEFAULT_QUAD_POSITIONS} / {@link #DEFAULT_SOLID_COLORS}. */
protected final int defaultQuadVbo() {
return uploadPosColorVbo(DEFAULT_QUAD_POSITIONS, DEFAULT_SOLID_COLORS);
}
protected final int uploadEbo(int indexType, int... values) {
final int bytesPer = switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> 1;
case GL11.GL_UNSIGNED_SHORT -> 2;
case GL11.GL_UNSIGNED_INT -> 4;
default -> throw new IllegalArgumentException("unsupported indexType 0x" + Integer.toHexString(indexType));
};
final int ebo = newBuffer();
final ByteBuffer data = MemoryUtilities.memAlloc(values.length * bytesPer).order(ByteOrder.nativeOrder());
for (int v : values) {
switch (indexType) {
case GL11.GL_UNSIGNED_BYTE -> data.put((byte) v);
case GL11.GL_UNSIGNED_SHORT -> data.putShort((short) v);
case GL11.GL_UNSIGNED_INT -> data.putInt(v);
}
}
data.flip();
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
MemoryUtilities.memFree(data);
return ebo;
}
/** VAO with loc0 = 2*FLOAT pos, loc1 = 4*UNSIGNED_BYTE norm color, stride 12. */
protected final int setupPosColorVao(int vbo) {
final int vao = newVao();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, 0);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, 8);
GLStateManager.glEnableVertexAttribArray(1);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLStateManager.glBindVertexArray(0);
return vao;
}
/**
* Record one {@code glDrawElements} into a {@code GL_COMPILE} list with the given VAO and EBO
* already-bound setup. Returns the list id (tracked for cleanup).
*/
protected final int recordSingleIndexedDraw(int vao, int ebo, int drawMode, int indexCount, int indexType) {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLStateManager.glDrawElements(drawMode, indexCount, indexType, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
return list;
}
protected static CompiledDisplayList compiledOf(int listId) {
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(listId);
assertNotNull(compiled, "compiled display list must exist for id " + listId);
return compiled;
}
protected static List<BatchedIndexedDrawCmd> allBatchedCmds(int listId) {
final List<BatchedIndexedDrawCmd> out = new ArrayList<>();
final Object[] complex = compiledOf(listId).getComplexObjects();
if (complex == null) return out;
for (Object o : complex) {
if (o instanceof BatchedIndexedDrawCmd b) out.add(b);
}
return out;
}
protected static BatchedIndexedDrawCmd firstBatchedCmd(int listId) {
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(listId);
return cmds.isEmpty() ? null : cmds.getFirst();
}
protected static List<IndexedDrawBatch> batchesOf(int listId) {
return compiledOf(listId).getIndexedBatches();
}
protected static IndexedDrawBatch firstBatch(int listId) {
return batchesOf(listId).getFirst();
}
protected static ByteBuffer readBufferBytes(int target, int bufferId, int size) {
final int prev;
if (target == GL15.GL_ARRAY_BUFFER) prev = GLStateManager.getBoundVBO();
else if (target == GL15.GL_ELEMENT_ARRAY_BUFFER) prev = GLStateManager.getBoundEBO();
else prev = 0;
GLStateManager.glBindBuffer(target, bufferId);
final ByteBuffer buf = MemoryUtilities.memAlloc(size).order(ByteOrder.nativeOrder());
GL15.glGetBufferSubData(target, 0, buf);
GLStateManager.glBindBuffer(target, prev);
return buf;
}
protected static void overwriteBufferWithGarbage(int target, int bufferId, int size) {
final ByteBuffer garbage = MemoryUtilities.memAlloc(size);
for (int i = 0; i < garbage.capacity(); i++) garbage.put(i, (byte) 0xAA);
GLStateManager.glBindBuffer(target, bufferId);
GL15.glBufferData(target, garbage, GL15.GL_STATIC_DRAW);
GLStateManager.glBindBuffer(target, 0);
MemoryUtilities.memFree(garbage);
}
}
@@ -45,23 +45,9 @@ public class CompatConfig {
@Config.RequiresMcRestart
public static boolean fixSecurityCraft;
@Config.Comment("Apply tweaks to NTM:Space")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean tweakNTMSpace;
@Config.Comment("Apply fixes to NTM:Space")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean fixNTMSpace;
@Config.Comment("Disables rendering of the planet below the player at high altitudes [Requires tweakNTMSpace]")
@Config.DefaultEnum("ShadersOnly")
public static altitudePlanetRendererState NTMSpace_disableAltitudePlanetRenderer;
public enum altitudePlanetRendererState {
Always,
ShadersOnly,
Never
}
}
@@ -19,21 +19,25 @@ import net.coderbot.iris.texture.pbr.PBRTextureManager;
import net.coderbot.iris.vertices.ImmediateState;
public class IrisGLSMBridge {
private static Runnable alphaFuncListener = null;
private static Runnable alphaTestListener = null;
private static Runnable blendFuncListener = null;
private static Runnable fogModeListener = null;
private static Runnable fogStartListener = null;
private static Runnable fogEndListener = null;
private static Runnable fogDensityListener = null;
static {
StateUpdateNotifiers.alphaFuncNotifier = listener -> alphaFuncListener = listener;
StateUpdateNotifiers.alphaTestNotifier = listener -> alphaTestListener = listener;
StateUpdateNotifiers.blendFuncNotifier = listener -> blendFuncListener = listener;
StateUpdateNotifiers.fogModeNotifier = listener -> fogModeListener = listener;
StateUpdateNotifiers.fogStartNotifier = listener -> fogStartListener = listener;
StateUpdateNotifiers.fogEndNotifier = listener -> fogEndListener = listener;
StateUpdateNotifiers.fogDensityNotifier = listener -> fogDensityListener = listener;
}
public static void register() {
IrisSamplers.initRenderer();
GLSMHooks.blendHandler = new DeferredBlendHandler() {
@@ -91,13 +95,20 @@ public class IrisGLSMBridge {
DepthColorStorage.deferColorMask(r, g, b, a);
}
};
GLSMHooks.ALPHA_STATE_CHANGE.addListener(event -> {
if (Iris.enabled) {
if (alphaFuncListener != null) alphaFuncListener.run();
if (alphaTestListener != null) alphaTestListener.run();
}
});
GLSMHooks.BLEND_FUNC_CHANGE.addListener(event -> {
if (Iris.enabled) {
if (blendFuncListener != null) blendFuncListener.run();
}
});
GLSMHooks.FOG_STATE_CHANGE.addListener(event -> {
if (Iris.enabled) {
if (fogModeListener != null) fogModeListener.run();
@@ -106,7 +117,7 @@ public class IrisGLSMBridge {
if (fogDensityListener != null) fogDensityListener.run();
}
});
GLSMHooks.TEXTURE_BIND.addListener(event -> {
if (Iris.enabled) {
TextureTracker.INSTANCE.onBindTexture();
@@ -105,9 +105,6 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
new DependencyVerifier.Check(
"/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class",
"Missing dependency: Angelica requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib"),
new DependencyVerifier.Check(
"/com/gtnewhorizon/gtnhlib/client/renderer/PrimitiveExtractor.class",
"GTNHLib is outdated: Angelica requires GTNHLib 0.8.21 or newer! Download: https://modrinth.com/mod/gtnhlib"),
new DependencyVerifier.Check(
"/xyz/wagyourtail/jvmdg/exc/MissingStubError.class",
"GTNHLib is outdated: Angelica requires GTNHLib 0.9.0 or newer! Download: https://modrinth.com/mod/gtnhlib")
@@ -287,6 +287,14 @@ public enum Mixins implements IMixins {
)
),
DRAGONAPI_SHADER_REGISTRY_PARSE_ERROR(new MixinBuilder()
.setPhase(Phase.EARLY)
.addRequiredMod(TargetedMod.DRAGON_API)
.addClientMixins(
"dragonapi.MixinShaderRegistry_ParseError"
)
),
IRIS_RENDERING_NOBACKHAND(new MixinBuilder("Iris Hand Shaders")
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableIris)
@@ -380,17 +388,16 @@ public enum Mixins implements IMixins {
.addRequiredMod(TargetedMod.MINEFACTORY_RELOADED)
.setApplyIf(() -> CompatConfig.fixMinefactoryReloaded)
.addClientMixins("client.minefactoryreloaded.MixinRedNetCableRenderer")),
NTM_SPACE_TWEAKS(new MixinBuilder("Support for 'Disable Horizon' & 'disableAltitudePlanetRenderer' options in NTM:Space")
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.NTM_SPACE)
.setApplyIf(() -> CompatConfig.tweakNTMSpace)
.addClientMixins("client.ntmSpace.MixinSkyProviderCelestial_Tweaks")),
NTM_SPACE_SHADER_COMPAT(new MixinBuilder("Multiple shader fixes for NTM:Space")
NTM_SPACE_COMPAT(new MixinBuilder("Multiple fixes for NTM:Space")
.setPhase(Phase.LATE)
.addRequiredMod(TargetedMod.NTM_SPACE)
.setApplyIf(() -> CompatConfig.fixNTMSpace && AngelicaConfig.enableIris)
.addClientMixins("client.ntmSpace.MixinSkyProviderCelestial_ShaderCompat", "client.ntmSpace.MixinSkyProviderLaytheSunset")),
.addClientMixins(
"client.ntmSpace.MixinSkyProviderCelestial",
"client.ntmSpace.MixinSkyProviderOrbit",
"client.ntmSpace.MixinSkyProviderLaytheSunset"
)),
SPEEDUP_CAMPFIRE_BACKPORT_ANIMATIONS(new MixinBuilder("Add animation speedup support to Campfire Backport")
.setPhase(Phase.LATE)
@@ -43,6 +43,7 @@ import cpw.mods.fml.relauncher.ReflectionHelper;
import jss.notfine.core.Settings;
import jss.notfine.gui.GuiCustomMenu;
import jss.notfine.gui.NotFineGameOptionPages;
import jss.notfine.gui.options.named.FOVMode;
import me.flashyreese.mods.reeses_sodium_options.client.gui.ReeseSodiumVideoOptionsScreen;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI;
@@ -342,10 +343,19 @@ public final class ClientProxy extends CommonProxy {
}
}
// This is a bit of a hack to prevent the FOV from being modified by other mods
// This only disables FOV changes from vanilla, leaving mod FOV changes untouched
@SubscribeEvent(priority = EventPriority.HIGHEST)
public void onFOVModifierUpdateEarly(FOVUpdateEvent event) {
if(Settings.DYNAMIC_FOV.option.getStore() == FOVMode.MODS) {
event.fov = 1.0F;
event.newfov = 1.0F;
}
}
// This removes all FOV dynamics, both from vanilla (speed/sprinting), and mods
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onFOVModifierUpdate(FOVUpdateEvent event) {
if (!(boolean) Settings.DYNAMIC_FOV.option.getStore()) {
public void onFOVModifierUpdateLate(FOVUpdateEvent event) {
if(Settings.DYNAMIC_FOV.option.getStore() == FOVMode.NONE) {
event.newfov = 1.0F;
}
}
@@ -34,11 +34,13 @@ public final class TileEntityRenderBoundsRegistry {
}
private static boolean probeAndCache(TileEntity te, Class<? extends TileEntity> clazz) {
boolean isInfinite = false;
boolean isInfinite;
try {
final AxisAlignedBB aabb = te.getRenderBoundingBox();
isInfinite = isInfiniteExtentsBox(aabb);
} catch (Exception ignored) {}
} catch (Throwable t) {
isInfinite = true;
}
synchronized (classRegistry) {
classRegistry.put(clazz, isInfinite ? INFINITE : FINITE);
@@ -144,13 +144,18 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
if (TileEntityRenderBoundsRegistry.isAlwaysInfiniteExtent(tileEntity)) {
isGlobal = true;
} else {
final AxisAlignedBB aabb = tileEntity.getRenderBoundingBox();
AxisAlignedBB aabb;
try {
aabb = tileEntity.getRenderBoundingBox();
} catch (Throwable t) {
aabb = null;
}
if (aabb != null) {
final int secMinX = x & ~15, secMinY = y & ~15, secMinZ = z & ~15;
isGlobal = aabb.minX < secMinX || aabb.minY < secMinY || aabb.minZ < secMinZ
|| aabb.maxX > secMinX + 16 || aabb.maxY > secMinY + 16 || aabb.maxZ > secMinZ + 16;
} else {
isGlobal = false;
isGlobal = true;
}
}
(isGlobal ? renderData.globalBlockEntities : renderData.culledBlockEntities).add(tileEntity);
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.utils;
import net.minecraft.util.ResourceLocation;
public class WorkaroundUtils {
public static ResourceLocation GL_TEXTURE_2D_WORKAROUND = new ResourceLocation("angelica:textures/shaders_workaround.png");
}
@@ -233,6 +233,7 @@ public class CTMUtils {
}
public static void reset() {
clearCurrentCompact();
}
private static boolean checkFace(int face) {
@@ -105,7 +105,6 @@ abstract public class TileOverrideIterator implements Iterator<TileOverride> {
public TileOverride go(RenderBlockState renderBlockState, IIcon origIcon) {
CTMUtils.clearCurrentCompact();
this.renderBlockState = renderBlockState;
renderBlockState.setFilter(null);
currentIcon = origIcon;
+3 -2
View File
@@ -11,6 +11,7 @@ import jss.notfine.gui.options.control.NotFineControlValueFormatter;
import jss.notfine.gui.options.named.AlwaysNever;
import jss.notfine.gui.options.named.BackgroundSelect;
import jss.notfine.gui.options.named.DownfallQuality;
import jss.notfine.gui.options.named.FOVMode;
import jss.notfine.gui.options.named.GraphicsQualityOff;
import jss.notfine.gui.options.named.LeavesQuality;
import jss.notfine.gui.options.named.GraphicsToggle;
@@ -49,7 +50,7 @@ public enum Settings {
SettingsManager.downfallDistanceUpdated();
}
},
DYNAMIC_FOV(new NotFineOptionTickBox(true, null)),
DYNAMIC_FOV(new NotFineOptionCycling<>(FOVMode.ALL, OptionImpact.VARIES)),
HURT_SHAKE(new NotFineOptionSliderPercentage(100, 0, 300, 5, OptionImpact.LOW)),
DYNAMIC_LIGHTS(new NotFineOptionCycling<>(DynamicLightsMode.FANCY, OptionImpact.VARIES)){
@Override
@@ -99,7 +100,7 @@ public enum Settings {
ChunkRebuildManager.setMaxTicksWaiting((int) this.option.getStore());
}
},
HORIZON(new NotFineOptionTickBox(true, OptionImpact.LOW)),
HORIZON(new NotFineOptionTickBox(false, 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)) {
@@ -0,0 +1,21 @@
package jss.notfine.gui.options.named;
import me.jellysquid.mods.sodium.client.gui.options.named.NamedState;
public enum FOVMode implements NamedState {
ALL("options.fov.all"),
MODS("options.fov.mods"),
NONE("options.fov.none");
private final String name;
FOVMode(String name) {
this.name = name;
}
@Override
public String getKey() {
return this.name;
}
}
@@ -7,8 +7,10 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
import net.coderbot.iris.Iris;
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
import net.coderbot.iris.shaderpack.materialmap.BlockRenderType;
import net.coderbot.iris.shaderpack.materialmap.FlatteningMap;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.minecraft.block.Block;
import net.minecraft.init.Blocks;
@@ -39,8 +41,16 @@ public class BlockMaterialMapping {
Map<Block, BlockRenderLayer> blockTypeIds = new Reference2ReferenceOpenHashMap<>();
blockPropertiesMap.forEach((id, blockType) -> {
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
final Block block = Block.getBlockFromName(resourceLocation.toString());
Block block = resolveBlock(id);
// Try flattening map for modern names
if ((block == null || block == Blocks.air) && "minecraft".equals(id.getNamespace())) {
List<BlockEntry> legacyEntries = FlatteningMap.toLegacy(id.getName(), Map.of());
if (legacyEntries != null) {
// Use the first entry for render type (render type applies per-block)
block = resolveBlock(legacyEntries.getFirst().getId());
}
}
if (block == null || block == Blocks.air) {
return;
@@ -73,19 +83,32 @@ public class BlockMaterialMapping {
* Based on Iris's addBlockStates method, adapted for 1.7.10 metadata system.
*/
private static void addBlockMetas(BlockEntry entry, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId) {
final NamespacedId id = entry.getId();
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
NamespacedId id = entry.getId();
String name = id.getName();
boolean hasStateProps = !entry.getStateProperties().isEmpty();
boolean hasExplicitMetas = !entry.getMetas().isEmpty();
final Block block = (Block) Block.blockRegistry.getObject(resourceLocation.toString());
if ("minecraft".equals(id.getNamespace()) && (hasStateProps || !hasExplicitMetas)) {
List<BlockEntry> legacyEntries = FlatteningMap.toLegacy(name, entry.getStateProperties());
if (legacyEntries != null) {
for (BlockEntry legacy : legacyEntries) {
applyMetas(resolveBlock(legacy.getId()), legacy.getMetas(), idMap, intId);
}
return;
}
}
// If the block doesn't exist, by default the registry will return AIR. That probably isn't what we want.
// TODO: Assuming that Registry.BLOCK.getDefaultId() == "minecraft:air" here
// Fall back to registry with the entry's own metas
Block block = resolveBlock(id);
applyMetas(block, entry.getMetas(), idMap, intId);
}
// If the block doesn't exist, by default the registry will return AIR. That probably isn't what we want.
// TODO: Assuming that Registry.BLOCK.getDefaultId() == "minecraft:air" here
private static void applyMetas(Block block, Set<Integer> metas, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId) {
if (block == null || block == Blocks.air) {
return;
}
Set<Integer> metas = entry.getMetas();
Int2IntMap metaMap = idMap.get(block);
if (metaMap == null) {
metaMap = new Int2IntOpenHashMap();
@@ -94,15 +117,18 @@ public class BlockMaterialMapping {
}
if (metas.isEmpty()) {
// Add all metadata values (0-15) if there aren't any specific ones
for (int meta = 0; meta < 16; meta++) {
metaMap.putIfAbsent(meta, intId);
}
} else {
// Add only specific metadata values
for (int meta : metas) {
metaMap.putIfAbsent(meta, intId);
}
}
}
private static Block resolveBlock(NamespacedId id) {
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
return (Block) Block.blockRegistry.getObject(resourceLocation.toString());
}
}
@@ -8,6 +8,8 @@ public class StateUpdateNotifiers {
public static ValueUpdateNotifier fogStartNotifier;
public static ValueUpdateNotifier fogEndNotifier;
public static ValueUpdateNotifier fogDensityNotifier;
public static ValueUpdateNotifier alphaFuncNotifier;
public static ValueUpdateNotifier alphaTestNotifier;
public static ValueUpdateNotifier blendFuncNotifier;
public static ValueUpdateNotifier bindTextureNotifier;
public static ValueUpdateNotifier normalTextureChangeNotifier;
@@ -46,7 +46,22 @@ public class CommonTransformer {
// runtime discard using the GLSM-tracked alpha reference value.
if (found.contains(0) && parameters.patch != Patch.COMPOSITE && parameters.patch != Patch.COMPUTE) {
root.injectVariable("uniform float iris_currentAlphaTest;");
root.appendMain("if (iris_FragData0.a <= iris_currentAlphaTest) discard;");
root.injectVariable("uniform int iris_currentAlphaFunc;");
root.injectFunction(
"bool iris_alphaTestPass(float a) {" +
" if (iris_currentAlphaFunc == 7) return true;" + // ALWAYS / disabled
" if (iris_currentAlphaFunc == 0) return false;" + // NEVER
" float qa = round(a * 255.0);" +
" float qr = round(iris_currentAlphaTest * 255.0);" +
" if (iris_currentAlphaFunc == 1) return qa < qr;" + // LESS
" if (iris_currentAlphaFunc == 2) return qa == qr;" + // EQUAL
" if (iris_currentAlphaFunc == 3) return qa <= qr;" + // LEQUAL
" if (iris_currentAlphaFunc == 4) return qa > qr;" + // GREATER
" if (iris_currentAlphaFunc == 5) return qa != qr;" + // NOTEQUAL
" return qa >= qr;" + // GEQUAL (6)
"}"
);
root.appendMain("if (!iris_alphaTestPass(iris_FragData0.a)) discard;");
}
}
}
@@ -8,8 +8,10 @@ import it.unimi.dsi.fastutil.objects.Object2IntFunction;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
import net.coderbot.iris.shaderpack.materialmap.EntityFlatteningMap;
import net.coderbot.iris.shaderpack.materialmap.BlockRenderType;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.coderbot.iris.shaderpack.option.ShaderPackOptions;
@@ -56,7 +58,8 @@ public class IdMap {
/**
* A set of render type overrides for specific blocks. Allows shader packs to move blocks to different render types.
*/
private Map<NamespacedId, BlockRenderType> blockRenderTypeMap;
@Getter
private Map<NamespacedId, BlockRenderType> blockRenderTypeMap;
IdMap(Path shaderPath, ShaderPackOptions shaderPackOptions, Iterable<StringPair> environmentDefines) {
// Check if block.properties has a dedicated 1.7.10 section
@@ -89,7 +92,6 @@ public class IdMap {
loadProperties(shaderPath, "block.properties", shaderPackOptions, modernDefines).ifPresent(blockProperties -> {
blockPropertiesMap = parseBlockMap(blockProperties, "block.", "block.properties");
blockRenderTypeMap = parseRenderTypeMap(blockProperties, "layer.", "block.properties");
blockPropertiesMap = LegacyIdMap.convertModernBlockEntries(blockPropertiesMap);
});
}
@@ -141,7 +143,7 @@ public class IdMap {
private static String readProperties(Path shaderPath, String name) {
try {
// ID maps should be encoded in ISO_8859_1.
return new String(Files.readAllBytes(shaderPath.resolve(name)), StandardCharsets.ISO_8859_1);
return Files.readString(shaderPath.resolve(name), StandardCharsets.ISO_8859_1);
} catch (NoSuchFileException e) {
Iris.logger.debug("An " + name + " file was not found in the current shaderpack");
@@ -158,7 +160,24 @@ public class IdMap {
}
private static Object2IntMap<NamespacedId> parseEntityIdMap(Properties properties) {
return parseIdMap(properties, "entity.", "entity.properties");
Object2IntMap<NamespacedId> idMap = parseIdMap(properties, "entity.", "entity.properties");
// For modern shader packs: translate modern entity names to 1.7.10 names
// so runtime lookups match EntityList's registered names directly.
Object2IntMap<NamespacedId> augmented = new Object2IntOpenHashMap<>(idMap);
augmented.defaultReturnValue(-1);
for (Object2IntMap.Entry<NamespacedId> entry : idMap.object2IntEntrySet()) {
NamespacedId id = entry.getKey();
if ("minecraft".equals(id.getNamespace())) {
String legacyName = EntityFlatteningMap.toLegacy(id.getName());
if (legacyName != null) {
augmented.putIfAbsent(new NamespacedId(legacyName), entry.getIntValue());
}
}
}
return Object2IntMaps.unmodifiable(augmented);
}
/**
@@ -210,7 +229,7 @@ public class IdMap {
} else if (c == '"') {
inQuotes = !inQuotes;
} else if (Character.isWhitespace(c) && !inQuotes) {
if (current.length() > 0) {
if (!current.isEmpty()) {
result.add(current.toString());
current.setLength(0);
}
@@ -230,7 +249,7 @@ public class IdMap {
}
// Add final token
if (current.length() > 0) {
if (!current.isEmpty()) {
result.add(current.toString());
}
@@ -382,11 +401,7 @@ public class IdMap {
return entityIdMap;
}
public Map<NamespacedId, BlockRenderType> getBlockRenderTypeMap() {
return blockRenderTypeMap;
}
@Override
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
@@ -1,131 +1,49 @@
package net.coderbot.iris.shaderpack;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.Function;
import cpw.mods.fml.common.registry.GameData;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.minecraft.block.Block;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class LegacyIdMap {
private static final ImmutableList<String> COLORS =
ImmutableList.of("white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray",
"light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black");
private static final ImmutableList<String> WOOD_TYPES =
ImmutableList.of("oak", "birch", "jungle", "spruce", "acacia", "dark_oak");
private static final Map<String, String> MODERN_TO_LEGACY_BLOCK_IDS = Map.of(
"grass", "tallgrass",
"grass_block", "grass"
);
/**
* Fills the block ID map with all vanilla blocks in the case where block.properties is not found.
*
* ShadersMod passed block IDs as a signed byte, so IDs 128-255 wrapped to -128 to -1.
* For example, emerald_block (ID 133) becomes -123. Shader packs like SEUS v11 were written
* against this behavior, so we replicate it here.
*/
public static void addLegacyValues(Int2ObjectMap<List<BlockEntry>> blockIdMap) {
add(blockIdMap, 1, block("stone"), block("granite"), block("diorite"), block("andesite"));
add(blockIdMap, 2, block("grass_block"));
add(blockIdMap, 4, block("cobblestone"));
add(blockIdMap, 50, block("torch"));
add(blockIdMap, 89, block("glowstone"));
// TODO: what about inactive redstone lamps?
add(blockIdMap, 124, block("redstone_lamp"));
add(blockIdMap, 12, block("sand"));
add(blockIdMap, 24, block("sandstone"));
add(blockIdMap, 41, block("gold_block"));
add(blockIdMap, 42, block("iron_block"));
add(blockIdMap, 57, block("diamond_block"));
// Apparently this is what SEUS v11 expects? Maybe old shadersmod was buggy.
add(blockIdMap, -123, block("emerald_block"));
addMany(blockIdMap, 35, COLORS, color -> block(color + "_wool"));
// NB: Use the "still" IDs for water and lava, since some shader packs don't properly support the "flowing"
// versions: https://github.com/IrisShaders/Iris/issues/1462
add(blockIdMap, 9, block("water"));
add(blockIdMap, 11, block("lava"));
add(blockIdMap, 79, block("ice"));
addMany(blockIdMap, 18, WOOD_TYPES, woodType -> block(woodType + "_leaves"));
addMany(blockIdMap, 95, COLORS, color -> block(color + "_stained_glass"));
addMany(blockIdMap, 160, COLORS, color -> block(color + "_stained_glass_pane"));
// Short grass / bush
add(blockIdMap, 31, block("grass"), block("seagrass"), block("sweet_berry_bush"));
// Crops (59 = wheat), but we include carrots and potatoes too.
add(blockIdMap, 59, block("wheat"), block("carrots"), block("potatoes"));
// Small flowers
add(blockIdMap, 37, block("dandelion"), block("poppy"), block("blue_orchid"),
block("allium"), block("azure_bluet"), block("red_tulip"), block("pink_tulip"),
block("white_tulip"), block("orange_tulip"), block("oxeye_daisy"),
block("cornflower"), block("lily_of_the_valley"), block("wither_rose"));
// Big tall grass / flowers
// Also include seagrass here
add(blockIdMap, 175, block("sunflower"), block("lilac"), block("tall_grass"),
block("large_fern"), block("rose_bush"), block("peony"), block("tall_seagrass"));
// Fire
add(blockIdMap, 51, block("fire"));
// Lily pad
add(blockIdMap, 111, block("lily_pad"));
// TODO: 76 -> redstone_torch (on)
}
public static Int2ObjectMap<List<BlockEntry>> convertModernBlockEntries(Int2ObjectMap<List<BlockEntry>> blockIdMap) {
Int2ObjectMap<List<BlockEntry>> converted = new Int2ObjectOpenHashMap<>();
blockIdMap.forEach((intId, entries) -> {
List<BlockEntry> convertedEntries = new ArrayList<>(entries.size());
for (BlockEntry entry : entries) {
NamespacedId id = entry.getId();
if (!"minecraft".equals(id.getNamespace())) {
convertedEntries.add(entry);
continue;
}
String convertedName = MODERN_TO_LEGACY_BLOCK_IDS.get(id.getName());
if (convertedName == null) {
convertedEntries.add(entry);
continue;
}
convertedEntries.add(new BlockEntry(new NamespacedId(id.getNamespace(), convertedName), entry.getMetas()));
for (String name : (Iterable<String>) GameData.getBlockRegistry().getKeys()) {
// Only include vanilla minecraft blocks
if (!name.startsWith("minecraft:")) {
continue;
}
converted.put(intId, Collections.unmodifiableList(convertedEntries));
});
return converted;
}
final Block block = (Block) GameData.getBlockRegistry().getObject(name);
if (block == null) {
continue;
}
private static BlockEntry block(String name) {
return new BlockEntry(new NamespacedId("minecraft", name), Collections.emptySet());
}
final int id = GameData.getBlockRegistry().getIDForObject(block);
if (id <= 0 || id > 255) {
continue;
}
private static void addMany(Int2ObjectMap<List<BlockEntry>> blockIdMap, int id, List<String> prefixes, Function<String, BlockEntry> toId) {
List<BlockEntry> entries = new ArrayList<>();
// Apply signed byte overflow
final int overflowId = (byte) id;
for (String prefix : prefixes) {
entries.add(toId.apply(prefix));
final String blockName = name.substring("minecraft:".length());
final BlockEntry entry = new BlockEntry(new NamespacedId("minecraft", blockName), Collections.emptySet());
blockIdMap.computeIfAbsent(overflowId, k -> new ArrayList<>()).add(entry);
}
blockIdMap.put(id, entries);
}
private static void add(Int2ObjectMap<List<BlockEntry>> blockIdMap, int id, BlockEntry... entries) {
blockIdMap.put(id, Arrays.asList(entries));
}
}
@@ -6,7 +6,9 @@ import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -14,10 +16,16 @@ import java.util.Set;
public class BlockEntry {
private final NamespacedId id;
private final Set<Integer> metas;
private final Map<String, String> stateProperties;
public BlockEntry(NamespacedId id, Set<Integer> metas) {
public BlockEntry(NamespacedId id, Set<Integer> metas) {
this(id, metas, Collections.emptyMap());
}
public BlockEntry(NamespacedId id, Set<Integer> metas, Map<String, String> stateProperties) {
this.id = id;
this.metas = metas;
this.stateProperties = stateProperties;
}
/**
@@ -39,60 +47,66 @@ public class BlockEntry {
return new BlockEntry(new NamespacedId("minecraft", entry), Collections.emptySet());
}
// Examples of what we'll accept
// Examples of what we accept:
// stone
// stone:0
// minecraft:stone:0
// minecraft:stone:0,1,2 # Theoretically valid, but I haven't seen any examples in actual shaders
// minecraft:stone:0,1,2
// minecraft:furnace:lit=true (blockstate property)
// minecraft:oak_log:axis=y (blockstate property)
// Examples of what we don't (Yet?) accept - Seems to be from MC 1.8+
// minecraft:farmland:moisture=0
// minecraft:farmland:moisture=1
// minecraft:double_plant:half=lower
// minecraft:double_plant:half=upper
// minecraft:grass:snowy=true
// minecraft:unpowered_comparator:powered=false
// Less trivial case: no metas involved, just a namespace
//
// The first term MUST be a valid ResourceLocation component
// The second term, if it is not numeric, must be a valid ResourceLocation component.
if (splitStates.length == 2 && !StringUtils.isNumeric(splitStates[1].substring(0, 1))) {
// Less trivial case: no metas or state properties, just a namespace
if (splitStates.length == 2 && !StringUtils.isNumeric(splitStates[1].substring(0, 1))
&& !splitStates[1].contains("=")) {
return new BlockEntry(new NamespacedId(splitStates[0], splitStates[1]), Collections.emptySet());
}
// Complex case: One or more states involved...
// Complex case: metas and/or state properties
final int statesStart;
final NamespacedId id;
if (StringUtils.isNumeric(splitStates[1].substring(0, 1))) {
// We have an entry of the form "stone:0"
if (splitStates.length == 2) {
// "stone:0" or "stone:lit=true"
statesStart = 1;
id = new NamespacedId("minecraft", splitStates[0]);
} else if (StringUtils.isNumeric(splitStates[1].substring(0, 1)) || splitStates[1].contains("=")) {
// "stone:0:something" or "stone:lit=true:something" — unlikely but handle it
statesStart = 1;
id = new NamespacedId("minecraft", splitStates[0]);
} else {
// We have an entry of the form "minecraft:stone:0"
// "minecraft:stone:0" or "minecraft:furnace:lit=true"
statesStart = 2;
id = new NamespacedId(splitStates[0], splitStates[1]);
}
final Set<Integer> metas = new HashSet<>();
final Set<Integer> metas = new HashSet<>();
final Map<String, String> properties = new HashMap<>();
for (int index = statesStart; index < splitStates.length; index++) {
// Parse out one or more metadata ids
final String[] metaParts = splitStates[index].split(",");
String segment = splitStates[index];
for (String metaPart : metaParts) {
try {
metas.add(Integer.parseInt(metaPart));
} catch (NumberFormatException e) {
Iris.logger.warn("Warning: the block ID map entry \"" + entry + "\" could not be fully parsed:");
Iris.logger.warn("- Metadata ids must be a comma separated list of one or more integers, but "+ splitStates[index] + " is not of that form!");
}
}
if (segment.contains("=")) {
// Blockstate property: key=value
for (String prop : segment.split(",")) {
int eq = prop.indexOf('=');
if (eq > 0 && eq < prop.length() - 1) {
properties.put(prop.substring(0, eq), prop.substring(eq + 1));
}
}
} else {
// Metadata IDs: comma-separated integers
for (String metaPart : segment.split(",")) {
try {
metas.add(Integer.parseInt(metaPart));
} catch (NumberFormatException e) {
Iris.logger.warn("Warning: the block ID map entry \"" + entry + "\" could not be fully parsed:");
Iris.logger.warn("- Metadata ids must be a comma separated list of one or more integers, but " + segment + " is not of that form!");
}
}
}
}
return new BlockEntry(id, metas);
return new BlockEntry(id, metas, properties);
}
@Override
@@ -100,11 +114,11 @@ public class BlockEntry {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final BlockEntry that = (BlockEntry) o;
return Objects.equals(id, that.id) && Objects.equals(metas, that.metas);
return Objects.equals(id, that.id) && Objects.equals(metas, that.metas) && Objects.equals(stateProperties, that.stateProperties);
}
@Override
public int hashCode() {
return Objects.hash(id, metas);
return Objects.hash(id, metas, stateProperties);
}
}
@@ -0,0 +1,95 @@
package net.coderbot.iris.shaderpack.materialmap;
import java.util.HashMap;
import java.util.Map;
/**
* Maps modern entity names to their 1.7.10 equivalents.
*
* In 1.11+, entity IDs were changed to namespaced: "minecraft:cave_spider".
* In 1.7.10, entities are registered with names like "CaveSpider".
* Some entities were also renamed or split into multiple types.
*
* This map is used at lookup time: when a modern shader pack references an entity by its
* modern name, we resolve it to the 1.7.10 name that EntityList uses.
*/
public class EntityFlatteningMap {
/** Maps modern entity name -> 1.7.10 entity name. */
private static final Map<String, String> MODERN_TO_LEGACY = new HashMap<>();
static {
// Simple case changes
simple("Arrow", "arrow");
simple("Bat", "bat");
simple("Blaze", "blaze");
simple("Boat", "boat");
simple("Chicken", "chicken");
simple("Cow", "cow");
simple("Creeper", "creeper");
simple("Enderman", "enderman");
simple("Ghast", "ghast");
simple("Giant", "giant");
simple("Item", "item");
simple("Painting", "painting");
simple("Pig", "pig");
simple("Sheep", "sheep");
simple("Silverfish", "silverfish");
simple("Skeleton", "skeleton");
simple("Slime", "slime");
simple("Snowball", "snowball");
simple("Spider", "spider");
simple("Squid", "squid");
simple("Villager", "villager");
simple("Witch", "witch");
simple("Wolf", "wolf");
simple("Zombie", "zombie");
// Renamed entities
simple("CaveSpider", "cave_spider");
simple("EnderCrystal", "end_crystal");
simple("EnderDragon", "ender_dragon");
simple("EntityHorse", "horse");
simple("EyeOfEnderSignal", "eye_of_ender");
simple("FallingSand", "falling_block");
simple("Fireball", "fireball");
simple("FireworksRocketEntity", "firework_rocket");
simple("ItemFrame", "item_frame");
simple("LavaSlime", "magma_cube");
simple("LeashKnot", "leash_knot");
simple("MushroomCow", "mooshroom");
simple("Ozelot", "ocelot");
simple("PigZombie", "zombified_piglin");
simple("PrimedTnt", "tnt");
simple("SmallFireball", "small_fireball");
simple("SnowMan", "snow_golem");
simple("ThrownEnderpearl", "ender_pearl");
simple("ThrownExpBottle", "experience_bottle");
simple("ThrownPotion", "potion");
simple("VillagerGolem", "iron_golem");
simple("WitherBoss", "wither");
simple("WitherSkull", "wither_skull");
simple("XPOrb", "experience_orb");
// Minecarts
simple("MinecartRideable", "minecart");
simple("MinecartChest", "chest_minecart");
simple("MinecartCommandBlock", "command_block_minecart");
simple("MinecartFurnace", "furnace_minecart");
simple("MinecartHopper", "hopper_minecart");
simple("MinecartSpawner", "spawner_minecart");
simple("MinecartTNT", "tnt_minecart");
}
/**
* Returns the 1.7.10 entity name for a modern entity name, or null if no mapping exists.
* The input name should not include the "minecraft:" namespace prefix.
*/
public static String toLegacy(String modernName) {
return MODERN_TO_LEGACY.get(modernName);
}
private static void simple(String legacy, String modern) {
MODERN_TO_LEGACY.put(modern, legacy);
}
}
File diff suppressed because it is too large. Load diff
@@ -2,6 +2,7 @@ package net.coderbot.iris.uniforms;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.coderbot.iris.gl.state.FogMode;
import net.coderbot.iris.gl.state.StateUpdateNotifiers;
import net.coderbot.iris.gl.uniform.DynamicUniformHolder;
import org.joml.Vector3d;
import org.joml.Vector4f;
@@ -22,6 +23,12 @@ public class IrisInternalUniforms {
return GLStateManager.getAlphaState().getReference();
}
private static int getEffectiveAlphaFunc() {
if (!GLStateManager.getAlphaTest().isEnabled()) return 7;
final int func = GLStateManager.getAlphaState().getFunction();
return func & 0x7;
}
public static void addFogUniforms(DynamicUniformHolder uniforms, FogMode fogMode) {
uniforms.uniform4f(PER_FRAME, "iris_FogColor", () -> {
final Vector3d color = GLStateManager.getFogState().getFogColor();
@@ -34,7 +41,8 @@ public class IrisInternalUniforms {
.uniform1f(PER_FRAME, "iris_FogDensity", () -> Math.max(0.0F, GLStateManager.getFogState().getDensity()));
uniforms
.uniform1f(PER_FRAME, "iris_currentAlphaTest", IrisInternalUniforms::getEffectiveAlphaRef)
.uniform1f(PER_FRAME, "alphaTestRef", IrisInternalUniforms::getEffectiveAlphaRef);
.uniform1f("iris_currentAlphaTest", IrisInternalUniforms::getEffectiveAlphaRef, StateUpdateNotifiers.alphaTestNotifier)
.uniform1f("alphaTestRef", IrisInternalUniforms::getEffectiveAlphaRef, StateUpdateNotifiers.alphaTestNotifier)
.uniform1i("iris_currentAlphaFunc", IrisInternalUniforms::getEffectiveAlphaFunc, StateUpdateNotifiers.alphaFuncNotifier);
}
}
+3 -1
View File
@@ -13,4 +13,6 @@ public net.minecraft.block.BlockLeaves field_150129_M
#For the interpolated icon class
public net.minecraft.client.renderer.texture.TextureAtlasSprite field_110982_k #animationMetadata
#For interpolated icon mixin
public net.minecraft.client.resources.SimpleResource field_110530_g #mcmetaJson
public net.minecraft.client.resources.SimpleResource field_110530_g #mcmetaJson
#To prevent mods from un-disabling FOV changes
public-f net.minecraftforge.client.event.FOVUpdateEvent fov # fov
@@ -19,13 +19,16 @@ options.gui_background.tooltip=Background
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.dynamic_fov.tooltip=Depending on the mode, the game's field of view will change based on the player's speed or from certain mod interactions (including tool zooming/weapon scopes).
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=Distance away from the player when the view distance fog will start (0%% opacity then linear increase to render distance)
options.fov.all=Enabled
options.fov.mods=Mods Only
options.fov.none=Disabled
options.mode_cloud_translucency=Cloud Translucency
options.mode_cloud_translucency.tooltip=Cloud Translucency
options.mode_clouds=Clouds
@@ -35,6 +35,13 @@ public class MixinSplashProgressCaching {
ImmediateModeRecorder.initSplashTessellator();
}
// VAOs aren't shared across GL contexts; the SharedDrawable the client thread just swapped
// onto has none. Core profile (macOS) rejects glValidateProgram without one.
@Inject(method = "start", at = @At("RETURN"))
private static void angelica$bindSharedDrawableVAO(CallbackInfo ci) {
GLStateManager.glBindVertexArray(GLStateManager.glGenVertexArrays());
}
/**
* On return from finish() - destroy splash tessellator and mark splash complete
*/
@@ -0,0 +1,19 @@
package com.gtnewhorizons.angelica.mixins.early.dragonapi;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Pseudo
@Mixin(targets = { "Reika/DragonAPI/IO/Shaders/ShaderRegistry" }, remap = false)
public class MixinShaderRegistry_ParseError {
@Redirect(method = "parseError",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL20;glGetShaderInfoLog(II)Ljava/lang/String;"))
private static String angelica$routeInfoLogByType(int id, int maxLen) {
return GLStateManager.glIsProgram(id) ? GLStateManager.glGetProgramInfoLog(id, maxLen) : GLStateManager.glGetShaderInfoLog(id, maxLen);
}
}
@@ -55,11 +55,13 @@ public abstract class MixinRenderBlocks {
@Inject(method = "renderBlockByRenderType", at = @At("HEAD"))
private void renderingByTypeEnable(CallbackInfoReturnable<Boolean> ci) {
CTMUtils.clearCurrentCompact();
this.isRenderingByType = true;
}
@Inject(method = "renderBlockByRenderType", at = @At("TAIL"))
private void renderingByTypeDisable(CallbackInfoReturnable<Boolean> ci) {
CTMUtils.clearCurrentCompact();
this.isRenderingByType = false;
}
@@ -9,6 +9,7 @@ import net.minecraft.init.Blocks;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
@@ -23,7 +24,7 @@ public abstract class MixinTileEntity implements ITileEntityBoundingBoxCache {
@Shadow public int yCoord;
@Shadow public int zCoord;
@Shadow(remap = false) public static final AxisAlignedBB INFINITE_EXTENT_AABB = null;
@Shadow(remap = false) @Final public static AxisAlignedBB INFINITE_EXTENT_AABB;
/**
* @author mitchej123
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.prupe.mcpatcher.ctm.CTMUtils;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
@@ -17,6 +18,7 @@ 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) {
CTMUtils.clearCurrentCompact();
if (te == null) {
CapturedRenderingState.INSTANCE.setCurrentBlockEntity(0);
return;
@@ -46,6 +48,7 @@ public class MixinTileEntityRendererDispatcher {
@Inject(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V", at = @At("RETURN"))
private void iris$resetBlockEntityId(TileEntity te, double x, double y, double z, float partialTicks, CallbackInfo ci) {
CTMUtils.clearCurrentCompact();
CapturedRenderingState.INSTANCE.setCurrentBlockEntity(0);
}
}
@@ -0,0 +1,132 @@
package com.gtnewhorizons.angelica.mixins.late.client.ntmSpace;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.utils.WorkaroundUtils;
import com.hbm.dim.CelestialBody;
import com.hbm.dim.SkyProviderCelestial;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalIntRef;
import jss.notfine.core.Settings;
import net.coderbot.iris.Iris;
import net.coderbot.iris.pipeline.WorldRenderingPhase;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.renderer.Tessellator;
import org.lwjgl.opengl.GL11;
import org.objectweb.asm.Opcodes;
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.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Ported with additional fixes from the ShaderFixer mod
* @author kotmatross28729
*/
@Mixin(value = SkyProviderCelestial.class, priority = 100)
public class MixinSkyProviderCelestial {
/**
* Avoid program rebinding due to pipeline.setInputs
*/
@WrapWithCondition(method = {
"renderSunset",
"renderCelestials",
"renderAtmosphereGlow",
}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V"), remap = false)
private boolean iris$common$redirectTex2D(int cap) {
return this.iris$common$redirectTexture(cap);
}
/**
* Avoid program rebinding due to pipeline.setInputs | Doesn't affect texture disabling before glSkyList
*/
@WrapWithCondition(method = "render",
slice = @Slice(from = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", ordinal = 1)),
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V"), remap = false)
private boolean iris$common$redirectTex2DException(int cap) {
return this.iris$common$redirectTexture(cap);
}
@Unique
private boolean iris$common$redirectTexture(int cap) {
if (cap == GL11.GL_TEXTURE_2D) {
Minecraft.getMinecraft().renderEngine.bindTexture(WorkaroundUtils.GL_TEXTURE_2D_WORKAROUND);
return false;
}
return true;
}
/**
* Force skybox to render with the default program | Fix various issues with shaders enabled
*/
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderHelper;disableStandardItemLighting()V"), remap = false)
public void iris$main$renderInDefaultProgram(CallbackInfo ci, @Share("main$previousProgram") LocalIntRef main$previousProgram) {
main$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
}
@WrapOperation(method = "render",
at = @At(value = "INVOKE", target = "Lcom/hbm/dim/SkyProviderCelestial;renderSun(FLnet/minecraft/client/multiplayer/WorldClient;Lnet/minecraft/client/Minecraft;Lcom/hbm/dim/CelestialBody;DDFF)V"), remap = false)
private void iris$main$renderSunInShaderProgram(SkyProviderCelestial instance, float partialTicks, WorldClient world, Minecraft mc, CelestialBody sun, double sunSize, double coronaSize, float visibility, float pressure, Operation<Void> original, @Share("main$previousProgram") LocalIntRef main$previousProgram){
GLStateManager.glUseProgram(main$previousProgram.get());
original.call(instance, partialTicks, world, mc, sun, sunSize, coronaSize, visibility, pressure);
GLStateManager.glUseProgram(0);
}
@Inject(method = "render", at = @At(value = "TAIL"), remap = false)
public void iris$main$restorePreviousProgram(CallbackInfo ci, @Share("main$previousProgram") LocalIntRef main$previousProgram) {
GLStateManager.glUseProgram(main$previousProgram.get());
}
@Inject(method = "renderSun", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I"),
slice = @Slice(
from = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glColor4f(FFFF)V", ordinal = 0),
to = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glColor4f(FFFF)V", ordinal = 1)
)
)
private void iris$sun$disableBlanking(CallbackInfo ci) {
Tessellator.instance.vertexCount = 0;
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I"),
slice = @Slice(
from = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", ordinal = 1),
to = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", ordinal = 2)
)
)
private void iris$main$disableVoid(CallbackInfo ci) {
Tessellator.instance.vertexCount = 0;
}
@WrapWithCondition(method = "render", at = {
@At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", ordinal = 1),
@At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", ordinal = 2)
}, remap = false)
private boolean angelica$renderHorizon(int i) {
return (boolean) Settings.HORIZON.option.getStore();
}
@Inject(method = "render", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/Tessellator;instance:Lnet/minecraft/client/renderer/Tessellator;", opcode = Opcodes.GETSTATIC))
private void iris$renderSky$beginNormalSky(CallbackInfo ci) {
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setPhase(WorldRenderingPhase.SKY));
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glRotatef(FFFF)V", ordinal = 2), remap = false)
private void iris$renderSky$tiltSun(CallbackInfo ci) {
Iris.getPipelineManager().getPipeline().ifPresent(
p -> GLStateManager.glRotatef(p.getSunPathRotation(), 0.0F, 0.0F, 1.0F)
);
}
@Inject(method = "renderSun", at = @At(value = "HEAD"), remap = false)
private void iris$setSunRenderStage(CallbackInfo ci) {
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setPhase(WorldRenderingPhase.SUN));
}
}
@@ -1,259 +0,0 @@
package com.gtnewhorizons.angelica.mixins.late.client.ntmSpace;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.hbm.dim.SkyProviderCelestial;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalIntRef;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.coderbot.iris.Iris;
import net.coderbot.iris.pipeline.WorldRenderingPhase;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.irisshaders.iris.api.v0.IrisApi;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Ported with additional fixes from the ShaderFixer mod
* @author kotmatross28729
*/
@Mixin(value = SkyProviderCelestial.class, priority = 100)
public class MixinSkyProviderCelestial_ShaderCompat {
/// Simple white 2x2 texture
@Unique
private static ResourceLocation GL_TEXTURE_2D_WORKAROUND = new ResourceLocation("angelica:textures/shaders_workaround.png");
// Fixes
/**
* Disables {@link GL11#glDisable(int GL_TEXTURE_2D)} call
* @see #iris$atmosphereBlend$renderInDefaultProgram(CallbackInfo, Minecraft, LocalIntRef)
*/
@WrapWithCondition(method = "renderSunset", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", ordinal = 0), remap = false)
private boolean iris$sunset$revertTextureDisable(int i) {
return false;
}
/**
* Disables {@link GL11#glEnable(int GL_TEXTURE_2D)} call
* @see #iris$atmosphereBlend$renderInDefaultProgram(CallbackInfo, Minecraft, LocalIntRef)
*/
@WrapWithCondition(method = "renderSunset", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", ordinal = 0), remap = false)
private boolean iris$sunsetBlend$revertTextureEnable(int i) {
return false;
}
/**
* Forces sunset to render with the default program
* <p>
* Fixes sunset not rendering at all with shaders enabled
*/
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lcom/hbm/dim/SkyProviderCelestial;renderSunset(FLnet/minecraft/client/multiplayer/WorldClient;Lnet/minecraft/client/Minecraft;FFLnet/minecraft/util/ResourceLocation;)V"), remap = false)
public void iris$sunset$renderInDefaultProgram(CallbackInfo ci, @Local(argsOnly = true) Minecraft mc, @Share("sunset$previousProgram") LocalIntRef sunset$previousProgram) {
sunset$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
mc.getTextureManager().bindTexture(GL_TEXTURE_2D_WORKAROUND);
}
/**
* Sets the program back before {@link #iris$sunset$renderInDefaultProgram(CallbackInfo, Minecraft, LocalIntRef)} call
*/
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lcom/hbm/dim/SkyProviderCelestial;renderSunset(FLnet/minecraft/client/multiplayer/WorldClient;Lnet/minecraft/client/Minecraft;FFLnet/minecraft/util/ResourceLocation;)V", shift = At.Shift.AFTER), remap = false)
public void iris$sunset$restorePreviousProgram(CallbackInfo ci, @Share("sunset$previousProgram") LocalIntRef sunset$previousProgram) {
GLStateManager.glUseProgram(sunset$previousProgram.get());
}
/**
* Disables sun blanking with shaders enabled (shaders ignore it anyway)
* <p>
* Fixes the huge white square near the sun with shaders enabled
*/
@Inject(method = "renderSun", at = @At(value="INVOKE", target="Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 1))
private void iris$sun$disableBlanking(CallbackInfo ci) {
if(IrisApi.getInstance().isShaderPackInUse())
Tessellator.instance.vertexCount = 0;
}
/**
* Moves the sun a little closer to the camera
* <p>
* Fixes z-fighting between the sun and the shader skybox
*/
@ModifyArg(method = "renderSun",
slice = @Slice(
from = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;addVertexWithUV(DDDDD)V", ordinal = 8),
to = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 3)),
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;addVertexWithUV(DDDDD)V"),
index = 1)
private double iris$sun$offsetY(double original) {
return original - 0.3D;
}
/**
* In accordance with {@link #iris$sun$offsetY(double)} - moves the flare closer to the camera
*/
@ModifyArg(method = "renderSun",
slice = @Slice(
from = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;addVertexWithUV(DDDDD)V", ordinal = 12),
to = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 4)),
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;addVertexWithUV(DDDDD)V"),
index = 1)
private double iris$sunFlare$offsetY(double original) {
return original - 0.3D;
}
/**
* Disables {@link GL11#glDisable(int GL_TEXTURE_2D)} call
* @see #iris$atmosphereBlend$renderInDefaultProgram(CallbackInfo, Minecraft, LocalIntRef)
*/
@WrapWithCondition(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", ordinal = 2), remap = false)
private boolean iris$atmosphereBlend$revertTextureDisable(int i) {
return false;
}
/**
* Disables {@link GL11#glEnable(int GL_TEXTURE_2D)} call
* @see #iris$atmosphereBlend$renderInDefaultProgram(CallbackInfo, Minecraft, LocalIntRef)
*/
@WrapWithCondition(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", ordinal = 3), remap = false)
private boolean iris$atmosphereBlend$revertTextureEnable(int i) {
return false;
}
/**
* Forces atmosphere blend to render with the default program
* <p>
* Sometimes shaders have serious visual issues if something is rendered with {@link GL11#glDisable(int GL_TEXTURE_2D)}
* <p>
* Using {@link net.minecraft.client.renderer.texture.TextureManager#bindTexture(ResourceLocation GL_TEXTURE_2D_WORKAROUND)}, we achieve the same effect as {@link GL11#glDisable(int GL_TEXTURE_2D)}, except it works with shaders
*/
@Inject(method = "renderCelestials", at = @At(value="INVOKE", target="Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V", ordinal = 10))
private void iris$atmosphereBlend$renderInDefaultProgram(CallbackInfo ci, @Local(argsOnly = true) Minecraft mc, @Share("atmo$previousProgram") LocalIntRef atmo$previousProgram) {
atmo$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
mc.getTextureManager().bindTexture(GL_TEXTURE_2D_WORKAROUND);
}
/**
* Sets the program back before {@link #iris$atmosphereBlend$renderInDefaultProgram(CallbackInfo, Minecraft, LocalIntRef)} call
*/
@Inject(method = "renderCelestials", at = @At(value="INVOKE", target="Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 10, shift = At.Shift.AFTER))
private void iris$atmosphereBlend$restorePreviousProgram(CallbackInfo ci, @Share("atmo$previousProgram") LocalIntRef atmo$previousProgram) {
GLStateManager.glUseProgram(atmo$previousProgram.get());
}
/**
* Forces celestial bodies to render with the default program
* <p>
* Requires `iris$atmosphereBlend$.*`
* <p>
* Fixes many bugs, such as:
* <p>
* - [All Shaders] At certain time, celestial bodies are rendered as a just solid color squares
* <p>
* - [All Shaders] Some celestial bodies appear as heavily (70-90%) translucent
* <p>
* - [Complementary 5.5/6.x] Stars render on top of celestial bodies; not behind them
* <p>
* - [Complementary 5.5.x] Celestial bodies have incorrect colors near the sun
* <p>
* - [Complementary 5.6.x] Celestial bodies don't render at all far from the sun
*/
@Inject(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V", ordinal = 5))
private void iris$celestials$renderInDefaultProgram(CallbackInfo ci, @Share("celestials$previousProgram") LocalIntRef celestials$previousProgram) {
celestials$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
}
/**
* Sets the program back before {@link #iris$celestials$renderInDefaultProgram(CallbackInfo, LocalIntRef)} call
*/
@Inject(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 5, shift = At.Shift.AFTER))
private void iris$celestials$restorePreviousProgram(CallbackInfo ci, @Share("celestials$previousProgram") LocalIntRef celestials$previousProgram) {
GLStateManager.glUseProgram(celestials$previousProgram.get());
}
/**
* Forces celestial body ring (back) to render with the default program
* @see #iris$celestials$renderInDefaultProgram(CallbackInfo, LocalIntRef)
*/
@Inject(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V", ordinal = 0))
private void iris$ringsBack$renderInDefaultProgram(CallbackInfo ci, @Share("ringsBack$previousProgram") LocalIntRef ringsBack$previousProgram) {
ringsBack$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
}
/**
* Sets the program back before {@link #iris$ringsBack$renderInDefaultProgram(CallbackInfo, LocalIntRef)} call
*/
@Inject(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 0, shift = At.Shift.AFTER))
private void iris$ringsBack$restorePreviousProgram(CallbackInfo ci, @Share("ringsBack$previousProgram") LocalIntRef ringsBack$previousProgram) {
GLStateManager.glUseProgram(ringsBack$previousProgram.get());
}
/**
* Forces celestial body ring (front) to render with the default program
* @see #iris$celestials$renderInDefaultProgram(CallbackInfo, LocalIntRef)
*/
@Inject(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;startDrawingQuads()V", ordinal = 11))
private void iris$ringsFront$renderInDefaultProgram(CallbackInfo ci, @Share("ringsFront$previousProgram") LocalIntRef ringsFront$previousProgram) {
ringsFront$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
}
/**
* Sets the program back before {@link #iris$ringsFront$renderInDefaultProgram(CallbackInfo, LocalIntRef)} call
*/
@Inject(method = "renderCelestials", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I", ordinal = 11, shift = At.Shift.AFTER))
private void iris$ringsFront$restorePreviousProgram(CallbackInfo ci, @Share("ringsFront$previousProgram") LocalIntRef ringsFront$previousProgram) {
GLStateManager.glUseProgram(ringsFront$previousProgram.get());
}
// Pipeline setup
@Inject(method = "render", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/Tessellator;instance:Lnet/minecraft/client/renderer/Tessellator;"))
private void iris$renderSky$beginNormalSky(CallbackInfo ci, @Share("pipeline") LocalRef<WorldRenderingPipeline> pipeline) {
pipeline.set(Iris.getPipelineManager().getPipelineNullable());
if(pipeline.get() != null)
pipeline.get().setPhase(WorldRenderingPhase.SKY);
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/entity/EntityClientPlayerMP;getPosition(F)Lnet/minecraft/util/Vec3;"))
private void iris$setVoidRenderStage(CallbackInfo ci, @Share("pipeline") LocalRef<WorldRenderingPipeline> pipeline) {
if(pipeline.get() != null)
pipeline.get().setPhase(WorldRenderingPhase.VOID);
}
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glRotatef(FFFF)V", ordinal = 2), remap = false)
private void iris$renderSky$tiltSun(CallbackInfo ci, @Share("pipeline") LocalRef<WorldRenderingPipeline> pipeline) {
if(pipeline.get() != null)
GLStateManager.glRotatef(pipeline.get().getSunPathRotation(), 0.0F, 0.0F, 1.0F);
}
@Inject(method = "renderSun", at = @At(value = "FIELD", target = "Lcom/hbm/dim/CelestialBody;texture:Lnet/minecraft/util/ResourceLocation;"), remap = false)
private void iris$setSunRenderStage(CallbackInfo ci) {
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setPhase(WorldRenderingPhase.SUN));
}
@Inject(method = "renderSunset", at = @At(value = "INVOKE", target = "Lcom/hbm/dim/SkyProviderCelestial;calcSunriseSunsetColors(FLnet/minecraft/client/multiplayer/WorldClient;Lnet/minecraft/client/Minecraft;FF)[F"), remap = false)
private void iris$setSunsetRenderStage(CallbackInfo ci) {
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setPhase(WorldRenderingPhase.SUNSET));
}
@Inject(method = "renderStars", at = @At(value = "HEAD"), remap = false)
private void iris$setStarRenderStage(CallbackInfo ci, @Share("pipeline") LocalRef<WorldRenderingPipeline> pipeline) {
Iris.getPipelineManager().getPipeline().ifPresent(p -> p.setPhase(WorldRenderingPhase.STARS));
}
}
@@ -1,29 +0,0 @@
package com.gtnewhorizons.angelica.mixins.late.client.ntmSpace;
import com.gtnewhorizons.angelica.config.CompatConfig;
import com.hbm.dim.SkyProviderCelestial;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import jss.notfine.core.Settings;
import net.irisshaders.iris.api.v0.IrisApi;
import net.minecraft.client.renderer.Tessellator;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@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$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;
}
}
}
@@ -1,7 +1,9 @@
package com.gtnewhorizons.angelica.mixins.late.client.ntmSpace;
import com.gtnewhorizons.angelica.utils.WorkaroundUtils;
import com.hbm.dim.laythe.SkyProviderLaytheSunset;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.client.Minecraft;
import org.lwjgl.opengl.GL11;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@@ -10,19 +12,15 @@ import org.spongepowered.asm.mixin.injection.At;
public class MixinSkyProviderLaytheSunset {
/**
* Disables {@link GL11#glDisable(int GL_TEXTURE_2D)} call
* Avoid program rebinding due to pipeline.setInputs
*/
@WrapWithCondition(method = "renderSunset", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V", ordinal = 0), remap = false)
private boolean iris$sunset$revertTextureDisable(int i) {
return false;
@WrapWithCondition(method = "renderSunset", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V"), remap = false)
private boolean iris$sunset$redirectTex2D(int cap) {
if(cap == GL11.GL_TEXTURE_2D) {
Minecraft.getMinecraft().renderEngine.bindTexture(WorkaroundUtils.GL_TEXTURE_2D_WORKAROUND);
return false;
}
return true;
}
/**
* Disables {@link GL11#glEnable(int GL_TEXTURE_2D)} call
*/
@WrapWithCondition(method = "renderSunset", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEnable(I)V", ordinal = 0), remap = false)
private boolean iris$sunsetBlend$revertTextureEnable(int i) {
return false;
}
}
@@ -0,0 +1,60 @@
package com.gtnewhorizons.angelica.mixins.late.client.ntmSpace;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.utils.WorkaroundUtils;
import com.hbm.dim.CelestialBody;
import com.hbm.dim.orbit.SkyProviderOrbit;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalIntRef;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.WorldClient;
import org.lwjgl.opengl.GL11;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = SkyProviderOrbit.class, priority = 100)
public class MixinSkyProviderOrbit {
/**
* Avoid program rebinding due to pipeline.setInputs
*/
@WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glDisable(I)V"), remap = false)
private boolean iris$common$redirectTex2D(int cap) {
if (cap == GL11.GL_TEXTURE_2D) {
Minecraft.getMinecraft().renderEngine.bindTexture(WorkaroundUtils.GL_TEXTURE_2D_WORKAROUND);
return false;
}
return true;
}
/**
* Force skybox to render with the default program | Fix various issues with shaders enabled
*/
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderHelper;disableStandardItemLighting()V"), remap = false)
public void iris$main$renderInDefaultProgram(CallbackInfo ci, @Share("main$previousProgram") LocalIntRef main$previousProgram) {
main$previousProgram.set(GLStateManager.getActiveProgram());
GLStateManager.glUseProgram(0);
}
@WrapOperation(method = "render",
at = @At(value = "INVOKE", target = "Lcom/hbm/dim/orbit/SkyProviderOrbit;renderSun(FLnet/minecraft/client/multiplayer/WorldClient;Lnet/minecraft/client/Minecraft;Lcom/hbm/dim/CelestialBody;DDFF)V"), remap = false)
private void iris$main$renderSunInShaderProgram(SkyProviderOrbit instance, float partialTicks, WorldClient world, Minecraft mc, CelestialBody sun, double sunSize, double coronaSize, float visibility, float pressure, Operation<Void> original, @Share("main$previousProgram") LocalIntRef main$previousProgram){
GLStateManager.glUseProgram(main$previousProgram.get());
original.call(instance, partialTicks, world, mc, sun, sunSize, coronaSize, visibility, pressure);
GLStateManager.glUseProgram(0);
}
/**
* Sets the program back before {@link #iris$main$renderInDefaultProgram(CallbackInfo, LocalIntRef)} call
*/
@Inject(method = "render", at = @At(value = "TAIL"), remap = false)
public void iris$main$restorePreviousProgram(CallbackInfo ci, @Share("main$previousProgram") LocalIntRef main$previousProgram) {
GLStateManager.glUseProgram(main$previousProgram.get());
}
}
@@ -70,8 +70,8 @@ public class UmbraClientTweaker implements IFMLLoadingPlugin, IEarlyMixinLoader
private static void verifyDependencies() {
DependencyVerifier.verify(UmbraClientTweaker.class, List.of(
new DependencyVerifier.Check(
"/com/gtnewhorizon/gtnhlib/client/renderer/PrimitiveExtractor.class",
"Missing dependency: Umbra requires GTNHLib 0.8.21+! Download: https://modrinth.com/mod/gtnhlib"),
"/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class",
"Missing dependency: Umbra requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib"),
new DependencyVerifier.Check(
"/xyz/wagyourtail/jvmdg/exc/MissingStubError.class",
"GTNHLib is outdated: Umbra requires GTNHLib 0.9.0+! Download: https://modrinth.com/mod/gtnhlib")