Initial commit for gui-item-supersampling (#2299)

This commit is contained in:
Eclipse-Sol authored and GitHub committed 2026-10-25 13:10:15 -07:00
1 parent 34ed1d101b
commit 17a2ed7f37
22 files changed
+562 -4

No files matched your search

@@ -275,6 +275,7 @@ public class GLContextState {
public float savedFilterAniso = -1.0f;
public boolean lineQuadsActive = false;
public boolean lineStippleActive = false;
public int textureSupersamplingSamples = 1;
public Instancing ffpInstancing = Instancing.NONE;
public float lastBrightnessX;
public float lastBrightnessY;
@@ -653,6 +653,28 @@ public class GLStateManager {
ctx().ffpInstancing = mode;
}
public static int setTextureSupersamplingSamples(int samples) {
if (samples < 1 || samples > 16 || (samples & (samples - 1)) != 0) {
throw new IllegalArgumentException("Texture supersampling requires 1, 2, 4, 8, or 16 samples");
}
final GLContextState glCtx = ctx();
final int previous = glCtx.textureSupersamplingSamples;
if (previous != samples) {
beforeUncapturedStateChange();
glCtx.textureSupersamplingSamples = samples;
}
return previous;
}
public static void markTextureAsAtlas(int texture, int gutter) {
if (texture <= 0) return;
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(texture);
if (info.getAtlasGutter() != gutter) {
beforeUncapturedStateChange();
info.setAtlasGutter(gutter);
}
}
private static final MethodHandle MAT4_STACK_CURR_DEPTH;
static {
@@ -104,6 +104,7 @@ public final class FFPUniformBlock {
public static final int TEX_ENV_COLOR_3 = vec4("u_TexEnvColor3");
public static final int OVERLAY_COLOR = vec4("u_OverlayColor");
public static final int SECONDARY_COLOR = vec3("u_SecondaryColor");
public static final int ATLAS_GUTTER = flt("u_AtlasGutter");
public static final int FOG_PARAMS = vec4("u_FogParams");
public static final int FOG_COLOR = vec4("u_FogColor");
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
@@ -82,7 +83,7 @@ public final class FragmentKey {
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_TEXTURE_SUPERSAMPLING = 7; // 3 bits, log2(samples)
private static final int BIT_OVERLAY_ENABLED = 10; // 1 bit
private static final int BIT_LINE_STIPPLE = 11; // 1 bit
private static final int BIT_COLOR_SUM = 12; // 1 bit
@@ -172,7 +173,10 @@ public final class FragmentKey {
}
final int nrEnabled = highestEnabled + 1;
global |= ((long) (nrEnabled & 0x7)) << BIT_NR_ENABLED_UNITS;
if (glCtx.textureSupersamplingSamples > 1 && (unit0Bits & 1L) != 0
&& TextureInfoCache.INSTANCE.getInfo(textures.getTextureUnitBindings(0).getBinding()).getAtlasGutter() > 0) {
global |= (long) Integer.numberOfTrailingZeros(glCtx.textureSupersamplingSamples) << BIT_TEXTURE_SUPERSAMPLING;
}
global |= (unit0Bits << GLOBAL_BITS);
out[0] = global;
@@ -256,12 +260,13 @@ public final class FragmentKey {
public int fogMode() { return (int) (packed[0] & 0x3); }
public boolean combinedGlint() { return (packed[0] & (1L << BIT_COMBINED_GLINT)) != 0; }
public boolean glintReplaceAlpha() { return (packed[0] & (1L << BIT_GLINT_REPLACE_ALPHA)) != 0; }
public int textureSupersamplingSamples() { return 1 << (int) ((packed[0] >> BIT_TEXTURE_SUPERSAMPLING) & 0x7); }
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 colorSum() { return ((packed[0] >> BIT_COLOR_SUM) & 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 int nrEnabledUnits() { return packed.length == 1 && !unitEnabled(0) ? 0 : packed.length; }
public boolean overlayEnabled() { return ((packed[0] >> BIT_OVERLAY_ENABLED) & 1) != 0; }
public boolean overlayInstanced() { return ((packed[0] >> BIT_OVERLAY_INSTANCED) & 1) != 0; }
@@ -400,6 +405,7 @@ public final class FragmentKey {
sb.append(String.format("FFPFragmentKey[fog=%s alpha=%b(%s) specSep=%b colorSum=%b overlay=%b units=%d", fogName, alphaTestEnabled(),
alphaTestEnabled() ? String.format("0x%04X", decodeAlphaFunc(alphaTestFunc())) : "-", separateSpecular(), colorSum(),
overlayEnabled() || overlayInstanced(), nrEnabledUnits()));
if (textureSupersamplingSamples() > 1) sb.append(" samples=").append(textureSupersamplingSamples());
for (int i = 0; i < nrEnabledUnits(); i++) {
if (!unitEnabled(i)) {
sb.append(String.format(" u%d=OFF", i));
@@ -28,6 +28,10 @@ public final class FragmentShaderGenerator {
return sb.toString();
}
sb.append("out vec4 fragColor;\n\n");
if (key.textureSupersamplingSamples() > 1) {
sb.append("#define TEXTURE_SUPERSAMPLING_SAMPLES ").append(key.textureSupersamplingSamples()).append('\n');
sb.append(ShaderLoader.getShaderSource("angelica:texture_supersampling.glsl"));
}
sb.append("void main() {\n");
emitLineStipple(sb, key);
@@ -86,7 +90,8 @@ public final class FragmentShaderGenerator {
private static void emitTextureSampling(StringBuilder sb, FragmentKey key) {
for (int i = 0; i < key.nrEnabledUnits(); i++) {
if (!key.unitEnabled(i)) continue;
sb.append(" vec4 tex").append(i).append("Color = textureProj(u_Sampler").append(i).append(", v_TexCoord").append(i).append(");\n");
final String sample = i == 0 && key.textureSupersamplingSamples() > 1 ? "sampleSupersampledTexture" : "textureProj";
sb.append(" vec4 tex").append(i).append("Color = ").append(sample).append("(u_Sampler").append(i).append(", v_TexCoord").append(i).append(");\n");
}
}
@@ -190,6 +190,10 @@ public final class ShaderManager {
}
}
boolean isTextureSupersampling() {
return currentProgram != null && currentProgram.getFragmentKey().textureSupersamplingSamples() > 1;
}
public static void endFrame() {
final ShaderManager sm = GLStateManager.ctx().ffp;
sm.lastFramePreDrawCalls = sm.preDrawCalls;
@@ -11,6 +11,7 @@ import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import com.gtnewhorizons.angelica.glsm.states.ViewportState;
import com.gtnewhorizons.angelica.glsm.streaming.UniformRingBuffer;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.joml.Math;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
@@ -49,6 +50,7 @@ public class Uniforms {
private int viewportX = Integer.MIN_VALUE, viewportY = Integer.MIN_VALUE, viewportWidth = -1, viewportHeight = -1;
private int lineStipple = -1;
private int weatherGen = -1;
private int atlasGutter = -1;
int blockWrites;
int blockSkips;
@@ -170,6 +172,15 @@ public class Uniforms {
stagedFragment++;
dirty = true;
}
if (glCtx.textureSupersamplingSamples > 1 && ffp.isTextureSupersampling()) {
final int gutter = TextureInfoCache.INSTANCE.getInfo(glCtx.textures.getTextureUnitBindings(0).getBinding()).getAtlasGutter();
if (gutter != atlasGutter) {
putFloat(FFPUniformBlock.ATLAS_GUTTER, gutter);
atlasGutter = gutter;
stagedMisc++;
dirty = true;
}
}
final LineState line = glCtx.lineState;
final float lw = line.getWidth();
if (lw != lineWidth) {
@@ -21,6 +21,7 @@ public class TextureInfo {
protected int resolvedInternalFormat = -1;
protected int width = -1;
protected int height = -1;
@Getter @Setter private int atlasGutter;
protected int minFilter = UNSET_INT;
protected int magFilter = UNSET_INT;
@@ -0,0 +1,58 @@
const vec2 SAMPLE_OFFSETS[TEXTURE_SUPERSAMPLING_SAMPLES] = vec2[](
#if TEXTURE_SUPERSAMPLING_SAMPLES == 2
vec2(0.25, 0.25), vec2(-0.25, -0.25)
#elif TEXTURE_SUPERSAMPLING_SAMPLES == 4
vec2(0.125, 0.375), vec2(-0.125, -0.375),
vec2(0.375, -0.125), vec2(-0.375, 0.125)
#elif TEXTURE_SUPERSAMPLING_SAMPLES == 8
vec2(-0.1875, -0.0625), vec2( 0.3125, 0.0625),
vec2(-0.0625, 0.1875), vec2(-0.3125, -0.1875),
vec2( 0.1875, -0.3125), vec2( 0.4375, -0.4375),
vec2(-0.4375, 0.3125), vec2( 0.0625, 0.4375)
#elif TEXTURE_SUPERSAMPLING_SAMPLES == 16
vec2( 0.0625, 0.0625), vec2(-0.0625, -0.1875),
vec2(-0.1875, 0.1250), vec2( 0.2500, -0.0625),
vec2(-0.3125, -0.1250), vec2( 0.1250, 0.3125),
vec2( 0.3125, 0.1875), vec2( 0.1875, -0.3125),
vec2(-0.1250, 0.3750), vec2( 0.0000, -0.4375),
vec2(-0.2500, -0.3750), vec2(-0.3750, 0.2500),
vec2(-0.5000, 0.0000), vec2( 0.4375, -0.2500),
vec2( 0.3750, 0.4375), vec2(-0.4375, -0.5000)
#endif
);
vec4 sampleSupersampledTexture(sampler2D tex, vec4 coord) {
vec2 uv = coord.xy / coord.w;
vec2 dx = dFdx(uv);
vec2 dy = dFdy(uv);
ivec2 size = textureSize(tex, 0);
vec2 offsets[TEXTURE_SUPERSAMPLING_SAMPLES];
vec2 extent = vec2(0.0);
for (int i = 0; i < TEXTURE_SUPERSAMPLING_SAMPLES; i++) {
offsets[i] = SAMPLE_OFFSETS[i].x * dx + SAMPLE_OFFSETS[i].y * dy;
extent = max(extent, abs(offsets[i]));
}
extent *= vec2(size);
float footprint = max(extent.x, extent.y);
if (footprint >= u_AtlasGutter) return textureGrad(tex, uv, dx, dy);
float supersampling = 1.0 - smoothstep(0.75 * u_AtlasGutter, u_AtlasGutter, footprint);
// The atlas gutter is 2^maxMipLevel.
float texelsPerPixel = max(length(dx * vec2(size)), length(dy * vec2(size)));
int lod = int(min(ceil(log2(max(texelsPerPixel / sqrt(float(TEXTURE_SUPERSAMPLING_SAMPLES)), 1.0))),
log2(u_AtlasGutter)));
// Transparent texels must not tint the visible samples.
vec4 color = vec4(0.0);
for (int i = 0; i < TEXTURE_SUPERSAMPLING_SAMPLES; i++) {
ivec2 texel = min(ivec2(fract(uv + offsets[i]) * vec2(size)), size - 1);
vec4 sampleColor = texelFetch(tex, texel >> lod, lod);
color += vec4(sampleColor.rgb * sampleColor.a, sampleColor.a);
}
color /= float(TEXTURE_SUPERSAMPLING_SAMPLES);
if (supersampling < 1.0) {
vec4 filtered = textureGrad(tex, uv, dx, dy);
color = mix(vec4(filtered.rgb * filtered.a, filtered.a), color, supersampling);
}
return vec4(color.rgb / max(color.a, 0.00001), color.a);
}
@@ -0,0 +1,302 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.function.IntBinaryOperator;
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.TEXTURE_BIT;
import static org.joml.Math.clamp;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
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
class TextureSupersamplingGLTest {
private static final int SIZE = 200;
private int texture;
private int vao;
private int vbo;
private static int dotColor(int x, int y) {
final boolean dot = (x == 3 || x == 6 || x == 9 || x == 12)
&& (y == 3 || y == 6 || y == 9 || y == 12)
&& (x == 3 || x == 12 || y == 3 || y == 12);
return dot ? 0x444444FF : 0xFFFFFFFF;
}
private static int uploadTexture(int spriteSize, int gutter, IntBinaryOperator color) {
final int atlasSize = Integer.highestOneBit(spriteSize + 2 * gutter) << 1;
final ByteBuffer pixels = BufferUtils.createByteBuffer(atlasSize * atlasSize * 4);
for (int y = 0; y < atlasSize; y++) {
for (int x = 0; x < atlasSize; x++) {
final int rgba = x < spriteSize + 2 * gutter && y < spriteSize + 2 * gutter
? color.applyAsInt(clamp(0, spriteSize - 1, x - gutter), clamp(0, spriteSize - 1, y - gutter))
: 0xFF00FFFF;
pixels.put((byte) (rgba >>> 24)).put((byte) (rgba >>> 16)).put((byte) (rgba >>> 8)).put((byte) rgba);
}
}
pixels.flip();
final int texture = GLStateManager.glGenTextures();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, atlasSize, atlasSize, 0,
GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
return texture;
}
private static void vertex(ByteBuffer vertices, float x, float z, float u, float v) {
vertices.putFloat(x).putFloat(0.5f).putFloat(z).putFloat(u).putFloat(v);
}
private static int countDotRegions(int[] pixels) {
final boolean[] remaining = new boolean[pixels.length];
for (int i = 0; i < pixels.length; i++) {
final int gray = pixels[i] & 255;
remaining[i] = gray > 0 && gray < 250
&& ((pixels[i] >> 8) & 255) == gray && ((pixels[i] >> 16) & 255) == gray;
}
int regions = 0;
final ArrayDeque<Integer> pending = new ArrayDeque<>();
for (int i = 0; i < remaining.length; i++) {
if (!remaining[i]) continue;
regions++;
remaining[i] = false;
pending.add(i);
while (!pending.isEmpty()) {
final int pixel = pending.removeFirst();
for (int neighbor : new int[]{pixel - 1, pixel + 1, pixel - SIZE, pixel + SIZE}) {
if (neighbor >= 0 && neighbor < remaining.length && remaining[neighbor]) {
remaining[neighbor] = false;
pending.add(neighbor);
}
}
}
}
return regions;
}
@BeforeEach
void setup() {
FfpFixture.resetFfpState();
GLStateManager.setTextureSupersamplingSamples(1);
GLStateManager.glDisable(GL11.GL_LIGHTING);
GLStateManager.glDisable(GL11.GL_FOG);
GLStateManager.glColor4f(1, 1, 1, 1);
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
GLStateManager.glViewport(0, 0, SIZE, SIZE);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glLoadIdentity();
GLStateManager.glOrtho(0, SIZE, SIZE, 0, -100, 100);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
FfpFixture.activateFfp();
}
@AfterEach
void cleanup() {
GLStateManager.setTextureSupersamplingSamples(1);
FfpFixture.resetFfpState();
if (texture != 0) GLStateManager.glDeleteTextures(texture);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
if (vbo != 0) GLStateManager.glDeleteBuffers(vbo);
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glLoadIdentity();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glLoadIdentity();
GLStateManager.glViewport(0, 0, 800, 600);
}
@ParameterizedTest
@ValueSource(ints = {4, 16})
void recoversTwelveDotsAtScaleThreeAndRestoresNearestAfterward(int samples) {
createDotAtlas();
final int[] nearest = drawTop(3);
assertEquals(0, countDotRegions(nearest));
final int previous = GLStateManager.setTextureSupersamplingSamples(samples);
assertEquals(12, countDotRegions(drawTop(3)));
GLStateManager.setTextureSupersamplingSamples(previous);
assertArrayEquals(nearest, drawTop(3));
}
@ParameterizedTest
@ValueSource(ints = {4, 16})
void highResolutionCheckerboardsStaySmoothWithTheDefaultAtlasGutter(int samples) {
createAtlas(256, 16, (x, y) -> ((x ^ y) & 1) == 0 ? 0xFFFFFFFF : 0x000000FF);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 4);
GLStateManager.glGenerateMipmap(GL11.GL_TEXTURE_2D);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_LINEAR);
final int[] filtered = drawFlat();
GLStateManager.setTextureSupersamplingSamples(samples);
final int[] supersampled = drawFlat();
for (int y = 97; y <= 102; y++) {
for (int x = 97; x <= 102; x++) {
final int index = y * SIZE + x;
assertEquals(128, filtered[index] & 255, 1, "The mip-filtered checkerboard must average to gray");
assertEquals(filtered[index] & 255, supersampled[index] & 255, 1,
"Supersampling must preserve the mip average instead of aliasing the base-level checkerboard");
}
}
}
@ParameterizedTest
@ValueSource(ints = {4, 16})
void transparentTexelsDoNotTintPartialCoverage(int samples) {
createAtlas(16, (x, y) -> x < 8 ? 0xFF0000FF : 0x0000FF00);
GLStateManager.glEnable(GL11.GL_BLEND);
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.setTextureSupersamplingSamples(samples);
final int[] pixels = drawTop(3);
int partialPixels = 0;
for (int pixel : pixels) {
final int red = pixel & 255;
if (red > 0 && red < 255) {
partialPixels++;
assertEquals(0, (pixel >> 8) & 255);
assertEquals(0, (pixel >> 16) & 255, "Transparent blue texels must not tint the red edge");
}
}
assertTrue(partialPixels > 0, "The test must cover a partially transparent pixel");
}
@Test
void modelTexturesKeepTheirOwnFilteringWhileSupersamplingIsActive() {
createDotAtlas();
final int modelTexture = uploadTexture(16, 1, TextureSupersamplingGLTest::dotColor);
try {
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
final int[] normal = drawTop(3);
GLStateManager.setTextureSupersamplingSamples(16);
assertArrayEquals(normal, drawTop(3));
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
assertFalse(Arrays.equals(normal, drawTop(3)));
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, modelTexture);
assertArrayEquals(normal, drawTop(3));
} finally {
GLStateManager.glDeleteTextures(modelTexture);
}
}
@Test
void atlasFetchesIgnoreSamplerFilteringAndRepeatAcrossNegativeUvs() {
createAtlas(16, 4, TextureSupersamplingGLTest::dotColor);
GLStateManager.setTextureSupersamplingSamples(16);
final int[] nearest = drawTop(3);
final int sampler = RenderSystem.genSampler();
try {
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
RenderSystem.samplerParameteri(sampler, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
RenderSystem.bindSamplerToUnit(0, sampler);
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glTranslatef(-2, 3, 0);
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
assertArrayEquals(nearest, drawTop(3));
} finally {
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glLoadIdentity();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
RenderSystem.bindSamplerToUnit(0, 0);
RenderSystem.destroySampler(sampler);
}
}
@Test
void changingTheAtlasGutterSwitchesBetweenMipFilteringAndFullFootprintSampling() {
createAtlas(32, 4, (x, y) -> 0xFF0000FF);
final int[] baseLevel = drawFlat();
for (int level = 1; level <= 6; level++) {
final int size = 64 >> level;
final ByteBuffer pixels = BufferUtils.createByteBuffer(size * size * 4);
for (int i = 0; i < size * size; i++) pixels.putInt(0xFFFF0000);
pixels.flip();
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA8, size, size, 0,
GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
}
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 6);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_NEAREST);
final int[] mipFiltered = drawFlat();
assertFalse(Arrays.equals(baseLevel, mipFiltered));
GLStateManager.setTextureSupersamplingSamples(16);
assertArrayEquals(baseLevel, drawFlat());
GLStateManager.markTextureAsAtlas(texture, 1);
assertArrayEquals(mipFiltered, drawFlat());
GLStateManager.markTextureAsAtlas(texture, 4);
assertArrayEquals(baseLevel, drawFlat());
}
private int[] drawFlat() {
FfpFixture.clear();
GLStateManager.glLoadIdentity();
GLStateManager.glTranslatef(100.25f, 100.125f, 0);
GLStateManager.glScalef(11, 11, 1);
GLStateManager.glRotatef(90, 1, 0, 0);
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, 4);
return FfpFixture.readRegion(SIZE);
}
private void createDotAtlas() {
createAtlas(16, TextureSupersamplingGLTest::dotColor);
}
private void createAtlas(int spriteSize, IntBinaryOperator color) {
createAtlas(spriteSize, 1, color);
}
private void createAtlas(int spriteSize, int gutter, IntBinaryOperator color) {
texture = uploadTexture(spriteSize, gutter, color);
GLStateManager.markTextureAsAtlas(texture, gutter);
final int atlasSize = Integer.highestOneBit(spriteSize + 2 * gutter) << 1;
final float minUv = (gutter + 0.01f) / atlasSize;
final float maxUv = (spriteSize + gutter - 0.01f) / atlasSize;
final ByteBuffer vertices = BufferUtils.createByteBuffer(4 * 20);
vertex(vertices, -0.5f, -0.5f, minUv, minUv);
vertex(vertices, 0.5f, -0.5f, maxUv, minUv);
vertex(vertices, 0.5f, 0.5f, maxUv, maxUv);
vertex(vertices, -0.5f, 0.5f, minUv, maxUv);
vertices.flip();
vao = GLStateManager.glGenVertexArrays();
GLStateManager.glBindVertexArray(vao);
vbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, vertices, GL15.GL_STATIC_DRAW);
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, 20, 0);
FfpFixture.attrib(2, 2, GL11.GL_FLOAT, false, 20, 12);
VAOManager.setCurrentVertexFlags(TEXTURE_BIT);
}
private int[] drawTop(int scale) {
FfpFixture.clear();
GLStateManager.glLoadIdentity();
GLStateManager.glTranslatef(100 + 8 * scale, 100 + 8 * scale, 0);
GLStateManager.glScalef(10 * scale, 10 * scale, -10 * scale);
GLStateManager.glRotatef(210, 1, 0, 0);
GLStateManager.glRotatef(45, 0, 1, 0);
GLStateManager.glDrawArrays(GL11.GL_QUADS, 0, 4);
return FfpFixture.readRegion(SIZE);
}
}
@@ -68,6 +68,10 @@ public enum Mixins implements IMixins {
, "angelica.optimizations.MixinRenderBiped_ArmorResource"
, "angelica.optimizations.MixinRendererLivingEntity"
, "angelica.rendering.MixinRenderGlobal_SelectionBox"
, "angelica.textures.MixinForgeHooksClient_GuiSupersampling"
, "angelica.textures.MixinRenderBlocks_GuiSupersampling"
, "angelica.textures.MixinRenderItem_GuiSupersampling"
, "angelica.textures.MixinTileEntityRendererDispatcher_GuiSupersampling"
, "angelica.gui.MixinGuiIngameForge_ModernF3"
)
),
@@ -0,0 +1,18 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
public final class GuiBlockSupersampling {
private GuiBlockSupersampling() {}
public static int begin() {
if (!RenderingState.INSTANCE.isRenderingGuiItem() || RenderingState.INSTANCE.isRenderingWorld()) return 0;
final int samples = ClientProxy.options().quality.guiBlockSupersampling;
return samples > 1 ? GLStateManager.setTextureSupersamplingSamples(samples) : 0;
}
public static void end(int previous) {
if (previous != 0) GLStateManager.setTextureSupersamplingSamples(previous);
}
}
@@ -23,6 +23,12 @@ public class RenderingState {
@Getter
@Setter
private float fov;
@Getter
@Setter
private boolean renderingWorld;
@Getter
@Setter
private boolean renderingGuiItem;
public void setCameraPosition(double x, double y, double z) {
@@ -286,6 +286,15 @@ public class SodiumGameOptionPages {
.add(texelSampling)
.add(anisotropicFilteringSlider(vanillaOpts, textureFilterMode::getValue),
SodiumGameOptions.anisotropySupported())
.add(OptionImpl.createBuilder(int.class, sodiumOpts)
.setName(I18n.format("sodium.options.gui_block_supersampling.name"))
.setTooltip(I18n.format("sodium.options.gui_block_supersampling.tooltip"))
.setControl(option -> new SliderControl(option, 0, 4, 1,
value -> value == 0 ? I18n.format("options.off") : (1 << value) + "x"))
.setBinding((opts, value) -> opts.quality.guiBlockSupersampling = 1 << value,
opts -> Integer.numberOfTrailingZeros(opts.quality.guiBlockSupersampling))
.setImpact(OptionImpact.LOW)
.build())
.add(biomeBlendRadius)
.add(biomeBlendMode)
// TODO
@@ -151,6 +151,7 @@ public class SodiumGameOptions {
if (config.quality.texelSampling == null) {
config.quality.texelSampling = TexelSampling.LINEAR;
}
config.quality.guiBlockSupersampling = Integer.highestOneBit(MathHelper.clamp_int(config.quality.guiBlockSupersampling, 1, 16));
if (GLStateManager.capabilities != null) {
if (!GLStateManager.capabilities.OpenGL32) {
@@ -250,6 +251,7 @@ public class SodiumGameOptions {
public boolean useCeleritasSmoothLighting = true;
public TextureFilterMode textureFilterMode = TextureFilterMode.RGSS_ANISOTROPIC;
public TexelSampling texelSampling = TexelSampling.LINEAR;
public int guiBlockSupersampling = 1;
}
public static class ReducerSettings {
@@ -53,6 +53,8 @@ sodium.options.texel_sampling.linear=Linear
sodium.options.texel_sampling.nearest=Nearest
sodium.options.anisotropic_filtering.name=Anisotropic Filtering
sodium.options.anisotropic_filtering.tooltip=Sharpens textures viewed at steep angles.
sodium.options.gui_block_supersampling.name=GUI Block Supersampling
sodium.options.gui_block_supersampling.tooltip=Sets the number of texture samples per pixel for 3D block icons within GUIs.
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.
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.mixins.early.angelica;
import com.gtnewhorizons.angelica.compat.mojang.Camera;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor;
import com.gtnewhorizons.angelica.rendering.RenderingState;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
@@ -33,6 +34,17 @@ public abstract class MixinEntityRenderer implements EntityRendererAccessor {
@Accessor("rendererUpdateCount")
public abstract int getRendererUpdateCount();
@Surround(method = "renderWorld(FJ)V")
private void angelica$beginWorldRendering() {
@Surround.Carry final boolean renderingWorld = RenderingState.INSTANCE.isRenderingWorld();
RenderingState.INSTANCE.setRenderingWorld(true);
}
@Surround.Finally
private void angelica$endWorldRendering(@Surround.Carry boolean renderingWorld) {
RenderingState.INSTANCE.setRenderingWorld(renderingWorld);
}
@Inject(method = "renderWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/ActiveRenderInfo;updateRenderInfo(Lnet/minecraft/entity/player/EntityPlayer;Z)V", shift = At.Shift.AFTER))
private void angelica$captureCameraMatrix(float partialTicks, long finishTimeNano, CallbackInfo ci) {
final Minecraft mc = Minecraft.getMinecraft();
@@ -0,0 +1,21 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.rendering.GuiBlockSupersampling;
import net.minecraftforge.client.ForgeHooksClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(value = ForgeHooksClient.class, remap = false)
public class MixinForgeHooksClient_GuiSupersampling {
@Surround(method = "renderInventoryItem", at = @At(value = "INVOKE", ordinal = 0,
target = "Lnet/minecraftforge/client/IItemRenderer;renderItem(Lnet/minecraftforge/client/IItemRenderer$ItemRenderType;Lnet/minecraft/item/ItemStack;[Ljava/lang/Object;)V"))
private static void angelica$beginGuiBlock() {
@Surround.Carry final int previous = GuiBlockSupersampling.begin();
}
@Surround.Finally
private static void angelica$endGuiBlock(@Surround.Carry int previous) {
GuiBlockSupersampling.end(previous);
}
}
@@ -0,0 +1,19 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.rendering.GuiBlockSupersampling;
import net.minecraft.client.renderer.RenderBlocks;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(RenderBlocks.class)
public class MixinRenderBlocks_GuiSupersampling {
@Surround(method = "renderBlockAsItem(Lnet/minecraft/block/Block;IF)V")
private void angelica$beginGuiBlock() {
@Surround.Carry final int previous = GuiBlockSupersampling.begin();
}
@Surround.Finally
private void angelica$endGuiBlock(@Surround.Carry int previous) {
GuiBlockSupersampling.end(previous);
}
}
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.rendering.RenderingState;
import net.minecraft.client.renderer.entity.RenderItem;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(RenderItem.class)
public class MixinRenderItem_GuiSupersampling {
@Surround(id = "withEffects", method = "renderItemAndEffectIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;II)V")
private void angelica$beginGuiEffects() {
@Surround.Carry final boolean previous = RenderingState.INSTANCE.isRenderingGuiItem();
RenderingState.INSTANCE.setRenderingGuiItem(true);
}
@Surround.Finally("withEffects")
private void angelica$endGuiEffects(@Surround.Carry boolean previous) {
RenderingState.INSTANCE.setRenderingGuiItem(previous);
}
@Surround(id = "base", method = "renderItemIntoGUI(Lnet/minecraft/client/gui/FontRenderer;Lnet/minecraft/client/renderer/texture/TextureManager;Lnet/minecraft/item/ItemStack;IIZ)V", remap = false)
private void angelica$beginGuiBase() {
@Surround.Carry final boolean previous = RenderingState.INSTANCE.isRenderingGuiItem();
RenderingState.INSTANCE.setRenderingGuiItem(true);
}
@Surround.Finally("base")
private void angelica$endGuiBase(@Surround.Carry boolean previous) {
RenderingState.INSTANCE.setRenderingGuiItem(previous);
}
}
@@ -0,0 +1,19 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.textures;
import com.gtnewhorizons.angelica.experimental.surround.Surround;
import com.gtnewhorizons.angelica.rendering.GuiBlockSupersampling;
import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(TileEntityRendererDispatcher.class)
public class MixinTileEntityRendererDispatcher_GuiSupersampling {
@Surround(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V")
private void angelica$beginGuiTileEntity() {
@Surround.Carry final int previous = GuiBlockSupersampling.begin();
}
@Surround.Finally
private void angelica$endGuiTileEntity(@Surround.Carry int previous) {
GuiBlockSupersampling.end(previous);
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.features.textures;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.TextureMetadataExtension;
import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension;
import com.gtnewhorizons.angelica.rendering.celeritas.TextureMapExtension;
@@ -178,6 +179,8 @@ public class MixinTextureMap implements TextureMapExtension {
@Inject(method = "loadTextureAtlas", at = @At("RETURN"))
private void celeritas$generateQuadTree(CallbackInfo ci, @Local(ordinal = 0) Stitcher stitcher) {
if ((Object) this == Minecraft.getMinecraft().getTextureMapBlocks()) {
GLStateManager.markTextureAsAtlas(((TextureMap) (Object) this).getGlTextureId(),
((SpriteExtension) this.missingImage).angelica$getGutterWidth());
SodiumGameOptions.recordAtlasMipmapClamp(Minecraft.getMinecraft().gameSettings.mipmapLevels,
this.mipmapLevels);
SodiumGameOptions.recordAtlasMipmapLevels(this.mipmapLevels);