mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-10-25 20:22:22 +00:00
Instance animated TESRs and let mods register shaders for batched TESRs (#2275)
This commit is contained in:
1 parent
32f8eef15b
commit
b446968e66
60 files changed
+3916
-1635
No files matched your search
@@ -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()) {
|
||||
|
||||
+2
-13
@@ -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
@@ -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);
|
||||
|
||||
+14
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
+29
@@ -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)));
|
||||
|
||||
+1
@@ -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();
|
||||
}
|
||||
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+21
-15
@@ -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" }) {
|
||||
|
||||
+78
-21
@@ -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
|
||||
|
||||
+129
-129
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-50
@@ -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);
|
||||
}
|
||||
}
|
||||
+461
-123
@@ -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);
|
||||
}
|
||||
}
|
||||
+332
-252
@@ -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
|
||||
|
||||
+32
-7
@@ -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 {
|
||||
|
||||
+24
-143
@@ -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");
|
||||
}
|
||||
}
|
||||
+33
@@ -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));
|
||||
}
|
||||
}
|
||||
+358
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user