fix(render): skip shaders that fail to compile instead of crashing

ShaderProgram now logs and disables itself (valid=false) on shader compile/link failure instead of throwing IllegalStateException. The throw previously bubbled out of StencilHelper's static initializer as ExceptionInInitializerError and crashed the ClickGUI on drivers that reject legacy GLSL (Intel forward-compatible GL contexts: 'texture2D removed in Forward Compatible context').

Callers degrade gracefully when a shader is unavailable: use() is a no-op, drawRoundedRect falls back to a plain rect, drawBlurredRect disables blur for the session, and StencilHelper.applyStencil draws content directly without the rounded composite.

Closes #20, #24

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shirona1337
2026-06-01 16:00:10 +08:00
parent 07be28c11e
commit d3e4748966
3 changed files with 44 additions and 3 deletions
@@ -29,6 +29,12 @@ public class StencilHelper {
private static final RenderTarget mainRenderTarget = ClientBase.mc.getMainRenderTarget();
public static void applyStencil(PoseStack poseStack, Runnable drawMask, Runnable drawContent, float opacity) {
if (!stencilShader.isValid()) {
// Stencil shader unavailable on this driver — skip the rounded composite and
// just draw the content directly so the panel still shows (without rounding).
drawContent.run();
return;
}
Matrix4f pose = poseStack.last().pose();
RenderTarget mask = maskTarget.get();
if (mask.width != StencilHelper.mainRenderTarget.width || mask.height != StencilHelper.mainRenderTarget.height) {
@@ -19,6 +19,8 @@ public class ShaderProgram {
@Getter
private final int programId;
@Getter
private final boolean valid;
@Getter
private final Map<String, Integer> uniformCache = new HashMap<>();
@Getter
private final Matrix4Uniform modelViewUniform;
@@ -50,6 +52,18 @@ public class ShaderProgram {
this.programId = GL20.glCreateProgram();
int fragmentShader = ShaderProgram.compileShader(ShaderSource.getByFileName(fragmentName + ".fsh").getSource(), 35632);
int vertexShader = ShaderProgram.compileShader(ShaderSource.getByFileName(vertexName + ".vsh").getSource(), 35633);
// Shader compilation can fail on some drivers (notably Intel forward-compatible
// GL contexts, which reject legacy GLSL such as texture2D). Degrade gracefully —
// disable this program so callers skip the effect — instead of crashing the game.
if (fragmentShader == 0 || vertexShader == 0) {
if (fragmentShader != 0) GL20.glDeleteShader(fragmentShader);
if (vertexShader != 0) GL20.glDeleteShader(vertexShader);
LOGGER.error("Shader '{}' disabled: compilation failed; the effect will be skipped.", fragmentName);
this.modelViewUniform = null;
this.projectionUniform = null;
this.valid = false;
return;
}
GL20.glAttachShader(this.programId, fragmentShader);
GL20.glAttachShader(this.programId, vertexShader);
for (Map.Entry<Integer, String> entry : attributesSupplier.get().entrySet()) {
@@ -59,15 +73,25 @@ public class ShaderProgram {
GL20.glLinkProgram(this.programId);
if (GL20.glGetProgrami(this.programId, 35714) == 0) {
LOGGER.error(GL20.glGetProgramInfoLog(this.programId, Short.MAX_VALUE));
throw new IllegalStateException("Failed to link shader program!");
LOGGER.error("Shader '{}' disabled: program link failed; the effect will be skipped.", fragmentName);
GL20.glDeleteShader(fragmentShader);
GL20.glDeleteShader(vertexShader);
this.modelViewUniform = null;
this.projectionUniform = null;
this.valid = false;
return;
}
GL20.glDeleteShader(fragmentShader);
GL20.glDeleteShader(vertexShader);
this.modelViewUniform = new Matrix4Uniform(modelViewName).bindToProgram(this.programId);
this.projectionUniform = new Matrix4Uniform(projName).bindToProgram(this.programId);
this.valid = true;
}
public void use() {
if (!this.valid) {
return;
}
prevProgram = GL20.glGetInteger(35725);
GL20.glUseProgram(this.programId);
this.setModelView(RenderSystem.getModelViewMatrix());
@@ -112,8 +136,9 @@ public class ShaderProgram {
GL20.glShaderSource(shader, source);
GL20.glCompileShader(shader);
if (GL20.glGetShaderi(shader, 35713) == 0) {
LOGGER.error(GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
throw new IllegalStateException(String.format("Failed to compile shader! (Type: %s)", new Object[]{type}));
LOGGER.error("Failed to compile shader (type {}):\n{}", type, GL20.glGetShaderInfoLog(shader, Short.MAX_VALUE));
GL20.glDeleteShader(shader);
return 0;
}
return shader;
}
@@ -141,6 +141,11 @@ extends ClientBase {
if (roundedRectShader == null) {
roundedRectShader = new ShaderProgram("rounded_rect", "vertex_color", ShaderFormats.POSITION_UV_COLOR);
}
if (!roundedRectShader.isValid()) {
// Rounded-rect shader unavailable on this driver — draw a plain rect instead of crashing.
RenderUtil.drawFilledRect(poseStack, x, y, width, height, color);
return;
}
Matrix4f matrix4f = poseStack.last().pose();
roundedRectShader.use();
GL20.glUniform2f(roundedRectShader.getUniformLocation("Size"), width, height);
@@ -222,6 +227,11 @@ extends ClientBase {
if (blurShader == null) {
blurShader = new ShaderProgram("blur", ShaderFormats.POSITION_UV_COLOR);
}
if (!blurShader.isValid()) {
// Blur shader unavailable on this driver — disable blur for the rest of the session.
blurFailed = true;
return;
}
if (mainRenderTarget == null) {
mainRenderTarget = mc.getMainRenderTarget();
}