Instance animated TESRs and let mods register shaders for batched TESRs (#2275)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-18 23:59:55 -07:00
1 parent 32f8eef15b
commit b446968e66
60 files changed
+3916 -1635

No files matched your search

+1 -1
View File
@@ -64,7 +64,7 @@ DependencyHandlerScope.of(dependencies).apply {
runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.63:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:LogisticsPipes:1.5.32-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.55-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.64-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Mobs-Info:0.5.18-GTNH:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:IguanaTweaksTConstruct:2.7.10:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenBlocks:1.12.18-GTNH:dev")
@@ -1,5 +1,7 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey;
import com.gtnewhorizons.angelica.glsm.ffp.Uniforms;
import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack;
import com.gtnewhorizons.angelica.glsm.stacks.LightStateStack;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
@@ -66,7 +68,13 @@ public class CompatUniformManager {
static final int LOC_IRIS_MODELVIEW_INVERSE = LOC_MAT_BASE + MAT_FIELDS; // 48
static final int LOC_IRIS_PROJECTION_INVERSE = LOC_IRIS_MODELVIEW_INVERSE + 1; // 49
static final int LOC_COUNT = LOC_IRIS_PROJECTION_INVERSE + 1; // 50
static final int LOC_FOG_MODE = LOC_IRIS_PROJECTION_INVERSE + 1; // 50
static final int LOC_FOG_PARAMS = LOC_FOG_MODE + 1; // 51
static final int LOC_FOG_DISTANCE_MODE = LOC_FOG_PARAMS + 1; // 52
static final int LOC_MODELVIEW_PROJECTION = LOC_FOG_DISTANCE_MODE + 1; // 53
static final int LOC_COUNT = LOC_MODELVIEW_PROJECTION + 1; // 54
private static final String[] LIGHT_FIELD_NAMES = {
"ambient", "diffuse", "specular", "position", "halfVector",
@@ -84,6 +92,7 @@ public class CompatUniformManager {
UNIFORM_NAMES[LOC_MODELVIEW] = "angelica_ModelViewMatrix";
UNIFORM_NAMES[LOC_MODELVIEW_INVERSE] = "angelica_ModelViewMatrixInverse";
UNIFORM_NAMES[LOC_PROJECTION] = "angelica_ProjectionMatrix";
UNIFORM_NAMES[LOC_MODELVIEW_PROJECTION] = "angelica_ModelViewProjectionMatrix";
UNIFORM_NAMES[LOC_PROJECTION_INVERSE] = "angelica_ProjectionMatrixInverse";
UNIFORM_NAMES[LOC_NORMAL] = "angelica_NormalMatrix";
UNIFORM_NAMES[LOC_LIGHTMAP_TEXTURE_MATRIX] = "angelica_LightmapTextureMatrix";
@@ -98,6 +107,9 @@ public class CompatUniformManager {
UNIFORM_NAMES[LOC_IRIS_TEXTURE_MATRIX] = "iris_TextureMatrix";
UNIFORM_NAMES[LOC_IRIS_MODELVIEW_INVERSE] = "iris_ModelViewMatrixInverse";
UNIFORM_NAMES[LOC_IRIS_PROJECTION_INVERSE] = "iris_ProjectionMatrixInverse";
UNIFORM_NAMES[LOC_FOG_MODE] = "angelica_FogMode";
UNIFORM_NAMES[LOC_FOG_PARAMS] = "angelica_FogParams";
UNIFORM_NAMES[LOC_FOG_DISTANCE_MODE] = "angelica_FogDistanceMode";
UNIFORM_NAMES[LOC_ALPHA_TEST_REF] = "angelica_currentAlphaTest";
UNIFORM_NAMES[LOC_SCENE_COLOR] = "angelica_SceneColor";
UNIFORM_NAMES[LOC_CLIP_PLANES] = "angelica_ClipPlane[0]";
@@ -120,7 +132,14 @@ public class CompatUniformManager {
GLContextState owner;
int lastMvGen = -1, lastMvLinearGen = -1, lastProjGen = -1, lastTexMatGen = -1;
int lastFragmentGen = -1, lastLightingGen = -1, lastClipPlaneGen = -1;
ProgramUniforms(int[] locs) { this.locs = locs; }
int lastFogMode = -1, lastFogDistanceMode = -1;
final boolean lights;
ProgramUniforms(int[] locs) {
this.locs = locs;
boolean any = false;
for (int i = LOC_LIGHT_BASE; i < LOC_MAT_BASE; i++) any |= locs[i] != -1;
this.lights = any;
}
}
// Reusable NIO buffers for upload
@@ -186,6 +205,7 @@ public class CompatUniformManager {
pu.owner = glCtx;
pu.lastMvGen = pu.lastMvLinearGen = pu.lastProjGen = pu.lastTexMatGen = -1;
pu.lastFragmentGen = pu.lastLightingGen = pu.lastClipPlaneGen = -1;
pu.lastFogMode = pu.lastFogDistanceMode = -1;
}
// Matrix uniforms — skip if this program's storage already holds the current generation
@@ -212,12 +232,15 @@ public class CompatUniformManager {
uploadFragmentUniforms(locs);
}
pu.lastFogMode = uploadIfChanged(locs[LOC_FOG_MODE], FragmentKey.activeFogMode(glCtx), pu.lastFogMode);
pu.lastFogDistanceMode = uploadIfChanged(locs[LOC_FOG_DISTANCE_MODE], glCtx.fogState.getFogDistanceMode(), pu.lastFogDistanceMode);
// Lighting-derived uniforms (scene color, light sources, material)
final int litGen = glCtx.lightingGeneration;
if (litGen != pu.lastLightingGen) {
pu.lastLightingGen = litGen;
if (locs[LOC_SCENE_COLOR] != -1) uploadSceneColor(locs);
uploadLightSources(locs);
if (pu.lights) uploadLightSources(locs);
uploadMaterial(locs);
}
@@ -262,6 +285,11 @@ public class CompatUniformManager {
}
}
if ((mvChanged || projChanged) && locs[LOC_MODELVIEW_PROJECTION] != -1) {
Uniforms.modelViewProjection(proj, mv, scratchMatrix).get(mat4Buf);
RENDER_BACKEND.uniformMatrix4(locs[LOC_MODELVIEW_PROJECTION], false, mat4Buf);
}
if (mvLinearChanged) {
// Normal Matrix (inverse-transpose of upper-left 3x3 of ModelView)
if (locs[LOC_NORMAL] != -1 || locs[LOC_IRIS_NORMAL] != -1) {
@@ -324,6 +352,11 @@ public class CompatUniformManager {
}
}
private static int uploadIfChanged(int location, int value, int last) {
if (location != -1 && value != last) RENDER_BACKEND.uniform1i(location, value);
return value;
}
private static void uploadFragmentUniforms(int[] locs) {
// Fog uniforms
FogStateStack fog = GLStateManager.getFogState();
@@ -336,6 +369,9 @@ public class CompatUniformManager {
if (locs[LOC_FOG_END] != -1) {
RENDER_BACKEND.uniform1f(locs[LOC_FOG_END], fog.getEnd());
}
if (locs[LOC_FOG_PARAMS] != -1) {
RENDER_BACKEND.uniform4f(locs[LOC_FOG_PARAMS], fog.linearScale(), fog.linearBias(), fog.expScale(), fog.exp2Scale());
}
if (locs[LOC_FOG_COLOR] != -1) {
vec4Buf.clear();
vec4Buf.put((float) fog.getFogColor().x).put((float) fog.getFogColor().y).put((float) fog.getFogColor().z).put(fog.getFogAlpha());
@@ -372,8 +408,6 @@ public class CompatUniformManager {
}
private static void uploadLightSources(int[] locs) {
// Early exit: if the first light field isn't present, no lighting uniforms exist
if (locs[LOC_LIGHT_BASE] == -1) return;
final LightStateStack[] lights = GLStateManager.getLightDataStates();
for (int li = 0; li < 2; li++) {
final int base = LOC_LIGHT_BASE + li * LIGHT_FIELDS;
@@ -0,0 +1,32 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.states.FogState;
public final class FfpGlsl {
private FfpGlsl() {}
// Mesa ffvertex_prog.c build_fog(): FDM_EYE_RADIAL/EYE_PLANE/EYE_PLANE_ABS
public static String fogDistance(int mode, String eye) {
return switch (mode) {
case FogState.DISTANCE_EYE_RADIAL -> "length(" + eye + ".xyz)";
case FogState.DISTANCE_EYE_PLANE -> eye + ".z";
default -> "abs(" + eye + ".z)";
};
}
public static void appendFogFactor(StringBuilder sb, String indent, int fogMode, String coord, String params) {
switch (fogMode) {
case FragmentKey.FOG_LINEAR -> sb.append(indent).append("f = ").append(coord).append(" * ").append(params).append(".x + ").append(params).append(".y;\n");
case FragmentKey.FOG_EXP -> sb.append(indent).append("f = exp2(-(").append(coord).append(" * ").append(params).append(".z));\n");
case FragmentKey.FOG_EXP2 -> {
sb.append(indent).append("float fogTmp = ").append(coord).append(" * ").append(params).append(".w;\n");
sb.append(indent).append("f = exp2(-(fogTmp * fogTmp));\n");
}
}
}
public static String quantizeAlpha(String alpha) {
return "round(" + alpha + " * 255.0)";
}
}
@@ -65,6 +65,16 @@ public final class FragmentKey {
public static final int FOG_EXP = 2;
public static final int FOG_EXP2 = 3;
public static int activeFogMode(GLContextState glCtx) {
if (!glCtx.fogMode.isEnabled()) return FOG_NONE;
return switch (glCtx.fogState.getFogMode()) {
case GL11.GL_LINEAR -> FOG_LINEAR;
case GL11.GL_EXP -> FOG_EXP;
case GL11.GL_EXP2 -> FOG_EXP2;
default -> FOG_NONE;
};
}
private static final int GLOBAL_BITS = 14;
private static final int BIT_COMBINED_GLINT = 61;
private static final int BIT_GLINT_REPLACE_ALPHA = 62;
@@ -117,15 +127,7 @@ public final class FragmentKey {
if (CombinedGlint.replacesAlpha()) global |= 1L << BIT_GLINT_REPLACE_ALPHA;
// Fog
if (glCtx.fogMode.isEnabled()) {
final int fogMode = switch (glCtx.fogState.getFogMode()) {
case GL11.GL_LINEAR -> FOG_LINEAR;
case GL11.GL_EXP -> FOG_EXP;
case GL11.GL_EXP2 -> FOG_EXP2;
default -> FOG_NONE;
};
global |= (long) fogMode;
}
global |= activeFogMode(glCtx);
// Alpha test
if (glCtx.alphaTest.isEnabled()) {
@@ -305,7 +305,7 @@ public final class FragmentShaderGenerator {
final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc());
sb.append(" float qa = round(color.a * 255.0);\n");
sb.append(" float qa = ").append(FfpGlsl.quantizeAlpha("color.a")).append(";\n");
switch (func) {
case GL11.GL_NEVER ->
sb.append(" discard;\n");
@@ -335,18 +335,7 @@ public final class FragmentShaderGenerator {
sb.append(" // Fog\n");
sb.append(" float f;\n");
switch (fogMode) {
case FragmentKey.FOG_LINEAR ->
sb.append(" f = v_FogCoord * u_FogParams.x + u_FogParams.y;\n");
case FragmentKey.FOG_EXP ->
sb.append(" f = exp2(-(v_FogCoord * u_FogParams.z));\n");
case FragmentKey.FOG_EXP2 -> {
sb.append(" float fogTmp = v_FogCoord * u_FogParams.w;\n");
sb.append(" f = exp2(-(fogTmp * fogTmp));\n");
}
}
FfpGlsl.appendFogFactor(sb, " ", fogMode, "v_FogCoord", "u_FogParams");
sb.append(" f = clamp(f, 0.0, 1.0);\n");
sb.append(" color.rgb = mix(u_FogColor.rgb, color.rgb, f);\n");
}
@@ -80,9 +80,6 @@ public class Uniforms {
private final Vector3f tempVec3 = new Vector3f();
private final FloatBuffer clipPlaneBuf = memAllocFloat(32); // 8 planes * vec4
private static final double LN2 = java.lang.Math.log(2.0);
private static final double SQRT_LN2 = java.lang.Math.sqrt(LN2);
public void upload(GLContextState glCtx) {
boolean dirty = false;
contentChanged = false;
@@ -233,8 +230,11 @@ public class Uniforms {
if (projChanged) {
putMat4(FFPUniformBlock.PROJECTION_MATRIX, proj);
}
proj.mul(mv, mvpMatrix);
putMat4(FFPUniformBlock.MVP_MATRIX, mvpMatrix);
putMat4(FFPUniformBlock.MVP_MATRIX, modelViewProjection(proj, mv, mvpMatrix));
}
public static Matrix4f modelViewProjection(Matrix4f proj, Matrix4f mv, Matrix4f dest) {
return proj.mul(mv, dest);
}
private void stageNormalMatrix(GLContextState glCtx) {
@@ -343,17 +343,8 @@ public class Uniforms {
final var secondary = glCtx.secondaryColor;
putVec3(FFPUniformBlock.SECONDARY_COLOR, secondary.getRed(), secondary.getGreen(), secondary.getBlue());
// Mesa STATE_FOG_PARAMS_OPTIMIZED
final FogState fog = glCtx.fogState;
final float start = fog.getStart();
final float end = fog.getEnd();
final float density = fog.getDensity();
final float range = end - start;
putVec4(FFPUniformBlock.FOG_PARAMS,
range != 0.0f ? -1.0f / range : 0.0f,
range != 0.0f ? end / range : 1.0f,
(float) (density / LN2),
(float) (density / SQRT_LN2));
putVec4(FFPUniformBlock.FOG_PARAMS, fog.linearScale(), fog.linearBias(), fog.expScale(), fog.exp2Scale());
putVec4(FFPUniformBlock.FOG_COLOR,
(float) fog.getFogColor().x, (float) fog.getFogColor().y, (float) fog.getFogColor().z,
fog.getFogAlpha());
@@ -100,9 +100,9 @@ public final class VertexShaderGenerator {
private static void emitOutputs(StringBuilder sb, VertexKey key) {
sb.append("// Outputs\n");
sb.append("out vec4 v_Color;\n");
sb.append("invariant gl_Position;\n");
if (key.instancing().hasInstanceHead()) {
sb.append("out vec4 v_Overlay;\n");
sb.append("invariant gl_Position;\n");
}
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
@@ -414,20 +414,11 @@ public final class VertexShaderGenerator {
private static void emitFogDistance(StringBuilder sb, VertexKey key) {
sb.append(" // Fog distance\n");
final String own = fogDistance(key, "eyePos");
final String fog = key.lineQuads() ? acrossLine(own, fogDistance(key, "lineEyeOther")) : own;
final String own = FfpGlsl.fogDistance(key.fogDistanceMode(), "eyePos");
final String fog = key.lineQuads() ? acrossLine(own, FfpGlsl.fogDistance(key.fogDistanceMode(), "lineEyeOther")) : own;
sb.append(" v_FogCoord = ").append(fog).append(";\n");
}
// Mesa ffvertex_prog.c build_fog(): FDM_EYE_RADIAL/EYE_PLANE/EYE_PLANE_ABS
private static String fogDistance(VertexKey key, String eye) {
return switch (key.fogDistanceMode()) {
case 0 -> "length(" + eye + ".xyz)"; // FDM_EYE_RADIAL: Euclidean distance
case 1 -> eye + ".z"; // FDM_EYE_PLANE: raw Z (can be negative, clamped by fog factor)
default -> "abs(" + eye + ".z)"; // FDM_EYE_PLANE_ABS: absolute Z
};
}
private static void emitClipDistances(StringBuilder sb, VertexKey key) {
sb.append(" // Clip distances\n");
for (int i = 0; i < 8; i++) {
@@ -9,6 +9,13 @@ import java.nio.FloatBuffer;
@Getter @Setter
public class FogState implements ISettableState<FogState> {
public static final int DISTANCE_EYE_RADIAL = 0;
public static final int DISTANCE_EYE_PLANE = 1;
public static final int DISTANCE_EYE_PLANE_ABS = 2;
private static final double LN2 = Math.log(2.0);
private static final double SQRT_LN2 = Math.sqrt(LN2);
protected int fogMode = GL11.GL_EXP;
protected final Vector3d fogColor = new Vector3d(0.0F, 0.0F, 0.0F);
protected float fogAlpha = 1.0F;
@@ -16,8 +23,26 @@ public class FogState implements ISettableState<FogState> {
protected float density = 1.0F;
protected float start;
protected float end = 1.0F;
/** Fog distance computation mode: 0=EYE_RADIAL, 1=EYE_PLANE, 2=EYE_PLANE_ABS */
protected int fogDistanceMode = 2;
protected int fogDistanceMode = DISTANCE_EYE_PLANE_ABS;
// Mesa STATE_FOG_PARAMS_OPTIMIZED
public float linearScale() {
final float range = end - start;
return range != 0.0f ? -1.0f / range : 0.0f;
}
public float linearBias() {
final float range = end - start;
return range != 0.0f ? end / range : 1.0f;
}
public float expScale() {
return (float) (density / LN2);
}
public float exp2Scale() {
return (float) (density / SQRT_LN2);
}
@Override
public FogState set(FogState state) {
@@ -0,0 +1,58 @@
# OpenComputers TESR scene, close ring. ~ is the flight start; ground-standing level is ~-2. Yaw 0 faces south (+z).
# Use with FLYBY_ORIGIN=x,z,-21 SMOKE_PITCH=30.
# setup
gamerule doMobSpawning false
gamerule doFireTick false
difficulty 1
# northwest corner: test ISBRH
setblock ~-7 ~-2 ~-7 angelica:test_isbrh
# south: geolyzers
setblock ~-1 ~-2 ~4 OpenComputers:geolyzer
setblock ~0 ~-2 ~4 OpenComputers:geolyzer
setblock ~1 ~-2 ~4 OpenComputers:geolyzer
setblock ~0 ~-2 ~5 OpenComputers:geolyzer
# southwest: power distributor, assembler, capacitor
setblock ~-3 ~-2 ~3 OpenComputers:powerDistributor
setblock ~-3 ~-1 ~3 OpenComputers:assembler
setblock ~-4 ~-2 ~3 OpenComputers:capacitor
# west: relay, transposer, adapter
setblock ~-4 ~-2 ~0 OpenComputers:relay
setblock ~-4 ~-2 ~1 OpenComputers:transposer
setblock ~-4 ~-1 ~1 OpenComputers:adapter
# northwest: powered chargers
setblock ~-3 ~-2 ~-3 OpenComputers:charger
setblock ~-3 ~-2 ~-4 minecraft:redstone_block
setblock ~-4 ~-2 ~-2 OpenComputers:charger
setblock ~-4 ~-2 ~-3 minecraft:redstone_block
# north: microcontrollers, raids
setblock ~-1 ~-2 ~-4 OpenComputers:microcontroller
setblock ~0 ~-2 ~-4 OpenComputers:microcontroller
setblock ~1 ~-2 ~-4 OpenComputers:raid
setblock ~-1 ~-1 ~-4 OpenComputers:microcontroller
setblock ~0 ~-1 ~-4 OpenComputers:raid
setblock ~1 ~-1 ~-4 OpenComputers:raid
# northeast: cases, disk drive
setblock ~3 ~-2 ~-3 OpenComputers:case1
setblock ~4 ~-2 ~-3 OpenComputers:case2
setblock ~3 ~-1 ~-3 OpenComputers:case3
setblock ~4 ~-1 ~-3 OpenComputers:diskDrive
# east: rack, screens
setblock ~4 ~-2 ~-2 OpenComputers:rack
setblock ~4 ~-2 ~-1 OpenComputers:screen2
setblock ~4 ~-1 ~-1 OpenComputers:screen2
setblock ~4 ~-2 ~1 OpenComputers:screen1
# southeast: hologram projectors, disassembler, net splitter
setblock ~2 ~-2 ~3 OpenComputers:hologram1
setblock ~3 ~-2 ~2 OpenComputers:hologram2
setblock ~3 ~-2 ~3 OpenComputers:disassembler
setblock ~3 ~-1 ~3 OpenComputers:netSplitter
@@ -0,0 +1,67 @@
# OpenComputers TESR perf scene: running machines and a 12x12 overlay grid. ~ is the flight start; ground-standing level is ~-2. Yaw 0 faces south (+z).
# Use with FLYBY_ORIGIN=x,z,-21 SMOKE_PITCH=20 SMOKE_WARMUP_TICKS=400.
define BUFFER_3X2 {width:24,height:8,depth:1,buffer:["ANGELICA TESR FLYBY TEST","RED GREEN BLUE YELLOW ok","0123456789ABCDEF01234567","========================","abcdefghijklmnopqrstuvwx","The quick brown fox jump","+-*/=<>#%&!?.+-*/=<>#%&!","screen1 3x2 16 color pal"],color:[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,3599,3599,3599,2063,1295,1295,1295,1295,1295,2063,783,783,783,783,2063,1039,1039,1039,1039,1039,1039,2063,2063,2063,7,263,519,775,1031,1287,1543,1807,2055,2311,2567,2823,3079,3335,3591,3847,7,263,519,775,1031,1287,1543,1807,3840,3840,3841,3842,3842,3843,3844,3844,3845,3846,3846,3847,3848,3848,3849,3850,3850,3851,3852,3852,3853,3854,3854,3855,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266]}
define BUFFER_1X1 {width:8,height:4,depth:0,buffer:["ONE BIT.","01234567","abcdefgh","+-*/=<>#"],color:[256,256,256,256,256,256,256,256,1,1,1,1,1,1,1,1,256,256,256,256,256,256,256,256,256,1,256,1,256,1,256,1]}
define EEPROM {id:"OpenComputers:eeprom",Count:1b,Damage:0s,tag:{oc;data:{oc;eeprom#:"local c,k=component,computer local function f(t) return (c.list(t)()) end local h,m,r,g=f('hologram'),f('modem'),f('robot'),f('debug') local function s(t) if g then pcall(c.invoke,g,'runCommand','say flyby-oc '..t) end end s('boot hologram='..tostring(h)..' modem='..tostring(m)..' robot='..tostring(r)) if h then s('hologram '..tostring(pcall(function() c.invoke(h,'clear') local n=1 if c.invoke(h,'maxDepth')==2 then n=3 end for x=1,48 do for z=1,48 do local a,b=x-24.5,z-24.5 local d=math.sqrt(a*a+b*b) if d<23 then c.invoke(h,'fill',x,z,1,math.floor(math.sqrt(529-d*d))+1,math.floor(d/4)%n+1) end end end end))) end while true do if m then pcall(c.invoke,m,'broadcast',1,'flyby') end if r then pcall(c.invoke,r,'turn',true) end k.pullSignal(0.5) end",oc;label:"flyby",oc;readonly:1b}}}
# setup
gamerule doMobSpawning false
gamerule doFireTick false
difficulty 1
# held screen: up-facing screens draw the up indicator
clear @p
give @p OpenComputers:screen1 1
# south: test ISBRH
setblock ~0 ~-2 ~5 angelica:test_isbrh
# southwest: up-facing screen on a creative case
setblock ~-3 ~-2 ~4 OpenComputers:caseCreative
tile ~-3 ~-2 ~3 OpenComputers:screen1 0 {oc;pitch:1,buffer:${BUFFER_1X1}}
# north: creative case, 3x2 16-color screen, 1x1 mono screen (every wall block carries the buffer, the merge keeps the origin's)
setblock ~2 ~-2 ~-5 OpenComputers:caseCreative
setblock ~-1 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~0 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~1 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-1 ~-1 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~0 ~-1 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~1 ~-1 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~3 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_1X1}}
# west: powered case, distributor, capacitor, chargers
setblock ~-4 ~-2 ~0 OpenComputers:caseCreative
setblock ~-4 ~-1 ~0 OpenComputers:powerDistributor
setblock ~-4 ~-2 ~1 OpenComputers:capacitor
setblock ~-4 ~-2 ~-1 OpenComputers:charger
setblock ~-5 ~-2 ~-1 minecraft:redstone_block
setblock ~-4 ~-2 ~2 OpenComputers:charger
setblock ~-5 ~-2 ~2 minecraft:redstone_block
# east: running computer, tier 2 projector on top, relay
tile ~5 ~-2 ~0 OpenComputers:caseCreative 0 {oc;tier:3b,oc;yaw:4,oc;items:[{slot:0b,item:{id:"OpenComputers:item",Count:1b,Damage:11s}},{slot:1b,item:{id:"OpenComputers:item",Count:1b,Damage:73s}},{slot:3b,item:{id:"OpenComputers:item",Count:1b,Damage:39s}},{slot:8b,item:{id:"OpenComputers:item",Count:1b,Damage:43s}},{slot:9b,item:${EEPROM}}]}
tile ~5 ~-1 ~0 OpenComputers:hologram2 0 {oc;tier:1b,oc;scale:1.0d}
setblock ~5 ~-2 ~1 OpenComputers:relay
after 30 activate ~5 ~-2 ~0 sneak
# far southeast: same computer, projector at scale 4 in the hologram fade band
tile ~40 ~-2 ~40 OpenComputers:caseCreative 0 {oc;tier:3b,oc;yaw:4,oc;items:[{slot:1b,item:{id:"OpenComputers:item",Count:1b,Damage:73s}},{slot:3b,item:{id:"OpenComputers:item",Count:1b,Damage:39s}},{slot:8b,item:{id:"OpenComputers:item",Count:1b,Damage:43s}},{slot:9b,item:${EEPROM}}]}
tile ~40 ~-1 ~40 OpenComputers:hologram2 0 {oc;tier:1b,oc;scale:4.0d}
after 30 activate ~40 ~-2 ~40 sneak
# southeast: empty tier 1 and 2 projectors
setblock ~3 ~-3 ~2 OpenComputers:caseCreative
setblock ~3 ~-2 ~2 OpenComputers:hologram1
setblock ~3 ~-3 ~4 OpenComputers:caseCreative
setblock ~3 ~-2 ~4 OpenComputers:hologram2
# northwest: turning robot
tile ~-4 ~-2 ~-4 OpenComputers:robot 0 {display:{Name:"Flyby"},oc;tier:3,oc;components:[{id:"OpenComputers:item",Count:1b,Damage:73s},{id:"OpenComputers:item",Count:1b,Damage:43s},{id:"OpenComputers:item",Count:1b,Damage:39s},${EEPROM}]}
after 30 activate ~-4 ~-2 ~-4 sneak
# south: 12x12 overlay grid, one 12x4 band per block
fill ~-6 ~-2 ~9 ~5 ~-2 ~12 OpenComputers:geolyzer
fill ~-6 ~-2 ~13 ~5 ~-2 ~16 OpenComputers:microcontroller
fill ~-6 ~-2 ~17 ~5 ~-2 ~20 OpenComputers:raid
@@ -0,0 +1,39 @@
# OpenComputers screen text fade scene, for a shader pack run. ~ is the flight start; ground-standing level is ~-2. Yaw 0 faces south (+z).
# Use with FLYBY_ORIGIN=x,z,-21 SMOKE_PITCH=15 SMOKE_WARMUP_TICKS=300. Shot 4 (north) holds all three walls.
define BUFFER_3X2 {width:24,height:8,depth:1,buffer:["ANGELICA TESR FLYBY TEST","RED GREEN BLUE YELLOW ok","0123456789ABCDEF01234567","========================","abcdefghijklmnopqrstuvwx","The quick brown fox jump","+-*/=<>#%&!?.+-*/=<>#%&!","screen1 3x2 16 color pal"],color:[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,3599,3599,3599,2063,1295,1295,1295,1295,1295,2063,783,783,783,783,2063,1039,1039,1039,1039,1039,1039,2063,2063,2063,7,263,519,775,1031,1287,1543,1807,2055,2311,2567,2823,3079,3335,3591,3847,7,263,519,775,1031,1287,1543,1807,3840,3840,3841,3842,3842,3843,3844,3844,3845,3846,3846,3847,3848,3848,3849,3850,3850,3851,3852,3852,3853,3854,3854,3855,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266]}
# setup
gamerule doMobSpawning false
gamerule doFireTick false
difficulty 1
# south: test ISBRH
setblock ~0 ~-2 ~5 angelica:test_isbrh
# north, near: full text
setblock ~-2 ~-2 ~-7 OpenComputers:caseCreative
setblock ~-5 ~-2 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-4 ~-2 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-3 ~-2 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-5 ~-1 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-4 ~-1 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-3 ~-1 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
# north, mid: fading text
setblock ~2 ~-2 ~-26 OpenComputers:caseCreative
setblock ~-1 ~-2 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~0 ~-2 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~1 ~-2 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~-1 ~-1 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~0 ~-1 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~1 ~-1 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
# north, far: no text
setblock ~10 ~-2 ~-32 OpenComputers:caseCreative
setblock ~7 ~-2 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~8 ~-2 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~9 ~-2 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~7 ~-1 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~8 ~-1 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
setblock ~9 ~-1 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}}
+15 -4
View File
@@ -9,7 +9,11 @@
# QUICKPLAY_WORLD - save to load (default: Flat World - No Mods).
# FLYBY_ORIGIN - x,z[,yaw] flight start (default: 1509.5,-924.5,0).
# SMOKE_TIMEOUT - seconds before the client is killed (default: 600).
# SMOKE_SCENE - scene command file (default: scripts/flyby-scenes/surround-smoke.txt).
# SMOKE_PACK - shaderpack name for the pack variant (default: ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1).
# SMOKE_DEBUG_HUD - true forces F3 on; false keeps the saved setting (default: true).
# SMOKE_PITCH - camera pitch in degrees, positive looks down (default: 15).
# SMOKE_WARMUP_TICKS - ticks at the first pose before the pan (default: 100).
#
# Runs from this script's own checkout. Restores run/client/config/shaders.properties and angelica-options.json on exit.
@@ -28,8 +32,15 @@ QUICKPLAY_WORLD="${QUICKPLAY_WORLD:-Flat World - No Mods}"
FLYBY_ORIGIN="${FLYBY_ORIGIN:-1509.5,-924.5,0}"
SMOKE_TIMEOUT="${SMOKE_TIMEOUT:-600}"
SMOKE_PACK="${SMOKE_PACK:-ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1}"
SMOKE_DEBUG_HUD="${SMOKE_DEBUG_HUD:-true}"
SMOKE_PITCH="${SMOKE_PITCH:-15}"
SMOKE_WARMUP_TICKS="${SMOKE_WARMUP_TICKS:-100}"
SCENE="$ROOT/scripts/flyby-scenes/surround-smoke.txt"
SCENE="${SMOKE_SCENE:-$ROOT/scripts/flyby-scenes/surround-smoke.txt}"
case "$SCENE" in
/*) ;;
*) SCENE="$ROOT/$SCENE" ;;
esac
RUN="$ROOT/run/client"
SHADERS="$RUN/config/shaders.properties"
OUT="$ROOT/tmp/smoke/$NAME"
@@ -146,13 +157,13 @@ PY
./gradlew runClient25 --console=plain \
-Dangelica.flyby.route=pan \
-Dangelica.flyby.length=360 \
-Dangelica.flyby.warmupTicks=100 \
-Dangelica.flyby.warmupTicks="$SMOKE_WARMUP_TICKS" \
-Dangelica.flyby.exitWhenDone=true \
-Dangelica.flyby.commands="$SCENE" \
-Dangelica.flyby.origin="$FLYBY_ORIGIN" \
-Dangelica.flyby.pitch=15 \
-Dangelica.flyby.pitch="$SMOKE_PITCH" \
-Dangelica.flyby.screenshots=8 \
-Dangelica.flyby.debugHud=true \
-Dangelica.flyby.debugHud="$SMOKE_DEBUG_HUD" \
-Dangelica.flyby.crashTest=isbrh \
-Dangelica.debug.testBlocks=true \
-DquickPlaySingleplayer="$QUICKPLAY_WORLD" \
@@ -11,16 +11,17 @@ public final class ClipZRemap {
private ClipZRemap() {}
public static void collectEdits(GLSLParser.Translation_unitContext root, List<Edit> edits) {
public static boolean collectEdits(GLSLParser.Translation_unitContext root, List<Edit> edits) {
for (GLSLParser.External_declarationContext external : root.external_declaration()) {
final GLSLParser.Function_definitionContext function = external.function_definition();
if (function == null || function.function_prototype().IDENTIFIER() == null) continue;
if (!"main".equals(function.function_prototype().IDENTIFIER().getText())) continue;
final GLSLParser.Compound_statement_no_new_scopeContext body = function.compound_statement_no_new_scope();
if (body == null || body.RIGHT_BRACE() == null) return;
if (body == null || body.RIGHT_BRACE() == null) return false;
final int idx = body.RIGHT_BRACE().getSymbol().getStartIndex();
edits.add(new Edit(idx, idx - 1, INJECTION_STD));
return;
return true;
}
return false;
}
}
@@ -296,7 +296,7 @@ public final class ShaderManager {
return shaderKindFor(obj.type);
}
private static int shaderKindFor(int glShaderType) {
static int shaderKindFor(int glShaderType) {
if (glShaderType == GL20.GL_VERTEX_SHADER) return Shaderc.shaderc_vertex_shader;
if (glShaderType == GL32.GL_GEOMETRY_SHADER) return Shaderc.shaderc_geometry_shader;
if (glShaderType == GL43.GL_COMPUTE_SHADER) return Shaderc.shaderc_compute_shader;
@@ -311,14 +311,20 @@ public final class ShaderManager {
}
private static PrewarmTransformResult transformOrNull(String transformedSource, int glShaderType, String debugName, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
final String source = preprocessed(transformedSource, glShaderType);
final GLSLParser.Translation_unitContext root;
try {
root = GlslTransformUtils.parseFullQuiet(transformedSource);
root = GlslTransformUtils.parseFullQuiet(source);
} catch (Exception e) {
LOG.warn("glsl-transformation-lib parse failed for '{}': {}", debugName, e.getMessage());
return null;
}
return transform(transformedSource, 0, root, glShaderType, debugName, perFrame, perPass);
return transform(source, 0, root, glShaderType, debugName, perFrame, perPass);
}
static String preprocessed(String source, int glShaderType) {
final String preprocessed = SpirvCompiler.preprocess(source, shaderKindFor(glShaderType), "transform", SpirvCompiler.Options.vulkanForced460Core());
return preprocessed != null ? preprocessed : source;
}
public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
@@ -333,8 +339,8 @@ public final class ShaderManager {
PerFrameBlockInjector.collectEdits(index, perFrame, perPass, edits);
}
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(index, glShaderType, debugName, true, edits);
if (glShaderType == GL20.GL_VERTEX_SHADER) {
ClipZRemap.collectEdits(tree, edits);
if (glShaderType == GL20.GL_VERTEX_SHADER && !ClipZRemap.collectEdits(tree, edits)) {
LOG.warn("No main() found in vertex shader '{}'; its clip-space depth stays in OpenGL's -1..1 range", debugName);
}
final List<TextureDeclarations.Declaration> textureDeclarations = TextureDeclarations.collect(index);
final Set<String> strippedSamplers = SamplerStripper.collectEdits(index, textureDeclarations, edits);
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
@@ -15,10 +16,13 @@ import org.lwjgl.opengl.GL32;
import org.taumc.glsl.ShaderParser;
import org.taumc.glsl.grammar.GLSLParser;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class PrewarmTreeHandoffEquivalenceTest {
@@ -104,7 +108,16 @@ class PrewarmTreeHandoffEquivalenceTest {
GlslVulkanPreprocess.clearCache();
final ShaderManager.PrewarmTransformResult treeFed = ShaderManager.applyPrewarmTransformsFull(finalSource, tree, HEADER.length(), glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
assertEquals(reparse.source(), treeFed.source(), "tree handoff diverged from re-parse for " + label);
assertArrayEquals(spirv(reparse.source(), glShaderType, label), spirv(treeFed.source(), glShaderType, label), "tree handoff compiled differently from re-parse for " + label);
assertEquals(reparse.boolUniforms(), treeFed.boolUniforms(), "bool uniforms diverged for " + label);
}
private static byte[] spirv(String source, int glShaderType, String label) {
final SpirvCompiler.Result result = SpirvCompiler.compile(source, ShaderManager.shaderKindFor(glShaderType), label, SpirvCompiler.Options.vulkanForced460Core(), false);
assertNotNull(result.spirv(), () -> label + " failed to compile: " + result.error() + "\n" + source);
final ByteBuffer words = result.spirv().duplicate();
final byte[] bytes = new byte[words.remaining()];
words.get(bytes);
return bytes;
}
}
@@ -49,6 +49,23 @@ class ShaderSourceTransformCacheTest {
}
""";
private static final String CONDITIONAL_MAIN_VERT = """
#version 330 core
#define OC_CORE
#ifdef OC_CORE
layout(location = 0) in vec3 a_Position;
uniform mat4 u_MVP;
void main() {
gl_Position = u_MVP * vec4(a_Position, 1.0);
}
#else
uniform mat4 u_ModelMatrix;
void main() {
gl_Position = gl_ModelViewProjectionMatrix * u_ModelMatrix * gl_Vertex;
}
#endif
""";
private static final String CLIP_Z = "gl_Position.z = gl_Position.z * 0.5 + gl_Position.w * 0.5;";
@BeforeEach
@@ -93,6 +110,18 @@ class ShaderSourceTransformCacheTest {
assertTrue(sm.getShaderSource(v).contains(CLIP_Z));
}
@Test
void mainInsideConditionalBlocksIsPreprocessedAndCarriesClipZOnce() {
final ShaderManager sm = new ShaderManager(null);
final int v = sm.createShader(GL20.GL_VERTEX_SHADER);
sm.shaderSource(v, CONDITIONAL_MAIN_VERT);
final String out = sm.getShaderSource(v);
assertEquals(out.indexOf(CLIP_Z), out.lastIndexOf(CLIP_Z), out);
assertTrue(out.contains(CLIP_Z), out);
assertFalse(out.contains("gl_ModelViewProjectionMatrix"), out);
}
@Test
void blockChangeMissesTheCache() {
GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.FLOAT)));
@@ -18,6 +18,7 @@ final class ShaderTransformChain {
private ShaderTransformChain() {}
static String run(String source, int glShaderType) {
source = ShaderManager.preprocessed(source, glShaderType);
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true);
String src = pre != null ? pre.rewrittenSource() : source;
if (glShaderType == GL20.GL_VERTEX_SHADER) {
@@ -97,7 +97,7 @@ public final class TesrMaterial {
return this;
}
/** Depth prepass: writes depth only, no color. Mutually exclusive with noDepthWrite */
/** Depth prepass: writes depth only, no color. Mutually exclusive with noDepthWrite and depthEqual */
public Builder depthOnly() {
this.depthOnly = true;
return this;
@@ -109,13 +109,13 @@ public final class TesrMaterial {
return this;
}
/** Discards pixels with alpha at or below threshold - hard-edged cutout, no blending. ALPHA_TEST GREATER */
/** Discards pixels with alpha at or below threshold. ALPHA_TEST GREATER */
public Builder cutout(float threshold) {
this.cutoutAlpha = threshold;
return this;
}
/** Draws only where geometry already exists at the same depth; for decal/overlay passes. GL_EQUAL depth */
/** GL_EQUAL depth for decal/overlay passes; matches only depth writers in the same tile-entity render or renderCached call */
public Builder depthEqual() {
this.depthEqual = true;
return this;
@@ -165,6 +165,9 @@ public final class TesrMaterial {
if (special != SpecialRender.NONE && shader != null) {
throw new IllegalStateException("special() and shader() are mutually exclusive");
}
if (depthOnly && (noDepthWrite || depthEqual)) {
throw new IllegalStateException("depthOnly() excludes noDepthWrite() and depthEqual()");
}
return intern(new TesrMaterial(hasColor, red, green, blue, alpha, hasLightmap, lightmapX, lightmapY, noCull, unlit, noDepthWrite, depthOnly, stream, cutoutAlpha, depthEqual, unfilteredAtlas, transparency, special, shader));
}
}
@@ -1,17 +1,23 @@
package com.gtnewhorizons.angelica.api.tesr;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import net.minecraft.util.ResourceLocation;
/** A registered mod-owned GL program; see {@link TesrShaders#register} */
import javax.annotation.Nullable;
/** A registered mod shader; see {@link TesrShaders#register} */
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
public final class TesrShader {
private final String name;
private final Runnable bind;
private final Runnable release;
@Nullable private final Runnable bind;
@Nullable private final Runnable release;
@Nullable private final ResourceLocation vertex;
@Nullable private final ResourceLocation fragment;
@Nullable private final TesrShaderUniforms uniforms;
}
@@ -0,0 +1,11 @@
package com.gtnewhorizons.angelica.api.tesr;
/** Uniforms of a source-registered {@link TesrShader}; render thread, program in use, no draws */
public interface TesrShaderUniforms {
/** After each variant link (first draw, resource reload): resolve locations, set constants */
default void angelica$linked(int program, boolean instanced) {}
/** Per layer bind: upload per-draw values; Angelica's own uniforms are not uploaded yet */
void angelica$bound(int program, boolean instanced);
}
@@ -1,5 +1,8 @@
package com.gtnewhorizons.angelica.api.tesr;
import net.minecraft.util.ResourceLocation;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@@ -9,17 +12,36 @@ public final class TesrShaders {
private TesrShaders() {}
/** Registers a GL program under a unique name and returns its handle, wraps draws of any {@link TesrMaterial} referencing it */
/** Registers a GL program under a unique name and returns its handle, wraps draws of any {@link TesrMaterial} referencing it */
public static TesrShader register(String name, Runnable bind, Runnable release) {
checkName(name);
Objects.requireNonNull(bind, "bind");
Objects.requireNonNull(release, "release");
return add(new TesrShader(name, bind, release, null, null, null));
}
public static TesrShader register(String name, ResourceLocation vertex, ResourceLocation fragment) {
return register(name, vertex, fragment, null);
}
/** Registers GLSL bodies (no {@code #version}) wrapped in Angelica's prelude, compiled at the first draw that binds them */
public static TesrShader register(String name, ResourceLocation vertex, ResourceLocation fragment, @Nullable TesrShaderUniforms uniforms) {
checkName(name);
Objects.requireNonNull(vertex, "vertex");
Objects.requireNonNull(fragment, "fragment");
return add(new TesrShader(name, null, null, vertex, fragment, uniforms));
}
private static void checkName(String name) {
final int colon = name.indexOf(':');
if (colon <= 0 || colon == name.length() - 1 || name.indexOf(':', colon + 1) >= 0) {
throw new IllegalArgumentException("TESR shader name must be namespace:path, got '" + name + "'");
}
Objects.requireNonNull(bind, "bind");
Objects.requireNonNull(release, "release");
final TesrShader shader = new TesrShader(name, bind, release);
if (REGISTRY.putIfAbsent(name, shader) != null) {
throw new IllegalStateException("TESR shader '" + name + "' already registered");
}
private static TesrShader add(TesrShader shader) {
if (REGISTRY.putIfAbsent(shader.name(), shader) != null) {
throw new IllegalStateException("TESR shader '" + shader.name() + "' already registered");
}
return shader;
}
@@ -1,5 +1,5 @@
/** TESR mesh cache/batch API (Render thread only) */
@API(owner = "angelica", provides = "angelica|tesr", apiVersion = "2")
@API(owner = "angelica", provides = "angelica|tesr", apiVersion = "3")
package com.gtnewhorizons.angelica.api.tesr;
import cpw.mods.fml.common.API;
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.api.tesr.TesrShader;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.hooks.TextureUtilHooks;
import com.gtnewhorizons.angelica.rendering.tesr.DrawState;
import com.gtnewhorizons.angelica.rendering.tesr.TesrShaderPrograms;
import com.gtnewhorizons.angelica.shadercompat.ShaderGlint;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
@@ -46,7 +47,7 @@ public final class RenderLayer implements BlendingStateHolder {
private static final Hook BEACON_BEAM_HOOK = new Hook(
() -> GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.BEACON_BEAM),
GbufferPrograms::teardownSpecialRenderCondition);
private static final Reference2ObjectOpenHashMap<TesrShader, Hook> SHADER_HOOKS = new Reference2ObjectOpenHashMap<>();
private static final Reference2ObjectOpenHashMap<TesrShader, Reference2ObjectOpenHashMap<PassOverride, Hook[]>> SHADER_HOOKS = new Reference2ObjectOpenHashMap<>();
private static final Reference2ObjectOpenHashMap<PassOverride, Hook> PASS_HOOKS = new Reference2ObjectOpenHashMap<>();
private String name;
@@ -100,7 +101,7 @@ public final class RenderLayer implements BlendingStateHolder {
private static RenderLayer build(ResourceLocation texture, TesrMaterial material, PassOverride pass, float offsetFactor, float offsetUnits, int glintSlot, int cull, boolean lit, boolean lightmap, boolean noPass, boolean afterGlint) {
final DrawState state = DrawState.forMaterial(material, cull, lit, lightmap, offsetFactor, offsetUnits);
final Hook hook = hookFor(material, noPass ? PassOverride.NONE : pass, glintSlot);
final Hook hook = hookFor(material, noPass ? PassOverride.NONE : pass, glintSlot, state.isLightmap());
final TransparencyType transparency = afterGlint ? TransparencyType.AFTER_GLINT : transparencyFor(state.getBlend());
final RenderLayer candidate = new RenderLayer(null, BatchVertexFormats.POSITION_COLOR_TEXTURE_LIGHTF_NORMAL,
GL11.GL_QUADS, TESR_BUFFER_SIZE, state, texture, material.isUnfilteredAtlas(), transparency, hook,
@@ -129,10 +130,6 @@ public final class RenderLayer implements BlendingStateHolder {
}
}
public static int cacheSize() {
return CACHE.size();
}
private static TransparencyType transparencyFor(int blend) {
return switch (blend) {
case DrawState.OPAQUE -> TransparencyType.OPAQUE;
@@ -141,7 +138,7 @@ public final class RenderLayer implements BlendingStateHolder {
};
}
private static Hook hookFor(TesrMaterial material, PassOverride pass, int glintSlot) {
private static Hook hookFor(TesrMaterial material, PassOverride pass, int glintSlot, boolean lightmap) {
switch (material.special()) {
case GLINT -> {
return glintHook(glintSlot);
@@ -152,7 +149,7 @@ public final class RenderLayer implements BlendingStateHolder {
case NONE -> {
final TesrShader shader = material.shader();
if (shader != null) {
return shaderHook(shader);
return shaderHook(shader, pass, lightmap);
}
}
}
@@ -171,11 +168,40 @@ public final class RenderLayer implements BlendingStateHolder {
return hook;
}
private static Hook shaderHook(TesrShader shader) {
Hook hook = SHADER_HOOKS.get(shader);
private static Hook shaderHook(TesrShader shader, PassOverride pass, boolean lightmap) {
Reference2ObjectOpenHashMap<PassOverride, Hook[]> byPass = SHADER_HOOKS.get(shader);
if (byPass == null) {
byPass = new Reference2ObjectOpenHashMap<>();
SHADER_HOOKS.put(shader, byPass);
}
Hook[] byLightmap = byPass.get(pass);
if (byLightmap == null) {
byLightmap = new Hook[2];
byPass.put(pass, byLightmap);
}
final int slot = lightmap ? 1 : 0;
Hook hook = byLightmap[slot];
if (hook == null) {
hook = new Hook(shader.bind(), shader.release());
SHADER_HOOKS.put(shader, hook);
final Hook packHook = pass == PassOverride.NONE ? null : passHook(pass);
final TesrShaderPrograms.Entry entry = TesrShaderPrograms.entry(shader);
final boolean[] packDrew = new boolean[1];
final Runnable bind = shader.bind() != null ? shader.bind() : () -> TesrShaderPrograms.bindBaked(entry, lightmap);
final Runnable release = shader.release() != null ? shader.release() : () -> TesrShaderPrograms.release(entry);
hook = new Hook(() -> {
packDrew[0] = TesrShaderPrograms.packActive();
if (!packDrew[0]) {
bind.run();
} else if (packHook != null) {
packHook.begin.run();
}
}, () -> {
if (!packDrew[0]) {
release.run();
} else if (packHook != null) {
packHook.end.run();
}
});
byLightmap[slot] = hook;
}
return hook;
}
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.client.rendering.GlUniformFloat3Array;
import com.gtnewhorizons.angelica.client.rendering.GlUniformFloat4Array;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.StateSet;
import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
@@ -51,8 +52,6 @@ final class PortalDrawer {
private static final int LIGHTMAP_UNIT = 1;
private static final int FIELD_UNIT = 2;
private static final int FACE_BYTES = FLOATS_PER_FACE * Float.BYTES;
private static final double LN2 = Math.log(2.0);
private static final double SQRT_LN2 = Math.sqrt(LN2);
private static final float[] LAYER_DEPTH = new float[LAYERS];
private static final float[] LAYER_SCALE = new float[LAYERS];
private static final float[] LAYER_ANGLE = new float[LAYERS];
@@ -105,26 +104,12 @@ final class PortalDrawer {
faces[i] = 0.0F;
}
private static int fogMode() {
if (!GLStateManager.getFogMode().isEnabled()) return 0;
return switch (GLStateManager.getFogState().getFogMode()) {
case GL11.GL_LINEAR -> 1;
case GL11.GL_EXP -> 2;
case GL11.GL_EXP2 -> 3;
default -> 0;
};
}
private static void uploadFog(Uniforms uniforms, int fogMode) {
final FogState fog = GLStateManager.getFogState();
final float start = fog.getStart();
final float end = fog.getEnd();
final float density = fog.getDensity();
final float range = end - start;
FOG_PARAMS[0] = range != 0.0f ? -1.0f / range : 0.0f;
FOG_PARAMS[1] = range != 0.0f ? end / range : 1.0f;
FOG_PARAMS[2] = (float) (density / LN2);
FOG_PARAMS[3] = (float) (density / SQRT_LN2);
FOG_PARAMS[0] = fog.linearScale();
FOG_PARAMS[1] = fog.linearBias();
FOG_PARAMS[2] = fog.expScale();
FOG_PARAMS[3] = fog.exp2Scale();
uniforms.fogParams.set(FOG_PARAMS);
uniforms.fogColor.set((float) fog.getFogColor().x, (float) fog.getFogColor().y, (float) fog.getFogColor().z);
uniforms.fogMode.setInt(fogMode);
@@ -187,9 +172,9 @@ final class PortalDrawer {
}
void drawDirect(float[] faces, int count, int tunnelTexture, int fieldTexture, Frame frame, boolean fogged) {
final int fogMode = fogged ? fogMode() : 0;
final int fogMode = fogged ? FragmentKey.activeFogMode(GLStateManager.ctx()) : FragmentKey.FOG_NONE;
final GlProgram<Uniforms> program;
if (fogMode == 0) {
if (fogMode == FragmentKey.FOG_NONE) {
if (direct == null) direct = buildProgram(false, false);
program = direct;
} else {
@@ -276,7 +261,7 @@ final class PortalDrawer {
uniforms.lightmapUv.set(frame.lightmapU, frame.lightmapV);
uniforms.lightmapEnabled.setFloat(frame.lightmapEnabled ? 1.0F : 0.0F);
if (uniforms.atlasSize != null) uniforms.atlasSize.setFloat(ATLAS_SIZE);
if (fogMode != 0) uploadFog(uniforms, fogMode);
if (fogMode != FragmentKey.FOG_NONE) uploadFog(uniforms, fogMode);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + FIELD_UNIT);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, fieldTexture);
@@ -22,6 +22,10 @@ final class FlybyCommandSender implements ICommandSender {
this.anchor = anchor;
}
EntityPlayerMP player() {
return this.player;
}
@Override
public String getCommandSenderName() {
return NAME;
@@ -32,6 +32,7 @@ import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraft.world.storage.ThreadedFileIOBase;
import net.minecraftforge.common.DimensionManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -39,7 +40,9 @@ import org.lwjgl.opengl.Display;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
@@ -83,6 +86,9 @@ public final class FlybyRunner {
private String[] sceneCommands = NO_COMMANDS;
private volatile boolean sceneClearRequested;
private int sceneSpawnTotal;
private final List<FlybyScene.Deferred> sceneDeferred = new ArrayList<>();
private FlybyCommandSender sceneSender;
private int sceneTick;
private boolean pauseOnLostFocusSaved;
private boolean pauseOnLostFocusOverridden;
private boolean vsyncOverridden;
@@ -623,7 +629,7 @@ public final class FlybyRunner {
public void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
final boolean clear = this.sceneClearRequested;
if (!clear && this.pendingRequest.get() == null && this.pendingDimensionPlayer.get() == null) return;
if (!clear && this.pendingRequest.get() == null && this.pendingDimensionPlayer.get() == null && this.sceneDeferred.isEmpty()) return;
final MinecraftServer server = MinecraftServer.getServer();
if (server == null) return;
@@ -645,6 +651,8 @@ public final class FlybyRunner {
this.clearScene(server);
}
this.runDeferred(server);
final FlybyRequest request = this.pendingRequest.getAndSet(null);
if (request == null) return;
@@ -669,6 +677,12 @@ public final class FlybyRunner {
}
}
server.getConfigurationManager().saveAllPlayerData();
// Queued chunk NBT aliases the live block arrays: without this drain the scene placed this tick reaches disk.
try {
ThreadedFileIOBase.threadedIOInstance.waitForFinish();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
worldChangesDiscarded = true;
LOGGER.warn("Flyby: world saving is off until you leave this world");
@@ -755,14 +769,22 @@ public final class FlybyRunner {
this.clearScene(server);
final ChunkCoordinates anchor = new ChunkCoordinates(MathHelper.floor_double(request.originX()), MathHelper.floor_double(feetY + 0.5D), MathHelper.floor_double(request.originZ()));
final FlybyCommandSender sender = new FlybyCommandSender(player, anchor);
this.sceneSender = new FlybyCommandSender(player, anchor);
this.sceneTick = 0;
for (String raw : this.sceneCommands) {
final String line = FlybyScene.expand(raw, anchor.posX, anchor.posY, anchor.posZ);
if (server.getCommandManager().executeCommand(sender, line) == 0) {
if (!line.startsWith("after ")) {
this.runSceneLine(server, line);
continue;
}
final FlybyScene.Deferred deferred = FlybyScene.deferred(line);
if (deferred == null) {
LOGGER.warn("flyby scene command did not execute: {}", line);
} else {
this.sceneDeferred.add(deferred);
}
}
LOGGER.info("Flyby scene: ran {} commands", this.sceneCommands.length);
LOGGER.info("Flyby scene: ran {} commands, {} deferred", this.sceneCommands.length - this.sceneDeferred.size(), this.sceneDeferred.size());
if (Tracy.ENABLED) Tracy.message("flyby scene " + this.sceneCommands.length + " commands");
int total = 0;
for (WorldServer world : server.worldServers) {
@@ -771,7 +793,32 @@ public final class FlybyRunner {
this.sceneSpawnTotal = total;
}
private void runSceneLine(MinecraftServer server, String line) {
if (!FlybyScene.run(server, this.sceneSender, line)) {
LOGGER.warn("flyby scene command did not execute: {}", line);
}
}
private void runDeferred(MinecraftServer server) {
if (this.sceneDeferred.isEmpty()) return;
this.sceneTick++;
int i = 0;
while (i < this.sceneDeferred.size()) {
final FlybyScene.Deferred deferred = this.sceneDeferred.get(i);
if (deferred.ticks() > this.sceneTick) {
i++;
continue;
}
this.sceneDeferred.remove(i);
LOGGER.info("Flyby scene: tick {} {}", this.sceneTick, deferred.line().length() > 80 ? deferred.line().substring(0, 80) : deferred.line());
this.runSceneLine(server, deferred.line());
}
}
private void clearScene(MinecraftServer server) {
this.sceneDeferred.clear();
this.sceneSender = null;
int remaining = 0;
int cleared = 0;
for (WorldServer world : server.worldServers) {
@@ -1,17 +1,38 @@
package com.gtnewhorizons.angelica.debug.flyby;
import net.minecraft.block.Block;
import net.minecraft.command.CommandBase;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.nbt.NBTBase;
import net.minecraft.nbt.NBTException;
import net.minecraft.nbt.NBTTagByteArray;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagShort;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.server.MinecraftServer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityDispenser;
import net.minecraft.util.ChunkCoordinates;
import net.minecraft.util.MathHelper;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraftforge.common.util.Constants;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -20,6 +41,7 @@ final class FlybyScene {
private static final Logger LOGGER = LogManager.getLogger("Angelica/Flyby");
private static final String[] NO_COMMANDS = new String[0];
private static final Pattern COORD_TOKEN = Pattern.compile("\\$([xyz])([+-]\\d+)?");
private static final Pattern DEFINE_TOKEN = Pattern.compile("\\$\\{(\\w+)}");
static final String MARKER = "flyby";
@@ -33,13 +55,23 @@ final class FlybyScene {
}
final String[] commands = new String[lines.size()];
final Map<String, String> defines = new HashMap<>();
int n = 0;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();
if (line.isEmpty() || line.charAt(0) == '#') continue;
if (line.charAt(0) == '/') line = line.substring(1).trim();
if (line.isEmpty()) continue;
commands[n++] = line;
if (line.startsWith("define ")) {
final String[] parts = line.split("\\s+", 3);
if (parts.length == 3) {
defines.put(parts[1], parts[2]);
} else {
LOGGER.warn("Flyby scene '{}': malformed define '{}'", path, line);
}
continue;
}
commands[n++] = substitute(line, defines);
}
if (n == commands.length) return commands;
final String[] trimmed = new String[n];
@@ -47,6 +79,19 @@ final class FlybyScene {
return trimmed;
}
private static String substitute(String line, Map<String, String> defines) {
if (line.indexOf("${") < 0) return line;
final Matcher matcher = DEFINE_TOKEN.matcher(line);
final StringBuilder sb = new StringBuilder(line.length() + 256);
while (matcher.find()) {
final String value = defines.get(matcher.group(1));
if (value == null) LOGGER.warn("Flyby scene: undefined '{}'", matcher.group());
matcher.appendReplacement(sb, Matcher.quoteReplacement(value != null ? value : matcher.group()));
}
matcher.appendTail(sb);
return sb.toString();
}
static String expand(String line, int x, int y, int z) {
if (line.indexOf('$') < 0) return line;
final Matcher matcher = COORD_TOKEN.matcher(line);
@@ -64,6 +109,153 @@ final class FlybyScene {
return sb.toString();
}
static Deferred deferred(String line) {
final String[] parts = line.split("\\s+", 3);
if (parts.length < 3) return null;
try {
final int ticks = Integer.parseInt(parts[1]);
return ticks > 0 ? new Deferred(ticks, parts[2]) : null;
} catch (NumberFormatException e) {
return null;
}
}
static boolean run(MinecraftServer server, FlybyCommandSender sender, String line) {
try {
if (line.startsWith("tile ")) return placeTile(sender, line);
if (line.startsWith("activate ")) return activate(sender, line);
if (line.startsWith("fill ")) return fill(sender, line);
} catch (NBTException | RuntimeException e) {
LOGGER.warn("Flyby scene: '{}' failed", line, e);
return false;
}
return server.getCommandManager().executeCommand(sender, line) != 0;
}
private static boolean placeTile(FlybyCommandSender sender, String line) throws NBTException {
final int brace = line.indexOf('{');
if (brace < 0) return false;
final String[] args = line.substring(0, brace).trim().split("\\s+");
if (args.length < 5 || args.length > 6 || !Block.blockRegistry.containsKey(args[4])) return false;
final ChunkCoordinates pos = position(sender, args, 1);
final int x = pos.posX, y = pos.posY, z = pos.posZ;
final Block block = Block.getBlockFromName(args[4]);
final int meta = args.length == 6 ? Integer.parseInt(args[5]) : 0;
final World world = sender.getEntityWorld();
if (!world.blockExists(x, y, z) || !block.hasTileEntity(meta)) return false;
if (!(JsonToNBT.func_150315_a(line.substring(brace)) instanceof NBTTagCompound parsed)) return false;
final NBTTagCompound tag = decode(parsed);
tag.setInteger("x", x);
tag.setInteger("y", y);
tag.setInteger("z", z);
final TileEntity tile = block.createTileEntity(world, meta);
if (tile == null) return false;
tile.readFromNBT(tag);
world.setBlock(x, y, z, block, meta, 3);
world.setTileEntity(x, y, z, tile);
return world.getTileEntity(x, y, z) == tile;
}
private static boolean activate(FlybyCommandSender sender, String line) {
final String[] args = line.split("\\s+");
final boolean sneak = args.length == 5 && "sneak".equals(args[4]);
if (args.length < 4 || args.length > 5 || (args.length == 5 && !sneak)) return false;
final ChunkCoordinates pos = position(sender, args, 1);
final int x = pos.posX, y = pos.posY, z = pos.posZ;
final EntityPlayerMP player = sender.player();
final World world = player.worldObj;
if (!world.blockExists(x, y, z)) return false;
final double playerX = player.posX;
final double playerY = player.posY;
final double playerZ = player.posZ;
final boolean sneaking = player.isSneaking();
final ItemStack[] hotbar = player.inventory.mainInventory;
final int slot = player.inventory.currentItem;
final ItemStack held = hotbar[slot];
player.posX = x + 0.5D;
player.posY = y + 1.0D;
player.posZ = z + 0.5D;
player.setSneaking(sneak);
hotbar[slot] = null;
try {
return world.getBlock(x, y, z).onBlockActivated(world, x, y, z, player, 1, 0.5F, 1.0F, 0.5F);
} finally {
player.posX = playerX;
player.posY = playerY;
player.posZ = playerZ;
player.setSneaking(sneaking);
hotbar[slot] = held;
}
}
private static boolean fill(FlybyCommandSender sender, String line) {
final String[] args = line.split("\\s+");
if (args.length < 8 || args.length > 9 || !Block.blockRegistry.containsKey(args[7])) return false;
final ChunkCoordinates from = position(sender, args, 1);
final ChunkCoordinates to = position(sender, args, 4);
final Block block = Block.getBlockFromName(args[7]);
final int meta = args.length == 9 ? Integer.parseInt(args[8]) : 0;
final World world = sender.getEntityWorld();
for (int x = Math.min(from.posX, to.posX); x <= Math.max(from.posX, to.posX); x++) {
for (int y = Math.min(from.posY, to.posY); y <= Math.max(from.posY, to.posY); y++) {
for (int z = Math.min(from.posZ, to.posZ); z <= Math.max(from.posZ, to.posZ); z++) {
if (!world.blockExists(x, y, z)) return false;
world.setBlock(x, y, z, block, meta, 3);
}
}
}
return true;
}
private static ChunkCoordinates position(FlybyCommandSender sender, String[] args, int first) {
return new ChunkCoordinates(coordinate(sender, 'x', args[first]), coordinate(sender, 'y', args[first + 1]), coordinate(sender, 'z', args[first + 2]));
}
private static int coordinate(FlybyCommandSender sender, char axis, String token) {
final ChunkCoordinates anchor = sender.getPlayerCoordinates();
final int base = switch (axis) {
case 'x' -> anchor.posX;
case 'y' -> anchor.posY;
default -> anchor.posZ;
};
return MathHelper.floor_double(CommandBase.func_110666_a(sender, base, token));
}
private static NBTTagCompound decode(NBTTagCompound source) {
final NBTTagCompound decoded = new NBTTagCompound();
final boolean itemStack = source.hasKey("Count");
for (String key : source.func_150296_c()) {
String name = key.replace(';', ':');
NBTBase value = source.getTag(key);
if (value instanceof NBTTagCompound compound) {
value = decode(compound);
} else if (value instanceof NBTTagList list && list.func_150303_d() == Constants.NBT.TAG_COMPOUND) {
final NBTTagList entries = new NBTTagList();
for (int i = 0; i < list.tagCount(); i++) {
entries.appendTag(decode(list.getCompoundTagAt(i)));
}
value = entries;
} else if (value instanceof NBTTagString string) {
final String text = string.func_150285_a_();
if (name.endsWith("#")) {
name = name.substring(0, name.length() - 1);
value = new NBTTagByteArray(text.getBytes(StandardCharsets.UTF_8));
} else if (itemStack && "id".equals(name)) {
if (!Item.itemRegistry.containsKey(text)) throw new IllegalArgumentException("Unknown item " + text);
value = new NBTTagShort((short) Item.getIdFromItem((Item) Item.itemRegistry.getObject(text)));
}
}
decoded.setTag(name, value);
}
return decoded;
}
static int count(WorldServer world) {
final List<Entity> entities = world.loadedEntityList;
int total = 0;
@@ -107,5 +299,7 @@ final class FlybyScene {
return tileEntity instanceof TileEntityDispenser dispenser && !dispenser.isInvalid() && dispenser.getClass() == TileEntityDispenser.class && MARKER.equals(dispenser.getInventoryName());
}
record Deferred(int ticks, String line) {}
private FlybyScene() {}
}
@@ -19,6 +19,7 @@ import com.gtnewhorizons.angelica.rendering.particles.ParticleInstancer;
import com.gtnewhorizons.angelica.rendering.tesr.AngelicaTesrMeshCache;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
import com.gtnewhorizons.angelica.rendering.tesr.TesrShaderPrograms;
import net.coderbot.iris.pipeline.ShadowRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.WorldRenderer;
@@ -76,7 +77,7 @@ public final class TracyFramePlots {
private static final long P_FFP_PROGRAMS = Tracy.plotHandle("ffp.programs");
private static final PlotDelta P_FFP_VARIANT_SWITCHES = new PlotDelta("ffp.variantSwitches");
private static final PlotDelta P_TESR_REBUILDS = new PlotDelta("tesr.rebuilds");
private static final PlotDelta P_TESR_REBAKES = new PlotDelta("tesr.rebakes");
private static final PlotDelta P_TESR_RETAINED_DRAWS = new PlotDelta("tesr.retainedDraws");
private static final PlotDelta P_TESR_INSTANCED_DRAWS = new PlotDelta("tesr.instancedDraws");
private static final PlotDelta P_TESR_INSTANCED_INSTANCES = new PlotDelta("tesr.instancedInstances");
@@ -88,6 +89,10 @@ public final class TracyFramePlots {
private static final long P_TESR_BUFFER_SOURCE_BYTES = Tracy.plotHandle("tesr.bufferSourceBytes", TracyBackend.PLOT_FORMAT_MEMORY);
private static final PlotDelta P_TESR_CACHE_HITS = new PlotDelta("tesr.cacheHits");
private static final PlotDelta P_TESR_CACHE_MISSES = new PlotDelta("tesr.cacheMisses");
private static final PlotDelta P_TESR_SHADER_BAKED_BINDS = new PlotDelta("tesr.shaderBakedBinds");
private static final PlotDelta P_TESR_SHADER_INSTANCED_BINDS = new PlotDelta("tesr.shaderInstancedBinds");
private static final PlotDelta P_TESR_SHADER_FALLBACKS = new PlotDelta("tesr.shaderFallbacks");
private static final PlotDelta P_TESR_SHADER_LINK_FAILURES = new PlotDelta("tesr.shaderLinkFailures");
private static final PlotDelta P_TESR_MODEL_PARTS = new PlotDelta("tesr.modelParts");
private static final PlotDelta P_ARMOR_BASE_REUSES = new PlotDelta("glint.armorBaseReuses");
private static final PlotDelta P_GLINT_PASS_REUSES = new PlotDelta("glint.passReuses");
@@ -227,7 +232,7 @@ public final class TracyFramePlots {
final TesrBatchRenderer tesr = TesrBatchRenderer.INSTANCE;
final ModelPartBatcher batcher = ModelPartBatcher.INSTANCE;
P_TESR_REBUILDS.plot(tesr.statRebuilds());
P_TESR_REBAKES.plot(tesr.statRebakes());
P_TESR_RETAINED_DRAWS.plot(tesr.statRetainedDraws());
P_TESR_INSTANCED_DRAWS.plot(tesr.statInstancedDraws() + batcher.statInstancedDraws());
P_TESR_INSTANCED_INSTANCES.plot(tesr.statInstancedInstances() + batcher.statInstancedInstances());
@@ -241,6 +246,10 @@ public final class TracyFramePlots {
}
P_TESR_CACHE_HITS.plot(AngelicaTesrMeshCache.cacheHits);
P_TESR_CACHE_MISSES.plot(AngelicaTesrMeshCache.cacheMisses);
P_TESR_SHADER_BAKED_BINDS.plot(TesrShaderPrograms.bakedBinds);
P_TESR_SHADER_INSTANCED_BINDS.plot(TesrShaderPrograms.instancedBinds);
P_TESR_SHADER_FALLBACKS.plot(TesrShaderPrograms.fallbacks);
P_TESR_SHADER_LINK_FAILURES.plot(TesrShaderPrograms.linkFailures);
P_TESR_MODEL_PARTS.plot(batcher.statParts());
P_ARMOR_BASE_REUSES.plot(batcher.statArmorBaseReuses());
P_GLINT_PASS_REUSES.plot(batcher.statGlintPassReuses());
@@ -33,6 +33,7 @@ public final class AngelicaTesrMeshCache {
public static final long LRU_TIMEOUT_MS = 60_000L;
private static final BatchRendererSink SINK = new BatchRendererSink();
public static final AngelicaTesrMeshCache INSTANCE = new AngelicaTesrMeshCache(new GtnhMeshBackend(), System::currentTimeMillis);
private static final Logger LOG = LogManager.getLogger("AngelicaTesrMeshCache");
@@ -53,7 +54,35 @@ public final class AngelicaTesrMeshCache {
}
public interface ReplaySink {
void beginScope();
void endScope();
void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material);
void retire(TemplateBuffer template);
}
private static final class BatchRendererSink implements ReplaySink {
@Override
public void beginScope() {
TesrBatchRenderer.INSTANCE.beginScope();
}
@Override
public void endScope() {
TesrBatchRenderer.INSTANCE.endScope();
}
@Override
public void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material) {
TesrBatchRenderer.INSTANCE.queue(template, texture, material);
}
@Override
public void retire(TemplateBuffer template) {
TesrBatchRenderer.INSTANCE.retire(template);
}
}
private static final class Bucket {
@@ -77,7 +106,7 @@ public final class AngelicaTesrMeshCache {
private static final Tracy.ZoneId Z_TESR_CAPTURE = Tracy.zoneId("tesrCapture", Tracy.COLOR_CLIENT);
AngelicaTesrMeshCache(MeshBackend backend, LongSupplier clock) {
this(backend, clock, (template, texture, material) -> TesrBatchRenderer.INSTANCE.queue(template, texture, material));
this(backend, clock, SINK);
}
AngelicaTesrMeshCache(MeshBackend backend, LongSupplier clock, ReplaySink replaySink) {
@@ -113,10 +142,18 @@ public final class AngelicaTesrMeshCache {
public void invalidate(Object key) {
final Entry entry = entries.remove(key);
if (entry != null) {
entry.buckets.clear();
retireBuckets(entry);
}
}
private void retireBuckets(Entry entry) {
final List<Bucket> buckets = entry.buckets;
for (int i = 0, n = buckets.size(); i < n; i++) {
replaySink.retire(buckets.get(i).template);
}
buckets.clear();
}
private Entry capture(Object key, Entry existing, TesrMeshBuilder builder) {
if (Tracy.ENABLED) Tracy.beginZone(Z_TESR_CAPTURE);
try {
@@ -132,7 +169,7 @@ public final class AngelicaTesrMeshCache {
entry = new Entry();
entries.put(key, entry);
} else {
entry.buckets.clear();
retireBuckets(entry);
}
final CaptureSink sink = new CaptureSink(entry);
builder.angelica$build(sink);
@@ -181,9 +218,14 @@ public final class AngelicaTesrMeshCache {
private void replay(Entry entry) {
final List<Bucket> buckets = entry.buckets;
for (int i = 0, n = buckets.size(); i < n; i++) {
final Bucket b = buckets.get(i);
replaySink.queue(b.template, b.texture, b.material);
replaySink.beginScope();
try {
for (int i = 0, n = buckets.size(); i < n; i++) {
final Bucket b = buckets.get(i);
replaySink.queue(b.template, b.texture, b.material);
}
} finally {
replaySink.endScope();
}
}
@@ -194,6 +236,7 @@ public final class AngelicaTesrMeshCache {
DroppedItemInstancer.clear();
BopParticleDescriptors.invalidate();
ShaderGlint.invalidate();
TesrShaderPrograms.invalidateAll();
}
}
@@ -202,6 +245,9 @@ public final class AngelicaTesrMeshCache {
}
public void clear() {
for (final Entry entry : entries.values()) {
retireBuckets(entry);
}
entries.clear();
}
@@ -215,7 +261,7 @@ public final class AngelicaTesrMeshCache {
while (it.hasNext()) {
final Entry entry = it.next().getValue();
if (entry.lastUsedMs + LRU_TIMEOUT_MS < nowMs) {
entry.buckets.clear();
retireBuckets(entry);
it.remove();
}
}
@@ -25,7 +25,7 @@ public final class DrawState {
public static final int ADDITIVE_ALPHA = 3;
public static final int GLINT = 4;
private static final int LIGHTMAP_UNIT = 1;
static final int LIGHTMAP_UNIT = 1;
private static final ObjectOpenCustomHashSet<DrawState> CACHE = new ObjectOpenCustomHashSet<>(HashStrategy.INSTANCE);
@@ -33,10 +33,6 @@ public final class DrawState {
CACHE.clear();
}
static int internedStateCount() {
return CACHE.size();
}
@Getter private final int cull;
@Getter private final int blend;
@Getter private final boolean depthEqual;
@@ -114,6 +110,10 @@ public final class DrawState {
return cull != DISABLED && polygon.getFrontFace() == GL11.GL_CW ? cull | FRONT_FACE_CLOCKWISE : cull;
}
public static int liveCull(TesrMaterial material) {
return material.isNoCull() ? DISABLED : liveCull();
}
public static boolean liveLit(TesrMaterial material) {
return GLStateManager.getLightingState().isEnabled() && !material.isUnlit();
}
@@ -56,6 +56,7 @@ final class InstancedTemplateRenderer {
private final Reference2ObjectOpenHashMap<TemplateBuffer, TemplateMesh> meshes = new Reference2ObjectOpenHashMap<>();
private final ObjectArrayList<Bucket> bucketPool = new ObjectArrayList<>();
private final ObjectArrayList<Bucket> liveBuckets = new ObjectArrayList<>();
private final ObjectArrayList<TemplateBuffer> retired = new ObjectArrayList<>();
private final InstanceRing ring;
private ByteBuffer staging;
@@ -314,6 +315,18 @@ final class InstancedTemplateRenderer {
ring.postDraw();
}
void retire(TemplateBuffer template) {
retired.add(template);
}
void releaseRetired() {
for (int i = 0, n = retired.size(); i < n; i++) {
final TemplateMesh mesh = meshes.remove(retired.get(i));
if (mesh != null) delete(mesh);
}
retired.clear();
}
void sweep(long now) {
if (meshes.isEmpty()) return;
final ObjectIterator<TemplateMesh> it = meshes.values().iterator();
@@ -330,6 +343,7 @@ final class InstancedTemplateRenderer {
delete(mesh);
}
meshes.clear();
retired.clear();
liveBuckets.clear();
bucketPool.clear();
staging = null;
@@ -462,7 +462,7 @@ public final class ModelPartBatcher {
if (!canQueueGlint() || GLStateManager.getActiveTextureUnit() != 0 || !activeGroups.canCopyInstances(EntityMaterials.ITEM_GLINT) || !MatrixHelper.isIdentity(GLStateManager.getTextures().getTextureUnitMatrix(0))) return false;
final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL);
final PolygonState polygon = GLStateManager.getPolygonState();
final int cullCode = EntityMaterials.ITEM_GLINT.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final int cullCode = DrawState.liveCull(EntityMaterials.ITEM_GLINT);
final RenderLayer layer = layerFor(HeldItemGlint.texture(), EntityMaterials.ITEM_GLINT, PassOverride.NONE, offset ? polygon.getOffsetFactor() : 0, offset ? polygon.getOffsetUnits() : 0, ShaderGlint.NO_TINT, cullCode, false, DrawState.liveLightmap());
activeGroups.queueGlintLayers(template, layer, EntityMaterials.ITEM_GLINT, currentModelView(), GLSMConfig.packedLastBrightness(), VANILLA_GLINT_COLOR, currentOverlay(), currentEntityInfo(0),
HeldItemGlint.firstMatrix(), HeldItemGlint.secondMatrix());
@@ -506,7 +506,7 @@ public final class ModelPartBatcher {
private void setArmorGlint(ResourceLocation texture, int glintSlot, boolean lit, Matrix4f textureMatrix, int color) {
final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL);
final PolygonState polygon = GLStateManager.getPolygonState();
final int cullCode = EntityMaterials.GLINT.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final int cullCode = DrawState.liveCull(EntityMaterials.GLINT);
armorGlintLayer = layerFor(texture, EntityMaterials.GLINT, PassOverride.NONE, offset ? polygon.getOffsetFactor() : 0, offset ? polygon.getOffsetUnits() : 0, glintSlot, cullCode, lit, DrawState.liveLightmap());
armorGlintTexture.set(textureMatrix);
armorGlintColor = color;
@@ -630,7 +630,7 @@ public final class ModelPartBatcher {
final float offsetFactor = offset ? polygon.getOffsetFactor() : 0.0f;
final float offsetUnits = offset ? polygon.getOffsetUnits() : 0.0f;
final int glintSlot = material.special() == TesrMaterial.SpecialRender.GLINT ? lastBoundGlintSlot : ShaderGlint.NO_TINT;
final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final int cullCode = DrawState.liveCull(material);
final boolean lit = DrawState.liveLit(material);
final EntityTextureAtlas.Slot atlasSlot = texMatrix == null && cubesInsideTexture ? atlasSlotFor(material) : null;
final RenderLayer layer = layerFor(atlasSlot != null ? Objects.requireNonNull(atlasSlot.page).location : texture, material, pass, offsetFactor, offsetUnits, glintSlot, cullCode, lit, DrawState.liveLightmap(), afterGlint);
@@ -641,7 +641,7 @@ public final class ModelPartBatcher {
final int overlayABGR = currentOverlay();
final long entityInfo = currentEntityInfo(CapturedRenderingState.INSTANCE.getCurrentRenderedItem());
final int packedLight = GLSMConfig.packedLastBrightness();
activeGroups.queue(template, cubes, scale, layer, material, currentModelView(), packedLight, colorABGR, overlayABGR, entityInfo, entityId, texMatrix, atlasSlot);
activeGroups.queue(0L, template, cubes, scale, layer, material, currentModelView(), packedLight, colorABGR, overlayABGR, entityInfo, entityId, texMatrix, atlasSlot);
parts++;
return true;
}
@@ -698,7 +698,7 @@ public final class ModelPartBatcher {
final PassOverride pass = shaderPipeline != null ? PassOverride.capture() : PassOverride.NONE;
final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL);
final PolygonState polygon = GLStateManager.getPolygonState();
final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final int cullCode = DrawState.liveCull(material);
final RenderLayer layer = layerFor(lastBoundLocation, material, pass, offset ? polygon.getOffsetFactor() : 0.0f, offset ? polygon.getOffsetUnits() : 0.0f,
ShaderGlint.NO_TINT, cullCode, DrawState.liveLit(material), DrawState.liveLightmap());
// Flames carry entity_flame, which some packs may voxelize as a light source in the shadow pass
@@ -31,13 +31,16 @@ import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
static final int PROMOTE_AFTER_REBUILDS = 3;
static final int DEMOTE_AFTER_STABLE_FRAMES = 4;
static final int PROMOTE_AFTER_REBAKES = 3;
static final long PROMOTE_WINDOW_MS = 500;
static final int PROMOTE_MAX_TEMPLATES = 8;
static final long STABILITY_PROBE_MS = 250;
static final int DEMOTE_AFTER_STABLE_PROBES = 2;
static final long IDLE_RESET_FRAMES = 100;
static final long GROUP_TTL_MS = AngelicaTesrMeshCache.LRU_TIMEOUT_MS;
static final int MERGE_SCAN = 4;
private static final Tracy.ZoneId Z_TESR_REBUILD = Tracy.zoneId("tesrRebuild", Tracy.COLOR_CLIENT);
private static final Tracy.ZoneId Z_TESR_REBAKE = Tracy.zoneId("tesrRebake", Tracy.COLOR_CLIENT);
private final AngelicaBufferSource source;
private final LongSupplier clock;
@@ -52,11 +55,12 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
private final Matrix4f savedTexMatrix = new Matrix4f();
private boolean texApplied;
private final float[] matScratch = new float[16];
private final TemplateBuffer[] budgetSeen = new TemplateBuffer[PROMOTE_MAX_TEMPLATES];
private double camX, camY, camZ;
private long frameMark;
GlintCapture glintCapture;
long rebuilds;
long rebakes;
long retainedDraws;
long streamPromotions;
long streamedInstances;
@@ -184,6 +188,171 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
}
}
static final class Snapshot {
final ObjectArrayList<TemplateBuffer> templates = new ObjectArrayList<>();
final IntArrayList lights = new IntArrayList();
final IntArrayList colors = new IntArrayList();
float[] matrices = new float[16];
float[] texMatrices = new float[TesrAnchorMath.TEX_MATRIX_TERMS];
int size;
long hashSum;
boolean matches(int i, TemplateBuffer template, float[] matrix, int offset, int light, int color, Matrix4f texMatrix) {
return i < size && templates.get(i) == template && lights.getInt(i) == light && colors.getInt(i) == color
&& TesrAnchorMath.sameTransform(matrices, i * 16, matrix, offset)
&& TesrAnchorMath.sameTexMatrix(texMatrices, i * TesrAnchorMath.TEX_MATRIX_TERMS, texMatrix);
}
private void ensure(int instances) {
if (instances * 16 <= matrices.length) return;
matrices = Arrays.copyOf(matrices, Math.max(instances * 16, matrices.length << 1));
texMatrices = Arrays.copyOf(texMatrices, matrices.length / 16 * TesrAnchorMath.TEX_MATRIX_TERMS);
}
boolean exchange(int i, TemplateBuffer template, float[] matrix, int light, int color, Matrix4f texMatrix) {
final boolean same = matches(i, template, matrix, 0, light, color, texMatrix);
ensure(i + 1);
System.arraycopy(matrix, 0, matrices, i * 16, 16);
if (i < templates.size()) {
templates.set(i, template);
lights.set(i, light);
colors.set(i, color);
} else {
templates.add(template);
lights.add(light);
colors.add(color);
}
TesrAnchorMath.storeTexMatrix(texMatrices, i * TesrAnchorMath.TEX_MATRIX_TERMS, texMatrix);
return same;
}
void reset() {
templates.clear();
lights.clear();
colors.clear();
size = 0;
}
boolean sameTemplates(InstanceColumns cols) {
if (cols.size != size) return false;
for (int i = 0; i < size; i++) {
if (templates.get(i) != cols.templates.get(i)) return false;
}
return true;
}
void copyFrom(InstanceColumns cols, ObjectArrayList<Matrix4f> instTexMatrices, long hashSum) {
final int n = cols.size;
ensure(n);
System.arraycopy(cols.matrices, 0, matrices, 0, n * 16);
templates.clear();
templates.addAll(cols.templates);
lights.clear();
lights.addElements(0, cols.lights.elements(), 0, n);
colors.clear();
colors.addElements(0, cols.colors.elements(), 0, n);
for (int i = 0; i < n; i++) {
TesrAnchorMath.storeTexMatrix(texMatrices, i * TesrAnchorMath.TEX_MATRIX_TERMS, instTexMatrices.get(i));
}
size = n;
this.hashSum = hashSum;
}
}
static final class Probe {
final Snapshot snapshot = new Snapshot();
boolean probing;
int count;
long hashSum;
boolean ordered;
int lastCount = -1;
long lastHashSum;
int stableProbes;
long lastMs;
void start(long nowMs) {
probing = true;
lastMs = nowMs;
count = 0;
hashSum = 0;
ordered = true;
}
void record(TemplateBuffer template, float[] matrix, int light, int color, Matrix4f texMatrix, long hash) {
ordered &= snapshot.exchange(count++, template, matrix, light, color, texMatrix);
hashSum += hash;
}
boolean finish() {
final boolean stable = count == lastCount && (ordered || hashSum == lastHashSum);
lastCount = count;
lastHashSum = hashSum;
snapshot.size = count;
stableProbes = stable ? stableProbes + 1 : 0;
return stableProbes >= DEMOTE_AFTER_STABLE_PROBES;
}
void reset() {
stableProbes = 0;
lastCount = -1;
snapshot.reset();
probing = false;
}
}
static final class Anchor {
long x, y, z;
void set(Anchor other) {
x = other.x;
y = other.y;
z = other.z;
}
boolean sameAs(Anchor other) {
return x == other.x && y == other.y && z == other.z;
}
boolean stale(double camX, double camY, double camZ) {
return TesrAnchorMath.shouldReanchor(camX, camY, camZ, x, y, z);
}
void moveTo(double camX, double camY, double camZ) {
x = TesrAnchorMath.anchorCoord(camX);
y = TesrAnchorMath.anchorCoord(camY);
z = TesrAnchorMath.anchorCoord(camZ);
}
}
static final class DepthEqualSet {
final ObjectArrayList<Group> members = new ObjectArrayList<>();
int depthEqualMembers;
int streamMembers;
boolean instanced;
long effectiveFrame = -1;
long checkedFrame = -1;
final Anchor anchor = new Anchor();
long anchorFrame = -1;
void add(Group group) {
members.add(group);
if (group.material.isDepthEqual()) depthEqualMembers++;
if (group.stream) streamMembers++;
group.depthEqualSet = this;
}
void remove(Group group) {
members.remove(group);
if (group.material.isDepthEqual()) depthEqualMembers--;
if (group.stream) streamMembers--;
}
void clearPath() {
instanced = false;
effectiveFrame = -1;
}
}
static final class Group {
final RenderLayer layer;
final TesrMaterial material;
@@ -191,9 +360,11 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
final boolean opaque;
final boolean stream;
final boolean mergeRuns;
final boolean couplable;
long coupledScope;
Class<?> renderableClass;
int rebuildsWindow;
long anchorX, anchorY, anchorZ;
int rebakesWindow;
final Anchor anchor = new Anchor();
boolean anchored;
long frameMark = -1;
long lastUsedMs;
@@ -208,16 +379,18 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
private int texMatrixPoolUsed;
private final ObjectArrayList<TexRun> runPool = new ObjectArrayList<>();
private int runPoolUsed;
long hashAcc;
final Snapshot snapshot = new Snapshot();
boolean dirty;
DepthEqualSet depthEqualSet;
final TesrShaderPrograms.Entry shaderEntry;
long builtHash;
int builtCount = -1;
int builtVertexCount;
boolean streaming;
int consecutiveRebuilds;
long frameHash;
long prevFrameHash;
int stableFrames;
boolean streamPath;
long promoteWindowStartMs;
int promoteWindowRebakes;
boolean queuedThisFrame;
Probe probe;
final MeshBuffer mesh = new MeshBuffer();
ByteBuffer scratch;
int meshBytes;
@@ -362,12 +535,17 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
this.opaque = material.transparency() == TesrMaterial.Transparency.OPAQUE;
this.stream = material.isStream();
this.mergeRuns = material.isDepthEqual() && material.isNoDepthWrite();
this.streaming = stream;
this.streamPath = stream;
this.couplable = material.isDepthEqual() || !material.isNoDepthWrite();
this.shaderEntry = TesrShaderPrograms.entryFor(material);
}
}
private final Reference2ObjectOpenHashMap<RenderLayer, Reference2ObjectOpenHashMap<TesrMaterial, Object2ObjectOpenHashMap<DrawStateKey, Group>>> groups = new Reference2ObjectOpenHashMap<>();
private final Reference2ObjectOpenHashMap<RenderLayer, ObjectArrayList<Group>> byLayer = new Reference2ObjectOpenHashMap<>();
private long openScope;
private DepthEqualSet openDepthEqualSet;
private final ObjectArrayList<Group> openWriters = new ObjectArrayList<>();
private static final int RECENT_GROUPS = 4;
private final Group[] recentGroups = new Group[RECENT_GROUPS];
private int recentGroupNext;
@@ -438,22 +616,27 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
private final DrawStateKey lookupState = new DrawStateKey();
private Group groupFor(RenderLayer layer, TesrMaterial material, int id) {
final boolean perInstance = instanceableMaterial(material);
final boolean entityKind = !perInstance && source.effectiveIdKind() == AngelicaBufferSource.GroupIdKind.ENTITY;
final CapturedRenderingState captured = CapturedRenderingState.INSTANCE;
final int entity = perInstance ? 0 : entityKind ? id : captured.getCurrentRenderedEntity();
final int block = perInstance ? 0 : entityKind ? captured.getCurrentRenderedBlockEntity() : id;
final int item = perInstance ? 0 : captured.getCurrentRenderedItem();
final int color = perInstance ? 0 : AngelicaBufferSource.packEntityColor(captured.getCurrentEntityColor());
Group group = null;
boolean keyed = false;
boolean perInstance = false;
for (int i = 0; i < RECENT_GROUPS; i++) {
final Group recent = recentGroups[i];
if (recent != null && recent.layer == layer && recent.material == material && recent.state.matches(entity, block, item, color)) {
if (recent == null || recent.layer != layer || recent.material != material) continue;
if (!keyed) {
keyed = true;
perInstance = recent.stream && canInstance(recent);
keyLookup(perInstance, id);
}
if (recent.state.equals(lookupState)) {
group = recent;
break;
}
}
if (group == null) {
if (!keyed) {
perInstance = streamsInstanced(material);
keyLookup(perInstance, id);
}
Reference2ObjectOpenHashMap<TesrMaterial, Object2ObjectOpenHashMap<DrawStateKey, Group>> byMaterial = groups.get(layer);
if (byMaterial == null) {
byMaterial = new Reference2ObjectOpenHashMap<>();
@@ -464,9 +647,9 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
byState = new Object2ObjectOpenHashMap<>();
byMaterial.put(material, byState);
}
group = byState.get(lookupState.set(entity, block, item, color));
group = byState.get(lookupState);
if (group == null) {
final DrawStateKey key = new DrawStateKey().set(entity, block, item, color);
final DrawStateKey key = new DrawStateKey().set(lookupState.entity, lookupState.block, lookupState.item, lookupState.color);
group = new Group(layer, material, perInstance ? 0 : id, key);
group.lastUsedMs = nowMs;
byState.put(key, group);
@@ -481,6 +664,17 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
return group;
}
private void keyLookup(boolean perInstance, int id) {
if (perInstance) {
lookupState.set(0, 0, 0, 0);
return;
}
final CapturedRenderingState captured = CapturedRenderingState.INSTANCE;
final boolean entityKind = source.effectiveIdKind() == AngelicaBufferSource.GroupIdKind.ENTITY;
lookupState.set(entityKind ? id : captured.getCurrentRenderedEntity(), entityKind ? captured.getCurrentRenderedBlockEntity() : id,
captured.getCurrentRenderedItem(), AngelicaBufferSource.packEntityColor(captured.getCurrentEntityColor()));
}
void forceStreaming(RenderLayer layer, TesrMaterial material, int blockEntityId) {
promote(groupFor(layer, material, blockEntityId));
}
@@ -491,21 +685,27 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
streamedInstances++;
}
void queue(TemplateBuffer template, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix) {
queue(template, null, 1.0f, layer, material, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, id, texMatrix, null);
void queue(long scope, TemplateBuffer template, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix) {
queue(scope, template, null, 1.0f, layer, material, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, id, texMatrix, null);
}
boolean instancesCubes(TesrMaterial material) {
return instanceableMaterial(material) && glintCapture == null;
return streamsInstanced(material) && material.shader() == null && glintCapture == null;
}
void queue(TemplateBuffer template, CubeParams[] cubes, float scale, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix, EntityTextureAtlas.Slot atlasSlot) {
void queue(long scope, TemplateBuffer template, CubeParams[] cubes, float scale, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix, EntityTextureAtlas.Slot atlasSlot) {
final Group group = groupFor(layer, material, id);
touch(group, layer);
if (group.stream) {
final boolean instanceable = instanceable(group);
if (group.couplable) linkDepthScope(group, scope);
group.queuedThisFrame = true;
if (group.streamPath) {
final boolean instancedPath = drawsInstanced(group);
final boolean segments = !instancedPath && template.drawMode == layer.getDrawMode();
if (group.probe != null && group.probe.probing && (instancedPath || segments)) {
probe(group, template, currentMV, packedLight, colorABGR, texMatrix);
}
final boolean capturingBase = glintCapture != null && glintCapture.capturesBase();
if (instanceable && cubes != null) {
if (instancedPath && cubes != null && material.shader() == null) {
final TexRun run = group.openOrExtendRun(group.cubeColumns, texMatrix, false, capturingBase && group.opaque);
group.cubeColumns.addCubes(deferred != null ? template : null, cubes, scale, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, atlasSlot);
run.last.end = group.cubeColumns.size;
@@ -514,7 +714,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
if (glintCapture != null) glintCapture.record(group, group.cubeColumns, run, group.cubeColumns.size - 1, group.cubeColumns.size, cubes.length);
return;
}
if (instanceable) {
if (instancedPath) {
final TexRun run = group.openOrExtendRun(group.templateColumns, texMatrix, false, capturingBase && group.opaque);
group.templateColumns.add(template, currentMV, packedLight, colorABGR, overlayABGR, entityInfo);
run.last.end = group.templateColumns.size;
@@ -523,32 +723,168 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
if (glintCapture != null) glintCapture.record(group, group.templateColumns, run, group.templateColumns.size - 1, group.templateColumns.size, 1);
return;
}
if (glintCapture != null) glintCapture.invalidate();
if (template.drawMode == layer.getDrawMode()) {
if (group.stream && glintCapture != null) glintCapture.invalidate();
if (segments) {
source.getBuffer(layer, id).addTemplateInstance(template, currentMV, scratchVec, colorABGR, packedLight, texMatrix);
streamedInstances++;
return;
}
if (!group.stream) {
final DepthEqualSet set = activeDepthEqualSet(group);
if (set != null) {
switchSetNextFrame(set, false);
} else {
resetPromotion(group);
}
}
}
scratchMat.set(baseMVInv).mul(currentMV);
TesrAnchorMath.toAnchorRelative(scratchMat, camX, camY, camZ, group.anchorX, group.anchorY, group.anchorZ);
long hash = TesrAnchorMath.instanceHash(System.identityHashCode(template), scratchMat, packedLight, colorABGR);
if (texMatrix != null) {
hash += TesrAnchorMath.texMatrixHash(texMatrix);
toAnchorRelative(group, currentMV);
final Matrix4f bakedTexMatrix = atlasSlot != null ? atlasSlot.uvMatrix : texMatrix;
final InstanceColumns cols = group.templateColumns;
final int index = cols.size;
cols.add(template, scratchMat, packedLight, colorABGR, overlayABGR, entityInfo);
group.addTexMatrix(bakedTexMatrix);
if (!group.dirty && !group.snapshot.matches(index, template, cols.matrices, index * 16, packedLight, colorABGR, bakedTexMatrix)) {
group.dirty = true;
}
if (group.streaming && template.drawMode == layer.getDrawMode()) {
group.frameHash += hash;
source.getBuffer(layer, id).addTemplateInstance(template, currentMV, scratchVec, colorABGR, packedLight, texMatrix);
streamedInstances++;
}
private void linkDepthScope(Group group, long scope) {
if (scope == 0) return;
if (scope != openScope) {
openScope = scope;
openDepthEqualSet = null;
openWriters.clear();
}
if (group.coupledScope == scope) return;
group.coupledScope = scope;
if (openDepthEqualSet == null && !group.material.isDepthEqual()) {
openWriters.add(group);
return;
}
group.hashAcc += hash;
group.templateColumns.add(template, scratchMat, packedLight, colorABGR, overlayABGR, entityInfo);
group.addTexMatrix(texMatrix);
openDepthEqualSet = join(openDepthEqualSet, group);
for (int i = 0, n = openWriters.size(); i < n; i++) {
openDepthEqualSet = join(openDepthEqualSet, openWriters.get(i));
}
openWriters.clear();
if (!group.queuedThisFrame) adopt(group, openDepthEqualSet.instanced);
}
private DepthEqualSet join(DepthEqualSet set, Group group) {
final DepthEqualSet own = group.depthEqualSet;
if (own == null) {
if (set == null) {
set = new DepthEqualSet();
set.anchor.set(group.anchor);
}
set.add(group);
joinAnchor(set, group);
settle(set, set.instanced || promoted(group));
return set;
}
if (set == null || set == own) return own;
final DepthEqualSet survivor = set.members.size() >= own.members.size() ? set : own;
final DepthEqualSet absorbed = survivor == set ? own : set;
final boolean instanced = wantsInstanced(survivor) || wantsInstanced(absorbed);
for (int i = 0, n = absorbed.members.size(); i < n; i++) {
final Group member = absorbed.members.get(i);
survivor.add(member);
joinAnchor(survivor, member);
}
settle(survivor, instanced);
absorbed.members.clear();
absorbed.depthEqualMembers = 0;
absorbed.streamMembers = 0;
absorbed.clearPath();
if (openDepthEqualSet == absorbed) openDepthEqualSet = survivor;
return survivor;
}
private void joinAnchor(DepthEqualSet set, Group group) {
if (group.frameMark != frameMark) return;
if (!group.queuedThisFrame) {
adoptAnchor(group, set);
} else if (set.anchorFrame < 0) {
set.anchor.set(group.anchor);
set.anchorFrame = frameMark;
for (int i = 0, n = set.members.size(); i < n; i++) {
final Group member = set.members.get(i);
if (member.frameMark == frameMark && !member.queuedThisFrame) adoptAnchor(member, set);
}
}
}
private static void adoptAnchor(Group group, DepthEqualSet set) {
if (group.anchored && group.anchor.sameAs(set.anchor)) return;
group.anchor.set(set.anchor);
group.anchored = true;
group.builtCount = -1;
}
private static boolean promoted(Group group) {
return group.streamPath && !group.stream;
}
private static boolean wantsInstanced(DepthEqualSet set) {
if (set.depthEqualMembers > 0) return set.instanced;
for (int i = 0, n = set.members.size(); i < n; i++) {
if (promoted(set.members.get(i))) return true;
}
return false;
}
private void settle(DepthEqualSet set, boolean wantInstanced) {
switchSetNextFrame(set, (wantInstanced || set.streamMembers > 0) && allCanInstance(set));
}
private void refresh(DepthEqualSet set) {
if (set.checkedFrame == frameMark) return;
set.checkedFrame = frameMark;
if (set.anchorFrame != frameMark && set.anchor.stale(camX, camY, camZ)) set.anchor.moveTo(camX, camY, camZ);
set.anchorFrame = frameMark;
if ((set.streamMembers > 0 || set.instanced) && set.instanced != allCanInstance(set)) {
set.instanced = !set.instanced;
set.effectiveFrame = frameMark - 1;
}
}
private void adopt(Group group, boolean instanced) {
if (group.streamPath == instanced) return;
if (instanced) {
promote(group);
} else {
group.streamPath = false;
clearPromotionTracking(group);
}
}
private void toAnchorRelative(Group group, Matrix4f currentMV) {
scratchMat.set(baseMVInv).mul(currentMV);
TesrAnchorMath.toAnchorRelative(scratchMat, camX, camY, camZ, group.anchor.x, group.anchor.y, group.anchor.z);
}
private void probe(Group group, TemplateBuffer template, Matrix4f currentMV, int packedLight, int colorABGR, Matrix4f texMatrix) {
toAnchorRelative(group, currentMV);
scratchMat.get(matScratch);
group.probe.record(template, matScratch, packedLight, colorABGR, texMatrix, instanceHash(template, scratchMat, packedLight, colorABGR, texMatrix));
}
private static long instanceHash(TemplateBuffer template, Matrix4f anchorRelative, int packedLight, int colorABGR, Matrix4f texMatrix) {
return TesrAnchorMath.instanceHash(System.identityHashCode(template), anchorRelative, packedLight, colorABGR) ^ TesrAnchorMath.texMatrixHash(texMatrix);
}
private long hashSum(Group group) {
final InstanceColumns cols = group.templateColumns;
long sum = 0;
for (int i = 0, n = cols.size; i < n; i++) {
scratchMat.set(cols.matrices, i * 16);
sum += instanceHash(cols.templates.get(i), scratchMat, cols.lights.getInt(i), cols.colors.getInt(i), group.instTexMatrices.get(i));
}
return sum;
}
boolean canCopyInstances(TesrMaterial material) {
return material.isStream() && instanceableMaterial(material);
return material.isStream() && streamsInstanced(material);
}
void copyInstances(InstanceColumns src, int start, int end, RenderLayer layer, TesrMaterial material, int packedLight, int colorABGR, int overlayABGR, Matrix4f texMatrix) {
@@ -620,33 +956,40 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
private void touch(Group group, RenderLayer layer) {
if (group.frameMark != frameMark) {
final DepthEqualSet set = activeDepthEqualSet(group);
if (set != null) refresh(set);
if (frameMark - group.frameMark > IDLE_RESET_FRAMES) {
group.streaming = group.stream;
group.consecutiveRebuilds = 0;
group.stableFrames = 0;
group.builtCount = -1;
} else if (group.streaming && group.opaque && !group.stream) {
if (group.frameHash == group.prevFrameHash) {
if (++group.stableFrames >= DEMOTE_AFTER_STABLE_FRAMES) {
group.streaming = false;
group.consecutiveRebuilds = 0;
group.stableFrames = 0;
group.builtCount = -1;
resetPromotion(group);
} else if (promoted(group)) {
final boolean pinned = set != null && set.streamMembers > 0;
final Probe probe = group.probe;
if (probe.probing && probe.finish()) {
if (set == null) {
resetPromotion(group);
} else if (!pinned && set.instanced && allStable(set)) {
switchSetNextFrame(set, false);
}
} else {
group.stableFrames = 0;
}
group.prevFrameHash = group.frameHash;
group.frameHash = 0;
if (group.streamPath && !pinned && nowMs - probe.lastMs >= STABILITY_PROBE_MS) {
probe.start(nowMs);
} else {
probe.probing = false;
}
}
if (set != null) {
if (frameMark > set.effectiveFrame) adopt(group, set.instanced);
} else if (group.stream && !group.streamPath) {
resetPromotion(group);
}
group.queuedThisFrame = false;
group.frameMark = frameMark;
group.lastUsedMs = nowMs;
group.hashAcc = 0;
group.dirty = false;
group.clearInstances();
if (!group.anchored || TesrAnchorMath.shouldReanchor(camX, camY, camZ, group.anchorX, group.anchorY, group.anchorZ)) {
group.anchorX = TesrAnchorMath.anchorCoord(camX);
group.anchorY = TesrAnchorMath.anchorCoord(camY);
group.anchorZ = TesrAnchorMath.anchorCoord(camZ);
if (set != null) {
adoptAnchor(group, set);
} else if (!group.anchored || group.anchor.stale(camX, camY, camZ)) {
group.anchor.moveTo(camX, camY, camZ);
group.anchored = true;
group.builtCount = -1;
}
@@ -654,6 +997,43 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
}
}
private void switchSetNextFrame(DepthEqualSet set, boolean instanced) {
if (set.instanced == instanced) return;
set.instanced = instanced;
set.effectiveFrame = frameMark;
}
private static DepthEqualSet activeDepthEqualSet(Group group) {
final DepthEqualSet set = group.depthEqualSet;
return set != null && set.depthEqualMembers > 0 ? set : null;
}
private boolean allStable(DepthEqualSet set) {
for (int i = 0, n = set.members.size(); i < n; i++) {
final Group member = set.members.get(i);
if (member.streamPath && frameMark - member.frameMark <= IDLE_RESET_FRAMES && (member.probe == null || member.probe.stableProbes < DEMOTE_AFTER_STABLE_PROBES)) return false;
}
return true;
}
private boolean allCanInstance(DepthEqualSet set) {
for (int i = 0, n = set.members.size(); i < n; i++) {
if (!canInstance(set.members.get(i))) return false;
}
return true;
}
private static void resetPromotion(Group group) {
group.streamPath = group.stream;
clearPromotionTracking(group);
}
private static void clearPromotionTracking(Group group) {
group.promoteWindowRebakes = 0;
if (group.probe != null) group.probe.reset();
group.builtCount = -1;
}
@Override
public boolean hasDraws(RenderLayer layer) {
final ObjectArrayList<Group> list = byLayer.get(layer);
@@ -671,7 +1051,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
if (list == null) return;
boolean matrixAvailable = true;
boolean cubeAvailable = true;
if (deferred != null && anyInstanceable(list)) {
if (deferred != null && anyQueuedInstanced(list)) {
deferred.rebindCurrentPass();
matrixAvailable = deferred.hasInstancedVariant(Instancing.TEMPLATE);
cubeAvailable = deferred.hasInstancedVariant(Instancing.CUBE);
@@ -679,8 +1059,19 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
final int need = drawUninstanced(list, matrixAvailable, cubeAvailable);
restoreMV();
if ((need & NEED_MATRIX) != 0) {
if (deferred != null) deferred.bindInstancedVariant(Instancing.TEMPLATE);
drawMatrixVariant(list, cubeAvailable);
boolean bound = true;
if (deferred != null) {
deferred.bindInstancedVariant(Instancing.TEMPLATE);
} else {
final TesrShaderPrograms.Entry layerShader = list.get(0).shaderEntry;
bound = layerShader == null || TesrShaderPrograms.bindInstanced(layerShader);
}
if (bound) {
drawMatrixVariant(list, cubeAvailable);
} else {
drawMatrixCpu(list, cubeAvailable);
restoreMV();
}
}
if ((need & NEED_CUBE) != 0) {
if (deferred != null) deferred.bindInstancedVariant(Instancing.CUBE);
@@ -698,10 +1089,14 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
return g.frameMark == frameMark && (g.templateColumns.hasInstances() || g.cubeColumns.hasInstances());
}
private boolean anyInstanceable(ObjectArrayList<Group> list) {
private static boolean queuedInstanced(Group g) {
return !g.templateColumns.runs.isEmpty() || !g.cubeColumns.runs.isEmpty();
}
private boolean anyQueuedInstanced(ObjectArrayList<Group> list) {
for (int i = 0, n = list.size(); i < n; i++) {
final Group group = list.get(i);
if (live(group) && instanceable(group)) return true;
if (live(group) && queuedInstanced(group)) return true;
}
return false;
}
@@ -711,7 +1106,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
for (int i = 0, n = list.size(); i < n; i++) {
final Group group = list.get(i);
if (!live(group)) continue;
if (!instanceable(group)) {
if (!queuedInstanced(group)) {
drawRetained(group);
continue;
}
@@ -738,7 +1133,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
private void drawMatrixVariant(ObjectArrayList<Group> list, boolean cubeAvailable) {
for (int i = 0, n = list.size(); i < n; i++) {
final Group group = list.get(i);
if (!live(group) || !instanceable(group)) continue;
if (!live(group) || !queuedInstanced(group)) continue;
if (group.templateColumns.hasInstances()) {
final boolean fixedFunction = deferred == null && GLStateManager.getActiveProgram() == 0;
drawTemplateRuns(group.templateColumns, group.material.special() == TesrMaterial.SpecialRender.GLINT && fixedFunction, group.material == EntityMaterials.ITEM_GLINT);
@@ -750,38 +1145,106 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
restoreTexMatrix();
}
private void drawMatrixCpu(ObjectArrayList<Group> list, boolean cubeAvailable) {
for (int i = 0, n = list.size(); i < n; i++) {
final Group group = list.get(i);
if (!live(group) || !queuedInstanced(group)) continue;
if (group.templateColumns.hasInstances()) drawInstancesCpu(group, group.templateColumns);
if (!cubeAvailable && group.cubeColumns.hasInstances()) drawInstancesCpu(group, group.cubeColumns);
}
}
private void drawCubeVariant(ObjectArrayList<Group> list) {
for (int i = 0, n = list.size(); i < n; i++) {
final Group group = list.get(i);
if (!live(group) || !instanceable(group) || !group.cubeColumns.hasInstances()) continue;
if (!live(group) || !queuedInstanced(group) || !group.cubeColumns.hasInstances()) continue;
drawCubeInstances(group);
}
restoreTexMatrix();
}
private boolean instanceable(Group group) {
return instanceableMaterial(group.material);
private boolean drawsInstanced(Group group) {
return group.streamPath && canInstance(group);
}
private boolean instanceableMaterial(TesrMaterial material) {
return material.isStream() && instancedActive && (deferred == null || material.shader() == null);
private boolean streamsInstanced(TesrMaterial material) {
return material.isStream() && instancedActive && instancedShaderReady(TesrShaderPrograms.entryFor(material));
}
private boolean canInstance(Group group) {
return instancedActive && instancedShaderReady(group.shaderEntry);
}
private boolean instancedShaderReady(TesrShaderPrograms.Entry entry) {
return entry == null || deferred != null || TesrShaderPrograms.instancedAvailable(entry);
}
private void drawRetained(Group group) {
if (group.builtCount != group.templateColumns.size || group.builtHash != group.hashAcc || !group.mesh.isUploaded()) {
if (group.mesh.isUploaded()) {
group.consecutiveRebuilds++;
final InstanceColumns cols = group.templateColumns;
final Snapshot snapshot = group.snapshot;
boolean instanceChanged = false;
if (group.builtCount < 0 || !group.mesh.isUploaded()) {
rebake(group, hashSum(group));
} else if (group.dirty || group.builtCount != cols.size) {
final long sum = hashSum(group);
final boolean sameShape = snapshot.sameTemplates(cols);
final boolean reorderedOnly = sameShape && sum == snapshot.hashSum && group.material.transparency() != TesrMaterial.Transparency.TRANSLUCENT;
if (reorderedOnly) {
snapshot.copyFrom(cols, group.instTexMatrices, sum);
} else {
instanceChanged = sameShape && sum != snapshot.hashSum;
rebake(group, sum);
}
rebuild(group);
} else {
group.consecutiveRebuilds = 0;
}
draw(group);
if (group.consecutiveRebuilds >= PROMOTE_AFTER_REBUILDS && !group.streaming && group.opaque) {
promote(group);
if (instanceChanged && !group.stream && !group.streamPath && rebakesTooOften(group) && withinTemplateBudget(cols.templates)) {
final DepthEqualSet set = activeDepthEqualSet(group);
if (set != null) {
if (set.streamMembers == 0 && allCanInstance(set)) switchSetNextFrame(set, true);
} else if (canInstance(group) || group.opaque && templatesMatchLayerDrawMode(group)) {
promote(group);
}
}
}
private boolean withinTemplateBudget(ObjectArrayList<TemplateBuffer> templates) {
final TemplateBuffer[] seen = budgetSeen;
int distinct = 0;
boolean within = true;
outer:
for (int i = 0, n = templates.size(); i < n; i++) {
final TemplateBuffer template = templates.get(i);
for (int j = 0; j < distinct; j++) {
if (seen[j] == template) continue outer;
}
if (distinct == PROMOTE_MAX_TEMPLATES) {
within = false;
break;
}
seen[distinct++] = template;
}
Arrays.fill(seen, 0, distinct, null);
return within;
}
private static boolean templatesMatchLayerDrawMode(Group group) {
final ObjectArrayList<TemplateBuffer> templates = group.templateColumns.templates;
final int drawMode = group.layer.getDrawMode();
for (int i = 0, n = templates.size(); i < n; i++) {
if (templates.get(i).drawMode != drawMode) return false;
}
return true;
}
private boolean rebakesTooOften(Group group) {
if (group.promoteWindowRebakes == 0 || nowMs - group.promoteWindowStartMs > PROMOTE_WINDOW_MS) {
group.promoteWindowStartMs = nowMs;
group.promoteWindowRebakes = 1;
return false;
}
return ++group.promoteWindowRebakes >= PROMOTE_AFTER_REBAKES;
}
private void drawTemplateRuns(InstanceColumns cols, boolean combineGlint, boolean itemStencil) {
for (int r = 0, n = cols.runs.size(); r < n; r++) {
final TexRun run = cols.runs.get(r);
@@ -920,8 +1383,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
final long base = memAddress0(fallbackScratch);
long ptr = base;
for (int i = start; i < end; i++) {
System.arraycopy(data.matrices, i * 16, matScratch, 0, 16);
scratchMat.set(matScratch);
scratchMat.set(data.matrices, i * 16);
final int color = head.source != null ? head.color : data.colors.getInt(i);
final EntityTextureAtlas.Slot slot = data.atlasSlot(head, i);
ptr = VertexTransform.writeInstance(ptr, format, templates.get(i), scratchMat, scratchVec, color, data.lights.getInt(i), slot != null ? slot.uvMatrix : texMatrix);
@@ -940,13 +1402,11 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
}
private void promote(Group group) {
group.streaming = true;
streamPromotions++;
group.hashAcc = 0;
group.frameHash = 0;
group.prevFrameHash = 0;
group.stableFrames = 0;
group.builtCount = -1;
group.streamPath = true;
if (!group.stream) streamPromotions++;
if (group.probe == null) group.probe = new Probe();
clearPromotionTracking(group);
group.probe.lastMs = nowMs;
group.clearInstances();
group.mesh.delete();
group.meshBytes = 0;
@@ -960,6 +1420,11 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
if (now - group.lastUsedMs <= GROUP_TTL_MS) continue;
group.mesh.delete();
list.remove(i);
final DepthEqualSet set = group.depthEqualSet;
if (set != null) {
set.remove(group);
if (group.material.isDepthEqual() && set.depthEqualMembers == 0) set.clearPath();
}
final Reference2ObjectOpenHashMap<TesrMaterial, Object2ObjectOpenHashMap<DrawStateKey, Group>> byMaterial = groups.get(group.layer);
if (byMaterial != null) {
final Object2ObjectOpenHashMap<DrawStateKey, Group> byId = byMaterial.get(group.material);
@@ -979,6 +1444,9 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
}
byLayer.clear();
groups.clear();
openScope = 0;
openDepthEqualSet = null;
openWriters.clear();
Arrays.fill(recentGroups, null);
fallbackMesh.delete();
fallbackScratch = null;
@@ -1000,7 +1468,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
int count = 0;
for (final ObjectArrayList<Group> list : byLayer.values()) {
for (int i = 0, n = list.size(); i < n; i++) {
if (list.get(i).streaming) count++;
if (list.get(i).streamPath) count++;
}
}
return count;
@@ -1018,25 +1486,25 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
return bytes;
}
private void rebuild(Group group) {
rebuilds++;
group.rebuildsWindow++;
private void rebake(Group group, long hashSum) {
rebakes++;
group.rebakesWindow++;
if (Tracy.ENABLED) {
Tracy.beginZone(Z_TESR_REBUILD);
Tracy.beginZone(Z_TESR_REBAKE);
Tracy.zoneText(attribution(group));
}
try {
rebuildInner(group);
rebakeInner(group, hashSum);
} finally {
if (Tracy.ENABLED) Tracy.endZone();
}
}
void collectRebuilders(ObjectArrayList<Group> out) {
void collectRebakers(ObjectArrayList<Group> out) {
for (final ObjectArrayList<Group> list : byLayer.values()) {
for (int i = 0, n = list.size(); i < n; i++) {
final Group group = list.get(i);
if (group.rebuildsWindow > 0) out.add(group);
if (group.rebakesWindow > 0) out.add(group);
}
}
}
@@ -1057,7 +1525,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
return sb.toString();
}
private void rebuildInner(Group group) {
private void rebakeInner(Group group, long hashSum) {
final VertexFormat format = group.layer.getVertexFormat();
final InstanceColumns cols = group.templateColumns;
int totalVerts = 0;
@@ -1070,16 +1538,15 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
final long base = memAddress0(group.scratch);
long ptr = base;
for (int i = 0, n = cols.size; i < n; i++) {
System.arraycopy(cols.matrices, i * 16, matScratch, 0, 16);
scratchMat.set(matScratch);
scratchMat.set(cols.matrices, i * 16);
ptr = VertexTransform.writeInstance(ptr, format, cols.templates.get(i), scratchMat, scratchVec, cols.colors.getInt(i), cols.lights.getInt(i), group.instTexMatrices.get(i));
}
group.snapshot.copyFrom(cols, group.instTexMatrices, hashSum);
group.scratch.position((int) (ptr - base));
group.scratch.flip();
group.builtVertexCount = totalVerts;
group.mesh.upload(format, group.layer.getDrawMode(), group.scratch, group.builtVertexCount, true);
group.meshBytes = bytes;
group.builtHash = group.hashAcc;
group.builtCount = cols.size;
}
@@ -1087,7 +1554,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook {
retainedDraws++;
source.applyIdsAndRebind(group.state.entity, group.state.block, group.state.item);
AngelicaBufferSource.setEntityColor(group.entityColor);
drawMV.set(baseMV).translate((float) (group.anchorX - camX), (float) (group.anchorY - camY), (float) (group.anchorZ - camZ));
drawMV.set(baseMV).translate((float) (group.anchor.x - camX), (float) (group.anchor.y - camY), (float) (group.anchor.z - camZ));
saveMV();
GLStateManager.setModelViewMatrix(drawMV);
group.mesh.render();
@@ -6,7 +6,10 @@ public final class TesrAnchorMath {
public static final int ANCHOR_GRID = 16;
public static final double REANCHOR_DISTANCE = 512.0;
private static final float HASH_QUANT = 4096f;
public static final float TRANSFORM_TOLERANCE = 1f / 2048;
public static final int TEX_MATRIX_TERMS = 7;
private static final int TEX_PRESENT = TEX_MATRIX_TERMS - 1;
private static final float HASH_QUANT = 1f / TRANSFORM_TOLERANCE;
private TesrAnchorMath() {}
@@ -27,6 +30,48 @@ public final class TesrAnchorMath {
m.m32(m.m32() + (float) (camZ - anchorZ));
}
public static boolean close(float a, float b) {
return Math.abs(a - b) <= TRANSFORM_TOLERANCE || Float.floatToRawIntBits(a) == Float.floatToRawIntBits(b);
}
public static boolean sameTransform(float[] a, int aOffset, float[] b, int bOffset) {
for (int column = 0; column < 16; column += 4) {
for (int row = 0; row < 3; row++) {
if (!close(a[aOffset + column + row], b[bOffset + column + row])) return false;
}
}
return true;
}
public static void storeTexMatrix(float[] dst, int offset, Matrix4f t) {
if (t == null) {
for (int i = 0; i < TEX_MATRIX_TERMS; i++) dst[offset + i] = 0f;
return;
}
for (int i = 0; i < TEX_PRESENT; i++) dst[offset + i] = texTerm(t, i);
dst[offset + TEX_PRESENT] = 1f;
}
public static boolean sameTexMatrix(float[] stored, int offset, Matrix4f t) {
final boolean storedPresent = stored[offset + TEX_PRESENT] != 0f;
if (t == null || !storedPresent) return t == null && !storedPresent;
for (int i = 0; i < TEX_PRESENT; i++) {
if (!close(stored[offset + i], texTerm(t, i))) return false;
}
return true;
}
private static float texTerm(Matrix4f t, int i) {
return switch (i) {
case 0 -> t.m00();
case 1 -> t.m01();
case 2 -> t.m10();
case 3 -> t.m11();
case 4 -> t.m30();
default -> t.m31();
};
}
public static long instanceHash(int templateIdentity, Matrix4f mA, int packedLight, int colorABGR) {
long h = templateIdentity * 0x9E3779B97F4A7C15L;
h = mix(h, quantize(mA.m00()));
@@ -47,13 +92,9 @@ public final class TesrAnchorMath {
}
public static long texMatrixHash(Matrix4f t) {
long h = 0x2545F4914F6CDD1DL;
h = mix(h, quantize(t.m00()));
h = mix(h, quantize(t.m01()));
h = mix(h, quantize(t.m10()));
h = mix(h, quantize(t.m11()));
h = mix(h, quantize(t.m30()));
h = mix(h, quantize(t.m31()));
if (t == null) return 0L;
long h = 0x9E3779B97F4A7C15L;
for (int i = 0; i < TEX_PRESENT; i++) h = mix(h, quantize(texTerm(t, i)));
return avalanche(h);
}
@@ -68,6 +68,9 @@ public final class TesrBatchRenderer {
final InstanceRing instanceRing = new InstanceRing();
final InstancedTemplateRenderer instancedRenderer = new InstancedTemplateRenderer(instanceRing);
private int activePass = -1;
private long scopeCounter;
private long currentScope;
private int scopeDepth;
private long lastSweepMs;
private boolean deferredFlushPending;
private RetainedTesrGroups pendingDeferredHook;
@@ -82,6 +85,14 @@ public final class TesrBatchRenderer {
}
}
public void beginScope() {
if (scopeDepth++ == 0) currentScope = ++scopeCounter;
}
public void endScope() {
if (--scopeDepth == 0) currentScope = 0;
}
public void beginPass(int passKey, Matrix4f baseMV, double camX, double camY, double camZ) {
BatchStateFallback.install();
if (deferredFlushPending) {
@@ -90,8 +101,10 @@ public final class TesrBatchRenderer {
activePass = passKey;
bufferSource.capturePassLighting();
BatchDrawDefaults.capturePassFramebuffer();
retained[passKey].beginPass(baseMV, camX, camY, camZ, instancedCapable() ? instancedRenderer : null, deferredPipeline());
final DeferredWorldRenderingPipeline deferred = deferredPipeline();
retained[passKey].beginPass(baseMV, camX, camY, camZ, instancedCapable() ? instancedRenderer : null, deferred);
if (passKey == PASS_MAIN_0) {
instancedRenderer.releaseRetired();
final long now = System.currentTimeMillis();
if (now - lastSweepMs >= SWEEP_INTERVAL_MS) {
lastSweepMs = now;
@@ -166,6 +179,10 @@ public final class TesrBatchRenderer {
return hasPendingGeometry() && bufferSource.hasPendingLayers();
}
void retire(TemplateBuffer template) {
instancedRenderer.retire(template);
}
public void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material) {
modelView.set(GLStateManager.getModelViewMatrix());
final int packedLight = currentPackedLight(material);
@@ -174,7 +191,7 @@ public final class TesrBatchRenderer {
final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL);
final PolygonState polygon = GLStateManager.getPolygonState();
final PassOverride pass = PassOverride.capture();
final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final int cullCode = DrawState.liveCull(material);
final boolean lit = DrawState.liveLit(material);
final RenderLayer layer = layerFor(texture, material, pass,
offset ? polygon.getOffsetFactor() : 0.0f, offset ? polygon.getOffsetUnits() : 0.0f, cullCode, lit, DrawState.liveLightmap());
@@ -182,7 +199,7 @@ public final class TesrBatchRenderer {
? CapturedRenderingState.INSTANCE.getCurrentRenderedEntity()
: CapturedRenderingState.INSTANCE.getCurrentRenderedBlockEntity();
final long entityInfo = InstancedAttribs.packEntityInfo(CapturedRenderingState.INSTANCE.getCurrentRenderedEntity(), CapturedRenderingState.INSTANCE.getCurrentRenderedBlockEntity(), CapturedRenderingState.INSTANCE.getCurrentRenderedItem());
retained[activePass].queue(template, layer, material, modelView, packedLight, colorABGR,
retained[activePass].queue(currentScope, template, layer, material, modelView, packedLight, colorABGR,
AngelicaBufferSource.packEntityColor(CapturedRenderingState.INSTANCE.getCurrentEntityColor()), entityInfo, blockEntityId, captureTextureMatrix());
} else {
drawImmediate(template, texture, material, packedLight, colorABGR);
@@ -313,7 +330,7 @@ public final class TesrBatchRenderer {
}
private RenderLayer noPassLayerFor(ResourceLocation texture, TesrMaterial material) {
final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
final int cullCode = DrawState.liveCull(material);
final boolean lit = DrawState.liveLit(material);
final boolean lightmap = DrawState.liveLightmap();
if (texture == lastImmediateTexture && material == lastImmediateMaterial && cullCode == lastImmediateCull && lit == lastImmediateLit
@@ -460,15 +477,15 @@ public final class TesrBatchRenderer {
}
}
private long lastRebuilds, lastRetainedDraws, lastStreamed, lastInstInstances;
private long lastRebakes, lastRetainedDraws, lastStreamed, lastInstInstances;
private long lastSummaryNanos;
public String getDebugSummaryLine() {
long rebuilds = 0, retainedDraws = 0, streamed = 0, instInstances = 0, retainedBytes = 0;
long rebakes = 0, retainedDraws = 0, streamed = 0, instInstances = 0, retainedBytes = 0;
int groupCount = 0, streamingCount = 0;
for (int i = 0; i < PASS_COUNT; i++) {
final RetainedTesrGroups r = retained[i];
rebuilds += r.rebuilds;
rebakes += r.rebakes;
retainedDraws += r.retainedDraws;
streamed += r.streamedInstances;
instInstances += r.instancedInstances;
@@ -480,13 +497,13 @@ public final class TesrBatchRenderer {
final long elapsed = now - lastSummaryNanos;
final String line = String.format("TESR: %d grp (%d strm), reb %d/s, drw %d/s, inst %d/s, stream %d/s, mem %.1fMB",
groupCount, streamingCount,
perSecond(rebuilds - lastRebuilds, elapsed),
perSecond(rebakes - lastRebakes, elapsed),
perSecond(retainedDraws - lastRetainedDraws, elapsed),
perSecond(instInstances - lastInstInstances, elapsed),
perSecond(streamed - lastStreamed, elapsed),
(retainedBytes + bufferSource.allocatedBytes() + instancedRenderer.meshBytes()) / 1048576.0);
lastSummaryNanos = now;
lastRebuilds = rebuilds;
lastRebakes = rebakes;
lastRetainedDraws = retainedDraws;
lastStreamed = streamed;
lastInstInstances = instInstances;
@@ -506,37 +523,37 @@ public final class TesrBatchRenderer {
retainedBytes / 1048576.0, bufferSource.allocatedBytes() / 1048576.0,
instancedRenderer.meshCount(), instancedRenderer.meshBytes() / 1048576.0,
AngelicaTesrMeshCache.INSTANCE.size());
final String attribution = rebuildAttributionLine();
final String attribution = rebakeAttributionLine();
return attribution == null ? Arrays.asList(mem) : Arrays.asList(mem, attribution);
}
private final ObjectArrayList<RetainedTesrGroups.Group> rebuilders = new ObjectArrayList<>();
private final ObjectArrayList<RetainedTesrGroups.Group> rebakers = new ObjectArrayList<>();
private String rebuildAttributionLine() {
rebuilders.clear();
private String rebakeAttributionLine() {
rebakers.clear();
for (int i = 0; i < PASS_COUNT; i++) {
retained[i].collectRebuilders(rebuilders);
retained[i].collectRebakers(rebakers);
}
if (rebuilders.isEmpty()) return null;
rebuilders.sort((a, b) -> Integer.compare(b.rebuildsWindow, a.rebuildsWindow));
if (rebakers.isEmpty()) return null;
rebakers.sort((a, b) -> Integer.compare(b.rebakesWindow, a.rebakesWindow));
final StringBuilder sb = new StringBuilder("TESR reb:");
final int shown = Math.min(3, rebuilders.size());
final int shown = Math.min(3, rebakers.size());
for (int i = 0; i < shown; i++) {
final RetainedTesrGroups.Group group = rebuilders.get(i);
final RetainedTesrGroups.Group group = rebakers.get(i);
if (i > 0) sb.append(',');
sb.append(' ').append(group.rebuildsWindow).append("x ").append(RetainedTesrGroups.attribution(group)).append(' ').append(group.builtVertexCount).append('v');
sb.append(' ').append(group.rebakesWindow).append("x ").append(RetainedTesrGroups.attribution(group)).append(' ').append(group.builtVertexCount).append('v');
}
if (rebuilders.size() > shown) {
sb.append(" +").append(rebuilders.size() - shown);
if (rebakers.size() > shown) {
sb.append(" +").append(rebakers.size() - shown);
}
for (int i = 0, n = rebuilders.size(); i < n; i++) {
rebuilders.get(i).rebuildsWindow = 0;
for (int i = 0, n = rebakers.size(); i < n; i++) {
rebakers.get(i).rebakesWindow = 0;
}
rebuilders.clear();
rebakers.clear();
return sb.toString();
}
public long statRebuilds() { return sumStat(0); }
public long statRebakes() { return sumStat(0); }
public long statRetainedDraws() { return sumStat(1); }
public long statInstancedDraws() { return sumStat(2); }
public long statInstancedInstances() { return sumStat(3); }
@@ -548,7 +565,7 @@ public final class TesrBatchRenderer {
for (int i = 0; i < PASS_COUNT; i++) {
final RetainedTesrGroups r = retained[i];
v += switch (which) {
case 0 -> r.rebuilds;
case 0 -> r.rebakes;
case 1 -> r.retainedDraws;
case 2 -> r.instancedDraws;
case 3 -> r.instancedInstances;
@@ -2,28 +2,13 @@ package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrMeshProvider;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.minecraft.block.Block;
import net.minecraft.tileentity.TileEntity;
public final class TesrProviderDispatch {
private TesrProviderDispatch() {}
public static int resolveBlockEntityId(TileEntity te) {
if (te == null) return 0;
final Block block = te.getBlockType();
if (block == null) return 0;
final Reference2ObjectMap<Block, Int2IntMap> matches = BlockRenderingSettings.INSTANCE.getBlockMetaMatches();
if (matches == null) return 0;
final Int2IntMap metaMap = matches.get(block);
if (metaMap == null) return 0;
return Math.max(0, metaMap.get(blockMetadata(te)));
}
/**
* Item renderers often hand a world-less dummy TE to the dispatcher, and some mods override getBlockMetadata()
* to read the world unconditionally (e.g. EFR shulker boxes), so don't call it without a world.
@@ -0,0 +1,171 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.ffp.FfpGlsl;
import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedGlslHelpers;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
final class TesrShaderPrelude {
static final String LAYER_SAMPLER = "angelica_LayerTexture";
static final String LIGHTMAP_SAMPLER = "angelica_LightmapTexture";
private static final Logger LOG = LogManager.getLogger("AngelicaTesrShaders");
private static final String VERSION_DIRECTIVE = "#version";
private static final ObjectOpenHashSet<String> WARNED_VERSION = new ObjectOpenHashSet<>();
private static final String VERTEX_BAKED = buildVertex(false);
private static final String VERTEX_INSTANCED = buildVertex(true);
private static final String FRAGMENT = buildFragment();
private TesrShaderPrelude() {}
static String vertex(boolean instanced) {
return instanced ? VERTEX_INSTANCED : VERTEX_BAKED;
}
static String fragment() {
return FRAGMENT;
}
private static String buildVertex(boolean instanced) {
final StringBuilder sb = new StringBuilder(2048);
sb.append("#version 330 core\n\n");
sb.append("#define angelica_out out\n");
sb.append("invariant gl_Position;\n\n");
sb.append("// Vertex attributes\n");
attribute(sb, Usage.POSITION, "vec3", "angelica_Position");
attribute(sb, Usage.COLOR, "vec4", "angelica_VertexColor");
attribute(sb, Usage.PRIMARY_UV, "vec2", "angelica_TexCoord");
attribute(sb, Usage.NORMAL, "vec3", "angelica_Normal");
if (instanced) {
for (String d : InstancedGlslHelpers.attributeDecls("angelica_", Instancing.TEMPLATE)) {
sb.append(d).append('\n');
}
} else {
attribute(sb, Usage.SECONDARY_UV, "vec2", "angelica_VertexLightmap");
}
sb.append('\n');
sb.append("// Uniforms\n");
if (instanced) {
sb.append("uniform mat4 iris_TextureMatrix;\n");
} else {
sb.append("uniform mat4 angelica_ModelViewMatrix;\n");
sb.append("uniform mat4 angelica_ModelViewProjectionMatrix;\n");
sb.append("uniform mat3 angelica_NormalMatrix;\n");
}
sb.append("uniform mat4 angelica_ProjectionMatrix;\n");
sb.append("uniform mat4 angelica_LightmapTextureMatrix;\n");
sb.append("uniform int angelica_FogDistanceMode;\n");
sb.append("struct angelica_LightSourceParameters { vec4 diffuse; vec4 position; };\n");
sb.append("uniform angelica_LightSourceParameters angelica_LightSource[2];\n\n");
sb.append("// Accessors\n");
if (instanced) {
sb.append("mat4 angelica_modelView() { return ")
.append(InstancedGlslHelpers.mat4FromRows("angelica_InstRow0", "angelica_InstRow1", "angelica_InstRow2")).append("; }\n");
sb.append("vec4 angelica_eyePosition() { return angelica_modelView() * vec4(angelica_Position, 1.0); }\n");
sb.append("vec4 angelica_clipPosition() { return angelica_ProjectionMatrix * angelica_eyePosition(); }\n");
sb.append("vec3 angelica_eyeNormal() { return normalize(mat3(angelica_modelView()) * angelica_Normal); }\n");
sb.append("vec4 angelica_color() { return angelica_VertexColor * angelica_InstColor; }\n");
sb.append("vec2 angelica_uv() { return (iris_TextureMatrix * vec4(angelica_TexCoord, 0.0, 1.0)).xy; }\n");
sb.append("vec2 angelica_lightmapUv() { return (angelica_LightmapTextureMatrix * vec4(angelica_InstLightmap, 0.0, 1.0)).xy; }\n");
} else {
sb.append("mat4 angelica_modelView() { return angelica_ModelViewMatrix; }\n");
sb.append("vec4 angelica_eyePosition() { return angelica_ModelViewMatrix * vec4(angelica_Position, 1.0); }\n");
sb.append("vec4 angelica_clipPosition() { return angelica_ModelViewProjectionMatrix * vec4(angelica_Position, 1.0); }\n");
sb.append("vec3 angelica_eyeNormal() { return normalize(angelica_NormalMatrix * angelica_Normal); }\n");
sb.append("vec4 angelica_color() { return angelica_VertexColor; }\n");
sb.append("vec2 angelica_uv() { return angelica_TexCoord; }\n");
sb.append("vec2 angelica_lightmapUv() { return (angelica_LightmapTextureMatrix * vec4(angelica_VertexLightmap, 0.0, 1.0)).xy; }\n");
}
sb.append("float angelica_fogCoord() {\n");
sb.append(" vec4 eye = angelica_eyePosition();\n");
for (final int mode : new int[] { FogState.DISTANCE_EYE_RADIAL, FogState.DISTANCE_EYE_PLANE }) {
sb.append(" if (angelica_FogDistanceMode == ").append(mode).append(") return ").append(FfpGlsl.fogDistance(mode, "eye")).append(";\n");
}
sb.append(" return ").append(FfpGlsl.fogDistance(FogState.DISTANCE_EYE_PLANE_ABS, "eye")).append(";\n");
sb.append("}\n");
sb.append("vec3 angelica_lightDirection(int i) { return angelica_LightSource[i].position.xyz; }\n");
sb.append("vec3 angelica_lightDiffuse(int i) { return angelica_LightSource[i].diffuse.rgb; }\n\n");
return sb.toString();
}
private static String buildFragment() {
final StringBuilder sb = new StringBuilder(1024);
sb.append("#version 330 core\n\n");
sb.append("#define angelica_in in\n\n");
sb.append("// Outputs\n");
sb.append("layout(location = 0) out vec4 angelica_FragColor;\n\n");
sb.append("// Uniforms\n");
sb.append("uniform sampler2D ").append(LAYER_SAMPLER).append(";\n");
sb.append("uniform sampler2D ").append(LIGHTMAP_SAMPLER).append(";\n");
sb.append("uniform int angelica_FogMode;\n");
sb.append("uniform vec4 angelica_FogParams;\n");
sb.append("uniform vec4 angelica_FogColor;\n");
sb.append("uniform float angelica_currentAlphaTest;\n\n");
sb.append("// Helpers\n");
sb.append("vec4 angelica_sampleTexture(vec2 uv) { return texture(").append(LAYER_SAMPLER).append(", uv); }\n");
sb.append("vec4 angelica_sampleLightmap(vec2 uv) { return texture(").append(LIGHTMAP_SAMPLER).append(", uv); }\n");
sb.append("vec3 angelica_fog(vec3 color, float fogCoord) {\n");
sb.append(" float f;\n");
fogBranch(sb, "if", FragmentKey.FOG_LINEAR);
fogBranch(sb, "} else if", FragmentKey.FOG_EXP);
fogBranch(sb, "} else if", FragmentKey.FOG_EXP2);
sb.append(" } else {\n");
sb.append(" return color;\n");
sb.append(" }\n");
sb.append(" return mix(angelica_FogColor.rgb, color, clamp(f, 0.0, 1.0));\n");
sb.append("}\n");
sb.append("bool angelica_alphaTest(float a) { return ").append(FfpGlsl.quantizeAlpha("a"))
.append(" > angelica_currentAlphaTest * 255.0; }\n\n");
return sb.toString();
}
private static void fogBranch(StringBuilder sb, String keyword, int fogMode) {
sb.append(" ").append(keyword).append(" (angelica_FogMode == ").append(fogMode).append(") {\n");
FfpGlsl.appendFogFactor(sb, " ", fogMode, "fogCoord", "angelica_FogParams");
}
static String stripVersion(String shaderName, String body) {
final int start = skipCommentsAndWhitespace(body);
if (!body.startsWith(VERSION_DIRECTIVE, start)) return body;
if (WARNED_VERSION.add(shaderName)) {
LOG.warn("TESR shader {} declares #version; Angelica supplies it, the line is ignored", shaderName);
}
final int end = body.indexOf('\n', start);
return end < 0 ? "" : body.substring(end + 1);
}
private static int skipCommentsAndWhitespace(String s) {
int i = 0;
while (i < s.length()) {
if (Character.isWhitespace(s.charAt(i))) {
i++;
} else if (s.startsWith("//", i)) {
final int eol = s.indexOf('\n', i);
i = eol < 0 ? s.length() : eol + 1;
} else if (s.startsWith("/*", i)) {
final int close = s.indexOf("*/", i + 2);
i = close < 0 ? s.length() : close + 2;
} else {
break;
}
}
return i;
}
private static void attribute(StringBuilder sb, Usage usage, String type, String name) {
sb.append("layout(location = ").append(usage.getAttributeLocation()).append(") in ").append(type).append(' ').append(name).append(";\n");
}
}
@@ -0,0 +1,245 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
import com.gtnewhorizons.angelica.api.tesr.TesrShader;
import com.gtnewhorizons.angelica.api.tesr.TesrShaderUniforms;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import net.coderbot.iris.Iris;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.minecraft.client.Minecraft;
import net.minecraft.util.ResourceLocation;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL20;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public final class TesrShaderPrograms {
private static final Logger LOG = LogManager.getLogger("AngelicaTesrShaders");
private static final int UNKNOWN = 0;
private static final int READY = 1;
private static final int FAILED = 2;
private static final Reference2ObjectOpenHashMap<TesrShader, Entry> ENTRIES = new Reference2ObjectOpenHashMap<>();
public static long bakedBinds;
public static long instancedBinds;
public static long fallbacks;
public static long linkFailures;
private TesrShaderPrograms() {}
static final class Variant {
final boolean instanced;
final String label;
int state;
int program;
Variant(boolean instanced) {
this.instanced = instanced;
this.label = instanced ? "instanced" : "baked";
}
}
public static final class Entry {
final TesrShader shader;
final Variant baked = new Variant(false);
final Variant instanced = new Variant(true);
private boolean programBound;
private int savedProgram;
private boolean wantsLightmap;
private boolean lightmapBound;
private int savedLightmapBinding;
Entry(TesrShader shader) {
this.shader = shader;
}
}
public static boolean packActive() {
return Iris.enabled && Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline drp && drp.shouldOverrideShaders();
}
public static Entry entry(TesrShader shader) {
Entry entry = ENTRIES.get(shader);
if (entry == null) {
entry = new Entry(shader);
ENTRIES.put(shader, entry);
}
return entry;
}
static Entry entryFor(TesrMaterial material) {
final TesrShader shader = material.shader();
return shader == null ? null : entry(shader);
}
public static boolean bindBaked(Entry entry, boolean lightmap) {
entry.wantsLightmap = lightmap;
if (!use(entry, entry.baked)) return false;
bakedBinds++;
return true;
}
static boolean bindInstanced(Entry entry) {
if (!use(entry, entry.instanced)) return false;
instancedBinds++;
return true;
}
public static void release(Entry entry) {
if (entry.lightmapBound) {
entry.lightmapBound = false;
bindLightmapUnit(entry.savedLightmapBinding);
}
if (entry.programBound) {
entry.programBound = false;
GLStateManager.glUseProgram(entry.savedProgram);
}
}
static boolean instancedAvailable(Entry entry) {
return entry.instanced.state == READY;
}
static void invalidateAll() {
for (final Entry entry : ENTRIES.values()) {
reset(entry.baked);
reset(entry.instanced);
}
}
private static void reset(Variant variant) {
if (variant.program != 0) GLStateManager.glDeleteProgram(variant.program);
variant.program = 0;
variant.state = UNKNOWN;
}
private static boolean use(Entry entry, Variant variant) {
ensureLinked(entry);
if (variant.state != READY) {
fallbacks++;
return false;
}
if (!entry.programBound) {
entry.programBound = true;
entry.savedProgram = GLStateManager.getActiveProgram();
}
GLStateManager.glUseProgram(variant.program);
if (entry.wantsLightmap && !entry.lightmapBound) {
entry.lightmapBound = true;
entry.savedLightmapBinding = GLStateManager.getBoundTextureForServerState(DrawState.LIGHTMAP_UNIT);
bindLightmapUnit(((EntityRendererAccessor) Minecraft.getMinecraft().entityRenderer).getLightmapTexture().getGlTextureId());
}
final TesrShaderUniforms uniforms = entry.shader.uniforms();
if (uniforms != null) uniforms.angelica$bound(variant.program, variant.instanced);
return true;
}
private static void bindLightmapUnit(int texture) {
final int active = GLStateManager.getActiveTextureUnit();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + DrawState.LIGHTMAP_UNIT);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + active);
}
private static void ensureLinked(Entry entry) {
final boolean baked = entry.baked.state == UNKNOWN;
final boolean instanced = entry.instanced.state == UNKNOWN;
if (!baked && !instanced) return;
if (GLStateManager.isRecordingDisplayList()) return;
final TesrShader shader = entry.shader;
final String vertexBody;
final String fragmentBody;
try {
vertexBody = read(shader.vertex());
fragmentBody = read(shader.fragment());
} catch (IOException | RuntimeException e) {
if (baked) fail(entry, entry.baked, "could not be read", e.toString());
if (instanced) fail(entry, entry.instanced, "could not be read", e.toString());
return;
}
if (baked) link(entry, false, vertexBody, fragmentBody);
if (instanced) link(entry, true, vertexBody, fragmentBody);
}
private static String read(ResourceLocation location) throws IOException {
try (InputStream stream = Minecraft.getMinecraft().getResourceManager().getResource(location).getInputStream()) {
return IOUtils.toString(stream, StandardCharsets.UTF_8);
}
}
static void link(Entry entry, boolean instanced, String vertexBody, String fragmentBody) {
final Variant variant = instanced ? entry.instanced : entry.baked;
reset(variant);
final String name = entry.shader.name();
int vertex = 0;
int fragment = 0;
int program = 0;
try {
vertex = compile(entry, variant, GL20.GL_VERTEX_SHADER, TesrShaderPrelude.vertex(instanced) + TesrShaderPrelude.stripVersion(name, vertexBody));
if (vertex == 0) return;
fragment = compile(entry, variant, GL20.GL_FRAGMENT_SHADER, TesrShaderPrelude.fragment() + TesrShaderPrelude.stripVersion(name, fragmentBody));
if (fragment == 0) return;
program = GLStateManager.glCreateProgram();
GLStateManager.glAttachShader(program, vertex);
GLStateManager.glAttachShader(program, fragment);
GLStateManager.glLinkProgram(program);
if (GLStateManager.glGetProgrami(program, GL20.GL_LINK_STATUS) != GL11.GL_TRUE) {
fail(entry, variant, "failed to link", GLStateManager.glGetProgramInfoLog(program));
return;
}
final int previous = GLStateManager.getActiveProgram();
GLStateManager.glUseProgram(program);
try {
setSampler(program, TesrShaderPrelude.LAYER_SAMPLER, 0);
setSampler(program, TesrShaderPrelude.LIGHTMAP_SAMPLER, DrawState.LIGHTMAP_UNIT);
final TesrShaderUniforms uniforms = entry.shader.uniforms();
if (uniforms != null) uniforms.angelica$linked(program, instanced);
} finally {
GLStateManager.glUseProgram(previous);
}
variant.program = program;
variant.state = READY;
LOG.info("TESR shader {} ({} variant) linked: program {}", name, variant.label, program);
program = 0;
} catch (RuntimeException e) {
fail(entry, variant, "failed to build", e.toString());
} finally {
if (vertex != 0) GLStateManager.glDeleteShader(vertex);
if (fragment != 0) GLStateManager.glDeleteShader(fragment);
if (program != 0) GLStateManager.glDeleteProgram(program);
}
}
private static int compile(Entry entry, Variant variant, int type, String source) {
final int shader = GLStateManager.glCreateShader(type);
GLStateManager.glShaderSource(shader, source);
GLStateManager.glCompileShader(shader);
if (GLStateManager.glGetShaderi(shader, GL20.GL_COMPILE_STATUS) == GL11.GL_TRUE) return shader;
fail(entry, variant, type == GL20.GL_VERTEX_SHADER ? "vertex stage failed to compile" : "fragment stage failed to compile", GLStateManager.glGetShaderInfoLog(shader));
GLStateManager.glDeleteShader(shader);
return 0;
}
private static void setSampler(int program, String name, int unit) {
final int location = GLStateManager.glGetUniformLocation(program, name);
if (location >= 0) GLStateManager.glUniform1i(location, unit);
}
private static void fail(Entry entry, Variant variant, String what, String detail) {
variant.state = FAILED;
linkFailures++;
LOG.warn("TESR shader {} ({} variant) {}; variant disabled until the next resource reload: {}",
entry.shader.name(), variant.label, what, detail);
}
}
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.IBatchEligibility;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
import com.gtnewhorizons.angelica.rendering.tesr.TesrProviderDispatch;
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import net.minecraft.tileentity.TileEntity;
@@ -17,23 +18,28 @@ public class MixinTileEntityRendererDispatcher {
@Redirect(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer;renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V"))
private void angelica$dispatchTesrProvider(TileEntitySpecialRenderer renderer, TileEntity te, double x, double y, double z, float partialTicks) {
if (AngelicaConfig.enableTESRProviderDispatch && TesrProviderDispatch.tryRender(renderer, te, x, y, z)) return;
if (!AngelicaConfig.enableEntityBatching) {
renderer.renderTileEntityAt(te, x, y, z, partialTicks);
return;
}
final IBatchEligibility holder = (IBatchEligibility) renderer;
final byte state = holder.angelica$batchState();
BatchEligibility.begin(state, GLStateManager.drawCalls);
TesrBatchRenderer.INSTANCE.beginScope();
try {
renderer.renderTileEntityAt(te, x, y, z, partialTicks);
} finally {
final byte next = BatchEligibility.end(state, GLStateManager.drawCalls);
if (next != state) {
holder.angelica$setBatchState(next);
BatchEligibility.onStateChange(renderer, next);
if (AngelicaConfig.enableTESRProviderDispatch && TesrProviderDispatch.tryRender(renderer, te, x, y, z)) return;
if (!AngelicaConfig.enableEntityBatching) {
renderer.renderTileEntityAt(te, x, y, z, partialTicks);
return;
}
final IBatchEligibility holder = (IBatchEligibility) renderer;
final byte state = holder.angelica$batchState();
BatchEligibility.begin(state, GLStateManager.drawCalls);
try {
renderer.renderTileEntityAt(te, x, y, z, partialTicks);
} finally {
final byte next = BatchEligibility.end(state, GLStateManager.drawCalls);
if (next != state) {
holder.angelica$setBatchState(next);
BatchEligibility.onStateChange(renderer, next);
}
}
} finally {
TesrBatchRenderer.INSTANCE.endScope();
}
}
}
@@ -1,29 +1,32 @@
package com.gtnewhorizons.angelica.api.tesr;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
class TesrMaterialInternTest {
@Test
void equalSettingsBuildSameInstance() {
final TesrMaterial a = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).translucent().stream().build();
final TesrMaterial b = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).translucent().stream().build();
assertSame(a, b);
private static final TesrShader SPECIAL_EXCLUDED = TesrShaders.register("test:invalid_special", () -> {}, () -> {});
static Stream<Arguments> invalidCombinations() {
return Stream.of(
Arguments.of("depthOnly + noDepthWrite", (Executable) () -> TesrMaterial.builder().depthOnly().noDepthWrite().build()),
Arguments.of("depthOnly + depthEqual", (Executable) () -> TesrMaterial.builder().depthOnly().depthEqual().build()),
Arguments.of("special + shader", (Executable) () -> TesrMaterial.builder().special(TesrMaterial.SpecialRender.GLINT).shader(SPECIAL_EXCLUDED).build()));
}
@Test
void differingSettingsBuildDistinctInstances() {
final TesrMaterial a = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).translucent().build();
final TesrMaterial b = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).additive().build();
assertNotSame(a, b);
}
@Test
void defaultsFoldToCurrentState() {
assertSame(TesrMaterial.CURRENT_STATE, TesrMaterial.builder().build());
@ParameterizedTest(name = "{0}")
@MethodSource("invalidCombinations")
void invalidCombinationsThrow(String name, Executable build) {
assertThrows(IllegalStateException.class, build);
}
@Test
@@ -1,25 +1,14 @@
package com.gtnewhorizons.angelica.api.tesr;
import net.minecraft.util.ResourceLocation;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
class TesrShadersTest {
private static final Runnable NOOP = () -> {};
@Test
void registerReturnsHandleWithAccessors() {
final Runnable bind = () -> {};
final Runnable release = () -> {};
final TesrShader shader = TesrShaders.register("test:accessors", bind, release);
assertEquals("test:accessors", shader.name());
assertSame(bind, shader.bind());
assertSame(release, shader.release());
}
@Test
void duplicateNameThrows() {
TesrShaders.register("test:duplicate", NOOP, NOOP);
@@ -35,8 +24,11 @@ class TesrShadersTest {
}
@Test
void nullRunnablesThrow() {
void nullArgumentsThrow() {
final ResourceLocation location = new ResourceLocation("test", "shaders/a.vert");
assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_bind", null, NOOP));
assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_release", NOOP, null));
assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_vertex", null, location));
assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_fragment", location, null));
}
}
@@ -37,7 +37,7 @@ class FlybySceneTest {
Arguments.of("trims surrounding whitespace", " time set day \n", new String[] { "time set day" }),
Arguments.of("handles CRLF line endings", "time set day\r\nweather clear\r\n", new String[] { "time set day", "weather clear" }),
Arguments.of("drops a bare slash line", "time set day\n/\nweather clear\n", new String[] { "time set day", "weather clear" }),
Arguments.of("preserves order", "one\ntwo\nthree\n", new String[] { "one", "two", "three" }));
Arguments.of("substitutes defines and drops them", "define BUF {buffer:[\"a$b\"]}\nsetblock ~ ~ ~ stone 0 replace ${BUF}\nsay ${MISSING}\n", new String[] { "setblock ~ ~ ~ stone 0 replace {buffer:[\"a$b\"]}", "say ${MISSING}" }));
}
@ParameterizedTest(name = "{0}")
@@ -53,12 +53,6 @@ class FlybySceneTest {
assertSame(plain, FlybyScene.expand(plain, 100, 64, -50));
}
@Test
void missingFileReturnsEmptyArray() {
final String path = tempDir.resolve("does-not-exist.txt").toString();
assertEquals(0, FlybyScene.load(path).length);
}
@Test
void shippedScenesTagEverythingTheyPlace() {
for (String path : new String[] { "scripts/flyby-scenes/entities.txt", "scripts/flyby-scenes/entities-x8.txt" }) {
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrMeshProvider;
import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
@@ -7,10 +8,14 @@ import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
import com.gtnewhorizons.angelica.rendering.tesr.AngelicaTesrMeshCache.MeshBackend;
import com.gtnewhorizons.angelica.rendering.tesr.TemplateBuffer;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.ResourceLocation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.LongSupplier;
@@ -31,17 +36,6 @@ class AngelicaTesrMeshCacheTest {
private final AngelicaTesrMeshCache cache = new AngelicaTesrMeshCache(backend, clock, replay);
private final Object key = new Object();
@Test
void firstFrameCapturesAndReplays() {
cache.renderCached(key, sink -> sink.angelica$bucket(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, TEX_A));
assertEquals(1, backend.beginCaptures);
assertTrue(cache.hasEntry(key));
assertEquals(1, cache.version(key));
assertEquals(1, replay.calls.size());
assertSame(TEX_A, replay.calls.get(0).texture);
}
@Test
void cleanFrameReplaysWithoutRecapture() {
cache.renderCached(key, this::oneBucket);
@@ -53,24 +47,69 @@ class AngelicaTesrMeshCacheTest {
}
@Test
void invalidateForcesRecapture() {
cache.renderCached(key, this::oneBucket);
cache.invalidate(key);
assertFalse(cache.hasEntry(key));
void replayIsWrappedInOneScope() {
cache.renderCached(key, sink -> {
sink.angelica$bucket(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, TEX_A);
sink.angelica$bucket(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, TEX_B);
});
cache.renderCached(key, this::oneBucket);
assertEquals(2, backend.beginCaptures);
assertEquals(List.of("begin", "queue", "queue", "end"), replay.events);
}
enum RetireTrigger { INVALIDATE, DIRTY_RECAPTURE, EVICT, CLEAR }
@ParameterizedTest
@EnumSource(RetireTrigger.class)
void droppedTemplatesAreRetiredOnce(RetireTrigger trigger) {
final TesrMeshProvider provider = new TesrMeshProvider() {
@Override
public Object angelica$meshKey(TileEntity te) {
return key;
}
@Override
public void angelica$build(TesrMeshSink sink, TileEntity te) {
oneBucket(sink);
}
};
cache.renderCached(key, false, provider, null);
final TemplateBuffer captured = replay.calls.get(0).template;
assertTrue(replay.retired.isEmpty());
switch (trigger) {
case INVALIDATE -> cache.invalidate(key);
case DIRTY_RECAPTURE -> cache.renderCached(key, true, provider, null);
case EVICT -> {
now[0] = 1_000L + AngelicaTesrMeshCache.LRU_TIMEOUT_MS + 1;
cache.tick();
}
case CLEAR -> cache.clear();
}
assertEquals(List.of(captured), replay.retired);
if (trigger != RetireTrigger.DIRTY_RECAPTURE) {
assertFalse(cache.hasEntry(key));
cache.renderCached(key, false, provider, null);
assertEquals(2, backend.beginCaptures);
}
cache.clear();
assertEquals(1, Collections.frequency(replay.retired, captured), "a dropped template must not be retired again");
}
@Test
void lruEvicts() {
final long timeout = AngelicaTesrMeshCache.LRU_TIMEOUT_MS;
cache.renderCached(key, this::oneBucket);
now[0] = 1_000L + AngelicaTesrMeshCache.LRU_TIMEOUT_MS; // boundary -> retained
cache.tick();
assertTrue(cache.hasEntry(key));
now[0] = 1_000L + timeout;
cache.renderCached(key, this::oneBucket);
now[0] = 1_000L + AngelicaTesrMeshCache.LRU_TIMEOUT_MS + 1; // past timeout -> evicted
now[0] = 1_000L + timeout + 1;
cache.tick();
assertTrue(cache.hasEntry(key), "a hit refreshes the entry");
now[0] = 1_000L + 2 * timeout + 1;
cache.tick();
assertFalse(cache.hasEntry(key));
}
@@ -113,10 +152,28 @@ class AngelicaTesrMeshCacheTest {
private static final class RecordingReplay implements AngelicaTesrMeshCache.ReplaySink {
final List<Recorded> calls = new ArrayList<>();
final List<String> events = new ArrayList<>();
final List<TemplateBuffer> retired = new ArrayList<>();
@Override
public void beginScope() {
events.add("begin");
}
@Override
public void endScope() {
events.add("end");
}
@Override
public void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material) {
calls.add(new Recorded(template, texture, material));
events.add("queue");
}
@Override
public void retire(TemplateBuffer template) {
retired.add(template);
}
}
}
@@ -4,6 +4,12 @@ import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.rendering.items.DroppedItemInstancer;
import org.junit.jupiter.api.BeforeEach;
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 java.util.function.Consumer;
import java.util.stream.Stream;
import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.DENIED;
import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.SAFE;
@@ -46,35 +52,18 @@ class BatchEligibilityTest {
BatchEligibility.onPartFallback(before, drawCalls);
}
@Test
void aRendererDrawingOnlyModelPartsIsPromoted() {
assertFalse(BatchEligibility.begin(UNKNOWN, drawCalls), "unproven renderers run unbatched");
unbatchedPart(1);
unbatchedPart(1);
assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls));
static Stream<Arguments> demotions() {
return Stream.of(
Arguments.of("a raw draw", (Consumer<BatchEligibilityTest>) t -> t.draw(1)),
Arguments.of("a fallback part", (Consumer<BatchEligibilityTest>) t -> t.unbatchedPart(1)));
}
@Test
void aRendererMixingItsOwnDrawsIsDenied() {
BatchEligibility.begin(UNKNOWN, drawCalls);
unbatchedPart(1);
draw(1);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls));
}
@Test
void aPromotedRendererIsDemotedWhenItStartsMixing() {
@ParameterizedTest(name = "{0}")
@MethodSource("demotions")
void aPromotedRendererIsDemotedWhenItStartsMixing(String name, Consumer<BatchEligibilityTest> mixing) {
assertTrue(BatchEligibility.begin(SAFE, drawCalls), "proven renderers batch");
BatchEligibility.onPartQueued();
draw(1);
assertEquals(DENIED, BatchEligibility.end(SAFE, drawCalls));
}
@Test
void aBatchedPartFallingBackCountsAsMixing() {
BatchEligibility.begin(SAFE, drawCalls);
BatchEligibility.onPartQueued();
unbatchedPart(1);
mixing.accept(this);
assertEquals(DENIED, BatchEligibility.end(SAFE, drawCalls));
}
@@ -111,7 +100,7 @@ class BatchEligibilityTest {
draw(1);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls));
BatchEligibility.begin(UNKNOWN, drawCalls);
assertFalse(BatchEligibility.begin(UNKNOWN, drawCalls), "unproven renderers run unbatched");
unbatchedPart(1);
assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls), "the previous dispatch's foreign draws must not leak");
}
@@ -213,10 +202,8 @@ class BatchEligibilityTest {
BatchEligibility.beginExpectedDraws(drawCalls);
unbatchedPart(2);
BatchEligibility.endExpectedDraws(drawCalls);
draw(3);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls));
final long foreign = Reflect.getStatic(BatchEligibility.class, "foreignDraws");
assertEquals(3, foreign, "only the 3 unbracketed draws must count as foreign");
draw(1);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls), "only the unbracketed draw is foreign");
}
@Test
@@ -230,10 +217,7 @@ class BatchEligibilityTest {
BatchEligibility.endExpectedDraws(drawCalls);
draw(1);
BatchEligibility.endExpectedDraws(drawCalls);
draw(2);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls));
final long foreign = Reflect.getStatic(BatchEligibility.class, "foreignDraws");
assertEquals(2, foreign, "the inner bracket must not reset the outer span");
assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls), "the inner bracket must not reset the outer span");
}
@Test
@@ -8,12 +8,16 @@ import com.gtnewhorizons.angelica.iris.IrisDisplayListState;
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.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL30;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
@@ -46,55 +50,141 @@ class BatchStateGuardGLTest {
BatchStateGuard.flush = flushes::incrementAndGet;
}
@Test
void projectionMutationFlushesBeforeOverwritingTheMatrixAndDoesNotReenter() {
private static final Runnable NOTHING = () -> { };
private void assertFlushes(Runnable setup, Runnable atFlush, Runnable action, int expected, Runnable cleanup) {
final int drawBuffer = GLStateManager.ctx().drawBuffer.getValue();
setup.run();
BatchStateGuard.flush = () -> {
atFlush.run();
flushes.incrementAndGet();
assertEquals(1f, GLStateManager.getProjectionMatrix().m00());
// A flushing renderer is allowed to set up its own draw state.
GLStateManager.glScissor(0, 0, 8, 8);
};
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
GLStateManager.glScalef(2, 3, 1);
assertEquals(1, flushes.get());
assertEquals(2f, GLStateManager.getProjectionMatrix().m00());
try {
action.run();
assertEquals(expected, flushes.get());
} finally {
BatchStateGuard.flush = null;
cleanup.run();
GLStateManager.glDrawBuffer(drawBuffer);
}
}
@Test
void commonCapturedStateAndManagedPassSetupDoNotDrainBatches() {
static Stream<Arguments> flushCases() {
return Stream.of(
Arguments.of("fog", (Runnable) () -> GLStateManager.glFogf(GL11.GL_FOG_START, 17),
(Runnable) () -> assertEquals(17f, GLStateManager.getFogState().getStart()),
(Runnable) () -> {
GLStateManager.glFogf(GL11.GL_FOG_START, 29);
assertEquals(29f, GLStateManager.getFogState().getStart());
}, 1, NOTHING),
Arguments.of("stencil", (Runnable) () -> GLStateManager.glDisable(GL11.GL_STENCIL_TEST),
(Runnable) () -> assertFalse(GLStateManager.glIsEnabled(GL11.GL_STENCIL_TEST)),
(Runnable) () -> GLStateManager.glEnable(GL11.GL_STENCIL_TEST), 1, NOTHING),
Arguments.of("polygon mode", NOTHING,
(Runnable) () -> assertEquals(GL11.GL_FILL, GLStateManager.getPolygonState().getFrontMode()),
(Runnable) () -> {
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
}, 1, NOTHING),
Arguments.of("depth range", NOTHING,
(Runnable) () -> assertEquals(1.0, GLStateManager.getViewportState().depthRangeFar),
(Runnable) () -> {
GLStateManager.glDepthRange(0.0, 0.5);
GLStateManager.glDepthRange(0.0, 0.5);
}, 1, NOTHING),
Arguments.of("logic op and color sum", NOTHING, NOTHING,
(Runnable) () -> {
GLStateManager.glEnable(GL11.GL_COLOR_LOGIC_OP);
GLStateManager.glLogicOp(GL11.GL_XOR);
GLStateManager.glEnable(GL14.GL_COLOR_SUM);
}, 3, NOTHING),
Arguments.of("sample and discard state", NOTHING, NOTHING,
(Runnable) () -> {
GLStateManager.glEnable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE);
GLStateManager.glDisable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE);
GLStateManager.glEnable(GL30.GL_RASTERIZER_DISCARD);
GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD);
GLStateManager.glSampleCoverage(0.5f, false);
}, 5, (Runnable) () -> GLStateManager.glSampleCoverage(1.0f, false)),
Arguments.of("draw buffer", NOTHING, NOTHING,
(Runnable) () -> {
GLStateManager.glDrawBuffer(GL11.GL_NONE);
GLStateManager.glDrawBuffer(GL11.GL_NONE);
}, 1, NOTHING),
Arguments.of("draw buffers", NOTHING, NOTHING, (Runnable) () -> GLStateManager.glDrawBuffers(GL11.GL_NONE), 1, NOTHING),
Arguments.of("projection matrix", (Runnable) () -> GLStateManager.glMatrixMode(GL11.GL_PROJECTION),
(Runnable) () -> {
assertEquals(1f, GLStateManager.getProjectionMatrix().m00());
GLStateManager.glScissor(0, 0, 8, 8);
},
(Runnable) () -> {
GLStateManager.glScalef(2, 3, 1);
assertEquals(2f, GLStateManager.getProjectionMatrix().m00());
}, 1, NOTHING),
Arguments.of("texture matrix unit 2", (Runnable) () -> {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
},
(Runnable) () -> assertEquals(0f, GLStateManager.getTextures().getTextureUnitMatrix(2).m30()),
(Runnable) () -> GLStateManager.glTranslatef(0.5f, 0, 0), 1,
(Runnable) () -> {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.glPopMatrix();
}),
Arguments.of("texture matrix unit 0 stays instanced", (Runnable) () -> {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
}, NOTHING,
(Runnable) () -> {
GLStateManager.glPushMatrix();
GLStateManager.glTranslatef(0.5f, 0, 0);
GLStateManager.glPopMatrix();
}, 0, NOTHING));
}
@ParameterizedTest(name = "{0}")
@MethodSource("flushCases")
void changesFlushBeforeMutatingAndOnlyWhenChanged(String name, Runnable setup, Runnable atFlush, Runnable action, int expected, Runnable cleanup) {
assertFlushes(setup, atFlush, action, expected, cleanup);
}
static Stream<Arguments> undrainedCases() {
return Stream.of(
Arguments.of("matrices color and blend", (Runnable) () -> {
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
GLStateManager.glTranslatef(1, 2, 3);
GLStateManager.glPopMatrix();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glColor4f(0.5f, 1, 1, 1);
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
}),
Arguments.of("managed pass setup", (Runnable) () -> IrisDisplayListState.runProgramTransition(() -> {
GLStateManager.glFogf(GL11.GL_FOG_START, 123);
GLStateManager.glViewport(0, 0, 8, 8);
})),
Arguments.of("lighting", (Runnable) () -> {
GLStateManager.glEnable(GL11.GL_LIGHT0);
GLStateManager.glDisable(GL11.GL_LIGHT0);
GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL);
GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL);
GLStateManager.glEnable(GL11.GL_NORMALIZE);
GLStateManager.glLightf(GL11.GL_LIGHT0, GL11.GL_CONSTANT_ATTENUATION, 2.0f);
}));
}
@ParameterizedTest(name = "{0}")
@MethodSource("undrainedCases")
void commonCapturedStateAndManagedPassSetupDoNotDrainBatches(String name, Runnable action) {
countFlushes();
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
GLStateManager.glPushMatrix();
GLStateManager.glTranslatef(1, 2, 3);
GLStateManager.glPopMatrix();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glColor4f(0.5f, 1, 1, 1);
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
IrisDisplayListState.runProgramTransition(() -> {
GLStateManager.glFogf(GL11.GL_FOG_START, 123);
GLStateManager.glViewport(0, 0, 8, 8);
});
action.run();
assertEquals(0, flushes.get());
assertFalse(BatchStateGuard.isSuspended());
}
@Test
void fogAndStencilFlushBeforeChangingTheCachedState() {
GLStateManager.glFogf(GL11.GL_FOG_START, 17);
BatchStateGuard.flush = () -> assertEquals(17f, GLStateManager.getFogState().getStart());
GLStateManager.glFogf(GL11.GL_FOG_START, 29);
assertEquals(29f, GLStateManager.getFogState().getStart());
BatchStateGuard.flush = null;
GLStateManager.glDisable(GL11.GL_STENCIL_TEST);
BatchStateGuard.flush = () -> {
assertFalse(GLStateManager.glIsEnabled(GL11.GL_STENCIL_TEST));
flushes.incrementAndGet();
};
GLStateManager.glEnable(GL11.GL_STENCIL_TEST);
assertEquals(1, flushes.get());
}
@Test
void extraTextureBindingsAndAttributeRestoresCannotOvertakeQueuedDraws() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
@@ -174,58 +264,6 @@ class BatchStateGuardGLTest {
}
}
@Test
void rasterStateNotStoredWithLayersFlushesBeforeChanging() {
BatchStateGuard.flush = () -> {
assertEquals(GL11.GL_FILL, GLStateManager.getPolygonState().getFrontMode());
flushes.incrementAndGet();
};
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE);
assertEquals(1, flushes.get());
flushes.set(0);
BatchStateGuard.flush = () -> {
assertEquals(1.0, GLStateManager.getViewportState().depthRangeFar);
flushes.incrementAndGet();
};
GLStateManager.glDepthRange(0.0, 0.5);
GLStateManager.glDepthRange(0.0, 0.5);
assertEquals(1, flushes.get());
flushes.set(0);
countFlushes();
GLStateManager.glEnable(GL11.GL_COLOR_LOGIC_OP);
GLStateManager.glLogicOp(GL11.GL_XOR);
GLStateManager.glEnable(GL14.GL_COLOR_SUM);
assertEquals(3, flushes.get());
}
@Test
void sampleDrawBufferAndDiscardStateFlushesBeforeChanging() {
countFlushes();
final int drawBuffer = GLStateManager.ctx().drawBuffer.getValue();
try {
GLStateManager.glEnable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE);
GLStateManager.glDisable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE);
GLStateManager.glEnable(GL30.GL_RASTERIZER_DISCARD);
GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD);
GLStateManager.glSampleCoverage(0.5f, false);
assertEquals(5, flushes.get());
flushes.set(0);
GLStateManager.glDrawBuffer(GL11.GL_NONE);
GLStateManager.glDrawBuffer(GL11.GL_NONE);
assertEquals(1, flushes.get(), "unchanged draw buffer does not flush");
GLStateManager.glDrawBuffers(GL11.GL_NONE);
assertEquals(2, flushes.get());
} finally {
BatchStateGuard.flush = null;
GLStateManager.glSampleCoverage(1.0f, false);
GLStateManager.glDrawBuffer(drawBuffer);
}
}
@Test
void secondaryColorFlushesOnlyWhileColorSumIsOn() {
GLStateManager.glSecondaryColor3f(0f, 0f, 0f);
@@ -243,42 +281,4 @@ class BatchStateGuardGLTest {
GLStateManager.glSecondaryColor3f(0f, 0f, 0f);
}
}
@Test
void lightingChangesDoNotDrainBatches() {
countFlushes();
GLStateManager.glEnable(GL11.GL_LIGHT0);
GLStateManager.glDisable(GL11.GL_LIGHT0);
GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL);
GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL);
GLStateManager.glEnable(GL11.GL_NORMALIZE);
GLStateManager.glLightf(GL11.GL_LIGHT0, GL11.GL_CONSTANT_ATTENUATION, 2.0f);
assertEquals(0, flushes.get(), "batches draw with the lighting captured when their pass began");
}
@Test
void textureMatrixChangesDrainOtherUnitsButKeepUnitZeroInstanced() {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
try {
BatchStateGuard.flush = () -> {
assertEquals(0f, GLStateManager.getTextures().getTextureUnitMatrix(2).m30());
flushes.incrementAndGet();
};
GLStateManager.glTranslatef(0.5f, 0, 0);
assertEquals(1, flushes.get());
countFlushes();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glPushMatrix();
GLStateManager.glTranslatef(0.5f, 0, 0);
GLStateManager.glPopMatrix();
assertEquals(1, flushes.get(), "the base texture matrix is already stored in batch runs");
} finally {
BatchStateGuard.flush = null;
GLStateManager.glActiveTexture(GL13.GL_TEXTURE2);
GLStateManager.glPopMatrix();
}
}
}
@@ -9,8 +9,6 @@ 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.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class DrawStateTest {
@@ -20,9 +18,7 @@ class DrawStateTest {
arguments(true, GL11.GL_BACK, DrawState.CULL_BACK),
arguments(true, GL11.GL_FRONT, DrawState.CULL_FRONT),
arguments(true, GL11.GL_FRONT_AND_BACK, DrawState.CULL_BOTH),
arguments(false, GL11.GL_BACK, DrawState.DISABLED),
arguments(false, GL11.GL_FRONT, DrawState.DISABLED),
arguments(false, GL11.GL_FRONT_AND_BACK, DrawState.DISABLED)
arguments(false, GL11.GL_BACK, DrawState.DISABLED)
);
}
@@ -32,42 +28,6 @@ class DrawStateTest {
assertEquals(expected, DrawState.packCull(enabled, cullFaceMode));
}
@Test
void equalValuesInternToOneInstance() {
assertSame(DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f), DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f));
assertNotSame(DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f), DrawState.of(DrawState.CULL_FRONT, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f));
assertNotSame(DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f), DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, false, 0f, 0f));
}
@Test
void lifecycleResetBoundsInterningAndReinternsIdentity() {
DrawState.clearInterning();
final DrawState before = DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f);
DrawState.of(DrawState.CULL_FRONT, DrawState.TRANSLUCENT, false, false, true, 0f, false, true, 1f, 1f);
assertEquals(2, DrawState.internedStateCount());
DrawState.clearInterning();
DrawState.clearInterning();
assertEquals(0, DrawState.internedStateCount());
final DrawState after = DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f);
assertNotSame(before, after);
assertSame(after, DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f));
}
@Test
void noCullMaterialNormalizesToDisabled() {
final TesrMaterial noCull = TesrMaterial.builder().noCull().build();
assertSame(DrawState.forMaterial(noCull, DrawState.CULL_FRONT, true, true, 0f, 0f), DrawState.forMaterial(noCull, DrawState.CULL_BACK, true, true, 0f, 0f));
assertEquals(DrawState.DISABLED, DrawState.forMaterial(noCull, DrawState.CULL_FRONT, true, true, 0f, 0f).getCull());
}
@Test
void unlitMaterialNormalizesToUnlit() {
final TesrMaterial unlit = TesrMaterial.builder().unlit().build();
assertSame(DrawState.forMaterial(unlit, DrawState.CULL_BACK, true, true, 0f, 0f), DrawState.forMaterial(unlit, DrawState.CULL_BACK, false, true, 0f, 0f));
}
@Test
void transparencyMapsToBlendMode() {
assertEquals(DrawState.OPAQUE, DrawState.blendFor(TesrMaterial.Transparency.OPAQUE));
@@ -1,10 +1,14 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import org.junit.jupiter.api.Test;
import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -13,120 +17,91 @@ class EntityMaterialsTest {
private static final int SA = GL11.GL_SRC_ALPHA;
private static final int OMSA = GL11.GL_ONE_MINUS_SRC_ALPHA;
@Test
void mainModelStateMapsToCutout() {
assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
private static Arguments row(String name, TesrMaterial expected, boolean textured, boolean blend, Boolean texAnimated, int srcRgb, int dstRgb, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) {
return Arguments.of(name, expected, textured, blend, texAnimated, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask);
}
@Test
void shadowPassHalfAlphaRefMapsToCutoutHalf() {
assertSame(EntityMaterials.CUTOUT_HALF, EntityMaterials.fromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true));
static Stream<Arguments> fromStateCases() {
return Stream.of(
row("main model state maps to cutout", EntityMaterials.CUTOUT, true, false, null, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
row("shadow pass half alpha ref maps to cutout half", EntityMaterials.CUTOUT_HALF, true, false, null, SA, OMSA, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true),
row("no alpha no blend maps to solid", EntityMaterials.SOLID, true, false, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("invisibility maps to translucent", EntityMaterials.TRANSLUCENT, true, true, null, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, false),
row("invisibility with depth write maps to translucent depth write", EntityMaterials.TRANSLUCENT_DEPTH_WRITE, true, true, null, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, true),
row("eyes map to additive", EntityMaterials.ADDITIVE, true, true, null, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("eyes without depth write map to additive no depth write", EntityMaterials.ADDITIVE_NO_DEPTH_WRITE, true, true, null, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false),
row("additive keeps captured alpha test", EntityMaterials.ADDITIVE_CUTOUT, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
row("additive cutout without depth write", EntityMaterials.ADDITIVE_CUTOUT_NO_DEPTH_WRITE, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false),
row("additive alpha keeps captured alpha test", EntityMaterials.ADDITIVE_ALPHA_CUTOUT, true, true, null, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
row("additive alpha cutout without depth write", EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, true, true, null, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false),
row("animated additive keeps captured alpha test", EntityMaterials.ADDITIVE_CUTOUT, true, true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
row("animated additive alpha cutout without depth write", EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false),
row("additive with a nonstandard alpha ref stays live", null, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true),
row("additive with a non-GREATER alpha func stays live", null, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true),
row("animated additive with a nonstandard alpha ref stays live", null, true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true),
row("hurt overlay maps to overlay", EntityMaterials.OVERLAY, false, true, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true),
row("glint maps to glint when texture animated", EntityMaterials.GLINT, true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false),
row("an alpha-blended animated decal such as a CIT armor overlay is not glint", null, true, true, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false),
row("glint blend without texture animation stays live", null, true, true, false, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false),
row("glint blend with depth write is not vanilla glint", null, true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, true),
row("animated additive maps", EntityMaterials.ADDITIVE, true, true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("animated additive alpha without depth write maps", EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, true, true, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false),
row("animated standard-alpha scrolling translucent stays live", null, true, true, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("animated without blend stays live", null, true, false, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("additive alpha maps without animation", EntityMaterials.ADDITIVE_ALPHA, true, true, false, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("additive alpha without animation or depth write", EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, true, true, false, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false),
row("glint blend at EQUAL without animation stays live", null, true, true, null, GL11.GL_SRC_COLOR, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, false),
row("nonstandard alpha ref stays live", null, true, false, null, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true),
row("texture off without overlay signature stays live", null, false, false, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
row("textured EQUAL-depth re-render stays live", null, true, true, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true));
}
@Test
void noAlphaNoBlendMapsToSolid() {
assertSame(EntityMaterials.SOLID, EntityMaterials.fromState(true, false, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true));
@ParameterizedTest(name = "{0}")
@MethodSource("fromStateCases")
void fromState(String name, TesrMaterial expected, boolean textured, boolean blend, Boolean texAnimated, int srcRgb, int dstRgb, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) {
final TesrMaterial actual = texAnimated == null
? EntityMaterials.fromState(textured, blend, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask)
: EntityMaterials.fromState(textured, texAnimated, blend, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask);
assertSame(expected, actual, name);
}
@Test
void invisibilityMapsToTranslucent() {
assertSame(EntityMaterials.TRANSLUCENT, EntityMaterials.fromState(true, true, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, false));
assertSame(EntityMaterials.TRANSLUCENT_DEPTH_WRITE, EntityMaterials.fromState(true, true, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, true));
static Stream<Arguments> itemFromStateCases() {
return Stream.of(
Arguments.of("dropped item cutout", EntityMaterials.DROPPED_ITEM_CUTOUT, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("held block cutout", EntityMaterials.HELD_BLOCK_CUTOUT, false, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("dropped item translucent", EntityMaterials.DROPPED_ITEM_TRANSLUCENT, true, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("held block translucent", EntityMaterials.HELD_BLOCK_TRANSLUCENT, false, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("nonstandard alpha ref must stay live", null, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true),
Arguments.of("non-GREATER alpha func must stay live", null, true, false, SA, OMSA, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("alpha test disabled must stay live", null, true, false, SA, OMSA, false, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("depth mask off must stay live", null, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false),
Arguments.of("non-LEQUAL/LESS depth func must stay live", null, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_ALWAYS, true),
Arguments.of("additive blend has no item material", null, true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
}
@Test
void eyesMapToAdditive() {
assertSame(EntityMaterials.ADDITIVE, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.ADDITIVE_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false));
@ParameterizedTest(name = "{0}")
@MethodSource("itemFromStateCases")
void itemFromState(String name, TesrMaterial expected, boolean unfilteredAtlas, boolean blend, int srcRgb, int dstRgb, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) {
assertSame(expected, EntityMaterials.itemFromState(unfilteredAtlas, blend, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask), name);
}
@Test
void additiveKeepsCapturedAlphaTest() {
assertSame(EntityMaterials.ADDITIVE_CUTOUT, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.ADDITIVE_CUTOUT_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false));
assertSame(EntityMaterials.ADDITIVE_ALPHA_CUTOUT, EntityMaterials.fromState(true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false));
static Stream<Arguments> depthOnlyFromStateCases() {
return Stream.of(
Arguments.of("color masked off with alpha test maps to depth-only cutout", EntityMaterials.DEPTH_ONLY_CUTOUT, true, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true),
Arguments.of("color masked off maps to depth-only", EntityMaterials.DEPTH_ONLY, true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true),
Arguments.of("nonstandard alpha ref must stay live", null, true, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true),
Arguments.of("no color and no depth writes must stay live", null, true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false),
Arguments.of("non-LEQUAL/LESS depth func must stay live", null, true, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true),
Arguments.of("texture off must stay live", null, false, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true));
}
@Test
void texAnimatedAdditiveKeepsCapturedAlphaTest() {
assertSame(EntityMaterials.ADDITIVE_CUTOUT, EntityMaterials.fromState(true, true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false));
}
@Test
void additiveWithUnrepresentableAlphaTestStaysLive() {
assertNull(EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), () -> "additive with a nonstandard alpha ref must stay live, not silently drop the test");
assertNull(EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true), () -> "additive with a non-GREATER alpha func must stay live");
assertNull(EntityMaterials.fromState(true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), () -> "animated additive with a nonstandard alpha ref must stay live");
}
@Test
void hurtOverlayMapsToOverlay() {
assertSame(EntityMaterials.OVERLAY, EntityMaterials.fromState(false, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true));
}
@Test
void glintStateMapsToGlintOnlyWhenTexAnimated() {
assertSame(EntityMaterials.GLINT, EntityMaterials.fromState(true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false));
assertNull(EntityMaterials.fromState(true, true, true, GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), () -> "an alpha-blended animated decal such as a CIT armor overlay is not glint");
assertNull(EntityMaterials.fromState(true, false, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), () -> "glint blend without texture animation must stay live");
assertNull(EntityMaterials.fromState(true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, true), () -> "glint blend with depth write is not vanilla glint");
}
@Test
void texAnimatedAdditiveVariantsMap() {
assertSame(EntityMaterials.ADDITIVE, EntityMaterials.fromState(true, true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false));
assertNull(EntityMaterials.fromState(true, true, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), () -> "animated standard-alpha (scrolling translucent) must stay live");
assertNull(EntityMaterials.fromState(true, true, false, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), () -> "animated without blend must stay live");
}
@Test
void additiveAlphaMapsWithoutAnimation() {
assertSame(EntityMaterials.ADDITIVE_ALPHA, EntityMaterials.fromState(true, false, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, EntityMaterials.fromState(true, false, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false));
}
@Test
void itemStateCutoutVsTranslucent() {
assertSame(EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.HELD_BLOCK_CUTOUT, EntityMaterials.itemFromState(false, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.DROPPED_ITEM_TRANSLUCENT, EntityMaterials.itemFromState(true, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.HELD_BLOCK_TRANSLUCENT, EntityMaterials.itemFromState(false, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
}
@Test
void itemStateRejectsWrongAlphaRefOrFunc() {
assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true), "nonstandard alpha ref must stay live");
assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true), "non-GREATER alpha func must stay live");
assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, false, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), "alpha test disabled must stay live");
assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false), "depth mask off must stay live");
assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_ALWAYS, true), "non-LEQUAL/LESS depth func must stay live");
assertNull(EntityMaterials.itemFromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), "additive blend has no item material");
}
@Test
void colorMaskedOffMapsToDepthOnly() {
assertSame(EntityMaterials.DEPTH_ONLY_CUTOUT, EntityMaterials.depthOnlyFromState(true, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true));
assertSame(EntityMaterials.DEPTH_ONLY, EntityMaterials.depthOnlyFromState(true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true));
assertFalse(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isColorWrite(), "depth-only must replay with color writes off");
assertTrue(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isDepthWrite(), "depth-only must replay with depth writes on");
}
@Test
void unrepresentableDepthOnlyStaysLive() {
assertNull(EntityMaterials.depthOnlyFromState(true, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), "nonstandard alpha ref must stay live");
assertNull(EntityMaterials.depthOnlyFromState(true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false), "no color and no depth writes must stay live");
assertNull(EntityMaterials.depthOnlyFromState(true, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true), "non-LEQUAL/LESS depth func must stay live");
assertNull(EntityMaterials.depthOnlyFromState(false, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), "texture off must stay live");
}
@Test
void unrecognizedStatesFallBackToLive() {
assertNull(EntityMaterials.fromState(true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, false), () -> "glint blend at EQUAL must stay live");
assertNull(EntityMaterials.fromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true), () -> "nonstandard alpha ref must stay live");
assertNull(EntityMaterials.fromState(false, false, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), () -> "texture off without overlay signature must stay live");
assertNull(EntityMaterials.fromState(true, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true), () -> "textured EQUAL-depth re-render must stay live");
@ParameterizedTest(name = "{0}")
@MethodSource("depthOnlyFromStateCases")
void depthOnlyFromState(String name, TesrMaterial expected, boolean textured, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) {
assertSame(expected, EntityMaterials.depthOnlyFromState(textured, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask), name);
if (expected == EntityMaterials.DEPTH_ONLY) {
assertFalse(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isColorWrite(), "depth-only must replay with color writes off");
assertTrue(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isDepthWrite(), "depth-only must replay with depth writes on");
}
}
}
@@ -5,73 +5,91 @@ import com.gtnewhorizons.angelica.compat.mojang.RenderLayer;
import com.gtnewhorizons.angelica.shadercompat.ShaderGlint;
import net.coderbot.iris.layer.PassOverride;
import net.minecraft.util.ResourceLocation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
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.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
class RenderLayerInterningTest {
private static final ResourceLocation TEX = new ResourceLocation("angelica", "textures/render_layer_interning_test.png");
@Test
void noCullMaterialInternsToOneLayerRegardlessOfCullCode() {
private static RenderLayer layer(TesrMaterial material, int cull, boolean lit, boolean lightmap) {
return RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, cull, lit, lightmap);
}
private static RenderLayer glint(int slot) {
return RenderLayer.tesr(null, EntityMaterials.GLINT, PassOverride.NONE, 0f, 0f, slot, DrawState.CULL_BACK, true, true);
}
static Stream<?> normalizeCases() {
final TesrMaterial noCull = TesrMaterial.builder().noCull().build();
final RenderLayer back = RenderLayer.tesr(TEX, noCull, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true);
final RenderLayer front = RenderLayer.tesr(TEX, noCull, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_FRONT, true, true);
assertSame(back, front);
assertEquals(DrawState.DISABLED, back.getState().getCull());
}
@Test
void unlitMaterialRequestedLitYieldsUnlitState() {
final TesrMaterial unlit = TesrMaterial.builder().unlit().build();
final RenderLayer layer = RenderLayer.tesr(TEX, unlit, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true);
assertFalse(layer.getState().isLit());
return Stream.of(
arguments("noCull",
(Supplier<RenderLayer>) () -> layer(noCull, DrawState.CULL_BACK, true, true),
(Supplier<RenderLayer>) () -> layer(noCull, DrawState.CULL_FRONT, true, true),
(BiConsumer<RenderLayer, RenderLayer>) (a, b) -> assertEquals(DrawState.DISABLED, a.getState().getCull())),
arguments("unlit",
(Supplier<RenderLayer>) () -> layer(unlit, DrawState.CULL_BACK, true, true),
(Supplier<RenderLayer>) () -> layer(unlit, DrawState.CULL_BACK, false, true),
(BiConsumer<RenderLayer, RenderLayer>) (a, b) -> assertFalse(a.getState().isLit()))
);
}
@Test
void litMaterialRequestedUnlitYieldsUnlitStateAndDistinctLayer() {
@ParameterizedTest(name = "{0}")
@MethodSource("normalizeCases")
void normalizesToOneLayer(String name, Supplier<RenderLayer> requestA, Supplier<RenderLayer> requestB, BiConsumer<RenderLayer, RenderLayer> stateCheck) {
final RenderLayer a = requestA.get();
final RenderLayer b = requestB.get();
assertSame(a, b);
stateCheck.accept(a, b);
}
static Stream<?> splitCases() {
final TesrMaterial lit = TesrMaterial.builder().build();
final RenderLayer unlitRequest = RenderLayer.tesr(TEX, lit, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, false, true);
final RenderLayer litRequest = RenderLayer.tesr(TEX, lit, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true);
assertFalse(unlitRequest.getState().isLit());
assertTrue(litRequest.getState().isLit());
assertNotSame(unlitRequest, litRequest);
return Stream.of(
arguments("lit material requested unlit",
(Supplier<RenderLayer>) () -> layer(lit, DrawState.CULL_BACK, false, true),
(Supplier<RenderLayer>) () -> layer(lit, DrawState.CULL_BACK, true, true),
(BiConsumer<RenderLayer, RenderLayer>) (a, b) -> {
assertFalse(a.getState().isLit());
assertTrue(b.getState().isLit());
}),
arguments("lightmap",
(Supplier<RenderLayer>) () -> layer(lit, DrawState.CULL_BACK, true, true),
(Supplier<RenderLayer>) () -> layer(lit, DrawState.CULL_BACK, true, false),
(BiConsumer<RenderLayer, RenderLayer>) (a, b) -> {
assertTrue(a.getState().isLightmap());
assertFalse(b.getState().isLightmap());
}),
arguments("glint tint slots 0 and 1",
(Supplier<RenderLayer>) () -> glint(1),
(Supplier<RenderLayer>) () -> glint(0),
(BiConsumer<RenderLayer, RenderLayer>) (a, b) -> { }),
arguments("glint untinted and tinted",
(Supplier<RenderLayer>) () -> glint(ShaderGlint.NO_TINT),
(Supplier<RenderLayer>) () -> glint(0),
(BiConsumer<RenderLayer, RenderLayer>) (a, b) -> { })
);
}
@Test
void lightmapIsPartOfLayerIdentity() {
final TesrMaterial material = TesrMaterial.builder().build();
final RenderLayer withLightmap = RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true);
final RenderLayer withoutLightmap = RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, false);
assertTrue(withLightmap.getState().isLightmap());
assertFalse(withoutLightmap.getState().isLightmap());
assertNotSame(withLightmap, withoutLightmap);
}
@Test
void noPassIsPartOfStructuredInterningIdentity() {
final TesrMaterial material = TesrMaterial.builder().build();
final RenderLayer regular = RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true);
final RenderLayer noPass = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true);
assertNotSame(regular, noPass);
}
@Test
void cacheResetAllowsFreshCanonicalIdentity() {
final TesrMaterial material = TesrMaterial.builder().build();
final RenderLayer before = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true);
RenderLayer.clearInterningAndHooks();
final RenderLayer first = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true);
final RenderLayer second = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true);
assertNotSame(before, first);
assertSame(first, second);
assertTrue(RenderLayer.cacheSize() > 0);
@ParameterizedTest(name = "{0}")
@MethodSource("splitCases")
void fieldSplitsLayerIdentity(String name, Supplier<RenderLayer> requestA, Supplier<RenderLayer> requestB, BiConsumer<RenderLayer, RenderLayer> stateCheck) {
final RenderLayer a = requestA.get();
final RenderLayer b = requestB.get();
assertNotSame(a, b);
assertSame(a, requestA.get(), "the same request must reuse the layer");
stateCheck.accept(a, b);
}
}
@@ -4,6 +4,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.MatrixHelper;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
import com.gtnewhorizons.angelica.api.tesr.TesrShader;
import com.gtnewhorizons.angelica.compat.mojang.RenderLayer;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
@@ -17,6 +18,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.states.BlendState;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.rendering.GlintClock;
import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrGroups.Group;
import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrGroups.InstanceColumns;
import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrGroups.TexRun;
import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.CountingPipeline;
@@ -26,12 +28,17 @@ import net.coderbot.iris.layer.PassOverride;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.ItemRenderer;
import net.minecraft.util.ResourceLocation;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.joml.Matrix4f;
import org.joml.Matrix4fc;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
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.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
@@ -40,7 +47,11 @@ import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.SCOPE;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.COLOR_INDEX;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.VERTEX_SIZE;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.X_INDEX;
@@ -63,6 +74,7 @@ class RetainedTesrGroupsGLTest {
private static boolean probeRegistered;
private static Runnable onBufferSetup;
private final long[] clock = {1_000L};
private AngelicaBufferSource source;
private InstanceRing ring;
private InstancedTemplateRenderer instanced;
@@ -110,7 +122,7 @@ class RetainedTesrGroupsGLTest {
source = new AngelicaBufferSource();
ring = new InstanceRing();
instanced = new InstancedTemplateRenderer(ring);
groups = new RetainedTesrGroups(source);
groups = new RetainedTesrGroups(source, () -> clock[0]);
layer = RetainedTesrTestSupport.testLayer();
}
@@ -124,6 +136,7 @@ class RetainedTesrGroupsGLTest {
groups.clear();
instanced.clear();
ring.delete();
TesrShaderPrograms.invalidateAll();
final ShaderManager ffp = ShaderManager.getInstance();
if (ffp.isActive()) ffp.deactivate();
ShaderManager.disable();
@@ -190,11 +203,193 @@ class RetainedTesrGroupsGLTest {
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline);
for (int id = 1; id <= 3; id++) {
final Matrix4f mv = new Matrix4f().translation(id, 0f, 0f);
groups.queue(template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null);
groups.queue(template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null);
groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null);
groups.queue(SCOPE, template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null);
}
}
private static final TesrMaterial BAKED_ADDITIVE = TesrMaterial.builder().additiveAlpha().build();
private static final TesrMaterial BAKED_TRANSLUCENT = TesrMaterial.builder().translucent().build();
private void bakedFrame(TesrMaterial material, TemplateBuffer template, Matrix4f base, boolean instancing, Matrix4f... models) {
final Matrix4f mv = new Matrix4f();
groups.beginPass(base, 0, 0, 0, instancing ? instanced : null);
for (final Matrix4f model : models) {
groups.queue(SCOPE, template, layer, material, mv.set(base).mul(model), 0, -1, 0, 0L, 5, null);
}
groups.drawLayer(layer);
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
}
@Test
void staticGroupBakesOnceWhileTheCameraRotates() {
final TemplateBuffer template = capturedQuad(0.1);
final Matrix4f[] models = new Matrix4f[256];
for (int i = 0; i < models.length; i++) {
models[i] = new Matrix4f().translation((i % 29) * 3.3f - 48f, (i % 7) * 1.7f - 6f, (i % 31) * 3.1f - 48f);
}
for (int frame = 0; frame < 120; frame++) {
clock[0] += 1;
bakedFrame(BAKED_ADDITIVE, template, new Matrix4f().rotationX(0.1f).rotateY(0.002f * frame), true, models);
}
assertEquals(1, groups.rebakes, "rotation alone must not re-bake a static group");
assertEquals(0, groups.streamPromotions);
}
static Stream<Arguments> nonOpaqueMaterials() {
return Stream.of(Arguments.of("additive", BAKED_ADDITIVE), Arguments.of("translucent", BAKED_TRANSLUCENT));
}
@ParameterizedTest(name = "{0}")
@MethodSource("nonOpaqueMaterials")
void nonOpaqueGroupsThatKeepRebakingPromoteToInstancing(String name, TesrMaterial material) {
final TemplateBuffer template = capturedQuad(0.1);
for (int frame = 0; frame < 30; frame++) {
clock[0] += 1;
bakedFrame(material, template, new Matrix4f(), true, new Matrix4f().translation((frame / 3) * 0.03f, 0f, 0f));
}
assertEquals(1, groups.streamPromotions, "a group re-baking every third frame must promote");
assertEquals(1 + RetainedTesrGroups.PROMOTE_AFTER_REBAKES, groups.rebakes, "the first bake plus the re-bakes that triggered promotion, none after");
assertTrue(groups.instancedInstances > 0, "a promoted group must draw instanced");
assertEquals(0, groups.streamedInstances, "promotion with instancing available must not CPU-stream");
}
@Test
void slowRebakesOutsideTheWindowDoNotPromote() {
final TemplateBuffer template = capturedQuad(0.1);
for (int frame = 0; frame < 8; frame++) {
clock[0] += RetainedTesrGroups.PROMOTE_WINDOW_MS + 1;
bakedFrame(BAKED_ADDITIVE, template, new Matrix4f(), true, new Matrix4f().translation(frame * 0.01f, 0f, 0f));
}
assertEquals(0, groups.streamPromotions);
assertEquals(8, groups.rebakes);
}
static Stream<Arguments> reorderRebakes() {
return Stream.of(Arguments.of("additive keeps its mesh", BAKED_ADDITIVE, 1), Arguments.of("translucent re-bakes every reorder", BAKED_TRANSLUCENT, 8));
}
@ParameterizedTest(name = "{0}")
@MethodSource("reorderRebakes")
void reorderRebakesOnlyUnderAlphaBlending(String name, TesrMaterial material, int rebakes) {
final TemplateBuffer template = capturedQuad(0.1);
final Matrix4f left = new Matrix4f().translation(-0.3f, 0f, 0f);
final Matrix4f right = new Matrix4f().translation(0.3f, 0f, 0f);
for (int frame = 0; frame < 8; frame++) {
clock[0] += 1;
bakedFrame(material, template, new Matrix4f(), true, frame % 2 == 0 ? left : right, frame % 2 == 0 ? right : left);
}
assertEquals(rebakes, groups.rebakes, "draw order matters only under alpha blending");
assertEquals(0, groups.streamPromotions, "a reorder is not an instance change");
}
@ParameterizedTest(name = "count changes {0}")
@ValueSource(booleans = { true, false })
void countOrTemplateChangeDoesNotCountTowardPromotion(boolean changeCount) {
final TemplateBuffer[] templates = {capturedQuad(0.1), capturedQuad(0.2)};
final Matrix4f left = new Matrix4f().translation(-0.3f, 0f, 0f);
final Matrix4f right = new Matrix4f().translation(0.3f, 0f, 0f);
for (int frame = 0; frame < 8; frame++) {
clock[0] += 1;
final boolean odd = frame % 2 == 1;
if (changeCount && odd) {
bakedFrame(BAKED_ADDITIVE, templates[0], new Matrix4f(), true, left);
} else {
bakedFrame(BAKED_ADDITIVE, templates[odd ? 1 : 0], new Matrix4f(), true, left, right);
}
}
assertEquals(8, groups.rebakes, "sanity: every frame re-bakes");
assertEquals(0, groups.streamPromotions, "instances entering view or swapping meshes are not volatile transforms");
}
@ParameterizedTest(name = "{0} distinct templates")
@ValueSource(ints = { RetainedTesrGroups.PROMOTE_MAX_TEMPLATES, RetainedTesrGroups.PROMOTE_MAX_TEMPLATES + 1 })
void templateBudgetGatesPromotion(int count) {
final TemplateBuffer[] templates = new TemplateBuffer[count];
for (int i = 0; i < count; i++) templates[i] = capturedQuad(0.01);
for (int frame = 0; frame < 8; frame++) {
clock[0] += 1;
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
for (int i = 0; i < count; i++) {
groups.queue(SCOPE, templates[i], layer, BAKED_ADDITIVE, new Matrix4f().translation(frame * 0.01f, i * 0.05f, 0f), 0, -1, 0, 0L, 5, null);
}
groups.drawLayer(layer);
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
}
assertEquals(count <= RetainedTesrGroups.PROMOTE_MAX_TEMPLATES ? 1 : 0, groups.streamPromotions);
}
@Test
void templateRetiredBeforeFirstDrawIsFreed() {
final TemplateBuffer template = template(TRIANGLE, COLOR_ABGR, GL11.GL_TRIANGLES);
instanced.retire(template);
drawSingleInstance(instanced, template, new Matrix4f(), COLOR_ABGR);
instanced.releaseRetired();
assertEquals(0, instanced.meshCount(), "a retire that precedes the mesh's first build must still free it");
}
static Stream<Arguments> materialsWithoutInstancing() {
return Stream.of(Arguments.of("opaque", TesrMaterial.CURRENT_STATE, true), Arguments.of("additive", BAKED_ADDITIVE, false));
}
@ParameterizedTest(name = "{0}")
@MethodSource("materialsWithoutInstancing")
void withoutInstancingOnlyOpaqueGroupsPromote(String name, TesrMaterial material, boolean promotes) {
final TemplateBuffer template = RetainedTesrTestSupport.template();
for (int frame = 0; frame < 12; frame++) {
clock[0] += 1;
bakedFrame(material, template, new Matrix4f(), false, new Matrix4f().translation(frame * 0.01f, 0f, 0f));
}
assertEquals(promotes ? 1 : 0, groups.streamPromotions);
assertEquals(promotes, groups.streamedInstances > 0, "only a promoted group CPU-streams");
}
@Test
void depthEqualOverlayAndItsDepthWriterAlwaysShareAPath() {
final TemplateBuffer template = capturedQuad(0.1);
final Matrix4f fixed = new Matrix4f().translation(0.2f, 0f, 0f);
boolean sawPromoted = false, sawDemotedAgain = false;
for (int frame = 0; frame < 40; frame++) {
final boolean animating = frame < 12;
clock[0] += animating ? 1 : RetainedTesrGroups.STABILITY_PROBE_MS;
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, fixed, 0, -1, 0, 0L, 5, null);
groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.DEPTH_EQUAL_OVERLAY, fixed, 0, animating ? 0xFF000000 | frame : -1, 0, 0L, 5, null);
final ObjectArrayList<Group> pair = RetainedTesrTestSupport.groupsOn(groups, layer);
assertEquals(2, pair.size());
assertEquals(pair.get(0).streamPath, pair.get(1).streamPath, "base and overlay diverged at frame " + frame);
if (pair.get(0).streamPath) sawPromoted = true;
else if (sawPromoted) sawDemotedAgain = true;
groups.drawLayer(layer);
instanced.endFrame();
}
assertTrue(sawPromoted, "the animating overlay must pull its static depth writer onto the instanced path");
assertTrue(sawDemotedAgain, "once the overlay settles the pair must return to baked together");
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
}
@Test
void coupledPairWithoutInstancingStaysBaked() {
final TemplateBuffer template = RetainedTesrTestSupport.template();
for (int frame = 0; frame < 12; frame++) {
clock[0] += 1;
groups.beginPass(new Matrix4f(), 0, 0, 0);
final Matrix4f pose = new Matrix4f().translation(frame * 0.01f, 0f, 0f);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, pose, 0, -1, 0, 0L, 5, null);
groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.DEPTH_EQUAL_OVERLAY, pose, 0, -1, 0, 0L, 5, null);
final ObjectArrayList<Group> pair = RetainedTesrTestSupport.groupsOn(groups, layer);
assertEquals(2, pair.size(), "base and overlay must form two groups at frame " + frame);
assertEquals(pair.get(0).streamPath, pair.get(1).streamPath, "pair diverged at frame " + frame);
assertFalse(pair.get(0).streamPath, "frame " + frame);
groups.drawLayer(layer);
}
assertEquals(0, groups.streamPromotions, "a depth-coupled group must not promote on its own when its set cannot instance");
source.discard();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
}
@Test
void cpuFallbackRestoresEveryIdAndTintBeforeDrawingAndRestoresTheCaller() {
final var state = net.coderbot.iris.uniforms.CapturedRenderingState.INSTANCE;
@@ -202,7 +397,7 @@ class RetainedTesrGroupsGLTest {
state.pushCurrentBlockEntity();
state.pushCurrentEntityColor();
try {
final var draws = new java.util.ArrayList<long[]>();
final var draws = new ArrayList<long[]>();
onBufferSetup = () -> draws.add(new long[] {
state.getCurrentRenderedEntity(), state.getCurrentRenderedBlockEntity(), state.getCurrentRenderedItem(),
AngelicaBufferSource.packEntityColor(state.getCurrentEntityColor())
@@ -212,7 +407,7 @@ class RetainedTesrGroupsGLTest {
for (int i = 0; i < 3; i++) {
final long info = com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs.packEntityInfo(45010 + i, 45020 + i, 45030 + i);
final int color = AngelicaBufferSource.packAbgr(i / 2f, 0, 0, 1);
groups.queue(template, layer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, -1, color, info, 45020 + i, null);
groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, -1, color, info, 45020 + i, null);
}
state.setCurrentEntityAndItem(81, 82);
state.setCurrentBlockEntity(83);
@@ -239,36 +434,25 @@ class RetainedTesrGroupsGLTest {
}
}
@Test
void bothVariantsBindOncePerLayer() {
final CountingPipeline pipeline = new CountingPipeline(true, true);
queueThreeEntities(pipeline);
groups.drawLayer(layer);
assertEquals(1, pipeline.matrixBinds, "one matrix variant bind for the whole layer");
assertEquals(1, pipeline.cubeBinds, "one cube variant bind for the whole layer");
assertEquals(2, pipeline.rebinds, "one resolve, one release");
assertEquals(3, groups.instancedInstances);
assertEquals(3, groups.cubeInstances);
assertEquals(2, groups.instancedDraws, "the three ids collapse into one group: one template-run draw, one cube-run draw");
assertEquals(0, groups.streamedInstances);
static Stream<Arguments> variantBinds() {
return Stream.of(Arguments.of(true, 1, 1, 3, 3), Arguments.of(false, 1, 0, 6, 0));
}
@Test
void cubePartsRideTheMatrixVariantWhenTheCubeVariantIsMissing() {
final CountingPipeline pipeline = new CountingPipeline(true, false);
@ParameterizedTest(name = "cube variant {0}")
@MethodSource("variantBinds")
void bothVariantsBindOncePerLayer(boolean cubeVariant, int matrixBinds, int cubeBinds, int instancedInstances, int cubeInstances) {
final CountingPipeline pipeline = new CountingPipeline(true, cubeVariant);
queueThreeEntities(pipeline);
groups.drawLayer(layer);
assertEquals(1, pipeline.matrixBinds);
assertEquals(0, pipeline.cubeBinds);
assertEquals(matrixBinds, pipeline.matrixBinds, "one matrix variant bind for the whole layer");
assertEquals(cubeBinds, pipeline.cubeBinds, "one cube variant bind for the whole layer, none without it");
assertEquals(2, pipeline.rebinds, "one resolve, one release");
assertEquals(6, groups.instancedInstances, "cube parts draw as their own templates");
assertEquals(0, groups.cubeInstances);
assertEquals(instancedInstances, groups.instancedInstances, "without a cube variant, cube parts draw as their own templates");
assertEquals(cubeInstances, groups.cubeInstances);
assertEquals(2, groups.instancedDraws, "the three ids collapse into one group: one template-run draw, one cube-run draw");
assertEquals(0, groups.streamedInstances);
assertEquals(2, groups.instancedDraws, "one template-run draw, one cube-as-template-run draw");
}
@Test
@@ -282,8 +466,8 @@ class RetainedTesrGroupsGLTest {
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline);
for (int id = 1; id <= 3; id++) {
final Matrix4f mv = new Matrix4f().translation(id, 0f, 0f);
groups.queue(template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]);
groups.queue(template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null);
groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]);
groups.queue(SCOPE, template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null);
}
groups.drawLayer(layer);
@@ -301,9 +485,9 @@ class RetainedTesrGroupsGLTest {
final TemplateBuffer second = RetainedTesrTestSupport.template();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline);
groups.queue(first, layer, EntityMaterials.GLINT, new Matrix4f().translation(1f, 0f, 0f), 0, -1, 0, 0L, 1, null);
groups.queue(second, layer, EntityMaterials.GLINT, new Matrix4f().translation(2f, 0f, 0f), 0, -1, 0, 0L, 2, null);
groups.queue(first, layer, EntityMaterials.GLINT, new Matrix4f().translation(3f, 0f, 0f), 0, -1, 0, 0L, 3, null);
groups.queue(SCOPE, first, layer, EntityMaterials.GLINT, new Matrix4f().translation(1f, 0f, 0f), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, second, layer, EntityMaterials.GLINT, new Matrix4f().translation(2f, 0f, 0f), 0, -1, 0, 0L, 2, null);
groups.queue(SCOPE, first, layer, EntityMaterials.GLINT, new Matrix4f().translation(3f, 0f, 0f), 0, -1, 0, 0L, 3, null);
groups.drawLayer(layer);
@@ -322,16 +506,15 @@ class RetainedTesrGroupsGLTest {
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline);
for (int id = 1; id <= 3; id++) {
final Matrix4f mv = new Matrix4f().translation(id, 0f, 0f);
groups.queue(template, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]);
groups.queue(template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1], null);
groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]);
groups.queue(SCOPE, template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1], null);
}
final RetainedTesrGroups.Group group = RetainedTesrTestSupport.onlyGroup(groups);
final Group group = RetainedTesrTestSupport.onlyGroup(groups);
assertEquals(2, group.templateColumns.runs.size(), "sanity: template runs A,B,A merge into two heads");
assertNotNull(group.templateColumns.runs.get(0).next, "sanity: the template A head must carry a second segment");
assertEquals(2, group.cubeColumns.runs.size(), "sanity: cube runs A,B,A merge into two heads");
assertNotNull(group.cubeColumns.runs.get(0).next, "sanity: the cube A head must carry a second segment");
groups.drawLayer(layer);
assertEquals(0, pipeline.matrixBinds);
@@ -360,7 +543,7 @@ class RetainedTesrGroupsGLTest {
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced draw must not raise a GL error");
final int[] atTranslation = FfpFixture.readPixel(tx, ty);
assertTrue(atTranslation[0] > 200 && atTranslation[1] < 50 && atTranslation[2] < 50, "translated instance must render red at (" + tx + "," + ty + "), got " + atTranslation[0] + "," + atTranslation[1] + "," + atTranslation[2]);
assertRed(atTranslation, "translated instance must render red at (" + tx + "," + ty + ")");
final int[] atOrigin = FfpFixture.readPixel(originX, originY);
assertTrue(atOrigin[0] < 50 && atOrigin[1] < 50 && atOrigin[2] < 50, "the untranslated origin must stay background, got " + atOrigin[0] + "," + atOrigin[1] + "," + atOrigin[2]);
@@ -370,7 +553,7 @@ class RetainedTesrGroupsGLTest {
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced draw must not raise a GL error");
final int[] identityAtOrigin = FfpFixture.readPixel(originX, originY);
assertTrue(identityAtOrigin[0] > 200 && identityAtOrigin[1] < 50 && identityAtOrigin[2] < 50, "an identity instance must render red at the origin, got " + identityAtOrigin[0] + "," + identityAtOrigin[1] + "," + identityAtOrigin[2]);
assertRed(identityAtOrigin, "an identity instance must render red at the origin");
}
private void queueTriangleOnClearedTarget() {
@@ -379,9 +562,13 @@ class RetainedTesrGroupsGLTest {
GLStateManager.disableTexture();
GLStateManager.glDisable(GL11.GL_SCISSOR_TEST);
FfpFixture.clear();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
groups.queue(template(TRIANGLE, COLOR_ABGR, GL11.GL_TRIANGLES), layer, RetainedTesrTestSupport.STREAM,
new Matrix4f(), 0, COLOR_ABGR, 0, 0L, 0, null);
queueTriangle(groups, new Matrix4f());
}
private void queueTriangle(RetainedTesrGroups target, Matrix4f mv) {
target.beginPass(new Matrix4f(), 0, 0, 0, instanced);
target.queue(SCOPE, template(TRIANGLE, COLOR_ABGR, GL11.GL_TRIANGLES), layer, RetainedTesrTestSupport.STREAM,
mv, 0, COLOR_ABGR, 0, 0L, 0, null);
}
/** Runs body with the named fields of a batcher singleton swapped and the state fallback installed. */
@@ -416,12 +603,23 @@ class RetainedTesrGroupsGLTest {
void heldEntityTranslucentsSurviveUncapturedStateChange() {
GLStateManager.glScissor(0, 0, 0, 0);
queueTriangleOnClearedTarget();
final ModelPartBatcher models = ModelPartBatcher.INSTANCE;
withBatcherFields(models, () -> {
GLStateManager.glEnable(GL11.GL_SCISSOR_TEST);
assertTrue(models.hasHeldEntities(), "held entity translucents wait for the post-deferred flush");
assertTrue(FfpFixture.readPixel(400, 300)[0] < 50, "held entity geometry must not draw before deferred");
}, "entitySource", source, "entityGroups", groups, "entityTranslucentsHeld", true);
final AngelicaBufferSource blockEntitySource = new AngelicaBufferSource();
final RetainedTesrGroups blockEntityGroups = new RetainedTesrGroups(blockEntitySource, () -> clock[0]);
try {
queueTriangle(blockEntityGroups, new Matrix4f().translation(0.5f, 0f, 0f));
final ModelPartBatcher models = ModelPartBatcher.INSTANCE;
withBatcherFields(models, () -> {
assertTrue(models.hasQueuedGeometry(), "sanity: the block-entity pass has queued geometry to drain");
GLStateManager.glEnable(GL11.GL_SCISSOR_TEST);
assertTrue(FfpFixture.readPixel(600, 300)[0] > 200, "the active block-entity batch must draw before the zero-area scissor takes effect");
assertTrue(FfpFixture.readPixel(400, 300)[0] < 50, "held entity geometry must not draw before deferred");
assertTrue(models.hasHeldEntities(), "held entity translucents wait for the post-deferred flush");
}, "entitySource", source, "entityGroups", groups, "entityTranslucentsHeld", true,
"activeSource", blockEntitySource, "activeGroups", blockEntityGroups, "active", true);
} finally {
blockEntitySource.discard();
blockEntityGroups.clear();
}
}
@Test
@@ -637,10 +835,10 @@ class RetainedTesrGroupsGLTest {
setGlintBlendState();
bindGlint();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixA);
groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixB);
groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixA);
groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixB);
groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixA);
groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixB);
groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixA);
groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixB);
groups.drawLayer(layer);
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "the merged candidate draw must not raise a GL error");
@@ -668,10 +866,10 @@ class RetainedTesrGroupsGLTest {
setGlintBlendState();
bindGlint();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixA, null);
groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixB, null);
groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixA, null);
groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixB, null);
groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixA, null);
groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixB, null);
groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixA, null);
groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixB, null);
groups.drawLayer(layer);
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "the merged candidate draw must not raise a GL error");
@@ -682,75 +880,53 @@ class RetainedTesrGroupsGLTest {
return "0x" + Integer.toHexString(pixel);
}
@Test
void templateGlintRunsMatchSequentialDrawsUnderAlphaBlend() {
@ParameterizedTest(name = "cubes {0}")
@ValueSource(booleans = { false, true })
void glintRunsMatchSequentialDrawsUnderAlphaBlend(boolean cubes) {
glintState();
final int[] base = drawBaseTemplates();
final int[] reference = templateReference(false);
final int[] base = cubes ? drawBaseCubes() : drawBaseTemplates();
final int[] reference = cubes ? cubeReference() : templateReference(false);
FfpFixture.assertOverlayFootprint(base, reference, BACKGROUND, "glint");
final int[] swapped = templateReference(true);
int differing = 0;
for (int i = 0; i < reference.length; i++) {
if (reference[i] != swapped[i]) differing++;
if (!cubes) {
final int[] swapped = templateReference(true);
int differing = 0;
for (int i = 0; i < reference.length; i++) {
if (reference[i] != swapped[i]) differing++;
}
assertTrue(differing > 0, "swapping e1's k0/k1 draw order under alpha blending must change the rendered " + "pixels, otherwise the parity tests could not detect a reversed emitter order");
}
assertTrue(differing > 0, "swapping e1's k0/k1 draw order under alpha blending must change the rendered " + "pixels, otherwise the parity tests could not detect a reversed emitter order");
CubeParityFixture.assertPixelParity(reference, templateCandidate(), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe);
CubeParityFixture.assertPixelParity(reference, cubes ? cubeCandidate() : templateCandidate(), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe);
}
@Test
void cubeGlintRunsMatchSequentialDrawsUnderAlphaBlend() {
@ParameterizedTest(name = "cubes {0}")
@ValueSource(booleans = { false, true })
void replayedGlintMatchesIndependentPasses(boolean cubes) {
glintState();
final int[] base = drawBaseCubes();
final int[] reference = cubeReference();
FfpFixture.assertOverlayFootprint(base, reference, BACKGROUND, "glint");
CubeParityFixture.assertPixelParity(reference, cubeCandidate(), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe);
}
@Test
void replayedGlintTemplatesMatchIndependentPasses() {
glintState();
final int[] reference = templateReference(false);
drawBaseTemplates();
final int[] reference = cubes ? cubeReference() : templateReference(false);
if (cubes) drawBaseCubes();
else drawBaseTemplates();
setGlintBlendState();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
final GlintCapture capture = new GlintCapture();
for (Matrix4f pose : new Matrix4f[] {templateE1, templateE2}) {
for (Matrix4f pose : cubes ? new Matrix4f[] {cubeE1, cubeE2} : new Matrix4f[] {templateE1, templateE2}) {
applyGlintTexMatrix(glintMatrixA);
assertTrue(capture.begin(groups));
groups.queue(glintTemplate, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA);
capture.end();
applyGlintTexMatrix(glintMatrixB);
GLStateManager.glColor4f(0.5f, 1, 1, 1);
assertFalse(capture.replay(groups), "changed layer color must retain the original render call");
GLStateManager.glColor4f(1, 1, 1, 1);
GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE);
assertFalse(capture.replay(groups), "changed layer blending must retain the original render call");
setGlintBlendState();
assertTrue(capture.replay(groups));
assertFalse(capture.replay(groups), "a captured pass is replayed only once");
}
groups.drawLayer(layer);
instanced.endFrame();
CubeParityFixture.assertPixelParity(reference, FfpFixture.readRegion(SIZE), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe);
}
@Test
void replayedGlintCubesMatchIndependentPasses() {
glintState();
final int[] reference = cubeReference();
drawBaseCubes();
setGlintBlendState();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
final GlintCapture capture = new GlintCapture();
for (Matrix4f pose : new Matrix4f[] {cubeE1, cubeE2}) {
applyGlintTexMatrix(glintMatrixA);
assertTrue(capture.begin(groups));
groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA, null);
if (cubes) groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA, null);
else groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA);
capture.end();
applyGlintTexMatrix(glintMatrixB);
if (!cubes) {
GLStateManager.glColor4f(0.5f, 1, 1, 1);
assertFalse(capture.replay(groups), "changed layer color must retain the original render call");
GLStateManager.glColor4f(1, 1, 1, 1);
GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE);
assertFalse(capture.replay(groups), "changed layer blending must retain the original render call");
setGlintBlendState();
}
assertTrue(capture.replay(groups));
if (!cubes) assertFalse(capture.replay(groups), "a captured pass is replayed only once");
}
groups.drawLayer(layer);
instanced.endFrame();
@@ -773,7 +949,7 @@ class RetainedTesrGroupsGLTest {
final GlintCapture base = new GlintCapture();
assertTrue(base.begin(groups));
base.allowBase();
groups.queue(glintTemplate, layer, EntityMaterials.CUTOUT, templateE1, 0, 0xFF123456, 0, 0L, 0, null);
groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.CUTOUT, templateE1, 0, 0xFF123456, 0, 0L, 0, null);
base.end();
setGlintBlendState();
GLStateManager.glBlendFunc(GL11.GL_SRC_COLOR, GL11.GL_ONE);
@@ -822,7 +998,6 @@ class RetainedTesrGroupsGLTest {
GLStateManager.enableCull();
GLStateManager.glCullFace(GL11.GL_FRONT);
final int cullCode = DrawState.liveCull();
assertEquals(DrawState.CULL_FRONT, cullCode, "queue-time glCullFace(GL_FRONT) must pack as CULL_FRONT");
final int center = SIZE / 2 * SIZE + SIZE / 2;
FfpFixture.clear();
@@ -835,23 +1010,13 @@ class RetainedTesrGroupsGLTest {
final RenderLayer culled = RenderLayer.tesr(null, RetainedTesrTestSupport.STREAM, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, cullCode, false, true);
FfpFixture.clear();
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
groups.queue(quad, culled, RetainedTesrTestSupport.STREAM, mirrored, 0, COLOR_ABGR, 0, 0L, 1, null);
groups.queue(SCOPE, quad, culled, RetainedTesrTestSupport.STREAM, mirrored, 0, COLOR_ABGR, 0, 0L, 1, null);
GLStateManager.glCullFace(GL11.GL_BACK);
final boolean cullEnabledBefore = GLStateManager.getCullState().isEnabled();
final int cullFaceBefore = GLStateManager.getPolygonState().getCullFaceMode();
final boolean glCullEnabledBefore = GL11.glIsEnabled(GL11.GL_CULL_FACE);
final int glCullFaceBefore = GL11.glGetInteger(GL11.GL_CULL_FACE_MODE);
source.endBatch(groups);
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "the batched draw must not raise a GL error");
assertEquals(cullEnabledBefore, GLStateManager.getCullState().isEnabled(), "the flush must leave the cached cull enable where it found it");
assertEquals(cullFaceBefore, GLStateManager.getPolygonState().getCullFaceMode(), "the flush must leave the cached cull face mode where it found it");
assertEquals(glCullEnabledBefore, GL11.glIsEnabled(GL11.GL_CULL_FACE), "the flush must leave the real cull enable where it found it");
assertEquals(glCullFaceBefore, GL11.glGetInteger(GL11.GL_CULL_FACE_MODE), "the flush must leave the real cull face mode where it found it");
CubeParityFixture.assertPixelParity(reference, FfpFixture.readRegion(SIZE), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe);
}
@@ -890,10 +1055,10 @@ class RetainedTesrGroupsGLTest {
final TemplateBuffer quad = capturedQuad(0.5);
final TesrMaterial opposite = TesrMaterial.builder().build();
final RenderLayer opposingLayer = RenderLayer.tesr(null, opposite, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, true);
final RenderLayer opposingLayer = RetainedTesrTestSupport.layerFor(opposite);
groups.beginPass(new Matrix4f(), 0, 0, 0, instanced);
groups.queue(quad, opposingLayer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, COLOR_ABGR, 0, 0L, 1, null);
groups.queue(SCOPE, quad, opposingLayer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, COLOR_ABGR, 0, 0L, 1, null);
source.endBatch(groups);
instanced.endFrame();
@@ -917,4 +1082,177 @@ class RetainedTesrGroupsGLTest {
assertEquals(offsetFactorBefore, GLStateManager.getPolygonState().getOffsetFactor(), 0.0001f, "cache polygon offset factor must be restored");
assertEquals(glOffsetFactorBefore, GL11.glGetFloat(GL11.GL_POLYGON_OFFSET_FACTOR), 0.0001f, "driver polygon offset factor must be restored");
}
private static final int[][] SHADER_PROBES = { { 200, 300 }, { 600, 300 }, { 400, 300 }, { 200, 500 } };
private static void assertRed(int[] pixel, String what) {
assertTrue(pixel[0] > 200 && pixel[1] < 50 && pixel[2] < 50, what + ", got " + Arrays.toString(pixel));
}
private static RenderLayer noLightmapLayer(TesrMaterial material) {
return RenderLayer.tesr(null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, false);
}
private RenderLayer queueShaderPair(TesrMaterial material, boolean instancing) {
final RenderLayer shaderLayer = noLightmapLayer(material);
final TemplateBuffer template = capturedQuad(0.1);
FfpFixture.clear();
groups.beginPass(new Matrix4f(), 0, 0, 0, instancing ? instanced : null);
groups.queue(SCOPE, template, shaderLayer, material, templateE1, 0, COLOR_ABGR, 0, 0L, 5, null);
groups.queue(SCOPE, template, shaderLayer, material, templateE2, 0, COLOR_ABGR, 0, 0L, 5, null);
return shaderLayer;
}
private int[][] drawShaderLayer(RenderLayer shaderLayer) {
shaderLayer.startDrawing();
groups.drawLayer(shaderLayer);
shaderLayer.endDrawing();
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "shader draw must not raise a GL error");
final int[][] pixels = new int[SHADER_PROBES.length][];
for (int i = 0; i < pixels.length; i++) {
pixels[i] = FfpFixture.readPixel(SHADER_PROBES[i][0], SHADER_PROBES[i][1]);
}
return pixels;
}
@Test
void shaderLayerBindsTheInstancedProgramOnceAndMatchesTheRetainedDraw() {
final TesrShader shader = RetainedTesrTestSupport.linkedSource("angelica:test_gl_source_shader").shader;
GLStateManager.disableBlend();
GLStateManager.disableAlphaTest();
long bakedBinds = TesrShaderPrograms.bakedBinds;
long instancedBinds = TesrShaderPrograms.instancedBinds;
final int[][] instancedPixels = drawShaderLayer(queueShaderPair(TesrMaterial.builder().stream().shader(shader).build(), true));
assertEquals(1, TesrShaderPrograms.bakedBinds - bakedBinds, "the layer hook binds the baked variant once");
assertEquals(1, TesrShaderPrograms.instancedBinds - instancedBinds, "the instanced variant binds once for the layer");
assertEquals(0, GLStateManager.getActiveProgram(), "release must restore the caller's program");
assertEquals(1, groups.instancedDraws, "both instances share one template run");
assertEquals(2, groups.instancedInstances);
bakedBinds = TesrShaderPrograms.bakedBinds;
instancedBinds = TesrShaderPrograms.instancedBinds;
final int[][] retainedPixels = drawShaderLayer(queueShaderPair(TesrMaterial.builder().shader(shader).build(), false));
assertEquals(1, TesrShaderPrograms.bakedBinds - bakedBinds);
assertEquals(0, TesrShaderPrograms.instancedBinds - instancedBinds, "nothing instanced, so no instanced bind");
assertEquals(1, groups.instancedDraws, "the baked draw is not an instanced draw");
for (int i = 0; i < SHADER_PROBES.length; i++) {
assertArrayEquals(retainedPixels[i], instancedPixels[i], "probe " + i + " differs between the baked and instanced variants");
}
assertRed(instancedPixels[0], "instance color must reach the left quad");
assertRed(instancedPixels[1], "instance color must reach the right quad");
assertTrue(instancedPixels[2][0] < 50 && instancedPixels[3][0] < 50, "probes off the quads must stay background");
}
private void drawDepthEqualPair(TesrShader shader, boolean instancing) {
drawDepthEqualPair(shader, instancing, null, new double[3]);
}
private Group[] drawDepthEqualPair(TesrShader shader, boolean instancing, double[] baseOnlyCam, double[] cam) {
final TesrMaterial.Builder baseBuilder = TesrMaterial.builder();
final TesrMaterial.Builder overlayBuilder = TesrMaterial.builder().depthEqual().noDepthWrite();
if (shader != null) overlayBuilder.shader(shader);
final TesrMaterial baseMaterial = (instancing ? baseBuilder.stream() : baseBuilder).build();
final TesrMaterial overlayMaterial = (instancing ? overlayBuilder.stream() : overlayBuilder).build();
final RenderLayer baseLayer = noLightmapLayer(baseMaterial);
final RenderLayer overlayLayer = noLightmapLayer(overlayMaterial);
final TemplateBuffer template = capturedQuad(0.6);
final String path = instancing ? "instanced" : "baked";
final Matrix4f savedProjection = new Matrix4f(GLStateManager.getProjectionMatrix());
GLStateManager.glViewport(0, 0, SIZE, SIZE);
GLStateManager.setProjectionMatrix(new Matrix4f().setPerspective(1.2217f, 1.3713f, 0.0517f, 173.31f));
GLStateManager.disableTexture();
GLStateManager.glDepthMask(true);
FfpFixture.clear();
try {
final Matrix4f view = new Matrix4f().translation(0.0713f, -0.0291f, -2.3177f).rotateX(0.413f);
final Matrix4f pose = new Matrix4f(view).rotateY(0.917f).rotateZ(0.2311f).translate(0.0137f, 0.0219f, 0.0071f);
if (baseOnlyCam != null) {
groups.beginPass(view, baseOnlyCam[0], baseOnlyCam[1], baseOnlyCam[2]);
groups.queue(SCOPE, template, baseLayer, baseMaterial, pose, 0, 0xFF00FF00, 0, 0L, 5, null);
baseLayer.startDrawing();
groups.drawLayer(baseLayer);
baseLayer.endDrawing();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError());
FfpFixture.clear();
}
final long instancedBefore = groups.instancedDraws;
groups.beginPass(view, cam[0], cam[1], cam[2], instancing ? instanced : null);
groups.queue(SCOPE + 1, template, baseLayer, baseMaterial, pose, 0, 0xFF00FF00, 0, 0L, 5, null);
groups.queue(SCOPE + 1, template, overlayLayer, overlayMaterial, pose, 0, COLOR_ABGR, 0, 0L, 5, null);
baseLayer.startDrawing();
groups.drawLayer(baseLayer);
baseLayer.endDrawing();
final int[] base = FfpFixture.readRegion(SIZE);
overlayLayer.startDrawing();
assertEquals(GL11.GL_EQUAL, GL11.glGetInteger(GL11.GL_DEPTH_FUNC));
groups.drawLayer(overlayLayer);
overlayLayer.endDrawing();
instanced.endFrame();
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "depth-equal pair must not raise a GL error");
assertEquals(instancing ? 2 : 0, groups.instancedDraws - instancedBefore, path + ": base and overlay must share the path");
final int[] overlaid = FfpFixture.readRegion(SIZE);
int covered = 0;
int missed = 0;
for (int i = 0; i < base.length; i++) {
if (base[i] == BACKGROUND) {
assertEquals(BACKGROUND, overlaid[i], path + ": overlay drew outside the base at pixel " + i);
continue;
}
assertEquals(0xFF00FF00, base[i], path + ": base pixel " + i);
covered++;
if (overlaid[i] != COLOR_ABGR) missed++;
}
assertTrue(covered > SIZE * SIZE / 20, path + ": base must cover a usable area, covered " + covered);
assertEquals(0, missed, path + ": depth-equal overlay missed " + missed + " of " + covered + " base pixels");
return new Group[] { RetainedTesrTestSupport.groupsOn(groups, baseLayer).get(0), RetainedTesrTestSupport.groupsOn(groups, overlayLayer).get(0) };
} finally {
GLStateManager.setProjectionMatrix(savedProjection);
}
}
@Test
void depthEqualSetMembersBakeAgainstOneAnchor() {
GLStateManager.disableBlend();
GLStateManager.disableAlphaTest();
final double[] first = { 1000.3, 70.6, -2000.7 };
final double[] moved = { 1300.4, 75.8, -1750.4 };
final Group[] pair = drawDepthEqualPair(null, false, first, moved);
assertEquals(TesrAnchorMath.anchorCoord(first[0]), pair[0].anchor.x, "sanity: the move stays inside the re-anchor distance, so the base keeps its first anchor");
assertTrue(pair[0].anchor.sameAs(pair[1].anchor), "an overlay joining later must bake against its writer's anchor");
assertFalse(pair[0].streamPath || pair[1].streamPath, "sanity: both members are on the baked path");
}
@Test
void failedInstancedRelinkDrawsQueuedInstancesOnTheCpuPath() {
final TesrShaderPrograms.Entry entry = RetainedTesrTestSupport.linkedSource("angelica:test_gl_relink_failure_shader");
GLStateManager.disableBlend();
GLStateManager.disableAlphaTest();
final RenderLayer shaderLayer = queueShaderPair(TesrMaterial.builder().stream().shader(entry.shader).build(), true);
assertEquals(1, RetainedTesrTestSupport.onlyGroup(groups).templateColumns.runs.size(), "sanity: both parts queued as instances");
TesrShaderPrograms.invalidateAll();
TesrShaderPrograms.link(entry, false, RetainedTesrTestSupport.COLOR_VERTEX, RetainedTesrTestSupport.COLOR_FRAGMENT);
TesrShaderPrograms.link(entry, true, RetainedTesrTestSupport.BAKED_ONLY_VERTEX, RetainedTesrTestSupport.COLOR_FRAGMENT);
assertFalse(TesrShaderPrograms.instancedAvailable(entry), "sanity: the relinked instanced variant failed");
final int[][] pixels = drawShaderLayer(shaderLayer);
assertEquals(0, groups.instancedDraws, "no instanced program, so no instanced draw");
assertEquals(2, groups.streamedInstances, "both queued instances must draw through the CPU fallback");
assertRed(pixels[0], "left instance must reach the framebuffer");
assertRed(pixels[1], "right instance must reach the framebuffer");
}
@Test
void shaderOverlayDepthEqualsItsEmulatedBaseOnBothPaths() {
final TesrShader shader = RetainedTesrTestSupport.linkedSource("angelica:test_gl_depth_parity_shader").shader;
GLStateManager.disableBlend();
GLStateManager.disableAlphaTest();
drawDepthEqualPair(shader, false);
drawDepthEqualPair(shader, true);
}
}
@@ -1,12 +1,11 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
import com.gtnewhorizons.angelica.api.tesr.TesrShader;
import com.gtnewhorizons.angelica.api.tesr.TesrShaders;
import com.gtnewhorizons.angelica.compat.mojang.RenderLayer;
import com.gtnewhorizons.angelica.glsm.ffp.CubeParams;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import net.coderbot.batchedentityrendering.impl.AngelicaBufferSource;
import net.coderbot.batchedentityrendering.impl.BufferSegment;
import net.coderbot.batchedentityrendering.impl.BufferSourceProbe;
@@ -15,13 +14,24 @@ import net.minecraft.util.ResourceLocation;
import org.joml.Matrix4f;
import org.junit.jupiter.api.BeforeEach;
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.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.opengl.GL11;
import java.util.List;
import java.util.function.IntFunction;
import java.util.stream.Stream;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.VERTEX_SIZE;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.DEPTH_EQUAL_OVERLAY;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.SCOPE;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.STREAM;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.byLayer;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.cubes;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.groupsOn;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.layerFor;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.onlyGroup;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.template;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.testLayer;
@@ -34,15 +44,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class RetainedTesrGroupsTest {
private static final TesrShader NOOP_SHADER = TesrShaders.register("angelica:test_groups_plain", () -> {}, () -> {});
private final InstanceRing ring = new InstanceRing();
private AngelicaBufferSource source;
private RetainedTesrGroups groups;
private RenderLayer layer;
private final long[] clock = {1_000L};
@BeforeEach
void freshGroups() {
source = new AngelicaBufferSource();
groups = new RetainedTesrGroups(source);
groups = new RetainedTesrGroups(source, () -> clock[0]);
layer = testLayer();
}
@@ -56,52 +69,6 @@ class RetainedTesrGroupsTest {
}
}
@Test
void retainedGroupsKeepDifferentItemIdsSeparateAcrossFrames() {
int previous = CapturedRenderingState.INSTANCE.getCurrentRenderedItem();
try {
final TemplateBuffer template = template();
RetainedTesrGroups.Group first = null;
for (int frame = 0; frame < 2; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
for (int item : new int[] {42, 43, 42}) {
CapturedRenderingState.INSTANCE.setCurrentRenderedItem(item);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null);
}
final Reference2ObjectOpenHashMap<RenderLayer, ObjectArrayList<RetainedTesrGroups.Group>> byLayer = Reflect.get(groups, "byLayer");
final ObjectArrayList<RetainedTesrGroups.Group> entries = byLayer.get(layer);
assertEquals(2, entries.size());
assertEquals(42, entries.get(0).itemId);
assertEquals(2, entries.get(0).templateColumns.size);
assertEquals(43, entries.get(1).itemId);
assertEquals(1, entries.get(1).templateColumns.size);
if (first != null) assertSame(first, entries.get(0), "stable groups must be reused across frames");
first = entries.get(0);
}
} finally {
CapturedRenderingState.INSTANCE.setCurrentRenderedItem(previous);
}
}
@Test
void instancedGroupsKeepItemIdsPerInstanceWithoutSplitting() {
int previous = CapturedRenderingState.INSTANCE.getCurrentRenderedItem();
try {
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(true, true));
for (int item : new int[] {42, 43}) {
CapturedRenderingState.INSTANCE.setCurrentRenderedItem(item);
long info = InstancedAttribs.packEntityInfo(-1, 5, item);
groups.queue(template(), layer, STREAM, new Matrix4f(), 0, -1, 0, info, 5, null);
}
final RetainedTesrGroups.Group group = onlyGroup(groups);
assertEquals(2, group.templateColumns.size);
assertEquals(42, RetainedTesrGroups.itemFromInfo(group.templateColumns.infos.getLong(0)));
assertEquals(43, RetainedTesrGroups.itemFromInfo(group.templateColumns.infos.getLong(1)));
} finally {
CapturedRenderingState.INSTANCE.setCurrentRenderedItem(previous);
}
}
@Test
void retainedGroupsSeparateTheFullIdTupleAndTintAndReuseStableKeys() {
final var state = CapturedRenderingState.INSTANCE;
@@ -112,16 +79,18 @@ class RetainedTesrGroupsTest {
for (int frame = 0; frame < 2; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
for (int entity : new int[] {11, 12}) {
state.setCurrentEntityAndItem(entity, 13);
for (int block : new int[] {21, 22}) {
state.setCurrentBlockEntity(block);
for (float red : new float[] {0, 1}) {
state.setCurrentEntityColor(red, 0, 0, 0.5f);
groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, block, null);
for (int item : new int[] {13, 14}) {
state.setCurrentEntityAndItem(entity, item);
for (int block : new int[] {21, 22}) {
state.setCurrentBlockEntity(block);
for (float red : new float[] {0, 1}) {
state.setCurrentEntityColor(red, 0, 0, 0.5f);
groups.queue(SCOPE, template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, block, null);
}
}
}
}
assertEquals(8, groups.groupCount(), "only distinct ID/tint combinations allocate groups, including across frames");
assertEquals(16, groups.groupCount(), "only distinct ID/tint combinations allocate groups, including across frames");
}
} finally {
state.popCurrentEntityColor();
@@ -144,7 +113,7 @@ class RetainedTesrGroupsTest {
state.setCurrentEntityColor(i / 100f, 0, 0, 1);
final long info = InstancedAttribs.packEntityInfo(i, i + 100, i + 200);
final int overlay = AngelicaBufferSource.packEntityColor(state.getCurrentEntityColor());
groups.queue(template(), layer, STREAM, new Matrix4f(), 0, -1, overlay, info, i + 100, null);
groups.queue(SCOPE, template(), layer, STREAM, new Matrix4f(), 0, -1, overlay, info, i + 100, null);
}
final var group = onlyGroup(groups);
assertEquals(100, group.templateColumns.size);
@@ -160,11 +129,31 @@ class RetainedTesrGroupsTest {
}
}
@Test
void retainedGroupAccumulatesInsteadOfStreaming() {
@FunctionalInterface
private interface GroupSetup {
void apply(RetainedTesrGroups groups, RenderLayer layer);
}
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null);
private static void idleGap(RetainedTesrGroups groups) {
for (long i = 0; i <= RetainedTesrGroups.IDLE_RESET_FRAMES + 1; i++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
}
}
static Stream<Arguments> retainedDraws() {
return Stream.of(
Arguments.of("fresh group", (GroupSetup) (g, l) -> g.beginPass(new Matrix4f(), 0, 0, 0)),
Arguments.of("promoted group after an idle gap", (GroupSetup) (g, l) -> {
g.forceStreaming(l, TesrMaterial.CURRENT_STATE, 5);
idleGap(g);
}));
}
@ParameterizedTest(name = "{0}")
@MethodSource("retainedDraws")
void retainedGroupDrawsWithoutSegments(String name, GroupSetup setup) {
setup.apply(groups, layer);
groups.queue(SCOPE, template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null);
assertTrue(groups.hasDraws(layer));
assertEquals(0, groups.streamedInstances);
@@ -173,156 +162,291 @@ class RetainedTesrGroupsTest {
assertNull(BufferSourceProbe.segmentsFor(source, layer), "retained instances must not produce dynamic segments");
}
@Test
void streamingGroupStreamsIntoDynamicSegments() {
private static final TesrMaterial TRANSLUCENT_STREAM = TesrMaterial.builder().translucent().stream().build();
groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5);
assertEquals(1, groups.streamPromotions);
private static void queueAt(RetainedTesrGroups groups, RenderLayer layer, TesrMaterial material, float x, float y, float z) {
groups.queue(SCOPE, template(), layer, material, new Matrix4f().translation(x, y, z), 0, -1, 0, 0L, 5, null);
}
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, 0L, 5, null);
groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f().translation(4f, 5f, 6f), 0, -1, 0, 0L, 5, null);
static Stream<Arguments> streamingDraws() {
return Stream.of(
Arguments.of("forced promotion", (GroupSetup) (g, l) -> {
g.forceStreaming(l, TesrMaterial.CURRENT_STATE, 5);
g.beginPass(new Matrix4f(), 0, 0, 0);
queueAt(g, l, TesrMaterial.CURRENT_STATE, 1f, 2f, 3f);
queueAt(g, l, TesrMaterial.CURRENT_STATE, 4f, 5f, 6f);
}, 2, 1),
Arguments.of("stream material from the first queue", (GroupSetup) (g, l) -> {
g.beginPass(new Matrix4f(), 0, 0, 0);
queueAt(g, l, TRANSLUCENT_STREAM, 1f, 2f, 3f);
}, 1, 0),
Arguments.of("stream material after an idle gap", (GroupSetup) (g, l) -> {
g.beginPass(new Matrix4f(), 0, 0, 0);
queueAt(g, l, TRANSLUCENT_STREAM, 0f, 0f, 0f);
idleGap(g);
queueAt(g, l, TRANSLUCENT_STREAM, 0f, 0f, 0f);
}, 2, 0),
Arguments.of("cube parts without an instanced renderer", (GroupSetup) (g, l) -> {
g.beginPass(new Matrix4f(), 0, 0, 0);
g.queue(SCOPE, template(), cubes(2), 0.0625f, l, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null);
}, 1, 0));
}
@ParameterizedTest(name = "{0}")
@MethodSource("streamingDraws")
void streamingGroupStreamsIntoDynamicSegments(String name, GroupSetup setup, int instances, int promotions) {
setup.apply(groups, layer);
assertFalse(groups.hasDraws(layer), "volatile group must not report retained draws");
assertEquals(2, groups.streamedInstances);
assertEquals(instances, groups.streamedInstances);
assertEquals(promotions, groups.streamPromotions);
assertEquals(0, cubeInstances(groups));
BufferSourceProbe.prepare(source);
final List<BufferSegment> segments = BufferSourceProbe.segmentsFor(source, layer);
assertEquals(1, segments.size());
assertEquals(8, segments.get(0).getVertexCount(), "both instances stream into one segment");
assertEquals(4 * instances, segments.get(0).getVertexCount());
assertEquals(5, segments.get(0).getBlockEntityId());
}
private void probedFrame(TemplateBuffer template, TesrMaterial material, Matrix4f... instances) {
clock[0] += RetainedTesrGroups.STABILITY_PROBE_MS;
groups.beginPass(new Matrix4f(), 0, 0, 0);
for (final Matrix4f mv : instances) groups.queue(SCOPE, template, layer, material, mv, 0, -1, 0, 0L, 5, null);
}
private static IntFunction<Matrix4f[]> perFrame(IntFunction<Matrix4f[]> instances) {
return instances;
}
static Stream<Arguments> probeSequences() {
final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f);
final Matrix4f other = new Matrix4f().translation(4f, 5f, 6f);
final float edge = 1f + 0.5f * TesrAnchorMath.TRANSFORM_TOLERANCE;
final float jitter = TesrAnchorMath.TRANSFORM_TOLERANCE / 8f;
final Matrix4f below = new Matrix4f().translation(edge - jitter, 2f, 3f);
final Matrix4f above = new Matrix4f().translation(edge + jitter, 2f, 3f);
return Stream.of(
Arguments.of("stable opaque", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { fixed }), true),
Arguments.of("reordered", TesrMaterial.CURRENT_STATE, perFrame(i -> i % 2 == 0 ? new Matrix4f[] { fixed, other } : new Matrix4f[] { other, fixed }), true),
Arguments.of("jitter inside tolerance", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { i % 2 == 0 ? below : above }), true),
Arguments.of("moving", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { new Matrix4f().translation(i, 0f, 0f) }), false),
Arguments.of("instance count changes", TesrMaterial.CURRENT_STATE, perFrame(i -> i % 2 == 0 ? new Matrix4f[] { fixed, fixed } : new Matrix4f[] { fixed }), false),
Arguments.of("rotating", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { new Matrix4f().rotateY(0.002f * i) }), false),
Arguments.of("stream material", STREAM, perFrame(i -> new Matrix4f[] { fixed }), false));
}
@ParameterizedTest(name = "{0}")
@MethodSource("probeSequences")
void probedInstancesDecideDemotion(String name, TesrMaterial material, IntFunction<Matrix4f[]> instances, boolean demotes) {
final TemplateBuffer template = template();
if (!material.isStream()) groups.forceStreaming(layer, material, 5);
int frames = 0;
while (frames < 8 && !groups.hasDraws(layer)) {
probedFrame(template, material, instances.apply(frames++));
}
assertEquals(demotes, groups.hasDraws(layer));
if (demotes) assertTrue(frames > RetainedTesrGroups.DEMOTE_AFTER_STABLE_PROBES + 1, "the first probe has nothing to match, took " + frames);
}
@Test
void stableTransformsDemoteStreamingGroup() {
void promotedGroupOnlyProbesOncePerInterval() {
final TemplateBuffer template = template();
final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f);
groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5);
long streamedFrames = 0;
for (int frame = 0; frame < RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4; frame++) {
for (int frame = 0; frame < 64; frame++) {
clock[0] += 1;
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, fixed, 0, -1, 0, 0L, 5, null);
if (groups.hasDraws(layer)) break;
streamedFrames++;
}
assertTrue(groups.hasDraws(layer), "identical frames must demote the group back to retained");
assertTrue(streamedFrames >= RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES, "demotion must wait for the stability window, streamed " + streamedFrames);
}
@Test
void movingTransformsStayStreaming() {
final TemplateBuffer template = template();
groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5);
for (int frame = 0; frame < RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f().translation(frame, 0f, 0f), 0, -1, 0, 0L, 5, null);
assertFalse(groups.hasDraws(layer), "moving group must keep streaming at frame " + frame);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, fixed, 0, -1, 0, 0L, 5, null);
assertFalse(groups.hasDraws(layer), "64 ms cannot hold enough probes to demote, frame " + frame);
}
}
@Test
void idleGapResetsVolatility() {
groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5);
for (long i = 0; i <= RetainedTesrGroups.IDLE_RESET_FRAMES + 1; i++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
}
groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null);
assertTrue(groups.hasDraws(layer), "idle reset must demote the group back to retained");
assertEquals(0, groups.streamedInstances);
private void queueBlock(long scope, TesrMaterial material, int id) {
groups.queue(scope, template(), layer, material, new Matrix4f(), 0, -1, 0, 0L, id, null);
}
@Test
void translucentStreamingGroupNeverDemotes() {
final TesrMaterial translucent = TesrMaterial.builder().translucent().build();
final TemplateBuffer template = template();
final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f);
groups.forceStreaming(layer, translucent, 5);
final int frames = RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4;
for (int frame = 0; frame < frames; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, translucent, fixed, 0, -1, 0, 0L, 5, null);
assertFalse(groups.hasDraws(layer), "translucent group must keep streaming at frame " + frame);
}
assertEquals(frames, groups.streamedInstances);
}
@Test
void streamMaterialStreamsFromFirstQueue() {
final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build();
void depthEqualOverlayCouplesOnlyWithinItsOwnNonZeroRenderScope() {
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template(), layer, stream, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, 0L, 5, null);
queueBlock(SCOPE, TesrMaterial.CURRENT_STATE, 5);
queueBlock(SCOPE, DEPTH_EQUAL_OVERLAY, 5);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6);
queueBlock(0L, TesrMaterial.CURRENT_STATE, 7);
queueBlock(0L, DEPTH_EQUAL_OVERLAY, 7);
assertFalse(groups.hasDraws(layer), "stream group must not report retained draws");
assertEquals(1, groups.streamedInstances);
assertEquals(0, groups.streamPromotions, "streaming from birth is not a promotion");
final ObjectArrayList<RetainedTesrGroups.Group> entries = groupsOn(groups, layer);
assertEquals(5, entries.size());
assertNotNull(entries.get(0).depthEqualSet, "the id-5 writer must belong to a path set");
assertSame(entries.get(0).depthEqualSet, entries.get(1).depthEqualSet, "the overlay must couple with the writer from its own scope");
assertNull(entries.get(2).depthEqualSet, "a writer from another scope must not be coupled");
assertNull(entries.get(3).depthEqualSet, "scope 0 never couples");
assertNull(entries.get(4).depthEqualSet, "scope 0 never couples");
}
BufferSourceProbe.prepare(source);
final List<BufferSegment> segments = BufferSourceProbe.segmentsFor(source, layer);
assertEquals(1, segments.size());
assertEquals(5, segments.get(0).getBlockEntityId());
private static final TesrMaterial STREAM_OVERLAY = TesrMaterial.builder().additiveAlpha().depthEqual().noDepthWrite().stream().build();
private static final TesrMaterial BAKED_PLAIN_SHADER = TesrMaterial.builder().shader(NOOP_SHADER).build();
private void instancedPair(InstancedTemplateRenderer renderer) {
groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5);
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
queueBlock(SCOPE, TesrMaterial.CURRENT_STATE, 5);
queueBlock(SCOPE, DEPTH_EQUAL_OVERLAY, 5);
}
@Test
void streamGroupNeverDemotesUnderStableTransforms() {
for (final TesrMaterial material : new TesrMaterial[] {
TesrMaterial.builder().stream().build(),
TesrMaterial.builder().translucent().stream().build()}) {
freshGroups();
final TemplateBuffer template = template();
final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f);
void promotedGroupJoiningAFreshDepthEqualSetKeepsItsPath() {
final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring);
instancedPair(renderer);
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 5);
queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 5);
final int frames = RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4;
for (int frame = 0; frame < frames; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, material, fixed, 0, -1, 0, 0L, 5, null);
assertFalse(groups.hasDraws(layer), material.transparency() + " stream group must keep streaming at frame " + frame);
}
assertEquals(frames, groups.streamedInstances);
final ObjectArrayList<RetainedTesrGroups.Group> pair = groupsOn(groups, layer);
assertTrue(pair.get(0).streamPath, "joining a fresh set must not demote the promoted writer");
assertTrue(pair.get(1).streamPath, "the overlay must follow its promoted writer");
}
@Test
void bakedGroupAdoptsAnInstancedDepthEqualSetBeforeItsFirstInstance() {
final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring);
instancedPair(renderer);
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 5);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6);
final RetainedTesrGroups.Group joined = groupsOn(groups, layer).get(2);
assertEquals(1, joined.templateColumns.runs.size(), "its first instance must already take the set's instanced path");
}
@Test
void memberThatCannotInstanceDemotesTheDepthEqualSet() {
final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring);
instancedPair(renderer);
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 5);
queueBlock(SCOPE + 1, BAKED_PLAIN_SHADER, 5);
assertFalse(groupsOn(groups, layer).get(0).depthEqualSet.instanced, "a member whose shader has no instanced variant forces the set to baked");
}
@Test
void atlasPartPulledOffTheInstancedPathBakesItsSlotUvs() {
final EntityTextureAtlas.Slot slot = new EntityTextureAtlas.Slot(new EntityTextureAtlas.Page(new ResourceLocation("angelica", "test_page"), 0), 0, 0, 4, 2);
final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring);
for (int frame = 0; frame < 2; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
groups.queue(SCOPE + frame, template(), cubes(1), 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, slot);
queueBlock(SCOPE + frame, DEPTH_EQUAL_OVERLAY, 5);
queueBlock(SCOPE + frame, BAKED_PLAIN_SHADER, 5);
}
final RetainedTesrGroups.Group atlas = groupsOn(groups, layer).get(0);
assertFalse(atlas.streamPath, "sanity: a member that cannot instance must pull the atlas writer off the instanced path");
assertEquals(1, atlas.instTexMatrices.size(), "sanity: the part must take the retained branch");
assertEquals(slot.uvMatrix, atlas.instTexMatrices.get(0), "a baked atlas part must sample its slot, not the whole atlas page");
}
@Test
void idleGapKeepsStreamGroupStreaming() {
final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build();
void mergedDepthEqualSetsKeepTheInstancedPath() {
final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring);
instancedPair(renderer);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 7);
queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 6);
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 5);
queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 6);
final ObjectArrayList<RetainedTesrGroups.Group> entries = groupsOn(groups, layer);
assertSame(entries.get(1).depthEqualSet, entries.get(4).depthEqualSet, "both overlays in one scope must merge their sets");
assertTrue(entries.get(4).depthEqualSet.instanced, "the larger baked set must take the instanced path of the one it absorbs");
}
@Test
void mergedDepthEqualSetsShareTheSurvivorsAnchor() {
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, null);
for (long i = 0; i <= RetainedTesrGroups.IDLE_RESET_FRAMES + 1; i++) {
groups.beginPass(new Matrix4f(), 0, 0, 0);
}
groups.queue(template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, null);
queueBlock(SCOPE, TesrMaterial.CURRENT_STATE, 5);
queueBlock(SCOPE, DEPTH_EQUAL_OVERLAY, 5);
groups.beginPass(new Matrix4f(), 304, 0, 0);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6);
queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 7);
queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 6);
final ObjectArrayList<RetainedTesrGroups.Group> entries = groupsOn(groups, layer);
assertEquals(0, entries.get(0).anchor.x, "sanity: the first set anchored at the origin");
assertEquals(304, entries.get(4).anchor.x, "sanity: the second set anchored at the moved camera");
assertFalse(groups.hasDraws(layer), "idle reset must not move a stream group to retained");
assertEquals(2, groups.streamedInstances);
groups.beginPass(new Matrix4f(), 304, 0, 0);
queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 5);
queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 6);
assertSame(entries.get(1).depthEqualSet, entries.get(4).depthEqualSet, "sanity: both overlays in one scope merge their sets");
assertEquals(0, entries.get(1).anchor.x, "a member that already queued this frame keeps its anchor until the next frame");
groups.beginPass(new Matrix4f(), 304, 0, 0);
queueBlock(SCOPE + 3, TesrMaterial.CURRENT_STATE, 5);
queueBlock(SCOPE + 3, DEPTH_EQUAL_OVERLAY, 5);
assertEquals(304, entries.get(0).anchor.x, "an absorbed writer must take the larger set's anchor");
assertEquals(304, entries.get(1).anchor.x, "an absorbed overlay must take the larger set's anchor");
}
@Test
void streamGroupRoutesToInstanceArraysWhenInstancedCapable() {
final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build();
@ParameterizedTest(name = "instancing {0}")
@ValueSource(booleans = { true, false })
void streamOverlayPinsDepthEqualSetInstancedOrBakesWithItsWriter(boolean available) {
final InstancedTemplateRenderer renderer = available ? new InstancedTemplateRenderer(ring) : null;
for (int frame = 0; frame < 2; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0, renderer);
queueBlock(SCOPE + frame, TesrMaterial.CURRENT_STATE, 5);
queueBlock(SCOPE + frame, STREAM_OVERLAY, 5);
}
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template(), layer, stream, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, 0L, 5, null);
final ObjectArrayList<RetainedTesrGroups.Group> pair = groupsOn(groups, layer);
assertEquals(available, pair.get(0).streamPath, "the writer must take the pinned path");
assertEquals(available, pair.get(1).streamPath, "the stream overlay must share its writer's path");
}
assertTrue(groups.hasDraws(layer), "instanced-capable stream group accumulates for the hook draw");
@ParameterizedTest(name = "stream material {0}")
@ValueSource(booleans = { false, true })
void streamingGroupRoutesToInstanceArraysWhenInstancedCapable(boolean streamMaterial) {
final TesrMaterial material = streamMaterial ? TesrMaterial.builder().translucent().stream().build() : TesrMaterial.builder().additiveAlpha().build();
final long info = InstancedAttribs.packEntityInfo(7, 5, 9);
if (!streamMaterial) groups.forceStreaming(layer, material, 5);
groups.beginPass(new Matrix4f(), 100.5, 64.25, -100.75, new InstancedTemplateRenderer(ring));
groups.queue(SCOPE, template(), layer, material, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, info, 5, null);
assertTrue(groups.hasDraws(layer), "a streaming group accumulates for the instanced hook draw");
assertEquals(0, groups.streamedInstances, "no segment streaming when instanced");
final RetainedTesrGroups.Group group = onlyGroup(groups);
assertEquals(1, group.templateColumns.runs.size());
assertEquals(info, group.templateColumns.infos.getLong(0));
assertEquals(3f, group.templateColumns.matrices[14], "instanced columns hold the caller's modelview, not the anchor-relative one");
BufferSourceProbe.prepare(source);
assertNull(BufferSourceProbe.segmentsFor(source, layer));
}
@ParameterizedTest(name = "stream material {0}")
@ValueSource(booleans = { false, true })
void drawModeMismatchFallsBackToRetained(boolean streamMaterial) {
final TesrMaterial material = streamMaterial ? TesrMaterial.builder().translucent().stream().build() : TesrMaterial.CURRENT_STATE;
final TemplateBuffer triangles = new TemplateBuffer(new int[3 * VERTEX_SIZE], 3, GL11.GL_TRIANGLES);
if (!streamMaterial) groups.forceStreaming(layer, material, 5);
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(SCOPE, triangles, layer, material, new Matrix4f(), 0, -1, 0, 0L, 5, null);
assertTrue(groups.hasDraws(layer), "a template the layer cannot segment-stream falls back to retained accumulation");
assertEquals(0, groups.streamedInstances);
assertEquals(streamMaterial, onlyGroup(groups).streamPath, "a promoted group returns to baked, a stream group keeps streaming");
}
@Test
void streamGroupTexMatrixOpensARun() {
final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build();
final Matrix4f texMatrix = new Matrix4f().translation(0.5f, 0.5f, 0f);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, texMatrix);
groups.queue(SCOPE, template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, texMatrix);
texMatrix.translation(9f, 9f, 9f);
assertTrue(groups.hasDraws(layer), "a texture matrix now opens an instanced run instead of falling back to streaming");
@@ -332,39 +456,24 @@ class RetainedTesrGroupsTest {
assertEquals(new Matrix4f().translation(0.5f, 0.5f, 0f), group.templateColumns.runs.get(0).matrix, "the run holds a copy, not the caller's matrix");
}
@Test
void streamGroupDrawModeMismatchFallsBackToRetained() {
final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build();
final int[] data = new int[3 * VERTEX_SIZE];
final TemplateBuffer triangles = new TemplateBuffer(data, 3, GL11.GL_TRIANGLES);
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(triangles, layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, null);
assertTrue(groups.hasDraws(layer), "mismatched drawMode must fall back to retained accumulation");
assertEquals(0, groups.streamedInstances);
}
@Test
void sweepEvictsIdleGroupsAndKeepsLive() {
final long[] clock = {1_000L};
groups = new RetainedTesrGroups(source, () -> clock[0]);
final TemplateBuffer template = template();
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null);
assertEquals(2, groups.groupCount());
clock[0] += RetainedTesrGroups.GROUP_TTL_MS + 1;
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.sweep(clock[0]);
assertEquals(1, groups.groupCount(), "idle group must be evicted, live group kept");
assertTrue(groups.hasDraws(layer), "live group still draws this frame");
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null);
assertEquals(2, groups.groupCount(), "evicted id must be re-creatable");
}
@@ -373,23 +482,22 @@ class RetainedTesrGroupsTest {
final TemplateBuffer template = template();
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null);
groups.clear();
assertEquals(0, groups.groupCount());
assertFalse(groups.hasDraws(layer));
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
assertEquals(1, groups.groupCount());
assertTrue(groups.hasDraws(layer));
}
private static int cubeInstances(RetainedTesrGroups groups) {
final Reference2ObjectOpenHashMap<RenderLayer, ObjectArrayList<RetainedTesrGroups.Group>> byLayer = Reflect.get(groups, "byLayer");
int total = 0;
for (final ObjectArrayList<RetainedTesrGroups.Group> list : byLayer.values()) {
for (final ObjectArrayList<RetainedTesrGroups.Group> list : byLayer(groups).values()) {
for (int i = 0; i < list.size(); i++) {
final ObjectArrayList<RetainedTesrGroups.TexRun> runs = list.get(i).cubeColumns.runs;
for (int r = 0; r < runs.size(); r++) total += runs.get(r).instances;
@@ -399,57 +507,21 @@ class RetainedTesrGroupsTest {
}
@Test
void cubePartsRouteToPerPartRecordsThatKeepTheirTemplate() {
void cubePartsKeepPerPartRecordsAndTheirTemplateWithoutAccumulatingAcrossFrames() {
final TemplateBuffer template = template();
final Matrix4f mv = new Matrix4f().translation(1f, 2f, 3f);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template, cubes(3), 0.0625f, layer, STREAM, mv, 7, -1, 0, 0L, 5, null, null);
groups.queue(template, cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 7, -1, 0, 0L, 5, null, null);
for (int frame = 0; frame < 2; frame++) {
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(false, false));
groups.queue(SCOPE, template, cubes(3), 0.0625f, layer, STREAM, new Matrix4f().translation(1f, 2f, 3f), 7, -1, 0, 0L, 5, null, null);
groups.queue(SCOPE, template, cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 7, -1, 0, 0L, 5, null, null);
}
assertTrue(groups.hasDraws(layer));
assertEquals(0, groups.streamedInstances);
final RetainedTesrGroups.Group group = onlyGroup(groups);
assertEquals(0, group.templateColumns.size, "cube parts stay out of the template columns");
assertEquals(2, group.cubeColumns.size, "one record per part, not per cube");
assertEquals(5, cubeInstances(groups));
assertTrue(group.cubeColumns.templates.isEmpty(), "the FFP cube path has no fallback that needs the template");
final RetainedTesrGroups piped = new RetainedTesrGroups(source);
piped.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(false, false));
piped.queue(template, cubes(3), 0.0625f, layer, STREAM, mv, 7, -1, 0, 0L, 5, null, null);
piped.queue(template, cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 7, -1, 0, 0L, 5, null, null);
final RetainedTesrGroups.Group pipedGroup = onlyGroup(piped);
assertEquals(2, pipedGroup.cubeColumns.templates.size(), "a pipeline can fall back to the per-part template");
assertSame(template, pipedGroup.cubeColumns.templates.get(0));
}
@Test
void cubePartsFallBackToTemplatesWithoutAnInstancedRenderer() {
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template(), cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null);
assertEquals(0, cubeInstances(groups));
assertEquals(1, groups.streamedInstances);
}
@Test
void cubeInstancesClearBetweenFrames() {
final CubeParams[] params = cubes(2);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template(), params, 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template(), params, 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null);
assertEquals(2, cubeInstances(groups), "the previous frame's instances must not accumulate");
}
@Test
void groupIdOnlySplitsPerEntityWhenAPackIsLoaded() {
assertEquals(ModelPartBatcher.groupId(true, true, -1, 9), RetainedTesrGroups.entityFromInfo(InstancedAttribs.packEntityInfo(-1, 9, 0)));
assertEquals(5, cubeInstances(groups), "the previous frame's instances must not accumulate");
assertEquals(0, group.templateColumns.size, "cube parts stay out of the template columns");
assertEquals(2, group.cubeColumns.templates.size(), "a pipeline can fall back to the per-part template");
assertSame(template, group.cubeColumns.templates.get(0));
}
@Test
@@ -459,10 +531,10 @@ class RetainedTesrGroupsTest {
final Matrix4f matrixB = new Matrix4f().translation(0f, 0.5f, 0f);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA);
groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 2, matrixA);
groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 3, matrixB);
groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 4, matrixA);
groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA);
groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 2, matrixA);
groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 3, matrixB);
groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 4, matrixA);
final RetainedTesrGroups.Group group = onlyGroup(groups);
assertEquals(3, group.templateColumns.runs.size(), "OVERLAY is depth-equal but writes depth, so it must not merge like GLINT");
@@ -475,15 +547,15 @@ class RetainedTesrGroupsTest {
final Matrix4f matrixB = new Matrix4f().translation(0f, 0.5f, 0f);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA);
groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 2, matrixB);
groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 3, matrixA);
groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA);
groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 2, matrixB);
groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 3, matrixA);
final RetainedTesrGroups.Group frame1 = onlyGroup(groups);
assertEquals(2, frame1.templateColumns.runs.size());
assertNotNull(frame1.templateColumns.runs.get(0).next, "sanity: frame 1 must build a chain");
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring));
groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA);
groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA);
final RetainedTesrGroups.Group frame2 = onlyGroup(groups);
assertEquals(1, frame2.templateColumns.runs.size(), "only one distinct matrix was queued this frame");
@@ -494,12 +566,10 @@ class RetainedTesrGroupsTest {
@Test
void sweptGroupIsNotResurrectedByTheMemo() {
final long[] clock = {1_000L};
groups = new RetainedTesrGroups(source, () -> clock[0]);
final TemplateBuffer template = template();
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
assertEquals(1, groups.groupCount());
clock[0] += RetainedTesrGroups.GROUP_TTL_MS + 1;
@@ -507,25 +577,35 @@ class RetainedTesrGroupsTest {
assertEquals(0, groups.groupCount(), "the idle group must be evicted");
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null);
assertEquals(1, groups.groupCount(), "a fresh group must be created, not the evicted one resurrected by the last-group memo");
assertTrue(groups.hasDraws(layer), "the new group must be reachable through the layer map for drawing");
}
private static final TesrMaterial PLAIN_SHADER = TesrMaterial.builder().stream().shader(NOOP_SHADER).build();
private int instancedAfterQueue(TesrMaterial material, TesrInstancingPipeline pipeline) {
final RetainedTesrGroups fresh = new RetainedTesrGroups(source);
fresh.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), pipeline);
fresh.queue(SCOPE, template(), layerFor(material), material, new Matrix4f(), 0, -1, 0, 0L, 5, null);
final RetainedTesrGroups.Group group = onlyGroup(fresh);
return group.templateColumns.runs.isEmpty() ? 0 : group.templateColumns.size;
}
@Test
void alternatingMaterialsRouteToTheirOwnGroups() {
final TemplateBuffer template = template();
final TesrMaterial materialA = TesrMaterial.builder().stream().build();
final TesrMaterial materialB = TesrMaterial.builder().translucent().stream().build();
void runnableShaderMaterialIsInstancedOnlyUnderAPack() {
assertEquals(0, instancedAfterQueue(PLAIN_SHADER, null), "its program cannot read the per-instance attributes");
assertEquals(1, instancedAfterQueue(PLAIN_SHADER, new RetainedTesrTestSupport.CountingPipeline(true, true)), "the pack draws it with its own instanced variant");
groups.beginPass(new Matrix4f(), 0, 0, 0);
groups.queue(template, layer, materialA, new Matrix4f(), 0, -1, 0, 0L, 5, null);
groups.queue(template, layer, materialB, new Matrix4f(), 0, -1, 0, 0L, 5, null);
groups.queue(template, layer, materialA, new Matrix4f(), 0, -1, 0, 0L, 5, null);
groups.queue(template, layer, materialB, new Matrix4f(), 0, -1, 0, 0L, 5, null);
final RenderLayer shaderLayer = layerFor(PLAIN_SHADER);
groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(true, true));
groups.queue(SCOPE, template(), cubes(3), 0.0625f, shaderLayer, PLAIN_SHADER, new Matrix4f(), 0, -1, 0, 0L, 5, null, null);
assertEquals(2, groups.groupCount(), "the last-group memo must compare material, not just layer and id, on every queue call");
final RetainedTesrGroups.Group group = onlyGroup(groups);
assertEquals(0, cubeInstances(groups), "a shader material never takes the cube path");
assertEquals(1, group.templateColumns.size);
assertEquals(1, group.templateColumns.runs.size());
}
@Test
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrMaterial;
import com.gtnewhorizons.angelica.api.tesr.TesrShaders;
import com.gtnewhorizons.angelica.compat.mojang.RenderLayer;
import com.gtnewhorizons.angelica.glsm.ffp.CubeParams;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
@@ -22,10 +23,38 @@ final class RetainedTesrTestSupport {
private RetainedTesrTestSupport() {}
static final long SCOPE = 1L;
static final TesrMaterial STREAM = TesrMaterial.builder().stream().build();
static final TesrMaterial DEPTH_EQUAL_OVERLAY = TesrMaterial.builder().additiveAlpha().depthEqual().noDepthWrite().build();
static final String COLOR_VERTEX = "angelica_out vec4 v_Color;\nvoid main() {\n gl_Position = angelica_clipPosition();\n v_Color = angelica_color();\n}\n";
static final String BAKED_ONLY_VERTEX = "angelica_out vec4 v_Color;\nvoid main() {\n gl_Position = angelica_ProjectionMatrix * (angelica_ModelViewMatrix * vec4(angelica_Position, 1.0));\n v_Color = angelica_color();\n}\n";
static final String COLOR_FRAGMENT = "angelica_in vec4 v_Color;\nvoid main() { angelica_FragColor = v_Color; }\n";
static TesrShaderPrograms.Entry linkedSource(String name) {
final ResourceLocation unread = new ResourceLocation("angelica", "test/unread");
final TesrShaderPrograms.Entry entry = TesrShaderPrograms.entry(TesrShaders.register(name, unread, unread));
TesrShaderPrograms.link(entry, false, COLOR_VERTEX, COLOR_FRAGMENT);
TesrShaderPrograms.link(entry, true, COLOR_VERTEX, COLOR_FRAGMENT);
return entry;
}
static RenderLayer testLayer() {
return RenderLayer.tesr(null, TesrMaterial.CURRENT_STATE, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, true);
return layerFor(TesrMaterial.CURRENT_STATE);
}
static RenderLayer layerFor(TesrMaterial material) {
return RenderLayer.tesr(null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, true);
}
static Reference2ObjectOpenHashMap<RenderLayer, ObjectArrayList<RetainedTesrGroups.Group>> byLayer(RetainedTesrGroups groups) {
return Reflect.get(groups, "byLayer");
}
static ObjectArrayList<RetainedTesrGroups.Group> groupsOn(RetainedTesrGroups groups, RenderLayer layer) {
return byLayer(groups).get(layer);
}
static TemplateBuffer template() {
@@ -48,21 +77,17 @@ final class RetainedTesrTestSupport {
}
static RetainedTesrGroups.Group onlyGroup(RetainedTesrGroups groups) {
final Reference2ObjectOpenHashMap<RenderLayer, ObjectArrayList<RetainedTesrGroups.Group>> byLayer = Reflect.get(groups, "byLayer");
final Reference2ObjectOpenHashMap<RenderLayer, ObjectArrayList<RetainedTesrGroups.Group>> byLayer = byLayer(groups);
assertEquals(1, byLayer.size());
final ObjectArrayList<RetainedTesrGroups.Group> list = byLayer.values().iterator().next();
assertEquals(1, list.size());
return list.get(0);
}
static int liveCull(TesrMaterial material) {
return material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull();
}
static RenderLayer capturedLayer(TesrMaterial material) {
return Reflect.invoke(ModelPartBatcher.INSTANCE, "layerFor",
new Class<?>[] { ResourceLocation.class, TesrMaterial.class, PassOverride.class, float.class, float.class, int.class, int.class, boolean.class, boolean.class },
null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap());
null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap());
}
static final class CountingPipeline implements TesrInstancingPipeline {
@@ -94,16 +94,25 @@ class StateSetOverrideGLTest {
AlphaTestStorage.overrideAlphaTest(new AlphaTest(AlphaTestFunction.LEQUAL, 0.25f));
}
@Test
void blendBracketUnderOverrideRestoresVanillaShadowAndLeavesLiveStateUntouched() {
static Stream<Arguments> blendFuncSetters() {
return Stream.of(
Arguments.of("glBlendFunc", (Runnable) () -> GLStateManager.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ZERO)),
Arguments.of("tryBlendFuncSeparate", (Runnable) () -> GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_ZERO, GL11.GL_DST_COLOR, GL11.GL_ZERO)));
}
@ParameterizedTest(name = "{0}")
@MethodSource("blendFuncSetters")
void blendBracketUnderOverrideRestoresVanillaShadowAndLeavesLiveStateUntouched(String name, Runnable setBlendFunc) {
overrideVanillaBlend();
final boolean liveEnabledBefore = GLStateManager.getBlendMode().isEnabled();
final BlendState liveBefore = new BlendState().set(GLStateManager.getBlendState());
final int d = GLStateManager.pushState(StateSet.BLEND);
final boolean vanillaEnabledBefore = BlendModeStorage.ENABLE_LAYER.getVanilla();
GLStateManager.disableBlend();
GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_ZERO, GL11.GL_DST_COLOR, GL11.GL_ZERO);
assertNotEquals(vanillaEnabledBefore, BlendModeStorage.ENABLE_LAYER.getVanilla(), "the defer branch changes the vanilla shadow immediately");
setBlendFunc.run();
GLStateManager.popStateTo(d);
assertTrue(BlendModeStorage.ENABLE_LAYER.getVanilla(), "vanilla shadow enable restored to the pre-bracket value");
@@ -132,7 +141,18 @@ class StateSetOverrideGLTest {
final int d = GLStateManager.pushState(StateSet.ALPHA);
GLStateManager.disableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_LESS, 0.1f);
final int[] calls = { 0 };
final AlphaState shadow = new AlphaState();
listener = () -> {
calls[0]++;
assertEquals(GL11.GL_LEQUAL, GLStateManager.getAlphaState().getFunction());
assertEquals(0.25f, GLStateManager.getAlphaState().getReference());
assertTrue(GLStateManager.isEffectiveAlphaTestEnabled());
assertEquals(GL11.GL_GREATER, GLStateManager.getEffectiveAlphaState(shadow).getFunction());
assertEquals(0.5f, shadow.getReference());
};
GLStateManager.popStateTo(d);
assertEquals(1, calls[0]);
assertTrue(AlphaTestStorage.ENABLE_LAYER.getVanilla(), "vanilla shadow enable restored to the pre-bracket value");
final AlphaState vanillaAfter = new AlphaState();
@@ -158,7 +178,7 @@ class StateSetOverrideGLTest {
final ColorMask liveColorBefore = new ColorMask().set(GLStateManager.getColorMask());
final int d = GLStateManager.pushState(StateSet.MASKS);
GLStateManager.glDepthMask(true);
GLStateManager.glDepthMask(false);
GLStateManager.glColorMask(true, true, true, true);
GLStateManager.popStateTo(d);
@@ -241,118 +261,6 @@ class StateSetOverrideGLTest {
assertEquals(GL11.GL_GREATER, GLStateManager.getAlphaState().getFunction(), "once released, the bracket pop restores the live cache to the vanilla baseline captured at push time through the vanilla-layer-aware slot, proving the pop actually acted rather than the assertion coincidentally already holding");
}
@Test
void glDepthMaskDeferBranchInsideBracketRestoredAtPop() {
VanillaLayerRig.installDepthColor();
GLStateManager.glDepthMask(true);
DepthColorStorage.disableDepthColor();
final int d = GLStateManager.pushState(StateSet.MASKS);
final boolean liveBefore = GLStateManager.getDepthState().isEnabled();
GLStateManager.glDepthMask(false);
GLStateManager.popStateTo(d);
assertEquals(liveBefore, GLStateManager.getDepthState().isEnabled(), "GLSM's own depth-mask cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was");
final DepthState vanillaAfter = new DepthState();
DepthColorStorage.DEPTH_LAYER.readVanilla(vanillaAfter);
assertTrue(vanillaAfter.isEnabled(), "the bracket pop reverts the vanilla depth-mask shadow to the pre-bracket baseline, undoing the defer branch's write");
}
@Test
void glColorMaskDeferBranchInsideBracketRestoredAtPop() {
VanillaLayerRig.installDepthColor();
GLStateManager.glColorMask(true, true, true, true);
DepthColorStorage.disableDepthColor();
final int d = GLStateManager.pushState(StateSet.MASKS);
final ColorMask liveBefore = new ColorMask().set(GLStateManager.getColorMask());
GLStateManager.glColorMask(false, false, true, true);
GLStateManager.popStateTo(d);
assertEquals(liveBefore.red, GLStateManager.getColorMask().red, "GLSM's own color-mask cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was");
assertEquals(liveBefore.green, GLStateManager.getColorMask().green, "GLSM's own color-mask cache green channel is untouched by the defer branch");
final ColorMask vanillaAfter = new ColorMask();
DepthColorStorage.COLOR_LAYER.readVanilla(vanillaAfter);
assertTrue(vanillaAfter.red && vanillaAfter.green, "the bracket pop reverts the vanilla color-mask shadow to the pre-bracket baseline, undoing the defer branch's write");
}
@Test
void glAlphaFuncDeferBranchInsideBracketRestoredAtPop() {
overrideVanillaAlpha();
final int d = GLStateManager.pushState(StateSet.ALPHA);
final int liveFuncBefore = GLStateManager.getAlphaState().getFunction();
GLStateManager.glAlphaFunc(GL11.GL_LESS, 0.1f);
GLStateManager.popStateTo(d);
assertEquals(liveFuncBefore, GLStateManager.getAlphaState().getFunction(), "GLSM's own alpha-func cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was");
final AlphaState vanillaAfter = new AlphaState();
AlphaTestStorage.FUNC_LAYER.readVanilla(vanillaAfter);
assertEquals(GL11.GL_GREATER, vanillaAfter.getFunction(), "the bracket pop reverts the vanilla alpha-func shadow to the pre-bracket baseline, undoing the defer branch's write");
assertEquals(0.5f, vanillaAfter.getReference(), 0.0001f, "the bracket pop reverts the vanilla alpha-ref shadow to the pre-bracket baseline");
}
static Stream<Arguments> blendFuncSetters() {
return Stream.of(
Arguments.of("glBlendFunc", (Runnable) () -> GLStateManager.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ZERO)),
Arguments.of("tryBlendFuncSeparate", (Runnable) () -> GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_ZERO, GL11.GL_DST_COLOR, GL11.GL_ZERO)));
}
@ParameterizedTest(name = "{0}")
@MethodSource("blendFuncSetters")
void blendFuncDeferBranchInsideBracketRestoredAtPop(String name, Runnable setBlendFunc) {
overrideVanillaBlend();
final int d = GLStateManager.pushState(StateSet.BLEND);
final int liveSrcBefore = GLStateManager.getBlendState().getSrcRgb();
setBlendFunc.run();
GLStateManager.popStateTo(d);
assertEquals(liveSrcBefore, GLStateManager.getBlendState().getSrcRgb(), "GLSM's own blend-func cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was");
final BlendState vanillaAfter = new BlendState();
BlendModeStorage.FUNC_LAYER.readVanilla(vanillaAfter);
assertEquals(GL11.GL_SRC_ALPHA, vanillaAfter.getSrcRgb(), "the bracket pop reverts the vanilla blend-func shadow to the pre-bracket baseline, undoing the defer branch's write");
}
@Test
void enableBlendDeferBranchInsideBracketRestoredAtPop() {
overrideVanillaBlend();
final int d = GLStateManager.pushState(StateSet.BLEND);
final boolean vanillaBefore = BlendModeStorage.ENABLE_LAYER.getVanilla();
GLStateManager.disableBlend();
assertNotEquals(vanillaBefore, BlendModeStorage.ENABLE_LAYER.getVanilla(), "the defer branch changes the vanilla shadow immediately");
GLStateManager.popStateTo(d);
assertEquals(vanillaBefore, BlendModeStorage.ENABLE_LAYER.getVanilla(), "the boolean defer branch is tracked by beforeModify, so the bracket pop reverts the vanilla shadow to the pre-bracket baseline");
}
@Test
void enableAlphaTestDeferBranchInsideBracketRestoredAtPop() {
overrideVanillaAlpha();
final int d = GLStateManager.pushState(StateSet.ALPHA);
final boolean vanillaBefore = AlphaTestStorage.ENABLE_LAYER.getVanilla();
GLStateManager.disableAlphaTest();
GLStateManager.popStateTo(d);
assertEquals(vanillaBefore, AlphaTestStorage.ENABLE_LAYER.getVanilla(), "the boolean defer branch is tracked by beforeModify, so the bracket pop reverts the vanilla shadow to the pre-bracket baseline");
}
@Test
void nonMemberBracketUnderOverrideCarriesToParent() {
overrideVanillaBlend();
@@ -418,33 +326,6 @@ class StateSetOverrideGLTest {
assertEquals(1, calls[0]);
}
@Test
void lockedOverrideRestoresEffectiveStateBeforeNotification() {
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.5f);
VanillaLayerRig.installAlpha();
AlphaTestStorage.overrideAlphaTest(new AlphaTest(AlphaTestFunction.LEQUAL, 0.75f));
final int depth = GLStateManager.pushState(StateSet.ALPHA);
GLStateManager.getAlphaState().beforeModify();
final AlphaState shadow = new AlphaState();
shadow.setFunction(GL11.GL_LESS);
shadow.setReference(0.25f);
AlphaTestStorage.FUNC_LAYER.writeVanilla(shadow);
GLStateManager.getAlphaTest().beforeModify();
AlphaTestStorage.ENABLE_LAYER.setVanilla(false);
final int[] calls = { 0 };
listener = () -> {
calls[0]++;
assertEquals(GL11.GL_LEQUAL, GLStateManager.getAlphaState().getFunction());
assertEquals(0.75f, GLStateManager.getAlphaState().getReference());
assertTrue(GLStateManager.isEffectiveAlphaTestEnabled());
assertEquals(GL11.GL_GREATER, GLStateManager.getEffectiveAlphaState(shadow).getFunction());
assertEquals(0.5f, shadow.getReference());
};
GLStateManager.popStateTo(depth);
assertEquals(1, calls[0]);
}
@Test
void dynamicUniformsUpdateWithoutRebindingAndAfterProgramRestoration() {
GLStateManager.enableAlphaTest();
@@ -2,6 +2,11 @@ package com.gtnewhorizons.angelica.rendering.tesr;
import org.joml.Matrix4f;
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 java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -22,14 +27,6 @@ class TesrAnchorMathTest {
return m;
}
@Test
void anchorCoordRoundsDownToGrid() {
assertEquals(96, TesrAnchorMath.anchorCoord(100.5));
assertEquals(112, TesrAnchorMath.anchorCoord(112.0));
assertEquals(-16, TesrAnchorMath.anchorCoord(-0.5));
assertEquals(-32, TesrAnchorMath.anchorCoord(-16.5));
}
@Test
void shouldReanchorTriggersOnlyBeyondThreshold() {
assertFalse(TesrAnchorMath.shouldReanchor(100, 0, 0, 0, 0, 0));
@@ -48,33 +45,37 @@ class TesrAnchorMathTest {
assertEquals(TE_X - ax, mA.m30(), 1e-2f);
assertEquals(TE_Y - ay, mA.m31(), 1e-2f);
assertEquals(TE_Z - az, mA.m32(), 1e-2f);
final Matrix4f moved = anchorRelative(camX + 137.37, camY + 41.03, camZ - 250.91, ax, ay, az);
assertEquals(TE_X - ax, moved.m30(), 1e-2f);
assertEquals(TE_Y - ay, moved.m31(), 1e-2f);
assertEquals(TE_Z - az, moved.m32(), 1e-2f);
}
@Test
void hashIsInvariantUnderCameraMotion() {
final double camAX = TE_X - 20.0, camAY = TE_Y - 3.0, camAZ = TE_Z + 8.0;
final long ax = TesrAnchorMath.anchorCoord(camAX);
final long ay = TesrAnchorMath.anchorCoord(camAY);
final long az = TesrAnchorMath.anchorCoord(camAZ);
final Matrix4f mA = anchorRelative(camAX, camAY, camAZ, ax, ay, az);
final long h1 = TesrAnchorMath.instanceHash(42, mA, 0x00F000F0, -1);
final double camBX = camAX + 137.37, camBY = camAY + 41.03, camBZ = camAZ - 250.91;
final Matrix4f mB = anchorRelative(camBX, camBY, camBZ, ax, ay, az);
final long h2 = TesrAnchorMath.instanceHash(42, mB, 0x00F000F0, -1);
assertEquals(h1, h2, "camera-only motion must not change the rebuild hash");
static Stream<Arguments> texMatrices() {
final Matrix4f scroll = new Matrix4f().m00(0.9f).m01(0.1f).m10(-0.2f).m11(0.8f).m30(0.25f).m31(0.5f);
final Matrix4f zeros = new Matrix4f().zero();
return Stream.of(
Arguments.of("absent vs absent", null, null, true),
Arguments.of("absent vs zeros", null, zeros, false),
Arguments.of("scroll vs scroll", scroll, scroll, true),
Arguments.of("scroll vs absent", scroll, null, false),
Arguments.of("m00 off", scroll, new Matrix4f(scroll).m00(0.91f), false),
Arguments.of("m01 off", scroll, new Matrix4f(scroll).m01(0.11f), false),
Arguments.of("m10 off", scroll, new Matrix4f(scroll).m10(-0.21f), false),
Arguments.of("m11 off", scroll, new Matrix4f(scroll).m11(0.81f), false),
Arguments.of("m30 off", scroll, new Matrix4f(scroll).m30(0.26f), false),
Arguments.of("m31 off", scroll, new Matrix4f(scroll).m31(0.51f), false),
Arguments.of("zeros vs zeros", zeros, zeros, true),
Arguments.of("zeros vs absent", zeros, null, false));
}
@Test
void reanchorRoundTripKeepsWorldPosition() {
final double camX = TE_X - 20.0, camY = TE_Y, camZ = TE_Z + 20.0;
final long ax1 = TesrAnchorMath.anchorCoord(camX);
final long ax2 = ax1 + 480;
final Matrix4f m1 = anchorRelative(camX, camY, camZ, ax1, 0, 0);
final Matrix4f m2 = anchorRelative(camX, camY, camZ, ax2, 0, 0);
assertEquals(ax1 + m1.m30(), ax2 + m2.m30(), 1e-2);
@ParameterizedTest(name = "{0}")
@MethodSource("texMatrices")
void texMatrixCompareSeparatesAbsentFromPresent(String name, Matrix4f stored, Matrix4f query, boolean same) {
final float[] terms = new float[2 * TesrAnchorMath.TEX_MATRIX_TERMS];
TesrAnchorMath.storeTexMatrix(terms, TesrAnchorMath.TEX_MATRIX_TERMS, stored);
assertEquals(same, TesrAnchorMath.sameTexMatrix(terms, TesrAnchorMath.TEX_MATRIX_TERMS, query));
}
@Test
@@ -86,5 +87,6 @@ class TesrAnchorMathTest {
assertNotEquals(base, TesrAnchorMath.instanceHash(42, mA, 0x00F000F0, 0xFF0000FF), "color");
final Matrix4f moved = new Matrix4f(mA).translate(1f, 0f, 0f);
assertNotEquals(base, TesrAnchorMath.instanceHash(42, moved, 0x00F000F0, -1), "transform");
assertNotEquals(TesrAnchorMath.texMatrixHash(null), TesrAnchorMath.texMatrixHash(new Matrix4f().translation(0.5f, 0f, 0f)), "tex matrix");
}
}
@@ -0,0 +1,33 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class TesrBatchRendererScopeTest {
private final TesrBatchRenderer renderer = TesrBatchRenderer.INSTANCE;
private long current() {
return Reflect.get(renderer, "currentScope");
}
@Test
void nestedBeginKeepsOuterIdOutermostEndReturnsToZeroAndIdsAreUnique() {
renderer.beginScope();
final long outer = current();
assertNotEquals(0L, outer);
renderer.beginScope();
assertEquals(outer, current());
renderer.endScope();
assertEquals(outer, current());
renderer.endScope();
assertEquals(0L, current());
renderer.beginScope();
final long second = current();
renderer.endScope();
assertNotEquals(outer, second);
}
}
@@ -24,13 +24,13 @@ class TesrLayerKeyTest {
@Test
void scratchProbeMatchesCopiedKey() {
final TesrBatchRenderer.LayerKey scratch = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
final TesrBatchRenderer.LayerKey scratch = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.TRANSLUCENT, true, false, true, false, 0.1f, true, TesrMaterial.SpecialRender.GLINT, SHADER, false, EYES, 1.0f, 2.0f, DrawState.CULL_FRONT, true, false);
final TesrBatchRenderer.LayerKey stored = scratch.copy();
assertNotSame(scratch, stored);
assertEquals(scratch, stored);
assertEquals(scratch.hashCode(), stored.hashCode());
scratch.set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
scratch.set(TEX, TesrMaterial.Transparency.TRANSLUCENT, true, false, true, false, 0.1f, true, TesrMaterial.SpecialRender.GLINT, SHADER, false, EYES, 1.0f, 2.0f, DrawState.CULL_FRONT, true, false);
assertEquals(stored, scratch, "re-set scratch still matches stored copy");
}
@@ -60,50 +60,30 @@ class TesrLayerKeyTest {
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, false);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, true, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, true, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, true, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, EYES, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, TRANSLUCENT, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, -1.0f, -1.0f, DrawState.DISABLED, false, true);
assertNotEquals(base, other);
other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertEquals(base, other);
}
@Test
void passSeparatesOtherwiseIdenticalKeys() {
final TesrBatchRenderer.LayerKey plain = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
final TesrBatchRenderer.LayerKey eyes = plain.copy();
eyes.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, EYES, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(plain, eyes);
final TesrBatchRenderer.LayerKey translucent = plain.copy();
translucent.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, TRANSLUCENT, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertNotEquals(plain, translucent);
assertNotEquals(eyes, translucent);
assertEquals(eyes, eyes.copy());
assertEquals(eyes.hashCode(), eyes.copy().hashCode());
// Polygon offset is the other thing a batch outlives -- the coplanar eye overlay needs it at emit time.
final TesrBatchRenderer.LayerKey offset = plain.copy();
offset.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, -1.0f, -1.0f, DrawState.DISABLED, false, true);
assertNotEquals(plain, offset);
assertEquals(offset, offset.copy());
assertEquals(offset.hashCode(), offset.copy().hashCode());
}
@Test
void nullTextureAndShaderHash() {
final TesrBatchRenderer.LayerKey key = new TesrBatchRenderer.LayerKey().set(null, null, false, false, false, false, 0f, false, null, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertEquals(key, key.copy());
assertEquals(key.hashCode(), key.copy().hashCode());
}
@Test
void passOverridesAreInterned() {
assertSame(EYES, PassOverride.of(SpecialCondition.ENTITY_EYES, null, null));
assertSame(TRANSLUCENT, PassOverride.of(null, Boolean.TRUE, null));
assertSame(PassOverride.NONE, PassOverride.of(null, null, null));
assertNotSame(PassOverride.of(null, Boolean.TRUE, null), PassOverride.of(null, Boolean.FALSE, null));
final TesrBatchRenderer.LayerKey a = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, PassOverride.of(SpecialCondition.GLINT, Boolean.FALSE, null), 0.0f, 0.0f, DrawState.DISABLED, false, true);
final TesrBatchRenderer.LayerKey b = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, PassOverride.of(SpecialCondition.GLINT, Boolean.FALSE, null), 0.0f, 0.0f, DrawState.DISABLED, false, true);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
}
@@ -15,28 +15,35 @@ import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.shadercompat.ShaderGlint;
import net.coderbot.batchedentityrendering.impl.AngelicaBufferSource;
import net.coderbot.batchedentityrendering.impl.TransparencyType;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.AlphaTestStorage;
import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition;
import net.coderbot.iris.gl.blending.DepthColorStorage;
import net.coderbot.iris.layer.GbufferPrograms;
import net.coderbot.iris.layer.PassOverride;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.pipeline.PipelineManager;
import net.coderbot.iris.pipeline.WorldRenderingPhase;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.minecraft.util.ResourceLocation;
import org.joml.Matrix4f;
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.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL30;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.capturedLayer;
import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.liveCull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@@ -44,6 +51,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@GLCoreTest
class TesrLayerStateTest {
@@ -69,6 +78,7 @@ class TesrLayerStateTest {
VanillaLayerRig.clear();
ModelPartBatcher.INSTANCE.clear();
TesrShaderPrograms.invalidateAll();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.glMatrixMode(GL11.GL_TEXTURE);
GLStateManager.glLoadIdentity();
@@ -118,33 +128,6 @@ class TesrLayerStateTest {
assertEquals(function, GL11.glGetInteger(GL11.GL_DEPTH_FUNC), "driver depth function");
}
@Test
void entityBatchRestoresDepthBeforeBlockEntities() {
final AngelicaBufferSource source = new AngelicaBufferSource();
final RenderLayer entityLayer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT);
final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() {
@Override
public boolean hasDraws(RenderLayer layer) {
return layer == entityLayer;
}
@Override
public void drawLayer(RenderLayer layer) {
assertTrue(GL11.glIsEnabled(GL11.GL_DEPTH_TEST), "entity draw depth test");
}
};
try {
source.declareUse(entityLayer);
source.endBatchWithType(TransparencyType.OPAQUE, hook);
source.endBatch(hook);
assertTrue(GLStateManager.getDepthTest().isEnabled(), "cached depth before block entities");
assertTrue(GL11.glIsEnabled(GL11.GL_DEPTH_TEST), "driver depth before block entities");
} finally {
source.discard();
source.freeBuffers();
}
}
@Test
void afterGlintLayerDrawsAfterEveryGlintLayer() {
final AngelicaBufferSource source = new AngelicaBufferSource();
@@ -178,11 +161,82 @@ class TesrLayerStateTest {
}
}
@Test
void batchDrawDoesNotInheritTheInterruptedRenderersTextureMatrixOrIrisOverride() {
record BatchScenario(Runnable during, Runnable after, Runnable cleanup) {}
static Stream<Arguments> batchDrawCases() {
return Stream.of(
Arguments.of("texture matrix and iris override", (Supplier<BatchScenario>) () -> {
GLStateManager.setTextureMatrix(0, new Matrix4f().translation(0.25f, 0f, 0f));
GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.GLINT);
GbufferPrograms.pushOverridePhase(WorldRenderingPhase.TERRAIN_CUTOUT);
return new BatchScenario(
() -> {
assertTrue(MatrixHelper.isIdentity(GLStateManager.getTextures().getTextureUnitMatrix(0)), "unit 0 texture matrix during batch draw");
assertNull(GbufferPrograms.getSpecialCondition(), "special condition during batch draw");
assertNull(GbufferPrograms.getOverridePhase(), "override phase during batch draw");
},
() -> {
assertEquals(0.25f, GLStateManager.getTextures().getTextureUnitMatrix(0).m30(), "caller texture matrix restored");
assertEquals(SpecialCondition.GLINT, GbufferPrograms.getSpecialCondition(), "caller special condition restored");
assertEquals(WorldRenderingPhase.TERRAIN_CUTOUT, GbufferPrograms.getOverridePhase(), "caller override phase restored");
},
() -> {
GbufferPrograms.popOverridePhase();
GbufferPrograms.teardownSpecialRenderCondition();
GLStateManager.setTextureMatrix(0, new Matrix4f());
});
}),
Arguments.of("foreign program and framebuffer", (Supplier<BatchScenario>) () -> {
final int program = GlTestSupport.program(GlTestSupport.TRIVIAL_VS, GlTestSupport.TRIVIAL_FS);
final int framebuffer = GLStateManager.glGenFramebuffers();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
BatchDrawDefaults.capturePassFramebuffer();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GLStateManager.glUseProgram(program);
return new BatchScenario(
() -> {
assertEquals(0, GLStateManager.getActiveProgram(), "program during batch draw");
assertEquals(0, GLStateManager.getDrawFramebuffer(), "framebuffer during batch draw");
},
() -> {
assertEquals(program, GLStateManager.getActiveProgram(), "caller program restored");
assertEquals(framebuffer, GLStateManager.getDrawFramebuffer(), "caller framebuffer restored");
},
() -> {
GLStateManager.glUseProgram(0);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(framebuffer);
GLStateManager.glDeleteProgram(program);
});
}),
Arguments.of("filter from before an item renderer's blur window", (Supplier<BatchScenario>) () -> {
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_LINEAR);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
// What TextureUtil.func_152777_a(false, false, 1) does around an item render.
TextureUtilHooks.captureBoundFilterTexture();
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);
return new BatchScenario(
() -> assertEquals(GL11.GL_LINEAR, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "filter from before the window"),
() -> assertEquals(GL11.GL_NEAREST, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "window filter restored"),
() -> {
TextureUtilHooks.clearSavedFilterTexture();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glDeleteTextures(texture);
});
}));
}
@ParameterizedTest(name = "{0}")
@MethodSource("batchDrawCases")
void batchDrawDoesNotInheritTheInterruptedRenderersState(String name, Supplier<BatchScenario> setup) {
final AngelicaBufferSource source = new AngelicaBufferSource();
final RenderLayer layer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT);
final int[] draws = { 0 };
final BatchScenario[] scenario = { null };
final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() {
@Override
public boolean hasDraws(RenderLayer candidate) {
@@ -192,25 +246,17 @@ class TesrLayerStateTest {
@Override
public void drawLayer(RenderLayer drawn) {
draws[0]++;
assertTrue(MatrixHelper.isIdentity(GLStateManager.getTextures().getTextureUnitMatrix(0)), "unit 0 texture matrix during batch draw");
assertNull(GbufferPrograms.getSpecialCondition(), "special condition during batch draw");
assertNull(GbufferPrograms.getOverridePhase(), "override phase during batch draw");
scenario[0].during().run();
}
};
GLStateManager.setTextureMatrix(0, new Matrix4f().translation(0.25f, 0f, 0f));
GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.GLINT);
GbufferPrograms.pushOverridePhase(WorldRenderingPhase.TERRAIN_CUTOUT);
try {
scenario[0] = setup.get();
source.declareUse(layer);
source.endBatch(hook);
assertEquals(1, draws[0]);
assertEquals(0.25f, GLStateManager.getTextures().getTextureUnitMatrix(0).m30(), "caller texture matrix restored");
assertEquals(SpecialCondition.GLINT, GbufferPrograms.getSpecialCondition(), "caller special condition restored");
assertEquals(WorldRenderingPhase.TERRAIN_CUTOUT, GbufferPrograms.getOverridePhase(), "caller override phase restored");
scenario[0].after().run();
} finally {
GbufferPrograms.popOverridePhase();
GbufferPrograms.teardownSpecialRenderCondition();
GLStateManager.setTextureMatrix(0, new Matrix4f());
if (scenario[0] != null) scenario[0].cleanup().run();
source.discard();
source.freeBuffers();
}
@@ -238,86 +284,6 @@ class TesrLayerStateTest {
}
}
@Test
void batchDrawSwapsOutAForeignProgramAndFramebuffer() {
final int program = GlTestSupport.program(GlTestSupport.TRIVIAL_VS, GlTestSupport.TRIVIAL_FS);
final int framebuffer = GLStateManager.glGenFramebuffers();
final AngelicaBufferSource source = new AngelicaBufferSource();
final RenderLayer layer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT);
final int[] draws = { 0 };
final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() {
@Override
public boolean hasDraws(RenderLayer candidate) {
return candidate == layer;
}
@Override
public void drawLayer(RenderLayer drawn) {
draws[0]++;
assertEquals(0, GLStateManager.getActiveProgram(), "program during batch draw");
assertEquals(0, GLStateManager.getDrawFramebuffer(), "framebuffer during batch draw");
}
};
try {
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
BatchDrawDefaults.capturePassFramebuffer();
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GLStateManager.glUseProgram(program);
source.declareUse(layer);
source.endBatch(hook);
assertEquals(1, draws[0]);
assertEquals(program, GLStateManager.getActiveProgram(), "caller program restored");
assertEquals(framebuffer, GLStateManager.getDrawFramebuffer(), "caller framebuffer restored");
} finally {
GLStateManager.glUseProgram(0);
GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLStateManager.glDeleteFramebuffers(framebuffer);
GLStateManager.glDeleteProgram(program);
source.discard();
source.freeBuffers();
}
}
@Test
void batchDrawUsesTheFilterFromBeforeAnItemRenderersBlurWindow() {
final int texture = GLStateManager.glGenTextures();
final AngelicaBufferSource source = new AngelicaBufferSource();
final RenderLayer layer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT);
final int[] draws = { 0 };
final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() {
@Override
public boolean hasDraws(RenderLayer candidate) {
return candidate == layer;
}
@Override
public void drawLayer(RenderLayer drawn) {
draws[0]++;
assertEquals(GL11.GL_LINEAR, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "filter from before the window");
}
};
try {
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_LINEAR);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
// What TextureUtil.func_152777_a(false, false, 1) does around an item render.
TextureUtilHooks.captureBoundFilterTexture();
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);
source.declareUse(layer);
source.endBatch(hook);
assertEquals(1, draws[0]);
assertEquals(GL11.GL_NEAREST, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "window filter restored");
} finally {
TextureUtilHooks.clearSavedFilterTexture();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glDeleteTextures(texture);
source.discard();
source.freeBuffers();
}
}
private static RenderLayer glint(int slot) {
return RenderLayer.tesr(null, EntityMaterials.GLINT, PassOverride.NONE, 0f, 0f, slot, DrawState.CULL_BACK, true, true);
}
@@ -341,10 +307,50 @@ class TesrLayerStateTest {
assertSame(teardown, thrown.getSuppressed()[0]);
}
@Test
void shaderLayerDefersToThePassHookWhileAPackIsActive() {
final List<String> calls = new ArrayList<>();
final TesrMaterial material = TesrMaterial.builder()
.shader(TesrShaders.register("angelica:test_pack_defers", () -> calls.add("bind"), () -> calls.add("release")))
.build();
final PassOverride pass = PassOverride.of(SpecialCondition.BEACON_BEAM, null, null);
final RenderLayer layer = RenderLayer.tesr(null, material, pass, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true);
withPack(true, drawing -> {
layer.startDrawing();
assertSame(SpecialCondition.BEACON_BEAM, GbufferPrograms.getSpecialCondition(), "the pass hook must run in the shader's place");
drawing[0] = false;
layer.endDrawing();
assertNull(GbufferPrograms.getSpecialCondition(), "end must undo what begin ran, not re-read the pack state");
assertTrue(calls.isEmpty(), "a pack draws the material; the mod program is never bound: " + calls);
layer.startDrawing();
drawing[0] = true;
layer.endDrawing();
assertEquals(List.of("bind", "release"), calls, "the same interned layer binds the shader once the pack stops drawing the world");
});
}
private static void withPack(boolean initiallyDrawing, Consumer<boolean[]> body) {
final PipelineManager manager = Iris.getPipelineManager();
final WorldRenderingPipeline previous = manager.getPipelineNullable();
final boolean enabled = Iris.enabled;
final boolean[] drawing = { initiallyDrawing };
final DeferredWorldRenderingPipeline pipeline = mock(DeferredWorldRenderingPipeline.class);
when(pipeline.shouldOverrideShaders()).thenAnswer(inv -> drawing[0]);
Reflect.set(manager, "pipeline", pipeline);
Reflect.setStaticFinal(Iris.class, "enabled", boolean.class, true);
try {
body.accept(drawing);
} finally {
Reflect.setStaticFinal(Iris.class, "enabled", boolean.class, enabled);
Reflect.set(manager, "pipeline", previous);
}
}
private static RenderLayer queuedLayer(TesrMaterial material) {
return Reflect.invoke(TesrBatchRenderer.INSTANCE, "layerFor",
new Class<?>[] { ResourceLocation.class, TesrMaterial.class, PassOverride.class, float.class, float.class, int.class, boolean.class, boolean.class },
null, material, PassOverride.NONE, 0f, 0f, liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap());
null, material, PassOverride.NONE, 0f, 0f, DrawState.liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap());
}
private static void drawInBatch(RenderLayer layer, boolean culled) {
@@ -355,22 +361,31 @@ class TesrLayerStateTest {
GLStateManager.popStateTo(depth);
}
@Test
void queuedCullingSplitsBothLayerCachesAndRestoresTheDrawCaller() {
for (TesrMaterial material : new TesrMaterial[] { EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.GLINT }) {
static Stream<Arguments> layerCaches() {
return Stream.of(
Arguments.of("ModelPartBatcher", (Function<TesrMaterial, RenderLayer>) RetainedTesrTestSupport::capturedLayer,
new TesrMaterial[] { EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.GLINT }),
Arguments.of("TesrBatchRenderer", (Function<TesrMaterial, RenderLayer>) TesrLayerStateTest::queuedLayer,
new TesrMaterial[] { TesrMaterial.CURRENT_STATE }));
}
@ParameterizedTest(name = "{0}")
@MethodSource("layerCaches")
void queuedCullingSplitsLayerCachesAndRestoresTheDrawCaller(String name, Function<TesrMaterial, RenderLayer> layerFor, TesrMaterial[] materials) {
for (TesrMaterial material : materials) {
GLStateManager.disableCull();
final RenderLayer twoSided = capturedLayer(material);
final RenderLayer twoSided = layerFor.apply(material);
GLStateManager.enableCull();
final RenderLayer culled = capturedLayer(material);
final RenderLayer culled = layerFor.apply(material);
assertNotSame(twoSided, culled, "the last-layer shortcut must include culling");
GLStateManager.disableCull();
assertSame(twoSided, capturedLayer(material), "the layer map must retain both states");
assertSame(twoSided, layerFor.apply(material), "the layer map must retain both states");
}
GLStateManager.disableCull();
final RenderLayer twoSided = capturedLayer(EntityMaterials.DROPPED_ITEM_CUTOUT);
final RenderLayer twoSided = layerFor.apply(materials[0]);
GLStateManager.enableCull();
final RenderLayer culled = capturedLayer(EntityMaterials.DROPPED_ITEM_CUTOUT);
final RenderLayer culled = layerFor.apply(materials[0]);
twoSided.startDrawing();
assertFalse(GL11.glIsEnabled(GL11.GL_CULL_FACE));
assertTrue(GL11.glIsEnabled(GL11.GL_DEPTH_TEST));
@@ -384,143 +399,108 @@ class TesrLayerStateTest {
GLStateManager.disableCull();
drawInBatch(culled, true);
assertFalse(GL11.glIsEnabled(GL11.GL_CULL_FACE));
}
@Test
void queuedTemplateCullingSurvivesCacheHitsAndRestoresTheCaller() {
final TesrMaterial material = TesrMaterial.CURRENT_STATE;
GLStateManager.disableCull();
final RenderLayer twoSided = queuedLayer(material);
GLStateManager.enableCull();
final RenderLayer culled = queuedLayer(material);
assertNotSame(twoSided, culled);
GLStateManager.disableCull();
assertSame(twoSided, queuedLayer(material));
drawInBatch(culled, true);
assertFalse(GL11.glIsEnabled(GL11.GL_CULL_FACE));
GLStateManager.enableCull();
drawInBatch(twoSided, false);
assertTrue(GL11.glIsEnabled(GL11.GL_CULL_FACE));
final RenderLayer forcedTwoSided = queuedLayer(TesrMaterial.builder().noCull().build());
drawInBatch(forcedTwoSided, false);
drawInBatch(layerFor.apply(TesrMaterial.builder().noCull().build()), false);
assertTrue(GL11.glIsEnabled(GL11.GL_CULL_FACE));
}
@Test
void glintTintSlotsSplitLayers() {
assertSame(glint(0), glint(0), "the same slot must reuse the layer");
assertNotSame(glint(1), glint(0), "distinct slots must not share a layer");
assertNotSame(glint(ShaderGlint.NO_TINT), glint(0), "an untinted glint must not share a tinted layer");
static Stream<Arguments> axisCases() {
return Stream.of(
Arguments.of("current state over a dirty caller", (Runnable) () -> {
GLStateManager.disableCull();
GLStateManager.glCullFace(GL11.GL_FRONT);
GLStateManager.disableLighting();
GLStateManager.glColorMask(false, false, false, false);
}, TesrMaterial.CURRENT_STATE, (Runnable) () -> {
assertFalse(GLStateManager.getAlphaTest().isEnabled(), "no cutout");
assertTrue(GLStateManager.getDepthTest().isEnabled(), "depth test");
assertEquals(GL11.GL_LEQUAL, GLStateManager.getDepthState().getFunc());
assertTrue(GLStateManager.getDepthState().isEnabled(), "depth write");
assertFalse(GLStateManager.getBlendMode().isEnabled(), "opaque");
assertTrue(GLStateManager.getCullState().isEnabled(), "cull pinned on");
assertEquals(GL11.GL_BACK, GLStateManager.getPolygonState().getCullFaceMode(), "cull face pinned to back");
assertTrue(GLStateManager.getLightingState().isEnabled(), "lit");
assertTrue(GLStateManager.getColorMask().red, "color mask pinned on");
assertFalse(GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL), "no polygon offset");
}),
Arguments.of("every material toggle", (Runnable) () -> { },
TesrMaterial.builder().noCull().unlit().noDepthWrite().cutout(0.5f).translucent().depthEqual().build(), (Runnable) () -> {
assertFalse(GLStateManager.getCullState().isEnabled(), "noCull");
assertFalse(GLStateManager.getLightingState().isEnabled(), "unlit");
assertFalse(GLStateManager.getDepthState().isEnabled(), "noDepthWrite");
assertTrue(GLStateManager.getAlphaTest().isEnabled(), "cutout");
assertEquals(0.5f, GLStateManager.getAlphaState().getReference(), "cutout threshold");
assertTrue(GLStateManager.getBlendMode().isEnabled(), "translucent");
assertEquals(GL11.GL_SRC_ALPHA, GLStateManager.getBlendState().getSrcRgb());
assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, GLStateManager.getBlendState().getDstRgb());
assertEquals(GL11.GL_EQUAL, GLStateManager.getDepthState().getFunc(), "depthEqual");
}));
}
@ParameterizedTest(name = "fog={0}")
@ValueSource(booleans = { false, true })
void drawingLeavesUndeclaredPhasesToTheSurroundingContext(boolean fog) {
if (fog) GLStateManager.enableFog();
final RenderLayer layer = layer(TesrMaterial.CURRENT_STATE);
layer.startDrawing();
assertEquals(fog, GLStateManager.getFogMode().isEnabled(), "fog after startDrawing");
layer.endDrawing();
assertEquals(fog, GLStateManager.getFogMode().isEnabled(), "fog after endDrawing");
@ParameterizedTest(name = "{0}")
@MethodSource("axisCases")
void everyAxisAppliesAbsolutely(String name, Runnable callerState, TesrMaterial material, Runnable asserts) {
callerState.run();
layer(material).startDrawing();
asserts.run();
}
@Test
void everyAxisAppliesAbsolutely() {
GLStateManager.disableCull();
GLStateManager.glCullFace(GL11.GL_FRONT);
GLStateManager.disableLighting();
GLStateManager.glColorMask(false, false, false, false);
static Stream<Arguments> effectiveStateCases() {
return Stream.of(
Arguments.of("alpha test", (Runnable) () -> {
GLStateManager.getAlphaTest().setVanillaLayer(AlphaTestStorage.ENABLE_LAYER);
GLStateManager.getAlphaState().setVanillaLayer(AlphaTestStorage.FUNC_LAYER);
layer(TesrMaterial.CURRENT_STATE).startDrawing();
GLStateManager.disableBlend();
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
assertFalse(GLStateManager.getAlphaTest().isEnabled(), "no cutout");
assertTrue(GLStateManager.getDepthTest().isEnabled(), "depth test");
assertEquals(GL11.GL_LEQUAL, GLStateManager.getDepthState().getFunc());
assertTrue(GLStateManager.getDepthState().isEnabled(), "depth write");
assertFalse(GLStateManager.getBlendMode().isEnabled(), "opaque");
assertTrue(GLStateManager.getCullState().isEnabled(), "cull pinned on");
assertEquals(GL11.GL_BACK, GLStateManager.getPolygonState().getCullFaceMode(), "cull face pinned to back");
assertTrue(GLStateManager.getLightingState().isEnabled(), "lit");
assertTrue(GLStateManager.getColorMask().red, "color mask pinned on");
assertFalse(GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL), "no polygon offset");
AlphaTestStorage.overrideAlphaTest(null);
assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw alpha test is disabled by the pack override; a raw read would give SOLID");
assertSame(EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.itemFromCurrentState(true));
}),
Arguments.of("depth mask", (Runnable) () -> {
GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER);
GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER);
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
DepthColorStorage.disableDepthColor();
assertSame(EntityMaterials.TRANSLUCENT_DEPTH_WRITE, EntityMaterials.fromCurrentState(true), "raw depth mask held off by another pass; the effective vanilla mask must pick the depth-write variant");
}),
Arguments.of("color mask", (Runnable) () -> {
GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER);
GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER);
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.disableBlend();
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
DepthColorStorage.disableDepthColor();
assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw color mask held off by another pass is not a depth-only draw");
DepthColorStorage.deferColorMask(false, false, false, false);
assertSame(EntityMaterials.DEPTH_ONLY_CUTOUT, EntityMaterials.fromCurrentState(true), "vanilla color mask off while held must pick depth-only");
}));
}
@Test
void everyMaterialToggleApplies() {
final TesrMaterial material = TesrMaterial.builder().noCull().unlit().noDepthWrite().cutout(0.5f).translucent().depthEqual().build();
final RenderLayer layer = layer(material);
layer.startDrawing();
assertFalse(GLStateManager.getCullState().isEnabled(), "noCull");
assertFalse(GLStateManager.getLightingState().isEnabled(), "unlit");
assertFalse(GLStateManager.getDepthState().isEnabled(), "noDepthWrite");
assertTrue(GLStateManager.getAlphaTest().isEnabled(), "cutout");
assertEquals(0.5f, GLStateManager.getAlphaState().getReference(), "cutout threshold");
assertTrue(GLStateManager.getBlendMode().isEnabled(), "translucent");
assertEquals(GL11.GL_SRC_ALPHA, GLStateManager.getBlendState().getSrcRgb());
assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, GLStateManager.getBlendState().getDstRgb());
assertEquals(GL11.GL_EQUAL, GLStateManager.getDepthState().getFunc(), "depthEqual");
layer.endDrawing();
assertTrue(GLStateManager.getBlendMode().isEnabled(), "endDrawing must not reset blend");
assertFalse(GLStateManager.getDepthState().isEnabled(), "endDrawing must not reset the depth mask");
assertFalse(GLStateManager.getLightingState().isEnabled(), "endDrawing must not reset lighting");
assertTrue(GLStateManager.getAlphaTest().isEnabled(), "endDrawing must not reset the alpha test");
}
@Test
void effectiveAlphaStateSurvivesAPackOverridingTheRawAlphaTest() {
GLStateManager.getAlphaTest().setVanillaLayer(AlphaTestStorage.ENABLE_LAYER);
GLStateManager.getAlphaState().setVanillaLayer(AlphaTestStorage.FUNC_LAYER);
GLStateManager.disableBlend();
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
AlphaTestStorage.overrideAlphaTest(null);
assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw alpha test is disabled by the pack override; a raw read would give SOLID");
assertSame(EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.itemFromCurrentState(true));
}
@Test
void effectiveDepthMaskSurvivesAnotherPassHoldingItOff() {
GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER);
GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER);
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.enableBlend();
GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
DepthColorStorage.disableDepthColor();
assertSame(EntityMaterials.TRANSLUCENT_DEPTH_WRITE, EntityMaterials.fromCurrentState(true), "raw depth mask held off by another pass; the effective vanilla mask must pick the depth-write variant");
}
@Test
void effectiveColorMaskPicksDepthOnlyWhileAnotherPassHoldsIt() {
GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER);
GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER);
GLStateManager.glEnable(GL11.GL_TEXTURE_2D);
GLStateManager.disableBlend();
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glDepthFunc(GL11.GL_LEQUAL);
GLStateManager.glDepthMask(true);
DepthColorStorage.disableDepthColor();
assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw color mask held off by another pass is not a depth-only draw");
DepthColorStorage.deferColorMask(false, false, false, false);
assertSame(EntityMaterials.DEPTH_ONLY_CUTOUT, EntityMaterials.fromCurrentState(true), "vanilla color mask off while held must pick depth-only");
@ParameterizedTest(name = "{0}")
@MethodSource("effectiveStateCases")
void effectiveStateSurvivesAnotherPassOverridingTheRawState(String name, Runnable body) {
body.run();
}
private static boolean unitTexIdentity() {
@@ -5,7 +5,6 @@ import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink;
import net.minecraft.tileentity.TileEntity;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class TesrProviderDispatchTest {
@@ -17,18 +16,8 @@ class TesrProviderDispatchTest {
@Override public void angelica$build(TesrMeshSink sink, TileEntity te) {}
}
@Test
void nonProviderIsNotHandled() {
assertFalse(TesrProviderDispatch.tryRender(new Object(), null, 0, 0, 0));
}
@Test
void nullMeshKeyFallsBackToNormalRender() {
assertFalse(TesrProviderDispatch.tryRender(new FakeProvider(null), null, 0, 0, 0));
}
@Test
void nullTileEntityResolvesToZeroBlockEntityId() {
assertEquals(0, TesrProviderDispatch.resolveBlockEntityId(null));
}
}
@@ -0,0 +1,50 @@
package com.gtnewhorizons.angelica.rendering.tesr;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
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;
class TesrShaderPreludeTest {
private static final Pattern GLOBAL = Pattern.compile("^(?:layout\\([^)]*\\) )?(?:in|out|uniform|struct|[a-z0-9]+) (?:[A-Za-z0-9_]+ )?([A-Za-z_][A-Za-z0-9_]*)[;(\\[ ]", Pattern.MULTILINE);
@Test
void everyGlobalIsPrefixedAndMainIsLeftToTheBody() {
for (final String source : new String[] { TesrShaderPrelude.vertex(false), TesrShaderPrelude.vertex(true), TesrShaderPrelude.fragment() }) {
final Matcher matcher = GLOBAL.matcher(source);
int globals = 0;
while (matcher.find()) {
globals++;
final String name = matcher.group(1);
assertTrue(name.startsWith("angelica_") || name.startsWith("iris_") || name.equals("gl_Position"), "unprefixed global " + name);
}
assertTrue(globals >= 10, "the scan must see the declarations, saw " + globals);
assertFalse(source.contains("main"));
}
}
static Stream<Arguments> versionedBodies() {
return Stream.of(
Arguments.of("leading directive", "#version 120\nvoid main() {}\n", "void main() {}\n"),
Arguments.of("after whitespace", "\n #version 330 core\nvoid main() {}\n", "void main() {}\n"),
Arguments.of("after a line comment", "// header\n#version 330 core\nvoid main() {}\n", "void main() {}\n"),
Arguments.of("after a block comment", "/* a\n b */ #version 330 core\nvoid main() {}\n", "void main() {}\n"),
Arguments.of("directive only", "#version 330 core", ""),
Arguments.of("no directive", "void main() {}\n", "void main() {}\n"));
}
@ParameterizedTest(name = "{0}")
@MethodSource("versionedBodies")
void stripVersionDropsOnlyALeadingDirective(String name, String body, String expected) {
assertEquals(expected, TesrShaderPrelude.stripVersion("test:strip", body));
}
}
@@ -0,0 +1,358 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.api.tesr.TesrShader;
import com.gtnewhorizons.angelica.api.tesr.TesrShaderUniforms;
import com.gtnewhorizons.angelica.api.tesr.TesrShaders;
import com.gtnewhorizons.angelica.glsm.CompatUniformManager;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.client.resources.IResource;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
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.GL13;
import org.lwjgl.opengl.GL20;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
@GLCoreTest
class TesrShaderProgramsGLTest {
private static final String LIT_VERTEX = """
angelica_out vec2 v_UV; angelica_out vec2 v_Light; angelica_out vec4 v_Color; angelica_out float v_Depth;
void main() { gl_Position = angelica_clipPosition(); vec3 normal = angelica_eyeNormal(); float d0 = max(dot(normal, normalize(angelica_lightDirection(0))), 0.0); float d1 = max(dot(normal, normalize(angelica_lightDirection(1))), 0.0); vec3 light = min(vec3(0.4) + angelica_lightDiffuse(0) * d0 + angelica_lightDiffuse(1) * d1, 1.0); vec4 color = angelica_color(); v_Color = vec4(color.rgb * light, color.a); v_UV = angelica_uv(); v_Light = angelica_lightmapUv(); v_Depth = angelica_fogCoord(); }
""";
private static final String LIT_FRAGMENT = """
angelica_in vec2 v_UV; angelica_in vec2 v_Light; angelica_in vec4 v_Color; angelica_in float v_Depth;
void main() { vec4 color = angelica_sampleTexture(v_UV) * v_Color; if (!angelica_alphaTest(color.a)) discard; color.rgb *= angelica_sampleLightmap(v_Light).rgb; angelica_FragColor = vec4(angelica_fog(color.rgb, v_Depth), color.a); }
""";
private static final String UNLIT_VERTEX = """
angelica_out vec2 v_UV; angelica_out vec4 v_Color; angelica_out float v_Depth;
void main() { gl_Position = angelica_clipPosition(); v_Color = angelica_color(); v_UV = angelica_uv(); v_Depth = angelica_fogCoord(); }
""";
private static final String UNLIT_FRAGMENT = """
angelica_in vec2 v_UV; angelica_in vec4 v_Color; angelica_in float v_Depth;
void main() { vec4 color = angelica_sampleTexture(v_UV) * v_Color; if (!angelica_alphaTest(color.a)) discard; angelica_FragColor = vec4(angelica_fog(color.rgb, v_Depth), color.a); }
""";
private static final String BROKEN_FRAGMENT = "void main() { angelica_FragColor = missing; }\n";
private static int names;
private final Map<ResourceLocation, String> sources = new HashMap<>();
private int reads;
private Minecraft savedMinecraft;
private int lightmapTexture;
private int otherTexture;
@BeforeEach
void installResources() {
ShaderManager.enable();
ShaderManager.getInstance().activate();
lightmapTexture = FfpFixture.solidTexture(255, 255, 255, 255);
otherTexture = FfpFixture.solidTexture(0, 0, 0, 255);
final IResourceManager manager = new IResourceManager() {
@Override
public Set<String> getResourceDomains() {
return Set.of("test");
}
@Override
public IResource getResource(ResourceLocation location) throws IOException {
reads++;
final String body = sources.get(location);
if (body == null) throw new FileNotFoundException(location.toString());
final IResource resource = mock(IResource.class);
when(resource.getInputStream()).thenReturn(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
return resource;
}
@Override
public List<IResource> getAllResources(ResourceLocation location) throws IOException {
return List.of(getResource(location));
}
};
final DynamicTexture lightmap = mock(DynamicTexture.class);
when(lightmap.getGlTextureId()).thenReturn(lightmapTexture);
final EntityRenderer entityRenderer = mock(EntityRenderer.class, withSettings().extraInterfaces(EntityRendererAccessor.class));
when(((EntityRendererAccessor) entityRenderer).getLightmapTexture()).thenReturn(lightmap);
final Minecraft minecraft = mock(Minecraft.class);
when(minecraft.getResourceManager()).thenReturn(manager);
minecraft.entityRenderer = entityRenderer;
savedMinecraft = Reflect.getStatic(Minecraft.class, "theMinecraft");
Reflect.setStatic(Minecraft.class, "theMinecraft", minecraft);
}
@AfterEach
void cleanup() {
GLStateManager.glUseProgram(0);
TesrShaderPrograms.invalidateAll();
Reflect.setStatic(Minecraft.class, "theMinecraft", savedMinecraft);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0);
GLStateManager.glDeleteTextures(lightmapTexture);
GLStateManager.glDeleteTextures(otherTexture);
GLStateManager.disableFog();
final ShaderManager ffp = ShaderManager.getInstance();
if (ffp.isActive()) ffp.deactivate();
ShaderManager.disable();
}
private TesrShaderPrograms.Entry entry(String vertexBody, String fragmentBody, TesrShaderUniforms uniforms) {
final int id = names++;
final ResourceLocation vertex = new ResourceLocation("test", "shaders/" + id + ".vert");
final ResourceLocation fragment = new ResourceLocation("test", "shaders/" + id + ".frag");
sources.put(vertex, vertexBody);
sources.put(fragment, fragmentBody);
final TesrShader shader = TesrShaders.register("test:programs_" + id, vertex, fragment, uniforms);
return TesrShaderPrograms.entry(shader);
}
private static int uniformInt(int program, String name) {
final int location = GL20.glGetUniformLocation(program, name);
assertNotEquals(-1, location, name + " must be active");
final IntBuffer value = BufferUtils.createIntBuffer(1);
GL20.glGetUniform(program, location, value);
return value.get(0);
}
private static void refresh(int program) {
assertTrue(CompatUniformManager.refreshCompatUniforms(program, GLStateManager.ctx()), "a linked variant must be registered for Angelica's uniforms");
}
@Test
void variantsLinkLazilyOnFirstBind() {
final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null);
assertEquals(0, reads, "registration and entry resolution read nothing");
assertFalse(TesrShaderPrograms.instancedAvailable(entry), "an unlinked variant reads as unavailable");
assertEquals(0, reads, "the queue-time availability read must not link");
assertEquals(0, entry.baked.program);
assertEquals(0, entry.instanced.program);
assertTrue(TesrShaderPrograms.bindBaked(entry, false));
assertEquals(2, reads, "one read of each stage serves both variants");
assertNotEquals(0, entry.baked.program);
assertNotEquals(0, entry.instanced.program, "the first layer bind links the instanced variant too");
assertTrue(TesrShaderPrograms.instancedAvailable(entry));
assertEquals(entry.baked.program, GLStateManager.getActiveProgram());
assertTrue(TesrShaderPrograms.bindInstanced(entry));
assertEquals(entry.instanced.program, GLStateManager.getActiveProgram());
TesrShaderPrograms.release(entry);
assertEquals(0, GLStateManager.getActiveProgram(), "release restores the program bound before the layer");
assertTrue(TesrShaderPrograms.bindBaked(entry, false));
TesrShaderPrograms.release(entry);
assertEquals(2, reads, "a ready variant is never read or linked again");
}
@Test
void linkWaitsWhileADisplayListRecords() {
final TesrShaderPrograms.Entry entry = entry(UNLIT_VERTEX, UNLIT_FRAGMENT, null);
final long failures = TesrShaderPrograms.linkFailures;
final int list = GLStateManager.glGenLists(1);
GLStateManager.glNewList(list, GL11.GL_COMPILE);
try {
assertFalse(TesrShaderPrograms.bindBaked(entry, false), "a recording list would capture the link's program switches");
TesrShaderPrograms.release(entry);
} finally {
GLStateManager.glEndList();
GLStateManager.glDeleteLists(list, 1);
}
assertEquals(0, reads);
assertEquals(failures, TesrShaderPrograms.linkFailures, "the variants stay unlinked, not failed");
assertTrue(TesrShaderPrograms.bindBaked(entry, false), "the next bind outside a list links");
TesrShaderPrograms.release(entry);
}
@ParameterizedTest(name = "missing resource {0}")
@ValueSource(booleans = { false, true })
void failedVariantsReportOnceAndAreNotRetried(boolean missingResource) {
final TesrShaderPrograms.Entry entry = entry(UNLIT_VERTEX, missingResource ? UNLIT_FRAGMENT : BROKEN_FRAGMENT, null);
if (missingResource) sources.clear();
final long failures = TesrShaderPrograms.linkFailures;
final long fallbacks = TesrShaderPrograms.fallbacks;
assertFalse(TesrShaderPrograms.bindBaked(entry, false));
assertEquals(0, GLStateManager.getActiveProgram(), "a failed bind leaves the emulation bound");
TesrShaderPrograms.release(entry);
final int readsAfterFirstBind = reads;
assertFalse(TesrShaderPrograms.bindBaked(entry, false));
TesrShaderPrograms.release(entry);
assertEquals(0, entry.baked.program);
assertEquals(2, TesrShaderPrograms.linkFailures - failures, "each variant reports its failure once, not per bind");
assertEquals(2, TesrShaderPrograms.fallbacks - fallbacks);
assertEquals(readsAfterFirstBind, reads, "a failed variant is not retried");
assertEquals(0, GLStateManager.getActiveProgram());
}
@Test
void instancedFailureDisablesInstancingOnly() {
final TesrShaderPrograms.Entry entry = entry(RetainedTesrTestSupport.BAKED_ONLY_VERTEX, RetainedTesrTestSupport.COLOR_FRAGMENT, null);
final long failures = TesrShaderPrograms.linkFailures;
assertTrue(TesrShaderPrograms.bindBaked(entry, false), "the baked variant still draws");
assertEquals(entry.baked.program, GLStateManager.getActiveProgram());
assertFalse(TesrShaderPrograms.instancedAvailable(entry), "the body names a uniform only the baked variant declares");
assertEquals(1, TesrShaderPrograms.linkFailures - failures);
assertFalse(TesrShaderPrograms.bindInstanced(entry));
assertEquals(entry.baked.program, GLStateManager.getActiveProgram(), "a failed instanced bind leaves the baked variant bound");
TesrShaderPrograms.release(entry);
assertEquals(1, TesrShaderPrograms.linkFailures - failures, "a failed variant is not retried");
}
@Test
void invalidateAllRelinks() {
final int[] linked = new int[2];
final int[] bound = new int[2];
final TesrShaderUniforms uniforms = new TesrShaderUniforms() {
@Override
public void angelica$linked(int program, boolean instanced) {
assertEquals(program, GLStateManager.getActiveProgram(), "the variant is bound while locations resolve");
linked[instanced ? 1 : 0]++;
}
@Override
public void angelica$bound(int program, boolean instanced) {
bound[instanced ? 1 : 0]++;
}
};
final TesrShaderPrograms.Entry entry = entry(UNLIT_VERTEX, BROKEN_FRAGMENT, uniforms);
assertFalse(TesrShaderPrograms.bindBaked(entry, false));
TesrShaderPrograms.release(entry);
assertEquals(0, linked[0]);
sources.put(entry.shader.fragment(), UNLIT_FRAGMENT);
TesrShaderPrograms.invalidateAll();
assertTrue(TesrShaderPrograms.bindBaked(entry, false), "a reload retries a failed variant");
TesrShaderPrograms.release(entry);
final int first = entry.baked.program;
assertTrue(GL20.glIsProgram(first));
TesrShaderPrograms.invalidateAll();
assertEquals(0, entry.baked.program);
assertTrue(!GL20.glIsProgram(first) || GL20.glGetProgrami(first, GL20.GL_DELETE_STATUS) == GL11.GL_TRUE, "the stale program is deleted, or flagged while the driver still has it current");
assertTrue(TesrShaderPrograms.bindBaked(entry, false));
TesrShaderPrograms.release(entry);
assertNotEquals(0, entry.baked.program);
assertEquals(2, linked[0], "linked runs once per linked variant");
assertEquals(2, linked[1], "each relink of the baked variant links the instanced one with it");
assertEquals(2, bound[0], "bound runs once per successful bind");
assertEquals(0, bound[1]);
}
@Test
void lightmapBoundOnUnitOneAndRestored() {
final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, otherTexture);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
assertTrue(TesrShaderPrograms.bindBaked(entry, false));
assertEquals(otherTexture, GLStateManager.getBoundTextureForServerState(1), "a layer without the lightmap flag leaves unit 1 alone");
TesrShaderPrograms.release(entry);
assertTrue(TesrShaderPrograms.bindBaked(entry, true));
assertEquals(lightmapTexture, GLStateManager.getBoundTextureForServerState(1));
assertEquals(0, GLStateManager.getActiveTextureUnit(), "the bind must not move the active unit");
assertEquals(1, uniformInt(entry.baked.program, TesrShaderPrelude.LIGHTMAP_SAMPLER));
assertTrue(TesrShaderPrograms.bindInstanced(entry));
assertEquals(lightmapTexture, GLStateManager.getBoundTextureForServerState(1));
TesrShaderPrograms.release(entry);
assertEquals(otherTexture, GLStateManager.getBoundTextureForServerState(1), "release restores the caller's unit-1 texture");
assertEquals(0, GLStateManager.getActiveTextureUnit());
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
assertEquals(otherTexture, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "the driver binding is restored too");
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
@Test
void fogUniformsFollowFogState() {
final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null);
assertTrue(TesrShaderPrograms.bindBaked(entry, false));
final int program = entry.baked.program;
GLStateManager.disableFog();
GLStateManager.glFogi(GL11.GL_FOG_MODE, GL11.GL_LINEAR);
refresh(program);
assertEquals(0, uniformInt(program, "angelica_FogMode"), "disabled fog is mode 0 whatever the fog function");
GLStateManager.enableFog();
refresh(program);
assertEquals(1, uniformInt(program, "angelica_FogMode"));
GLStateManager.glFogi(GL11.GL_FOG_MODE, GL11.GL_EXP);
refresh(program);
assertEquals(2, uniformInt(program, "angelica_FogMode"));
GLStateManager.glFogi(GL11.GL_FOG_MODE, GL11.GL_EXP2);
refresh(program);
assertEquals(3, uniformInt(program, "angelica_FogMode"));
GLStateManager.disableFog();
refresh(program);
assertEquals(0, uniformInt(program, "angelica_FogMode"));
GLStateManager.glFogf(GL11.GL_FOG_START, 2f);
GLStateManager.glFogf(GL11.GL_FOG_END, 10f);
refresh(program);
final FloatBuffer params = BufferUtils.createFloatBuffer(4);
GL20.glGetUniform(program, GL20.glGetUniformLocation(program, "angelica_FogParams"), params);
assertEquals(-1f / 8f, params.get(0), 1e-6f, "a fog range change re-uploads the params");
TesrShaderPrograms.release(entry);
GLStateManager.glFogf(GL11.GL_FOG_START, 0f);
GLStateManager.glFogf(GL11.GL_FOG_END, 1f);
}
@Test
void lightUniformsReachAProgramWithoutAnAmbientField() {
final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null);
assertTrue(TesrShaderPrograms.bindBaked(entry, false));
final int program = entry.baked.program;
final FloatBuffer diffuse = BufferUtils.createFloatBuffer(4);
diffuse.put(0.25f).put(0.5f).put(0.75f).put(1f).flip();
GLStateManager.glLight(GL11.GL_LIGHT1, GL11.GL_DIFFUSE, diffuse);
refresh(program);
final int location = GL20.glGetUniformLocation(program, "angelica_LightSource[1].diffuse");
assertNotEquals(-1, location);
final FloatBuffer uploaded = BufferUtils.createFloatBuffer(4);
GL20.glGetUniform(program, location, uploaded);
assertEquals(0.5f, uploaded.get(1), 1e-6f);
assertEquals(0.75f, uploaded.get(2), 1e-6f);
TesrShaderPrograms.release(entry);
diffuse.clear();
diffuse.put(0f).put(0f).put(0f).put(1f).flip();
GLStateManager.glLight(GL11.GL_LIGHT1, GL11.GL_DIFFUSE, diffuse);
}
}
@@ -7,8 +7,13 @@ import net.coderbot.batchedentityrendering.impl.BatchVertexFormats;
import org.joml.Matrix4f;
import org.joml.Vector3f;
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.nio.ByteBuffer;
import java.util.stream.Stream;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetFloat;
@@ -51,35 +56,20 @@ class VertexTransformTest {
@Test
void writeInstanceTranslatesPositionSetsColorLightKeepsUv() {
final TemplateBuffer template = template(oneVertexUp());
final Matrix4f mv = new Matrix4f().translation(10f, -2f, 3f);
final Matrix4f mv = new Matrix4f().translation(10f, -2f, 3f).rotateX((float) Math.toRadians(90));
final ByteBuffer dest = ByteBuffer.allocateDirect(DEST.getVertexSize());
final long addr = memAddress0(dest);
VertexTransform.writeInstance(addr, DEST, template, mv, new Vector3f(), 0xFF8040C0, 0x00F000F0, null);
assertEquals(10f, memGetFloat(addr + OFF_POS), 1e-6f);
assertEquals(-1f, memGetFloat(addr + OFF_POS + 4), 1e-6f);
assertEquals(3f, memGetFloat(addr + OFF_POS + 8), 1e-6f);
assertEquals(-2f, memGetFloat(addr + OFF_POS + 4), 1e-6f);
assertEquals(4f, memGetFloat(addr + OFF_POS + 8), 1e-6f);
assertEquals(0xFF8040C0, memGetInt(addr + OFF_COLOR));
assertEquals(0.25f, memGetFloat(addr + OFF_TEX), 0f);
assertEquals(0.75f, memGetFloat(addr + OFF_TEX + 4), 0f);
assertEquals(240f, memGetFloat(addr + OFF_LIGHT), 0f); // 0x00F0
assertEquals(240f, memGetFloat(addr + OFF_LIGHT + 4), 0f); // 0x00F0
final int n = memGetInt(addr + OFF_NORMAL);
assertEquals(0f, NormI8.unpackX(n), NORM_EPS);
assertEquals(1f, NormI8.unpackY(n), NORM_EPS);
assertEquals(0f, NormI8.unpackZ(n), NORM_EPS);
}
@Test
void writeInstanceRotatesNormal() {
final TemplateBuffer template = template(oneVertexUp());
final Matrix4f mv = new Matrix4f().rotationX((float) Math.toRadians(90));
final ByteBuffer dest = ByteBuffer.allocateDirect(DEST.getVertexSize());
final long addr = memAddress0(dest);
VertexTransform.writeInstance(addr, DEST, template, mv, new Vector3f(), 0, 0, null);
final int n = memGetInt(addr + OFF_NORMAL);
assertEquals(0f, NormI8.unpackX(n), NORM_EPS);
assertEquals(0f, NormI8.unpackY(n), NORM_EPS);
@@ -101,14 +91,6 @@ class VertexTransformTest {
assertEquals(0xFF0080FF, memGetInt(addr + OFF_COLOR));
}
@Test
void mulColorIsExactForWhiteAndRounds() {
assertEquals(0x12345678, VertexTransform.mulColor(0x12345678, 0xFFFFFFFF));
assertEquals(0x12345678, VertexTransform.mulColor(0xFFFFFFFF, 0x12345678));
assertEquals(0x80, VertexTransform.mulColor(0xFF, 0x80));
assertEquals(0x00, VertexTransform.mulColor(0x7F, 0x00));
}
@Test
void writeInstanceAppliesTextureMatrixToUvThenRestores() {
final TemplateBuffer template = template(oneVertexUp());
@@ -125,25 +107,37 @@ class VertexTransformTest {
assertEquals(0.75f, memGetFloat(addr + OFF_TEX + 4), 0f);
}
@Test
void decodeReadsPositionUvNormalByUsage() {
static Stream<Arguments> decodeCases() {
return Stream.of(
Arguments.of("explicit normal", GL11.GL_POINTS, new float[][] {{5f, 6f, 7f, 0.1f, 0.2f}}, NormI8.pack(1f, 0f, 0f), 1f, 0f, 0f),
Arguments.of("quad seeds its face normal", GL11.GL_QUADS, new float[][] {
{0f, 0f, 0f, 0f, 0f}, {1f, 0f, 0f, 1f, 0f}, {1f, 1f, 0f, 1f, 1f}, {0f, 1f, 0f, 0f, 1f}}, 0, 0f, 0f, 1f));
}
@ParameterizedTest(name = "{0}")
@MethodSource("decodeCases")
void decodeReadsPositionUvNormalByUsage(String name, int drawMode, float[][] vertices, int packedNormal, float nx, float ny, float nz) {
final VertexFormat src = DefaultVertexFormat.POSITION_TEXTURE_NORMAL;
final ByteBuffer buf = ByteBuffer.allocateDirect(src.getVertexSize());
final ByteBuffer buf = ByteBuffer.allocateDirect(src.getVertexSize() * vertices.length);
final long addr = memAddress0(buf);
memPutFloat(addr, 5f);
memPutFloat(addr + 4, 6f);
memPutFloat(addr + 8, 7f);
memPutFloat(addr + 12, 0.1f);
memPutFloat(addr + 16, 0.2f);
memPutInt(addr + 20, NormI8.pack(1f, 0f, 0f));
for (int i = 0; i < vertices.length; i++) {
final long v = addr + (long) i * src.getVertexSize();
for (int f = 0; f < 5; f++) memPutFloat(v + 4L * f, vertices[i][f]);
memPutInt(v + 20, packedNormal);
}
final int[] out = VertexTransform.decode(addr, src, 1);
final int[] out = VertexTransform.decode(addr, src, vertices.length, drawMode);
assertEquals(5f, Float.intBitsToFloat(out[X_INDEX]), 0f);
assertEquals(6f, Float.intBitsToFloat(out[Y_INDEX]), 0f);
assertEquals(7f, Float.intBitsToFloat(out[Z_INDEX]), 0f);
assertEquals(0.1f, Float.intBitsToFloat(out[TEX_X_INDEX]), 0f);
assertEquals(0.2f, Float.intBitsToFloat(out[TEX_Y_INDEX]), 0f);
assertEquals(1f, NormI8.unpackX(out[NORMAL_INDEX]), NORM_EPS);
for (int i = 0; i < vertices.length; i++) {
final int base = i * VERTEX_SIZE;
assertEquals(vertices[i][0], Float.intBitsToFloat(out[base | X_INDEX]), 0f);
assertEquals(vertices[i][1], Float.intBitsToFloat(out[base | Y_INDEX]), 0f);
assertEquals(vertices[i][2], Float.intBitsToFloat(out[base | Z_INDEX]), 0f);
assertEquals(vertices[i][3], Float.intBitsToFloat(out[base | TEX_X_INDEX]), 0f);
assertEquals(vertices[i][4], Float.intBitsToFloat(out[base | TEX_Y_INDEX]), 0f);
assertEquals(nx, NormI8.unpackX(out[base | NORMAL_INDEX]), NORM_EPS);
assertEquals(ny, NormI8.unpackY(out[base | NORMAL_INDEX]), NORM_EPS);
assertEquals(nz, NormI8.unpackZ(out[base | NORMAL_INDEX]), NORM_EPS);
}
}
}