Compare commits

...
11 Commits
104 changed files with 2929 additions and 639 deletions

No files matched your search

+5 -1
View File
@@ -90,13 +90,17 @@ dependencies {
compileOnly "org.apache.ant:ant:1.8.2"
// Celeritas (transitive=false: deps provided by GTNHLib/runtime)
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.5-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-common:2.5.8-GTNH") { transitive = false }
embedOnly("org.embeddedt.celeritas:celeritas-lwjgl2-service:2.5.3-GTNH") { transitive = false }
// Because who doesn't want NEI
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.8.35-GTNH:dev')
devOnlyNonPublishable('com.github.GTNewHorizons:CodeChickenCore:1.4.7:dev')
// BQ Testing
// devOnlyNonPublishable("com.github.GTNewHorizons:BetterQuesting:3.8.74-GTNH:dev")
// Display List Testing
/*
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-749-GTNH:dev")
+44 -10
View File
@@ -146,9 +146,13 @@ val downgradeTestClasses by tasks.registering(DowngradeFiles::class) {
dependsOn(tasks.named("testClasses"))
}
tasks.test {
useJUnitPlatform()
val GL_TASK_TAGS = mapOf(
"glCompatTest" to "gl-compat",
"glCoreTest" to "gl-core",
"glSharedTest" to "gl-shared",
)
fun Test.configureGlsmJava8() {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(8)
if (System.getProperty("os.name").lowercase().contains("mac")
@@ -171,23 +175,53 @@ tasks.test {
classpath = downgradedTest
.plus(downgradedMain)
.plus(downgradedDeps)
.plus(classpath.minus(mainClassesDirs).minus(testClassesDirs).minus(depsToDowngrade))
.plus(sourceSets["test"].runtimeClasspath.minus(mainClassesDirs).minus(testClassesDirs).minus(depsToDowngrade))
val extractNatives = rootProject.tasks.named("extractNatives2")
dependsOn(extractNatives)
jvmArgs("-Djava.library.path=${extractNatives.get().property("destinationFolder").let { (it as DirectoryProperty).asFile.get().path }}")
}
tasks.test {
useJUnitPlatform {
excludeTags = GL_TASK_TAGS.values.toSet()
}
configureGlsmJava8()
}
val glTestTasks = GL_TASK_TAGS.map { (taskName, tag) ->
tasks.register<Test>(taskName) {
description = "Runs the $tag GL tests in their own JVM, so they get their own Display and GL profile."
group = "verification"
useJUnitPlatform {
includeTags = setOf(tag)
}
configureGlsmJava8()
}
}
(listOf(tasks.test) + glTestTasks).zipWithNext { earlier, later ->
later.configure { mustRunAfter(earlier) }
}
tasks.test { finalizedBy(glTestTasks) }
val verifyTestsRan by tasks.registering {
val resultsDir = layout.buildDirectory.dir("test-results/test")
dependsOn(tasks.test)
doLast {
val dir = resultsDir.get().asFile
val xmls = dir.listFiles { f -> f.name.startsWith("TEST-") && f.name.endsWith(".xml") } ?: emptyArray()
check(xmls.isNotEmpty()) {
":glsm:test produced no TEST-*.xml in $dir - test task likely went NO-SOURCE"
dependsOn(tasks.test, glTestTasks)
}
(listOf(tasks.test) + glTestTasks).forEach { testTask ->
val name = testTask.name
val verify = tasks.register("verify${name.replaceFirstChar { it.uppercase() }}Ran") {
val resultsDir = layout.buildDirectory.dir("test-results/$name")
dependsOn(testTask)
doLast {
val dir = resultsDir.get().asFile
val xmls = dir.listFiles { f -> f.name.startsWith("TEST-") && f.name.endsWith(".xml") } ?: emptyArray()
check(xmls.isNotEmpty()) {
":glsm:$name produced no TEST-*.xml in $dir - test task likely went NO-SOURCE"
}
}
}
verifyTestsRan.configure { dependsOn(verify) }
}
tasks.check { dependsOn(verifyTestsRan) }
+1 -1
View File
@@ -1,6 +1,6 @@
lwjglVersion = 2.9.4-nightly-20150209
eventbusVersion = 7.0-beta.15
celeritasVersion = 2.5.3-GTNH
celeritasVersion = 2.5.8-GTNH
lombokVersion = 1.18.42
log4jVersion = 2.0-beta9
guavaVersion = 18.0
@@ -877,9 +877,14 @@ public class DisplayListManager {
// Empty display list - per OpenGL spec, still valid after glNewList/glEndList
compiled = CompiledDisplayList.EMPTY;
}
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
final boolean locked = GLStateManager.acquireDrawLock();
try {
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
}
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
// Log compilation details if enabled (before context restoration changes glListId)
@@ -1012,33 +1017,43 @@ public class DisplayListManager {
}
private static void executeDisplayList(int list) {
final CompiledDisplayList compiled = displayListCache.get(list);
if (compiled != null) {
compiled.render(list);
return;
}
// Fall back to VBOManager or legacy display list
if (list < 0) {
// Negative IDs are VBOManager space (entity models, etc.)
final VertexBuffer vbo = VBOManager.get(list);
if (vbo != null) {
vbo.render();
final boolean locked = GLStateManager.acquireDrawLock();
try {
final CompiledDisplayList compiled = displayListCache.get(list);
if (compiled != null) {
compiled.render(list);
return;
}
// Per OpenGL spec: if list is undefined, glCallList has no effect
// Fall back to VBOManager or legacy display list
if (list < 0) {
// Negative IDs are VBOManager space (entity models, etc.)
final VertexBuffer vbo = VBOManager.get(list);
if (vbo != null) {
vbo.render();
}
// Per OpenGL spec: if list is undefined, glCallList has no effect
}
// An uncached positive-ID list = no-op (per OpenGL spec)
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
// An uncached positive-ID list = no-op (per OpenGL spec)
}
/**
* Delete display lists and free their VBO resources.
*/
public static void glDeleteLists(int list, int range) {
for (int i = list; i < list + range; i++) {
CompiledDisplayList compiled = displayListCache.remove(i);
if (compiled != null) {
compiled.delete();
final boolean locked = GLStateManager.acquireDrawLock();
try {
for (int i = list; i < list + range; i++) {
CompiledDisplayList compiled = displayListCache.remove(i);
if (compiled != null) {
compiled.delete();
}
}
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
}
@@ -57,8 +57,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joml.Matrix4d;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.joml.Matrix4fStack;
import org.joml.Matrix4fc;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector4f;
@@ -97,6 +97,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.IntSupplier;
import static com.gtnewhorizons.angelica.glsm.Vendor.AMD;
@@ -252,6 +253,18 @@ public class GLStateManager {
// Reference to DrawableGL (main display context) - works with both FML and BLS splash
@Setter private static Drawable drawableGL = null;
private static final ReentrantLock DRAW_LOCK = new ReentrantLock();
public static boolean acquireDrawLock() {
if (splashComplete) return false;
DRAW_LOCK.lock();
return true;
}
public static void releaseDrawLock() {
DRAW_LOCK.unlock();
}
public static boolean isCachingEnabled() {
if (splashComplete) return true;
return Thread.currentThread() == drawableGLHolder;
@@ -368,16 +381,16 @@ public class GLStateManager {
@Getter protected static final BooleanStateStack ditherState = new BooleanStateStack(GL11.GL_DITHER, true); // Defaults to true per OpenGL spec
@Getter protected static final BooleanStateStack stencilTest = new BooleanStateStack(GL11.GL_STENCIL_TEST);
@Getter protected static final BooleanStateStack lineSmoothState = new BooleanStateStack(GL11.GL_LINE_SMOOTH);
@Getter protected static final BooleanStateStack lineStippleState = new BooleanStateStack(GL11.GL_LINE_STIPPLE);
@Getter protected static final BooleanStateStack pointSmoothState = new BooleanStateStack(GL11.GL_POINT_SMOOTH);
@Getter protected static final BooleanStateStack lineStippleState = new BooleanStateStack(GL11.GL_LINE_STIPPLE, false, true);
@Getter protected static final BooleanStateStack pointSmoothState = new BooleanStateStack(GL11.GL_POINT_SMOOTH, false, true);
@Getter protected static final BooleanStateStack polygonSmoothState = new BooleanStateStack(GL11.GL_POLYGON_SMOOTH);
@Getter protected static final BooleanStateStack polygonStippleState = new BooleanStateStack(GL11.GL_POLYGON_STIPPLE);
@Getter protected static final BooleanStateStack polygonStippleState = new BooleanStateStack(GL11.GL_POLYGON_STIPPLE, false, true);
@Getter protected static final BooleanStateStack multisampleState = new BooleanStateStack(GL13.GL_MULTISAMPLE, true); // Defaults to true per OpenGL spec
@Getter protected static final BooleanStateStack sampleAlphaToCoverageState = new BooleanStateStack(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE);
@Getter protected static final BooleanStateStack sampleAlphaToOneState = new BooleanStateStack(GL13.GL_SAMPLE_ALPHA_TO_ONE);
@Getter protected static final BooleanStateStack sampleCoverageState = new BooleanStateStack(GL13.GL_SAMPLE_COVERAGE);
@Getter protected static final BooleanStateStack colorLogicOpState = new BooleanStateStack(GL11.GL_COLOR_LOGIC_OP);
@Getter protected static final BooleanStateStack indexLogicOpState = new BooleanStateStack(GL11.GL_INDEX_LOGIC_OP);
@Getter protected static final BooleanStateStack indexLogicOpState = new BooleanStateStack(GL11.GL_INDEX_LOGIC_OP, false, true);
// Polygon offset states (enable bits)
@Getter protected static final BooleanStateStack polygonOffsetPointState = new BooleanStateStack(GL11.GL_POLYGON_OFFSET_POINT);
@@ -397,6 +410,7 @@ public class GLStateManager {
* {@link com.gtnewhorizons.angelica.glsm.ffp.VertexKey}.
*/
public static boolean wideLineEmulationActive = false;
public static boolean lineStippleActive = false;
// Point state (GL_POINT_BIT)
@Getter protected static final PointStateStack pointState = new PointStateStack();
@@ -408,25 +422,25 @@ public class GLStateManager {
@Getter protected static final StencilStateStack stencilState = new StencilStateStack();
private static int stencilBitMask = 0xFFFFFFFF;
@Getter protected static final BooleanStateStack autoNormalState = new BooleanStateStack(GL11.GL_AUTO_NORMAL);
@Getter protected static final BooleanStateStack map1Color4State = new BooleanStateStack(GL11.GL_MAP1_COLOR_4);
@Getter protected static final BooleanStateStack map1IndexState = new BooleanStateStack(GL11.GL_MAP1_INDEX);
@Getter protected static final BooleanStateStack map1NormalState = new BooleanStateStack(GL11.GL_MAP1_NORMAL);
@Getter protected static final BooleanStateStack map1TextureCoord1State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_1);
@Getter protected static final BooleanStateStack map1TextureCoord2State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_2);
@Getter protected static final BooleanStateStack map1TextureCoord3State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_3);
@Getter protected static final BooleanStateStack map1TextureCoord4State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_4);
@Getter protected static final BooleanStateStack map1Vertex3State = new BooleanStateStack(GL11.GL_MAP1_VERTEX_3);
@Getter protected static final BooleanStateStack map1Vertex4State = new BooleanStateStack(GL11.GL_MAP1_VERTEX_4);
@Getter protected static final BooleanStateStack map2Color4State = new BooleanStateStack(GL11.GL_MAP2_COLOR_4);
@Getter protected static final BooleanStateStack map2IndexState = new BooleanStateStack(GL11.GL_MAP2_INDEX);
@Getter protected static final BooleanStateStack map2NormalState = new BooleanStateStack(GL11.GL_MAP2_NORMAL);
@Getter protected static final BooleanStateStack map2TextureCoord1State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_1);
@Getter protected static final BooleanStateStack map2TextureCoord2State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_2);
@Getter protected static final BooleanStateStack map2TextureCoord3State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_3);
@Getter protected static final BooleanStateStack map2TextureCoord4State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_4);
@Getter protected static final BooleanStateStack map2Vertex3State = new BooleanStateStack(GL11.GL_MAP2_VERTEX_3);
@Getter protected static final BooleanStateStack map2Vertex4State = new BooleanStateStack(GL11.GL_MAP2_VERTEX_4);
@Getter protected static final BooleanStateStack autoNormalState = new BooleanStateStack(GL11.GL_AUTO_NORMAL, false, true);
@Getter protected static final BooleanStateStack map1Color4State = new BooleanStateStack(GL11.GL_MAP1_COLOR_4, false, true);
@Getter protected static final BooleanStateStack map1IndexState = new BooleanStateStack(GL11.GL_MAP1_INDEX, false, true);
@Getter protected static final BooleanStateStack map1NormalState = new BooleanStateStack(GL11.GL_MAP1_NORMAL, false, true);
@Getter protected static final BooleanStateStack map1TextureCoord1State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_1, false, true);
@Getter protected static final BooleanStateStack map1TextureCoord2State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_2, false, true);
@Getter protected static final BooleanStateStack map1TextureCoord3State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_3, false, true);
@Getter protected static final BooleanStateStack map1TextureCoord4State = new BooleanStateStack(GL11.GL_MAP1_TEXTURE_COORD_4, false, true);
@Getter protected static final BooleanStateStack map1Vertex3State = new BooleanStateStack(GL11.GL_MAP1_VERTEX_3, false, true);
@Getter protected static final BooleanStateStack map1Vertex4State = new BooleanStateStack(GL11.GL_MAP1_VERTEX_4, false, true);
@Getter protected static final BooleanStateStack map2Color4State = new BooleanStateStack(GL11.GL_MAP2_COLOR_4, false, true);
@Getter protected static final BooleanStateStack map2IndexState = new BooleanStateStack(GL11.GL_MAP2_INDEX, false, true);
@Getter protected static final BooleanStateStack map2NormalState = new BooleanStateStack(GL11.GL_MAP2_NORMAL, false, true);
@Getter protected static final BooleanStateStack map2TextureCoord1State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_1, false, true);
@Getter protected static final BooleanStateStack map2TextureCoord2State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_2, false, true);
@Getter protected static final BooleanStateStack map2TextureCoord3State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_3, false, true);
@Getter protected static final BooleanStateStack map2TextureCoord4State = new BooleanStateStack(GL11.GL_MAP2_TEXTURE_COORD_4, false, true);
@Getter protected static final BooleanStateStack map2Vertex3State = new BooleanStateStack(GL11.GL_MAP2_VERTEX_3, false, true);
@Getter protected static final BooleanStateStack map2Vertex4State = new BooleanStateStack(GL11.GL_MAP2_VERTEX_4, false, true);
// Clip plane states
@Getter protected static final BooleanStateStack[] clipPlaneStates = new BooleanStateStack[MAX_CLIP_PLANES];
@@ -1037,6 +1051,7 @@ public class GLStateManager {
case GL11.GL_SHADE_MODEL -> shadeModelState.getValue();
// GL_TEXTURE_2D makes no sense here, but some mod still queries it...
case GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_BINDING_2D -> getBoundTextureForServerState();
case GL13.GL_ACTIVE_TEXTURE -> GL13.GL_TEXTURE0 + getActiveTextureUnitForServerState();
case GL11.GL_COLOR_MATERIAL_FACE -> colorMaterialFace.getValue();
case GL11.GL_COLOR_MATERIAL_PARAMETER -> colorMaterialParameter.getValue();
case GL11.GL_MODELVIEW_STACK_DEPTH -> getMatrixStackDepth(modelViewMatrix);
@@ -1045,15 +1060,18 @@ public class GLStateManager {
case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> lightModel.colorControl;
case GL14.GL_BLEND_DST_ALPHA -> blendState.getDstAlpha();
case GL14.GL_BLEND_DST_RGB -> blendState.getDstRgb();
case GL11.GL_BLEND_DST, GL14.GL_BLEND_DST_RGB -> blendState.getDstRgb();
case GL14.GL_BLEND_SRC_ALPHA -> blendState.getSrcAlpha();
case GL14.GL_BLEND_SRC_RGB -> blendState.getSrcRgb();
case GL11.GL_BLEND_SRC, GL14.GL_BLEND_SRC_RGB -> blendState.getSrcRgb();
case GL14.GL_BLEND_EQUATION -> blendState.getEquationRgb();
case GL20.GL_BLEND_EQUATION_ALPHA -> blendState.getEquationAlpha();
case GL11.GL_LOGIC_OP_MODE -> logicOpMode.getValue();
case GL11.GL_DRAW_BUFFER -> drawBuffer.getValue();
case GL11.GL_CULL_FACE_MODE -> polygonState.getCullFaceMode();
case GL11.GL_FRONT_FACE -> polygonState.getFrontFace();
case GL11.GL_STENCIL_FUNC -> stencilState.getFuncFront();
case GL11.GL_STENCIL_REF -> stencilState.getRefFront();
case GL11.GL_STENCIL_VALUE_MASK -> stencilState.getValueMaskFront();
@@ -1092,6 +1110,10 @@ public class GLStateManager {
switch (pname) {
case GL11.GL_VIEWPORT -> viewportState.get(params);
case GL11.GL_POLYGON_MODE -> {
params.put(0, polygonState.getFrontMode());
params.put(1, polygonState.getBackMode());
}
default -> {
if (!HAS_MULTIPLE_SET.contains(pname)) {
params.put(0, glGetInteger(pname));
@@ -2244,15 +2266,25 @@ public class GLStateManager {
}
public static void preDraw(int drawMode) {
prepareWideLineEmulation(drawMode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
final boolean locked = acquireDrawLock();
try {
prepareLineEmulation(drawMode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
} finally {
if (locked) releaseDrawLock();
}
}
public static void preDraw() {
disableWideLineEmulation();
ShaderManager.getInstance().preDraw();
prepareClientArrays();
final boolean locked = acquireDrawLock();
try {
disableLineEmulation();
ShaderManager.getInstance().preDraw();
prepareClientArrays();
} finally {
if (locked) releaseDrawLock();
}
}
public static void glDrawElements(int mode, ByteBuffer indices) {
@@ -3246,6 +3278,7 @@ public class GLStateManager {
splashComplete = true;
drawableGLHolder = null;
drawableGL = null;
ImmediateModeRecorder.cleanupOrphanTessellators();
}
public static void glNewList(int list, int mode) {
@@ -4055,8 +4088,8 @@ public class GLStateManager {
return RENDER_BACKEND.getTexLevelParameteri(target, level, pname);
}
return switch (pname) {
case GL11.GL_TEXTURE_WIDTH -> info.getWidth();
case GL11.GL_TEXTURE_HEIGHT -> info.getHeight();
case GL11.GL_TEXTURE_WIDTH -> Math.max(info.getWidth() >> level, 1);
case GL11.GL_TEXTURE_HEIGHT -> Math.max(info.getHeight() >> level, 1);
case GL11.GL_TEXTURE_INTERNAL_FORMAT -> {
if (info.needsInternalFormatResolve() && isRecordingDisplayList()) {
throw new IllegalStateException(String.format("glGetTexLevelParameteri(GL_TEXTURE_INTERNAL_FORMAT) needs to resolve a generic compressed format for texture %d during display list recording", getBoundTextureForServerState()));
@@ -4582,15 +4615,21 @@ public class GLStateManager {
}
}
public static void prepareWideLineEmulation(int drawMode) {
wideLineEmulationActive =
(drawMode == GL11.GL_LINES || drawMode == GL11.GL_LINE_STRIP || drawMode == GL11.GL_LINE_LOOP)
&& lineState.getWidth() > 1.0f
&& wideLineEmulationEnabled;
public static void prepareLineEmulation(int drawMode) {
final boolean isLine = drawMode == GL11.GL_LINES || drawMode == GL11.GL_LINE_STRIP || drawMode == GL11.GL_LINE_LOOP;
wideLineEmulationActive = isLine && lineState.getWidth() > 1.0f && wideLineEmulationEnabled;
setLineStippleActive(isLine && lineStippleState.isEnabled());
}
public static void disableWideLineEmulation() {
public static void disableLineEmulation() {
wideLineEmulationActive = false;
setLineStippleActive(false);
}
private static void setLineStippleActive(boolean active) {
if (lineStippleActive == active) return;
lineStippleActive = active;
RENDER_BACKEND.provokingVertex(active ? GL32.GL_FIRST_VERTEX_CONVENTION : GL32.GL_LAST_VERTEX_CONVENTION);
}
public static void glFlush() {
@@ -5183,30 +5222,36 @@ public class GLStateManager {
private static void invalidateDeletedBuffer(int buffer) {
if (buffer == 0) return;
if (boundVBO == buffer) boundVBO = 0;
if (VAOManager.boundEBO == buffer) VAOManager.boundEBO = 0;
if (boundPixelUnpackBuffer == buffer) boundPixelUnpackBuffer = 0;
if (boundPixelPackBuffer == buffer) boundPixelPackBuffer = 0;
final boolean locked = acquireDrawLock();
try {
if (boundVBO == buffer) glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (VAOManager.boundEBO == buffer) glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
if (boundPixelUnpackBuffer == buffer) glBindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0);
if (boundPixelPackBuffer == buffer) glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0);
// Sweep all VAOs: an unbound VAO may later rebind with a dangling EBO id.
for (VAOManager.VAOData data : VAOManager.vaoMap.values()) {
if (data.ebo == buffer) data.ebo = 0;
VAOManager.onDeleteBuffer(buffer);
} finally {
if (locked) releaseDrawLock();
}
}
public static void glBindBuffer(int target, int buffer) {
if (target == GL15.GL_ARRAY_BUFFER) {
// if (boundVBO == buffer) return; TODO figure out why this breaks switching async occlusion mode
boundVBO = buffer;
} else if (target == GL15.GL_ELEMENT_ARRAY_BUFFER) {
VAOManager.onBindEBO(buffer);
} else if (target == GL21.GL_PIXEL_UNPACK_BUFFER) {
boundPixelUnpackBuffer = buffer;
} else if (target == GL21.GL_PIXEL_PACK_BUFFER) {
boundPixelPackBuffer = buffer;
final boolean locked = acquireDrawLock();
try {
if (target == GL15.GL_ARRAY_BUFFER) {
// if (boundVBO == buffer) return; TODO figure out why this breaks switching async occlusion mode
boundVBO = buffer;
} else if (target == GL15.GL_ELEMENT_ARRAY_BUFFER) {
VAOManager.onBindEBO(buffer);
} else if (target == GL21.GL_PIXEL_UNPACK_BUFFER) {
boundPixelUnpackBuffer = buffer;
} else if (target == GL21.GL_PIXEL_PACK_BUFFER) {
boundPixelPackBuffer = buffer;
}
RENDER_BACKEND.bindBuffer(target, buffer);
} finally {
if (locked) releaseDrawLock();
}
RENDER_BACKEND.bindBuffer(target, buffer);
}
public static void forcePixelUnpackState(PixelUnpackState target) {
@@ -5217,9 +5262,21 @@ public class GLStateManager {
PixelUnpackState.applyDiff(applied, pixelUnpackState);
}
private static boolean warnedUntrackedPixelBuffer;
private static void warnUntrackedPixelBuffer(int binding, String name) {
if (warnedUntrackedPixelBuffer || initConfig == null || !initConfig.isLwjglDebug()) return;
final int actual = RENDER_BACKEND.getInteger(binding);
if (actual == 0) return;
warnedUntrackedPixelBuffer = true;
LOGGER.warn("{} is bound to {} but GLSM's cache says 0; a call site bypassed GLStateManager.glBindBuffer", name, actual, new Throwable());
}
static void suspendPixelUnpackBuffer() {
if (boundPixelUnpackBuffer != 0) {
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0);
} else {
warnUntrackedPixelBuffer(GL21.GL_PIXEL_UNPACK_BUFFER_BINDING, "GL_PIXEL_UNPACK_BUFFER");
}
}
@@ -5232,6 +5289,8 @@ public class GLStateManager {
static void suspendPixelPackBuffer() {
if (boundPixelPackBuffer != 0) {
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0);
} else {
warnUntrackedPixelBuffer(GL21.GL_PIXEL_PACK_BUFFER_BINDING, "GL_PIXEL_PACK_BUFFER");
}
}
@@ -5527,22 +5586,32 @@ public class GLStateManager {
if (array == 0) {
array = defaultVAO;
}
if (boundVAO != array) {
boundVAO = array;
VAOManager.onBindVertexArrayPre(array);
RENDER_BACKEND.bindVertexArray(array);
final boolean locked = acquireDrawLock();
try {
if (boundVAO != array) {
boundVAO = array;
VAOManager.onBindVertexArrayPre(array);
RENDER_BACKEND.bindVertexArray(array);
}
} finally {
if (locked) releaseDrawLock();
}
}
public static void glDeleteVertexArrays(int array) {
VAOManager.onDeleteVertexArray(array);
if (array == boundVAO) {
// Deleting the bound VAO implicitly unbinds it. Rebind the default VAO.
boundVAO = defaultVAO;
VAOManager.onBindVertexArrayPre(defaultVAO);
RENDER_BACKEND.bindVertexArray(defaultVAO);
final boolean locked = acquireDrawLock();
try {
VAOManager.onDeleteVertexArray(array);
if (array == boundVAO) {
// Deleting the bound VAO implicitly unbinds it. Rebind the default VAO.
boundVAO = defaultVAO;
VAOManager.onBindVertexArrayPre(defaultVAO);
RENDER_BACKEND.bindVertexArray(defaultVAO);
}
RENDER_BACKEND.deleteVertexArrays(array);
} finally {
if (locked) releaseDrawLock();
}
RENDER_BACKEND.deleteVertexArrays(array);
}
public static int glGenVertexArrays() {
@@ -91,21 +91,26 @@ public final class QuadConverter {
}
private static void drawSharedQuadEbo(int first, int vertexCount, int primcount, boolean instanced) {
GLStateManager.preDraw();
assert first % 4 == 0 : "QuadConverter: first (" + first + ") must be a multiple of 4";
assert vertexCount % 4 == 0 : "QuadConverter: vertexCount (" + vertexCount + ") must be a multiple of 4";
final int quadCount = vertexCount / 4;
final int prevEbo = GLStateManager.getBoundEBO();
ensureCapacity(first / 4 + quadCount);
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
// Index offset: first vertex / 4 quads * 6 indices * 4 bytes per int
final long indexOffset = (long) (first / 4) * 6 * 4;
if (instanced) {
RENDER_BACKEND.drawElementsInstanced(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset, primcount);
} else {
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset);
final boolean locked = GLStateManager.acquireDrawLock();
try {
GLStateManager.preDraw();
assert first % 4 == 0 : "QuadConverter: first (" + first + ") must be a multiple of 4";
assert vertexCount % 4 == 0 : "QuadConverter: vertexCount (" + vertexCount + ") must be a multiple of 4";
final int quadCount = vertexCount / 4;
final int prevEbo = GLStateManager.getBoundEBO();
ensureCapacity(first / 4 + quadCount);
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
// Index offset: first vertex / 4 quads * 6 indices * 4 bytes per int
final long indexOffset = (long) (first / 4) * 6 * 4;
if (instanced) {
RENDER_BACKEND.drawElementsInstanced(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset, primcount);
} else {
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, quadCount * 6, INDEX_TYPE, indexOffset);
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
}
/**
@@ -123,32 +128,37 @@ public final class QuadConverter {
}
private static void uploadAndDraw(ByteBuffer dst, int triIndexCount, int indexType, int bytesPerIndex, int primcount, boolean instanced) {
GLStateManager.preDraw();
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = GLStateManager.getBoundEBO();
final boolean locked = GLStateManager.acquireDrawLock();
try {
GLStateManager.preDraw();
final int needed = triIndexCount * bytesPerIndex;
final int prevEbo = GLStateManager.getBoundEBO();
if (scratchEboId == 0) {
scratchEboId = RENDER_BACKEND.genBuffers();
if (scratchEboId == 0) {
scratchEboId = RENDER_BACKEND.genBuffers();
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, scratchEboId);
if (needed > scratchEboCapacity) {
// Power-of-2 growth -- allocate full capacity, upload actual data
int newCap = Math.max(4096, scratchEboCapacity);
while (newCap < needed) newCap *= 2;
RENDER_BACKEND.bufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, newCap, GL15.GL_STREAM_DRAW);
scratchEboCapacity = newCap;
}
RENDER_BACKEND.bufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, 0, dst);
if (instanced) {
RENDER_BACKEND.drawElementsInstanced(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L, primcount);
} else {
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L);
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, scratchEboId);
if (needed > scratchEboCapacity) {
// Power-of-2 growth — allocate full capacity, upload actual data
int newCap = Math.max(4096, scratchEboCapacity);
while (newCap < needed) newCap *= 2;
RENDER_BACKEND.bufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, newCap, GL15.GL_STREAM_DRAW);
scratchEboCapacity = newCap;
}
RENDER_BACKEND.bufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, 0, dst);
if (instanced) {
RENDER_BACKEND.drawElementsInstanced(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L, primcount);
} else {
RENDER_BACKEND.drawElements(GL11.GL_TRIANGLES, triIndexCount, indexType, 0L);
}
RENDER_BACKEND.bindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
memFree(dst);
}
@@ -12,7 +12,7 @@ import org.lwjgl.opengl.GL13;
* Packed-long fragment shader permutation key for FFP emulation.
*
* Layout:
* long[0]: global bits (11) + unit 0 (47 bits at offset 11)
* long[0]: global bits (12) + unit 0 (47 bits at offset 12)
* long[1..3]: unit 1..3 (47 bits each, low-aligned)
* Only max(1, nrEnabledUnits) longs are significant.
*
@@ -63,13 +63,14 @@ public final class FragmentKey {
public static final int FOG_EXP = 2;
public static final int FOG_EXP2 = 3;
private static final int GLOBAL_BITS = 11;
private static final int GLOBAL_BITS = 12;
private static final int BIT_FOG_MODE = 0; // 2 bits
private static final int BIT_ALPHA_TEST = 2; // 1 bit
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
private static final int BIT_SEPARATE_SPECULAR = 6; // 1 bit
private static final int BIT_NR_ENABLED_UNITS = 7; // 3 bits
private static final int BIT_OVERLAY_ENABLED = 10; // 1 bit
private static final int BIT_LINE_STIPPLE = 11; // 1 bit
private static final int U_ENABLED = 0;
private static final int U_MODE = 1; // 3 bits
@@ -125,6 +126,10 @@ public final class FragmentKey {
global |= (1L << BIT_OVERLAY_ENABLED);
}
if (GLStateManager.lineStippleActive) {
global |= (1L << BIT_LINE_STIPPLE);
}
// Separate specular
if (GLStateManager.getLightingState().isEnabled()
&& GLStateManager.getLightModel().colorControl == GL12.GL_SEPARATE_SPECULAR_COLOR) {
@@ -233,6 +238,7 @@ public final class FragmentKey {
public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; }
public int alphaTestFunc() { return (int) ((packed[0] >> BIT_ALPHA_FUNC) & 0x7); }
public boolean separateSpecular() { return ((packed[0] >> BIT_SEPARATE_SPECULAR) & 1) != 0; }
public boolean lineStipple() { return ((packed[0] >> BIT_LINE_STIPPLE) & 1) != 0; }
public int nrEnabledUnits() { return (int) ((packed[0] >> BIT_NR_ENABLED_UNITS) & 0x7); }
public boolean overlayEnabled() { return ((packed[0] >> BIT_OVERLAY_ENABLED) & 1) != 0; }
@@ -20,6 +20,7 @@ public final class FragmentShaderGenerator {
sb.append("out vec4 fragColor;\n\n");
sb.append("void main() {\n");
emitLineStipple(sb, key);
emitTextureSampling(sb, key);
emitTexEnvChain(sb, key);
emitSpecularAdd(sb, key);
@@ -46,9 +47,19 @@ public final class FragmentShaderGenerator {
if (key.fogMode() != FragmentKey.FOG_NONE) {
sb.append("in float v_FogCoord;\n");
}
if (key.lineStipple()) {
sb.append("flat in vec2 v_LineStart;\n");
}
sb.append('\n');
}
private static void emitLineStipple(StringBuilder sb, FragmentKey key) {
if (!key.lineStipple()) return;
sb.append(" vec2 stippleDelta = abs(gl_FragCoord.xy - v_LineStart);\n");
sb.append(" int stippleBit = int(floor(max(stippleDelta.x, stippleDelta.y) / float(u_LineStipple >> 16))) & 15;\n");
sb.append(" if (((u_LineStipple >> stippleBit) & 1) == 0) discard;\n\n");
}
private static void emitUniforms(StringBuilder sb, FragmentKey key) {
// Samplers for each enabled unit
for (int i = 0; i < key.nrEnabledUnits(); i++) {
@@ -59,6 +70,9 @@ public final class FragmentShaderGenerator {
if (key.alphaTestEnabled()) {
sb.append("uniform float u_AlphaRef;\n");
}
if (key.lineStipple()) {
sb.append("uniform int u_LineStipple;\n");
}
// Per-unit texEnvColor uniforms
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (key.unitEnabled(i) && key.unitNeedsEnvColor(i)) {
@@ -29,6 +29,9 @@ public final class GeometryShaderGenerator {
if (key.fogEnabled()) {
sb.append("in float v_FogCoord_gs[];\n");
}
if (key.lineStipple()) {
sb.append("flat in vec2 v_LineStart_gs[];\n");
}
sb.append('\n');
sb.append("out vec4 v_Color;\n");
@@ -42,6 +45,9 @@ public final class GeometryShaderGenerator {
if (key.fogEnabled()) {
sb.append("out float v_FogCoord;\n");
}
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
}
sb.append('\n');
sb.append("void main() {\n");
@@ -85,6 +91,9 @@ public final class GeometryShaderGenerator {
if (key.fogEnabled()) {
sb.append(" v_FogCoord = v_FogCoord_gs").append(idx).append(";\n");
}
if (key.lineStipple()) {
sb.append(" v_LineStart = v_LineStart_gs[0];\n");
}
if (key.clipPlanesEnabled()) {
for (int i = 0; i < 8; i++) {
@@ -33,6 +33,9 @@ public class Program {
int texGenGen = -1, clipPlaneGen = -1;
float lightmapX = Float.NaN, lightmapY = Float.NaN, lineWidth = Float.NaN;
int viewportWidth = -1, viewportHeight = -1;
int stippleViewportX = Integer.MIN_VALUE, stippleViewportY = Integer.MIN_VALUE;
int stippleViewportW = -1, stippleViewportH = -1;
int lineStipple = -1;
}
final UploadState uploadState = new UploadState();
@@ -98,6 +101,10 @@ public class Program {
public int locViewportSize = -1;
public int locLineWidth = -1;
// Line stipple emulation
public int locViewport = -1;
public int locLineStipple = -1;
// Fragment uniforms
public final int[] locSampler = { -1, -1, -1, -1 };
public int locAlphaRef = -1;
@@ -174,6 +181,10 @@ public class Program {
locViewportSize = loc("u_ViewportSize");
locLineWidth = loc("u_LineWidth");
// Line stipple emulation
locViewport = loc("u_Viewport");
locLineStipple = loc("u_LineStipple");
// Fragment
for (int i = 0; i < 4; i++) {
locSampler[i] = loc("u_Sampler" + i);
@@ -6,8 +6,10 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.stacks.Vec3fStack;
import com.gtnewhorizons.angelica.glsm.stacks.Vec4fStack;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import lombok.Getter;
import org.joml.Vector3f;
import org.joml.Vector4f;
@@ -66,11 +68,33 @@ public final class ShaderManager {
}
public void enable() {
warmUp();
enabled = true;
GLStateManager.LOGGER.info("FFP shader emulation enabled");
}
// Force loading of classes before the SplashThread kicks off
// Ensure it's GL free
private static void warmUp() {
try {
final long[] fkScratch = new long[FragmentKey.MAX_UNITS];
final int fkLen = FragmentKey.packFromState(fkScratch);
final int fragMask = FragmentKey.unitMaskFromPacked(fkScratch, fkLen);
final long vkPacked = VertexKey.packFromState(true, true, true, true, fragMask);
final VertexKey vk = VertexKey.fromPacked(vkPacked);
VertexShaderGenerator.generate(vk);
FragmentShaderGenerator.generate(FragmentKey.fromPacked(fkScratch, fkLen));
GeometryShaderGenerator.generate(vk);
final Class<?>[] touched = { Program.class, ShaderCache.class, TessellatorStreamingDrawer.class, QuadConverter.class };
for (Class<?> c : touched) {
c.getName();
}
} catch (Throwable t) {
GLStateManager.LOGGER.warn("FFP warmup failed; draw-path classes will resolve lazily", t);
}
}
public void disable() {
enabled = false;
}
@@ -4,8 +4,10 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
import com.gtnewhorizons.angelica.glsm.states.LineState;
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import org.joml.Math;
import org.joml.Matrix3f;
@@ -136,6 +138,31 @@ public class Uniforms {
if (program.locLineWidth != -1 && program.locViewportSize != -1) {
uploadWideLineUniforms(program);
}
if (program.locLineStipple != -1) {
uploadLineStippleUniforms(program);
}
}
private void uploadLineStippleUniforms(Program program) {
final Program.UploadState st = program.uploadState;
final LineState line = GLStateManager.getLineState();
final int factor = line.getStippleFactor() < 1 ? 1 : line.getStippleFactor();
final int packed = (line.getStipplePattern() & 0xFFFF) | (factor << 16);
if (packed != st.lineStipple) {
RENDER_BACKEND.uniform1i(program.locLineStipple, packed);
st.lineStipple = packed;
}
final ViewportState vp = GLStateManager.getViewportState();
if (vp.x != st.stippleViewportX || vp.y != st.stippleViewportY || vp.width != st.stippleViewportW || vp.height != st.stippleViewportH) {
RENDER_BACKEND.uniform4f(program.locViewport, vp.x, vp.y, vp.width, vp.height);
st.stippleViewportX = vp.x;
st.stippleViewportY = vp.y;
st.stippleViewportW = vp.width;
st.stippleViewportH = vp.height;
}
}
private void uploadMatrices(Program program, boolean mvChanged, boolean mvLinearChanged, boolean projChanged, boolean texMatChanged) {
@@ -15,6 +15,8 @@ public final class VAOManager {
public static final Int2ObjectOpenHashMap<VAOData> vaoMap = new Int2ObjectOpenHashMap<>();
static final Int2ObjectOpenHashMap<VAOData> eboOwners = new Int2ObjectOpenHashMap<>();
private static VAOData current = null;
private static int currentVertexFlags = 0;
private static Attrib[] currentAttribs = null;
@@ -38,7 +40,11 @@ public final class VAOManager {
// Save old VAO data
if (current != null) {
current.vertexFlags = currentVertexFlags;
current.ebo = boundEBO;
if (current.ebo != boundEBO) {
removeEboOwner(current.ebo, current);
current.ebo = boundEBO;
addEboOwner(current.ebo, current);
}
current.attribs = currentAttribs;
current.clientSideEnabledCount = clientSideEnabledCount;
}
@@ -56,7 +62,46 @@ public final class VAOManager {
}
public static void onDeleteVertexArray(int vaoId) {
vaoMap.remove(vaoId);
final VAOData data = vaoMap.remove(vaoId);
if (data == null) return;
removeEboOwner(data.ebo, data);
if (data == current) current = null;
}
public static void onDeleteBuffer(int buffer) {
if (buffer == 0) return;
VAOData owner = eboOwners.remove(buffer);
while (owner != null) {
final VAOData next = owner.nextEboOwner;
owner.nextEboOwner = null;
owner.ebo = 0;
owner = next;
}
}
private static void addEboOwner(int ebo, VAOData data) {
if (ebo == 0) return;
data.nextEboOwner = eboOwners.put(ebo, data);
}
private static void removeEboOwner(int ebo, VAOData data) {
if (ebo == 0) return;
VAOData node = eboOwners.get(ebo);
if (node == data) {
final VAOData next = data.nextEboOwner;
if (next == null) eboOwners.remove(ebo); else eboOwners.put(ebo, next);
data.nextEboOwner = null;
return;
}
while (node != null) {
final VAOData next = node.nextEboOwner;
if (next == data) {
node.nextEboOwner = data.nextEboOwner;
data.nextEboOwner = null;
return;
}
node = next;
}
}
public static void enableClientVertexFlag(int flag) { currentVertexFlags |= flag; }
@@ -69,6 +114,7 @@ public final class VAOManager {
public int vertexFlags;
public int ebo;
public int clientSideEnabledCount;
private VAOData nextEboOwner;
public VAOData() {
attribs = new Attrib[MAX_ATTRIBS];
@@ -50,6 +50,7 @@ public final class VertexKey {
// Per-unit texmat enable: bits 37-40 (unit 0..3). Unit 1 is the lightmap matrix.
private static final int BIT_UNIT_TEXMAT_BASE = 37;
private static final int BIT_UNIT23_UV_FROM_UNIT0 = 41;
private static final int BIT_LINE_STIPPLE = 42;
public static final int TG_NONE = 0;
public static final int TG_OBJ_LINEAR = 1;
@@ -98,6 +99,7 @@ public final class VertexKey {
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
public boolean wideLineEmulation() { return bit(BIT_WIDE_LINE); }
public boolean lineStipple() { return bit(BIT_LINE_STIPPLE); }
public boolean cmReplacesAmbient() { final int m = colorMaterialMode(); return m == CM_AMBIENT || m == CM_AMBIENT_AND_DIFFUSE; }
public boolean cmReplacesDiffuse() { final int m = colorMaterialMode(); return m == CM_DIFFUSE || m == CM_AMBIENT_AND_DIFFUSE; }
@@ -227,6 +229,10 @@ public final class VertexKey {
bits |= (1L << BIT_WIDE_LINE);
}
if (GLStateManager.lineStippleActive) {
bits |= (1L << BIT_LINE_STIPPLE);
}
return bits;
}
@@ -29,6 +29,9 @@ public final class VertexShaderGenerator {
emitColorPassthrough(sb, key);
}
emitTexCoordPassthrough(sb, key);
if (key.lineStipple()) {
emitLineStart(sb);
}
if (key.fogEnabled()) {
emitFogDistance(sb, key);
}
@@ -45,6 +48,7 @@ public final class VertexShaderGenerator {
.replace("v_TexCoord2", "v_TexCoord2_gs")
.replace("v_TexCoord3", "v_TexCoord3_gs")
.replace("v_FogCoord", "v_FogCoord_gs")
.replace("v_LineStart", "v_LineStart_gs")
.replace("v_Color", "v_Color_gs");
}
return source;
@@ -68,6 +72,11 @@ public final class VertexShaderGenerator {
sb.append('\n');
}
private static void emitLineStart(StringBuilder sb) {
sb.append(" vec2 ndc = gl_Position.xy / gl_Position.w;\n");
sb.append(" v_LineStart = u_Viewport.xy + (ndc * 0.5 + 0.5) * u_Viewport.zw;\n\n");
}
private static void emitUniforms(StringBuilder sb, VertexKey key) {
sb.append("// Matrices\n");
sb.append("uniform mat4 u_ModelViewMatrix;\n");
@@ -77,6 +86,9 @@ public final class VertexShaderGenerator {
if (key.lightingEnabled()) {
sb.append("uniform mat3 u_NormalMatrix;\n");
}
if (key.lineStipple()) {
sb.append("uniform vec4 u_Viewport;\n");
}
for (int i = 0; i < VertexKey.MAX_UNITS; i++) {
if (i == 1) continue;
@@ -182,6 +194,9 @@ public final class VertexShaderGenerator {
private static void emitOutputs(StringBuilder sb, VertexKey key) {
sb.append("// Outputs\n");
sb.append("out vec4 v_Color;\n");
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
}
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
@@ -62,11 +62,14 @@ public final class DisplayListVBO {
}
public void render() {
vao.bind();
vao.draw(drawMode, start, count);
vao.unbind();
if (vao == null) return;
vao.bind();
if (count <= 0) { vao.unbind(); return; }
vao.draw(drawMode, start, count);
vao.unbind();
}
public IVertexArrayObject getVAO() {
return vao;
}
@@ -10,6 +10,8 @@ import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* Records immediate mode GL calls (glBegin/glEnd/glVertex) via {@link DirectTessellator}.
@@ -18,28 +20,37 @@ import java.util.Arrays;
public final class ImmediateModeRecorder {
private static final CallbackTessellator tessellator = new CallbackTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
/** Separate tessellator for the splash thread. Nulled after splash completes. */
private static DirectTessellator splashTessellator;
private record ThreadTessellator(Thread thread, DirectTessellator tessellator) {}
private static final Queue<ThreadTessellator> threadTessellators = new ConcurrentLinkedQueue<>();
private static final ThreadLocal<DirectTessellator> threadTessellator = ThreadLocal.withInitial(() -> {
final DirectTessellator t = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
threadTessellators.add(new ThreadTessellator(Thread.currentThread(), t));
cleanupOrphanTessellators();
return t;
});
private ImmediateModeRecorder() {
}
public static void initSplashTessellator() {
splashTessellator = new DirectTessellator(TessellatorManager.DEFAULT_BUFFER_SIZE);
public static int getThreadTessellatorCount() {
return threadTessellators.size();
}
public static void destroySplashTessellator() {
splashTessellator.delete();
splashTessellator = null;
public static synchronized void cleanupOrphanTessellators() {
threadTessellators.removeIf(tt -> {
if (tt.thread().isAlive()) return false;
tt.tessellator().delete();
return true;
});
}
private static DirectTessellator tessellator() {
final DirectTessellator splash = splashTessellator;
if (splash != null && Thread.currentThread() != GLStateManager.getMainThread()) {
return splash;
if (Thread.currentThread() == GLStateManager.getMainThread()) {
return tessellator;
}
return tessellator;
return threadTessellator.get();
}
public static DirectTessellator getInternalTessellator() {
@@ -167,7 +167,9 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
private boolean syncOldest() {
if (fenceQueue.isEmpty()) return false;
final FencedRegion region = fenceQueue.first();
region.fence.sync();
if (!region.fence.sync()) {
GLStateManager.LOGGER.warn("Streaming buffer fence wait did not signal; reclaiming {} bytes anyway", region.bytes);
}
region.fence.delete();
fenceQueue.dequeue();
remaining += region.bytes;
@@ -88,13 +88,18 @@ public class TessellatorStreamingDrawer {
final int vertexSize = format.getVertexSize();
final int requiredBytes = vertexCount * vertexSize;
ensureRepackCapacity(requiredBytes);
final boolean locked = GLStateManager.acquireDrawLock();
try {
ensureRepackCapacity(requiredBytes);
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
repackBuffer.position(0);
repackBuffer.limit((int)(writePtr - repackAddress));
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
// Shrink rawBuffer if oversized
if (tess.rawBufferSize > 0x20000 && tess.rawBufferIndex < (tess.rawBufferSize << 3)) {
@@ -187,24 +192,29 @@ public class TessellatorStreamingDrawer {
* Tries the persistent ring buffer first, falls back to orphan buffer on overflow.
*/
private static void uploadAndDraw(ByteBuffer packed, int flags, VertexFormat format, int vertexSize, int drawMode, int vertexCount) {
ensureVAO(flags, format);
final boolean locked = GLStateManager.acquireDrawLock();
try {
ensureVAO(flags, format);
int firstVertex = -1;
int firstVertex = -1;
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(packed, vertexSize);
if (persistentBuffer != null) {
firstVertex = persistentBuffer.upload(packed, vertexSize);
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(packed);
firstVertex = 0;
}
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
if (firstVertex >= 0) {
GLStateManager.glBindVertexArray(persistentVAOs[flags]);
} else {
GLStateManager.glBindVertexArray(orphanVAOs[flags]);
orphanBuffers[flags].upload(packed);
firstVertex = 0;
}
drawWithQuadConversion(drawMode, firstVertex, vertexCount);
GLStateManager.glBindVertexArray(0);
}
private static void drawWithQuadConversion(int drawMode, int firstVertex, int vertexCount) {
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
@@ -12,7 +11,7 @@ import java.nio.IntBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMCoreExtension.class)
@GLCoreTest
class ArrayUniformUploadGLTest {
private static int compile(int type, String src) {
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL20;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -12,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class CompatUniformManagerTest {
private int program = 0;
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -17,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests GLSM display list behavior matches OpenGL spec. */
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class DisplayListCompilationGLSMTest {
private int testList = -1;
@@ -3,16 +3,17 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests raw GL11 display list behavior (reference for GLSM). */
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class DisplayListCompilationRawGLTest {
private int testList = -1;
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -11,9 +10,11 @@ import org.lwjgl.opengl.GL30;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class FeedbackManagerTest {
@BeforeEach
@@ -0,0 +1,18 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Tag(GLTestTags.COMPAT)
@ExtendWith(GLSMExtension.class)
public @interface GLCompatTest {
}
@@ -0,0 +1,18 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.extension.ExtendWith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Tag(GLTestTags.CORE)
@ExtendWith(GLSMCoreExtension.class)
public @interface GLCoreTest {
}
@@ -1,40 +1,73 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.PixelFormat;
import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL;
/**
* 3.3 core-profile context extension
*/
public class GLSMCoreExtension implements BeforeAllCallback, AfterAllCallback {
private boolean ownsDisplay = false;
public class GLSMCoreExtension implements BeforeAllCallback, BeforeEachCallback, ExtensionContext.Store.CloseableResource {
private static boolean started = false;
private static Throwable startFailure;
@Override
public void beforeAll(ExtensionContext context) throws LWJGLException {
if (Display.isCreated()) return;
Display.setDisplayModeAndFullscreen(new DisplayMode(800, 600));
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(3, 3).withProfileCore(true).withForwardCompatible(true));
ownsDisplay = true;
GLStateManager.initialize(GLSMInitConfig.builder().displaySize(800, 600).build());
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete();
GLStateManager.BYPASS_CACHE = false;
if (startFailure != null) {
throw new IllegalStateException("GLSMCoreExtension context setup failed earlier in this JVM", startFailure);
}
if (started) return;
try {
Display.setDisplayModeAndFullscreen(new DisplayMode(800, 600));
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(3, 3).withProfileCore(true).withForwardCompatible(true));
GLSMExtension.setMainThread(Thread.currentThread());
GLStateManager.initialize(GLSMInitConfig.builder().displaySize(800, 600).build());
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete();
GLStateManager.BYPASS_CACHE = false;
GLSMExtension.glVendor = GL11.glGetString(GL11.GL_VENDOR);
GLSMExtension.glRenderer = GL11.glGetString(GL11.GL_RENDERER);
GLSMExtension.glVersion = GL11.glGetString(GL11.GL_VERSION);
System.out.println("OpenGL Vendor: " + GLSMExtension.glVendor);
System.out.println("OpenGL Renderer: " + GLSMExtension.glRenderer);
System.out.println("OpenGL Version: " + GLSMExtension.glVersion);
final int profileMask = GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) == 0) {
throw new IllegalStateException("Expected a GL core context, got profile mask 0x" + Integer.toHexString(profileMask) + " (" + GLSMExtension.glVersion + ")");
}
context.getRoot().getStore(GLOBAL).put("GLSMCoreExtension", this);
started = true;
} catch (LWJGLException | RuntimeException | Error e) {
startFailure = e;
throw e;
}
}
@Override
public void afterAll(ExtensionContext context) {
if (ownsDisplay && Display.isCreated()) Display.destroy();
ownsDisplay = false;
public void beforeEach(ExtensionContext context) {
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
@Override
public void close() {
if (Display.isCreated()) Display.destroy();
}
}
@@ -8,9 +8,11 @@ import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.PixelFormat;
import sun.misc.Unsafe;
@@ -45,6 +47,7 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
}
private static boolean started = false;
private static Throwable startFailure;
private static DisplayMode displayMode;
public static String glVendor;
public static String glRenderer;
@@ -55,21 +58,23 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
@Override
public void beforeAll(ExtensionContext context) throws LWJGLException {
if (!started) {
started = true;
if (startFailure != null) {
throw new IllegalStateException("GLSMExtension context setup failed earlier in this JVM", startFailure);
}
if (started) return;
try {
displayMode = new DisplayMode(800, 600);
final GLSMInitConfig config = GLSMInitConfig.builder().displaySize(displayMode.getWidth(), displayMode.getHeight()).build();
setMainThread(Thread.currentThread());
latchRenderSystemOnCoreContext(config);
Display.setDisplayModeAndFullscreen(displayMode);
Display.setResizable(false);
Display.setFullscreen(false);
final PixelFormat format = new PixelFormat().withDepthBits(24).withStencilBits(8);
Display.create(format);
Display.create(new PixelFormat().withDepthBits(24).withStencilBits(8));
setMainThread(Thread.currentThread());
GLStateManager.initialize(GLSMInitConfig.builder()
.displaySize(displayMode.getWidth(), displayMode.getHeight())
.build());
GLStateManager.initialize(config);
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete();
GLStateManager.BYPASS_CACHE = false;
@@ -81,6 +86,30 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
System.out.println("OpenGL Vendor: " + glVendor);
System.out.println("OpenGL Renderer: " + glRenderer);
System.out.println("OpenGL Version: " + glVersion);
final int profileMask = GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
if ((profileMask & GL32.GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) == 0) {
throw new IllegalStateException("Expected a GL compatibility context, got profile mask 0x" + Integer.toHexString(profileMask) + " (" + glVersion + ")");
}
started = true;
} catch (LWJGLException | RuntimeException | Error e) {
startFailure = e;
throw e;
}
}
private static void latchRenderSystemOnCoreContext(GLSMInitConfig config) throws LWJGLException {
Display.setDisplayModeAndFullscreen(displayMode);
Display.setResizable(false);
Display.setFullscreen(false);
Display.create(
new PixelFormat().withDepthBits(24).withStencilBits(8),
new ContextAttribs(3, 3).withProfileCore(true).withForwardCompatible(true));
try {
GLStateManager.initialize(config);
} finally {
Display.destroy();
}
}
@@ -95,7 +124,7 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
}
}
private static void setMainThread(Thread thread) {
static void setMainThread(Thread thread) {
try {
final Field f = GLStateManager.class.getDeclaredField("MainThread");
final Object base = theUnsafe.staticFieldBase(f);
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
@@ -16,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* Verifies that per-buffer blend operations (glEnablei/glDisablei) properly invalidate
* the GLSM blend cache so subsequent global blend enable/disable calls are not skipped.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_BlendUnknownState_UnitTest {
@BeforeEach
@@ -0,0 +1,105 @@
package com.gtnewhorizons.angelica.glsm;
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.MethodSource;
import org.lwjgl.opengl.GL11;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
public class GLSM_CoreRemovedCaps_GLTest {
static Stream<Arguments> removedCaps() {
return Stream.of(
cap("GL_LINE_STIPPLE", GL11.GL_LINE_STIPPLE),
cap("GL_POINT_SMOOTH", GL11.GL_POINT_SMOOTH),
cap("GL_POLYGON_STIPPLE", GL11.GL_POLYGON_STIPPLE),
cap("GL_INDEX_LOGIC_OP", GL11.GL_INDEX_LOGIC_OP),
cap("GL_AUTO_NORMAL", GL11.GL_AUTO_NORMAL),
cap("GL_MAP1_COLOR_4", GL11.GL_MAP1_COLOR_4),
cap("GL_MAP1_INDEX", GL11.GL_MAP1_INDEX),
cap("GL_MAP1_NORMAL", GL11.GL_MAP1_NORMAL),
cap("GL_MAP1_TEXTURE_COORD_1", GL11.GL_MAP1_TEXTURE_COORD_1),
cap("GL_MAP1_TEXTURE_COORD_2", GL11.GL_MAP1_TEXTURE_COORD_2),
cap("GL_MAP1_TEXTURE_COORD_3", GL11.GL_MAP1_TEXTURE_COORD_3),
cap("GL_MAP1_TEXTURE_COORD_4", GL11.GL_MAP1_TEXTURE_COORD_4),
cap("GL_MAP1_VERTEX_3", GL11.GL_MAP1_VERTEX_3),
cap("GL_MAP1_VERTEX_4", GL11.GL_MAP1_VERTEX_4),
cap("GL_MAP2_COLOR_4", GL11.GL_MAP2_COLOR_4),
cap("GL_MAP2_INDEX", GL11.GL_MAP2_INDEX),
cap("GL_MAP2_NORMAL", GL11.GL_MAP2_NORMAL),
cap("GL_MAP2_TEXTURE_COORD_1", GL11.GL_MAP2_TEXTURE_COORD_1),
cap("GL_MAP2_TEXTURE_COORD_2", GL11.GL_MAP2_TEXTURE_COORD_2),
cap("GL_MAP2_TEXTURE_COORD_3", GL11.GL_MAP2_TEXTURE_COORD_3),
cap("GL_MAP2_TEXTURE_COORD_4", GL11.GL_MAP2_TEXTURE_COORD_4),
cap("GL_MAP2_VERTEX_3", GL11.GL_MAP2_VERTEX_3),
cap("GL_MAP2_VERTEX_4", GL11.GL_MAP2_VERTEX_4));
}
private static Arguments cap(String name, int value) {
return Arguments.of(name, value);
}
private static void assertNoGlError(String what) {
final int error = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, error, () -> what + " raised 0x" + Integer.toHexString(error));
}
@ParameterizedTest(name = "{0}")
@MethodSource("removedCaps")
void enableDisableIsCacheOnly(String name, int cap) {
try {
GLStateManager.glEnable(cap);
assertNoGlError("glEnable(" + name + ")");
assertTrue(GLStateManager.glIsEnabled(cap), name + " must still round-trip through the cache");
GLStateManager.glDisable(cap);
assertNoGlError("glDisable(" + name + ")");
assertFalse(GLStateManager.glIsEnabled(cap));
} finally {
GLStateManager.glDisable(cap);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
}
@ParameterizedTest(name = "{0}")
@MethodSource("removedCaps")
void pushPopAttribIsCacheOnly(String name, int cap) {
try {
GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT);
GLStateManager.glEnable(cap);
GLStateManager.glPopAttrib();
assertNoGlError("glPushAttrib/glEnable(" + name + ")/glPopAttrib");
assertFalse(GLStateManager.glIsEnabled(cap));
} finally {
GLStateManager.glDisable(cap);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
}
@Test
void displayListReplayIsCacheOnly() {
final int list = GLStateManager.glGenLists(1);
try {
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glEnable(GL11.GL_LINE_STIPPLE);
GLStateManager.glLineStipple(2, (short) 0xF0F0);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
GLStateManager.glEndList();
assertNoGlError("display list compile");
GLStateManager.glCallList(list);
assertNoGlError("display list replay");
} finally {
GLStateManager.glDeleteLists(list, 1);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
}
}
@@ -0,0 +1,71 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** Deleting a bound buffer must not strand an id in LWJGL2's StateTracker */
@GLCompatTest
public class GLSM_DeletedBufferBinding_GLTest {
private static int makeBoundBuffer(int target) {
final int buffer = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(target, buffer);
GLStateManager.glBufferData(target, 64L, GL15.GL_STREAM_DRAW);
return buffer;
}
private static void assertUnboundAfterDelete(int target, int binding, int buffer) {
GLStateManager.glDeleteBuffers(buffer);
assertEquals(0, GL11.glGetInteger(binding), "GL unbinds a deleted buffer; the cache must not be the only place that knows");
}
@Test
void deletedPixelUnpackBufferDoesNotBlockTexImage2D() {
final int pbo = makeBoundBuffer(GL21.GL_PIXEL_UNPACK_BUFFER);
assertUnboundAfterDelete(GL21.GL_PIXEL_UNPACK_BUFFER, GL21.GL_PIXEL_UNPACK_BUFFER_BINDING, pbo);
final int tex = GLStateManager.glGenTextures();
try {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 1, 1, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(4));
} finally {
GLStateManager.glDeleteTextures(tex);
}
}
@Test
void deletedPixelPackBufferDoesNotBlockReadPixels() {
final int pbo = makeBoundBuffer(GL21.GL_PIXEL_PACK_BUFFER);
assertUnboundAfterDelete(GL21.GL_PIXEL_PACK_BUFFER, GL21.GL_PIXEL_PACK_BUFFER_BINDING, pbo);
GL11.glReadPixels(0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(4));
}
@Test
void deletedArrayBufferDoesNotBlockClientVertexPointer() {
final int vbo = makeBoundBuffer(GL15.GL_ARRAY_BUFFER);
assertUnboundAfterDelete(GL15.GL_ARRAY_BUFFER, GL15.GL_ARRAY_BUFFER_BINDING, vbo);
final int prevVao = GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING);
GL30.glBindVertexArray(0);
try {
GL11.glVertexPointer(3, 0, BufferUtils.createFloatBuffer(9));
} finally {
GL30.glBindVertexArray(prevVao);
}
}
@Test
void deletedElementArrayBufferDoesNotBlockClientDrawElements() {
final int ebo = makeBoundBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER);
assertUnboundAfterDelete(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING, ebo);
GL11.glDrawElements(GL11.GL_TRIANGLES, BufferUtils.createIntBuffer(3));
}
}
@@ -4,7 +4,6 @@ import org.joml.Vector4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
@@ -13,7 +12,7 @@ import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(GLSMCoreExtension.class)
@GLCoreTest
public class GLSM_DirtyTracking_UnitTest {
static final FloatBuffer f4b = ByteBuffer.allocateDirect(4 << 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
@@ -6,13 +6,15 @@ import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBO;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import java.util.HashSet;
import java.util.Set;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.*;
import static org.junit.jupiter.api.Assertions.*;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createColorDraw;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createDisplayList;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createSimpleDraw;
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.createTextureDraw;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Tests for display list format-based batching behavior.
@@ -24,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.*;
* <li>Different formats produce separate VBOs</li>
* </ul>
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_Batching_Test {
private static int getVBOCount(CompiledDisplayList compiledDisplayList) {
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
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 static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
@@ -12,7 +11,7 @@ import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
* Verifies that display lists work correctly with different vertex attribute combinations
* and that optimal formats are selected.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_FormatOptimization_Test {
private int displayList = -1;
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -15,7 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
* Tests for nested display list execution.
* Verifies that child display lists compose correctly with parent transforms.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_Nesting_Test {
private int parentList = -1;
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
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 java.nio.ByteBuffer;
@@ -10,7 +9,7 @@ import java.nio.ByteOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(GLSMCoreExtension.class)
@GLCoreTest
public class GLSM_DisplayList_PixelStore_Test {
private static final int SRC = 8;
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -23,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
* need the full Minecraft environment. These tests focus on matrix
* operations and display list lifecycle which work with raw GL calls.</p>
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_Stress_Test {
private final List<Integer> testLists = new ArrayList<>();
@@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.IntList;
import org.joml.Matrix4f;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -18,7 +17,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Tests for transform collapsing optimization in display lists. */
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_TransformCollapsing_Test {
private int testList = -1;
@@ -1,14 +1,12 @@
package com.gtnewhorizons.angelica.glsm;
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 static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_DisplayList_UnitTest {
@Test
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
@@ -14,7 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests that GLStateManager's GLU implementations produce the same matrices as the equivalent JOML operations.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_GLU_UnitTest {
private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(16);
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
@@ -10,10 +9,10 @@ import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyLightState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyMaterialState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_Lighting_UnitTest {
static final FloatBuffer f4b = ByteBuffer.allocateDirect(4 << 2).order(ByteOrder.nativeOrder()).asFloatBuffer();
@@ -0,0 +1,184 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.function.IntPredicate;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
public class GLSM_LineStipple_GLTest {
private static final int W = 32;
private static final int POSITION_LOC = VertexFormatElement.Usage.POSITION.getAttributeLocation();
private final ByteBuffer pixels = BufferUtils.createByteBuffer(W * 4);
private final ByteBuffer verts = ByteBuffer.allocateDirect(3 * 12).order(ByteOrder.nativeOrder());
private int savedX, savedY, savedW, savedH;
private int vao, vbo;
@BeforeEach
void setUp() {
final ViewportState vp = GLStateManager.getViewportState();
savedX = vp.x;
savedY = vp.y;
savedW = vp.width;
savedH = vp.height;
GLStateManager.glUseProgram(0);
vao = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
vbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glVertexAttribPointer(POSITION_LOC, 3, GL11.GL_FLOAT, false, 12, 0L);
GLStateManager.glEnableVertexAttribArray(POSITION_LOC);
GLStateManager.glViewport(0, 0, W, 1);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
GLStateManager.glOrtho(0, W, 0, 1, -1, 1);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
GLStateManager.glDisable(GL11.GL_DEPTH_TEST);
GLStateManager.glDisable(GL11.GL_BLEND);
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
GLStateManager.glColor4f(1f, 1f, 1f, 1f);
}
@AfterEach
void tearDown() {
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
GLStateManager.glLineStipple(1, (short) 0xFFFF);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glPopMatrix();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPopMatrix();
GLStateManager.glViewport(savedX, savedY, savedW, savedH);
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (vbo != 0) GLStateManager.glDeleteBuffers(vbo);
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
private boolean[] renderLine(float x0, float x1) {
clear();
drawLines(x0, x1);
return readCoverage();
}
private void drawLines(float... xs) {
verts.clear();
for (float x : xs) verts.putFloat(x).putFloat(0.5f).putFloat(0f);
verts.flip();
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, verts, GL15.GL_STREAM_DRAW);
GLStateManager.glDrawArrays(xs.length == 2 ? GL11.GL_LINES : GL11.GL_LINE_STRIP, 0, xs.length);
}
private void clear() {
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
private boolean[] readCoverage() {
pixels.clear();
GLStateManager.glReadPixels(0, 0, W, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "GL error during readback");
final boolean[] covered = new boolean[W];
for (int i = 0; i < W; i++) {
covered[i] = (pixels.get(i * 4) & 0xFF) > 127;
}
return covered;
}
private static boolean[] expected(IntPredicate on) {
final boolean[] e = new boolean[W];
for (int i = 0; i < W; i++) e[i] = on.test(i);
return e;
}
private static void stipple(int factor, int pattern) {
GLStateManager.glEnable(GL11.GL_LINE_STIPPLE);
GLStateManager.glLineStipple(factor, (short) pattern);
}
@Test
void solidPatternCoversEveryPixel() {
stipple(1, 0xFFFF);
assertArrayEquals(expected(i -> true), renderLine(0f, W));
}
@Test
void emptyPatternCoversNothing() {
stipple(1, 0x0000);
assertArrayEquals(expected(i -> false), renderLine(0f, W));
}
@Test
void alternatingPatternDashesEveryOtherPixel() {
stipple(1, 0xAAAA);
assertArrayEquals(expected(i -> (i & 1) == 1), renderLine(0f, W));
}
@Test
void factorStretchesEachBitAcrossThatManyPixels() {
stipple(2, 0xAAAA);
assertArrayEquals(expected(i -> ((i / 2) & 1) == 1), renderLine(0f, W));
}
@Test
void disabledStippleCoversEveryPixel() {
GLStateManager.glLineStipple(1, (short) 0xAAAA);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
assertArrayEquals(expected(i -> true), renderLine(0f, W));
}
@Test
void patternIsAnchoredAtTheSegmentStart() {
stipple(1, 0xAAAA);
final boolean[] covered = renderLine(8f, W);
for (int i = 8; i < W; i++) {
assertEquals(((i - 8) & 1) == 1, covered[i], "pixel " + i + " must be phased from x=8, not x=0");
}
}
@Test
void lineStripRestartsThePatternPerSegment() {
stipple(1, 0xAAAA);
clear();
drawLines(0f, 9f, W);
final boolean[] covered = readCoverage();
for (int i = 0; i < 9; i++) {
assertEquals((i & 1) == 1, covered[i], "segment 1 pixel " + i);
}
for (int i = 10; i < W; i++) {
assertEquals(((i - 9) & 1) == 1, covered[i], "segment 2 pixel " + i + " must restart the phase at x=9");
}
}
@Test
void nonStippleDrawAfterStippleIsUnaffected() {
stipple(1, 0xAAAA);
renderLine(0f, W);
GLStateManager.glDisable(GL11.GL_LINE_STIPPLE);
assertArrayEquals(expected(i -> true), renderLine(0f, W));
}
}
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.joml.Matrix4f;
import org.joml.Matrix4fStack;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.lwjgl.BufferUtils;
@@ -12,7 +11,7 @@ import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_MatrixStack_UnitTest {
final static FloatBuffer buffer = BufferUtils.createFloatBuffer(16);
@@ -4,7 +4,6 @@ import com.gtnewhorizons.angelica.util.GLBit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
@@ -21,7 +20,7 @@ import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyLightState;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class GLSM_PushPop_UnitTest {
@BeforeEach
@@ -1132,4 +1131,52 @@ class GLSM_PushPop_UnitTest {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glDisable(GL11.GL_TEXTURE_2D);
}
@Test
void testBlendFuncGL11AliasesMatchGL14() {
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
verifyState(GL11.GL_BLEND_SRC, GL11.GL_SRC_ALPHA, "Blend Source");
verifyState(GL14.GL_BLEND_SRC_RGB, GL11.GL_SRC_ALPHA, "Blend Source RGB");
verifyState(GL11.GL_BLEND_DST, GL11.GL_ONE_MINUS_SRC_ALPHA, "Blend Destination");
verifyState(GL14.GL_BLEND_DST_RGB, GL11.GL_ONE_MINUS_SRC_ALPHA, "Blend Destination RGB");
GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ZERO);
}
@Test
void testBlendFuncSeparateGL11AliasesTrackRgb() {
GLStateManager.glBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
verifyState(GL11.GL_BLEND_SRC, GL11.GL_SRC_ALPHA, "Blend Source");
verifyState(GL11.GL_BLEND_DST, GL11.GL_ONE_MINUS_SRC_ALPHA, "Blend Destination");
verifyState(GL14.GL_BLEND_SRC_ALPHA, GL11.GL_ONE, "Blend Source Alpha");
verifyState(GL14.GL_BLEND_DST_ALPHA, GL11.GL_ZERO, "Blend Destination Alpha");
GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ZERO);
}
@Test
void testPolygonStateReadBack() {
GLStateManager.glCullFace(GL11.GL_FRONT);
GLStateManager.glFrontFace(GL11.GL_CW);
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
verifyState(GL11.GL_CULL_FACE_MODE, GL11.GL_FRONT, "Cull Face Mode");
verifyState(GL11.GL_FRONT_FACE, GL11.GL_CW, "Front Face");
verifyState(GL11.GL_POLYGON_MODE, new int[] { GL11.GL_LINE, GL11.GL_LINE }, "Polygon Mode");
GLStateManager.glCullFace(GL11.GL_BACK);
GLStateManager.glFrontFace(GL11.GL_CCW);
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL);
}
@Test
void testActiveTextureReadBack() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
verifyState(GL13.GL_ACTIVE_TEXTURE, GL13.GL_TEXTURE1, "Active Texture");
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
verifyState(GL13.GL_ACTIVE_TEXTURE, GL13.GL_TEXTURE0, "Active Texture - Reset");
}
}
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
@@ -15,7 +14,10 @@ import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests for server-side GL state synchronization between SharedDrawable and DrawableGL contexts.
@@ -24,7 +26,7 @@ import static org.junit.jupiter.api.Assertions.*;
* be properly synchronized in the GLSM cache. Client-side state (texture bindings, active unit)
* is per-context and should NOT be shared.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_ServerSideState_UnitTest {
private SharedDrawable sharedDrawable;
@@ -3,20 +3,24 @@ package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.EXTTextureCompressionS3TC;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.EXTTextureCompressionS3TC;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_TextureInfoCache_UnitTest {
@Test
@@ -5,7 +5,6 @@ import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.util.GLSMUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
@@ -13,7 +12,7 @@ import org.lwjgl.opengl.GL30;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public class GLSM_VAO_UnitTest {
@Test
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.glsm;
public final class GLTestTags {
public static final String COMPAT = "gl-compat";
public static final String CORE = "gl-core";
public static final String SHARED = "gl-shared";
private GLTestTags() {}
}
@@ -0,0 +1,124 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
class ImmediateModeThreadIsolationTest {
@Test
void backgroundThreadsGetDistinctTessellators() throws Exception {
final DirectTessellator mainTess = ImmediateModeRecorder.getInternalTessellator();
final AtomicReference<DirectTessellator> bg1 = new AtomicReference<>();
final AtomicReference<DirectTessellator> bg2 = new AtomicReference<>();
final Thread t1 = new Thread(() -> bg1.set(ImmediateModeRecorder.getInternalTessellator()), "iso-test-1");
final Thread t2 = new Thread(() -> bg2.set(ImmediateModeRecorder.getInternalTessellator()), "iso-test-2");
t1.start(); t2.start();
t1.join(); t2.join();
assertNotNull(bg1.get());
assertNotNull(bg2.get());
assertNotSame(mainTess, bg1.get(), "background thread must not share the main-thread tessellator");
assertNotSame(mainTess, bg2.get(), "background thread must not share the main-thread tessellator");
assertNotSame(bg1.get(), bg2.get(), "background threads must not share a tessellator");
}
@Test
void concurrentImmediateModeDoesNotCorruptRecording() throws Exception {
final List<Throwable> failures = new CopyOnWriteArrayList<>();
final CountDownLatch start = new CountDownLatch(1);
final AtomicBoolean stop = new AtomicBoolean(false);
final Runnable spam = () -> {
try {
start.await();
while (!stop.get()) {
GLStateManager.glBegin(GL11.GL_QUADS);
GLStateManager.glVertex3f(0, 0, 0);
GLStateManager.glVertex3f(1, 0, 0);
GLStateManager.glVertex3f(1, 1, 0);
GLStateManager.glVertex3f(0, 1, 0);
GLStateManager.glEnd();
}
} catch (Throwable t) {
failures.add(t);
}
};
final Thread t1 = new Thread(spam, "iso-spam-1");
final Thread t2 = new Thread(spam, "iso-spam-2");
t1.start(); t2.start();
start.countDown();
final List<Integer> lists = new ArrayList<>();
try {
for (int i = 0; i < 50; i++) {
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glBegin(GL11.GL_QUADS);
GLStateManager.glVertex3f(0, 0, 0);
GLStateManager.glVertex3f(1, 0, 0);
GLStateManager.glVertex3f(1, 1, 0);
GLStateManager.glVertex3f(0, 1, 0);
GLStateManager.glEnd();
GLStateManager.glEndList();
lists.add(list);
}
} finally {
stop.set(true);
t1.join(5000); t2.join(5000);
}
assertTrue(failures.isEmpty(), "background immediate mode threw: " + failures);
for (int list : lists) {
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(list);
assertNotNull(compiled, "list " + list + " should be compiled");
assertEquals(1, compiled.getCommandCounts().getOrDefault(GLCommand.DRAW_RANGE, 0), "list " + list + " should contain exactly the main thread's draw");
GLStateManager.glDeleteLists(list, 1);
}
}
@Test
void sweepFreesDeadThreadTessellators() throws Exception {
final Thread t = new Thread(() -> ImmediateModeRecorder.getInternalTessellator(), "iso-sweep");
t.start();
t.join();
final int before = ImmediateModeRecorder.getThreadTessellatorCount();
assertTrue(before >= 1);
ImmediateModeRecorder.cleanupOrphanTessellators();
assertTrue(ImmediateModeRecorder.getThreadTessellatorCount() < before, "dead thread's tessellator should be swept");
}
@Test
void drawLockGatesOnSplashComplete() throws Exception {
assertFalse(GLStateManager.acquireDrawLock(), "post-splash acquire must be a no-op");
final Field f = GLStateManager.class.getDeclaredField("splashComplete");
f.setAccessible(true);
f.set(null, false);
try {
assertTrue(GLStateManager.acquireDrawLock(), "during splash the lock must engage");
GLStateManager.releaseDrawLock();
} finally {
f.set(null, true);
}
}
}
@@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -15,7 +14,7 @@ import java.nio.IntBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ExtendWith(GLSMCoreExtension.class)
@GLCoreTest
class InstancedQuadDrawGLTest {
private static final String VS = "#version 330 core\nlayout(location=0) in vec2 aPos;\nvoid main(){ gl_Position = vec4(aPos, 0.0, 1.0); }\n";
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
@@ -16,6 +17,7 @@ import org.lwjgl.opengl.SharedDrawable;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@Tag(GLTestTags.SHARED)
class SharedDrawableVaoTest {
private static final String VERT_330 = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
@@ -10,7 +9,7 @@ import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class VertexAttribDefaultBridgeTest {
private static final int COLOR_ATTRIB = 1; // VertexFormatElement.Usage.COLOR
@@ -1,11 +1,10 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
import com.gtnewhorizons.angelica.glsm.GLCompatTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@@ -17,13 +16,16 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests for FFP TexEnv COMBINE / multi-unit features:
* TexEnvState tracking, FragmentKey packing, FragmentShaderGenerator COMBINE codegen, and glMultiTexCoord routing.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class FFPCombineShaderTest {
private final List<Integer> shadersToDelete = new ArrayList<>();
@@ -1,10 +1,9 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLSMCoreExtension;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
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;
@@ -14,7 +13,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMCoreExtension.class)
@GLCoreTest
class FFPUnboundTextureUnitTest {
private final List<Integer> texturesToDelete = new ArrayList<>();
@@ -1,16 +1,15 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLSMCoreExtension;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ExtendWith(GLSMCoreExtension.class)
@GLCoreTest
class ProgramUniformTrackingTest {
private static Program buildProgram(boolean hasColor) {
@@ -0,0 +1,189 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class VAOManagerEboIndexTest {
private static final int DEFAULT_VAO = 0;
private static final int NO_VAO = -1;
private final Int2IntOpenHashMap expectedEbo = new Int2IntOpenHashMap();
private int currentVao;
@BeforeEach
void reset() {
drainManagerState();
expectedEbo.clear();
VAOManager.init(DEFAULT_VAO);
expectedEbo.put(DEFAULT_VAO, 0);
currentVao = DEFAULT_VAO;
}
@AfterEach
void drain() {
drainManagerState();
}
private static void drainManagerState() {
for (int vao : VAOManager.vaoMap.keySet().toIntArray()) {
VAOManager.onDeleteVertexArray(vao);
}
VAOManager.boundEBO = 0;
assertEquals(0, VAOManager.eboOwners.size(), "draining every VAO left entries in the index");
}
private void bindVao(int vao) {
if (currentVao != NO_VAO) expectedEbo.put(currentVao, VAOManager.boundEBO);
VAOManager.onBindVertexArrayPre(vao);
expectedEbo.putIfAbsent(vao, 0);
currentVao = vao;
}
private void bindEbo(int ebo) {
VAOManager.onBindEBO(ebo);
}
private int countOwners(int buffer) {
int owners = 0;
for (int vao : expectedEbo.keySet().toIntArray()) {
if (expectedEbo.get(vao) == buffer) owners++;
}
return owners;
}
private void deleteBuffer(int buffer) {
VAOManager.onDeleteBuffer(buffer);
for (int vao : expectedEbo.keySet().toIntArray()) {
if (expectedEbo.get(vao) == buffer) expectedEbo.put(vao, 0);
}
}
private void deleteVao(int vao) {
VAOManager.onDeleteVertexArray(vao);
expectedEbo.remove(vao);
if (vao == currentVao) currentVao = NO_VAO;
}
private void assertExpectedEbos(int op) {
for (var entry : VAOManager.vaoMap.int2ObjectEntrySet()) {
final int vao = entry.getIntKey();
if (vao == currentVao) continue;
assertEquals(expectedEbo.get(vao), entry.getValue().ebo, () -> "op " + op + ": vao " + vao + " ebo mismatch");
}
}
@Test
void everyVaoKeepsItsExpectedEboUnderRandomOps() {
final Random random = new Random(0xC0FFEE);
final IntArrayList liveVaos = new IntArrayList();
final IntArrayList liveBuffers = new IntArrayList();
int nextVao = 1;
int nextBuffer = 1;
int sharedEboDeletes = 0;
for (int op = 0; op < 200_000; op++) {
switch (random.nextInt(5)) {
case 0 -> {
final int vao = nextVao++;
liveVaos.add(vao);
bindVao(vao);
}
case 1 -> {
if (liveVaos.isEmpty()) break;
bindVao(liveVaos.getInt(random.nextInt(liveVaos.size())));
}
case 2 -> {
if (currentVao == NO_VAO) break;
final int buffer = (random.nextBoolean() && nextBuffer > 8) ? 1 + random.nextInt(nextBuffer - 1) : nextBuffer++;
liveBuffers.add(buffer);
bindEbo(buffer);
}
case 3 -> {
if (liveBuffers.isEmpty()) break;
final int buffer = liveBuffers.removeInt(random.nextInt(liveBuffers.size()));
if (countOwners(buffer) > 1) sharedEboDeletes++;
deleteBuffer(buffer);
}
case 4 -> {
if (liveVaos.isEmpty()) break;
final int vao = liveVaos.removeInt(random.nextInt(liveVaos.size()));
final boolean wasBound = vao == currentVao;
deleteVao(vao);
if (wasBound) bindVao(DEFAULT_VAO);
}
}
assertExpectedEbos(op);
}
assertTrue(sharedEboDeletes > 20, "op stream never freed a multi-owner EBO (hits: " + sharedEboDeletes + ")");
}
@Test
void sharedEboClearsEveryOwner() {
final int ebo = 77;
bindVao(1);
bindEbo(ebo);
bindVao(2);
bindEbo(ebo);
bindVao(3);
bindEbo(ebo);
bindVao(DEFAULT_VAO);
deleteBuffer(ebo);
assertEquals(0, VAOManager.vaoMap.get(1).ebo, "vao 1 kept a freed shared EBO");
assertEquals(0, VAOManager.vaoMap.get(2).ebo, "vao 2 kept a freed shared EBO");
assertEquals(0, VAOManager.vaoMap.get(3).ebo, "vao 3 kept a freed shared EBO");
assertFalse(VAOManager.eboOwners.containsKey(ebo), "index kept owners for a freed EBO");
}
@Test
void sharedEboEntryDropsOnlyAfterItsLastOwnerGoes() {
final int ebo = 88;
bindVao(1);
bindEbo(ebo);
bindVao(2);
bindEbo(ebo);
bindVao(DEFAULT_VAO);
deleteVao(1);
assertTrue(VAOManager.eboOwners.containsKey(ebo), "index dropped a still-live owner");
deleteVao(2);
assertFalse(VAOManager.eboOwners.containsKey(ebo), "index kept an entry after its last owner went away");
}
@Test
void deletingTheBoundVaoDoesNotOrphanAnIndexEntry() {
final int ebo = 99;
bindVao(1);
bindEbo(ebo);
deleteVao(1);
bindVao(DEFAULT_VAO);
assertFalse(VAOManager.eboOwners.containsKey(ebo), "deleting the bound VAO left an unreachable owner in the index");
assertEquals(0, VAOManager.eboOwners.size(), "index leaked an entry for a deleted VAO");
}
@Test
void indexEmptiesOutWhenEverythingIsDeleted() {
for (int vao = 1; vao <= 64; vao++) {
bindVao(vao);
bindEbo(1000 + vao);
}
bindVao(DEFAULT_VAO);
for (int vao = 1; vao <= 64; vao++) {
deleteVao(vao);
}
assertEquals(0, VAOManager.eboOwners.size(), "index leaked entries after every VAO was deleted");
}
}
@@ -2,26 +2,24 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLCompatTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
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 it.unimi.dsi.fastutil.ints.Int2IntMap;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit tests for display list recording data structures.
*/
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class RecordingDataStructuresTest {
private int testList = -1;
@@ -2,13 +2,12 @@ package com.gtnewhorizons.angelica.glsm.recording.support;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
import com.gtnewhorizons.angelica.glsm.GLCompatTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
@@ -20,7 +19,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
public abstract class DisplayListTestFixture {
public static final float[] DEFAULT_QUAD_POSITIONS = {0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f};
@@ -1,13 +1,12 @@
package com.gtnewhorizons.angelica.glsm.streaming;
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
import com.gtnewhorizons.angelica.glsm.GLCompatTest;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader.UploadStrategy;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader.UploadStrategy;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
@@ -15,9 +14,10 @@ import java.nio.ByteBuffer;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(GLSMExtension.class)
@GLCompatTest
class StreamingBufferTest {
@ParameterizedTest
@@ -9,11 +9,13 @@ import org.lwjgl.opengl.GL13;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class GLSMUtil {
@@ -39,7 +41,10 @@ public class GLSMUtil {
GL11.GL_MAP2_VERTEX_3, GL11.GL_MAP2_VERTEX_4
};
private static final Set<Integer> FFP_ONLY_CAPS = Set.of(
private static final Set<Integer> FFP_ONLY_CAPS = ffpOnlyCaps();
private static Set<Integer> ffpOnlyCaps() {
final Set<Integer> caps = new HashSet<>(Set.of(
// Enable/disable caps (BooleanState ffpStateOnly=true)
GL11.GL_FOG, GL11.GL_ALPHA_TEST, GL11.GL_LIGHTING,
GL12.GL_RESCALE_NORMAL, GL11.GL_NORMALIZE, GL11.GL_COLOR_MATERIAL,
@@ -68,8 +73,15 @@ public class GLSMUtil {
// Fog params
GL11.GL_FOG_COLOR, GL11.GL_FOG_DENSITY,
GL11.GL_FOG_START, GL11.GL_FOG_END,
GL11.GL_FOG_MODE, GL11.GL_FOG_INDEX
);
GL11.GL_FOG_MODE, GL11.GL_FOG_INDEX,
// Also removed from core profile
GL11.GL_POINT_SMOOTH, GL11.GL_POLYGON_STIPPLE,
GL11.GL_INDEX_LOGIC_OP, GL11.GL_AUTO_NORMAL));
for (int cap : MAP1_STATES) caps.add(cap);
for (int cap : MAP2_STATES) caps.add(cap);
return caps;
}
private static boolean isFFPOnly(int glCap) {
return FFP_ONLY_CAPS.contains(glCap);
@@ -81,7 +93,10 @@ public class GLSMUtil {
public static void verifyIsEnabled(int glCap, boolean expected, String message) {
if (isFFPOnly(glCap)) {
assertEquals(expected, GLStateManager.glIsEnabled(glCap), message + " - GLSM State Mismatch");
assertAll(message,
() -> assertFalse(GL11.glIsEnabled(glCap), "GL State Mismatch - cache-only cap reached the driver"),
() -> assertEquals(expected, GLStateManager.glIsEnabled(glCap), "GLSM State Mismatch")
);
} else {
assertAll(message,
() -> assertEquals(expected, GL11.glIsEnabled(glCap), "GL State Mismatch"),
@@ -96,7 +111,10 @@ public class GLSMUtil {
public static void verifyState(int glCap, boolean expected, String message) {
if (isFFPOnly(glCap)) {
assertEquals(expected, GLStateManager.glGetBoolean(glCap), message + " - GLSM State Mismatch");
assertAll(message,
() -> assertFalse(GL11.glGetBoolean(glCap), "GL State Mismatch - cache-only cap reached the driver"),
() -> assertEquals(expected, GLStateManager.glGetBoolean(glCap), "GLSM State Mismatch")
);
} else {
assertAll(message,
() -> assertEquals(expected, GL11.glGetBoolean(glCap), "GL State Mismatch"),
+1 -1
View File
@@ -61,7 +61,7 @@ dependencies {
api(project(":glsm"))
compileOnly("com.github.GTNewHorizons:lwjgl3ify:3.0.15:dev") { isTransitive = false }
compileOnly("org.embeddedt.celeritas:celeritas-common:2.5.5-GTNH") { isTransitive = false }
compileOnly("org.embeddedt.celeritas:celeritas-common:2.5.8-GTNH") { isTransitive = false }
val osName = System.getProperty("os.name").lowercase()
val osArch = System.getProperty("os.arch").lowercase()
@@ -327,6 +327,15 @@ public class BatchingFontRenderer {
private int fontAAStrengthLast = -1;
private void flushBatch() {
final boolean locked = GLStateManager.acquireDrawLock();
try {
flushBatchInner();
} finally {
if (locked) GLStateManager.releaseDrawLock();
}
}
private void flushBatchInner() {
if (vertexDataPos == 0) {
clearBatch();
return;
@@ -250,7 +250,8 @@ public enum Mixins implements IMixins {
, "celeritas.frustum.MixinFrustrum"
, "celeritas.features.culling.MixinEffectRenderer"
, "celeritas.features.mipmaps.MixinTextureAtlasSprite"
, "celeritas.features.mipmaps.MixinTextureUtil"
, "celeritas.features.mipmaps.MixinTextureMetadataSection"
, "celeritas.features.mipmaps.MixinTextureMetadataSectionSerializer"
, "celeritas.features.textures.MixinTextureMap"
, "celeritas.features.textures.MixinTextureAtlasSprite"
, "celeritas.biome_blending.MixinBlockGrass"
@@ -489,7 +490,7 @@ public enum Mixins implements IMixins {
"clouds.MixinEntityRenderer",
"clouds.MixinGameSettings",
//"clouds.MixinRenderGlobal",
"clouds.MixinWorldType",
"clouds.MixinWorldProvider",
"fix.MixinRenderItem",
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
public interface TextureMetadataExtension {
void angelica$setMipmapStrategy(MipmapStrategy strategy);
MipmapStrategy angelica$getMipmapStrategy();
}
@@ -296,7 +296,7 @@ public final class ClientProxy extends CommonProxy {
event.left.set(i + 4, String.format(
"lc: %d b: %s bl: %d sl: %d rl: %d",
chunk.getTopFilledSegment() + 15,
this.mc.theWorld.getBiomeGenForCoords(bX & 15, bZ & 15).biomeName,
this.mc.theWorld.getBiomeGenForCoords(bX, bZ).biomeName,
chunk.getSavedLightValue(EnumSkyBlock.Block, bX & 15, CompatMathHelper.clampHeight(bY), bZ & 15),
chunk.getSavedLightValue(EnumSkyBlock.Sky, bX & 15, CompatMathHelper.clampHeight(bY), bZ & 15),
chunk.getBlockLightValue(bX & 15, CompatMathHelper.clampHeight(bY), bZ & 15, 0)));
@@ -193,11 +193,6 @@ public class CloudRenderer implements IResourceManagerReloadListener {
scaleMult = cloudScaleMult;
cloudElevation = finiteCloudHeight ? (int) dimCloudHeight : 128;
// Allows the setting to work with RFG and similar without hardcoding.
// The minimum height check is so stuff like Aether cloud height doesn't get messed up.
if (cloudElevation >= 96) {
cloudElevation = (int) Settings.CLOUD_HEIGHT.option.getStore();
}
}
/**
@@ -48,19 +48,18 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
private final WorldSlice worldSlice;
@Getter
private final BlockRenderContext blockRenderContext = new BlockRenderContext();
private List<IDynamicLightSource> chunkLightSources;
private DynamicLights dynamicLightsInstance;
private final LightDataCache lightDataCache = new LightDataCache();
private final SmoothLightPipeline smoothLightPipeline;
private final FlatLightPipeline flatLightPipeline;
private final QuadLightData quadLightData = new QuadLightData();
private final VertexArrayQuadView quadView;
private final boolean hasColoredLight;
private final float[] tintResult = new float[3];
private List<IDynamicLightSource> chunkLightSources;
private DynamicLights dynamicLightsInstance;
private boolean lightPipelineReady = false;
private int originX, originY, originZ;
private float cachedSkylightSubtracted;
private final boolean hasColoredLight;
public AngelicaChunkBuildContext(RenderPassConfiguration<?> renderPassConfiguration, WorldClient world) {
super(renderPassConfiguration);
@@ -98,19 +97,24 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
}
}
private Material selectMaterial(Material material, TextureAtlasSprite sprite, boolean isShaderPackOverride) {
private Material selectMaterial(Material material, TextureAtlasSprite sprite, boolean isShaderPackOverride,
boolean useRenderPassOptimization) {
// Don't apply transparency-based optimization when shader pack explicitly overrides the material
if (!ClientProxy.options().performance.useRenderPassOptimization || isShaderPackOverride) {
if (isShaderPackOverride) {
return material;
}
if (sprite != null && sprite.getClass() == TextureAtlasSprite.class && !sprite.hasAnimationMetadata()) {
final var transparencyLevel = ((SpriteExtension)sprite).celeritas$getTransparencyLevel();
if (transparencyLevel == SpriteTransparencyLevel.OPAQUE && material == AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL) {
if (useRenderPassOptimization
&& sprite != null && sprite.getClass() == TextureAtlasSprite.class && !sprite.hasAnimationMetadata()) {
final var transparencyLevel = ((SpriteExtension) sprite).celeritas$getTransparencyLevel();
if (transparencyLevel == SpriteTransparencyLevel.OPAQUE
&& material == AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL) {
return AngelicaRenderPassConfiguration.SOLID_MATERIAL;
} else if (material == AngelicaRenderPassConfiguration.TRANSLUCENT_MATERIAL && transparencyLevel != SpriteTransparencyLevel.TRANSLUCENT) {
return AngelicaRenderPassConfiguration.CUTOUT_MIPPED_MATERIAL;
}
}
return material;
}
@@ -123,7 +127,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
return;
}
final var animatedSprites = ((MinecraftBuiltRenderSectionData<TextureAtlasSprite, TileEntity>)buffers.getSectionContextBundle()).animatedSprites;
final var animatedSprites = ((MinecraftBuiltRenderSectionData<TextureAtlasSprite, TileEntity>) buffers.getSectionContextBundle()).animatedSprites;
if ((vertexCount & 0x3) != 0) {
throw new IllegalStateException("Only quads are supported, got: " + vertexCount);
@@ -132,6 +136,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
final boolean hasDynamicLights = chunkLightSources != null && !chunkLightSources.isEmpty();
final boolean separateAo = BlockRenderingSettings.INSTANCE.shouldUseSeparateAo();
final boolean celeritasSmoothLighting = ClientProxy.options().quality.useCeleritasSmoothLighting;
final boolean useRenderPassOptimization = ClientProxy.options().performance.useRenderPassOptimization;
final boolean shaderActive = IrisApi.getInstance().isShaderPackInUse();
final boolean useAoCalculation = lightPipelineReady && (separateAo || celeritasSmoothLighting || shaderActive);
final boolean shouldApplyDiffuse = !BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading();
@@ -150,7 +155,9 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
final short originalBlockId = blockRenderContext.blockId;
int stateIdx = 0;
int facesAtBaseMaterial = 0;
boolean hasCachedMaterial = false;
TextureAtlasSprite lastSprite = null;
Material lastMaterial = null;
for (int quadIdx = 0; quadIdx < numQuads; quadIdx++) {
float uSum = 0, vSum = 0;
@@ -243,16 +250,14 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
// Apply RGB block light tint from provider
applyBlockLightTint(worldX, worldY, worldZ, vertices);
final int faceBit = 1 << facing.ordinal();
final Material correctMaterial;
if ((facesAtBaseMaterial & faceBit) != 0) {
// Face already has a non-demoted quad, skip selectMaterial entirely
correctMaterial = material;
if (hasCachedMaterial && sprite == lastSprite) {
correctMaterial = lastMaterial;
} else {
correctMaterial = selectMaterial(material, sprite, isShaderPackOverride);
if (correctMaterial == material) {
facesAtBaseMaterial |= faceBit;
}
correctMaterial = selectMaterial(material, sprite, isShaderPackOverride, useRenderPassOptimization);
lastSprite = sprite;
lastMaterial = correctMaterial;
hasCachedMaterial = true;
}
final var builder = buffers.get(correctMaterial);
@@ -267,8 +272,6 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
}
}
private final float[] tintResult = new float[3];
private void applyBlockLightTint(int x, int y, int z, ChunkVertexEncoder.Vertex[] vertices) {
if (!hasColoredLight) return;
final ExtQuadLightData ext = (ExtQuadLightData) quadLightData;
@@ -2,24 +2,43 @@ package com.gtnewhorizons.angelica.rendering.celeritas;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
import org.embeddedt.embeddium.impl.gl.device.CommandList;
import org.embeddedt.embeddium.impl.gl.device.RenderDevice;
import org.embeddedt.embeddium.impl.gl.shader.GlProgram;
import org.embeddedt.embeddium.impl.gl.shader.GlShader;
import org.embeddedt.embeddium.impl.gl.shader.ShaderConstants;
import org.embeddedt.embeddium.impl.gl.shader.ShaderParser;
import org.embeddedt.embeddium.impl.gl.shader.ShaderType;
import org.embeddedt.embeddium.impl.render.chunk.DefaultChunkRenderer;
import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
import org.embeddedt.embeddium.impl.render.chunk.multidraw.DirectMultiDrawEmitter;
import org.embeddedt.embeddium.impl.render.chunk.multidraw.IndirectMultiDrawEmitter;
import org.embeddedt.embeddium.impl.render.chunk.multidraw.MultiDrawEmitter;
import org.embeddedt.embeddium.impl.render.chunk.shader.ChunkShaderBindingPoints;
import org.embeddedt.embeddium.impl.render.chunk.shader.ChunkShaderInterface;
import org.embeddedt.embeddium.impl.render.chunk.shader.ChunkShaderOptions;
import org.embeddedt.embeddium.impl.render.chunk.shader.ChunkShaderTextureSlot;
import org.embeddedt.embeddium.impl.render.chunk.shader.DefaultChunkShaderInterface;
import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
class AngelicaChunkRenderer extends DefaultChunkRenderer {
private static final int BLOCK_TEXTURE_UNIT = 0;
private GlProgram<? extends ChunkShaderInterface> irisProgram;
private boolean usingIrisProgram;
private int rgssSampler;
private boolean rgssSamplerResolved;
private boolean rgssSamplerBound;
public AngelicaChunkRenderer(RenderDevice device, RenderPassConfiguration<?> renderPassConfiguration) {
super(device, renderPassConfiguration, createEmitter());
@@ -48,6 +67,11 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
};
}
private static GlShader loadShader(ShaderType type, String path, ShaderConstants constants) {
final String source = ShaderParser.parseShader(ShaderLoader.getShaderSource(path), ShaderLoader::getShaderSource, constants);
return new GlShader(type, path, source);
}
@Override
protected void begin(TerrainRenderPass pass) {
final IrisShaderProvider provider = IrisShaderProviderHolder.getProvider();
@@ -70,10 +94,13 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
this.usingIrisProgram = false;
this.irisProgram = null;
super.begin(pass);
bindRgssSampler();
}
@Override
protected void end(TerrainRenderPass pass) {
unbindRgssSampler();
if (usingIrisProgram && irisProgram != null) {
irisProgram.getInterface().restoreState();
irisProgram.unbind();
@@ -87,6 +114,71 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
super.end(pass);
}
private void bindRgssSampler() {
if (!AngelicaRenderPassConfiguration.isRgssEnabled()) {
return;
}
if (!rgssSamplerResolved) {
rgssSamplerResolved = true;
rgssSampler = RenderSystem.genSampler();
if (rgssSampler != 0) {
RenderSystem.samplerParameteri(rgssSampler, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
RenderSystem.samplerParameteri(rgssSampler, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
} else {
AngelicaMod.LOGGER.warn("Sampler objects unavailable; RGSS terrain filtering will fall back to nearest sampling");
}
}
if (rgssSampler != 0) {
RenderSystem.bindSamplerToUnit(BLOCK_TEXTURE_UNIT, rgssSampler);
rgssSamplerBound = true;
}
}
private void unbindRgssSampler() {
if (rgssSamplerBound) {
RenderSystem.bindSamplerToUnit(BLOCK_TEXTURE_UNIT, 0);
rgssSamplerBound = false;
}
}
@Override
public void delete(CommandList commandList) {
super.delete(commandList);
unbindRgssSampler();
RenderSystem.destroySampler(rgssSampler);
rgssSampler = 0;
rgssSamplerResolved = false;
}
@Override
protected GlProgram<ChunkShaderInterface> createShader(String path, ChunkShaderOptions options) {
if (this.enableLegacyGLPatches) {
return super.createShader(path, options);
}
final ShaderConstants constants = options.constants();
final List<GlShader> loadedShaders = new ArrayList<>();
try {
loadedShaders.add(loadShader(ShaderType.VERTEX, "sodium:" + path + ".vsh", constants));
loadedShaders.add(loadShader(ShaderType.FRAGMENT, "angelica:" + path + ".fsh", constants));
final var builder = GlProgram.builder("sodium:chunk_shader");
loadedShaders.forEach(builder::attachShader);
int i = 0;
for (var attr : options.pass().vertexType().getVertexFormat().getAttributes()) {
builder.bindAttribute(attr.getName(), i++);
}
builder.bindFragmentData("fragColor", ChunkShaderBindingPoints.FRAG_COLOR);
return builder.link((shader) -> new DefaultChunkShaderInterface(shader, options));
} finally {
loadedShaders.forEach(GlShader::delete);
}
}
@Override
protected boolean useBlockFaceCulling() {
return IrisShaderProviderHolder.shouldUseFaceCulling();
@@ -3,6 +3,9 @@ package com.gtnewhorizons.angelica.rendering.celeritas;
import com.google.common.collect.ImmutableListMultimap;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
import lombok.Getter;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.options.named.TextureFilterMode;
import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration;
import org.embeddedt.embeddium.impl.render.chunk.compile.sorting.QuadPrimitiveType;
import org.embeddedt.embeddium.impl.render.chunk.terrain.TerrainRenderPass;
@@ -16,37 +19,27 @@ import java.util.Map;
public class AngelicaRenderPassConfiguration {
public static TerrainRenderPass SOLID_PASS, CUTOUT_MIPPED_PASS, TRANSLUCENT_PASS;
public static Material SOLID_MATERIAL, CUTOUT_MIPPED_MATERIAL, TRANSLUCENT_MATERIAL;
public static Material SOLID_MATERIAL, CUTOUT_MIPPED_MATERIAL, CUTOUT_MATERIAL, TRANSLUCENT_MATERIAL;
private record AngelicaPipelineState(int pass, boolean disableAlphaTest) implements TerrainRenderPass.PipelineState {
@Override
public void setup() {
GLStateManager.glDepthMask(true);
if (pass == 0) {
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1F);
}
if (disableAlphaTest) {
GLStateManager.disableAlphaTest();
}
}
@Override
public void clear() {
if (disableAlphaTest) {
GLStateManager.enableAlphaTest();
}
}
}
@Getter
private static boolean rgssEnabled;
private static TerrainRenderPass.TerrainRenderPassBuilder builderForRenderType(int pass, boolean disableAlphaTest, ChunkVertexType vertexType) {
return TerrainRenderPass.builder()
final TerrainRenderPass.TerrainRenderPassBuilder builder = TerrainRenderPass.builder()
.pipelineState(new AngelicaPipelineState(pass, disableAlphaTest))
.vertexType(vertexType)
.primitiveType(QuadPrimitiveType.TRIANGULATED);
if (rgssEnabled) {
builder.extraDefine("USE_RGSS", "");
}
return builder;
}
public static RenderPassConfiguration<BlockRenderLayer> build(ChunkVertexType vertexType) {
rgssEnabled = SodiumGameOptions.effectiveTextureFilterMode() == TextureFilterMode.RGSS;
SOLID_PASS = builderForRenderType(0, true, vertexType)
.name("solid")
.fragmentDiscard(false)
@@ -68,7 +61,8 @@ public class AngelicaRenderPassConfiguration {
TRANSLUCENT_MATERIAL = new Material(TRANSLUCENT_PASS, AlphaCutoffParameter.ZERO, true);
SOLID_MATERIAL = new Material(SOLID_PASS, AlphaCutoffParameter.ZERO, true);
CUTOUT_MIPPED_MATERIAL = new Material(CUTOUT_MIPPED_PASS, AlphaCutoffParameter.ONE_TENTH, true);
CUTOUT_MIPPED_MATERIAL = new Material(CUTOUT_MIPPED_PASS, AlphaCutoffParameter.HALF, true);
CUTOUT_MATERIAL = new Material(CUTOUT_MIPPED_PASS, AlphaCutoffParameter.ONE_TENTH, false);
final ImmutableListMultimap.Builder<BlockRenderLayer, TerrainRenderPass> vanillaRenderStages = ImmutableListMultimap.builder();
vanillaRenderStages.put(BlockRenderLayer.SOLID, SOLID_PASS);
@@ -79,10 +73,47 @@ public class AngelicaRenderPassConfiguration {
// Material lookup by terrain layer
final Map<BlockRenderLayer, Material> renderTypeToMaterialMap = new Reference2ReferenceOpenHashMap<>(4, Reference2ReferenceOpenHashMap.VERY_FAST_LOAD_FACTOR);
renderTypeToMaterialMap.put(BlockRenderLayer.SOLID, SOLID_MATERIAL);
renderTypeToMaterialMap.put(BlockRenderLayer.CUTOUT, CUTOUT_MIPPED_MATERIAL);
renderTypeToMaterialMap.put(BlockRenderLayer.CUTOUT, CUTOUT_MATERIAL);
renderTypeToMaterialMap.put(BlockRenderLayer.CUTOUT_MIPPED, CUTOUT_MIPPED_MATERIAL);
renderTypeToMaterialMap.put(BlockRenderLayer.TRANSLUCENT, TRANSLUCENT_MATERIAL);
return new RenderPassConfiguration<>(renderTypeToMaterialMap, vanillaRenderStages.build().asMap(), CUTOUT_MIPPED_MATERIAL, CUTOUT_MIPPED_MATERIAL, TRANSLUCENT_MATERIAL);
}
private static final class AngelicaPipelineState implements TerrainRenderPass.PipelineState {
private final int pass;
private final boolean disableAlphaTest;
private int savedAlphaFunction;
private float savedAlphaReference;
private AngelicaPipelineState(int pass, boolean disableAlphaTest) {
this.pass = pass;
this.disableAlphaTest = disableAlphaTest;
}
@Override
public void setup() {
GLStateManager.glDepthMask(true);
if (pass == 0) {
final var alphaState = GLStateManager.getAlphaState();
savedAlphaFunction = alphaState.getFunction();
savedAlphaReference = alphaState.getReference();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, AlphaCutoffParameter.HALF.cutoff());
}
if (disableAlphaTest) {
GLStateManager.disableAlphaTest();
}
}
@Override
public void clear() {
if (pass == 0) {
GLStateManager.glAlphaFunc(savedAlphaFunction, savedAlphaReference);
}
if (disableAlphaTest) {
GLStateManager.enableAlphaTest();
}
}
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.rendering.celeritas;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel;
public interface SpriteExtension {
@@ -10,4 +11,5 @@ public interface SpriteExtension {
/** One-shot: returns true if marked, then resets. */
boolean celeritas$shouldUpdate();
void celeritas$setMipmapStrategy(MipmapStrategy strategy, boolean explicit, int textureType);
}
@@ -0,0 +1,294 @@
package com.gtnewhorizons.angelica.utils;
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import org.embeddedt.embeddium.api.util.ColorARGB;
import org.embeddedt.embeddium.impl.texture.MipmapHelper;
/**
* Mipmap downsampling for atlas sprites, replacing vanilla's {@code TextureUtil.generateMipmapData}.
*/
public final class MipmapGenerator {
private static final float CUTOUT_ALPHA_REF = 0.5f;
private static final float STRICT_COVERAGE_TARGET = 0.35f;
private static final float ALPHA_BIAS = 0.025f;
private MipmapGenerator() {
}
private static MipmapStrategy resolve(MipmapStrategy strategy, boolean hasTransparentPixel) {
if (strategy == null || strategy == MipmapStrategy.AUTO) {
return hasTransparentPixel ? MipmapStrategy.CUTOUT : MipmapStrategy.MEAN;
}
return strategy;
}
static boolean isCutoutStrategy(MipmapStrategy strategy, boolean hasTransparentPixel) {
final MipmapStrategy resolved = resolve(strategy, hasTransparentPixel);
return resolved == MipmapStrategy.CUTOUT
|| resolved == MipmapStrategy.STRICT_CUTOUT
|| resolved == MipmapStrategy.DARK_CUTOUT;
}
static void solidify(int[] image, int width, int height) {
final int size = Math.min(width * height, image.length);
if (size <= 0) {
return;
}
boolean hasTransparent = false;
for (int i = 0; i < size; i++) {
if (ColorARGB.unpackAlpha(image[i]) == 0) {
hasTransparent = true;
break;
}
}
if (!hasTransparent) {
return;
}
final int[] color = new int[size];
final IntArrayFIFOQueue queue = new IntArrayFIFOQueue(64);
for (int i = 0; i < size; i++) {
final int pixel = image[i];
if (ColorARGB.unpackAlpha(pixel) == 0) {
continue;
}
color[i] = pixel;
if (hasTransparentNeighbour(image, i, width, size)) {
queue.enqueue(i);
}
}
if (queue.isEmpty()) {
return;
}
while (!queue.isEmpty()) {
final int index = queue.dequeueInt();
final int x = index % width;
final int source = color[index];
if (x > 0) {
fill(index - 1, source, color, queue);
}
if (x < width - 1) {
fill(index + 1, source, color, queue);
}
if (index >= width) {
fill(index - width, source, color, queue);
}
if (index + width < size) {
fill(index + width, source, color, queue);
}
}
for (int i = 0; i < size; i++) {
if (ColorARGB.unpackAlpha(image[i]) == 0) {
image[i] = color[i] & 0x00FFFFFF;
}
}
}
private static boolean hasTransparentNeighbour(int[] image, int index, int width, int size) {
final int x = index % width;
return (x > 0 && ColorARGB.unpackAlpha(image[index - 1]) == 0)
|| (x < width - 1 && ColorARGB.unpackAlpha(image[index + 1]) == 0)
|| (index >= width && ColorARGB.unpackAlpha(image[index - width]) == 0)
|| (index + width < size && ColorARGB.unpackAlpha(image[index + width]) == 0);
}
private static void fill(int neighbour, int source, int[] color, IntArrayFIFOQueue queue) {
if (color[neighbour] == 0) {
color[neighbour] = source;
queue.enqueue(neighbour);
}
}
static void fillEmptyAreasWithDarkColor(int[] image) {
int darkest = ColorARGB.pack(255, 255, 255, 255);
int darkestSum = 255 * 3;
for (final int pixel : image) {
if (ColorARGB.unpackAlpha(pixel) == 0) {
continue;
}
final int sum = ColorARGB.unpackRed(pixel) + ColorARGB.unpackGreen(pixel) + ColorARGB.unpackBlue(pixel);
if (sum < darkestSum) {
darkestSum = sum;
darkest = pixel;
}
}
final int fill = ColorARGB.pack(3 * ColorARGB.unpackRed(darkest) / 4,
3 * ColorARGB.unpackGreen(darkest) / 4, 3 * ColorARGB.unpackBlue(darkest) / 4, 0);
for (int i = 0; i < image.length; i++) {
if (ColorARGB.unpackAlpha(image[i]) == 0) {
image[i] = fill;
}
}
}
/**
* Estimates the fraction of the image that survives, with bilinear filtering modeled by
* supersampling each texel quad on a 4x4 interior grid.
*/
static float alphaTestCoverage(int[] image, int width, float alphaRef, float alphaScale, int border) {
final int height = image.length / width;
final float alphaFactor = alphaScale / 255.0f;
float total = 0.0f;
int quads = 0;
for (int y = border; y < height - border - 1; y++) {
for (int x = border; x < width - border - 1; x++) {
final int i = x + y * width;
final float a00 = scaledAlpha(image[i], alphaFactor);
final float a10 = scaledAlpha(image[i + 1], alphaFactor);
final float a01 = scaledAlpha(image[i + width], alphaFactor);
final float a11 = scaledAlpha(image[i + width + 1], alphaFactor);
final float lo = Math.min(Math.min(a00, a10), Math.min(a01, a11));
if (lo > alphaRef) {
total += 1.0f;
quads++;
continue;
}
final float hi = Math.max(Math.max(a00, a10), Math.max(a01, a11));
if (hi <= alphaRef) {
quads++;
continue;
}
int hits = 0;
for (int sy = 0; sy < 4; sy++) {
final float fy = (sy + 0.5f) / 4.0f;
for (int sx = 0; sx < 4; sx++) {
final float fx = (sx + 0.5f) / 4.0f;
final float top = a00 + (a10 - a00) * fx;
final float bottom = a01 + (a11 - a01) * fx;
if (top + (bottom - top) * fy > alphaRef) {
hits++;
}
}
}
total += hits / 16.0f;
quads++;
}
}
if (quads == 0) {
return scaledAlpha(image[0], alphaFactor) > alphaRef ? 1.0f : 0.0f;
}
return total / quads;
}
private static float scaledAlpha(int color, float alphaFactor) {
return clamp01(ColorARGB.unpackAlpha(color) * alphaFactor);
}
private static float clamp01(float value) {
return value < 0.0f ? 0.0f : Math.min(value, 1.0f);
}
static void scaleAlphaToCoverage(int[] image, int width, float desiredCoverage, float alphaRef, int border) {
float min = 0.0f;
float max = 4.0f;
float scale = 1.0f;
float bestScale = 1.0f;
float bestError = Float.POSITIVE_INFINITY;
for (int i = 0; i < 5; i++) {
final float coverage = alphaTestCoverage(image, width, alphaRef, scale, border);
final float error = Math.abs(coverage - desiredCoverage);
if (error < bestError) {
bestError = error;
bestScale = scale;
}
if (coverage < desiredCoverage) {
min = scale;
} else if (!(coverage > desiredCoverage)) {
break;
} else {
max = scale;
}
scale = (min + max) / 2.0f;
}
for (int i = 0; i < image.length; i++) {
final int color = image[i];
final int rawAlpha = ColorARGB.unpackAlpha(color);
final float bias = rawAlpha > 0 ? ALPHA_BIAS : 0.0f;
final int alpha = (int) Math.floor(clamp01(rawAlpha / 255.0f * bestScale + bias) * 255.0f);
image[i] = ColorARGB.withAlpha(color, alpha);
}
}
public static int[][] generateMipLevels(int mipLevel, int width, int[][] currentMips,
MipmapStrategy strategy, boolean hasTransparentPixel, int border) {
final MipmapStrategy resolved = resolve(strategy, hasTransparentPixel);
final boolean isCutout = isCutoutStrategy(strategy, hasTransparentPixel);
final boolean darkCutout = resolved == MipmapStrategy.DARK_CUTOUT;
final int[][] result = new int[mipLevel + 1][];
final int[] base = currentMips[0];
if (mipLevel > 0 && (currentMips.length == 1 || currentMips[1] == null)) {
if (darkCutout) {
fillEmptyAreasWithDarkColor(base);
} else {
solidify(base, width, base.length / width);
}
}
result[0] = base;
if (mipLevel <= 0) {
return result;
}
final boolean autoResolved = strategy == null || strategy == MipmapStrategy.AUTO;
final boolean strictCoverage = resolved == MipmapStrategy.STRICT_CUTOUT;
final float alphaRef = CUTOUT_ALPHA_REF;
float originalCoverage = 0.0f;
if (isCutout) {
originalCoverage = alphaTestCoverage(base, width, alphaRef, 1.0f, border);
if (strictCoverage || (autoResolved && originalCoverage < STRICT_COVERAGE_TARGET)) {
originalCoverage = Math.max(originalCoverage, STRICT_COVERAGE_TARGET);
}
}
for (int level = 1; level <= mipLevel; level++) {
final int levelWidth = width >> level;
final int[] provided = level < currentMips.length ? currentMips[level] : null;
if (provided != null) {
result[level] = provided;
} else {
final int[] input = result[level - 1];
final int prevWidth = width >> (level - 1);
final int prevHeight = input.length / prevWidth;
final int levelHeight = prevHeight >> 1;
final int[] output = new int[levelWidth * levelHeight];
for (int y = 0; y < levelHeight; y++) {
for (int x = 0; x < levelWidth; x++) {
final int i = x * 2 + y * 2 * prevWidth;
output[x + y * levelWidth] = MipmapHelper.weightedAverageColor(
MipmapHelper.weightedAverageColor(input[i], input[i + 1]),
MipmapHelper.weightedAverageColor(input[i + prevWidth], input[i + prevWidth + 1]));
}
}
result[level] = output;
}
if (isCutout) {
scaleAlphaToCoverage(result[level], levelWidth, originalCoverage, alphaRef, border >> level);
}
}
return result;
}
}
@@ -0,0 +1,85 @@
package com.gtnewhorizons.angelica.utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.block.Block;
import net.minecraft.block.BlockFlower;
import net.minecraft.block.BlockGlass;
import net.minecraft.block.BlockLeaves;
import net.minecraft.block.BlockMushroom;
import net.minecraft.block.BlockRedstoneWire;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Fallback {@link MipmapStrategy} assignments for sprites that carry no metadata.
*/
public final class MipmapStrategies {
private static final Map<Class<?>, MipmapStrategy> CLASS_RULES = new Object2ObjectOpenHashMap<>();
private static final Map<String, MipmapStrategy> SPRITE_RULES = new ConcurrentHashMap<>();
private static final ThreadLocal<Deque<MipmapStrategy>> STRATEGY_STACK = ThreadLocal.withInitial(LinkedList::new);
static {
CLASS_RULES.put(BlockLeaves.class, MipmapStrategy.DARK_CUTOUT);
CLASS_RULES.put(BlockFlower.class, MipmapStrategy.STRICT_CUTOUT);
CLASS_RULES.put(BlockMushroom.class, MipmapStrategy.STRICT_CUTOUT);
CLASS_RULES.put(BlockGlass.class, MipmapStrategy.MEAN);
CLASS_RULES.put(BlockRedstoneWire.class, MipmapStrategy.MEAN);
}
private MipmapStrategies() {
}
public static void reset() {
SPRITE_RULES.clear();
STRATEGY_STACK.remove();
}
public static void beginBlock(Block block) {
STRATEGY_STACK.get().push(resolve(block.getClass()));
}
public static void endBlock() {
final Deque<MipmapStrategy> stack = STRATEGY_STACK.get();
if (!stack.isEmpty()) {
stack.pop();
}
}
public static void recordSprite(int textureType, String iconName) {
if (iconName == null) {
return;
}
MipmapStrategy strategy = STRATEGY_STACK.get().peek();
if (strategy == null && textureType == 0 && iconName.contains("leaves")) {
strategy = MipmapStrategy.DARK_CUTOUT;
}
if (strategy == null) {
return;
}
SPRITE_RULES.merge(iconName, strategy,
(existing, incoming) -> incoming.conflictPrecedence() > existing.conflictPrecedence() ? incoming : existing);
}
public static MipmapStrategy inheritedFor(int textureType, String iconName) {
if (iconName == null || textureType != 0) {
return null;
}
return SPRITE_RULES.get(iconName);
}
private static MipmapStrategy resolve(Class<?> type) {
for (Class<?> c = type; c != null; c = c.getSuperclass()) {
final MipmapStrategy rule = CLASS_RULES.get(c);
if (rule != null) {
return rule;
}
}
return null;
}
}
@@ -0,0 +1,33 @@
package com.gtnewhorizons.angelica.utils;
public enum MipmapStrategy {
AUTO,
MEAN,
CUTOUT,
STRICT_CUTOUT,
DARK_CUTOUT;
private static final MipmapStrategy[] VALUES = values();
public static MipmapStrategy byName(String name) {
if (name == null) {
return null;
}
for (MipmapStrategy strategy : VALUES) {
if (strategy.name().equalsIgnoreCase(name)) {
return strategy;
}
}
return null;
}
public int conflictPrecedence() {
return switch (this) {
case MEAN -> 4;
case DARK_CUTOUT -> 3;
case STRICT_CUTOUT -> 2;
case CUTOUT -> 1;
case AUTO -> 0;
};
}
}
@@ -24,8 +24,11 @@ public class SettingsManager {
new File(Launch.minecraftHome + File.separator + "optionsnf.txt")
);
public static final float VANILLA_CLOUD_HEIGHT = 128.0f;
public static int minimumFarPlaneDistance;
public static double cloudTranslucencyCheck;
public static float cloudHeightOffset;
public static AlwaysNever cloudTranslucencyMode = AlwaysNever.DEFAULT;
public static boolean shadows;
public static boolean droppedItemDetail;
public static boolean leavesOpaque;
@@ -41,19 +44,27 @@ public class SettingsManager {
}
public static void cloudsUpdated() {
cloudHeightOffset = (int)Settings.CLOUD_HEIGHT.option.getStore() - VANILLA_CLOUD_HEIGHT;
if(Settings.MODE_CLOUDS.option.getStore() != GraphicsQualityOff.OFF) {
minimumFarPlaneDistance = 32 * ((int)Settings.RENDER_DISTANCE_CLOUDS.option.getStore());
minimumFarPlaneDistance += (int)Settings.CLOUD_HEIGHT.option.getStore();
mc.gameSettings.clouds = true;
} else {
minimumFarPlaneDistance = 128;
mc.gameSettings.clouds = false;
}
switch((AlwaysNever)Settings.MODE_CLOUD_TRANSLUCENCY.option.getStore()) {
case DEFAULT -> cloudTranslucencyCheck = (int)Settings.CLOUD_HEIGHT.option.getStore();
case ALWAYS -> cloudTranslucencyCheck = Double.NEGATIVE_INFINITY;
case NEVER -> cloudTranslucencyCheck = Double.POSITIVE_INFINITY;
}
cloudTranslucencyMode = (AlwaysNever)Settings.MODE_CLOUD_TRANSLUCENCY.option.getStore();
}
public static float currentCloudHeight() {
return mc.theWorld == null ? VANILLA_CLOUD_HEIGHT : mc.theWorld.provider.getCloudHeight();
}
public static double cloudRenderOrderHeight() {
return switch(cloudTranslucencyMode) {
case ALWAYS -> Double.NEGATIVE_INFINITY;
case NEVER -> Double.POSITIVE_INFINITY;
default -> currentCloudHeight();
};
}
public static void downfallDistanceUpdated() {
@@ -6,7 +6,8 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import jss.notfine.config.NotFineConfig;
import jss.notfine.core.Settings;
import jss.notfine.core.SettingsManager;
import jss.notfine.gui.options.control.NotFineControlValueFormatter;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptionPages;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
import me.jellysquid.mods.sodium.client.gui.options.OptionGroup;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpact;
@@ -20,6 +21,7 @@ import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsMode;
import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality;
import me.jellysquid.mods.sodium.client.gui.options.named.LightingQuality;
import me.jellysquid.mods.sodium.client.gui.options.named.ParticleMode;
import me.jellysquid.mods.sodium.client.gui.options.named.TextureFilterMode;
import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage;
import me.jellysquid.mods.sodium.client.gui.options.storage.SodiumOptionsStorage;
import net.coderbot.iris.Iris;
@@ -55,7 +57,7 @@ public class NotFineGameOptionPages {
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.renderDistance"))
.setTooltip(I18n.format("sodium.options.view_distance.tooltip"))
.setControl(option -> new SliderControl(option, 2, (int)GameSettings.Options.RENDER_DISTANCE.getValueMax(), 1, ControlValueFormatter.quantity("options.chunks")))
.setControl(option -> new SliderControl(option, 2, (int) GameSettings.Options.RENDER_DISTANCE.getValueMax(), 1, ControlValueFormatter.quantity("options.chunks")))
.setBinding((options, value) -> options.renderDistanceChunks = value, options -> options.renderDistanceChunks)
.setImpact(OptionImpact.HIGH)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
@@ -95,6 +97,29 @@ public class NotFineGameOptionPages {
.build())
.build());
final OptionImpl<GameSettings, Integer> mipmapLevels = OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.mipmapLevels"))
.setTooltip(I18n.format("sodium.options.mipmap_levels.tooltip"))
.setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.multiplier()))
.setBinding((opts, value) -> opts.mipmapLevels = value, opts -> opts.mipmapLevels)
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build();
final OptionImpl<SodiumGameOptions, TextureFilterMode> textureFilterMode =
OptionImpl.createBuilder(TextureFilterMode.class, sodiumOpts)
.setName(I18n.format("sodium.options.texture_filtering.name"))
.setTooltip(I18n.format("sodium.options.texture_filtering.tooltip"))
.setControl(option -> new CyclingControl<>(option, TextureFilterMode.class,
TextureFilterMode.selectableValues(SodiumGameOptions.anisotropySupported())))
.setBinding((opts, value) -> opts.quality.textureFilterMode = value,
opts -> SodiumGameOptions.resolveFilterMode(opts.quality.textureFilterMode))
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD, OptionFlag.REQUIRES_RENDERER_RELOAD)
.build();
textureFilterMode.iris$dynamicallyEnable(() -> mipmapLevels.getValue() > 0);
int maxGuiScale = Math.max(3, Math.min(Minecraft.getMinecraft().displayWidth / 320, Minecraft.getMinecraft().displayHeight / 240));
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
@@ -125,29 +150,16 @@ public class NotFineGameOptionPages {
.build())
.add(Settings.MODE_LIGHT_FLICKER.option)
.add(Settings.DYNAMIC_FOV.option)
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.mipmapLevels"))
.setTooltip(I18n.format("sodium.options.mipmap_levels.tooltip"))
.setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.multiplier()))
//mc.getTextureMapBlocks().setMipmapLevels(this.mipmapLevels); ?
.setBinding((opts, value) -> opts.mipmapLevels = value, opts -> opts.mipmapLevels)
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build())
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.anisotropicFiltering"))
.setTooltip(I18n.format("sodium.options.anisotropic_filtering.tooltip"))
.setControl(option -> new SliderControl(option, 0, 4, 1, NotFineControlValueFormatter.powerOfTwo()))
.setBinding(
//mc.getTextureMapBlocks().setAnisotropicFiltering(this.anisotropicFiltering); ?
(opts, value) -> opts.anisotropicFiltering = value == 0 ? 1 : (int)Math.pow(2, value),
(opts) -> opts.anisotropicFiltering == 1 ? 0 : (int)(Math.log(opts.anisotropicFiltering) / Math.log(2)))
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build())
.add(mipmapLevels)
.add(textureFilterMode)
.add(SodiumGameOptionPages.anisotropicFilteringSlider(
vanillaOpts,
textureFilterMode::getValue,
mipmapLevels::getValue),
SodiumGameOptions.anisotropySupported())
.build());
if(Iris.enabled) {
if (Iris.enabled) {
final OptionImpl<GameSettings, Integer> maxShadowDistanceSlider = OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.iris.shadowDistance"))
.setTooltip(I18n.format("options.iris.shadowDistance.sodium_tooltip"))
@@ -200,7 +212,7 @@ public class NotFineGameOptionPages {
.add(Settings.MODE_DROPPED_ITEMS.option)
.add(Settings.MODE_GLINT_WORLD.option)
.add(Settings.MODE_GLINT_INV.option)
.build());
.build());
return new OptionPage(I18n.format("options.button.detail"), ImmutableList.copyOf(groups));
}
@@ -219,7 +231,7 @@ public class NotFineGameOptionPages {
.add(Settings.HORIZON.option)
.add(Settings.TERRAIN_FOG.option)
.add(Settings.FOG_NEAR_DISTANCE.option)
.build());
.build());
return new OptionPage(I18n.format("options.button.sky"), ImmutableList.copyOf(groups));
}
@@ -235,7 +247,7 @@ public class NotFineGameOptionPages {
.build())
.add(Settings.PARTICLES_VOID.option)
.add(Settings.PARTICLES_ENC_TABLE.option)
.build());
.build());
return new OptionPage(I18n.format("options.button.particle"), ImmutableList.copyOf(groups));
}
@@ -274,11 +286,11 @@ public class NotFineGameOptionPages {
.setBinding((opts, value) -> opts.showCape = value, opts -> opts.showCape)
.build())
.add(Settings.MODE_GUI_BACKGROUND.option)
.build());
.build());
groups.add(OptionGroup.createBuilder()
.add(Settings.GUI_BACKGROUND.option)
.build());
.build());
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, sodiumOpts)
@@ -25,7 +25,7 @@ public class CyclingControlElement<T extends Enum<T>> extends NotFineControlElem
@Override
public String getLabel() {
Enum<T> value = option.getValue();
return super.getLabel() + names[value.ordinal()];
return super.getLabel() + formatValue(names[value.ordinal()]);
}
@Override
@@ -1,6 +1,7 @@
package jss.notfine.gui.options.control.element;
import jss.notfine.gui.GuiCustomMenu;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.options.Option;
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
import me.jellysquid.mods.sodium.client.gui.options.control.ControlElement;
@@ -22,6 +23,10 @@ public class NotFineControlElement<T> extends GuiButton implements ControlElemen
enabled = option.isAvailable();
}
private static String disabledStyle() {
return String.valueOf(EnumChatFormatting.GRAY) + EnumChatFormatting.STRIKETHROUGH;
}
@Override
public void drawButton(Minecraft mc, int mouseX, int mouseY) {
displayString = getLabel();
@@ -34,7 +39,9 @@ public class NotFineControlElement<T> extends GuiButton implements ControlElemen
String name = option.getName();
String label;
enabled = option.isAvailable();
if(enabled && option.hasChanged()) {
if (!enabled) {
label = disabledStyle() + name;
} else if (option.hasChanged()) {
label = EnumChatFormatting.ITALIC + name;
} else {
label = name;
@@ -43,17 +50,20 @@ public class NotFineControlElement<T> extends GuiButton implements ControlElemen
return label;
}
protected String formatValue(String value) {
return option.isAvailable() ? value : disabledStyle() + value;
}
protected void onOptionValueChanged() {
option.applyChanges();
Collection<OptionFlag> flags = option.getFlags();
Minecraft mc = Minecraft.getMinecraft();
if(flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {
mc.renderGlobal.loadRenderers();
}
if(flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) {
mc.getTextureMapBlocks().setMipmapLevels(mc.gameSettings.mipmapLevels);
if (flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) {
SodiumGameOptions.applyAtlasSettings();
mc.refreshResources();
} else if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {
mc.renderGlobal.loadRenderers();
}
GuiCustomMenu.dirtyStorages.add(option.getStorage());
}
@@ -28,7 +28,7 @@ public class SliderControlElement extends NotFineControlElement<Integer> {
@Override
public String getLabel() {
return super.getLabel() + formatter.format(option.getValue());
return super.getLabel() + formatValue(formatter.format(option.getValue()));
}
@Override
@@ -12,7 +12,7 @@ public class TickBoxControlElement extends NotFineControlElement<Boolean> {
@Override
public String getLabel() {
return super.getLabel() + I18n.format(option.getValue() ? "options.on" : "options.off");
return super.getLabel() + formatValue(I18n.format(option.getValue() ? "options.on" : "options.off"));
}
@Override
@@ -10,6 +10,7 @@ import me.flashyreese.mods.reeses_sodium_options.client.gui.frame.components.Sea
import me.flashyreese.mods.reeses_sodium_options.client.gui.frame.tab.Tab;
import me.flashyreese.mods.reeses_sodium_options.client.gui.frame.tab.TabFrame;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptionPages;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI;
import me.jellysquid.mods.sodium.client.gui.options.Option;
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
@@ -239,13 +240,11 @@ public class ReeseSodiumVideoOptionsScreen extends SodiumOptionsGUI {
}));
if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {
this.mc.renderGlobal.loadRenderers();
}
if (flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) {
this.mc.getTextureMapBlocks().setMipmapLevels(this.mc.gameSettings.mipmapLevels);
SodiumGameOptions.applyAtlasSettings();
this.mc.refreshResources();
} else if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {
this.mc.renderGlobal.loadRenderers();
}
for (OptionStorage<?> storage : dirtyStorages) {
@@ -3,16 +3,18 @@ package me.jellysquid.mods.sodium.client.gui;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.settings.GameSettings;
import net.minecraft.util.MathHelper;
import org.lwjgl.opengl.Display;
import com.cardinalstar.cubicchunks.api.compat.CubicChunksVideoSettings;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
@@ -35,6 +37,7 @@ import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality;
import me.jellysquid.mods.sodium.client.gui.options.named.LightingQuality;
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
import me.jellysquid.mods.sodium.client.gui.options.named.ParticleMode;
import me.jellysquid.mods.sodium.client.gui.options.named.TextureFilterMode;
import me.jellysquid.mods.sodium.client.gui.options.storage.AngelicaOptionsStorage;
import me.jellysquid.mods.sodium.client.gui.options.storage.CubicChunksOptionStorage;
import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage;
@@ -61,11 +64,11 @@ public class SodiumGameOptionPages {
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.build());
if (ModStatus.isCubicChunksLoaded) {
firstGroupBuilder.add(getCCVerticalViewDistance());
}
if(Iris.enabled) {
final OptionImpl<GameSettings, Integer> maxShadowDistanceSlider = OptionImpl.createBuilder(int.class, vanillaOpts)
@@ -169,6 +172,29 @@ public class SodiumGameOptionPages {
public static OptionPage quality() {
final List<OptionGroup> groups = new ArrayList<>();
final OptionImpl<GameSettings, Integer> mipmapLevels = OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.mipmapLevels"))
.setTooltip(I18n.format("sodium.options.mipmap_levels.tooltip"))
.setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.multiplier()))
.setBinding((opts, value) -> opts.mipmapLevels = value, opts -> opts.mipmapLevels)
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build();
final OptionImpl<SodiumGameOptions, TextureFilterMode> textureFilterMode =
OptionImpl.createBuilder(TextureFilterMode.class, sodiumOpts)
.setName(I18n.format("sodium.options.texture_filtering.name"))
.setTooltip(I18n.format("sodium.options.texture_filtering.tooltip"))
.setControl(option -> new CyclingControl<>(option, TextureFilterMode.class,
TextureFilterMode.selectableValues(SodiumGameOptions.anisotropySupported())))
.setBinding((opts, value) -> opts.quality.textureFilterMode = value,
opts -> SodiumGameOptions.resolveFilterMode(opts.quality.textureFilterMode))
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD, OptionFlag.REQUIRES_RENDERER_RELOAD)
.build();
textureFilterMode.iris$dynamicallyEnable(() -> mipmapLevels.getValue() > 0);
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(GraphicsMode.class, vanillaOpts)
.setName(I18n.format("options.graphics"))
@@ -222,6 +248,9 @@ public class SodiumGameOptionPages {
.setImpact(OptionImpact.LOW)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.build())
.add(textureFilterMode)
.add(anisotropicFilteringSlider(vanillaOpts, textureFilterMode::getValue, mipmapLevels::getValue),
SodiumGameOptions.anisotropySupported())
// TODO
/*.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(new TranslatableText("options.biomeBlendRadius"))
@@ -245,14 +274,7 @@ public class SodiumGameOptionPages {
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.mipmapLevels"))
.setTooltip(I18n.format("sodium.options.mipmap_levels.tooltip"))
.setControl(option -> new SliderControl(option, 0, 4, 1, ControlValueFormatter.multiplier()))
.setBinding((opts, value) -> opts.mipmapLevels = value, opts -> opts.mipmapLevels)
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build())
.add(mipmapLevels)
.build());
groups.add(OptionGroup.createBuilder()
.add(Settings.MODE_GLINT_INV.option)
@@ -512,6 +534,27 @@ public class SodiumGameOptionPages {
return new OptionPage(I18n.format("sodium.options.pages.performance"), ImmutableList.copyOf(groups));
}
public static OptionImpl<GameSettings, Integer> anisotropicFilteringSlider(MinecraftOptionsStorage storage,
Supplier<TextureFilterMode> mode, IntSupplier mipmapLevels) {
final int min = SodiumGameOptions.minAnisotropyLevel();
final int max = Math.max(SodiumGameOptions.maxAnisotropyLevel(), min + 1);
final OptionImpl<GameSettings, Integer> option = OptionImpl.createBuilder(int.class, storage)
.setName(I18n.format("sodium.options.anisotropic_filtering.name"))
.setTooltip(I18n.format("sodium.options.anisotropic_filtering.tooltip"))
.setControl(opt -> new SliderControl(opt, min, max, 1, value -> (1 << value) + "x"))
.setBinding(
(opts, value) -> opts.anisotropicFiltering = 1 << MathHelper.clamp_int(value, min, max),
opts -> MathHelper.clamp_int(Integer.numberOfTrailingZeros(Math.max(opts.anisotropicFiltering, 1)), min, max))
.setImpact(OptionImpact.MEDIUM)
.setFlags(OptionFlag.REQUIRES_ASSET_RELOAD)
.build();
option.iris$dynamicallyEnable(() -> mipmapLevels.getAsInt() > 0 && mode.get().usesAnisotropy()
&& SodiumGameOptions.hasAnisotropyRange());
return option;
}
@Method(modid = "cubicchunks")
private static OptionImpl<?, ?> getCCVerticalViewDistance() {
CubicChunksOptionStorage storage = new CubicChunksOptionStorage();
@@ -5,27 +5,198 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality;
import me.jellysquid.mods.sodium.client.gui.options.named.MultiDrawMode;
import me.jellysquid.mods.sodium.client.gui.options.named.TextureFilterMode;
import net.coderbot.iris.Iris;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.MathHelper;
import org.embeddedt.embeddium.impl.render.chunk.occlusion.AsyncOcclusionMode;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class SodiumGameOptions {
private static final int MIN_ANISOTROPY_FOR_PADDING = 2;
private static final int MIN_ANISOTROPY_LEVEL = 1;
private static final int MAX_ANISOTROPY_LEVEL = 4;
private static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.setPrettyPrinting()
.excludeFieldsWithModifiers(Modifier.PRIVATE)
.create();
private static int resourcePackMipmapCeiling = Integer.MAX_VALUE;
public final QualitySettings quality = new QualitySettings();
public final AdvancedSettings advanced = new AdvancedSettings();
public final PerformanceSettings performance = new PerformanceSettings();
private Path configPath;
public static void recordAtlasMipmapClamp(int requestedMipmapLevels, int actualMipmapLevels) {
resourcePackMipmapCeiling = actualMipmapLevels < requestedMipmapLevels ? actualMipmapLevels : Integer.MAX_VALUE;
}
public static void applyAtlasSettings() {
final Minecraft mc = Minecraft.getMinecraft();
final TextureMap atlas = mc.getTextureMapBlocks();
atlas.setMipmapLevels(mc.gameSettings.mipmapLevels);
atlas.setAnisotropicFiltering(resolvedAnisotropicFiltering());
}
public static int resolvedAnisotropicFiltering() {
final Minecraft mc = Minecraft.getMinecraft();
final TextureFilterMode mode = filteringPossible()
? resolveFilterMode(ClientProxy.options().quality.textureFilterMode)
: TextureFilterMode.NONE;
return mode.usesAnisotropy()
? MathHelper.clamp_int(mc.gameSettings.anisotropicFiltering, MIN_ANISOTROPY_FOR_PADDING, 1 << maxAnisotropyLevel())
: 1;
}
private static boolean filteringPossible() {
return effectiveMipmapLevelsEstimate() > 0;
}
private static int effectiveMipmapLevelsEstimate() {
return Math.min(Minecraft.getMinecraft().gameSettings.mipmapLevels, resourcePackMipmapCeiling);
}
public static boolean needsForcedSpritePadding() {
if (!AngelicaConfig.enableCeleritas) {
return false;
}
final TextureFilterMode mode = filteringPossible()
? resolveFilterMode(ClientProxy.options().quality.textureFilterMode)
: TextureFilterMode.NONE;
return mode.needsSpritePadding() && !mode.usesAnisotropy();
}
public static TextureFilterMode effectiveTextureFilterMode() {
final TextureFilterMode mode = resolveFilterMode(ClientProxy.options().quality.textureFilterMode);
if (mode == TextureFilterMode.NONE) {
return mode;
}
return Minecraft.getMinecraft().getTextureMapBlocks().mipmapLevels > 0 ? mode : TextureFilterMode.NONE;
}
public static TextureFilterMode resolveFilterMode(TextureFilterMode mode) {
if (mode == null) {
mode = TextureFilterMode.RGSS;
}
return mode.usesAnisotropy() && !anisotropySupported() ? TextureFilterMode.RGSS : mode;
}
public static int maxAnisotropyLevel() {
final int driverMax = OpenGlHelper.anisotropicFilteringMax;
if (driverMax < 1 << MIN_ANISOTROPY_LEVEL) {
return 0;
}
return Math.min(31 - Integer.numberOfLeadingZeros(driverMax), MAX_ANISOTROPY_LEVEL);
}
public static boolean anisotropySupported() {
return maxAnisotropyLevel() > 0;
}
public static boolean hasAnisotropyRange() {
return maxAnisotropyLevel() > MIN_ANISOTROPY_LEVEL;
}
public static int minAnisotropyLevel() {
return MIN_ANISOTROPY_LEVEL;
}
public static SodiumGameOptions load(Path path) {
SodiumGameOptions config;
boolean resaveConfig = true;
if (Files.exists(path)) {
try (FileReader reader = new FileReader(path.toFile())) {
config = GSON.fromJson(reader, SodiumGameOptions.class);
} catch (IOException e) {
throw new RuntimeException("Could not parse config", e);
} catch (JsonSyntaxException e) {
AngelicaMod.LOGGER.error("Could not parse config, will fallback to default settings", e);
config = new SodiumGameOptions();
resaveConfig = false;
}
} else {
config = new SodiumGameOptions();
// Lwjgl2 doesn't expose multidraw and actually does individual, so try indirect if available
if (isLwjgl2() && !isMacOS()) {
config.advanced.multiDrawMode = MultiDrawMode.INDIRECT;
}
}
config.configPath = path;
if (config.advanced.streamingUploadStrategy == null) {
config.advanced.streamingUploadStrategy = StreamingUploader.UploadStrategy.MAP_BUFFER_RANGE;
}
if (config.quality.textureFilterMode == null) {
config.quality.textureFilterMode = TextureFilterMode.RGSS;
}
// Clamp render-ahead to 0 if GL 3.2 fences aren't available
if (GLStateManager.capabilities == null || !GLStateManager.capabilities.OpenGL32) {
config.performance.cpuRenderAheadLimit = 0;
}
// Downgrade INDIRECT if hardware doesn't support it
if (config.advanced.multiDrawMode == MultiDrawMode.INDIRECT && (GLStateManager.capabilities == null || (!GLStateManager.capabilities.OpenGL43 && !GLStateManager.capabilities.GL_ARB_multi_draw_indirect))) {
config.advanced.multiDrawMode = MultiDrawMode.DIRECT;
}
try {
if (resaveConfig)
config.writeChanges();
} catch (IOException e) {
throw new RuntimeException("Couldn't update config file", e);
}
return config;
}
private static boolean isLwjgl2() {
return SodiumGameOptions.class.getClassLoader().getResource("org/lwjgl/Sys.class") != null;
}
private static boolean isMacOS() {
return System.getProperty("os.name", "").toLowerCase().contains("mac");
}
public void writeChanges() throws IOException {
final Path dir = this.configPath.getParent();
if (!Files.exists(dir)) {
Files.createDirectories(dir);
} else if (!Files.isDirectory(dir)) {
throw new IOException("Not a directory: " + dir);
}
Files.writeString(this.configPath, GSON.toJson(this));
if (Iris.enabled) {
try {
if (Iris.getIrisConfig() != null) {
Iris.getIrisConfig().save();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static class AdvancedSettings {
public boolean useVertexArrayObjects = true;
public boolean useChunkMultidraw = true;
@@ -61,95 +232,15 @@ public class SodiumGameOptions {
public static double getRenderDistanceMult() {
return entityRenderDistanceMultiplier;
}
public static void setRenderDistanceMult(double value) {
entityRenderDistanceMultiplier = value;
}
}
public static class QualitySettings {
public GraphicsQuality grassQuality = GraphicsQuality.DEFAULT;
public boolean useCeleritasSmoothLighting = true;
}
private static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.setPrettyPrinting()
.excludeFieldsWithModifiers(Modifier.PRIVATE)
.create();
public static SodiumGameOptions load(Path path) {
SodiumGameOptions config;
boolean resaveConfig = true;
if (Files.exists(path)) {
try (FileReader reader = new FileReader(path.toFile())) {
config = GSON.fromJson(reader, SodiumGameOptions.class);
} catch (IOException e) {
throw new RuntimeException("Could not parse config", e);
} catch (JsonSyntaxException e) {
AngelicaMod.LOGGER.error("Could not parse config, will fallback to default settings", e);
config = new SodiumGameOptions();
resaveConfig = false;
}
} else {
config = new SodiumGameOptions();
// Lwjgl2 doesn't expose multidraw and actually does individual, so try indirect if available
if (isLwjgl2() && !isMacOS()) {
config.advanced.multiDrawMode = MultiDrawMode.INDIRECT;
}
}
config.configPath = path;
if (config.advanced.streamingUploadStrategy == null) {
config.advanced.streamingUploadStrategy = StreamingUploader.UploadStrategy.MAP_BUFFER_RANGE;
}
// Clamp render-ahead to 0 if GL 3.2 fences aren't available
if (GLStateManager.capabilities == null || !GLStateManager.capabilities.OpenGL32) {
config.performance.cpuRenderAheadLimit = 0;
}
// Downgrade INDIRECT if hardware doesn't support it
if (config.advanced.multiDrawMode == MultiDrawMode.INDIRECT && (GLStateManager.capabilities == null || (!GLStateManager.capabilities.OpenGL43 && !GLStateManager.capabilities.GL_ARB_multi_draw_indirect))) {
config.advanced.multiDrawMode = MultiDrawMode.DIRECT;
}
try {
if(resaveConfig)
config.writeChanges();
} catch (IOException e) {
throw new RuntimeException("Couldn't update config file", e);
}
return config;
}
private static boolean isLwjgl2() {
return SodiumGameOptions.class.getClassLoader().getResource("org/lwjgl/Sys.class") != null;
}
private static boolean isMacOS() {
return System.getProperty("os.name", "").toLowerCase().contains("mac");
}
public void writeChanges() throws IOException {
final Path dir = this.configPath.getParent();
if (!Files.exists(dir)) {
Files.createDirectories(dir);
} else if (!Files.isDirectory(dir)) {
throw new IOException("Not a directory: " + dir);
}
Files.write(this.configPath, GSON.toJson(this).getBytes(StandardCharsets.UTF_8));
if(Iris.enabled) {
try {
if (Iris.getIrisConfig() != null) {
Iris.getIrisConfig().save();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public TextureFilterMode textureFilterMode = TextureFilterMode.RGSS;
}
}
@@ -107,7 +107,7 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen {
}
// Just use the first page for now
this.currentPage = this.pages.get(0);
this.currentPage = this.pages.getFirst();
}
this.rebuildGUIPages();
@@ -272,13 +272,11 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen {
dirtyStorages.add(option.getStorage());
}));
if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {
this.mc.renderGlobal.loadRenderers();
}
if (flags.contains(OptionFlag.REQUIRES_ASSET_RELOAD)) {
this.mc.getTextureMapBlocks().setMipmapLevels(this.mc.gameSettings.mipmapLevels);
SodiumGameOptions.applyAtlasSettings();
this.mc.refreshResources();
} else if (flags.contains(OptionFlag.REQUIRES_RENDERER_RELOAD)) {
this.mc.renderGlobal.loadRenderers();
}
for (OptionStorage<?> storage : dirtyStorages) {
@@ -16,6 +16,11 @@ public class SodiumControlElement<T> extends AbstractWidget implements ControlEl
this.dim = dim;
}
protected String formatValue(String value) {
return this.option.isAvailable() ? value
: String.valueOf(EnumChatFormatting.GRAY) + EnumChatFormatting.STRIKETHROUGH + value;
}
@Override
public void render(int mouseX, int mouseY, float delta) {
String name = this.option.getName();
@@ -49,7 +49,7 @@ public class SodiumControlElementFactory implements ControlElementFactory {
super.render(mouseX, mouseY, delta);
Enum<T> value = this.option.getValue();
String name = this.names[value.ordinal()];
String name = this.formatValue(this.names[value.ordinal()]);
int strWidth = this.getTextWidth(name);
this.drawString(name, this.dim.getLimitX() - strWidth - 6, this.dim.getCenterY() - 4, 0xFFFFFFFF);
@@ -120,7 +120,7 @@ public class SodiumControlElementFactory implements ControlElementFactory {
int sliderWidth = this.sliderBounds.getWidth();
int sliderHeight = this.sliderBounds.getHeight();
String label = this.formatter.format(this.option.getValue());
String label = this.formatValue(this.formatter.format(this.option.getValue()));
int labelWidth = this.font.getStringWidth(label);
this.drawString(label, sliderX + sliderWidth - labelWidth, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF);
@@ -137,12 +137,12 @@ public class SodiumControlElementFactory implements ControlElementFactory {
double thumbOffset = MathHelper.clamp_double((double) (this.getIntValue() - this.min) / this.range * sliderWidth, 0, sliderWidth);
double thumbX = sliderX + thumbOffset - THUMB_WIDTH;
double trackY = sliderY + (sliderHeight / 2) - ((double) TRACK_HEIGHT / 2);
double trackY = sliderY + ((double) sliderHeight / 2) - ((double) TRACK_HEIGHT / 2);
this.drawRect(thumbX, sliderY, thumbX + (THUMB_WIDTH * 2), sliderY + sliderHeight, 0xFFFFFFFF);
this.drawRect(sliderX, trackY, sliderX + sliderWidth, trackY + TRACK_HEIGHT, 0xFFFFFFFF);
String label = String.valueOf(this.getIntValue());
String label = this.formatter.format(this.getIntValue());
int labelWidth = this.font.getStringWidth(label);
@@ -0,0 +1,38 @@
package me.jellysquid.mods.sodium.client.gui.options.named;
/**
* Selects how the terrain fragment shader samples the block atlas.
*/
public enum TextureFilterMode implements NamedState {
RGSS("sodium.options.texture_filtering.rgss"),
ANISOTROPIC("sodium.options.texture_filtering.anisotropic"),
NONE("options.off");
private static final TextureFilterMode[] WITHOUT_ANISOTROPY = {RGSS, NONE};
private final String name;
TextureFilterMode(String name) {
this.name = name;
}
/**
* Anisotropic filtering rides on an extension that isn't guaranteed in GL 3.3, so it is only offered when the
* driver actually exposes it.
*/
public static TextureFilterMode[] selectableValues(boolean anisotropySupported) {
return anisotropySupported ? values() : WITHOUT_ANISOTROPY;
}
@Override
public String getKey() {
return this.name;
}
public boolean usesAnisotropy() {
return this == ANISOTROPIC;
}
public boolean needsSpritePadding() {
return this == RGSS;
}
}
@@ -94,6 +94,21 @@ public abstract class IrisGuiSlot extends GuiSlot {
if (leftMouseDown && mouseX >= scrollBarX && mouseX <= rightEdge) {
scrolling = true;
this.initialClickY = (float) mouseY;
int contentOverflow = this.func_148135_f/*getMaxScroll*/();
if (contentOverflow < 1) {
contentOverflow = 1;
}
int scrollPosSize = (this.bottom - this.top) * (this.bottom - this.top) / this.getContentHeight();
if (scrollPosSize < 32) {
scrollPosSize = 32;
}
if (scrollPosSize > this.bottom - this.top - 8) {
scrollPosSize = this.bottom - this.top - 8;
}
this.scrollMultiplier = -1.0F / ((float) (this.bottom - this.top - scrollPosSize) / (float) contentOverflow);
} else if ((leftMouseDown || rightMouseDown)) {
final int index = relativeY / this.slotHeight;
@@ -128,7 +143,7 @@ public abstract class IrisGuiSlot extends GuiSlot {
// Scrollbar nonsense
if (scrolling) {
this.amountScrolled += ((float) mouseY - this.initialClickY);
this.amountScrolled -= ((float) mouseY - this.initialClickY) * this.scrollMultiplier;
this.initialClickY = mouseY;
} else if (this.mc.currentScreen != null ){
while (!this.mc.gameSettings.touchscreen && Mouse.next()) {
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.compat.mojang.NativeImage;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import net.coderbot.iris.Iris;
import net.coderbot.iris.texture.format.TextureFormat;
import net.coderbot.iris.texture.format.TextureFormatLoader;
@@ -219,7 +220,9 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
protected PBRTextureAtlasSprite(TextureAtlasSpriteInfo info, AnimationMetadataSection animationMetaDataSection, int atlasWidth, int atlasHeight, int x, int y, NativeImage nativeImage, TextureMap texMap, int miplevel) {
super(info.name().toString());
super.initSprite(atlasWidth, atlasHeight, x, y, false);
super.loadSprite(getMipmapGenerator(info, atlasWidth, atlasHeight).generateMipLevels(nativeImage, miplevel), animationMetaDataSection, texMap.anisotropicFiltering > 1);
final boolean useAnisotropicFiltering = texMap.anisotropicFiltering > 1
|| SodiumGameOptions.needsForcedSpritePadding();
super.loadSprite(getMipmapGenerator(info, atlasWidth, atlasHeight).generateMipLevels(nativeImage, miplevel), animationMetaDataSection, useAnisotropicFiltering);
}
@Override
@@ -38,6 +38,12 @@ sodium.options.grass_quality.name=Grass Quality
sodium.options.grass_quality.tooltip=Controls the quality of grass block sides.
sodium.options.celeritas_smooth_lighting.name=Celeritas Smooth Lighting
sodium.options.celeritas_smooth_lighting.tooltip=If enabled, Celeritas's lighting pipeline will be used for smooth lighting. This only has an effect if smooth lighting is enabled.
sodium.options.texture_filtering.name=Texture Filtering
sodium.options.texture_filtering.tooltip=Controls how block textures are sampled at a distance.
sodium.options.texture_filtering.rgss=RGSS
sodium.options.texture_filtering.anisotropic=Anisotropic
sodium.options.anisotropic_filtering.name=Anisotropic Filtering
sodium.options.anisotropic_filtering.tooltip=Sharpens textures viewed at steep angles.
sodium.options.particle_quality.name=Particle Quality
sodium.options.particle_quality.tooltip=Controls the maximum number of particles which can be present on screen at any one time.
sodium.options.smooth_lighting.tooltip=Controls whether blocks will be smoothly lit and shaded. This slightly increases the amount of time needed to re-build a chunk, but doesn't affect frame rates.
@@ -0,0 +1,139 @@
#version 330 core
#import <sodium:include/fog.glsl>
in vec4 v_Color;
in vec2 v_TexCoord;
in float v_ChunkAgeMs;
in float v_MaterialMipBias;
#ifdef USE_FRAGMENT_DISCARD
in float v_MaterialAlphaCutoff;
#endif
#if defined(USE_FOG_POSTMODERN)
in float v_SphericalFragDistance;
in float v_CylindricalFragDistance;
#elif defined(USE_FOG)
in float v_FragDistance;
#endif
uniform sampler2D u_BlockTex;
uniform vec4 u_FogColor;
#ifdef USE_FOG_SMOOTH
uniform float u_FogStart;
uniform float u_FogEnd;
#endif
#ifdef USE_FOG_POSTMODERN
uniform float u_RenderDistFogStart;
uniform float u_RenderDistFogEnd;
uniform float u_EnvFogStart;
uniform float u_EnvFogEnd;
#endif
#ifdef USE_FOG_EXP2
uniform float u_FogDensity;
#endif
#ifndef LEGACY
out vec4 fragColor;
#else
#define fragColor gl_FragColor
#endif
#ifdef USE_RGSS
// Standard rotated-grid supersampling offsets, in texels.
const vec2 RGSS_OFFSETS[4] = vec2[4](
vec2( 0.125, 0.375),
vec2(-0.125, -0.375),
vec2( 0.375, -0.125),
vec2(-0.375, 0.125)
);
// Sprites are padded so RGSS taps don't cross into a neighbouring atlas sprite
const float MAX_TAP_SCALE = 8.0;
vec4 sampleRGSS(sampler2D tex, vec2 uv, vec2 texSize, vec2 pixelSize) {
vec2 du = dFdx(uv);
vec2 dv = dFdy(uv);
vec2 duTexels = du * texSize;
vec2 dvTexels = dv * texSize;
vec2 du2 = duTexels * duTexels;
vec2 dv2 = dvTexels * dvTexels;
vec2 texelScreenSize = sqrt(du2 + dv2);
float blendFactor = smoothstep(1.0, 2.0, max(texelScreenSize.x, texelScreenSize.y));
float lod = max(0.0, 0.25 * log2((du2.x + du2.y) * (dv2.x + dv2.y)) + v_MaterialMipBias);
vec4 rgss = vec4(0.0);
if (blendFactor > 0.0) {
vec2 tapSize = pixelSize * min(exp2(lod), MAX_TAP_SCALE);
for (int i = 0; i < 4; i++) {
rgss += textureLod(tex, uv + RGSS_OFFSETS[i] * tapSize, lod);
}
rgss *= 0.25;
if (blendFactor >= 1.0) {
return rgss;
}
}
vec2 t = uv * texSize;
vec2 c = round(t) - 0.5;
vec2 o = t - c;
o = clamp((o - 0.5) / max(texelScreenSize, vec2(1e-8)) + 0.5, 0.0, 1.0);
vec4 nearest = textureLod(tex, (c + o) * pixelSize, lod);
return mix(nearest, rgss, blendFactor);
}
#endif
void main() {
#ifdef USE_RGSS
vec2 texSize = vec2(textureSize(u_BlockTex, 0));
vec4 diffuseColor = sampleRGSS(u_BlockTex, v_TexCoord, texSize, 1.0 / texSize);
#else
vec4 diffuseColor = texture(u_BlockTex, v_TexCoord, v_MaterialMipBias);
#endif
#ifdef USE_FRAGMENT_DISCARD
if (diffuseColor.a < v_MaterialAlphaCutoff) {
discard;
}
#endif
vec4 m_color = v_Color;
#ifdef USE_VANILLA_COLOR_FORMAT
diffuseColor *= m_color;
#else
diffuseColor.rgb *= m_color.rgb;
diffuseColor.rgb *= m_color.a;
#endif
#ifdef USE_FOG
#if defined(CHUNK_FADE_IN_DURATION_MS) && CHUNK_FADE_IN_DURATION_MS > 0
diffuseColor = vec4(mix(u_FogColor.rgb, diffuseColor.rgb, (clamp(v_ChunkAgeMs, 0, CHUNK_FADE_IN_DURATION_MS) / CHUNK_FADE_IN_DURATION_MS)), diffuseColor.a);
#endif
#ifdef USE_FOG_POSTMODERN
float fogValue = max(_linearFogValue(v_CylindricalFragDistance, u_RenderDistFogStart, u_RenderDistFogEnd),
_linearFogValue(v_SphericalFragDistance, u_EnvFogStart, u_EnvFogEnd));
fragColor = vec4(mix(diffuseColor.rgb, u_FogColor.rgb, fogValue * u_FogColor.a), diffuseColor.a);
#elif defined(USE_FOG_EXP2)
fragColor = _exp2Fog(diffuseColor, v_FragDistance, u_FogColor, u_FogDensity);
#elif defined(USE_FOG_SMOOTH)
fragColor = _linearFog(diffuseColor, v_FragDistance, u_FogColor, u_FogStart, u_FogEnd);
#endif
#else
fragColor = diffuseColor;
#endif
}
@@ -10,7 +10,6 @@ options.graphics.ultra=Ultra
options.off=Aus
options.on=An
options.cloud_height=Wolkenhöhe
options.cloud_height.tooltip=Wolkenhöhe
options.cloud_scale=Wolkengröße
options.cloud_scale.tooltip=Wolkengröße
options.gui_background=Hintergrund
@@ -11,7 +11,7 @@ options.graphics.ultra=Ultra
options.off=Off
options.on=On
options.cloud_height=Cloud Elevation
options.cloud_height.tooltip=Cloud Elevation
options.cloud_height.tooltip=Height clouds render at in worlds that do not set their own. Worlds that set one keep it, shifted by this setting's difference from 128.
options.cloud_scale=Cloud Scale
options.cloud_scale.tooltip=Cloud Scale
options.gui_background=Background
@@ -11,7 +11,6 @@ options.graphics.ultra=ウルトラ
options.off=オフ
options.on=オン
options.cloud_height=雲の高さ
options.cloud_height.tooltip=雲の高さを設定します。
options.cloud_scale=雲の大きさ
options.cloud_scale.tooltip=雲の大きさを設定します。
options.gui_background=背景テーマ
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.mixin.Mixin;
@@ -27,14 +26,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class MixinSplashProgressCaching {
private static final Logger LOGGER = LogManager.getLogger("Angelica");
/**
* Before splash starts, create a separate DirectTessellator for the splash thread.
*/
@Inject(method = "start", at = @At("HEAD"))
private static void angelica$initSplashTessellator(CallbackInfo ci) {
ImmediateModeRecorder.initSplashTessellator();
}
// VAOs aren't shared across GL contexts; the SharedDrawable the client thread just swapped
// onto has none. Core profile (macOS) rejects glValidateProgram without one.
@Inject(method = "start", at = @At("RETURN"))
@@ -42,12 +33,8 @@ public class MixinSplashProgressCaching {
GLStateManager.glBindVertexArray(GLStateManager.glGenVertexArrays());
}
/**
* On return from finish() - destroy splash tessellator and mark splash complete
*/
@Inject(method = "finish", at = @At("RETURN"))
private static void angelica$enableCachingOnFinish(CallbackInfo ci) {
ImmediateModeRecorder.destroySplashTessellator();
GLStateManager.markSplashComplete();
LOGGER.info("Splash Complete");
}
@@ -2,108 +2,160 @@ package com.gtnewhorizons.angelica.mixins.early.celeritas.features.mipmaps;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension;
import com.gtnewhorizons.angelica.utils.MipmapGenerator;
import com.gtnewhorizons.angelica.utils.MipmapStrategies;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.embeddedt.embeddium.api.util.ColorABGR;
import org.embeddedt.embeddium.api.util.ColorARGB;
import org.embeddedt.embeddium.impl.render.chunk.sprite.SpriteTransparencyLevel;
import org.embeddedt.embeddium.impl.util.color.ColorSRGB;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
/** Calculates sprite transparency level during mipmap generation. */
/**
* Calculates sprite transparency and routes mipmap generation through MipmapGenerator.
*/
@Mixin(TextureAtlasSprite.class)
public abstract class MixinTextureAtlasSprite implements SpriteExtension {
@Shadow protected List<int[][]> framesTextureData;
@Shadow @Final private String iconName;
@Unique private SpriteTransparencyLevel celeritas$transparencyLevel = SpriteTransparencyLevel.TRANSLUCENT;
@Inject(method = "generateMipmaps", at = @At("HEAD"))
private void celeritas$processSprite(int level, CallbackInfo ci) {
if (this.framesTextureData.isEmpty() || this.framesTextureData.get(0) == null) {
return;
}
if (celeritas$isAlwaysTranslucent(this.iconName)) {
this.celeritas$transparencyLevel = SpriteTransparencyLevel.TRANSLUCENT;
return;
}
celeritas$processTransparentImages(this.framesTextureData.get(0)[0], level > 0 && !iconName.contains("leaves"));
}
@Unique
private static final int SEEN_TRANSPARENT = 1;
@Unique
private static final int SEEN_TRANSLUCENT = 2;
@Unique
private static final int SEEN_ALL = SEEN_TRANSPARENT | SEEN_TRANSLUCENT;
@Unique
private static final int SCAN_BLOCK = 256;
@Shadow
public boolean useAnisotropicFiltering;
@Shadow
public List<int[][]> framesTextureData;
@Shadow
@Final
private String iconName;
@Unique
private SpriteTransparencyLevel celeritas$transparencyLevel = SpriteTransparencyLevel.TRANSLUCENT;
@Unique
private MipmapStrategy celeritas$mipmapStrategy;
@Unique
private boolean celeritas$mipmapStrategyExplicit;
@Unique
private int celeritas$textureType;
@Unique
private boolean celeritas$hasTransparentTexels;
@Unique
private boolean celeritas$alwaysTranslucent;
@Unique
private static boolean celeritas$isAlwaysTranslucent(String name) {
final String[] list = AngelicaConfig.alwaysTranslucentSprites;
if (list == null || list.length == 0) return false;
if (list == null) return false;
for (String s : list) {
if (name.equals(s)) return true;
}
return false;
}
// Ordinals: OPAQUE=0, TRANSPARENT=1, TRANSLUCENT=2
@Unique private static final int ORDINAL_OPAQUE = SpriteTransparencyLevel.OPAQUE.ordinal();
@Unique private static final int ORDINAL_TRANSPARENT = SpriteTransparencyLevel.TRANSPARENT.ordinal();
@Unique private static final int ORDINAL_TRANSLUCENT = SpriteTransparencyLevel.TRANSLUCENT.ordinal();
@Unique private static final SpriteTransparencyLevel[] LEVELS_BY_ORDINAL = SpriteTransparencyLevel.values();
@Inject(method = "generateMipmaps", at = @At("HEAD"))
private void celeritas$processSprite(int level, CallbackInfo ci) {
this.celeritas$hasTransparentTexels = false;
this.celeritas$alwaysTranslucent = celeritas$isAlwaysTranslucent(this.iconName);
@Unique
private void celeritas$processTransparentImages(int[] nativeImage, boolean shouldRewriteColors) {
float r = 0.0f, g = 0.0f, b = 0.0f;
float totalWeight = 0.0f;
// Use primitive int instead of loop-carried enum reference to avoid JDK 25 C2 SuperWord non-convergence bug
int maxLevel = ORDINAL_OPAQUE;
for (int y = 0; y < nativeImage.length; y++) {
final int color = nativeImage[y];
final int alpha = ColorABGR.unpackAlpha(color);
if (alpha > 0) {
if (alpha < 255) {
maxLevel = Math.max(maxLevel, ORDINAL_TRANSLUCENT);
}
if (shouldRewriteColors) {
final float weight = (float) alpha;
r += ColorSRGB.srgbToLinear(ColorABGR.unpackRed(color)) * weight;
g += ColorSRGB.srgbToLinear(ColorABGR.unpackGreen(color)) * weight;
b += ColorSRGB.srgbToLinear(ColorABGR.unpackBlue(color)) * weight;
totalWeight += weight;
}
} else {
maxLevel = Math.max(maxLevel, ORDINAL_TRANSPARENT);
}
}
this.celeritas$transparencyLevel = LEVELS_BY_ORDINAL[maxLevel];
if (!shouldRewriteColors || totalWeight == 0.0f) {
if (this.framesTextureData.isEmpty()) {
return;
}
celeritas$classifyFrames();
if (this.celeritas$alwaysTranslucent) {
this.celeritas$transparencyLevel = SpriteTransparencyLevel.TRANSLUCENT;
}
}
r /= totalWeight;
g /= totalWeight;
b /= totalWeight;
@Unique
private MipmapStrategy celeritas$resolvedStrategy() {
if (this.celeritas$alwaysTranslucent) {
return MipmapStrategy.MEAN;
}
final int averageColor = ColorSRGB.linearToSrgb(r, g, b, 0);
MipmapStrategy strategy = this.celeritas$mipmapStrategy;
boolean explicit = this.celeritas$mipmapStrategyExplicit;
for (int y = 0; y < nativeImage.length; y++) {
final int color = nativeImage[y];
final int alpha = ColorABGR.unpackAlpha(color);
if (alpha == 0) {
nativeImage[y] = averageColor;
if (strategy == null) {
strategy = MipmapStrategies.inheritedFor(this.celeritas$textureType, this.iconName);
explicit = false;
}
if (strategy != null && !explicit && !this.celeritas$hasTransparentTexels) {
strategy = null;
}
return strategy;
}
@Unique
private void celeritas$classifyFrames() {
int seen = 0;
outer:
for (final int[][] frame : this.framesTextureData) {
if (frame == null || frame.length == 0 || frame[0] == null) {
continue;
}
final int[] pixels = frame[0];
for (int base = 0; base < pixels.length; base += SCAN_BLOCK) {
final int end = Math.min(base + SCAN_BLOCK, pixels.length);
for (int i = base; i < end; i++) {
final int alpha = ColorARGB.unpackAlpha(pixels[i]);
if (alpha == 0) {
seen |= SEEN_TRANSPARENT;
} else if (alpha < 255) {
seen |= SEEN_TRANSLUCENT;
}
}
if (seen == SEEN_ALL) {
break outer;
}
}
}
this.celeritas$hasTransparentTexels = (seen & SEEN_TRANSPARENT) != 0;
if ((seen & SEEN_TRANSLUCENT) != 0) {
this.celeritas$transparencyLevel = SpriteTransparencyLevel.TRANSLUCENT;
} else if ((seen & SEEN_TRANSPARENT) != 0) {
this.celeritas$transparencyLevel = SpriteTransparencyLevel.TRANSPARENT;
} else {
this.celeritas$transparencyLevel = SpriteTransparencyLevel.OPAQUE;
}
}
@Redirect(
method = "generateMipmaps",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/texture/TextureUtil;generateMipmapData(II[[I)[[I"))
private int[][] celeritas$generateMipmapData(int p_147949_0_, int p_147949_1_, int[][] p_147949_2_) {
final MipmapStrategy strategy = celeritas$resolvedStrategy();
final int border = this.useAnisotropicFiltering ? 8 : 0;
return MipmapGenerator.generateMipLevels(p_147949_0_, p_147949_1_, p_147949_2_, strategy, this.celeritas$hasTransparentTexels, border);
}
@Override
public SpriteTransparencyLevel celeritas$getTransparencyLevel() {
return this.celeritas$transparencyLevel;
}
@Override
public void celeritas$setMipmapStrategy(MipmapStrategy strategy, boolean explicit, int textureType) {
this.celeritas$mipmapStrategy = strategy;
this.celeritas$mipmapStrategyExplicit = explicit;
this.celeritas$textureType = textureType;
}
}
@@ -0,0 +1,24 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.features.mipmaps;
import com.gtnewhorizons.angelica.mixins.interfaces.TextureMetadataExtension;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
import net.minecraft.client.resources.data.TextureMetadataSection;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(TextureMetadataSection.class)
public class MixinTextureMetadataSection implements TextureMetadataExtension {
@Unique
private MipmapStrategy angelica$mipmapStrategy;
@Override
public void angelica$setMipmapStrategy(MipmapStrategy strategy) {
this.angelica$mipmapStrategy = strategy;
}
@Override
public MipmapStrategy angelica$getMipmapStrategy() {
return this.angelica$mipmapStrategy;
}
}
@@ -0,0 +1,42 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.features.mipmaps;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.gtnewhorizons.angelica.mixins.interfaces.TextureMetadataExtension;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
import net.minecraft.client.resources.data.TextureMetadataSection;
import net.minecraft.client.resources.data.TextureMetadataSectionSerializer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Type;
/**
* Reads the modern {@code "mipmap_strategy"} key out of a sprite's {@code texture} mcmeta section.
*/
@Mixin(TextureMetadataSectionSerializer.class)
public class MixinTextureMetadataSectionSerializer {
@Inject(
method = "deserialize(Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/minecraft/client/resources/data/TextureMetadataSection;",
at = @At("RETURN"))
private void angelica$readMipmapStrategy(JsonElement json, Type type, JsonDeserializationContext context,
CallbackInfoReturnable<TextureMetadataSection> cir) {
final TextureMetadataSection section = cir.getReturnValue();
if (section == null || !json.isJsonObject()) {
return;
}
final JsonObject object = json.getAsJsonObject();
if (!object.has("mipmap_strategy")) {
return;
}
final JsonElement value = object.get("mipmap_strategy");
if (!value.isJsonPrimitive()) {
return;
}
((TextureMetadataExtension) section).angelica$setMipmapStrategy(MipmapStrategy.byName(value.getAsString()));
}
}
@@ -1,21 +0,0 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.features.mipmaps;
import net.minecraft.client.renderer.texture.TextureUtil;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import static org.embeddedt.embeddium.impl.texture.MipmapHelper.weightedAverageColor;
@Mixin(TextureUtil.class)
public class MixinTextureUtil {
/**
* @author coderbot
* @reason replace the vanilla blending function with our improved function
*/
@Overwrite
private static int func_147943_a(int one, int two, int three, int four, boolean checkAlpha) {
// First blend horizontally, then blend vertically.
// This works well for the case where our change is the most impactful (grass side overlays)
return weightedAverageColor(weightedAverageColor(one, two), weightedAverageColor(three, four));
}
}
@@ -1,34 +1,166 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.features.textures;
import com.gtnewhorizons.angelica.mixins.interfaces.TextureMetadataExtension;
import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension;
import com.gtnewhorizons.angelica.rendering.celeritas.TextureMapExtension;
import com.gtnewhorizons.angelica.utils.MipmapStrategies;
import com.gtnewhorizons.angelica.utils.MipmapStrategy;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.client.renderer.texture.Stitcher;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.resources.data.TextureMetadataSection;
import net.minecraft.util.IIcon;
import net.minecraft.util.MathHelper;
import org.embeddedt.embeddium.impl.util.collections.quadtree.QuadTree;
import org.embeddedt.embeddium.impl.util.collections.quadtree.Rect2i;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
/** QuadTree for sprite UV lookup. */
/**
* QuadTree for sprite UV lookup, plus per-sprite mipmap strategy assignment.
*/
@Mixin(TextureMap.class)
public class MixinTextureMap implements TextureMapExtension {
@Shadow @Final private Map<String, TextureAtlasSprite> mapUploadedSprites;
@Shadow
@Final
public Map<String, TextureAtlasSprite> mapUploadedSprites;
@Shadow
public int mipmapLevels;
@Shadow
@Final
private int textureType;
@Unique
private QuadTree<TextureAtlasSprite> celeritas$quadTree;
@Unique
private int celeritas$width;
@Unique
private int celeritas$height;
@Unique private QuadTree<TextureAtlasSprite> celeritas$quadTree;
@Unique private int celeritas$width;
@Unique private int celeritas$height;
@Inject(method = "loadTextureAtlas", at = @At("HEAD"))
private void angelica$applyAtlasSettings(CallbackInfo ci) {
if ((Object) this == Minecraft.getMinecraft().getTextureMapBlocks()) {
SodiumGameOptions.applyAtlasSettings();
}
}
/**
* The block atlas re-gathers its icon list on every reload, so the previous attribution is stale.
*/
@Inject(method = "registerIcons", at = @At("HEAD"))
private void angelica$resetMipmapStrategies(CallbackInfo ci) {
if (this.textureType == 0) {
MipmapStrategies.reset();
}
}
/**
* Brackets each block's icon registration so the sprites it registers can be attributed to it.
*/
@WrapOperation(
method = "registerIcons",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/block/Block;registerBlockIcons(Lnet/minecraft/client/renderer/texture/IIconRegister;)V"))
private void angelica$trackBlockIcons(Block block, IIconRegister reg, Operation<Void> original) {
MipmapStrategies.beginBlock(block);
try {
original.call(block, reg);
} finally {
MipmapStrategies.endBlock();
}
}
/**
* Attributes the icon to whichever block is registering it.
*/
@Inject(method = "registerIcon", at = @At("HEAD"))
private void angelica$recordSpriteOwner(String p_94245_1_, CallbackInfoReturnable<IIcon> cir) {
MipmapStrategies.recordSprite(this.textureType, p_94245_1_);
}
@ModifyArg(
method = "loadTextureAtlas",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;loadSprite([Ljava/awt/image/BufferedImage;Lnet/minecraft/client/resources/data/AnimationMetadataSection;Z)V"))
private boolean angelica$forceSpritePadding(boolean useAnisotropicFiltering) {
if ((Object) this != Minecraft.getMinecraft().getTextureMapBlocks()) {
return useAnisotropicFiltering;
}
return useAnisotropicFiltering || SodiumGameOptions.needsForcedSpritePadding();
}
@ModifyExpressionValue(
method = "initMissingImage",
at = @At(
value = "FIELD",
target = "Lnet/minecraft/client/renderer/texture/TextureMap;anisotropicFiltering:I",
opcode = Opcodes.GETFIELD))
private int angelica$forceMissingImagePadding(int anisotropicFiltering) {
if ((Object) this != Minecraft.getMinecraft().getTextureMapBlocks()) {
return anisotropicFiltering;
}
final int forced = SodiumGameOptions.needsForcedSpritePadding() ? 2 : 1;
return Math.max(SodiumGameOptions.resolvedAnisotropicFiltering(), forced);
}
/**
* Assigns the sprite's mipmap strategy.
*/
@Inject(
method = "loadTextureAtlas",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;loadSprite([Ljava/awt/image/BufferedImage;Lnet/minecraft/client/resources/data/AnimationMetadataSection;Z)V"))
private void celeritas$assignMipmapStrategy(CallbackInfo ci, @Local(ordinal = 0) TextureAtlasSprite sprite,
@Local(ordinal = 0) TextureMetadataSection metadata) {
final MipmapStrategy declared = metadata != null
? ((TextureMetadataExtension) metadata).angelica$getMipmapStrategy()
: null;
if (declared != null) {
((SpriteExtension) sprite).celeritas$setMipmapStrategy(declared, true, this.textureType);
} else {
((SpriteExtension) sprite).celeritas$setMipmapStrategy(
MipmapStrategies.inheritedFor(this.textureType, sprite.getIconName()), false, this.textureType);
}
}
@Unique
private int celeritas$unpaddedMipmapCeiling() {
int minDimension = Integer.MAX_VALUE;
for (TextureAtlasSprite sprite : this.mapUploadedSprites.values()) {
final int padding = sprite.useAnisotropicFiltering ? 16 : 0;
minDimension = Math.min(minDimension, Math.min(sprite.getIconWidth(), sprite.getIconHeight()) - padding);
}
return minDimension == Integer.MAX_VALUE ? Integer.MAX_VALUE : MathHelper.calculateLogBaseTwo(minDimension);
}
@Inject(method = "loadTextureAtlas", at = @At("RETURN"))
private void celeritas$generateQuadTree(CallbackInfo ci, @Local(ordinal = 0) Stitcher stitcher) {
if ((Object) this == Minecraft.getMinecraft().getTextureMapBlocks()) {
SodiumGameOptions.recordAtlasMipmapClamp(Minecraft.getMinecraft().gameSettings.mipmapLevels,
Math.min(this.mipmapLevels, celeritas$unpaddedMipmapCeiling()));
}
this.celeritas$width = stitcher.getCurrentWidth();
this.celeritas$height = stitcher.getCurrentHeight();
@@ -17,6 +17,8 @@ public abstract class MixinEntityRenderer {
float required = SettingsManager.minimumFarPlaneDistance;
if (AngelicaConfig.enableVBOClouds) {
required = Math.max(required, CloudRenderer.getCloudRenderer().getRequiredFarPlaneDistance());
} else {
required += SettingsManager.currentCloudHeight();
}
return Math.max(original, required);
}
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff. Show more