Emulate clip distances on devices without them (#2348)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-11-07 13:42:32 -08:00
1 parent aee989d23b
commit 9493d630a0
49 files changed
+1359 -194

No files matched your search

@@ -21,6 +21,7 @@ public final class SystemProperties {
public static final String GL_PROFILE = System.getProperty("angelica.glProfile", "");
public static final boolean DISABLE_NVIDIA_WORKAROUND = Boolean.getBoolean("angelica.disableNvidiaWorkaround");
public static final boolean DISABLE_LTW_WORKAROUND = Boolean.getBoolean("angelica.disableLtwWorkaround");
public static final boolean EMULATE_CLIP_DISTANCES = Boolean.getBoolean("angelica.emulateClipDistances");
// Redirector
public static final UnmappedGLMode UNMAPPED_GL = parseEnum("angelica.unmappedGL", UnmappedGLMode.WARN, UnmappedGLMode.WARN, UnmappedGLMode.class);
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack;
public final class ClipPlaneBooleanState extends BooleanStateStack {
@@ -13,12 +14,22 @@ public final class ClipPlaneBooleanState extends BooleanStateStack {
this.index = index;
}
@Override
protected boolean forwardsToBackend() {
return !RenderSystem.isClipDistanceEmulated();
}
@Override
protected void onEnabledChanged() {
final int before = owner.clipPlaneEnabledMask;
if (isEnabled()) {
owner.clipPlaneEnabledMask |= (1 << index);
} else {
owner.clipPlaneEnabledMask &= ~(1 << index);
}
owner.clipPlaneGeneration++;
if ((before == 0) != (owner.clipPlaneEnabledMask == 0) && RenderSystem.isClipDistanceEmulated() && GLSMHooks.CLIP_PLANES_IN_USE.hasListeners()) {
GLSMHooks.CLIP_PLANES_IN_USE.post(GLSMHooks.clipPlanesInUseEvent);
}
}
}
@@ -249,7 +249,7 @@ public class CompatUniformManager {
final int cpGen = glCtx.clipPlaneGeneration;
if (cpGen != pu.lastClipPlaneGen) {
pu.lastClipPlaneGen = cpGen;
uploadClipPlanes(locs);
uploadClipPlanes(locs, glCtx.clipPlaneEnabledMask);
}
}
@@ -468,16 +468,14 @@ public class CompatUniformManager {
RENDER_BACKEND.uniform1f(locs[LOC_MAT_BASE + MF_SHININESS], mat.shininess);
}
private static void uploadClipPlanes(int[] locs) {
private static void uploadClipPlanes(int[] locs, int enabledMask) {
if (locs[LOC_CLIP_PLANES_ENABLED] != -1) {
RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], GLSMRasterizer.anyClipPlaneEnabled() ? 1 : 0);
RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], enabledMask != 0 ? 1 : 0);
}
if (locs[LOC_CLIP_PLANES] != -1) {
final ClipPlaneState cps = GLSMRasterizer.getClipPlaneState();
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
cps.putEffectivePlanes(enabledMask, clipPlaneBuf);
clipPlaneBuf.flip();
RENDER_BACKEND.uniform4(locs[LOC_CLIP_PLANES], clipPlaneBuf);
}
@@ -11,11 +11,8 @@ public final class GLESCaps {
private GLESCaps() {}
public static boolean isCapAllowed(int cap, boolean isGLES, boolean hasClipCullDistance) {
public static boolean isCapAllowed(int cap, boolean isGLES) {
if (!isGLES) return true;
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES) {
return hasClipCullDistance;
}
return switch (cap) {
case GL11.GL_LINE_SMOOTH,
GL11.GL_POLYGON_SMOOTH,
@@ -30,7 +30,6 @@ public final class GLSMEnable {
// Handle clip planes dynamically (supports up to MAX_CLIP_PLANES)
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) {
glCtx.clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].enable();
glCtx.clipPlaneGeneration++;
return;
}
@@ -119,7 +118,6 @@ public final class GLSMEnable {
// Handle clip planes dynamically (supports up to MAX_CLIP_PLANES)
if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) {
glCtx.clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].disable();
glCtx.clipPlaneGeneration++;
return;
}
@@ -509,6 +509,14 @@ public final class GLSMRasterizer {
return glCtx.clipPlaneEnabledMask != 0;
}
public static boolean clipEmulationActive() {
return RenderSystem.isClipDistanceEmulated() && anyClipPlaneEnabled();
}
public static boolean clipEmulationActive(GLContextState glCtx) {
return RenderSystem.isClipDistanceEmulated() && anyClipPlaneEnabled(glCtx);
}
public static void glSampleCoverage(float value, boolean invert) {
if (DisplayListManager.isRecording()) {
throw DisplayListManager.unsupportedInList("glSampleCoverage");
@@ -63,7 +63,7 @@ public class RenderSystem {
private static volatile boolean isGLES;
private static volatile int glesVersion;
private static volatile boolean glesDetected;
private static volatile boolean hasClipCullDistance;
private static boolean clipDistanceEmulated;
private static volatile boolean isLTW;
private static volatile boolean ltwDetected;
@@ -132,7 +132,12 @@ public class RenderSystem {
supportsClearTexture = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_clear_texture;
supportsStencilTexturing = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_stencil_texturing;
supportsTextureStorage = isGLES ? glesVersion >= 300 : GLStateManager.capabilities.OpenGL42 || GLStateManager.capabilities.GL_ARB_texture_storage;
hasClipCullDistance = !isGLES || hasExtension("GL_EXT_clip_cull_distance");
final boolean esClipExt = !isGLES || hasExtension("GL_EXT_clip_cull_distance");
final int maxClipDistances = esClipExt ? RENDER_BACKEND.getInteger(GL30.GL_MAX_CLIP_DISTANCES) : 0;
clipDistanceEmulated = SystemProperties.EMULATE_CLIP_DISTANCES || maxClipDistances < GLStateManager.MAX_CLIP_PLANES;
if (clipDistanceEmulated) {
GLStateManager.LOGGER.info("Clip distances emulated (GL_MAX_CLIP_DISTANCES={}, forced={})", maxClipDistances, SystemProperties.EMULATE_CLIP_DISTANCES);
}
// Cache maximum image units
if (supportsImageLoadStore) {
@@ -434,8 +439,8 @@ public class RenderSystem {
return supportsImageLoadStore;
}
public static boolean hasClipCullDistance() {
return hasClipCullDistance;
public static boolean isClipDistanceEmulated() {
return clipDistanceEmulated;
}
private static boolean hasExtension(String name) {
@@ -0,0 +1,17 @@
package com.gtnewhorizons.angelica.glsm.ffp;
public final class ClipDistanceGlsl {
public static final String VS_OUT_DIST0 = "out vec4 angelica_ClipDist0;";
public static final String VS_OUT_DIST1 = "out vec4 angelica_ClipDist1;";
public static final String FS_IN_DIST0 = "in vec4 angelica_ClipDist0;";
public static final String FS_IN_DIST1 = "in vec4 angelica_ClipDist1;";
public static final String FS_DISCARD = "if (any(lessThan(angelica_ClipDist0, vec4(0.0))) || any(lessThan(angelica_ClipDist1, vec4(0.0)))) discard;\n";
private ClipDistanceGlsl() {}
public static void appendVsAssign(StringBuilder sb, String[] distances) {
sb.append(" angelica_ClipDist0 = vec4(").append(distances[0]).append(", ").append(distances[1]).append(", ").append(distances[2]).append(", ").append(distances[3]).append(");\n");
sb.append(" angelica_ClipDist1 = vec4(").append(distances[4]).append(", ").append(distances[5]).append(", ").append(distances[6]).append(", ").append(distances[7]).append(");\n");
}
}
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import java.util.Arrays;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.TexEnvState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
@@ -79,6 +80,7 @@ public final class FragmentKey {
private static final int GLOBAL_BITS = 14;
private static final int BIT_COMBINED_GLINT = 61;
private static final int BIT_GLINT_REPLACE_ALPHA = 62;
private static final int BIT_CLIP_DISCARD = 63;
private static final int BIT_FOG_MODE = 0; // 2 bits
private static final int BIT_ALPHA_TEST = 2; // 1 bit
private static final int BIT_ALPHA_FUNC = 3; // 3 bits
@@ -126,6 +128,7 @@ public final class FragmentKey {
long global = 0;
if (CombinedGlint.isActive()) global |= 1L << BIT_COMBINED_GLINT;
if (CombinedGlint.replacesAlpha()) global |= 1L << BIT_GLINT_REPLACE_ALPHA;
if (GLSMRasterizer.clipEmulationActive(glCtx)) global |= 1L << BIT_CLIP_DISCARD;
// Fog
global |= activeFogMode(glCtx);
@@ -259,6 +262,7 @@ public final class FragmentKey {
public int fogMode() { return (int) (packed[0] & 0x3); }
public boolean combinedGlint() { return (packed[0] & (1L << BIT_COMBINED_GLINT)) != 0; }
public boolean clipDiscard() { return (packed[0] & (1L << BIT_CLIP_DISCARD)) != 0; }
public boolean glintReplaceAlpha() { return (packed[0] & (1L << BIT_GLINT_REPLACE_ALPHA)) != 0; }
public int textureSupersamplingSamples() { return 1 << (int) ((packed[0] >> BIT_TEXTURE_SUPERSAMPLING) & 0x7); }
public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; }
@@ -34,6 +34,9 @@ public final class FragmentShaderGenerator {
}
sb.append("void main() {\n");
if (key.clipDiscard()) {
sb.append(ClipDistanceGlsl.FS_DISCARD);
}
emitLineStipple(sb, key);
emitTextureSampling(sb, key);
emitTexEnvChain(sb, key);
@@ -49,6 +52,9 @@ public final class FragmentShaderGenerator {
private static void emitInputs(StringBuilder sb, FragmentKey key) {
sb.append("// Inputs from vertex shader\n");
sb.append("in vec4 v_Color;\n");
if (key.clipDiscard()) {
sb.append(ClipDistanceGlsl.FS_IN_DIST0).append('\n').append(ClipDistanceGlsl.FS_IN_DIST1).append('\n');
}
if (key.overlayInstanced()) {
sb.append("in vec4 v_Overlay;\n");
}
@@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.LightState;
@@ -330,9 +329,7 @@ public class Uniforms {
private void stageClipPlanes(GLContextState glCtx) {
final ClipPlaneState cps = glCtx.clipPlaneState;
clipPlaneBuf.clear();
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
cps.putEyePlane(i, clipPlaneBuf);
}
cps.putEffectivePlanes(glCtx.clipPlaneEnabledMask, clipPlaneBuf);
final long src = memAddress0(clipPlaneBuf);
for (int i = 0; i < 32 * 4; i += 4) {
putInt(FFPUniformBlock.CLIP_PLANES + i, memGetInt(src + i));
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import org.joml.Matrix4f;
@@ -53,6 +54,7 @@ public final class VertexKey {
private static final int BIT_UNIT23_UV_FROM_UNIT0 = BIT_UNIT_TEXMAT_BASE + MAX_UNITS;
private static final int BIT_LINE_STIPPLE = BIT_UNIT23_UV_FROM_UNIT0 + 1;
private static final int BIT_INSTANCING = BIT_LINE_STIPPLE + 1;
private static final int BIT_CLIP_EMULATED = BIT_INSTANCING + 3;
private static final long INSTANCING_MASK = 0x7L;
static {
@@ -105,6 +107,7 @@ public final class VertexKey {
public int texGenModeQ() { return (int)((packed >> BIT_TEXGEN_Q) & 0x7); }
public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; }
public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); }
public boolean clipEmulated() { return bit(BIT_CLIP_EMULATED); }
public boolean lineStipple() { return bit(BIT_LINE_STIPPLE); }
public boolean lineQuads() { return bit(BIT_LINE_QUADS); }
public Instancing instancing() { return Instancing.VALUES[(int) ((packed >> BIT_INSTANCING) & INSTANCING_MASK)]; }
@@ -238,6 +241,7 @@ public final class VertexKey {
if (GLSMRasterizer.anyClipPlaneEnabled(glCtx)) {
bits |= (1L << BIT_CLIP_PLANES);
if (RenderSystem.isClipDistanceEmulated()) bits |= 1L << BIT_CLIP_EMULATED;
}
if (glCtx.lineStippleActive) {
@@ -279,6 +283,6 @@ public final class VertexKey {
@Override
public String toString() {
return String.format("FFPVertexKey[0x%012X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b lquads=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), lineQuads(), instancing());
return String.format("FFPVertexKey[0x%012X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b clipEmu=%b lquads=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), clipEmulated(), lineQuads(), instancing());
}
}
@@ -111,6 +111,9 @@ public final class VertexShaderGenerator {
if (key.lineStipple()) {
sb.append("flat out vec2 v_LineStart;\n");
}
if (key.clipEmulated()) {
sb.append(ClipDistanceGlsl.VS_OUT_DIST0).append('\n').append(ClipDistanceGlsl.VS_OUT_DIST1).append('\n');
}
if (key.separateSpecular()) {
sb.append("out vec3 v_SpecularColor;\n");
}
@@ -398,10 +401,19 @@ public final class VertexShaderGenerator {
private static void emitClipDistances(StringBuilder sb, VertexKey key) {
sb.append(" // Clip distances\n");
final boolean emulated = key.clipEmulated();
final String[] distances = emulated ? new String[8] : null;
for (int i = 0; i < 8; i++) {
final String own = "dot(u_ClipPlane[" + i + "], eyePos)";
final String distance = key.lineQuads() ? acrossLine(own, "dot(u_ClipPlane[" + i + "], lineEyeOther)") : own;
sb.append(" gl_ClipDistance[").append(i).append("] = ").append(distance).append(";\n");
if (emulated) {
distances[i] = distance;
} else {
sb.append(" gl_ClipDistance[").append(i).append("] = ").append(distance).append(";\n");
}
}
if (emulated) {
ClipDistanceGlsl.appendVsAssign(sb, distances);
}
}
@@ -31,6 +31,7 @@ public final class GLSMHooks {
public static final EventBus<ProgramDeleteEvent> PROGRAM_DELETE = EventBus.create(ProgramDeleteEvent.class);
public static final EventBus<BlendFuncChangeEvent> BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class);
public static final EventBus<FogStateChangeEvent> FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class);
public static final EventBus<ClipPlanesInUseEvent> CLIP_PLANES_IN_USE = EventBus.create(ClipPlanesInUseEvent.class);
public static final EventBus<LightmapCoordsEvent> LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class);
public static final EventBus<AlphaStateChangeEvent> ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class);
public static final EventBus<ShaderColorChangeEvent> SHADER_COLOR_CHANGE = EventBus.create(ShaderColorChangeEvent.class);
@@ -46,6 +47,7 @@ public final class GLSMHooks {
public static final ProgramDeleteEvent programDeleteEvent = new ProgramDeleteEvent();
public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent();
public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent();
public static final ClipPlanesInUseEvent clipPlanesInUseEvent = new ClipPlanesInUseEvent();
public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent();
public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent();
public static final ShaderColorChangeEvent shaderColorChangeEvent = new ShaderColorChangeEvent();
@@ -0,0 +1,5 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
public final class ClipPlanesInUseEvent extends MutableEvent {}
@@ -106,22 +106,22 @@ final class GLSMOwners {
"glGetIntegeri", "glGetIntegeri_v", "glGetQueryObjecti", "glGetQueryObjectui", "glGetQueryObjectui64",
"glGetString", "glGetStringi", "glIsQuery", "glQueryCounter"},
{"com/gtnewhorizons/angelica/glsm/GLSMRasterizer",
"anyClipPlaneEnabled", "disableCull", "disableScissorTest", "enableCull", "enableScissorTest",
"getAutoNormalState", "getClipPlaneState", "getClipPlaneStates", "getCullState", "getDitherState",
"getEdgeFlagState", "getHintState", "getLineSmoothState", "getLineState", "getLineStippleState",
"getMap1Color4State", "getMap1IndexState", "getMap1NormalState", "getMap1TextureCoord1State",
"getMap1TextureCoord2State", "getMap1TextureCoord3State", "getMap1TextureCoord4State",
"getMap1Vertex3State", "getMap1Vertex4State", "getMap2Color4State", "getMap2IndexState",
"getMap2NormalState", "getMap2TextureCoord1State", "getMap2TextureCoord2State", "getMap2TextureCoord3State",
"getMap2TextureCoord4State", "getMap2Vertex3State", "getMap2Vertex4State", "getMultisampleState",
"getPointSmoothState", "getPointSpriteState", "getPointState", "getPolygonOffsetFillState",
"getPolygonOffsetLineState", "getPolygonOffsetPointState", "getPolygonSmoothState", "getPolygonState",
"getPolygonStippleState", "getSampleAlphaToCoverageState", "getSampleAlphaToOneState",
"getSampleCoverageState", "getScissorState", "getScissorTest", "getShadeModelState", "getViewportState",
"glClipPlane", "glCullFace", "glFrontFace", "glGetClipPlane", "glHint", "glLineStipple", "glLineWidth",
"glPointParameter", "glPointParameterf", "glPointParameteri", "glPointSize", "glPolygonMode",
"glPolygonOffset", "glPolygonOffsetClamp", "glPolygonStipple", "glSampleCoverage", "glScissor",
"glShadeModel", "glViewport"},
"anyClipPlaneEnabled", "clipEmulationActive", "disableCull", "disableScissorTest", "enableCull",
"enableScissorTest", "getAutoNormalState", "getClipPlaneState", "getClipPlaneStates", "getCullState",
"getDitherState", "getEdgeFlagState", "getHintState", "getLineSmoothState", "getLineState",
"getLineStippleState", "getMap1Color4State", "getMap1IndexState", "getMap1NormalState",
"getMap1TextureCoord1State", "getMap1TextureCoord2State", "getMap1TextureCoord3State",
"getMap1TextureCoord4State", "getMap1Vertex3State", "getMap1Vertex4State", "getMap2Color4State",
"getMap2IndexState", "getMap2NormalState", "getMap2TextureCoord1State", "getMap2TextureCoord2State",
"getMap2TextureCoord3State", "getMap2TextureCoord4State", "getMap2Vertex3State", "getMap2Vertex4State",
"getMultisampleState", "getPointSmoothState", "getPointSpriteState", "getPointState",
"getPolygonOffsetFillState", "getPolygonOffsetLineState", "getPolygonOffsetPointState",
"getPolygonSmoothState", "getPolygonState", "getPolygonStippleState", "getSampleAlphaToCoverageState",
"getSampleAlphaToOneState", "getSampleCoverageState", "getScissorState", "getScissorTest",
"getShadeModelState", "getViewportState", "glClipPlane", "glCullFace", "glFrontFace", "glGetClipPlane",
"glHint", "glLineStipple", "glLineWidth", "glPointParameter", "glPointParameterf", "glPointParameteri",
"glPointSize", "glPolygonMode", "glPolygonOffset", "glPolygonOffsetClamp", "glPolygonStipple",
"glSampleCoverage", "glScissor", "glShadeModel", "glViewport"},
{"com/gtnewhorizons/angelica/glsm/GLSMTexEnvGen",
"glGetTexEnv", "glGetTexEnvf", "glGetTexEnvi", "glGetTexGen", "glGetTexGend", "glGetTexGenf",
"glGetTexGeni", "glTexEnv", "glTexEnvf", "glTexEnvi", "glTexGen", "glTexGend", "glTexGenf", "glTexGeni"},
@@ -23,8 +23,12 @@ public class BooleanState implements ISettableState<BooleanState> {
this.ffpStateOnly = ffpStateOnly;
}
protected boolean forwardsToBackend() {
return !ffpStateOnly;
}
public void applyToBackend() {
if (ffpStateOnly) return;
if (!forwardsToBackend()) return;
if (enabled) {
RENDER_BACKEND.enable(glCap);
} else {
@@ -51,7 +55,7 @@ public class BooleanState implements ISettableState<BooleanState> {
stateUnknown = false;
this.enabled = enabled;
onEnabledChanged();
if (!ffpStateOnly) {
if (forwardsToBackend()) {
if (enabled) {
RENDER_BACKEND.enable(this.glCap);
} else {
@@ -27,10 +27,16 @@ public class ClipPlaneState {
dest[3] = tempPlane.w;
}
/** Writes the 4-component eye-space plane equation into {@code buf} at its current position. */
public void putEyePlane(int index, FloatBuffer buf) {
final float[] p = eyePlanes[index];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
// Vulkan and clip emulation have no per-distance enable: a disabled plane must read as distance 0.
public void putEffectivePlanes(int enabledMask, FloatBuffer buf) {
for (int i = 0; i < eyePlanes.length; i++) {
if ((enabledMask & (1 << i)) != 0) {
final float[] p = eyePlanes[i];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
} else {
buf.put(0f).put(0f).put(0f).put(0f);
}
}
}
public void getEyePlane(int index, DoubleBuffer buf) {
@@ -40,7 +40,7 @@ class GLESCapsTest {
};
private static boolean onGles(int cap) {
return GLESCaps.isCapAllowed(cap, true, true);
return GLESCaps.isCapAllowed(cap, true);
}
@Test
@@ -60,25 +60,10 @@ class GLESCapsTest {
@Test
void desktopContextAllowsEveryCap() {
for (int cap : DESKTOP_ONLY) {
assertTrue(GLESCaps.isCapAllowed(cap, false, false), () -> "cap 0x" + Integer.toHexString(cap) + " is legal on desktop core and the gate must never suppress it there");
assertTrue(GLESCaps.isCapAllowed(cap, false), () -> "cap 0x" + Integer.toHexString(cap) + " is legal on desktop core and the gate must never suppress it there");
}
for (int cap : VALID_ON_GLES) {
assertTrue(GLESCaps.isCapAllowed(cap, false, false));
assertTrue(GLESCaps.isCapAllowed(cap, false));
}
}
@Test
void clipPlanesFollowClipCullDistance() {
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
final int cap = GL11.GL_CLIP_PLANE0 + i;
assertTrue(GLESCaps.isCapAllowed(cap, true, true), () -> "clip plane " + cap + " works where EXT_clip_cull_distance is present and must not be suppressed");
assertFalse(GLESCaps.isCapAllowed(cap, true, false), () -> "clip plane " + cap + " is invalid on GLES without EXT_clip_cull_distance");
}
}
@Test
void capsOutsideTheClipPlaneRangeAreUnaffectedByClipCullDistance() {
assertTrue(GLESCaps.isCapAllowed(GL11.GL_BLEND, true, false));
assertTrue(GLESCaps.isCapAllowed(GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES, true, false));
}
}
@@ -0,0 +1,124 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLSMAttribStack;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.GL15;
import java.nio.FloatBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@GLCoreTest
class ClipPlaneGLTest {
private static final int SIZE = 64;
private static final int BACKGROUND = 0xFF000000;
private static final int QUAD = 0xFF0000FF;
@BeforeEach
void setUp() {
FfpFixture.resetFfpState();
FfpFixture.activateFfp();
GLSMRasterizer.glViewport(0, 0, SIZE, SIZE);
GLSMMatrices.glMatrixMode(GL11.GL_PROJECTION);
GLSMMatrices.glLoadIdentity();
GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW);
GLSMMatrices.glLoadIdentity();
GLSMEnable.glDisable(GL11.GL_DEPTH_TEST);
}
@AfterEach
void tearDown() {
for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) {
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0 + i);
}
FfpFixture.resetFfpState();
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void disabledPlaneDoesNotClip(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
ClipTestSupport.setPlane(1, 0, 0, 0, -1);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE1);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
ClipTestSupport.setPlane(0, 1, 0, 0, 0);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
assertRightHalfOnly(drawQuad(), "plane1 disabled, plane0 enabled");
}
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void attribPopRestoresClipping(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
ClipTestSupport.setPlane(0, 1, 0, 0, 0);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
assertDriverClipPlane0(!emulated, "after glEnable");
GLSMAttribStack.glPushAttrib(GL11.GL_ENABLE_BIT);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
final int[] unclipped = drawQuad();
GLSMAttribStack.glPopAttrib();
assertDriverClipPlane0(!emulated, "after glPopAttrib");
GLSMAttribStack.replayStateToBackend();
assertDriverClipPlane0(!emulated, "after replayStateToBackend");
final int[] restored = drawQuad();
for (int i = 0; i < unclipped.length; i++) {
final int px = i;
assertEquals(QUAD, unclipped[i], () -> "plane disabled inside the pushed scope, pixel " + px % SIZE + "," + px / SIZE);
}
assertRightHalfOnly(restored, "after glPopAttrib");
}
}
private static void assertDriverClipPlane0(boolean expected, String when) {
assertEquals(expected, GL11.glIsEnabled(GL11.GL_CLIP_PLANE0), () -> when + ": clip plane reaches the driver only when clip distances are native");
}
private static void assertRightHalfOnly(int[] pixels, String what) {
for (int i = 0; i < pixels.length; i++) {
final int x = i % SIZE, y = i / SIZE;
assertEquals(x < SIZE / 2 ? BACKGROUND : QUAD, pixels[i], () -> what + ": first mismatch at pixel " + x + "," + y);
}
}
private static int[] drawQuad() {
final int vao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(vao);
final int vbo = GLSMBuffers.glGenBuffers();
try {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
final FloatBuffer positions = BufferUtils.createFloatBuffer(18);
positions.put(new float[] { -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0 }).flip();
GLSMBuffers.glBufferData(GL15.GL_ARRAY_BUFFER, positions, GL15.GL_STATIC_DRAW);
FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, 12, 0);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
VAOManager.setCurrentVertexFlags(0);
FfpFixture.clear();
GLSMCurrentAttribs.glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
GLSMDraws.glDrawArrays(GL11.GL_TRIANGLES, 0, 6);
GLSMVertexInput.glBindVertexArray(0);
return FfpFixture.readRegion(SIZE);
} finally {
GLSMBuffers.glDeleteBuffers(vbo);
GLSMVertexInput.glDeleteVertexArrays(vao);
}
}
}
@@ -7,6 +7,7 @@ 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.PointerBuffer;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.shaderc.Shaderc;
@@ -55,6 +56,30 @@ class FFPUniformBlockSpirvLayoutTest {
MemoryUtil.memFree(result.spirv());
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void clipDistanceCapabilityFollowsEmulation(boolean emulated) {
long vbits = bit("BIT_CLIP_PLANES");
if (emulated) vbits |= bit("BIT_CLIP_EMULATED");
final String vs = VertexShaderGenerator.generate(VertexKey.fromPacked(vbits));
assertEquals(!emulated, declaresClipDistance(vs, Shaderc.shaderc_vertex_shader), "vertex ClipDistance capability");
}
private static boolean declaresClipDistance(String glsl, int shaderKind) {
final SpirvCompiler.Result result = SpirvCompiler.compile(glsl, shaderKind, "clip-emulation-test", SpirvCompiler.Options.vulkanForced460Core());
assertNotNull(result.spirv(), () -> "SPIR-V compile failed: " + result.error() + "\n" + glsl);
try {
final IntBuffer words = result.spirv().asIntBuffer();
for (int i = 5; i < words.limit(); i += Math.max(1, words.get(i) >>> 16)) {
final int word = words.get(i);
if ((word & 0xFFFF) == 17 && words.get(i + 1) == 32) return true;
}
return false;
} finally {
MemoryUtil.memFree(result.spirv());
}
}
private static VertexKey cubeKey(boolean lightmapUnit, boolean unit0TextureMatrix, boolean texGen) {
long bits = bit("BIT_HAS_VERTEX_COLOR") | bit("BIT_HAS_VERTEX_NORMAL") | bit("BIT_HAS_VERTEX_TEX") | bit("BIT_UNIT_TEX_BASE");
if (lightmapUnit) bits |= 1L << (position("BIT_UNIT_TEX_BASE") + 1);
@@ -3,21 +3,21 @@ package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs;
import com.gtnewhorizons.angelica.glsm.GLSMDepthStencilAlpha;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMFog;
import com.gtnewhorizons.angelica.glsm.GLSMLighting;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.GLSMTexEnvGen;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import java.nio.DoubleBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -32,12 +32,6 @@ class FfpUniformBlockDedupTest {
return GL30.glGetInteger(GL31.GL_UNIFORM_BUFFER_START, FFPUniformBlock.BINDING_POINT);
}
private static DoubleBuffer doubles(double... values) {
final DoubleBuffer buf = BufferUtils.createDoubleBuffer(values.length);
buf.put(values).flip();
return buf;
}
private static void resetState() {
GLSMMatrices.glMatrixMode(GL11.GL_TEXTURE);
GLSMMatrices.glLoadIdentity();
@@ -58,7 +52,8 @@ class FfpUniformBlockDedupTest {
GLSMLighting.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, floats(1.0f, 1.0f, 1.0f, 1.0f));
GLSMLighting.glMaterialf(GL11.GL_FRONT, GL11.GL_SHININESS, 0.0f);
GLSMTexEnvGen.glTexGen(GL11.GL_S, GL11.GL_OBJECT_PLANE, floats(1.0f, 0.0f, 0.0f, 0.0f));
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, doubles(0.0, 0.0, 0.0, 0.0));
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
ClipTestSupport.setPlane(0, 0.0, 0.0, 0.0, 0.0);
GLSMRasterizer.glLineStipple(1, (short) 0xFFFF);
GLSMRasterizer.glViewport(0, 0, 854, 480);
}
@@ -149,7 +144,10 @@ class FfpUniformBlockDedupTest {
addCategory(tests, "currentTexCoord", () -> GLSMCurrentAttribs.glTexCoord2f(0.125f, 0.375f));
addCategory(tests, "lightmapCoord", () -> GLStateManager.ctx().lastBrightnessX = 96.0f);
addCategory(tests, "texGenPlane", () -> GLSMTexEnvGen.glTexGen(GL11.GL_S, GL11.GL_OBJECT_PLANE, floats(0.5f, 0.0f, 0.0f, 0.25f)));
addCategory(tests, "clipPlane", () -> GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, doubles(0.0, 1.0, 0.0, 7.0)));
addCategory(tests, "clipPlane", () -> {
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
ClipTestSupport.setPlane(0, 0.0, 1.0, 0.0, 7.0);
});
addCategory(tests, "alphaRef", () -> GLSMDepthStencilAlpha.glAlphaFunc(GL11.GL_GREATER, 0.35f));
addCategory(tests, "fogColor", () -> GLSMFog.glFog(GL11.GL_FOG_COLOR, floats(0.1f, 0.2f, 0.3f, 0.4f)));
addCategory(tests, "fogDensity", () -> GLSMFog.glFogf(GL11.GL_FOG_DENSITY, 0.75f));
@@ -0,0 +1,37 @@
package com.gtnewhorizons.angelica.glsm.testutil;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.DoubleBuffer;
public final class ClipTestSupport {
private ClipTestSupport() {}
public static void setPlane(int index, double a, double b, double c, double d) {
final DoubleBuffer eq = BufferUtils.createDoubleBuffer(4);
eq.put(a).put(b).put(c).put(d).flip();
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0 + index, eq);
}
public static Emulation emulation(boolean emulated) {
return new Emulation(emulated);
}
public static final class Emulation implements AutoCloseable {
private final boolean previous;
private Emulation(boolean emulated) {
previous = Reflect.getStatic(RenderSystem.class, "clipDistanceEmulated");
Reflect.setStatic(RenderSystem.class, "clipDistanceEmulated", emulated);
}
@Override
public void close() {
Reflect.setStatic(RenderSystem.class, "clipDistanceEmulated", previous);
}
}
}
@@ -288,7 +288,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
public void finish() {GL11C.glFinish();}
private static boolean capAllowed(int cap) {
return GLESCaps.isCapAllowed(cap, RenderSystem.isGLES(), RenderSystem.hasClipCullDistance());
return GLESCaps.isCapAllowed(cap, RenderSystem.isGLES());
}
@Override
@@ -12,11 +12,14 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11C;
import org.lwjgl.opengl.GL12C;
@@ -153,6 +156,32 @@ class GlesSmokeTest {
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
}
@ParameterizedTest(name = "emulated={0}")
@ValueSource(booleans = {false, true})
void clipPlaneKeepsOnlyTheEnabledHalfSpace(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
try {
bindClearedTarget();
ClipTestSupport.setPlane(1, 0.0, 0.0, 0.0, -1.0);
ClipTestSupport.setPlane(0, 1.0, 0.0, 0.0, 0.0);
GLSMEnable.glEnable(GL30C.GL_CLIP_DISTANCE0);
GLSMDraws.glBegin(GL11C.GL_QUADS);
GLSMCurrentAttribs.glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f);
GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f);
GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f);
GLSMDraws.glEnd();
assertEquals(0xFF000000, readPixel(SIZE / 4, SIZE / 2), "x < 0 must be clipped by plane 0");
assertEquals(0xFF00FF00, readPixel(3 * SIZE / 4, SIZE / 2), "x >= 0 must survive; plane 1 set but not enabled must not clip");
assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError());
} finally {
GLSMEnable.glDisable(GL30C.GL_CLIP_DISTANCE0);
}
}
}
@Test
void proxyTextureProbeFindsTheMaxTextureSize() {
final int maxSize = Integer.highestOneBit(GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE));
@@ -173,9 +202,13 @@ class GlesSmokeTest {
}
private static int readCenterPixel() {
return readPixel(SIZE / 2, SIZE / 2);
}
private static int readPixel(int x, int y) {
final ByteBuffer pixel = MemoryUtil.memAlloc(4);
try {
GLSMFramebuffer.glReadPixels(SIZE / 2, SIZE / 2, 1, 1, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixel);
GLSMFramebuffer.glReadPixels(x, y, 1, 1, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixel);
return (pixel.get(3) & 0xFF) << 24 | (pixel.get(0) & 0xFF) << 16 | (pixel.get(1) & 0xFF) << 8 | pixel.get(2) & 0xFF;
} finally {
MemoryUtil.memFree(pixel);
+8
View File
@@ -0,0 +1,8 @@
# Clip-plane scene for -Dangelica.debug.testBlocks=true. ~ is the flight start, yaw 0 faces south (+z).
# Each angelica:test_clip_plane TESR keeps only the lower half of a gold cube (plane 0) and leaves a reject-all plane 1 disabled.
gamerule doMobSpawning false
gamerule doDaylightCycle false
time set 6000
fill ~-2 ~-1 ~4 ~2 ~0 ~4 angelica:test_clip_plane
@@ -114,6 +114,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import org.lwjgl.util.spvc.Spvc;
import org.lwjgl.sdl.SDLGPU;
import org.lwjgl.sdl.SDL_GPUBlitInfo;
import org.lwjgl.sdl.SDLTimer;
@@ -2547,17 +2548,23 @@ public class SDLGPURenderBackend extends RenderBackend {
if (length != null && length.remaining() > 0) length.put(0, 0);
return;
}
if (s == null) return;
final byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
final int n = Math.min(bytes.length, out.remaining());
out.put(bytes, 0, n);
final byte[] bytes = (s == null ? "" : s).getBytes(StandardCharsets.UTF_8);
final int pos = out.position();
final int max = out.remaining();
final int n = Math.max(0, Math.min(bytes.length, max - 1));
for (int i = 0; i < n; i++) out.put(pos + i, bytes[i]);
if (max > 0) out.put(pos + n, (byte) 0);
if (length != null && length.remaining() > 0) length.put(0, n);
}
private static int glStringLength(String s) {
return s == null || s.isEmpty() ? 0 : s.getBytes(StandardCharsets.UTF_8).length + 1;
}
@Override public int getShaderi(int shader, int pname) {
return switch (pname) {
case GL20.GL_COMPILE_STATUS -> shaderManager.isCompiled(shader) ? 1 : 0;
case GL20.GL_SHADER_TYPE -> shaderManager.getShaderType(shader);
case GL20.GL_INFO_LOG_LENGTH -> shaderManager.getShaderInfoLog(shader).length();
case GL20.GL_INFO_LOG_LENGTH -> glStringLength(shaderManager.getShaderInfoLog(shader));
case GL20.GL_SHADER_SOURCE_LENGTH -> glStringLength(shaderManager.getShaderSource(shader));
default -> 0;
};
}
@@ -2565,7 +2572,7 @@ public class SDLGPURenderBackend extends RenderBackend {
return switch (pname) {
case GL20.GL_LINK_STATUS -> shaderManager.isLinked(program) ? 1 : 0;
case GL20.GL_VALIDATE_STATUS -> shaderManager.isLinked(program) ? 1 : 0;
case GL20.GL_INFO_LOG_LENGTH -> shaderManager.getProgramInfoLog(program).length();
case GL20.GL_INFO_LOG_LENGTH -> glStringLength(shaderManager.getProgramInfoLog(program));
case GL20.GL_ATTACHED_SHADERS -> shaderManager.getAttachedShaderCount(program);
case GL20.GL_ACTIVE_UNIFORMS -> {
final var entries = getOrBuildUniformEntries(program);
@@ -2605,6 +2612,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int loc = e.getValue();
int glType;
int arrayLen = 1;
boolean array = false;
if (prog.allSamplerNames.contains(name)) {
glType = GL20.GL_SAMPLER_2D;
} else {
@@ -2613,6 +2621,7 @@ public class SDLGPURenderBackend extends RenderBackend {
if (umi != null) {
glType = FormatMap.mapMemberInfoToGlType(umi);
arrayLen = Math.max(1, umi.arrayLen());
array = umi.arrayStride() > 0;
} else {
glType = GL20.GL_FLOAT_VEC4;
}
@@ -2620,7 +2629,7 @@ public class SDLGPURenderBackend extends RenderBackend {
glType = FormatMap.remapUnsignedIntToBool(glType);
}
}
result.add(new UniformEntry(name, glType, arrayLen));
result.add(new UniformEntry(array ? name + "[0]" : name, glType, arrayLen));
}
result.sort(Comparator.comparing(UniformEntry::name));
final List<UniformEntry> immutable = Collections.unmodifiableList(result);
@@ -2679,6 +2688,78 @@ public class SDLGPURenderBackend extends RenderBackend {
@Override public int getUniformLocation(int program, ByteBuffer name) {
return shaderManager.getUniformLocation(program, MemoryUtil.memASCII(name));
}
@Override public void getUniformfv(int program, int location, FloatBuffer params) {
final Readback rb = readback(program, location);
if (rb == null) return;
final int pos = params.position();
final int count = Math.min(rb.count(), params.remaining());
for (int i = 0; i < count; i++) {
final float raw = rb.raw(i);
final int bits = Float.floatToRawIntBits(raw);
params.put(pos + i, switch (rb.type()) {
case FLOAT -> raw;
case INT -> (float) bits;
case UINT -> (float) (bits & 0xFFFFFFFFL);
case BOOL -> bits != 0 ? 1f : 0f;
case OPAQUE -> rb.opaqueValue();
});
}
}
@Override public void getUniformiv(int program, int location, IntBuffer params) {
final Readback rb = readback(program, location);
if (rb == null) return;
final int pos = params.position();
final int count = Math.min(rb.count(), params.remaining());
for (int i = 0; i < count; i++) {
final float raw = rb.raw(i);
final int bits = Float.floatToRawIntBits(raw);
params.put(pos + i, switch (rb.type()) {
case FLOAT -> roundHalfAwayFromZero(raw);
case BOOL -> bits != 0 ? 1 : 0;
case INT, UINT -> bits;
case OPAQUE -> rb.opaqueValue();
});
}
}
private enum ReadType { FLOAT, INT, UINT, BOOL, OPAQUE }
private record Readback(float[] storage, int start, int count, ReadType type, int opaqueValue) {
float raw(int i) {
return storage != null && start + i < storage.length ? storage[start + i] : 0f;
}
}
private Readback readback(int program, int location) {
final ShaderManager.ProgramObject prog = shaderManager.getProgram(program);
if (prog == null || !prog.linked || location < 0 || location >= prog.uniformSlotCount) return null;
final int kind = prog.locationKind[location];
if (kind != ShaderManager.ProgramObject.LOCATION_KIND_PLAIN) {
return new Readback(null, 0, 1, ReadType.OPAQUE, opaqueUnit(prog, location, kind));
}
final ShaderManager.UniformMemberInfo info = prog.memberInfo(location);
if (info == null) return null;
final int perElement = info.vectorSize() * info.columns();
final int index = prog.arrayIndexBySlot[location];
final int base = location - index;
final float[] storage = s().uniformStaging(prog).uniformDataBySlot[base];
return new Readback(storage, index * perElement, perElement, readType(prog, base, info), 0);
}
private static ReadType readType(ShaderManager.ProgramObject prog, int base, ShaderManager.UniformMemberInfo info) {
if (info.baseType() == Spvc.SPVC_BASETYPE_FP32) return ReadType.FLOAT;
if (info.baseType() != Spvc.SPVC_BASETYPE_UINT32) return ReadType.INT;
final String name = prog.locationName[base];
return name != null && prog.boolUniforms.contains(name) ? ReadType.BOOL : ReadType.UINT;
}
private static int roundHalfAwayFromZero(float f) {
final double r = Math.floor(Math.abs((double) f) + 0.5);
return (int) (f < 0 ? -r : r);
}
private static int opaqueUnit(ShaderManager.ProgramObject prog, int location, int kind) {
final String name = prog.locationName[location];
if (name == null) return 0;
final int unit = kind == ShaderManager.ProgramObject.LOCATION_KIND_SAMPLER
? prog.samplerTextureUnits.getInt(name)
: prog.imageTextureUnits.getInt(name);
return Math.max(unit, 0);
}
@Override public void getShaderSource(int shader, IntBuffer length, ByteBuffer source) {
writeUtf8(shaderManager.getShaderSource(shader), length, source);
}
@@ -4131,6 +4212,7 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -> 32;
case GL20.GL_MAX_VERTEX_ATTRIBS -> 16;
case GL20.GL_MAX_DRAW_BUFFERS -> 8;
case GL30.GL_MAX_CLIP_DISTANCES -> device.hasClipDistance() ? GLStateManager.MAX_CLIP_PLANES : 0;
case GL30.GL_MAX_COLOR_ATTACHMENTS -> 8;
case GL15.GL_ARRAY_BUFFER_BINDING -> cs.boundArrayBuffer;
case GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING -> cs.currentVao.elementBuffer;
@@ -45,6 +45,7 @@ public final class Device {
private long device;
private boolean claimed;
private boolean clipDistance = true;
private int supportedShaderFormats;
private String driverName;
private boolean fencePollEnabled;
@@ -116,6 +117,10 @@ public final class Device {
}
public boolean createDevice() {
return createDevice(!SystemProperties.EMULATE_CLIP_DISTANCES);
}
boolean createDevice(boolean requestClipDistance) {
if (device != 0) return true;
installLogCallback();
@@ -130,9 +135,9 @@ public final class Device {
}
final boolean gpuDebug = SystemProperties.LWJGL_DEBUG || SystemProperties.SDL_GPU_DEBUG;
device = createGPUDevice(gpuDebug, candidates);
device = createGPUDevice(gpuDebug, candidates, requestClipDistance);
if (device == 0 && VideoDriverRecovery.shouldRetryOnX11(SDL_GetCurrentVideoDriver(), vulkanLoaderAvailable(), System.getenv("DISPLAY"))) {
device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, candidates));
device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, candidates, requestClipDistance));
}
if (device == 0) {
logDeviceCreationFailure(REQUESTED_SHADER_FORMATS);
@@ -206,13 +211,22 @@ public final class Device {
}
}
private long createGPUDevice(boolean gpuDebug, String[] candidates) {
private long createGPUDevice(boolean gpuDebug, String[] candidates, boolean requestClipDistance) {
final boolean macos = Platform.get() == Platform.MACOSX;
for (final String driver : candidates) {
final boolean macVulkan = macos && isVulkan(driver);
if (macVulkan) useMoltenVk();
final long dev = SDL_CreateGPUDevice(REQUESTED_SHADER_FORMATS, gpuDebug, driver);
if (dev != 0) return dev;
long dev = createWithProperties(gpuDebug, driver, requestClipDistance);
boolean clip = requestClipDistance;
if (dev == 0 && clip && isVulkan(driver)) {
LOG.info("SDL GPU Vulkan device creation failed with shaderClipDistance ({}); the device may lack it, retrying without it, clip distances will be emulated", SDLError.SDL_GetError());
dev = createWithProperties(gpuDebug, driver, false);
clip = false;
}
if (dev != 0) {
clipDistance = clip;
return dev;
}
LOG.warn("Failed to create a '{}' SDL GPU device: {}", driver, SDLError.SDL_GetError());
if (macVulkan && MoltenVK.locate() == null) {
LOG.warn("Vulkan on macOS needs MoltenVK. Add {} to the mods folder or install MoltenVK.", MoltenVK.JAR_NAME);
@@ -221,6 +235,22 @@ public final class Device {
return 0;
}
private static long createWithProperties(boolean gpuDebug, String driver, boolean clipDistance) {
final int props = SDLProperties.SDL_CreateProperties();
try {
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_SPIRV) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXBC_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_DXBC) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_DXIL) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_MSL_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_MSL) != 0);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, gpuDebug);
SDLProperties.SDL_SetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_NAME_STRING, driver);
SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_FEATURE_CLIP_DISTANCE_BOOLEAN, clipDistance);
return SDL_CreateGPUDeviceWithProperties(props);
} finally {
SDLProperties.SDL_DestroyProperties(props);
}
}
public void claimWindow(long window) {
if (device == 0) {
throw new IllegalStateException("SDL GPU device not created");
@@ -298,6 +328,7 @@ public final class Device {
driverVersion = null;
driverInfo = null;
supportedShaderFormats = 0;
clipDistance = true;
}
private void logWindowDiagnostics(long window) {
@@ -351,6 +382,10 @@ public final class Device {
return device;
}
public boolean hasClipDistance() {
return clipDistance;
}
public String getDriverName() {
return driverName;
}
@@ -11,6 +11,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.LogicOpVariant;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2FloatOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
@@ -183,6 +184,7 @@ public final class ContextState {
private final Reference2ObjectOpenHashMap<ShaderManager.ProgramObject, UniformStaging> uniformStagingByProgram = new Reference2ObjectOpenHashMap<>();
private ShaderManager.ProgramObject stagingProg;
private UniformStaging currentUniformStaging;
public final Int2ObjectOpenHashMap<float[]> uniformArrayScratch = new Int2ObjectOpenHashMap<>();
public UniformStaging uniformStaging(ShaderManager.ProgramObject prog) {
if (prog == stagingProg) return currentUniformStaging;
@@ -754,16 +754,32 @@ public final class PipelineApplier {
public float[] reuseOrAlloc(ContextState st, int location, int length) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || location < 0 || location >= prog.uniformSlotCount) return null;
if (prog.floatLimitBySlot[location] < 0) return arrayScratch(st, length);
final float[] existing = st.uniformStaging(prog).uniformDataBySlot[location];
if (existing != null && existing.length == length) return existing;
return new float[length];
}
private static float[] arrayScratch(ContextState st, int length) {
float[] scratch = st.uniformArrayScratch.get(length);
if (scratch == null) {
scratch = new float[length];
st.uniformArrayScratch.put(length, scratch);
}
return scratch;
}
public void putUniformFv(ContextState st, int location, FloatBuffer value) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || location < 0 || location >= prog.uniformSlotCount) return;
final int n = value.remaining();
final int pos = value.position();
if (prog.floatLimitBySlot[location] < 0) {
final float[] v = arrayScratch(st, n);
value.get(pos, v);
putUniform(st, location, v);
return;
}
final float[] existing = st.uniformStaging(prog).uniformDataBySlot[location];
if (existing != null && existing.length == n) {
if (contentEquals(existing, value, pos, n)) {
@@ -824,6 +840,7 @@ public final class PipelineApplier {
if (Tracy.ENABLED) frameManager.noteUniformBlockFlush(b, size);
}
private static final long ARRAY_HASH_SEED = 0x9E3779B97F4A7C15L;
private static final boolean VERIFY_PER_FRAME_BLOCK = SystemProperties.SDL_VERIFY_PER_FRAME_UNIFORM_BLOCK;
private final HashSet<String> perFrameBlockDriftWarned = new HashSet<>();
@@ -837,6 +854,12 @@ public final class PipelineApplier {
public void putUniform(ContextState st, int location, float[] data) {
final ShaderManager.ProgramObject prog = st.boundProgramObj;
if (prog == null || location < 0 || location >= prog.uniformSlotCount) return;
final int limit = prog.floatLimitBySlot[location];
if (limit < 0) {
putArrayElement(st, prog, st.uniformStaging(prog), location - prog.arrayIndexBySlot[location], location, data);
return;
}
if (data.length > limit) return;
final UniformStaging us = st.uniformStaging(prog);
final int len = data.length;
final long valueKey;
@@ -884,6 +907,62 @@ public final class PipelineApplier {
}
return;
}
writeStages(st, prog, us, location, data);
}
private void putArrayElement(ContextState st, ShaderManager.ProgramObject prog, UniformStaging us, int base, int location, float[] data) {
final ShaderManager.UniformMemberInfo baseInfo = prog.memberInfo(base);
if (baseInfo == null) return;
final int len = prog.arrayLenBySlot[location];
final int index = prog.arrayIndexBySlot[location];
final int count = Math.min(elementCount(baseInfo, data), len - index);
if (count <= 0) return;
final int floatsPerElement = baseInfo.vectorSize() * baseInfo.columns();
if (Tracy.ENABLED) st.slotWrites++;
final int start = index * floatsPerElement;
final int floats = count * floatsPerElement;
float[] storage = us.uniformDataBySlot[base];
final boolean fresh = storage == null;
long storageHash;
if (fresh) {
storage = us.uniformDataBySlot[base] = new float[len * floatsPerElement];
storageHash = ARRAY_HASH_SEED;
} else {
storageHash = us.uniformValueHashBySlot[base];
}
boolean changed = fresh;
for (int i = 0; i < floats; i++) {
final int oldBits = Float.floatToRawIntBits(storage[start + i]);
final int newBits = Float.floatToRawIntBits(data[i]);
if (oldBits == newBits) continue;
storageHash ^= arrayHashTerm(start + i, oldBits) ^ arrayHashTerm(start + i, newBits);
storage[start + i] = data[i];
changed = true;
}
if (!changed) {
if (Tracy.ENABLED) st.slotWritesElided++;
return;
}
us.uniformValueHashBySlot[base] = storageHash;
for (int b = 0; b < ShaderManager.BLOCK_COUNT; b++) {
final ShaderManager.UniformMemberInfo info = prog.blockInfoBySlot[b][location];
if (info == null) continue;
final ContextState.UniformBlockState block = st.uniformBlocks[b];
if (prog.blockSize[b] > block.size) block.size = prog.blockSize[b];
final FloatBuffer blockFb = block.staging(prog.blockSize[b]);
final int hashSlot = block.hashSlot(prog.blockInfoBySlot[b][base].offset());
if (hashSlot >= 0) block.setHash(hashSlot, storageHash);
writeMember(st, prog, blockFb, prog.blockSize[b], info, data, location, false);
block.dirty = true;
if (VERIFY_PER_FRAME_BLOCK && b == ShaderManager.BLOCK_PER_FRAME && block.flushedThisFrame) {
notePerFrameBlockDrift(prog, base);
}
return;
}
writeStages(st, prog, us, location, data);
}
private void writeStages(ContextState st, ShaderManager.ProgramObject prog, UniformStaging us, int location, float[] data) {
final ShaderManager.UniformMemberInfo vs = prog.vsInfoBySlot[location];
if (vs != null && us.vsUniformFb != null) {
writeMember(st, prog, us.vsUniformFb, prog.vertexUboSize, vs, data, location, true);
@@ -896,6 +975,10 @@ public final class PipelineApplier {
}
}
private static long arrayHashTerm(int index, int bits) {
return bits == 0 ? 0L : Hashing.fmix64(ARRAY_HASH_SEED, Hashing.packHiLo(index, bits));
}
public void storeMatrix(ContextState st, int location, boolean transpose, FloatBuffer value, int size) {
if (location < 0) return;
final int n = value.remaining();
@@ -914,16 +997,19 @@ public final class PipelineApplier {
putUniform(st, location, out);
}
private void writeMember(ContextState st, ShaderManager.ProgramObject prog, FloatBuffer ubo, int uboSize, ShaderManager.UniformMemberInfo info, float[] data, int loc, boolean isVs) {
private static int elementCount(ShaderManager.UniformMemberInfo info, float[] data) {
final int floatsPerElement = info.vectorSize() * info.columns();
if (floatsPerElement <= 0) return;
final int srcCount = data.length / floatsPerElement;
if (srcCount <= 0) return;
final int dataBytes = srcCount * floatsPerElement * 4;
return floatsPerElement <= 0 ? 0 : data.length / floatsPerElement;
}
private void writeMember(ContextState st, ShaderManager.ProgramObject prog, FloatBuffer ubo, int uboSize, ShaderManager.UniformMemberInfo info, float[] data, int loc, boolean isVs) {
final int count = Math.min(elementCount(info, data), info.arrayLen());
if (count <= 0) return;
final int dataBytes = count * info.vectorSize() * info.columns() * 4;
final int avail = uboSize - info.offset();
if (dataBytes > avail) {
throw new IllegalStateException("Uniform overflows UBO: prog=" + st.boundProgram + " loc=" + loc + " name='" + prog.locationToName.getOrDefault(loc, "?") + "' dataBytes=" + dataBytes + " availFromOffset=" + avail + " stage=" + (isVs ? "VS" : "FS"));
}
Std140Writer.write(ubo, info, data, srcCount);
Std140Writer.write(ubo, info, data, count);
}
}
@@ -777,7 +777,7 @@ public final class ShaderManager {
else prog.fragmentUboSize = refl.uboSize();
}
for (UboMember m : refl.uboMembers()) {
final int loc = ensureLocation(prog, m.name());
final int loc = ensureLocation(prog, m.name(), m.arrayLen());
final UniformMemberInfo info = new UniformMemberInfo(
m.offset(), m.size(), m.arrayStride(), isVertex,
m.vectorSize(), m.columns(), m.baseType(), m.arrayLen()
@@ -795,7 +795,7 @@ public final class ShaderManager {
prog.blockSize[b] = Math.max(prog.blockSize[b], block.size());
prog.blockBinding[b] = block.binding();
for (UboMember m : block.members()) {
final int loc = ensureLocation(prog, m.name());
final int loc = ensureLocation(prog, m.name(), m.arrayLen());
if (prog.blockMemberInfo[b].containsKey(loc)) continue;
prog.blockMemberInfo[b].put(loc, new UniformMemberInfo(
m.offset(), m.size(), m.arrayStride(), false,
@@ -809,14 +809,34 @@ public final class ShaderManager {
}
private static int ensureLocation(ProgramObject prog, String name) {
return ensureLocation(prog, name, 1);
}
private static int ensureLocation(ProgramObject prog, String name, int arrayLen) {
final int existing = prog.nameToLocation.getInt(name);
if (existing != -1) return existing;
final int loc = prog.nextUniformLocation++;
if (existing != -1) {
final int registered = Math.max(1, prog.arrayLenByLocation.get(existing));
if (registered == arrayLen) return existing;
retireLocations(prog, existing, registered);
}
final int loc = prog.nextUniformLocation;
prog.nextUniformLocation += arrayLen;
prog.nameToLocation.put(name, loc);
prog.locationToName.put(loc, name);
for (int i = 1; i < arrayLen; i++) prog.locationToName.put(loc + i, name + "[" + i + "]");
if (arrayLen > 1) prog.arrayLenByLocation.put(loc, arrayLen);
return loc;
}
private static void retireLocations(ProgramObject prog, int base, int len) {
prog.arrayLenByLocation.remove(base);
for (int i = 0; i < len; i++) prog.locationToName.remove(base + i);
prog.locationToMemberInfo.remove(base);
prog.vertexMemberInfo.remove(base);
prog.fragmentMemberInfo.remove(base);
for (int b = 0; b < BLOCK_COUNT; b++) prog.blockMemberInfo[b].remove(base);
}
private static void applyVaryingMatch(ByteBuffer fragmentSpirv, StageReflection vs, StageReflection fs) {
if (vs.vsOutputs().isEmpty() || fs.fsInputs().isEmpty() || fragmentSpirv == null) return;
final HashMap<String, Integer> vsOutLocs = new HashMap<>(vs.vsOutputs().size() * 2);
@@ -1268,7 +1288,13 @@ public final class ShaderManager {
public int getUniformLocation(int program, String name) {
final ProgramObject prog = programObjects.get(program);
if (prog == null || !prog.linked) return -1;
return prog.nameToLocation.getInt(name);
final int exact = prog.nameToLocation.getInt(name);
if (exact != -1) return exact;
final int index = UniformNames.arrayIndex(name);
if (index < 0) return -1;
final int base = prog.nameToLocation.getInt(name.substring(0, name.lastIndexOf('[')));
if (base < 0 || base >= prog.uniformSlotCount || index >= prog.arrayLenBySlot[base]) return -1;
return base + index;
}
/**
@@ -1645,7 +1671,12 @@ public final class ShaderManager {
public record UniformMemberInfo(
int offset, int size, int arrayStride, boolean isVertex,
int vectorSize, int columns, int baseType, int arrayLen
) {}
) {
public UniformMemberInfo element(int i) {
return new UniformMemberInfo(offset + i * arrayStride, size - i * arrayStride, arrayStride, isVertex,
vectorSize, columns, baseType, arrayLen - i);
}
}
public record UboMember(
String name, int offset, int size, int arrayStride,
@@ -2069,11 +2100,24 @@ public final class ShaderManager {
return m != null ? m : blockMemberInfo[BLOCK_PER_PASS].get(loc);
}
public UniformMemberInfo memberInfo(int slot) {
for (int b = 0; b < BLOCK_COUNT; b++) {
final UniformMemberInfo info = blockInfoBySlot[b][slot];
if (info != null) return info;
}
final UniformMemberInfo vs = vsInfoBySlot[slot];
return vs != null ? vs : fsInfoBySlot[slot];
}
private static final UniformMemberInfo[] EMPTY_INFO = new UniformMemberInfo[0];
public UniformMemberInfo[] vsInfoBySlot;
public UniformMemberInfo[] fsInfoBySlot;
public int uniformSlotCount;
public final Int2IntOpenHashMap arrayLenByLocation = new Int2IntOpenHashMap();
public int[] arrayIndexBySlot = EMPTY_UNITS;
public int[] arrayLenBySlot = EMPTY_UNITS;
public int[] floatLimitBySlot = EMPTY_UNITS;
public final int[] vertexInputBaseType = new int[16];
public final int[] vertexInputVecSize = new int[16];
public final String[] vertexInputName = new String[16];
@@ -2200,24 +2244,51 @@ public final class ShaderManager {
locationKind[loc] = LOCATION_KIND_PLAIN;
}
}
arrayIndexBySlot = new int[n];
arrayLenBySlot = new int[n];
Arrays.fill(arrayLenBySlot, 1);
for (var it = arrayLenByLocation.int2IntEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int base = e.getIntKey();
final int len = e.getIntValue();
for (int i = 0; i < len; i++) {
arrayLenBySlot[base + i] = len;
arrayIndexBySlot[base + i] = i;
}
}
for (int b = 0; b < BLOCK_COUNT; b++) {
blockInfoBySlot[b] = new UniformMemberInfo[n];
for (var it = blockMemberInfo[b].int2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int k = e.getIntKey();
if (k >= 0 && k < n) blockInfoBySlot[b][k] = e.getValue();
fillSlots(blockInfoBySlot[b], e.getIntKey(), e.getValue());
}
}
for (var it = vertexMemberInfo.int2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int k = e.getIntKey();
if (k >= 0 && k < n) vsInfoBySlot[k] = e.getValue();
fillSlots(vsInfoBySlot, e.getIntKey(), e.getValue());
}
for (var it = fragmentMemberInfo.int2ObjectEntrySet().fastIterator(); it.hasNext(); ) {
final var e = it.next();
final int k = e.getIntKey();
if (k >= 0 && k < n) fsInfoBySlot[k] = e.getValue();
fillSlots(fsInfoBySlot, e.getIntKey(), e.getValue());
}
floatLimitBySlot = new int[n];
for (int slot = 0; slot < n; slot++) {
if (arrayLenBySlot[slot] > 1) {
floatLimitBySlot[slot] = -1;
continue;
}
final UniformMemberInfo info = memberInfo(slot);
final int floats = info == null || info.arrayStride() > 0 ? 0 : info.vectorSize() * info.columns();
floatLimitBySlot[slot] = floats > 0 ? floats : Integer.MAX_VALUE;
}
}
private void fillSlots(UniformMemberInfo[] bySlot, int base, UniformMemberInfo info) {
final int n = bySlot.length;
if (base < 0 || base >= n) return;
bySlot[base] = info;
final int len = arrayLenBySlot[base];
for (int i = 1; i < len; i++) bySlot[base + i] = info.element(i);
}
}
}
@@ -0,0 +1,25 @@
package com.gtnewhorizons.angelica.sdlgpu.shader;
public final class UniformNames {
private UniformNames() {}
public static int arrayIndex(String name) {
final int len = name.length();
if (len == 0 || name.charAt(len - 1) != ']') return -1;
int i = len - 1;
while (i > 0 && isDigit(name.charAt(i - 1))) i--;
if (i == 0 || name.charAt(i - 1) != '[') return -1;
if (name.charAt(i) == '0' && i + 1 != len - 1) return -1;
long index = 0;
for (int c = i; c < len - 1; c++) {
index = index * 10 + (name.charAt(c) - '0');
if (index > Integer.MAX_VALUE) return Integer.MAX_VALUE;
}
return (int) index;
}
private static boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
}
@@ -2,11 +2,13 @@ package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
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;
@@ -46,4 +48,12 @@ class DeviceLifecycleGpuTest {
device.destroyDevice();
assertEquals(0L, device.getDevice());
}
@Test
void createsVulkanDeviceWithoutClipDistance() {
device = new Device();
assertTrue(device.createDevice(false), "no SDL GPU device on this machine");
Assumptions.assumeTrue(device.isVulkan(), "test needs the vulkan driver, got " + device.getDriverName());
assertFalse(device.hasClipDistance());
}
}
@@ -10,6 +10,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.CubeParityFixture;
import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.ParticleParityFixture;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
@@ -127,6 +128,30 @@ class GlsmSdlFfpTest {
assertTexelEverywhere("two units, glCallLists");
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void disabledClipPlaneDoesNotClip(boolean emulated) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
bindBaseTexture();
try {
ClipTestSupport.setPlane(1, 0.0, 0.0, 0.0, -1.0);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
ClipTestSupport.setPlane(0, 1.0, 0.0, 0.0, 0.0);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
quadUnit0Only();
final int[] pixels = GlsmSdlHeadlessRig.readTarget();
final int left = pixelAt(pixels, SIZE, SIZE / 4, SIZE / 2);
final int right = pixelAt(pixels, SIZE, 3 * SIZE / 4, SIZE / 2);
assertTrue(left != TEXEL, () -> "left quarter must be clipped by enabled plane 0: " + describe(left));
assertEquals(TEXEL, right, () -> "right quarter must survive; disabled plane 1 must not clip: " + describe(right));
} finally {
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
}
}
}
@Test
void litTexturedQuadIsShaded() {
bindBaseTexture();
@@ -0,0 +1,246 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMPrograms;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlUniformArrayTest {
private static final int COUNT = 8;
private static final int[][] A = {
{10, 20, 30}, {40, 50, 60}, {70, 80, 90}, {100, 110, 120},
{130, 140, 150}, {160, 170, 180}, {190, 200, 210}, {220, 230, 240}
};
private static final int[] X = {250, 5, 15};
private static final int[][] B = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {11, 12, 13}};
private int program;
private int vao;
private int arr;
private int single;
private int pick;
@BeforeEach
void setUp() {
program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, """
#version 330 core
uniform vec4 arr[8];
uniform vec4 single;
uniform ivec4 iarr[2];
uniform vec4 one[1];
uniform int pick;
out vec4 color;
void main() { color = pick == -2 ? vec4(iarr[1]) / 255.0 : pick == -3 ? one[0] : pick < 0 ? single : arr[pick]; }
""");
vao = GLSMVertexInput.glGenVertexArrays();
GLSMVertexInput.glBindVertexArray(vao);
GLSMPrograms.glUseProgram(program);
arr = loc("arr");
single = loc("single");
pick = loc("pick");
}
@AfterEach
void tearDown() {
GLSMPrograms.glUseProgram(0);
GLSMVertexInput.glBindVertexArray(0);
GLSMVertexInput.glDeleteVertexArrays(vao);
GLSMPrograms.glDeleteProgram(program);
}
private int loc(String name) {
return GLSMPrograms.glGetUniformLocation(program, name);
}
private static int argb(int[] rgb) {
return 0xFF000000 | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
}
private static float[] vec(int[] rgb) {
return new float[] {rgb[0] / 255f, rgb[1] / 255f, rgb[2] / 255f, 1f};
}
private static FloatBuffer pack(int[]... colors) {
final FloatBuffer buf = MemoryUtil.memAllocFloat(colors.length * 4);
for (int[] c : colors) buf.put(vec(c));
buf.flip();
return buf;
}
private void writeAt(int location, int[]... colors) {
final FloatBuffer buf = pack(colors);
try {
GLSMPrograms.glUniform4(location, buf);
} finally {
MemoryUtil.memFree(buf);
}
}
private void writeElement(int location, int[] rgb) {
final float[] v = vec(rgb);
GLSMPrograms.glUniform4f(location, v[0], v[1], v[2], v[3]);
}
private void writeAll() {
writeAt(arr, A);
}
private int drawPick(int index) {
GlsmSdlHeadlessRig.beginFrame();
GlsmSdlHeadlessRig.bindTarget();
GLSMPrograms.glUniform1i(pick, index);
GLSMDraws.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
return GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1)[0];
}
private void assertPick(int index, int[] expected, String label) {
final int got = drawPick(index);
assertEquals(Integer.toHexString(argb(expected)), Integer.toHexString(got), label + ": " + GlsmSdlHeadlessRig.describe(got));
}
private float[] readUniform(int location) {
final FloatBuffer buf = MemoryUtil.memAllocFloat(4);
try {
GLSMPrograms.glGetUniform(program, location, buf);
final float[] out = new float[4];
buf.get(out);
return out;
} finally {
MemoryUtil.memFree(buf);
}
}
@Test
void arrayElementLocations() {
assertNotEquals(-1, arr, "arr");
assertNotEquals(-1, single, "single");
assertEquals(arr, loc("arr[0]"), "arr[0]");
assertEquals(arr + 3, loc("arr[3]"), "arr[3]");
assertEquals(arr + 7, loc("arr[7]"), "arr[7]");
assertEquals(-1, loc("arr[8]"), "arr[8]");
assertEquals(-1, loc("arr[03]"), "arr[03]");
assertEquals(-1, loc("arr[-1]"), "arr[-1]");
assertEquals(arr, loc("arr[]"), "arr[] is index 0, as Mesa parses it");
assertEquals(single, loc("single[0]"), "single[0]");
assertEquals(-1, loc("single[1]"), "single[1]");
}
@Test
void elementWriteLandsInTheArray() {
writeAll();
final int element3 = loc("arr[3]");
writeElement(element3, X);
assertPick(3, X, "pick=3 shows the element write");
assertPick(2, A[2], "pick=2 keeps the bulk write");
assertArrayEquals(vec(X), readUniform(element3), 0f, "glGetUniform arr[3]");
assertArrayEquals(vec(A[2]), readUniform(loc("arr[2]")), 0f, "glGetUniform arr[2]");
}
@Test
void elementWriteClampsToTheArrayEnd() {
writeAll();
writeAt(loc("arr[6]"), B);
assertPick(6, B[0], "pick=6");
assertPick(7, B[1], "pick=7");
assertPick(5, A[5], "pick=5");
}
@Test
void countBeyondTheDeclarationFollowsMesa() {
writeElement(single, A[0]);
writeAt(single, B);
assertPick(-1, A[0], "count 2 on a non-array is ignored");
assertArrayEquals(vec(A[0]), readUniform(single), 0f, "glGetUniform single keeps its value");
writeAt(loc("one"), B);
assertPick(-3, B[0], "count 2 on a size-1 array clamps");
}
@Test
void fullWriteAfterElementWriteIsNotElided() {
writeAll();
writeElement(loc("arr[3]"), X);
writeAll();
assertPick(3, A[3], "pick=3 after the repeated full write");
}
@Test
void intElementReadsBackAsInts() {
final int element1 = loc("iarr[1]");
assertEquals(loc("iarr") + 1, element1, "iarr[1]");
GLSMPrograms.glUniform4i(loc("iarr"), 1, 2, 3, 4);
GLSMPrograms.glUniform4i(element1, X[0], X[1], X[2], 255);
assertPick(-2, X, "pick=-2 shows iarr[1]");
assertArrayEquals(new int[] {X[0], X[1], X[2], 255}, readIntUniform(element1), "glGetUniformiv iarr[1]");
assertArrayEquals(new int[] {1, 2, 3, 4}, readIntUniform(loc("iarr")), "glGetUniformiv iarr[0]");
}
private int[] readIntUniform(int location) {
final IntBuffer buf = MemoryUtil.memAllocInt(4);
try {
GLSMPrograms.glGetUniform(program, location, buf);
final int[] out = new int[4];
buf.get(out);
return out;
} finally {
MemoryUtil.memFree(buf);
}
}
@Test
void activeUniformReportsArrayNameWithIndexZero() {
final Map<String, Integer> sizes = activeUniformSizes();
assertEquals(COUNT, sizes.getOrDefault("arr[0]", -1), "size of arr[0] in " + sizes);
assertEquals(1, sizes.getOrDefault("single", -1), "size of single in " + sizes);
assertEquals(1, sizes.getOrDefault("one[0]", -1), "size of one[0] in " + sizes);
}
private Map<String, Integer> activeUniformSizes() {
final int active = GLSMPrograms.glGetProgrami(program, GL20.GL_ACTIVE_UNIFORMS);
final Map<String, Integer> sizes = new HashMap<>();
final IntBuffer length = MemoryUtil.memAllocInt(1);
final IntBuffer size = MemoryUtil.memAllocInt(1);
final IntBuffer type = MemoryUtil.memAllocInt(1);
final ByteBuffer name = MemoryUtil.memAlloc(128);
try {
for (int i = 0; i < active; i++) {
GLSMPrograms.glGetActiveUniform(program, i, length, size, type, name);
final byte[] bytes = new byte[length.get(0)];
for (int c = 0; c < bytes.length; c++) bytes[c] = name.get(c);
sizes.put(new String(bytes, java.nio.charset.StandardCharsets.US_ASCII), size.get(0));
}
} finally {
MemoryUtil.memFree(name);
MemoryUtil.memFree(type);
MemoryUtil.memFree(size);
MemoryUtil.memFree(length);
}
return sizes;
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.common;
import net.minecraft.block.BlockContainer;
import net.minecraft.block.material.Material;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
public class BlockClipPlaneTest extends BlockContainer {
public BlockClipPlaneTest() {
super(Material.iron);
setHardness(0.7f);
}
@Override
public TileEntity createNewTileEntity(World world, int meta) {
return new TileEntityClipPlaneTest();
}
@Override
public boolean isOpaqueCube() {
return false;
}
@Override
public boolean renderAsNormalBlock() {
return false;
}
@Override
public int getRenderType() {
return -1;
}
}
@@ -0,0 +1,11 @@
package com.gtnewhorizons.angelica.common;
import net.minecraft.tileentity.TileEntity;
public class TileEntityClipPlaneTest extends TileEntity {
@Override
public boolean canUpdate() {
return false;
}
}
@@ -274,6 +274,13 @@ public class IrisGLSMBridge {
refreshBlendCondition();
});
GLSMHooks.CLIP_PLANES_IN_USE.addListener(event -> {
if (!Iris.enabled) return;
if (Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline drp) {
drp.onClipPlanesInUseChanged();
}
});
GLSMHooks.FOREIGN_DRAW_END.addListener(event -> {
if (!Iris.enabled) return;
if (programRestoreDeferred) {
@@ -336,12 +343,13 @@ public class IrisGLSMBridge {
WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (pipeline instanceof DeferredWorldRenderingPipeline drp) {
DeferredWorldRenderingPipeline.Pass activePass = drp.getActivePassProgram();
if (activePass != null && activePass.getProgram() != null && activePass.getProgram().getProgramId() == event.newProgram) {
final Program bound = activePass != null ? activePass.boundProgram() : null;
if (bound != null && bound.getProgramId() == event.newProgram) {
final int frame = SystemTimeUniforms.COUNTER.getAsInt();
if (programLastUpdatedFrame.get(event.newProgram) != frame
|| !ProgramUniforms.isActiveProgramBound()
|| ProgramUniforms.activeHasDeferredUploads()) {
activePass.getProgram().getUniforms().update();
bound.getUniforms().update();
programLastUpdatedFrame.put(event.newProgram, frame);
}
}
@@ -39,6 +39,7 @@ import com.gtnewhorizons.angelica.commands.AngelicaCommand;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.common.BlockError;
import com.gtnewhorizons.angelica.common.BlockIsbrhTest;
import com.gtnewhorizons.angelica.common.TileEntityClipPlaneTest;
import com.gtnewhorizons.angelica.compat.ModStatus;
import com.gtnewhorizons.angelica.compat.bettercrashes.BetterCrashesCompat;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
@@ -70,6 +71,7 @@ import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
import com.gtnewhorizons.angelica.render.CloudRenderer;
import com.gtnewhorizons.angelica.render.EmissiveTextureAutoloader;
import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry;
import com.gtnewhorizons.angelica.rendering.ClipPlaneTestRenderer;
import com.gtnewhorizons.angelica.rendering.FpsReducer;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
@@ -86,6 +88,7 @@ import com.gtnewhorizons.angelica.utils.AngelicaJar;
import com.gtnewhorizons.angelica.utils.AnimationMode;
import com.gtnewhorizons.angelica.utils.ManagedEnum;
import com.gtnewhorizons.angelica.zoom.Zoom;
import cpw.mods.fml.client.registry.ClientRegistry;
import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;
@@ -146,6 +149,7 @@ public final class ClientProxy extends CommonProxy {
if (AngelicaConfig.enableTestBlocks) {
BlockIsbrhTest.renderId = RenderingRegistry.getNextAvailableRenderId();
RenderingRegistry.registerBlockHandler(BlockIsbrhTest.renderId, new IsbrhTestRenderer());
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityClipPlaneTest.class, new ClipPlaneTestRenderer());
}
AngelicaJar.configureShaderDiskCache(event.getSourceFile());
if (AngelicaConfig.enableIris) {
@@ -1,7 +1,9 @@
package com.gtnewhorizons.angelica.proxy;
import com.gtnewhorizons.angelica.common.BlockClipPlaneTest;
import com.gtnewhorizons.angelica.common.BlockIsbrhTest;
import com.gtnewhorizons.angelica.common.BlockTest;
import com.gtnewhorizons.angelica.common.TileEntityClipPlaneTest;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
@@ -14,6 +16,8 @@ public class CommonProxy {
if (AngelicaConfig.enableTestBlocks) {
GameRegistry.registerBlock(new BlockTest(), "test_block");
GameRegistry.registerBlock(new BlockIsbrhTest(), "test_isbrh");
GameRegistry.registerBlock(new BlockClipPlaneTest(), "test_clip_plane");
GameRegistry.registerTileEntity(TileEntityClipPlaneTest.class, "angelica:test_clip_plane");
}
}
@@ -0,0 +1,41 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.glsm.GLSMEnable;
import com.gtnewhorizons.angelica.glsm.GLSMMatrices;
import com.gtnewhorizons.angelica.glsm.GLSMRasterizer;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import net.minecraft.init.Blocks;
import net.minecraft.tileentity.TileEntity;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.DoubleBuffer;
public class ClipPlaneTestRenderer extends TileEntitySpecialRenderer {
private static final DoubleBuffer KEEP_LOWER_HALF = plane(0.0, -1.0, 0.0, 0.5);
private static final DoubleBuffer REJECT_ALL = plane(0.0, 0.0, 0.0, -1.0);
private static DoubleBuffer plane(double a, double b, double c, double d) {
final DoubleBuffer buf = BufferUtils.createDoubleBuffer(4);
buf.put(a).put(b).put(c).put(d).flip();
return buf;
}
@Override
public void renderTileEntityAt(TileEntity te, double x, double y, double z, float partialTicks) {
GLSMMatrices.glPushMatrix();
GLSMMatrices.glTranslated(x, y, z);
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE1, REJECT_ALL);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1);
GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, KEEP_LOWER_HALF);
GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0);
GLSMMatrices.glTranslated(0.5, 0.5, 0.5);
bindTexture(TextureMap.locationBlocksTexture);
RenderBlocks.getInstance().renderBlockAsItem(Blocks.gold_block, 0, 1.0f);
GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0);
GLSMMatrices.glPopMatrix();
}
}
@@ -29,6 +29,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.PendingProgramSelection;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.rendering.RenderingState;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
@@ -141,6 +142,7 @@ import java.util.OptionalInt;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.IntFunction;
import java.util.function.Supplier;
@@ -148,6 +150,8 @@ import java.util.function.Supplier;
* Encapsulates the compiled shader program objects for the currently loaded shaderpack.
*/
public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, RenderTargetStateListener, TesrInstancingPipeline, PendingProgramSelection {
private static final Tracy.ZoneId Z_CLIP_VARIANT = Tracy.zoneId("irisClipVariant", Tracy.COLOR_IRIS);
private final RenderTargets renderTargets;
@Nullable
@@ -202,7 +206,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
private final EnumMap<Instancing, Map<Pair<String, InputAvailability>, Map<PatchShaderType, String>>> instancedAttributeTransforms = new EnumMap<>(Instancing.class);
private final boolean[] supportsInstancing = initialInstancingSupport();
private final Map<Pair<String, InputAvailability>, Map<PatchShaderType, String>> wideLineTransforms = new HashMap<>();
private final Map<SharedProgramKey, WideLineProgram> wideLinePrograms = new HashMap<>();
private final Map<WideLineKey, WideLineProgram> wideLinePrograms = new HashMap<>();
private final boolean packWidensLines;
private final ParityFlipState parityState = new ParityFlipState(AngelicaConfig.shaderParityFlip);
private final Supplier<ImmutableSet<Integer>> flippedGbuffers;
@@ -965,7 +969,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
public int getActivePassProgramId() {
if (current == null) return -1;
final Program p = current.getProgram();
final Program p = current.boundProgram();
return p != null ? p.getProgramId() : -1;
}
@@ -1044,6 +1048,29 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
}
}
private boolean clipRebindPending;
public void onClipPlanesInUseChanged() {
if (current == null) return;
clipRebindPending = true;
GLSMHooks.pendingProgramSelection = this;
}
private void resolveClipRebind() {
if (!clipRebindPending) return;
if (DisplayListManager.isRecording()) {
GLSMHooks.pendingProgramSelection = this;
return;
}
clipRebindPending = false;
final Pass pass = current;
if (pass == null) return;
final Program live = pass.liveProgram;
if (live != null && GLSMPrograms.getActiveProgram() == live.getProgramId() && pass.select(activeInstancing, GLSMRasterizer.clipEmulationActive()) != live) {
pass.rebindProgram();
}
}
private void clearPendingSelection() {
if (GLSMHooks.pendingProgramSelection == this) {
GLSMHooks.pendingProgramSelection = null;
@@ -1053,11 +1080,10 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
private void matchPass() {
clearPendingSelection();
if (!shouldOverrideShaders()) {
return;
if (shouldOverrideShaders()) {
matchPass(getCondition(getPhase()));
}
matchPass(getCondition(getPhase()));
resolveClipRebind();
}
private void matchPass(RenderCondition condition) {
@@ -1139,7 +1165,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
}
return;
}
final Program program = pass.getProgram();
final Program program = pass.select(Instancing.NONE, GLSMRasterizer.clipEmulationActive());
final GlFramebuffer expected = isBeforeTranslucent ? pass.framebufferBeforeTranslucents : pass.framebufferAfterTranslucents;
if (program != null && GLSMPrograms.getActiveProgram() == program.getProgramId() && GLSMFramebuffer.getDrawFramebuffer() == expected.getId()) {
return;
@@ -1199,7 +1225,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
if (shared != null) {
final Pass pass = createPassInner(shared.program(), source.getDirectives(), shadow, id);
this.customUniforms.mapPassLike(shared.firstPass(), pass);
pass.setInstancingKey(source.getName(), availability, shadow);
pass.setVariantSource(source, id, availability, shadow);
return pass;
}
@@ -1224,7 +1250,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
final Pass pass = createPassInner(program, source.getDirectives(), shadow, id);
this.customUniforms.mapholderToPass(builder, pass);
pass.setInstancingKey(source.getName(), availability, shadow);
pass.setVariantSource(source, id, availability, shadow);
if (shareKey != null) {
builtPrograms.put(shareKey, new BuiltProgram(program, pass));
}
@@ -1432,18 +1458,24 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
private final List<BufferBlendOverride> bufferBlendOverrides;
private final boolean shadowViewport;
private String instancingSourceName;
private InputAvailability instancingAvailability;
private boolean instancingShadow;
private final Program[] instancedVariants = new Program[Instancing.VALUES.length];
private final boolean[] instancedVariantAttempted = new boolean[Instancing.VALUES.length];
private ProgramSource variantSource;
private ProgramId variantId;
private InputAvailability variantAvailability;
private boolean variantShadow;
private final Program[][] variants = new Program[Instancing.VALUES.length][2];
private final boolean[][] variantAttempted = new boolean[Instancing.VALUES.length][2];
@Nullable
private WideLineProgram wideLineProgram;
private boolean wideLineProgramAttempted;
private Program boundProgram;
@Nullable
private Program liveProgram;
private final WideLineProgram[] wideLineVariants = new WideLineProgram[2];
private final boolean[] wideLineProgramAttempted = new boolean[2];
private Pass(@Nullable Program program, GlFramebuffer framebufferBeforeTranslucents, GlFramebuffer framebufferAfterTranslucents,
@Nullable AlphaTestOverride alphaTestOverride, @Nullable BlendModeOverride blendModeOverride, @Nullable List<BufferBlendOverride> bufferBlendOverrides, boolean shadowViewport) {
this.program = program;
this.variants[Instancing.NONE.ordinal()][0] = program;
this.variantAttempted[Instancing.NONE.ordinal()][0] = true;
this.framebufferBeforeTranslucents = framebufferBeforeTranslucents;
this.framebufferAfterTranslucents = framebufferAfterTranslucents;
this.alphaTestOverride = alphaTestOverride;
@@ -1457,15 +1489,22 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
}
void rebindProgram() {
if (program != null) program.use();
final boolean suspended = GLStateManager.suspendExitCapture();
try {
DeferredWorldRenderingPipeline.this.customUniforms.push(this);
bindSelected();
} finally {
GLStateManager.resumeExitCapture(suspended);
}
}
private void bindSelected() {
final Program selected = select(activeInstancing, GLSMRasterizer.clipEmulationActive());
liveProgram = selected;
if (activeInstancing == Instancing.NONE) boundProgram = selected;
if (selected != null) selected.use();
DeferredWorldRenderingPipeline.this.customUniforms.push(selected == null || selected == program ? this : selected);
}
private void useNow() {
final boolean suspended = GLStateManager.suspendExitCapture();
try {
@@ -1511,12 +1550,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
bufferBlendOverrides.forEach(BufferBlendOverride::apply);
}
if (activeInstancing != Instancing.NONE && program != null) {
DeferredWorldRenderingPipeline.this.bindInstancedVariant(activeInstancing);
} else {
if (program != null) program.use();
DeferredWorldRenderingPipeline.this.customUniforms.push(this);
}
bindSelected();
}
public void stopUsing() {
@@ -1541,62 +1575,113 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
return program;
}
void setInstancingKey(String sourceName, InputAvailability availability, boolean shadow) {
this.instancingSourceName = sourceName;
this.instancingAvailability = availability;
this.instancingShadow = shadow;
@Nullable
public Program boundProgram() {
return boundProgram != null ? boundProgram : program;
}
void setVariantSource(ProgramSource source, ProgramId id, InputAvailability availability, boolean shadow) {
this.variantSource = source;
this.variantId = id;
this.variantAvailability = availability;
this.variantShadow = shadow;
}
@Nullable
Program instancedVariant(Instancing kind) {
Program select(Instancing kind, boolean clipped) {
final int i = kind.ordinal();
if (!instancedVariantAttempted[i]) {
instancedVariantAttempted[i] = true;
if (instancingSourceName != null && supportsInstancing[i]) {
instancedVariants[i] = sharedInstancedVariant(instancingSourceName, instancingAvailability, instancingShadow, kind);
if (!variantAttempted[i][0]) {
variantAttempted[i][0] = true;
if (variantSource != null && supportsInstancing[i]) {
variants[i][0] = sharedVariant(new InstancedVariantKey(variantSource.getName(), variantAvailability, variantShadow, kind, false),
() -> buildInstancedVariant(variantSource.getName(), variantAvailability, variantShadow, kind));
}
}
return instancedVariants[i];
final Program unclipped = variants[i][0];
if (!clipped || unclipped == null) return unclipped;
if (!variantAttempted[i][1]) {
variantAttempted[i][1] = true;
variants[i][1] = sharedVariant(new InstancedVariantKey(variantSource.getName(), variantAvailability, variantShadow, kind, true),
() -> buildClipVariant(variantSource, variantId, variantAvailability, variantShadow, kind));
}
return variants[i][1] != null ? variants[i][1] : unclipped;
}
@Nullable
WideLineProgram wideLineProgram() {
if (!wideLineProgramAttempted) {
wideLineProgramAttempted = true;
if (instancingSourceName != null) {
wideLineProgram = sharedWideLineProgram(instancingSourceName, instancingAvailability, instancingShadow);
WideLineProgram wideLineProgram(boolean clipped) {
final int c = clipped ? 1 : 0;
if (!wideLineProgramAttempted[c]) {
wideLineProgramAttempted[c] = true;
if (variantSource != null && (!clipped || wideLineProgram(false) != null)) {
wideLineVariants[c] = sharedWideLineProgram(variantSource, variantAvailability, variantShadow, clipped);
}
}
return wideLineProgram;
return wideLineVariants[c] != null || !clipped ? wideLineVariants[c] : wideLineVariants[0];
}
public void destroy() {
if (this.program != null) {
this.program.destroy();
}
for (int i = 0; i < instancedVariants.length; i++) {
if (instancedVariants[i] != null) {
instancedVariants[i].destroy();
instancedVariants[i] = null;
for (int i = 0; i < variants.length; i++) {
for (int c = 0; c < 2; c++) {
if ((i != Instancing.NONE.ordinal() || c != 0) && variants[i][c] != null) {
variants[i][c].destroy();
variants[i][c] = null;
}
}
}
}
}
private record InstancedVariantKey(String sourceName, InputAvailability availability, boolean shadow, Instancing kind) {}
private record InstancedVariantKey(String sourceName, InputAvailability availability, boolean shadow, Instancing kind, boolean clipped) {}
// Passes sharing a program also share its instanced variants
private final Map<InstancedVariantKey, Program> instancedVariantPrograms = new HashMap<>();
@Nullable
private Program sharedInstancedVariant(String sourceName, InputAvailability availability, boolean shadow, Instancing kind) {
final InstancedVariantKey key = new InstancedVariantKey(sourceName, availability, shadow, kind);
private Program sharedVariant(InstancedVariantKey key, Supplier<Program> build) {
if (instancedVariantPrograms.containsKey(key)) return instancedVariantPrograms.get(key);
final Program variant = buildInstancedVariant(sourceName, availability, shadow, kind);
final Program variant = build.get();
instancedVariantPrograms.put(key, variant);
return variant;
}
@Nullable
private Program buildClipVariant(ProgramSource source, ProgramId id, InputAvailability availability, boolean shadow, Instancing kind) {
final String name = source.getName() + kind.variantSuffix + "_clipped";
final boolean glint = GlintScrollInjector.shouldInject(id, source);
return buildClipped(source, name, (vertex, fragment) -> {
final Map<PatchShaderType, String> transformed = TransformPatcher.patchAttributesInstanced(vertex, null, null, null, fragment, availability, glint, kind, true);
return transformed != null ? linkVariant(name, transformed, availability, shadow) : null;
});
}
@Nullable
private static <T> T buildClipped(ProgramSource source, String name, BiFunction<String, String, T> build) {
if (source.getGeometrySource().isPresent() || source.getTessControlSource().isPresent() || source.getTessEvalSource().isPresent()) {
return null;
}
if (Tracy.ENABLED) Tracy.beginZone(Z_CLIP_VARIANT);
try {
return build.apply(source.getVertexSource().orElse(null), source.getFragmentSource().orElse(null));
} catch (Exception e) {
Iris.logger.warn("Clip plane variant failed for {}; clip planes stay unclipped there", name, e);
return null;
} finally {
if (Tracy.ENABLED) Tracy.endZone();
}
}
private Program linkVariant(String name, Map<PatchShaderType, String> transformed, InputAvailability availability, boolean shadow) {
final ProgramBuilder builder = beginBuilder(name, transformed);
wireGbufferProgram(builder, availability, shadow);
final Program variant = builder.build();
vertexWriteReplays.attach(name, variant, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow));
this.customUniforms.mapholderToPass(builder, variant);
return variant;
}
@Nullable
private Program buildInstancedVariant(String sourceName, InputAvailability availability, boolean shadow, Instancing kind) {
final Map<Pair<String, InputAvailability>, Map<PatchShaderType, String>> transforms = instancedAttributeTransforms.get(kind);
@@ -1605,12 +1690,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
return null;
}
try {
final ProgramBuilder builder = beginBuilder(sourceName + kind.variantSuffix, transformed);
wireGbufferProgram(builder, availability, shadow);
final Program variant = builder.build();
vertexWriteReplays.attach(sourceName + kind.variantSuffix, variant, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow));
this.customUniforms.mapholderToPass(builder, variant);
return variant;
return linkVariant(sourceName + kind.variantSuffix, transformed, availability, shadow);
} catch (Exception e) {
Iris.logger.warn("{} instanced variant link failed for {}; disabling it", kind, sourceName, e);
supportsInstancing[kind.ordinal()] = false;
@@ -1641,50 +1721,52 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
@Override
public boolean hasInstancedVariant(Instancing kind) {
return current != null && current.instancedVariant(kind) != null;
return current != null && current.select(kind, false) != null;
}
public boolean hasInstancedVariant(RenderCondition condition, InputAvailability availability, Instancing kind) {
final Pass pass = table.match(condition, availability);
return pass != null && pass.instancedVariant(kind) != null;
return pass != null && pass.select(kind, false) != null;
}
@Override
public void bindInstancedVariant(Instancing kind) {
activeInstancing = kind;
final Program variant = current.instancedVariant(kind);
variant.use();
this.customUniforms.push(variant);
current.bindSelected();
}
private record WideLineProgram(Program program, int lineWidthLocation, int screenSizeLocation) {}
private record WideLineKey(String sourceName, InputAvailability availability, boolean shadow, boolean clipped) {}
@Nullable
private WideLineProgram sharedWideLineProgram(String sourceName, InputAvailability availability, boolean shadow) {
final SharedProgramKey key = new SharedProgramKey(sourceName, availability, shadow);
private WideLineProgram sharedWideLineProgram(ProgramSource source, InputAvailability availability, boolean shadow, boolean clipped) {
final WideLineKey key = new WideLineKey(source.getName(), availability, shadow, clipped);
if (wideLinePrograms.containsKey(key)) return wideLinePrograms.get(key);
final WideLineProgram built = buildWideLineProgram(sourceName, availability, shadow);
final WideLineProgram built = clipped ? buildClippedWideLineProgram(source, availability, shadow)
: buildWideLineProgram(source.getName() + "_widelines", wideLineTransforms.get(Pair.of(source.getName(), availability)), availability, shadow);
wideLinePrograms.put(key, built);
return built;
}
@Nullable
private WideLineProgram buildWideLineProgram(String sourceName, InputAvailability availability, boolean shadow) {
final Map<PatchShaderType, String> transformed = wideLineTransforms.get(Pair.of(sourceName, availability));
private WideLineProgram buildClippedWideLineProgram(ProgramSource source, InputAvailability availability, boolean shadow) {
final String name = source.getName() + "_widelines_clipped";
return buildClipped(source, name, (vertex, fragment) ->
buildWideLineProgram(name, TransformPatcher.patchAttributesWideLines(vertex, null, null, null, fragment, availability, true), availability, shadow));
}
@Nullable
private WideLineProgram buildWideLineProgram(String name, @Nullable Map<PatchShaderType, String> transformed, InputAvailability availability, boolean shadow) {
if (transformed == null) {
return null;
}
final String name = sourceName + "_widelines";
try {
final ProgramBuilder builder = beginBuilder(name, transformed);
wireGbufferProgram(builder, availability, shadow);
final Program program = builder.build();
vertexWriteReplays.attach(name, program, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow));
this.customUniforms.mapholderToPass(builder, program);
final Program program = linkVariant(name, transformed, availability, shadow);
final int id = program.getProgramId();
return new WideLineProgram(program, GLSMPrograms.glGetUniformLocation(id, "iris_LineWidth"), GLSMPrograms.glGetUniformLocation(id, "iris_ScreenSize"));
} catch (Exception e) {
Iris.logger.warn("Wide line program link failed for {}; lines there stay 1 px wide", sourceName, e);
Iris.logger.warn("Wide line program link failed for {}; lines there stay 1 px wide", name, e);
return null;
}
}
@@ -1698,8 +1780,8 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
}
GLSMHooks.resolvePendingProgram();
final Pass pass = current;
if (pass == null || pass.getProgram() == null || activeInstancing != Instancing.NONE
|| GLSMPrograms.getActiveProgram() != pass.getProgram().getProgramId()) {
final Program bound = pass != null ? pass.boundProgram() : null;
if (bound == null || activeInstancing != Instancing.NONE || GLSMPrograms.getActiveProgram() != bound.getProgramId()) {
return false;
}
@@ -1709,7 +1791,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
return false;
}
final boolean needsWidening = shadow || !packWidensLines;
final WideLineProgram wide = needsWidening ? linePass.wideLineProgram() : null;
final WideLineProgram wide = needsWidening ? linePass.wideLineProgram(GLSMRasterizer.clipEmulationActive()) : null;
if (needsWidening && wide == null) {
return false;
}
@@ -1742,11 +1824,11 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
}
public void compileWideLinePrograms() {
table.match(RenderCondition.LINES, LINE_INPUTS).wideLineProgram();
table.match(RenderCondition.LINES, LINE_INPUTS).wideLineProgram(false);
final Pass shadowPass = table.match(RenderCondition.SHADOW, SHADOW_INPUTS);
if (shadowPass != null) shadowPass.wideLineProgram();
if (shadowPass != null) shadowPass.wideLineProgram(false);
final Pass shadowWaterPass = table.match(RenderCondition.SHADOW_TRANSLUCENT, SHADOW_INPUTS);
if (shadowWaterPass != null) shadowWaterPass.wideLineProgram();
if (shadowWaterPass != null) shadowWaterPass.wideLineProgram(false);
}
@Override
@@ -1851,7 +1933,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
table.forEach(pass -> {
if (pass == null || !visited.add(pass)) return;
for (Instancing kind : Instancing.VALUES) {
if (kind != Instancing.NONE) pass.instancedVariant(kind);
if (kind != Instancing.NONE) pass.select(kind, false);
}
});
}
@@ -2667,7 +2749,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
source.getTessControlSource().orElse(null),
source.getTessEvalSource().orElse(null),
source.getFragmentSource().orElse(null),
inputs)));
inputs, false)));
}
private static final ProgramId[] INSTANCED_PROGRAM_IDS = {
@@ -1,7 +1,10 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.ClipDistanceGlsl;
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
import net.coderbot.iris.pipeline.transform.parameter.Parameters;
import org.taumc.glsl.Transformer;
import org.taumc.glsl.grammar.GLSLParser;
@@ -123,7 +126,9 @@ public class CommonTransformer {
});
}
if (parameters.patch == Patch.ATTRIBUTES && parameters.type == ShaderType.VERTEX) {
if (parameters.patch == Patch.ATTRIBUTES && RenderSystem.isClipDistanceEmulated()) {
injectClipDiscard(root, (AttributeParameters) parameters);
} else if (parameters.patch == Patch.ATTRIBUTES && parameters.type == ShaderType.VERTEX) {
root.injectVariable("uniform bool angelica_ClipPlanesEnabled;");
root.injectVariable("uniform vec4 angelica_ClipPlane[8];");
root.appendMain(
@@ -144,4 +149,31 @@ public class CommonTransformer {
);
}
}
private static final String CLIP_VS_BLOCK;
static {
final String[] distances = new String[8];
for (int i = 0; i < distances.length; i++) {
distances[i] = "dot(angelica_ClipPlane[" + i + "], _cp_ep)";
}
final StringBuilder sb = new StringBuilder(256).append("{ vec4 _cp_ep = iris_ModelViewMatrix * iris_Vertex;\n");
ClipDistanceGlsl.appendVsAssign(sb, distances);
CLIP_VS_BLOCK = sb.append('}').toString();
}
private static void injectClipDiscard(Transformer root, AttributeParameters parameters) {
if (!parameters.clipDiscard) {
return;
}
if (parameters.type == ShaderType.VERTEX) {
root.injectVariable("uniform vec4 angelica_ClipPlane[8];");
root.injectVariable(ClipDistanceGlsl.VS_OUT_DIST0);
root.injectVariable(ClipDistanceGlsl.VS_OUT_DIST1);
root.appendMain(CLIP_VS_BLOCK);
} else if (parameters.type == ShaderType.FRAGMENT) {
root.injectVariable(ClipDistanceGlsl.FS_IN_DIST0);
root.injectVariable(ClipDistanceGlsl.FS_IN_DIST1);
root.prependMain(ClipDistanceGlsl.FS_DISCARD);
}
}
}
@@ -238,7 +238,7 @@ public class ShaderTransformer {
final ShaderDiskCache.Key k = ShaderDiskCache.key("iris-transform").str(patch.name());
parameters.appendDiskKey(k);
k.i(RenderSystem.getMaxGlslVersion()).b(RenderSystem.supportsSSBO()).b(RenderSystem.supportsImageLoadStore())
.b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).b(BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()).str(RwImageStoreExtractor.atomicCustomImagesKey())
.b(RenderSystem.isGLES()).b(RenderSystem.isClipDistanceEmulated()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).b(BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()).str(RwImageStoreExtractor.atomicCustomImagesKey())
.i(maxSupportedHoistVersion);
if (enabledRequirements == null) {
k.i(-1);
@@ -143,11 +143,15 @@ public class TransformPatcher {
}
public static Map<PatchShaderType, String> patchAttributesInstanced(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs, boolean scrollGlint, Instancing instancing) {
return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, scrollGlint, instancing));
return patchAttributesInstanced(vertex, geometry, tessControl, tessEval, fragment, inputs, scrollGlint, instancing, false);
}
public static Map<PatchShaderType, String> patchAttributesWideLines(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs) {
return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, false, Instancing.NONE, true));
public static Map<PatchShaderType, String> patchAttributesInstanced(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs, boolean scrollGlint, Instancing instancing, boolean clipDiscard) {
return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, scrollGlint, instancing, false, clipDiscard));
}
public static Map<PatchShaderType, String> patchAttributesWideLines(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs, boolean clipDiscard) {
return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, false, Instancing.NONE, true, clipDiscard));
}
public static Map<PatchShaderType, String> patchAttributes(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs) {
@@ -13,13 +13,14 @@ public class AttributeParameters extends Parameters {
public final boolean scrollGlint;
public final Instancing instancing;
public final boolean wideLines;
public final boolean clipDiscard;
// WARNING: adding new fields requires updating hashCode and equals methods!
public AttributeParameters(Patch patch, boolean hasGeometry, boolean hasTesselation, InputAvailability inputs, boolean scrollGlint, Instancing instancing) {
this(patch, hasGeometry, hasTesselation, inputs, scrollGlint, instancing, false);
this(patch, hasGeometry, hasTesselation, inputs, scrollGlint, instancing, false, false);
}
public AttributeParameters(Patch patch, boolean hasGeometry, boolean hasTesselation, InputAvailability inputs, boolean scrollGlint, Instancing instancing, boolean wideLines) {
public AttributeParameters(Patch patch, boolean hasGeometry, boolean hasTesselation, InputAvailability inputs, boolean scrollGlint, Instancing instancing, boolean wideLines, boolean clipDiscard) {
super(patch, null);
this.hasGeometry = hasGeometry;
this.hasTesselation = hasTesselation;
@@ -27,6 +28,7 @@ public class AttributeParameters extends Parameters {
this.scrollGlint = scrollGlint;
this.instancing = instancing;
this.wideLines = wideLines;
this.clipDiscard = clipDiscard;
}
@Override
@@ -37,7 +39,7 @@ public class AttributeParameters extends Parameters {
@Override
public void appendDiskKey(ShaderDiskCache.Key k) {
super.appendDiskKey(k);
k.b(hasGeometry).b(hasTesselation).str(inputs == null ? null : inputs.name()).b(scrollGlint).str(instancing.name()).b(wideLines);
k.b(hasGeometry).b(hasTesselation).str(inputs == null ? null : inputs.name()).b(scrollGlint).str(instancing.name()).b(wideLines).b(clipDiscard);
}
@Override
@@ -49,6 +51,7 @@ public class AttributeParameters extends Parameters {
result = prime * result + (scrollGlint ? 1231 : 1237);
result = prime * result + instancing.ordinal();
result = prime * result + (wideLines ? 1231 : 1237);
result = prime * result + (clipDiscard ? 1231 : 1237);
result = prime * result + ((inputs == null) ? 0 : inputs.hashCode());
return result;
}
@@ -72,6 +75,8 @@ public class AttributeParameters extends Parameters {
return false;
if (wideLines != other.wideLines)
return false;
if (clipDiscard != other.clipDiscard)
return false;
if (inputs == null) {
return other.inputs == null;
} else return inputs.equals(other.inputs);
@@ -99,7 +99,7 @@ class AttributeTransformerLinkGLTest {
@Test
void wideLineVariantLinksWithLineUniformsLive() {
final Map<PatchShaderType, String> result = TransformPatcher.patchAttributesWideLines(COMPAT_VERTEX, null, null, null, COMPAT_FRAGMENT, InputAvailability.NONE);
final Map<PatchShaderType, String> result = TransformPatcher.patchAttributesWideLines(COMPAT_VERTEX, null, null, null, COMPAT_FRAGMENT, InputAvailability.NONE, false);
final int program = link("wideLines", result);
try {
assertNotEquals(-1, GLSMPrograms.glGetUniformLocation(program, "iris_LineWidth"), () -> result.get(PatchShaderType.VERTEX));
@@ -1,11 +1,13 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
import com.gtnewhorizons.angelica.glsm.ffp.ClipDistanceGlsl;
import com.gtnewhorizons.angelica.glsm.ffp.CubeInstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.ffp.ParticleInstancedAttribs;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability;
import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
import org.junit.jupiter.api.AfterAll;
@@ -467,4 +469,54 @@ class AttributeTransformerInstancedTest {
assertNotNull(entityResult);
assertHas(entityResult.get(PatchShaderType.VERTEX), "layout(location = " + ImmediateExtendedAttribHandler.LOC_ENTITY_IDS + ") in vec4 iris_Entity;");
}
private static final String[] CLIP_NAMES = {"gl_ClipDistance", "angelica_ClipDist", "angelica_ClipPlane"};
private static Map<PatchShaderType, String> clipStages(boolean emulated, boolean clipDiscard) {
try (var ignored = ClipTestSupport.emulation(emulated)) {
final Map<PatchShaderType, String> result = TransformPatcher.patchAttributesInstanced(VERTEX, null, null, null, FRAGMENT, TEX_LM, false, Instancing.NONE, clipDiscard);
assertNotNull(result);
for (String stage : result.values()) {
if (stage != null) ShaderParser.parseShader(stage);
}
return result;
}
}
@Test
void nativeClipDistancesWrittenByTheVertexStage() {
final Map<PatchShaderType, String> result = clipStages(false, false);
assertHas(result.get(PatchShaderType.VERTEX), "gl_ClipDistance[7] = dot(angelica_ClipPlane[7], _cp_ep);");
assertLacks(result.get(PatchShaderType.VERTEX), "angelica_ClipDist0");
assertLacks(result.get(PatchShaderType.FRAGMENT), "angelica_ClipDist0");
}
@Test
void emulatedUnclippedVariantCarriesNoClipCode() {
final Map<PatchShaderType, String> result = clipStages(true, false);
for (String stage : result.values()) {
if (stage == null) continue;
for (String name : CLIP_NAMES) assertLacks(stage, name);
}
}
@Test
void emulatedClippedVariantDiscardsFirstInFragmentMain() {
final Map<PatchShaderType, String> result = clipStages(true, true);
final String v = result.get(PatchShaderType.VERTEX);
assertHas(v, "uniform vec4 angelica_ClipPlane[8];");
assertHas(v, "out vec4 angelica_ClipDist0;");
assertHas(v, "out vec4 angelica_ClipDist1;");
assertHas(v, "angelica_ClipDist0 = vec4(dot(angelica_ClipPlane[0], _cp_ep), dot(angelica_ClipPlane[1], _cp_ep), dot(angelica_ClipPlane[2], _cp_ep), dot(angelica_ClipPlane[3], _cp_ep));");
assertHas(v, "angelica_ClipDist1 = vec4(dot(angelica_ClipPlane[4], _cp_ep), dot(angelica_ClipPlane[5], _cp_ep), dot(angelica_ClipPlane[6], _cp_ep), dot(angelica_ClipPlane[7], _cp_ep));");
assertLacks(v, "gl_ClipDistance");
final String f = result.get(PatchShaderType.FRAGMENT);
assertHas(f, "in vec4 angelica_ClipDist0;");
assertHas(f, "in vec4 angelica_ClipDist1;");
final String body = squash(f);
final int mainIdx = body.indexOf("voidmain(){");
assertTrue(mainIdx >= 0, f);
assertTrue(body.startsWith(squash(ClipDistanceGlsl.FS_DISCARD), mainIdx + "voidmain(){".length()), "discard is not the first statement of main:\n" + f);
}
}
@@ -1,11 +1,15 @@
package net.coderbot.iris.pipeline.transform;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.ffp.Instancing;
import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache;
import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.glsm.texture.TextureType;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability;
import net.coderbot.iris.helpers.Tri;
import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters;
import net.coderbot.iris.pipeline.transform.parameter.ComputeParameters;
import net.coderbot.iris.pipeline.transform.parameter.TextureStageParameters;
import net.coderbot.iris.shaderpack.texture.TextureStage;
@@ -97,6 +101,23 @@ class ShaderTransformerDiskCacheTest {
assertNotEquals(before, after);
}
@Test
void keyChangesWithClipEmulationAndClipDiscard() {
final InputAvailability avail = InputAvailability.of(true, true);
final AttributeParameters unclipped = new AttributeParameters(Patch.ATTRIBUTES, false, false, avail, false, Instancing.NONE, false, false);
final AttributeParameters clipped = new AttributeParameters(Patch.ATTRIBUTES, false, false, avail, false, Instancing.NONE, false, true);
final String nativeKey;
try (var ignored = ClipTestSupport.emulation(false)) {
nativeKey = ShaderTransformer.diskKey(Patch.ATTRIBUTES, compositeInputs(), unclipped).hex();
}
try (var ignored = ClipTestSupport.emulation(true)) {
final String emulatedKey = ShaderTransformer.diskKey(Patch.ATTRIBUTES, compositeInputs(), unclipped).hex();
final String clippedKey = ShaderTransformer.diskKey(Patch.ATTRIBUTES, compositeInputs(), clipped).hex();
assertNotEquals(nativeKey, emulatedKey);
assertNotEquals(emulatedKey, clippedKey);
}
}
private static Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> textureMap(int capacity) {
final Object2ObjectOpenHashMap<Tri<String, TextureType, TextureStage>, String> m = new Object2ObjectOpenHashMap<>(capacity);
m.put(new Tri<>("colortex0", TextureType.TEXTURE_2D, TextureStage.COMPOSITE_AND_FINAL), "a.png");