Compare commits

...
5 Commits
31 changed files with 1630 additions and 188 deletions

No files matched your search

+1 -1
View File
@@ -23,7 +23,7 @@ Targets GL 3.3+ core profile
# Required Dependencies
* [UniMixins](https://github.com/LegacyModdingMC/UniMixins/releases) >= 0.1.23
* [GTNHLib](https://github.com/GTNewHorizons/GTNHLib/releases) >= 0.9.0
* [GTNHLib](https://github.com/GTNewHorizons/GTNHLib/releases) >= 0.11.19
* NOTE: Some mods are not required, but a specific version is required if present - see: `Permanent Incompatibilities` below.
# Known (temporary) Incompatibilities
@@ -1,5 +1,10 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.taumc.glsl.grammar.GLSLLexer;
import org.taumc.glsl.grammar.GLSLParser;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.Transformer;
@@ -8,9 +13,11 @@ 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.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
@@ -182,8 +189,9 @@ public class CompatShaderTransformer {
transformer.rename("gl_FrontColor", "angelica_FrontColor");
transformer.prependMain("angelica_FrontColor = vec4(1.0);");
// Vertex attributes — replaces removed FFP vertex inputs with explicit in declarations
transformVertexAttributes(transformer, source);
final FFPInputs ffp = FFPInputs.detect(source);
assignVertexInputLocations(transformer, ffp);
transformVertexAttributes(transformer, source, ffp);
} else {
if (source.contains("gl_Color")) {
transformer.injectVariable("in vec4 angelica_FrontColor;");
@@ -310,25 +318,25 @@ public class CompatShaderTransformer {
/**
* Replace removed FFP vertex attributes with explicit {@code in} declarations at core profile attribute locations.
*/
private static void transformVertexAttributes(Transformer transformer, String source) {
if (source.contains("gl_Vertex") || source.contains("ftransform")) {
private static void transformVertexAttributes(Transformer transformer, String source, FFPInputs ffp) {
if (ffp.position()) {
transformer.injectVariable("layout(location = 0) in vec4 angelica_Vertex;");
transformer.rename("gl_Vertex", "angelica_Vertex");
}
// gl_Color in vertex shaders is the per-vertex color attribute, distinct from the fragment gl_Color (interpolated gl_FrontColor) handled above
if (source.contains("gl_Color")) {
if (ffp.color()) {
transformer.injectVariable("layout(location = 1) in vec4 angelica_Color;");
transformer.rename("gl_Color", "angelica_Color");
}
if (source.contains("gl_MultiTexCoord0")) {
if (ffp.uv0()) {
transformer.injectVariable("layout(location = 2) in vec4 angelica_MultiTexCoord0;");
transformer.rename("gl_MultiTexCoord0", "angelica_MultiTexCoord0");
}
if (source.contains("gl_MultiTexCoord1")) {
if (ffp.uv1()) {
transformer.injectVariable("layout(location = 3) in vec4 angelica_MultiTexCoord1;");
transformer.rename("gl_MultiTexCoord1", "angelica_MultiTexCoord1");
}
if (source.contains("gl_Normal")) {
if (ffp.normal()) {
transformer.injectVariable("layout(location = 4) in vec3 angelica_Normal;");
transformer.rename("gl_Normal", "angelica_Normal");
}
@@ -337,6 +345,145 @@ public class CompatShaderTransformer {
}
}
private static final int MAX_VERTEX_ATTRIBS = 16;
private record FFPInputs(boolean position, boolean color, boolean uv0, boolean uv1, boolean normal) {
static FFPInputs detect(String source) {
return new FFPInputs(
source.contains("gl_Vertex") || source.contains("ftransform"),
source.contains("gl_Color"),
source.contains("gl_MultiTexCoord0"),
source.contains("gl_MultiTexCoord1"),
source.contains("gl_Normal"));
}
}
/** Adaptation of Mesa's assign_attribute_or_color_locations */
private static void assignVertexInputLocations(Transformer transformer, FFPInputs ffp) {
int used = 0;
if (ffp.position()) used |= 1 << Usage.POSITION.getAttributeLocation();
if (ffp.color()) used |= 1 << Usage.COLOR.getAttributeLocation();
if (ffp.uv0()) used |= 1 << Usage.PRIMARY_UV.getAttributeLocation();
if (ffp.uv1()) used |= 1 << Usage.SECONDARY_UV.getAttributeLocation();
if (ffp.normal()) used |= 1 << Usage.NORMAL.getAttributeLocation();
record Input(String name, String baseType, String arraySuffix, int slots, int index) {}
final List<Input> toAssign = new ArrayList<>();
int index = 0;
for (TerminalNode storage : transformer.collectStorage()) {
final int tokenType = storage.getSymbol().getType();
if (tokenType != GLSLLexer.ATTRIBUTE && tokenType != GLSLLexer.IN) continue;
ParseTree node = storage.getParent();
while (node != null && !(node instanceof GLSLParser.Single_declarationContext)) {
node = node.getParent();
}
if (!(node instanceof GLSLParser.Single_declarationContext decl)) continue;
if (decl.typeless_declaration() == null) continue;
final GLSLParser.Type_specifierContext typeSpec = decl.fully_specified_type().type_specifier();
final String baseType = GlslTransformUtils.getFormattedShader(typeSpec.type_specifier_nonarray(), "").trim();
final int typeArraySlots = arraySlots(typeSpec.array_specifier());
final String typeArray = arraySuffix(typeSpec.array_specifier());
if (hasLayoutQualifier(decl)) {
final int loc = explicitLocation(decl);
final int slots = baseSlots(baseType) * typeArraySlots * arraySlots(decl.typeless_declaration().array_specifier());
if (loc >= 0) used |= slotMask(slots) << loc;
continue;
}
final List<GLSLParser.Typeless_declarationContext> declarators = new ArrayList<>();
declarators.add(decl.typeless_declaration());
if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) {
declarators.addAll(list.typeless_declaration());
}
for (GLSLParser.Typeless_declarationContext d : declarators) {
if (d.IDENTIFIER() == null) continue;
final int slots = baseSlots(baseType) * typeArraySlots * arraySlots(d.array_specifier());
final String arraySuffix = typeArray + arraySuffix(d.array_specifier());
toAssign.add(new Input(d.IDENTIFIER().getText(), baseType, arraySuffix, slots, index++));
}
}
toAssign.sort((a, b) -> a.slots() != b.slots() ? b.slots() - a.slots() : a.index() - b.index());
for (Input in : toAssign) {
final int loc = findAvailableSlots(used, in.slots());
if (loc < 0) continue;
used |= slotMask(in.slots()) << loc;
transformer.removeVariable(in.name());
transformer.variable = null;
transformer.injectVariable("layout(location = " + loc + ") in " + in.baseType() + " " + in.name() + in.arraySuffix() + ";");
}
}
private static String arraySuffix(GLSLParser.Array_specifierContext arraySpec) {
return arraySpec != null ? GlslTransformUtils.getFormattedShader(arraySpec, "").trim() : "";
}
private static int arraySlots(GLSLParser.Array_specifierContext arraySpec) {
if (arraySpec == null) return 1;
int product = 1;
for (GLSLParser.DimensionContext dim : arraySpec.dimension()) {
if (dim.constant_expression() == null) continue;
try {
product *= Integer.decode(GlslTransformUtils.getFormattedShader(dim.constant_expression(), "").trim());
} catch (NumberFormatException ignored) {}
}
return product;
}
private static int baseSlots(String type) {
final String dims = type.startsWith("dmat") ? type.substring(4) : type.startsWith("mat") ? type.substring(3) : null;
if (dims != null && !dims.isEmpty() && Character.isDigit(dims.charAt(0))) {
return dims.charAt(0) - '0';
}
return 1;
}
private static int slotMask(int slots) {
return (slots >= 32) ? -1 : (1 << slots) - 1;
}
private static int findAvailableSlots(int used, int needed) {
if (needed <= 0) return -1;
final int mask = slotMask(needed);
for (int i = 0; i <= MAX_VERTEX_ATTRIBS - needed; i++) {
if (((mask << i) & ~used) == (mask << i)) return i;
}
return -1;
}
private static int explicitLocation(GLSLParser.Single_declarationContext decl) {
final GLSLParser.Type_qualifierContext qualifier = decl.fully_specified_type().type_qualifier();
if (qualifier == null) return -1;
for (GLSLParser.Single_type_qualifierContext single : qualifier.single_type_qualifier()) {
final GLSLParser.Layout_qualifierContext layout = single.layout_qualifier();
if (layout == null) continue;
for (GLSLParser.Layout_qualifier_idContext id : layout.layout_qualifier_id_list().layout_qualifier_id()) {
if (id.IDENTIFIER() != null && "location".equals(id.IDENTIFIER().getText()) && id.constant_expression() != null) {
try {
return Integer.decode(GlslTransformUtils.getFormattedShader(id.constant_expression(), "").trim());
} catch (NumberFormatException ignored) {
return -1;
}
}
}
}
return -1;
}
private static boolean hasLayoutQualifier(GLSLParser.Single_declarationContext decl) {
final GLSLParser.Type_qualifierContext qualifier = decl.fully_specified_type().type_qualifier();
if (qualifier == null) return false;
for (GLSLParser.Single_type_qualifierContext single : qualifier.single_type_qualifier()) {
if (single.layout_qualifier() != null) return true;
}
return false;
}
private static void dumpShader(String original, String transformed, boolean isFragment, boolean wasTransformed) {
if (DUMP_DIR == null) return;
final int id = dumpCounter.getAndIncrement();
@@ -133,7 +133,10 @@ public class DisplayListManager {
}
public static UnsupportedOperationException unsupportedInList(String what) {
return new UnsupportedOperationException(what + " in display lists not yet implemented - if you see this, please report! " + describeCurrentCompilation());
final UnsupportedOperationException e = new UnsupportedOperationException( what + " in display lists not yet implemented - if you see this, please report! " + describeCurrentCompilation());
GLStateManager.warnOnce("unsupported:" + what, "Aborting display list {} compilation: {} unsupported during recording", glListId, what);
abortCompilation();
return e;
}
public static String describeCurrentCompilation() {
@@ -798,7 +801,8 @@ public class DisplayListManager {
*/
public static void glEndList() {
if (glListMode == 0) {
throw new RuntimeException("glEndList called outside of a display list!");
GLStateManager.warnOnce("endlist-outside", "glEndList called outside of a display list!");
return;
}
try {
finishCurrentList();
@@ -906,17 +910,56 @@ public class DisplayListManager {
// Parent's COMPILING callback is now active again
} else {
// Not nested - clear all compilation state
currentRecorder = null;
recordingThread = null;
accumulatedDraws = null;
pendingDraw = null;
transformCallback = null;
compilationStackTrace = null;
pendingTransformOps = null;
multMatrixSources = null;
drawRangeSources = null;
glListId = -1;
glListMode = 0;
resetCompilationState();
}
}
/** Clear all root-level compilation state back to "not recording". */
private static void resetCompilationState() {
currentRecorder = null;
recordingThread = null;
accumulatedDraws = null;
pendingDraw = null;
transformCallback = null;
compilationStackTrace = null;
pendingTransformOps = null;
multMatrixSources = null;
drawRangeSources = null;
glListId = -1;
glListMode = 0;
}
public static void abortCompilation() {
if (glListMode == 0 && compilationStack.isEmpty()) {
return;
}
final int levels = compilationStack.size() + (glListMode != 0 ? 1 : 0);
for (int i = 0; i < levels; i++) {
try {
TessellatorManager.stopCapturingDirect();
} catch (IllegalStateException ignored) {
break;
}
}
if (currentRecorder != null) {
currentRecorder.delete();
}
for (CompilationContext ctx : compilationStack) {
if (ctx.recorder() != null) {
ctx.recorder().delete();
}
}
compilationStack.clear();
resetCompilationState();
}
public static void abortIfLeaked() {
if (glListMode != 0 || !compilationStack.isEmpty()) {
GLStateManager.warnOnce("frame-leak", "Display list {} left open across a frame boundary - aborting to recover", glListId);
abortCompilation();
}
}
@@ -95,6 +95,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.IntSupplier;
import static com.gtnewhorizons.angelica.glsm.Vendor.AMD;
@@ -120,6 +121,12 @@ public class GLStateManager {
public static final Logger LOGGER = LogManager.getLogger("GLSM");
private static final Set<String> WARN_ONCE = ConcurrentHashMap.newKeySet();
public static void warnOnce(String key, String fmt, Object... args) {
if (WARN_ONCE.add(key)) LOGGER.warn(fmt, args);
}
// Thread Checking - must be early in static init order so isMainThread() works for state initialization
@Getter private static final Thread MainThread = Thread.currentThread();
@@ -2248,70 +2255,99 @@ public class GLStateManager {
}
public static void glDrawElements(int mode, ByteBuffer indices) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glDrawElements");
}
if (FeedbackManager.isFeedbackMode()) {
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, indices.remaining(), GL11.GL_UNSIGNED_BYTE, MemoryUtilities.memAddress(indices), indices.remaining());
if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture);
if (recordMode == RecordMode.COMPILE) return;
savedRecorder = DisplayListManager.pauseRecording();
} else if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices);
return;
}
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices.remaining(), GL11.GL_UNSIGNED_BYTE, indices);
return;
try {
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices.remaining(), GL11.GL_UNSIGNED_BYTE, indices);
return;
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices);
} finally {
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices);
}
public static void glDrawElements(int mode, IntBuffer indices) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glDrawElements");
}
if (FeedbackManager.isFeedbackMode()) {
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, indices.remaining(), GL11.GL_UNSIGNED_INT, MemoryUtilities.memAddress(indices), (long) indices.remaining() << 2);
if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture);
if (recordMode == RecordMode.COMPILE) return;
savedRecorder = DisplayListManager.pauseRecording();
} else if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices);
return;
}
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
return;
try {
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
return;
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices);
} finally {
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices);
}
public static void glDrawElements(int mode, ShortBuffer indices) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glDrawElements");
}
if (FeedbackManager.isFeedbackMode()) {
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, indices.remaining(), GL11.GL_UNSIGNED_SHORT, MemoryUtilities.memAddress(indices), (long) indices.remaining() << 1);
if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture);
if (recordMode == RecordMode.COMPILE) return;
savedRecorder = DisplayListManager.pauseRecording();
} else if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, indices);
return;
}
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
return;
try {
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
return;
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices);
} finally {
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices);
}
public static void glDrawElements(int mode, int count, int type, ByteBuffer indices) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glDrawElements");
}
if (FeedbackManager.isFeedbackMode()) {
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, count, type, MemoryUtilities.memAddress(indices), indices.remaining());
if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture);
if (recordMode == RecordMode.COMPILE) return;
savedRecorder = DisplayListManager.pauseRecording();
} else if (FeedbackManager.isFeedbackMode()) {
FeedbackManager.processDrawElements(mode, count, type, indices);
return;
}
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
return;
try {
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
return;
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, count, type, indices);
} finally {
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, count, type, indices);
}
public static void glDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
@@ -2332,14 +2368,16 @@ public class GLStateManager {
FeedbackManager.processDrawElements(mode, indices_count, type, indices_buffer_offset);
return;
}
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset);
return;
try {
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset);
return;
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices_count, type, indices_buffer_offset);
} finally {
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
preDraw(mode);
RENDER_BACKEND.drawElements(mode, indices_count, type, indices_buffer_offset);
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
public static void glDrawBuffer(int mode) {
@@ -2432,6 +2470,19 @@ public class GLStateManager {
if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder);
}
private static void ffpClientArrayPointer(int index, int size, int type, boolean normalized, int stride, long offset) {
if (VAOManager.isGenericPointerEnabled(index)) return;
VAOManager.setAttribute(index, size, type, normalized, stride, offset, boundVBO);
VAOManager.markConventional(index);
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, offset);
}
private static void ffpClientArrayPointer(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer) {
if (VAOManager.isGenericPointerEnabled(index)) return;
VAOManager.setAttribute(index, size, type, normalized, stride, pointer);
VAOManager.markConventional(index);
}
public static void glVertexPointer(int size, int stride, IntBuffer pointer) {
glVertexPointer(size, GL11.GL_INT, stride, MemoryUtilities.memByteBuffer(pointer));
}
@@ -2441,40 +2492,40 @@ public class GLStateManager {
}
public static void glVertexPointer(int size, int stride, FloatBuffer pointer) {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glVertexPointer(int size, int stride, DoubleBuffer pointer) {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glVertexPointer(int size, int type, int stride, ByteBuffer pointer) {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
ffpClientArrayPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
}
public static void glVertexPointer(int size, int type, int stride, long pointer_buffer_offset) {
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer_buffer_offset);
ffpClientArrayPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer_buffer_offset);
}
public static void glColorPointer(int size, int stride, FloatBuffer pointer) {
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_FLOAT, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_FLOAT, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glColorPointer(int size, int stride, DoubleBuffer pointer) {
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_DOUBLE, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.COLOR.getAttributeLocation(), size, GL11.GL_DOUBLE, Usage.COLOR.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glColorPointer(int size, int type, int stride, ByteBuffer pointer) {
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
ffpClientArrayPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
}
public static void glColorPointer(int size, boolean unsigned, int stride, ByteBuffer pointer) {
final int type = unsigned ? GL11.GL_UNSIGNED_BYTE : GL11.GL_BYTE;
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
ffpClientArrayPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer);
}
public static void glColorPointer(int size, int type, int stride, long pointer_buffer_offset) {
glVertexAttribPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer_buffer_offset);
ffpClientArrayPointer(Usage.COLOR.getAttributeLocation(), size, type, Usage.COLOR.isNormalized(), stride, pointer_buffer_offset);
}
public static void glNormalPointer(int stride, FloatBuffer pointer) {
@@ -2494,33 +2545,33 @@ public class GLStateManager {
}
public static void glNormalPointer(int type, int stride, ByteBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
ffpClientArrayPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
}
public static void glNormalPointer(int type, int stride, FloatBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_FLOAT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_FLOAT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, ShortBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_SHORT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_SHORT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, IntBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_INT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_INT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, DoubleBuffer pointer) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_DOUBLE, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_DOUBLE, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glNormalPointer(int type, int stride, long pointer_buffer_offset) {
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer_buffer_offset);
ffpClientArrayPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer_buffer_offset);
}
public static void glTexCoordPointer(int size, int type, int stride, ByteBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, type, false, stride, pointer);
ffpClientArrayPointer(loc, size, type, false, stride, pointer);
}
public static void glTexCoordPointer(int size, int stride, FloatBuffer pointer) {
@@ -2542,31 +2593,31 @@ public class GLStateManager {
public static void glTexCoordPointer(int size, int type, int stride, FloatBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(loc, size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, ShortBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, GL11.GL_SHORT, false, stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(loc, size, GL11.GL_SHORT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, IntBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, GL11.GL_INT, false, stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(loc, size, GL11.GL_INT, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, DoubleBuffer pointer) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
ffpClientArrayPointer(loc, size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
}
public static void glTexCoordPointer(int size, int type, int stride, long pointer_buffer_offset) {
final int loc = texCoordAttributeLocation();
if (loc < 0) return;
glVertexAttribPointer(loc, size, type, false, stride, pointer_buffer_offset);
ffpClientArrayPointer(loc, size, type, false, stride, pointer_buffer_offset);
}
public static void glEnableClientState(int cap) {
@@ -5792,6 +5843,9 @@ public class GLStateManager {
generateVertexShaderIfNeeded(program);
}
RENDER_BACKEND.linkProgram(program);
if (ShaderManager.getInstance().isEnabled() && RENDER_BACKEND.getProgrami(program, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
LOGGER.warn("Program {} failed to link: {}", program, RENDER_BACKEND.getProgramInfoLog(program));
}
CompatUniformManager.onLinkProgram(program);
}
@@ -135,7 +135,8 @@ public final class FragmentKey {
int highestEnabled = -1;
long unit0Bits = 0;
for (int i = 0; i < MAX_UNITS; i++) {
final boolean texEnabled = GLStateManager.getTextures().getTextureUnitStates(i).isEnabled();
// Per Mesa - an enabled unit with no complete texture bound counts as disabled.
final boolean texEnabled = GLStateManager.getTextures().getTextureUnitStates(i).isEnabled() && GLStateManager.getTextures().getTextureUnitBindings(i).getBinding() != 0;
if (texEnabled) highestEnabled = i;
final long unitBits = packUnit(i, texEnabled);
@@ -85,6 +85,7 @@ public final class VAOManager {
a.offset = offset;
a.vboId = vboId;
a.clientPointer = null;
a.genericPointer = true;
if (wasClient && a.enabled) {
clientSideEnabledCount--;
}
@@ -101,11 +102,24 @@ public final class VAOManager {
a.offset = 0;
a.vboId = 0;
a.clientPointer = pointer;
a.genericPointer = true;
if (!wasClient && a.enabled) {
clientSideEnabledCount++;
}
}
public static boolean isGenericPointerEnabled(int index) {
if (index < 0 || index >= MAX_ATTRIBS) return false;
final Attrib a = currentAttribs[index];
return a != null && a.genericPointer && a.enabled;
}
public static void markConventional(int index) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = currentAttribs[index];
if (a != null) a.genericPointer = false;
}
public static void setEnabled(int index, boolean enabled) {
if (index < 0 || index >= MAX_ATTRIBS) return;
final Attrib a = getAttrib(index);
@@ -239,6 +253,7 @@ public final class VAOManager {
public long offset;
public int vboId;
public ByteBuffer clientPointer;
public boolean genericPointer;
public boolean isClientSide() {
return enabled && clientPointer != null;
@@ -253,6 +268,7 @@ public final class VAOManager {
offset = 0;
vboId = 0;
clientPointer = null;
genericPointer = false;
}
public int effectiveStride() {
@@ -9,8 +9,6 @@ import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetLong;
@@ -52,20 +50,14 @@ public final class IndexedDrawCapture {
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);
GLStateManager.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);
GLStateManager.warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 — skipping", indicesCount);
return null;
}
final int srcIndexSize = VAOManager.Attrib.glTypeSizeBytes(srcIndexType);
@@ -74,29 +66,53 @@ public final class IndexedDrawCapture {
final int prevEBO = GLStateManager.getBoundEBO();
final int prevVBO = GLStateManager.getBoundVBO();
ByteBuffer eboSrc = null;
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) {
GLStateManager.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) {
GLStateManager.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);
return bake(mode, indicesCount, srcIndexType, MemoryUtilities.memAddress0(eboSrc));
} finally {
if (eboSrc != null) MemoryUtilities.memFree(eboSrc);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
}
}
public static IndexedDrawCapture createFromClientIndices(int mode, int indicesCount, int srcIndexType, long indexSrcAddr, long srcByteLength) {
if (indicesCount == 0) return null;
if (indexSrcAddr == 0L) return null;
if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) {
GLStateManager.warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 - skipping", indicesCount);
return null;
}
final int idxSize = VAOManager.Attrib.glTypeSizeBytes(srcIndexType);
if (idxSize <= 0) return null;
if ((long) indicesCount * idxSize > srcByteLength) {
GLStateManager.warnOnce("client-idx-range", "[IndexedDrawCapture] index count {} ({} bytes) exceeds client buffer {} bytes - skipping", indicesCount, (long) indicesCount * idxSize, srcByteLength);
return null;
}
return bake(mode, indicesCount, srcIndexType, indexSrcAddr);
}
private static IndexedDrawCapture bake(int mode, int indicesCount, int srcIndexType, long indexSrcAddr) {
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);
final long minMax = QuadConverter.scanMinMaxIndex(indexSrcAddr, srcIndexType, indicesCount);
if (minMax == -1L) {
warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType);
GLStateManager.warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType);
return null;
}
final int minVtx = (int) (minMax & 0xFFFFFFFFL);
@@ -110,7 +126,7 @@ public final class IndexedDrawCapture {
if (snap.get(i) != null) enabledCount++;
}
if (enabledCount == 0) {
warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping");
GLStateManager.warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping");
return null;
}
final int[] locations = new int[enabledCount];
@@ -137,6 +153,13 @@ public final class IndexedDrawCapture {
for (int a = 0; a < enabledCount; a++) {
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
final int relOffset = (int) (d.offset() - d.readBufferBaseOffset());
final long readStart = relOffset + (long) minVtx * d.effectiveStride();
final long readEnd = relOffset + (long) maxVtx * d.effectiveStride() + (long) d.size() * d.typeSizeBytes();
if (readStart < 0 || readEnd > d.readBuffer().capacity()) {
GLStateManager.warnOnce("vtx-oob", "[IndexedDrawCapture] attrib {} read extent {} exceeds snapshot {} bytes - skipping", d.location(), readEnd, d.readBuffer().capacity());
return null;
}
if (a == 0) {
sharedSrcBuffer = d.readBuffer();
sharedSrcStride = d.effectiveStride();
@@ -169,12 +192,12 @@ public final class IndexedDrawCapture {
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);
QuadConverter.triangulateQuads(indexSrcAddr, 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);
QuadConverter.widenIndices(indexSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, indicesCount, minVtx);
bakedDrawMode = mode;
}
@@ -184,14 +207,11 @@ public final class IndexedDrawCapture {
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);
}
}
@@ -1,6 +1,8 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -613,4 +615,239 @@ class CompatShaderTransformerTest {
assertFalse(result.matches("(?s).*\\bnew\\b(?!\\s*\\().*"), "'new' as identifier must be renamed in core profile output\n\n" + result);
assertTrue(result.contains("angelica_renamed_new"), "'new' should be renamed to angelica_renamed_new\n\n" + result);
}
private static String compact(String s) {
return s.replaceAll("\\s+", "");
}
private static String core(String src) {
return compact(CompatShaderTransformer.transform(src, false));
}
private static void assertLoc(String c, int loc, String decl) {
assertTrue(c.contains(compact("layout(location=" + loc + ")in" + decl)), decl + " expected at location " + loc + "\n\n" + c);
}
private static void assertNoLoc(String c, int loc, String decl) {
assertFalse(c.contains(compact("layout(location=" + loc + ")in" + decl)), decl + " must not be at location " + loc + "\n\n" + c);
}
@Test
void testCustomPositionAttributePinnedToLocationZero() {
// Betweenlands blshader fix
String src = """
#version 120
attribute vec4 a_position;
uniform mat4 u_projMat;
uniform vec2 u_outSize;
varying vec2 v_texCoord;
void main(){
gl_Position = u_projMat * vec4(a_position.xy, 0.0, 1.0);
v_texCoord = a_position.xy / u_outSize;
}
""";
String result = CompatShaderTransformer.transform(src, false);
assertLoc(compact(result), 0, "vec4 a_position");
assertTrue(result.contains("#version 330 core"), result);
}
@Test
void testMultipleLocationlessInputsGetSequentialLocations() {
String c = core("""
#version 120
attribute vec4 a_pos;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_pos.xy, 0.0, 1.0);
}
""");
assertLoc(c, 0, "vec4 a_pos");
assertLoc(c, 1, "vec2 a_uv");
}
@Test
void testCustomInputReservesAroundGlVertex() {
String c = core("""
#version 120
attribute vec4 a_foo;
void main() {
gl_Position = ftransform() + a_foo;
}
""");
assertLoc(c, 0, "vec4 angelica_Vertex");
assertLoc(c, 1, "vec4 a_foo");
}
@Test
void testCustomInputSkipsFfpSlots() {
String c = core("""
#version 120
attribute vec4 a_foo;
void main() {
gl_Position = vec4(a_foo.xy, 0.0, 1.0) + gl_Color;
}
""");
assertLoc(c, 0, "vec4 a_foo");
assertLoc(c, 1, "vec4 angelica_Color");
}
@Test
void testExplicitLowLocationReserved() {
String c = core("""
#version 120
layout(location = 0) in vec4 a_x;
attribute vec4 a_y;
void main() {
gl_Position = a_x + a_y;
}
""");
assertLoc(c, 0, "vec4 a_x");
assertLoc(c, 1, "vec4 a_y");
}
@Test
void testMatrixInputSortedFirst() {
String c = core("""
#version 120
attribute vec4 a_a;
attribute mat4 a_m;
void main() {
gl_Position = a_m * a_a;
}
""");
assertLoc(c, 0, "mat4 a_m"); // 4-slot matrix sorted first
assertLoc(c, 4, "vec4 a_a");
}
@Test
void testInputNamedLocationStillPinned() {
assertLoc(core("""
#version 120
attribute vec4 a_location;
void main() {
gl_Position = vec4(a_location.xy, 0.0, 1.0);
}
"""), 0, "vec4 a_location");
}
@Test
void testInputWithExplicitLayoutLeftAlone() {
String c = core("""
#version 120
layout(location = 3) in vec4 a_data;
attribute vec4 a_position;
varying vec2 v_uv;
void main() {
v_uv = a_data.xy;
gl_Position = vec4(a_position.xy, 0.0, 1.0);
}
""");
assertLoc(c, 0, "vec4 a_position"); // sole location-less input
assertLoc(c, 3, "vec4 a_data"); // explicit location preserved
assertNoLoc(c, 0, "vec4 a_data");
}
@Test
void testPinnedShaderRemainsWellFormed() {
String src = """
#version 120
attribute vec4 a_position;
void main() {
gl_FrontColor = gl_Color;
gl_TexCoord[0] = gl_MultiTexCoord0;
gl_Position = gl_ModelViewMatrix * vec4(a_position.xyz, 1.0);
}
""";
String result = CompatShaderTransformer.transform(src, false);
String c = compact(result);
assertLoc(c, 0, "vec4 a_position");
assertLoc(c, 1, "vec4 angelica_Color");
assertTrue(c.contains("uniformmat4angelica_ModelViewMatrix"), "matrix uniform injected\n\n" + result);
assertFalse(c.contains("gl_ModelViewMatrix"), "builtin rewritten (not fallback)\n\n" + result);
final int mainIdx = result.indexOf("void main");
assertTrue(mainIdx > 0, result);
assertTrue(result.indexOf("angelica_ModelViewMatrix") < mainIdx, "uniform before main\n\n" + result);
assertTrue(result.indexOf("a_position") < mainIdx, "pinned input before main\n\n" + result);
}
@Test
void testGlVertexShaderDoesNotPinCustomAttribute() {
String c = core("""
#version 120
attribute float activelights;
void main() {
gl_Position = ftransform();
float x = activelights;
}
""");
assertLoc(c, 0, "vec4 angelica_Vertex");
assertNoLoc(c, 0, "float activelights");
}
@ParameterizedTest // array on the declarator (vec4 a[2]) and on the type (vec4[2] a) must both round-trip
@ValueSource(strings = {"attribute vec4 positions[2];", "attribute vec4[2] positions;"})
void testArrayAttributePreservedEitherSyntax(String decl) {
String c = core("""
#version 120
%s
attribute vec4 a_tail;
void main() {
gl_Position = positions[0] + positions[1] + a_tail;
}
""".formatted(decl));
assertLoc(c, 0, "vec4 positions[2]"); // 2-slot array pinned to 0..1
assertLoc(c, 2, "vec4 a_tail");
}
@Test
void testMultiDeclaratorSplitIntoSeparateLocations() {
String result = CompatShaderTransformer.transform("""
#version 120
attribute vec4 a, b;
void main() {
gl_Position = a + b;
}
""", false);
String c = compact(result);
assertLoc(c, 0, "vec4 a");
assertLoc(c, 1, "vec4 b");
assertFalse(result.matches("(?s).*\\battribute\\b.*"), "no deprecated 'attribute' keyword may survive\n\n" + result);
assertTrue(result.contains("#version 330 core"), result);
}
@Test
void testMultiDeclaratorWithMixedArray() {
String result = CompatShaderTransformer.transform("""
#version 120
attribute vec4 a, b[2];
void main() {
gl_Position = a + b[0] + b[1];
}
""", false);
String c = compact(result);
// b takes 2 slots; sorted before the 1-slot a, so b -> 0..1, a -> 2.
assertLoc(c, 0, "vec4 b[2]");
assertLoc(c, 2, "vec4 a");
assertFalse(result.matches("(?s).*\\battribute\\b.*"), "no deprecated 'attribute' keyword may survive\n\n" + result);
}
@Test
void testHexExplicitLocationReserved() {
String c = core("""
#version 120
layout(location = 0x1) in vec4 a_x;
attribute vec4 a_y;
void main() {
gl_Position = a_x + a_y;
}
""");
assertTrue(c.contains("layout(location=0x1)invec4a_x"), "hex explicit location preserved\n\n" + c);
assertNoLoc(c, 1, "vec4 a_y"); // must not collide with the hex-reserved slot
assertLoc(c, 0, "vec4 a_y");
}
}
@@ -9,9 +9,11 @@ import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests GLSM display list behavior matches OpenGL spec. */
@@ -257,4 +259,30 @@ class DisplayListCompilationGLSMTest {
GLStateManager.glEndList();
}
@Test
void glsm_unsupportedOpInList_abortsCleanly() {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
assertTrue(DisplayListManager.isRecording(), "recording after glNewList");
assertThrows(UnsupportedOperationException.class, () -> GLStateManager.glReadBuffer(GL11.GL_FRONT));
assertFalse(DisplayListManager.isRecording(), "recording state must be reset after an in-list throw");
assertEquals(DisplayListManager.RecordMode.NONE, DisplayListManager.getRecordMode());
assertDoesNotThrow(() -> GLStateManager.glReadBuffer(GL11.GL_FRONT));
assertDoesNotThrow(GLStateManager::glEndList);
}
@Test
void glsm_abortIfLeaked_recoversOpenList() {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
assertTrue(DisplayListManager.isRecording());
DisplayListManager.abortIfLeaked();
assertFalse(DisplayListManager.isRecording(), "frame-begin backstop must clear a leaked open list");
}
}
@@ -122,6 +122,68 @@ public class GLSM_VAO_UnitTest {
UniversalVAO.deleteVertexArrays(vao);
}
@Test
void testGenericAttributeSupersedesFfpConventional() {
final int vao = UniversalVAO.genVertexArrays();
glBindVertexArray(vao);
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(0, 1, GL11.GL_FLOAT, false, 0, 0L);
GLStateManager.glEnableVertexAttribArray(0);
GLStateManager.glVertexPointer(3, GL11.GL_FLOAT, 0, 0L);
Assertions.assertEquals(1, VAOManager.get(0).size, "enabled generic attribute must supersede the FFP conventional array at the same location");
Assertions.assertTrue(VAOManager.get(0).genericPointer, "location must remain generic-sourced");
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
}
@Test
void testGenericAttributeWinsRegardlessOfOrder() {
final int vao = UniversalVAO.genVertexArrays();
glBindVertexArray(vao);
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexPointer(3, GL11.GL_FLOAT, 0, 0L);
GLStateManager.glVertexAttribPointer(0, 1, GL11.GL_FLOAT, false, 0, 0L);
GLStateManager.glEnableVertexAttribArray(0);
Assertions.assertEquals(1, VAOManager.get(0).size, "generic attribute set after the FFP conventional array must still win");
Assertions.assertTrue(VAOManager.get(0).genericPointer, "location must remain generic-sourced");
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
}
@Test
void testFfpConventionalAppliesWithoutGenericAttribute() {
final int vao = UniversalVAO.genVertexArrays();
glBindVertexArray(vao);
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexPointer(3, GL11.GL_FLOAT, 0, 0L);
GLStateManager.glEnableClientState(GL11.GL_VERTEX_ARRAY);
Assertions.assertEquals(3, VAOManager.get(0).size, "plain FFP conventional array must apply when no generic attribute occupies the location");
Assertions.assertFalse(VAOManager.get(0).genericPointer, "location must be conventional-sourced");
GLStateManager.glDisableClientState(GL11.GL_VERTEX_ARRAY);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
GL15.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
}
private static void testClientStates() {
verifyState(GL11.GL_VERTEX_ARRAY, false, "GL_VERTEX_ARRAY Default State");
verifyState(GL11.GL_TEXTURE_COORD_ARRAY, false, "GL_TEXTURE_COORD_ARRAY Default State");
@@ -28,6 +28,7 @@ class FFPCombineShaderTest {
private final List<Integer> shadersToDelete = new ArrayList<>();
private final List<Integer> programsToDelete = new ArrayList<>();
private final List<Integer> texturesToDelete = new ArrayList<>();
@AfterEach
void cleanup() {
@@ -37,15 +38,25 @@ class FFPCombineShaderTest {
programsToDelete.clear();
shadersToDelete.clear();
// Reset texture units to defaults
// Reset texture units to defaults (unbind so a later test does not see a stale binding)
for (int i = 3; i >= 0; i--) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.getTextures().getTexEnvState(i).reset();
if (i > 0) {
GLStateManager.getTextures().getTextureUnitStates(i).disable();
}
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
for (int t : texturesToDelete) GL11.glDeleteTextures(t);
texturesToDelete.clear();
}
private int bindUnitTexture() {
final int tex = GL11.glGenTextures();
texturesToDelete.add(tex);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
return tex;
}
@Test
@@ -113,6 +124,7 @@ class FFPCombineShaderTest {
void testSingleUnitCombineCompiles() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE);
@@ -169,6 +181,7 @@ class FFPCombineShaderTest {
void testAllCombineFunctionsCompile(String name, int glFunc, int numArgs) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, glFunc);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
@@ -202,6 +215,7 @@ class FFPCombineShaderTest {
void testCombineScopeDeclaresColorOutsideBlock() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE);
@@ -258,6 +272,7 @@ class FFPCombineShaderTest {
// Unit 0: COMBINE / SUBTRACT
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL13.GL_SUBTRACT);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
@@ -273,6 +288,7 @@ class FFPCombineShaderTest {
// Unit 2: COMBINE / ADD
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_ADD);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE);
@@ -286,6 +302,7 @@ class FFPCombineShaderTest {
// Unit 3: simple MODULATE
GLStateManager.glActiveTexture(GL13.GL_TEXTURE3);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
// Restore active unit
@@ -0,0 +1,72 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLSMCoreExtension;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMCoreExtension.class)
class FFPUnboundTextureUnitTest {
private final List<Integer> texturesToDelete = new ArrayList<>();
@AfterEach
void cleanup() {
for (int i = 3; i >= 0; i--) {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
if (i > 0) GLStateManager.getTextures().getTextureUnitStates(i).disable();
}
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
for (int t : texturesToDelete) GL11.glDeleteTextures(t);
texturesToDelete.clear();
}
private void bindUnitTexture() {
final int tex = GL11.glGenTextures();
texturesToDelete.add(tex);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
}
@Test
void enabledUnitWithoutTextureIsExcluded() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.enableTexture(); // enabled, nothing bound
GLStateManager.glActiveTexture(GL13.GL_TEXTURE3);
GLStateManager.enableTexture(); // enabled, nothing bound
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final FragmentKey fk = FragmentKey.fromState();
assertEquals(1, fk.nrEnabledUnits(), "units 2/3 have no texture bound -> excluded -> only unit 0");
assertEquals(0b0001, fk.enabledUnitMask(), "only unit 0 in the key; enabled-but-unbound units 2/3 excluded");
assertTrue(fk.unitEnabled(0), "unit 0 (texture bound) stays enabled");
}
@Test
void enabledUnitsWithTextureAreCounted() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.enableTexture();
bindUnitTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final FragmentKey fk = FragmentKey.fromState();
assertTrue(fk.unitEnabled(0));
assertTrue(fk.unitEnabled(2), "unit 2 with a bound texture must be counted");
assertEquals(3, fk.nrEnabledUnits(), "highest enabled is unit 2 -> nrEnabledUnits=3");
}
}
@@ -9,6 +9,9 @@ import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -17,7 +20,10 @@ import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -25,6 +31,7 @@ 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;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
@@ -521,7 +528,153 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
"listBA: batch[1] must match the second-recorded layout");
}
// === local helpers ===
private static Stream<Arguments> clientIndexedDrawCases() {
return Stream.of(
arguments("byte indices", GL11.GL_COMPILE, (Runnable) () -> {
final ByteBuffer idx = BufferUtils.createByteBuffer(3);
idx.put(new byte[]{0, 1, 2}).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx);
}, GL11.GL_TRIANGLES, 3),
arguments("short indices", GL11.GL_COMPILE, (Runnable) () -> {
final ShortBuffer idx = BufferUtils.createShortBuffer(3);
idx.put(new short[]{0, 1, 2}).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx);
}, GL11.GL_TRIANGLES, 3),
arguments("int indices", GL11.GL_COMPILE, (Runnable) () -> {
final IntBuffer idx = BufferUtils.createIntBuffer(3);
idx.put(new int[]{0, 1, 2}).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx);
}, GL11.GL_TRIANGLES, 3),
arguments("explicit count+type", GL11.GL_COMPILE, (Runnable) () -> {
final ByteBuffer idx = BufferUtils.createByteBuffer(3 * 2).order(ByteOrder.nativeOrder());
idx.putShort((short) 0).putShort((short) 1).putShort((short) 2).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, idx);
}, GL11.GL_TRIANGLES, 3),
arguments("quads triangulate at record", GL11.GL_COMPILE, (Runnable) () -> {
final IntBuffer idx = BufferUtils.createIntBuffer(4);
idx.put(new int[]{0, 1, 2, 3}).flip();
GLStateManager.glDrawElements(GL11.GL_QUADS, idx);
}, GL11.GL_TRIANGLES, 6),
arguments("compile-and-execute fires live and bakes", GL11.GL_COMPILE_AND_EXECUTE, (Runnable) () -> {
final IntBuffer idx = BufferUtils.createIntBuffer(3);
idx.put(new int[]{0, 1, 2}).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx);
}, GL11.GL_TRIANGLES, 3)
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("clientIndexedDrawCases")
void clientIndexedDraw_capturedAndWidenedToUnsignedInt(String name, int listMode, Runnable draw, int expectedMode, int expectedCount) {
final int list = recordClientIndexDraw(listMode, draw);
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(expectedMode, cmd.getDrawMode());
assertEquals(expectedCount, cmd.getIndexCount());
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "client indices always widen to UNSIGNED_INT");
}
@Test
void clientVerticesAndIndices_pointersSetDuringRecording_bake() {
final float[][] verts = {{0f, 0f, 0f}, {1f, 0f, 0f}, {1f, 1f, 0f}, {0f, 1f, 0f}};
final ByteBuffer clientVerts = BufferUtils.createByteBuffer(verts.length * 12).order(ByteOrder.nativeOrder());
for (float[] v : verts) {
clientVerts.putFloat(v[0]);
clientVerts.putFloat(v[1]);
clientVerts.putFloat(v[2]);
}
clientVerts.flip();
final IntBuffer indices = BufferUtils.createIntBuffer(6);
indices.put(new int[]{0, 1, 2, 2, 3, 0}).flip();
final int vao = newVao();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
VAOManager.setAttribute(0, 3, GL11.GL_FLOAT, false, 0, clientVerts);
VAOManager.enableAttribute(0);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, indices);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
assertNotNull(cmd);
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
assertEquals(6, cmd.getIndexCount());
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType());
final ByteBuffer owned = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), verts.length * 12);
for (int i = 0; i < verts.length; i++) {
assertEquals(verts[i][0], owned.getFloat(i * 12), 0f, "pos.x v=" + i);
assertEquals(verts[i][1], owned.getFloat(i * 12 + 4), 0f, "pos.y v=" + i);
assertEquals(verts[i][2], owned.getFloat(i * 12 + 8), 0f, "pos.z v=" + i);
}
MemoryUtilities.memFree(owned);
}
@Test
void quadOffsetDraw_compileAndExecute_doesNotPauseRecordingForRestOfList() {
final int vao = setupPosColorVao(defaultQuadVbo());
final int quadEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int triEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, quadEbo);
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
assertTrue(DisplayListManager.isRecording(), "recording must resume after a GL_QUADS offset draw");
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, triEbo);
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertEquals(2, allBatchedCmds(list).size(), "both the QUADS draw and the following triangle draw must be recorded");
}
private enum MalformedClientDraw { OVERSIZED_COUNT, INDEX_PAST_VERTEX_BUFFER }
@ParameterizedTest(name = "{0}")
@EnumSource(MalformedClientDraw.class)
void malformedClientDraw_skippedAndRecordingSurvives(MalformedClientDraw scenario) {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
switch (scenario) {
case OVERSIZED_COUNT -> {
GLStateManager.glBindVertexArray(setupPosColorVao(defaultQuadVbo()));
final ByteBuffer idx = BufferUtils.createByteBuffer(3 * 2).order(ByteOrder.nativeOrder());
idx.putShort((short) 0).putShort((short) 1).putShort((short) 2).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 100, GL11.GL_UNSIGNED_SHORT, idx);
}
case INDEX_PAST_VERTEX_BUFFER -> {
final int vbo = uploadPosColorVbo(new float[]{0f, 0f, 1f, 0f}, new byte[][]{{1, 1, 1, 1}, {2, 2, 2, 2}});
GLStateManager.glBindVertexArray(setupPosColorVao(vbo));
final IntBuffer idx = BufferUtils.createIntBuffer(4);
idx.put(new int[]{0, 1, 2, 3}).flip();
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx);
}
}
assertTrue(DisplayListManager.isRecording(), "recording survives a skipped malformed draw");
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
assertNull(firstBatchedCmd(list), "malformed draw must be skipped, not captured or over-read");
}
private int recordClientIndexDraw(int listMode, Runnable draw) {
final int vao = setupPosColorVao(defaultQuadVbo());
final int list = newList();
GLStateManager.glNewList(list, listMode);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
draw.run();
GLStateManager.glBindVertexArray(0);
GLStateManager.glEndList();
return list;
}
private static void assertUintIndices(ByteBuffer ib, int... expected) {
for (int i = 0; i < expected.length; i++) {
@@ -16,7 +16,7 @@ import static com.gtnewhorizons.angelica.AngelicaMod.MOD_ID;
name = "Angelica",
version = Tags.VERSION,
dependencies = """
required-after:gtnhlib@[0.8.21,);\
required-after:gtnhlib@[0.11.19,);\
before:lwjgl3ify@[1.5.3,);\
after:hodgepodge@[2.4.4,);\
after:CodeChickenCore@[1.2.0,);\
@@ -120,7 +120,10 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
"Missing dependency: Angelica requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib"),
new DependencyVerifier.Check(
"/com/gtnewhorizon/gtnhlib/client/renderer/VertexCallbackManager.class",
"GTNHLib is outdated: Angelica requires GTNHLib 0.10.0 or newer! Download: https://modrinth.com/mod/gtnhlib")
"GTNHLib is outdated: Angelica requires GTNHLib 0.10.0 or newer! Download: https://modrinth.com/mod/gtnhlib"),
new DependencyVerifier.Check(
"/it/unimi/dsi/fastutil/objects/ObjectBooleanBiConsumer.class",
"GTNHLib is outdated: Angelica requires GTNHLib 0.11.19 or newer (fastutil 8.5.18+)! Download: https://modrinth.com/mod/gtnhlib")
));
} catch (RuntimeException ex) {
fatalDependencyError(ex.getMessage());
@@ -11,6 +11,7 @@ import net.coderbot.iris.gui.screen.ShaderPackScreen;
import net.coderbot.iris.shaderpack.ShaderPack;
import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiTextField;
import net.minecraft.client.renderer.Tessellator;
import java.util.ArrayList;
@@ -24,6 +25,12 @@ public class ShaderPackOptionList extends IrisGuiSlot {
private OptionMenuContainer container;
private final List<BaseEntry> entries = new ArrayList<>();
@Getter
private boolean searchModeActive = false;
private String typedSearchQuery = "";
private int reservedLeftWidth = 48; // Pixels reserved on the left of the header row for the search/clear button + gap.
private GuiTextField searchBox = null;
public ShaderPackOptionList(ShaderPackScreen screen, NavigationController navigation, ShaderPack pack, Minecraft client, int width, int height, int top, int bottom, int left, int right) {
super(client, width, height, top, bottom, 24);
this.navigation = navigation;
@@ -36,6 +43,74 @@ public class ShaderPackOptionList extends IrisGuiSlot {
this.container = pack.getMenuContainer();
}
/** Applies {@code query} to the option container and rebuilds the list rows. */
public void updateSearchQuery(String query) {
typedSearchQuery = query != null ? query : "";
if (this.container != null) {
this.container.setSearchQuery(query);
}
this.rebuild();
}
/** Resets all search state and clears the active filter without rebuilding entries. */
public void disableSearchMode() {
searchModeActive = false;
typedSearchQuery = "";
searchBox = null;
if (this.container != null) {
this.container.setSearchQuery(null);
}
}
public void disableSearchModeAndRebuild() {
disableSearchMode();
this.rebuild();
}
public void enableSearchModeAndRebuild() {
searchModeActive = true;
this.rebuild(); // First rebuild so that the button width is the current one
int listWidth = getListWidth();
int entryX = this.width / 2 - listWidth / 2;
int boxX = entryX + reservedLeftWidth + 3;
int boxY = this.top + 5; // got figured out by trial and error and works, don't question
int boxW = Math.max(40, listWidth - reservedLeftWidth - 5);
this.searchBox = new GuiTextField(Minecraft.getMinecraft().fontRenderer, boxX, boxY, boxW, 14);
this.searchBox.setMaxStringLength(64);
this.searchBox.setFocused(true);
}
/** True when the navigation stack is non-empty (a sub-screen is open). */
public boolean isOnSubScreen() {
return navigation != null && navigation.getCurrentScreen() != null;
}
public void setReservedLeftWidth(int w) {
reservedLeftWidth = Math.max(0, w);
}
/**
* Routes a keystroke into the live search text field, then propagates any text change
* to the option container and rebuilds the filtered list.
*/
public void handleSearchKeyTyped(char typedChar, int keyCode) {
if (searchBox == null || !searchModeActive) return;
searchBox.textboxKeyTyped(typedChar, keyCode);
String newText = searchBox.getText();
if (!newText.equals(typedSearchQuery)) {
updateSearchQuery(newText);
}
}
/** Draws the search text box overlay above the header row */
public void drawSearchBox() {
if (searchBox == null || !searchModeActive) return;
// Hide when the header has scrolled out of view
if (this.amountScrolled > 0.5f) return;
searchBox.drawTextBox();
}
public void rebuild() {
this.entries.clear();
this.amountScrolled = 0;
@@ -56,7 +131,7 @@ public class ShaderPackOptionList extends IrisGuiSlot {
}
public void addHeader(String text, boolean backButton) {
this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton));
this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton, this));
}
public void addWidgets(int columns, List<AbstractElementWidget<?>> elements) {
@@ -86,6 +161,15 @@ public class ShaderPackOptionList extends IrisGuiSlot {
return entries.size();
}
@Override
public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) {
// Pass the click to the search box first so it can update its focus state
if (searchModeActive && searchBox != null && amountScrolled <= 0.5f) {
searchBox.mouseClicked(mouseX, mouseY, mouseButton);
}
return super.mouseClicked(mouseX, mouseY, mouseButton);
}
@Override
protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int mouseButton) {
final BaseEntry entry = this.entries.get(index);
@@ -14,6 +14,7 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.MathHelper;
import java.io.IOException;
import java.nio.file.FileSystems;
@@ -142,10 +143,15 @@ public class ShaderPackSelectionList extends IrisGuiSlot {
topButtonRow.allowEnableShadersButton = !names.isEmpty();
int index = 0;
String selectedName = Iris.getIrisConfig().getShaderPackName().orElse(null);
int targetSelectionIndex = -1;
for (String name : names) {
this.addPackEntry(index, name);
if (name.equals(selectedName)) {
targetSelectionIndex = index;
}
index++;
addPackEntry(index, name);
}
// Try not to gaslight users in case they swap back and forth from lwjgl2 and lwjgl3
@@ -153,6 +159,29 @@ public class ShaderPackSelectionList extends IrisGuiSlot {
? "pack.iris.list.label"
: "pack.iris.list.label.lwjgl2";
addLabelEntries(EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format(footerKey));
if (targetSelectionIndex != -1) {
scrollToIndex(targetSelectionIndex);
}
}
/**
* Scrolls the list to center the entry at {@code targetIndex} in the viewport.
*/
public void scrollToIndex(int targetIndex) {
if (targetIndex < 0 || targetIndex >= this.getSize()) {
return;
}
this.elementClicked(targetIndex, false, 0, 0);
this.selectedElement = targetIndex;
int viewportCenterY = (this.bottom - this.top) / 2;
int itemCenterPos = (targetIndex * this.slotHeight) + this.headerPadding + (this.slotHeight / 2);
float targetScroll = (float) (itemCenterPos - viewportCenterY);
float maxScroll = (float) this.func_148135_f();
this.amountScrolled = MathHelper.clamp_float(targetScroll, 0.0F, maxScroll);
}
public void addPackEntry(int index, String name) {
@@ -1,9 +1,11 @@
package net.coderbot.iris.gui.element.shaderoptions;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gui.GuiUtil;
import net.coderbot.iris.gui.NavigationController;
import net.coderbot.iris.gui.element.IrisElementRow;
import net.coderbot.iris.gui.element.ShaderPackOptionList;
import net.coderbot.iris.gui.screen.ShaderPackScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
@@ -16,58 +18,83 @@ import org.jetbrains.annotations.Nullable;
public class HeaderEntry extends BaseEntry {
public static final String BACK_BUTTON_TEXT = EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back");
public static final String RESET_BUTTON_TEXT_INACTIVE = EnumChatFormatting.GRAY + I18n.format("options.iris.reset");
public static final String RESET_BUTTON_TEXT_ACTIVE = EnumChatFormatting.YELLOW + I18n.format("options.iris.reset");
public static final String RESET_HOLD_SHIFT_TOOLTIP = EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift");
public static final String RESET_TOOLTIP = EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip");
public static final String IMPORT_TOOLTIP = I18n.format("options.iris.importSettings.tooltip");
// .withStyle(style -> style.withColor(TextColor.fromRgb(0x4da6ff)));
public static final String EXPORT_TOOLTIP = I18n.format("options.iris.exportSettings.tooltip");
// .withStyle(style -> style.withColor(TextColor.fromRgb(0xfc7d3d)));
private static String backButtonText() { return EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back"); }
private static String resetButtonInactive() { return EnumChatFormatting.GRAY + I18n.format("options.iris.reset"); }
private static String resetButtonActive() { return EnumChatFormatting.YELLOW + I18n.format("options.iris.reset"); }
private static String resetHoldShiftTooltip() { return EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift"); }
private static String resetTooltip() { return EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip"); }
private static String importTooltip() { return I18n.format("options.iris.importSettings.tooltip"); }
private static String exportTooltip() { return I18n.format("options.iris.exportSettings.tooltip"); }
private static String searchButtonText() { return I18n.format("options.iris.search.button"); }
private static String clearButtonText() { return I18n.format("options.iris.clear.button"); }
private static String searchTooltip() { return I18n.format("options.iris.search.tooltip"); }
private static String clearTooltip() { return I18n.format("options.iris.clear.tooltip"); }
private static final int MIN_SIDE_BUTTON_WIDTH = 42;
private static final int BUTTON_HEIGHT = 16;
private static final int SEARCH_BOX_GAP = 3;
private final ShaderPackScreen screen;
private final ShaderPackOptionList optionList;
private final @Nullable IrisElementRow backButton;
/** Width of the left-side button, used to clip the title text away from it. */
private final int leftButtonWidth;
private final IrisElementRow utilityButtons = new IrisElementRow();
private final IrisElementRow.TextButtonElement resetButton;
private final IrisElementRow.IconButtonElement importButton;
private final IrisElementRow.IconButtonElement exportButton;
private final String text;
public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton) {
/** Non-null only on the main-screen header (search/clear toggle). */
private final @Nullable IrisElementRow.TextButtonElement searchToggleButton;
public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton, ShaderPackOptionList optionList) {
super(navigation);
this.optionList = optionList;
this.screen = screen;
this.text = text;
IrisElementRow.TextButtonElement toggleRef = null;
if (hasBackButton) {
this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(BACK_BUTTON_TEXT, this::backButtonClicked),
Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(BACK_BUTTON_TEXT) + 8));
// Sub-screen: navigating here while search is active → force-disable so the search box disappears
if (optionList.isSearchModeActive()) {
optionList.disableSearchMode();
}
String backText = backButtonText();
int w = Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(backText) + 8);
this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(backText, this::backButtonClicked), w);
this.leftButtonWidth = w;
} else {
this.backButton = null;
// Main screen: left button is the search/clear toggle
String buttonText = optionList.isSearchModeActive() ? clearButtonText() : searchButtonText();
IrisElementRow.TextButtonElement searchBtn = new IrisElementRow.TextButtonElement(buttonText, this::searchButtonClicked);
int w = Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(buttonText) + 8);
this.backButton = new IrisElementRow().add(searchBtn, w);
toggleRef = searchBtn;
this.leftButtonWidth = w;
optionList.setReservedLeftWidth(w + SEARCH_BOX_GAP);
}
this.resetButton = new IrisElementRow.TextButtonElement(RESET_BUTTON_TEXT_INACTIVE, this::resetButtonClicked);
this.searchToggleButton = toggleRef;
this.resetButton = new IrisElementRow.TextButtonElement(resetButtonInactive(), this::resetButtonClicked);
this.importButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.IMPORT, GuiUtil.Icon.IMPORT_COLORED, this::importSettingsButtonClicked);
this.exportButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.EXPORT, GuiUtil.Icon.EXPORT_COLORED, this::exportSettingsButtonClicked);
this.utilityButtons.add(this.importButton, 15).add(this.exportButton, 15)
.add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth((RESET_BUTTON_TEXT_INACTIVE) + 8)));
this.screen = screen;
this.text = text;
.add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(resetButtonInactive()) + 8));
}
@Override
public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int slotWidth, int slotHeight, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) {
Gui.drawRect(x - 3, (y + slotHeight) - 3, x + slotWidth, (y + slotHeight) - 2, 0x66BEBEBE);
// Gui.drawRect leaves GL color set to the rect's color; reset to white so button textures aren't tinted.
GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
final int tickDelta = 0;
final FontRenderer font = Minecraft.getMinecraft().fontRenderer;
GuiUtil.drawScrollingText(font, text, x + (int) (slotWidth * 0.5), x + 5,
((x + slotWidth) - 10) - this.utilityButtons.getWidth(), y + 5, y + 15, 0xFFFFFF);
GuiUtil.bindIrisWidgetsTexture();
// Draw back button if present
@@ -75,26 +102,42 @@ public class HeaderEntry extends BaseEntry {
backButton.drawScreen(x, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver);
}
if (optionList.isSearchModeActive() && searchToggleButton != null) {
if (searchToggleButton.isHovered()) {
ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, clearTooltip(), mouseX, mouseY - 16));
}
return;
}
GuiUtil.drawScrollingText(font, text, x + (int) (slotWidth * 0.5),
x + leftButtonWidth + 5,
((x + slotWidth) - 10) - this.utilityButtons.getWidth(),
y + 5, y + 15, 0xFFFFFF);
final boolean shiftDown = GuiScreen.isShiftKeyDown();
// Set the appearance of the reset button
this.resetButton.disabled = !shiftDown;
this.resetButton.text = shiftDown ? RESET_BUTTON_TEXT_ACTIVE : RESET_BUTTON_TEXT_INACTIVE;
this.resetButton.text = shiftDown ? resetButtonActive() : resetButtonInactive();
// Draw the utility buttons
this.utilityButtons.renderRightAligned((x + slotWidth) - 3, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver);
// Draw the reset button's tooltip
if (this.resetButton.isHovered()) {
final String tooltip = shiftDown ? RESET_TOOLTIP : RESET_HOLD_SHIFT_TOOLTIP;
final String tooltip = shiftDown ? resetTooltip() : resetHoldShiftTooltip();
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, tooltip);
}
// Draw the import/export button tooltips
if (this.importButton.isHovered()) {
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, IMPORT_TOOLTIP);
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, importTooltip());
}
if (this.exportButton.isHovered()) {
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, EXPORT_TOOLTIP);
queueBottomRightAnchoredTooltip(mouseX, mouseY, font, exportTooltip());
}
// Search-button tooltip
if (searchToggleButton != null && searchToggleButton.isHovered()) {
ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, searchTooltip(), mouseX, mouseY - 16));
}
}
@@ -104,6 +147,9 @@ public class HeaderEntry extends BaseEntry {
@Override
public boolean mouseClicked(int mouseX, int mouseY, int button) {
if (optionList.isSearchModeActive()) {
return backButton != null && backButton.mouseClicked(mouseX, mouseY, button);
}
final boolean backButtonResult = backButton != null && backButton.mouseClicked(mouseX, mouseY, button);
final boolean utilButtonResult = utilityButtons.mouseClicked(mouseX, mouseY, button);
@@ -122,6 +168,16 @@ public class HeaderEntry extends BaseEntry {
return true;
}
private boolean searchButtonClicked(IrisElementRow.TextButtonElement button) {
GuiUtil.playButtonClickSound();
if (optionList.isSearchModeActive()) {
optionList.disableSearchModeAndRebuild();
} else {
optionList.enableSearchModeAndRebuild();
}
return true;
}
private boolean resetButtonClicked(IrisElementRow.TextButtonElement button) {
if (GuiScreen.isShiftKeyDown()) {
Iris.resetShaderPackOptionsOnNextReload();
@@ -10,9 +10,10 @@ import net.minecraft.client.resources.I18n;
import java.util.function.Function;
public class TopButtonRowEntry extends BaseEntry {
private static final String NONE_PRESENT_LABEL = I18n.format("options.iris.shaders.nonePresent"); //.withStyle(ChatFormatting.GRAY);
private static final String SHADERS_DISABLED_LABEL = I18n.format("options.iris.shaders.disabled");
private static final String SHADERS_ENABLED_LABEL = I18n.format("options.iris.shaders.enabled");
private static String nonePresentLabel() { return I18n.format("options.iris.shaders.nonePresent"); }
private static String shadersDisabledLabel() { return I18n.format("options.iris.shaders.disabled"); }
private static String shadersEnabledLabel() { return I18n.format("options.iris.shaders.enabled"); }
private final ShaderPackSelectionList shaderPackSelectionList;
private final IrisElementRow buttons = new IrisElementRow();
@@ -54,7 +55,7 @@ public class TopButtonRowEntry extends BaseEntry {
}
private String getEnableDisableLabel() {
return this.allowEnableShadersButton ? this.shadersEnabled ? SHADERS_ENABLED_LABEL : SHADERS_DISABLED_LABEL : NONE_PRESENT_LABEL;
return this.allowEnableShadersButton ? this.shadersEnabled ? shadersEnabledLabel() : shadersDisabledLabel() : nonePresentLabel();
}
public boolean mouseClicked(double mouseX, double mouseY, int button) {
@@ -4,16 +4,19 @@ import net.coderbot.iris.gui.GuiUtil;
import net.coderbot.iris.gui.NavigationController;
import net.coderbot.iris.gui.screen.ShaderPackScreen;
import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement;
import net.coderbot.iris.shaderpack.option.menu.OptionMenuOptionElement;
import net.coderbot.iris.shaderpack.option.menu.ShaderSearchEngine;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.EnumChatFormatting;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public abstract class BaseOptionElementWidget<T extends OptionMenuElement> extends CommentedElementWidget<T> {
protected static final String SET_TO_DEFAULT = I18n.format("options.iris.setToDefault");
protected static String setToDefault() { return I18n.format("options.iris.setToDefault"); }
protected static final String DIVIDER = ": ";
protected String unmodifiedLabel;
@@ -101,7 +104,7 @@ public abstract class BaseOptionElementWidget<T extends OptionMenuElement> exten
protected final void tryRenderTooltip(int mouseX, int mouseY, boolean hovered) {
if (GuiScreen.isShiftKeyDown()) {
renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered);
renderTooltip(setToDefault(), mouseX, mouseY, hovered);
} else if (this.isLabelTrimmed && !this.screen.isDisplayingComment()) {
renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered);
}
@@ -145,13 +148,39 @@ public abstract class BaseOptionElementWidget<T extends OptionMenuElement> exten
@Override
public Optional<String> getCommentBody() {
final String key = getCommentKey();
if (key == null) {
return Optional.empty();
Optional<String> base = Optional.ofNullable(getCommentKey()).map(I18n::format);
if (this.screen == null || !this.screen.isOptionMenuSearchActive()) {
return base;
}
final String translated = GuiUtil.translateLenient(key);
// Don't show comment boxes without a translation, otherwise you get something like option.{commentname}.comment
return translated.equals(key) ? Optional.empty() : Optional.of(translated);
String pathLabel = buildTranslatedPath();
if (pathLabel == null) {
return base;
}
String breadcrumb = EnumChatFormatting.BOLD.toString() + EnumChatFormatting.ITALIC + pathLabel + EnumChatFormatting.RESET;
if (base.isPresent()) {
return Optional.of(breadcrumb + ". " + base.get());
}
return Optional.of(breadcrumb);
}
@Nullable
private String buildTranslatedPath() {
if (!(this.element instanceof OptionMenuOptionElement)) return null;
OptionMenuOptionElement optEl = (OptionMenuOptionElement) this.element;
String rawPath = optEl.container.getOptionPath(optEl.optionId);
String[] segments = rawPath.split("/");
StringBuilder display = new StringBuilder();
for (String segment : segments) {
if ("root".equals(segment)) continue;
String translated = ShaderSearchEngine.getDisplaySettingsName(segment).replaceAll("\\s+>", "");
String label = translated.isEmpty() ? segment : translated;
if (display.length() > 0) display.append(" > ");
display.append(label);
}
return display.length() > 0 ? display.toString() : null;
}
@Override
@@ -9,10 +9,9 @@ import net.coderbot.iris.shaderpack.option.menu.OptionMenuBooleanOptionElement;
import net.minecraft.client.resources.I18n;
public class BooleanElementWidget extends BaseOptionElementWidget<OptionMenuBooleanOptionElement> {
private static final String TEXT_TRUE = I18n.format("label.iris.true");
private static final String TEXT_FALSE = I18n.format("label.iris.false");
private static final String TEXT_TRUE_DEFAULT = I18n.format("label.iris.true");
private static final String TEXT_FALSE_DEFAULT = I18n.format("label.iris.false");
private static String textTrue() { return I18n.format("label.iris.true"); }
private static String textFalse() { return I18n.format("label.iris.false"); }
private final BooleanOption option;
@@ -63,11 +62,7 @@ public class BooleanElementWidget extends BaseOptionElementWidget<OptionMenuBool
// Compatibility Mode to "On" when not needed. Now we use white for
// default to avoid giving a positive or negative connotation to a
// default value.
if (this.value == this.defaultValue) {
return this.value ? TEXT_TRUE_DEFAULT : TEXT_FALSE_DEFAULT;
}
return this.value ? TEXT_TRUE : TEXT_FALSE;
return this.value ? textTrue() : textFalse();
}
@Override
@@ -16,8 +16,9 @@ import net.minecraft.client.resources.I18n;
import java.util.Optional;
public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProfileElement> {
private static final String PROFILE_LABEL = I18n.format("options.iris.profile");
private static final String PROFILE_CUSTOM = I18n.format("options.iris.profile.custom");
private static String profileLabel() { return I18n.format("options.iris.profile"); }
private static String profileCustom() { return I18n.format("options.iris.profile.custom"); }
private Profile next;
private Profile previous;
@@ -30,7 +31,7 @@ public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProf
@Override
public void init(ShaderPackScreen screen, NavigationController navigation) {
super.init(screen, navigation);
this.setLabel(PROFILE_LABEL);
this.setLabel(profileLabel());
boolean secondProfileSet = ProfileElementTracker.isSecondProfileSet(this.element);
final ProfileSet profiles = this.element.profiles;
@@ -44,12 +45,12 @@ public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProf
final Optional<String> profileName = result.current.map(p -> p.name);
String translationKey = "profile" + (secondProfileSet ? "2." : ".") + profileName.orElse("custom");
this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, translationKey)).orElse(PROFILE_CUSTOM);
this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, translationKey)).orElse(profileCustom());
}
@Override
public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) {
this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(PROFILE_LABEL) + 16));
this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(profileLabel()) + 16));
this.renderOptionWithValue(x, y, width, height, hovered);
}
@@ -61,12 +62,12 @@ public class ProfileElementWidget extends BaseOptionElementWidget<OptionMenuProf
@Override
protected int getValueColor() {
return PROFILE_CUSTOM.equals(this.profileLabel) ? 0xFFFF55 : 0xFFFFFF;
return profileCustom().equals(this.profileLabel) ? 0xFFFF55 : 0xFFFFFF;
}
@Override
public Optional<String> getCommentTitle() {
return Optional.of(PROFILE_LABEL);
return Optional.of(profileLabel());
}
@Override
@@ -28,7 +28,7 @@ public class SliderElementWidget extends StringElementWidget {
}
if (GuiScreen.isShiftKeyDown()) {
renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered);
renderTooltip(setToDefault(), mouseX, mouseY, hovered);
} else if (!this.screen.isDisplayingComment()) {
renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered);
}
@@ -53,8 +53,8 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
*/
public static final Set<Runnable> TOP_LAYER_RENDER_QUEUE = new HashSet<>();
private static final String SELECT_TITLE = EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.select.title");
private static final String CONFIGURE_TITLE = EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.configure.title");
private static String selectTitle() { return EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.select.title"); }
private static String configureTitle() { return EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.configure.title"); }
private static final int COMMENT_PANEL_WIDTH = 314;
private final GuiScreen parent;
@@ -104,6 +104,11 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
refreshForChangedPack();
}
/** Returns true when the option menu is open and the user has an active search query. */
public boolean isOptionMenuSearchActive() {
return this.optionMenuOpen && this.shaderOptionList != null && this.shaderOptionList.isSearchModeActive();
}
@Override
public void drawScreen(int mouseX, int mouseY, float delta) {
GLStateManager.glPushAttrib(GL11.GL_COLOR_BUFFER_BIT);
@@ -124,6 +129,7 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
if (!this.guiHidden) {
if (optionMenuOpen && this.shaderOptionList != null) {
this.shaderOptionList.drawScreen(mouseX, mouseY, delta);
this.shaderOptionList.drawSearchBox();
} else {
this.shaderPackList.drawScreen(mouseX, mouseY, delta);
}
@@ -144,9 +150,9 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
drawCenteredString(this.fontRendererObj, notificationDialog, (int) (this.width * 0.5), 21, 0xFFFFFF);
} else {
if (optionMenuOpen) {
drawCenteredString(this.fontRendererObj, CONFIGURE_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF);
drawCenteredString(this.fontRendererObj, configureTitle(), (int) (this.width * 0.5), 21, 0xFFFFFF);
} else {
drawCenteredString(this.fontRendererObj, SELECT_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF);
drawCenteredString(this.fontRendererObj, selectTitle(), (int) (this.width * 0.5), 21, 0xFFFFFF);
}
}
@@ -207,6 +213,10 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
if (Iris.getCurrentPack().isPresent() && this.navigation != null) {
final ShaderPack currentPack = Iris.getCurrentPack().get();
if (currentPack.getMenuContainer() != null) {
currentPack.getMenuContainer().setSearchQuery(null);
}
this.shaderOptionList = new ShaderPackOptionList(this, this.navigation, currentPack, this.mc, this.width, this.height, 32, this.height - 58, 0, this.width);
this.navigation.setActiveOptionList(this.shaderOptionList);
@@ -364,6 +374,31 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
@Override
protected void keyTyped(char typedChar, int keyCode) {
// Route all input to the search box while search mode is active
if (optionMenuOpen && this.shaderOptionList != null && this.shaderOptionList.isSearchModeActive()) {
if (keyCode == Keyboard.KEY_ESCAPE) {
this.shaderOptionList.disableSearchModeAndRebuild();
return;
}
// Ctrl+F while search is open: toggle it closed
if (keyCode == Keyboard.KEY_F && GuiScreen.isCtrlKeyDown()) {
GuiUtil.playButtonClickSound();
this.shaderOptionList.disableSearchModeAndRebuild();
return;
}
this.shaderOptionList.handleSearchKeyTyped(typedChar, keyCode);
return;
}
// Ctrl+F: open search on the main option screen
if (optionMenuOpen && this.shaderOptionList != null
&& keyCode == Keyboard.KEY_F && GuiScreen.isCtrlKeyDown()) {
if (!this.shaderOptionList.isOnSubScreen()) {
GuiUtil.playButtonClickSound();
this.shaderOptionList.enableSearchModeAndRebuild();
return;
}
}
if (GuiScreen.isCtrlKeyDown() && keyCode == Keyboard.KEY_D) {
this.mc.displayGuiScreen(new GuiYesNo((result, id) -> {
Iris.setDebug(result);
@@ -496,6 +531,10 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable {
}
public void onClose() {
if (this.shaderOptionList != null) {
this.shaderOptionList.disableSearchModeAndRebuild();
}
if (!dropChanges) {
applyChanges();
} else {
@@ -10,9 +10,12 @@ import net.coderbot.iris.shaderpack.option.ShaderPackOptions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class OptionMenuContainer {
public final OptionMenuElementScreen mainScreen;
@@ -24,6 +27,8 @@ public class OptionMenuContainer {
private final Map<List<OptionMenuElement>, Integer> unusedOptionDumpQueue = new HashMap<>(); // Used by screens with "*" element
@Getter private final ProfileSet profiles;
@Getter private final ProfileSet profiles2;
private final List<OptionMenuElement> originalMainElements = new ArrayList<>(); // Saved snapshot of mainScreen.elements before any search filter is applied.
private final Map<String, String> cachedOptionPaths = new HashMap<>(); // Full "root/SCREEN1/SCREEN2" path for every option ID, built once after construction
public OptionMenuContainer(ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, ProfileSet profiles) {
this.profiles = profiles;
@@ -69,6 +74,10 @@ public class OptionMenuContainer {
entry.getKey().addAll(entry.getValue(), elementsToInsert);
}
// Save the original layout and build the path cache once the full tree is constructed
this.originalMainElements.addAll(this.mainScreen.elements);
generateAllPaths();
}
// Screens will call this when they contain a "*" element, so that the list of
@@ -85,4 +94,81 @@ public class OptionMenuContainer {
unusedOptions.remove(optionId);
}
private void generateAllPaths() {
cachedOptionPaths.clear();
traverseScreen(this.mainScreen, "root", new HashSet<String>());
}
private void traverseScreen(OptionMenuElementScreen screen, String currentPath, Set<String> visited) {
if (screen == null) return;
for (OptionMenuElement element : screen.elements) {
if (element instanceof OptionMenuOptionElement optEl) {
if (optEl.optionId != null) {
cachedOptionPaths.putIfAbsent(optEl.optionId, currentPath);
}
} else if (element instanceof OptionMenuLinkElement link) {
if (link.targetScreenId != null) {
String targetId = link.targetScreenId;
if (visited.add(targetId)) {
OptionMenuElementScreen next = this.subScreens.get(targetId);
if (next != null) {
traverseScreen(next, currentPath + "/" + targetId, visited);
}
visited.remove(targetId);
}
}
}
}
}
public String getOptionPath(String optionId) {
if (optionId == null) return "root";
return cachedOptionPaths.getOrDefault(optionId, "root");
}
/**
* Filters and re-orders the main-screen options to match {@code query}, or restores the
* original layout when {@code query} is null or blank.
*/
public void setSearchQuery(String query) {
if (query == null || query.trim().isEmpty()) {
restoreOriginalLayout();
return;
}
String normalizedQuery = query.toLowerCase(Locale.ROOT).trim();
List<OptionMenuOptionElement> flatOptions = ShaderSearchEngine.getAllOptionsFlattened(usedOptionElements);
List<ShaderSearchEngine.ScoredOptionElement> scored = new ArrayList<>();
for (OptionMenuOptionElement el : flatOptions) {
int score = ShaderSearchEngine.computeMatchTier(el.optionId, normalizedQuery);
if (score > 0) {
scored.add(new ShaderSearchEngine.ScoredOptionElement(
el,
ShaderSearchEngine.getReadableTranslatedName(el.optionId),
ShaderSearchEngine.getReadableDefaultName(el.optionId),
getOptionPath(el.optionId),
score,
normalizedQuery
));
}
}
Collections.sort(scored);
applyFilteredLayout(scored);
}
private void applyFilteredLayout(List<ShaderSearchEngine.ScoredOptionElement> sortedElements) {
this.mainScreen.elements.clear();
for (ShaderSearchEngine.ScoredOptionElement scored : sortedElements) {
this.mainScreen.elements.add(scored.getElement());
}
}
private void restoreOriginalLayout() {
this.mainScreen.elements.clear();
this.mainScreen.elements.addAll(this.originalMainElements);
}
}
@@ -0,0 +1,251 @@
package net.coderbot.iris.shaderpack.option.menu;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.shaderpack.ShaderPack;
import net.minecraft.client.resources.I18n;
import org.jspecify.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* Utility class for searching and scoring shader option elements based on a query string.
* <br>By SpacEagle17
*/
public class ShaderSearchEngine {
private static final String WHOLE_WORD_REGEX = "(?<=^|[^a-zA-Z0-9])%s(?=$|[^a-zA-Z0-9])";
private static final String STARTS_WITH_REGEX = "(?<=^|[^a-zA-Z0-9])%s";
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§.");
/**
* Bitmask of bits from readable-name (translated + default) comparisons.
* Used by the comparator to separate name-match quality from rawId/comment noise.
*/
private static final int READABLE_NAME_BITS =
(1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 3) | (1 << 2);
private static String strippedLanguageString(String key) {
String translated = I18n.format(key);
if (translated.equals(key)) return "";
return COLOR_CODE_PATTERN.matcher(translated).replaceAll("");
}
/** Translated display name for a sub-screen ID (looks up {@code "screen.<screenId>"}). */
public static String getDisplaySettingsName(String screenId) {
return strippedLanguageString("screen." + screenId);
}
/** Lowercase Minecraft-locale translation of an option's human-readable name. */
public static String getReadableTranslatedName(String optionId) {
return strippedLanguageString("option." + optionId).toLowerCase(Locale.ROOT);
}
/**
* Lowercase default (en_us from the shader pack's lang folder) translation.
* Returns {@code ""} when the current pack has no en_us lang file or the key is missing.
*/
public static String getReadableDefaultName(String optionId) {
try {
Optional<ShaderPack> packOpt = Iris.getCurrentPack();
if (packOpt.isEmpty()) return "";
Map<String, String> translations = packOpt.get().getLanguageMap().getTranslations("en_us");
if (translations == null) return "";
String value = translations.get("option." + optionId);
if (value == null) return "";
return COLOR_CODE_PATTERN.matcher(value).replaceAll("").toLowerCase(Locale.ROOT);
} catch (Throwable t) {
return "";
}
}
private static boolean isOnlyAscii(String s) {
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) > 127) return false;
}
return true;
}
/**
* Computes a 14-bit bitmask score for how well {@code optionId} matches {@code query}.
* Returns 0 when there is no match at all.
* <br> Read code to know what each bit means lmao
*/
public static int computeMatchTier(String optionId, String query) {
try {
if (optionId == null || query == null) return 0;
String q = query.toLowerCase(Locale.ROOT).trim();
if (q.isEmpty()) return 0;
String translatedName = getReadableTranslatedName(optionId);
String defaultName = getReadableDefaultName(optionId);
// Single ASCII char: only match when a readable name begins with it
if (q.length() == 1 && isOnlyAscii(q)) {
return (!translatedName.isEmpty() && translatedName.startsWith(q)) ? 1 : 0;
}
String escaped = Pattern.quote(q);
Pattern wholeWord = Pattern.compile(String.format(WHOLE_WORD_REGEX, escaped));
Pattern startsWith = Pattern.compile(String.format(STARTS_WITH_REGEX, escaped));
String rawId = optionId.toLowerCase(Locale.ROOT);
String commentText = strippedLanguageString("option." + optionId + ".comment").toLowerCase(Locale.ROOT);
int score = 0;
if (!translatedName.isEmpty() && translatedName.equals(q)) score |= (1 << 13);
if (!defaultName.isEmpty() && defaultName.equals(q)) score |= (1 << 12);
if (!translatedName.isEmpty() && wholeWord.matcher(translatedName).find()) score |= (1 << 11);
if (!defaultName.isEmpty() && wholeWord.matcher(defaultName).find()) score |= (1 << 10);
if (!translatedName.isEmpty() && startsWith.matcher(translatedName).find()) score |= (1 << 9);
if (!defaultName.isEmpty() && startsWith.matcher(defaultName).find()) score |= (1 << 8);
if (wholeWord.matcher(rawId).find()) score |= (1 << 7);
if (!commentText.isEmpty() && wholeWord.matcher(commentText).find()) score |= (1 << 6);
if (startsWith.matcher(rawId).find()) score |= (1 << 5);
if (!commentText.isEmpty() && startsWith.matcher(commentText).find()) score |= (1 << 4);
if (!translatedName.isEmpty() && translatedName.contains(q)) score |= (1 << 3);
if (!defaultName.isEmpty() && defaultName.contains(q)) score |= (1 << 2);
if (rawId.contains(q)) score |= (1 << 1);
if (!commentText.isEmpty() && commentText.contains(q)) score |= 1;
return score;
} catch (Exception e) {
return 0;
}
}
/** De-duplicates and flattens the pool of registered option elements. */
public static List<OptionMenuOptionElement> getAllOptionsFlattened(List<OptionMenuOptionElement> usedOptionElements) {
List<OptionMenuOptionElement> flat = new ArrayList<>();
List<String> seen = new ArrayList<>();
for (OptionMenuOptionElement el : usedOptionElements) {
if (el == null) continue;
String id = el.optionId != null ? el.optionId : el.toString();
if (!seen.contains(id)) {
seen.add(id);
flat.add(el);
}
}
return flat;
}
/**
* A scored search result carrying the original element and all data required for sorting.
*/
public static final class ScoredOptionElement implements Comparable<ScoredOptionElement> {
@Getter
private final OptionMenuOptionElement element;
public final String readableTranslatedName;
public final String readableDefaultName;
public final String path;
public final int score;
public final String query;
public ScoredOptionElement(OptionMenuOptionElement element,
String readableTranslatedName,
String readableDefaultName,
String path,
int score,
String query) {
this.element = element;
this.readableTranslatedName = readableTranslatedName;
this.readableDefaultName = readableDefaultName;
this.path = path;
this.score = score;
this.query = query;
}
@Override
public int compareTo(@NonNull ScoredOptionElement other) {
String q = this.query != null ? this.query.toLowerCase(Locale.ROOT).trim() : "";
int r;
if ((r = compareByReadableNameQuality(this, other)) != 0) return r;
if ((r = compareByMatchedWordLength(this, other, q)) != 0) return r;
if ((r = compareByWordCount(this, other)) != 0) return r;
if ((r = compareByFullScore(this, other)) != 0) return r;
if ((r = compareByPrefixBoost(this, other, q)) != 0) return r;
if ((r = compareByPathDepth(this, other)) != 0) return r;
return compareByAlphabetical(this, other);
}
// 1. How well does the readable name (translated + default) match?
private static int compareByReadableNameQuality(ScoredOptionElement a, ScoredOptionElement b) {
return Integer.compare(b.score & READABLE_NAME_BITS, a.score & READABLE_NAME_BITS);
}
// 2. Shorter matched word = query covers a higher fraction of that word.
private static int compareByMatchedWordLength(ScoredOptionElement a, ScoredOptionElement b, String q) {
if (q.isEmpty()) return 0;
String aw = findMatchingWord(a.readableTranslatedName, q);
if (aw == null) aw = findMatchingWord(a.readableDefaultName, q);
String bw = findMatchingWord(b.readableTranslatedName, q);
if (bw == null) bw = findMatchingWord(b.readableDefaultName, q);
int aLen = aw != null ? aw.length() : Integer.MAX_VALUE;
int bLen = bw != null ? bw.length() : Integer.MAX_VALUE;
return Integer.compare(aLen, bLen);
}
// 3. Fewer words = more precise ("Bloom" before "Bloom Strength").
private static int compareByWordCount(ScoredOptionElement a, ScoredOptionElement b) {
String ae = !a.readableTranslatedName.isEmpty() ? a.readableTranslatedName : a.readableDefaultName;
String be = !b.readableTranslatedName.isEmpty() ? b.readableTranslatedName : b.readableDefaultName;
return Integer.compare(countWords(ae), countWords(be));
}
// 4. Higher raw score wins.
private static int compareByFullScore(ScoredOptionElement a, ScoredOptionElement b) {
return Integer.compare(b.score, a.score);
}
// 5. Prefix boost: readable name starts with the exact query string.
private static int compareByPrefixBoost(ScoredOptionElement a, ScoredOptionElement b, String q) {
if (q.isEmpty()) return 0;
boolean ap = (a.readableTranslatedName != null && a.readableTranslatedName.startsWith(q))
|| (a.readableDefaultName != null && a.readableDefaultName.startsWith(q));
boolean bp = (b.readableTranslatedName != null && b.readableTranslatedName.startsWith(q))
|| (b.readableDefaultName != null && b.readableDefaultName.startsWith(q));
if (ap == bp) return 0;
return ap ? -1 : 1;
}
// 6. Shallower option path wins (fewer slashes = closer to root menu).
private static int compareByPathDepth(ScoredOptionElement a, ScoredOptionElement b) {
return Integer.compare(countSlashes(a.path), countSlashes(b.path));
}
// 7. Alphabetical tie-breaker.
private static int compareByAlphabetical(ScoredOptionElement a, ScoredOptionElement b) {
String aId = a.element != null ? a.element.optionId : null;
String bId = b.element != null ? b.element.optionId : null;
if (aId != null && bId != null) return aId.compareTo(bId);
return 0;
}
private static String findMatchingWord(String name, String query) {
if (name == null || query.isEmpty()) return null;
for (String word : name.split("\\s+")) {
if (word.startsWith(query)) return word;
}
return null;
}
private static int countWords(String s) {
if (s == null || s.trim().isEmpty()) return 0;
return s.trim().split("\\s+").length;
}
private static int countSlashes(String path) {
if (path == null) return 0;
int count = 0;
for (int i = 0; i < path.length(); i++) {
if (path.charAt(i) == '/') count++;
}
return count;
}
}
}
@@ -145,3 +145,7 @@ pack.iris.select.title=Auswählen
pack.iris.configure.title=Konfigurieren
label.iris.true=An
label.iris.false=Aus
options.iris.search.button=Suchen
options.iris.clear.button=Schließen
options.iris.search.tooltip=Shader-Einstellungen durchsuchen (Strg + F)
options.iris.clear.tooltip=Shader-Suche beenden (Esc)
@@ -139,6 +139,10 @@ options.iris.shadowDistance.enabled=Allows you to change the maximum distance fo
options.iris.shadowDistance.disabled=Your current shader pack has already set a render distance for shadows; you cannot change it.
options.iris.gui.hide=Hide GUI
options.iris.gui.show=Show GUI
options.iris.search.button=Search
options.iris.clear.button=Clear
options.iris.search.tooltip=Search Shader Settings (Ctrl + F)
options.iris.clear.tooltip=Exit Shader Search (Esc)
options.dynamic_lights=Dynamic Lights
options.dynamic_lights.tooltip=Enables dynamic lights. Modes: OFF - off, FASTEST - 500ms delay, FAST - 250ms delay, FANCY - 50ms delay, REALTIME - no delay.
@@ -132,3 +132,7 @@ pack.iris.select.title=Выбрать
pack.iris.configure.title=Настроить
label.iris.true=Вкл
label.iris.false=Выкл
options.iris.search.button=Поиск
options.iris.clear.button=Очистить
options.iris.search.tooltip=Поиск настроек шейдеров (Ctrl + F)
options.iris.clear.tooltip=Выход из поиска шейдеров (Esc)
@@ -138,6 +138,10 @@ options.iris.shadowDistance.enabled=更改渲染阴影的最远距离。超出
options.iris.shadowDistance.disabled=当前光影包已重设阴影渲染距离,无法更改
options.iris.gui.hide=隐藏 GUI
options.iris.gui.show=显示 GUI
options.iris.search.button=搜索
options.iris.clear.button=清除搜索栏
options.iris.search.tooltip=搜索光影包设定 (Ctrl + F)
options.iris.clear.tooltip=退出搜索 (Esc)
options.dynamic_lights=动态光源
options.dynamic_lights.tooltip=启用动态光源,模式选择:OFF-关闭,FASTEST-500ms延迟,FAST-250ms延迟,FANCY-50ms延迟,REALTIME-无延迟
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.angelica;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import net.minecraft.client.Minecraft;
@@ -20,6 +21,7 @@ public class MixinMinecraft_FrameHook {
@Inject(method = "runGameLoop", at = @At("HEAD"))
private void angelica$onFrameBegin(CallbackInfo ci) {
DisplayListManager.abortIfLeaked();
RENDER_BACKEND.onFrameBegin();
}