GLSL Downgrading (#1348)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-01-31 13:23:02 -08:00
1 parent dd970b4c6a
commit 41eafbd151
9 files changed
+481 -101

No files matched your search

@@ -553,6 +553,57 @@ public final class GLDebug {
};
}
public static String getFramebufferStatusName(int status) {
return switch (status) {
case GL30.GL_FRAMEBUFFER_COMPLETE -> "COMPLETE";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT -> "INCOMPLETE_ATTACHMENT";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT -> "INCOMPLETE_MISSING_ATTACHMENT";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER -> "INCOMPLETE_DRAW_BUFFER";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER -> "INCOMPLETE_READ_BUFFER";
case GL30.GL_FRAMEBUFFER_UNSUPPORTED -> "UNSUPPORTED";
case GL30.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE -> "INCOMPLETE_MULTISAMPLE";
case 0 -> "NO_FRAMEBUFFER_BOUND";
default -> String.format("UNKNOWN(0x%X)", status);
};
}
public static String getInternalFormatName(int format) {
return switch (format) {
// Basic
case GL11.GL_RGBA -> "RGBA";
case GL11.GL_RGB -> "RGB";
// 8-bit normalized
case GL30.GL_R8 -> "R8";
case GL30.GL_RG8 -> "RG8";
case GL11.GL_RGB8 -> "RGB8";
case GL11.GL_RGBA8 -> "RGBA8";
// 8-bit signed normalized
case 0x8F94 -> "R8_SNORM";
case 0x8F95 -> "RG8_SNORM";
case 0x8F96 -> "RGB8_SNORM";
case 0x8F97 -> "RGBA8_SNORM";
// 16-bit normalized
case GL30.GL_R16 -> "R16";
case GL30.GL_RG16 -> "RG16";
case GL11.GL_RGB16 -> "RGB16";
case GL11.GL_RGBA16 -> "RGBA16";
// 16-bit float
case GL30.GL_R16F -> "R16F";
case GL30.GL_RG16F -> "RG16F";
case GL30.GL_RGB16F -> "RGB16F";
case GL30.GL_RGBA16F -> "RGBA16F";
// 32-bit float
case GL30.GL_R32F -> "R32F";
case GL30.GL_RG32F -> "RG32F";
case GL30.GL_RGB32F -> "RGB32F";
case GL30.GL_RGBA32F -> "RGBA32F";
// Packed
case GL30.GL_R11F_G11F_B10F -> "R11F_G11F_B10F";
case GL11.GL_RGB10_A2 -> "RGB10_A2";
default -> String.format("0x%X", format);
};
}
private static DebugState debugState;
private interface DebugState {
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSAAccess;
import com.gtnewhorizons.angelica.glsm.dsa.DSACore;
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -28,6 +29,8 @@ import org.lwjgl.opengl.GL43;
import org.lwjgl.opengl.GL44;
import org.lwjgl.opengl.NVXGpuMemoryInfo;
import net.coderbot.iris.gl.shader.StandardMacros;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
@@ -48,6 +51,8 @@ public class RenderSystem {
private static boolean supportsSamplerObjects;
private static int maxImageUnits;
private static int maxSSBOBindings;
private static int maxGlslVersion;
private static boolean supportsGpuShader4;
// Sampler object state tracking (null if unsupported)
private static int[] samplers;
@@ -112,6 +117,10 @@ public class RenderSystem {
samplers = new int[GL11.glGetInteger(GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS)];
}
maxGlslVersion = Integer.parseInt(StandardMacros.getGlVersion(GL20.GL_SHADING_LANGUAGE_VERSION));
supportsGpuShader4 = GLStateManager.capabilities.GL_EXT_gpu_shader4;
LOGGER.info("Max GLSL version: {}, GPU Shader4: {}", maxGlslVersion, supportsGpuShader4);
LOGGER.info("Image Load/Store: {}, Max Image Units: {}", supportsImageLoadStore, maxImageUnits);
LOGGER.info("SSBO: {}, Max SSBO Bindings: {}", supportsSSBO, maxSSBOBindings);
LOGGER.info("Buffer Storage: {}, Clear Texture: {}, Sampler Objects: {}", supportsBufferStorage, supportsClearTexture, supportsSamplerObjects);
@@ -126,8 +135,11 @@ public class RenderSystem {
}
public static void texImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
GL11.glBindTexture(target, texture);
GLStateManager.glBindTexture(target, texture);
GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
if (target == GL11.GL_TEXTURE_2D && level == 0) {
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
}
public static void uniformMatrix4fv(int location, boolean transpose, FloatBuffer matrix) {
@@ -449,6 +461,22 @@ public class RenderSystem {
dsaState.textureStorage3D(texture, target, levels, internalFormat, width, height, depth);
}
public static int getMaxGlslVersion() {
return maxGlslVersion;
}
public static boolean supportsGpuShader4() {
return supportsGpuShader4;
}
public static boolean supportsSnormFormats() {
return GLStateManager.capabilities.OpenGL31;
}
public static boolean supportsPackedFloatRenderable() {
return GLStateManager.capabilities.OpenGL30 && maxGlslVersion >= 130;
}
public static boolean supportsTesselation() {
return supportsTesselation;
}
@@ -1,5 +1,6 @@
package net.coderbot.iris.gl.texture;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import net.coderbot.iris.gl.GlVersion;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
@@ -105,4 +106,29 @@ public enum InternalTextureFormat {
public GlVersion getMinimumGlVersion() {
return minimumGlVersion;
}
public InternalTextureFormat getColorRenderableFallback() {
if (!RenderSystem.supportsSnormFormats()) {
return switch (this) {
case R8_SNORM, RG8_SNORM, RGB8_SNORM, RGBA8_SNORM -> RGBA16F;
case R16_SNORM, RG16_SNORM, RGB16_SNORM, RGBA16_SNORM -> RGBA16F;
default -> this.getPackedFloatFallback();
};
}
return this.getPackedFloatFallback();
}
private InternalTextureFormat getPackedFloatFallback() {
if (!RenderSystem.supportsPackedFloatRenderable()) {
return switch (this) {
case R11F_G11F_B10F, RGB9_E5 -> RGBA16F;
case RGB8 -> RGBA8;
case RGB16 -> RGBA16;
case RGB16F -> RGBA16F;
case RGB32F -> RGBA32F;
default -> this;
};
}
return this;
}
}
@@ -54,6 +54,10 @@ public class CommonTransformer {
root.rename("texture", "gtexture");
}
if (root.hasVariable("iris_renamed_texture")) {
root.rename("iris_renamed_texture", "gtexture");
}
if (root.containsCall("gcolor") && root.hasVariable("gcolor")) {
root.rename("gcolor", "gtexture");
}
@@ -26,9 +26,11 @@ import com.gtnewhorizons.angelica.glsm.RenderSystem;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -38,33 +40,40 @@ public class ShaderTransformer {
private static final Pattern inOutVaryingPattern = Pattern.compile("(?m)^(\\s*(?:(?:flat|smooth|noperspective)\\s+)?)(in|out)(\\s+)");
private static final Pattern inPattern = Pattern.compile("(?m)^(\\s*(?:(?:flat|smooth|noperspective)\\s+)?)(in)(\\s+)");
private static final Pattern outPattern = Pattern.compile("(?m)^(\\s*(?:(?:flat|smooth|noperspective)\\s+)?)(out)(\\s+)");
private static final Pattern texturePattern = Pattern.compile("\\btexture\\s*\\(|(\\btexture\\b)");
private static final Pattern unsignedSuffixPattern = Pattern.compile("(\\b(?:\\d+|0[xX][0-9a-fA-F]+))[uU]\\b");
private static final int CACHE_SIZE = 100;
private static final Object2ObjectLinkedOpenHashMap<TransformKey<?>, Map<PatchShaderType, String>> shaderTransformationCache = new Object2ObjectLinkedOpenHashMap<>();
private static final boolean useCache = true;
// Track logged negotiations to avoid spam - cleared on shader pack reload
private static final Set<String> loggedNegotiations = new HashSet<>();
public static void clearCache() {
synchronized (shaderTransformationCache) {
shaderTransformationCache.clear();
}
loggedNegotiations.clear();
}
/**
* These are words which need to be renamed by iris if a shader uses them, regardless o the GLSL version.
* The words will get caught and renamed to iris_renamed_$WORD
*/
private static final List<String> fullReservedWords = new ArrayList<>();
/**
* This does the same thing as fullReservedWords, but based on a maximum GLSL version. As an example
* if something was register to 400 here, it would get applied on any version below 400.
*/
private static final Map<Integer, List<String>> versionedReservedWords = new HashMap<>();
static {
// texture seems to be reserved by some drivers but not others, however this is not actually reserved by the GLSL spec
fullReservedWords.add("texture");
private static String replaceTexture(String input) {
final var matcher = texturePattern.matcher(input);
final StringBuilder builder = new StringBuilder();
while (matcher.find()) {
if (matcher.group(1) != null) {
matcher.appendReplacement(builder, "iris_renamed_texture");
} else {
matcher.appendReplacement(builder, Matcher.quoteReplacement(matcher.group(0)));
}
}
matcher.appendTail(builder);
return builder.toString();
}
static {
// sample was added as a keyword in GLSL 400, many shaders use it
versionedReservedWords.put(400, List.of("sample"));
}
@@ -72,14 +81,160 @@ public class ShaderTransformer {
private record VersionRequirement(String keyword, int minVersion, BooleanSupplier supported) {}
// Sorted descending by minVersion for early exit in getRequiredVersion
private static final VersionRequirement[] VERSION_REQUIREMENTS = {
new VersionRequirement("std430", 430, RenderSystem::supportsSSBO),
new VersionRequirement("iimage", 420, RenderSystem::supportsImageLoadStore),
new VersionRequirement("uimage", 420, RenderSystem::supportsImageLoadStore),
new VersionRequirement("imageLoad", 420, RenderSystem::supportsImageLoadStore),
new VersionRequirement("imageStore", 420, RenderSystem::supportsImageLoadStore)
new VersionRequirement("imageStore", 420, RenderSystem::supportsImageLoadStore),
new VersionRequirement("uint", 130, () -> RenderSystem.getMaxGlslVersion() >= 130),
new VersionRequirement("uvec2", 130, () -> RenderSystem.getMaxGlslVersion() >= 130),
new VersionRequirement("uvec3", 130, () -> RenderSystem.getMaxGlslVersion() >= 130),
new VersionRequirement("uvec4", 130, () -> RenderSystem.getMaxGlslVersion() >= 130),
new VersionRequirement("flat", 130, () -> RenderSystem.getMaxGlslVersion() >= 130),
};
private record DowngradeRule(
int fromVersion,
int toVersion,
BooleanSupplier supported,
List<String> extensions,
Map<String, String> defines,
List<String> preprocessorDefines,
boolean convertStorageQualifiers
) {}
private static final DowngradeRule[] DOWNGRADE_RULES = {
new DowngradeRule(130, 120,
RenderSystem::supportsGpuShader4,
List.of("GL_EXT_gpu_shader4"),
Map.of("texture", "texture2D"),
List.of(
// Preprocessor defines
"#define uint int",
"#define uvec2 ivec2",
"#define uvec3 ivec3",
"#define uvec4 ivec4",
"#define isnan(x) ((x) != (x))",
"#define isinf(x) ((x) == (1.0/0.0) || (x) == (-1.0/0.0))",
"#define trunc(x) (sign(x) * floor(abs(x)))",
"#define round(x) (floor((x) + 0.5))",
"#define texelFetch texelFetch2D",
"#define textureSize textureSize2D",
"#define modf iris_modf",
// Function definitions
"GLSL_FUNC:float iris_modf(float x, out float i) { i = sign(x) * floor(abs(x)); return x - i; }",
"GLSL_FUNC:vec2 iris_modf(vec2 x, out vec2 i) { i = sign(x) * floor(abs(x)); return x - i; }",
"GLSL_FUNC:vec3 iris_modf(vec3 x, out vec3 i) { i = sign(x) * floor(abs(x)); return x - i; }",
"GLSL_FUNC:vec4 iris_modf(vec4 x, out vec4 i) { i = sign(x) * floor(abs(x)); return x - i; }",
"GLSL_FUNC:vec2 mix(vec2 a, vec2 b, bvec2 sel) { return vec2(sel.x ? b.x : a.x, sel.y ? b.y : a.y); }",
"GLSL_FUNC:vec3 mix(vec3 a, vec3 b, bvec3 sel) { return vec3(sel.x ? b.x : a.x, sel.y ? b.y : a.y, sel.z ? b.z : a.z); }",
"GLSL_FUNC:vec4 mix(vec4 a, vec4 b, bvec4 sel) { return vec4(sel.x ? b.x : a.x, sel.y ? b.y : a.y, sel.z ? b.z : a.z, sel.w ? b.w : a.w); }",
"GLSL_FUNC:bool any(bool b) { return b; }",
"GLSL_FUNC:bool all(bool b) { return b; }"
),
true
)
};
record NegotiationResult(
int targetVersion,
String profile,
List<String> extensions,
Map<String, String> defines,
List<String> preprocessorDefines,
boolean convertStorageQualifiers,
String error
) {
static NegotiationResult success(int targetVersion, String profile, List<String> extensions, Map<String, String> defines, List<String> preprocessorDefines, boolean convertStorageQualifiers) {
return new NegotiationResult(targetVersion, profile, extensions, defines, preprocessorDefines, convertStorageQualifiers, null);
}
static NegotiationResult error(String message) {
return new NegotiationResult(-1, "", List.of(), Map.of(), List.of(), false, message);
}
static NegotiationResult noop(int version, String profile) {
return new NegotiationResult(version, profile, List.of(), Map.of(), List.of(), false, null);
}
boolean isError() { return error != null; }
}
private static int getStageMinimumVersion(PatchShaderType stage) {
return switch (stage) {
case COMPUTE -> 330;
case TESS_CONTROL, TESS_EVAL -> 400;
case GEOMETRY -> 150;
default -> 110;
};
}
static NegotiationResult negotiateVersion(int effectiveVersion, PatchShaderType stage) {
final int maxGlsl = RenderSystem.getMaxGlslVersion();
if (effectiveVersion <= maxGlsl) {
if (effectiveVersion <= 120 && RenderSystem.supportsGpuShader4()) {
final String profile = "";
for (DowngradeRule rule : DOWNGRADE_RULES) {
if (rule.fromVersion == 130 && rule.toVersion == 120) {
final List<String> definesOnly = rule.preprocessorDefines.stream().filter(d -> !d.startsWith("GLSL_FUNC:")).toList();
return NegotiationResult.success(effectiveVersion, profile, rule.extensions, rule.defines, definesOnly, true);
}
}
return NegotiationResult.success(effectiveVersion, profile, List.of("GL_EXT_gpu_shader4"), Map.of(), List.of(), true);
}
return NegotiationResult.noop(effectiveVersion, effectiveVersion >= 150 ? "compatibility" : "");
}
final int stageMin = getStageMinimumVersion(stage);
if (maxGlsl < stageMin) {
return NegotiationResult.error("Hardware GLSL " + maxGlsl + " below stage minimum " + stageMin + " for " + stage.name());
}
// Walk downgrade rules from effectiveVersion toward maxGlsl
final List<String> accExtensions = new ArrayList<>();
final Map<String, String> accDefines = new HashMap<>();
final List<String> accPreprocessorDefines = new ArrayList<>();
boolean accConvertQualifiers = false;
int currentVersion = effectiveVersion;
while (currentVersion > maxGlsl) {
DowngradeRule matched = null;
for (DowngradeRule rule : DOWNGRADE_RULES) {
if (rule.fromVersion == currentVersion) {
matched = rule;
break;
}
}
if (matched == null) {
return NegotiationResult.error("No downgrade rule from GLSL " + currentVersion + " (hardware max: " + maxGlsl + ", shader requires: " + effectiveVersion + ")");
}
// Verify rule's required extensions are supported
if (!matched.supported.getAsBoolean()) {
return NegotiationResult.error("Downgrade from " + matched.fromVersion + " to " + matched.toVersion + " requires extensions " + matched.extensions + " which are not supported");
}
accExtensions.addAll(matched.extensions);
accDefines.putAll(matched.defines);
accPreprocessorDefines.addAll(matched.preprocessorDefines);
accConvertQualifiers |= matched.convertStorageQualifiers;
currentVersion = matched.toVersion;
}
if (currentVersion < stageMin) {
return NegotiationResult.error("Downgrade reached GLSL " + currentVersion + " which is below stage minimum " + stageMin + " for " + stage.name());
}
final String profile = currentVersion >= 150 ? "compatibility" : "";
return NegotiationResult.success(currentVersion, profile, accExtensions, accDefines, accPreprocessorDefines, accConvertQualifiers);
}
private static Pattern hoistPattern;
private static Object2IntMap<String> keywordToVersion;
private static int maxSupportedHoistVersion;
@@ -93,7 +248,7 @@ public class ShaderTransformer {
if (req.supported.getAsBoolean()) {
if (!patternBuilder.isEmpty()) patternBuilder.append('|');
patternBuilder.append(Pattern.quote(req.keyword));
patternBuilder.append("\\b").append(Pattern.quote(req.keyword)).append("\\b");
versionMap.put(req.keyword, req.minVersion);
maxVersion = Math.max(maxVersion, req.minVersion);
}
@@ -207,12 +362,12 @@ public class ShaderTransformer {
} else {
Map<PatchShaderType, String> result;
var patchType = parameters.patch;
final var patchType = parameters.patch;
EnumMap<PatchShaderType, String> inputs = new EnumMap<>(PatchShaderType.class);
inputs.put(PatchShaderType.COMPUTE, compute);
var key = new TransformKey<>(patchType, inputs, parameters);
final var key = new TransformKey<>(patchType, inputs, parameters);
synchronized (shaderTransformationCache) {
result = shaderTransformationCache.getAndMoveToLast(key);
@@ -223,7 +378,7 @@ public class ShaderTransformer {
parameters.type = null;
synchronized (shaderTransformationCache) {
// Double-check in case another thread added it while we were transforming
Map<PatchShaderType, String> existing = shaderTransformationCache.getAndMoveToLast(key);
final Map<PatchShaderType, String> existing = shaderTransformationCache.getAndMoveToLast(key);
if (existing != null) {
return existing;
}
@@ -239,13 +394,13 @@ public class ShaderTransformer {
}
private static <P extends Parameters> Map<PatchShaderType, String> transformComputeInternal(String compute, Patch patchType, P parameters) {
EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
final EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
Stopwatch watch = Stopwatch.createStarted();
final Stopwatch watch = Stopwatch.createStarted();
parameters.type = ShaderType.COMPUTE;
Matcher matcher = versionPattern.matcher(compute);
final Matcher matcher = versionPattern.matcher(compute);
if (!matcher.find()) {
throw new IllegalArgumentException("No #version directive found in compute shader source code!");
}
@@ -254,7 +409,7 @@ public class ShaderTransformer {
int versionInt = Integer.parseInt(versionString);
// Check if shader uses features requiring a higher GLSL version
int requiredVersion = getRequiredVersion(compute, versionInt);
final int requiredVersion = getRequiredVersion(compute, versionInt);
if (requiredVersion > versionInt) {
Iris.logger.debug("Compute shader requires GLSL {} for detected features, hoisting from {}", requiredVersion, versionInt);
versionInt = requiredVersion;
@@ -266,33 +421,40 @@ public class ShaderTransformer {
versionString = "330";
versionInt = 330;
}
// Negotiate version downgrade if needed
final NegotiationResult negotiation = negotiateVersion(versionInt, PatchShaderType.COMPUTE);
if (negotiation.isError()) {
throw new RuntimeException("Compute shader version negotiation failed: " + negotiation.error());
}
if (negotiation.targetVersion() != versionInt) {
Iris.logger.debug("Negotiated compute shader from GLSL {} to {}", versionInt, negotiation.targetVersion());
versionInt = negotiation.targetVersion();
versionString = String.valueOf(versionInt);
}
String profileString = "#version " + versionString + " core\n";
// Rename reserved words
String input = compute;
for (String reservedWord : fullReservedWords) {
String newName = "iris_renamed_" + reservedWord;
input = input.replaceAll("\\b" + reservedWord + "\\b", newName);
}
String input = replaceTexture(compute);
for (int version : versionedReservedWords.keySet()) {
if (versionInt < version) {
for (String reservedWord : versionedReservedWords.get(version)) {
String newName = "iris_renamed_" + reservedWord;
final String newName = "iris_renamed_" + reservedWord;
input = input.replaceAll("\\b" + reservedWord + "\\b", newName);
}
}
}
var parsedShader = ShaderParser.parseShader(input);
var transformer = new Transformer(parsedShader.full());
final var parsedShader = ShaderParser.parseShader(input);
final var transformer = new Transformer(parsedShader.full());
doTransform(transformer, patchType, parameters, "core", versionInt);
// Extract extensions
var extensions = versionPattern.matcher(getFormattedShader(parsedShader.pre(), "")).replaceFirst("").trim();
String header = profileString + (extensions.isEmpty() ? "" : "\n" + extensions);
final var extensions = versionPattern.matcher(getFormattedShader(parsedShader.pre(), "")).replaceFirst("").trim();
final String finalHeader = header;
final String finalHeader = profileString + (extensions.isEmpty() ? "" : "\n" + extensions);
final StringBuilder formattedShaderBuilder = new StringBuilder();
transformer.mutateTree(tree -> {
@@ -313,13 +475,13 @@ public class ShaderTransformer {
}
private static <P extends Parameters> Map<PatchShaderType, String> transformInternal(EnumMap<PatchShaderType, String> inputs, Patch patchType, P parameters) {
EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
EnumMap<PatchShaderType, Transformer> types = new EnumMap<>(PatchShaderType.class);
EnumMap<PatchShaderType, String> prepatched = new EnumMap<>(PatchShaderType.class);
List<Transformer> textureLodExtensionPatches = new ArrayList<>();
List<Transformer> hacky120Patches = new ArrayList<>();
final EnumMap<PatchShaderType, String> result = new EnumMap<>(PatchShaderType.class);
final EnumMap<PatchShaderType, Transformer> types = new EnumMap<>(PatchShaderType.class);
final EnumMap<PatchShaderType, String> prepatched = new EnumMap<>(PatchShaderType.class);
final EnumMap<PatchShaderType, NegotiationResult> negotiations = new EnumMap<>(PatchShaderType.class);
final EnumMap<PatchShaderType, Boolean> needsTextureLodExtension = new EnumMap<>(PatchShaderType.class);
Stopwatch watch = Stopwatch.createStarted();
final Stopwatch watch = Stopwatch.createStarted();
for (PatchShaderType type : PatchShaderType.VALUES) {
parameters.type = type.glShaderType;
@@ -329,7 +491,7 @@ public class ShaderTransformer {
String input = inputs.get(type);
Matcher matcher = versionPattern.matcher(input);
final Matcher matcher = versionPattern.matcher(input);
if (!matcher.find()) {
throw new IllegalArgumentException("No #version directive found in source code!");
}
@@ -342,41 +504,56 @@ public class ShaderTransformer {
String profile = "";
int versionInt = Integer.parseInt(versionString);
// Check if shader uses features requiring a higher GLSL version
int requiredVersion = getRequiredVersion(input, versionInt);
// Include celeritas header in scan — it's injected post-negotiation but contains uint/uvec3
final String scanSource = (patchType == Patch.CELERITAS_TERRAIN && type == PatchShaderType.VERTEX) ? input + computeCeleritasHeader() : input;
final int requiredVersion = getRequiredVersion(scanSource, versionInt);
if (requiredVersion > versionInt) {
Iris.logger.debug("Shader requires GLSL {} for detected features, hoisting from {}", requiredVersion, versionInt);
versionInt = requiredVersion;
versionString = String.valueOf(versionInt);
}
if (versionInt >= 150) {
profile = matcher.group(2);
if (profile == null) {
profile = "compatibility";
}
// Negotiate version downgrade if needed
final NegotiationResult negotiation = negotiateVersion(versionInt, type);
if (negotiation.isError()) {
throw new RuntimeException("Shader version negotiation failed for " + type.name() + ": " + negotiation.error());
}
String profileString = "#version " + versionString + " " + profile + "\n";
if (negotiation.targetVersion() != versionInt) {
String negotiationKey = type.name() + "_" + versionInt + "_" + negotiation.targetVersion();
if (loggedNegotiations.add(negotiationKey)) {
Iris.logger.info("Negotiated {} shader from GLSL {} to {} (extensions: {}, polyfills: {})", type.name(), versionInt, negotiation.targetVersion(), negotiation.extensions(), negotiation.preprocessorDefines().size());
}
versionInt = negotiation.targetVersion();
versionString = String.valueOf(versionInt);
profile = negotiation.profile();
} else {
if (versionInt >= 150) {
profile = matcher.group(2);
if (profile == null) {
profile = "compatibility";
}
}
}
negotiations.put(type, negotiation);
final String profileString = "#version " + versionString + (profile.isEmpty() ? "" : " " + profile) + "\n";
input = replaceTexture(input);
// The primary reason we rename words here using regex, is because if the words cause invalid
// GLSL, regardless of the version being used, it will cause glsl-transformation-lib to fail
// so we need to rename them prior to passing the shader input to glsl-transformation-lib.
for (String reservedWord : fullReservedWords) {
String newName = "iris_renamed_" + reservedWord;
input = input.replaceAll("\\b" + reservedWord + "\\b", newName);
}
for (int version : versionedReservedWords.keySet()) {
if (versionInt < version) {
for (String reservedWord : versionedReservedWords.get(version)) {
String newName = "iris_renamed_" + reservedWord;
final String newName = "iris_renamed_" + reservedWord;
input = input.replaceAll("\\b" + reservedWord + "\\b", newName);
}
}
}
var parsedShader = ShaderParser.parseShader(input);
var transformer = new Transformer(parsedShader.full());
final var parsedShader = ShaderParser.parseShader(input);
final var transformer = new Transformer(parsedShader.full());
if (parameters.type == ShaderType.VERTEX || parameters.type == ShaderType.FRAGMENT) {
upgradeStorageQualifiers(transformer, parameters);
@@ -386,28 +563,25 @@ public class ShaderTransformer {
// Check if we need to patch in texture LOD extension enabling
if (versionInt <= 120 && (transformer.containsCall("texture2DLod") || transformer.containsCall("texture3DLod") || transformer.containsCall("texture2DGradARB"))) {
textureLodExtensionPatches.add(transformer);
}
// Check if we need to patch in some hacky GLSL 120 compat
if (versionInt <= 120) {
hacky120Patches.add(transformer);
needsTextureLodExtension.put(type, true);
}
// Extract extensions from the pre-parsed content (version + extensions before main code)
// This preserves #extension directives that the shader pack declares
var extensions = versionPattern.matcher(getFormattedShader(parsedShader.pre(), "")).replaceFirst("").trim();
final var extensions = versionPattern.matcher(getFormattedShader(parsedShader.pre(), "")).replaceFirst("").trim();
types.put(type, transformer);
prepatched.put(type, profileString + (extensions.isEmpty() ? "" : "\n" + extensions));
}
CompatibilityTransformer.transformGrouped(types, parameters);
for (var entry : types.entrySet()) {
final PatchShaderType shaderType = entry.getKey();
final Transformer transformer = entry.getValue();
String header = prepatched.get(entry.getKey());
String header = prepatched.get(shaderType);
final NegotiationResult negotiation = negotiations.get(shaderType);
// For Celeritas terrain vertex shaders, inject chunk_vertex.glsl header
if (patchType == Patch.CELERITAS_TERRAIN && entry.getKey() == PatchShaderType.VERTEX) {
if (patchType == Patch.CELERITAS_TERRAIN && shaderType == PatchShaderType.VERTEX) {
header += computeCeleritasHeader();
}
@@ -424,39 +598,38 @@ public class ShaderTransformer {
formattedShader = formattedShader.replace("iris_renamed_texture", "texture");
formattedShader = formattedShader.replace("iris_renamed_sample", "sample");
// Please don't mind the entire rest of this loop basically, we're doing awful fragile regex on the transformed
// shader output to do things that I can't figure out with AST because I'm bad at it
if (textureLodExtensionPatches.contains(transformer)) {
String[] parts = formattedShader.split("\n", 2);
// Inject texture LOD extension if needed
if (needsTextureLodExtension.containsKey(shaderType)) {
final String[] parts = formattedShader.split("\n", 2);
parts[1] = "#extension GL_ARB_shader_texture_lod : require\n" + parts[1];
formattedShader = parts[0] + "\n" + parts[1];
}
if (hacky120Patches.contains(transformer)) {
// Forcibly enable GL_EXT_gpu_shader4, it has a lot of compatibility backports with GLSL 130+
// and seems more or less universally supported by hardware/drivers
String[] parts = formattedShader.split("\n", 2);
parts[1] = "#extension GL_EXT_gpu_shader4 : require\n" + parts[1];
formattedShader = parts[0] + "\n" + parts[1];
if (negotiation != null && (!negotiation.extensions().isEmpty() || !negotiation.preprocessorDefines().isEmpty())) {
formattedShader = injectGlslPreamble(formattedShader, negotiation.extensions(), negotiation.preprocessorDefines());
}
// GLSL 120 compatibility for in/out specifiers:
// - Vertex shaders: "in" = vertex attribute input -> "attribute", "out" = to fragment -> "varying"
// - Fragment shaders: "in" = from vertex -> "varying", "out" = color output -> handled elsewhere
if (entry.getKey() == PatchShaderType.VERTEX) {
// In vertex shaders, "in" declarations are vertex attributes, not varyings
Matcher inMatcher = inPattern.matcher(formattedShader);
formattedShader = inMatcher.replaceAll("$1attribute$3");
Matcher outMatcher = outPattern.matcher(formattedShader);
formattedShader = outMatcher.replaceAll("$1varying$3");
} else {
// In fragment (and geometry) shaders, both in/out become varying
Matcher inOutVaryingMatcher = inOutVaryingPattern.matcher(formattedShader);
formattedShader = inOutVaryingMatcher.replaceAll("$1varying$3");
if (negotiation != null && !negotiation.defines().isEmpty()) {
for (var defineEntry : negotiation.defines().entrySet()) {
formattedShader = formattedShader.replaceAll("\\b" + Pattern.quote(defineEntry.getKey()) + "\\b", Matcher.quoteReplacement(defineEntry.getValue()));
}
}
result.put(entry.getKey(), formattedShader);
// Convert storage qualifiers for downgraded shaders or native 120 shaders
if (negotiation != null && (negotiation.convertStorageQualifiers() || negotiation.targetVersion() <= 120)) {
if (shaderType == PatchShaderType.VERTEX) {
final Matcher inMatcher = inPattern.matcher(formattedShader);
formattedShader = inMatcher.replaceAll("$1attribute$3");
final Matcher outMatcher = outPattern.matcher(formattedShader);
formattedShader = outMatcher.replaceAll("$1varying$3");
} else {
final Matcher inOutVaryingMatcher = inOutVaryingPattern.matcher(formattedShader);
formattedShader = inOutVaryingMatcher.replaceAll("$1varying$3");
}
formattedShader = unsignedSuffixPattern.matcher(formattedShader).replaceAll("$1");
}
result.put(shaderType, formattedShader);
}
watch.stop();
Iris.logger.info("[Load #{}] Transformed shader for {} in {}", Iris.getShaderPackLoadId(), patchType.name(), watch);
@@ -491,13 +664,6 @@ public class ShaderTransformer {
transformer.renameFunctionCall("ftransform", "iris_ftransform");
}
public static void replaceGlMultiTexCoordBounded(Transformer transformer, int min, int max) {
for (int i = min; i <= max; i++) {
transformer.replaceExpression("gl_MultiTexCoord" + i, "vec4(0.0, 0.0, 0.0, 1.0)");
}
}
public static void patchMultiTexCoord3(Transformer transformer, Parameters parameters) {
if (parameters.type == ShaderType.VERTEX && transformer.hasVariable("gl_MultiTexCoord3") && !transformer.hasVariable("mc_midTexCoord")) {
transformer.rename("gl_MultiTexCoord3", "mc_midTexCoord");
@@ -506,7 +672,7 @@ public class ShaderTransformer {
}
public static void replaceMidTexCoord(Transformer transformer, float textureScale) {
int type = transformer.findType("mc_midTexCoord");
final int type = transformer.findType("mc_midTexCoord");
if (type != 0) {
transformer.removeVariable("mc_midTexCoord");
}
@@ -590,6 +756,62 @@ public class ShaderTransformer {
}
}
private static String injectGlslPreamble(String shader, List<String> extensions, List<String> preprocessorDefines) {
final StringBuilder extensionBlock = new StringBuilder();
final StringBuilder defineBlock = new StringBuilder();
final StringBuilder functionBlock = new StringBuilder();
for (String ext : extensions) {
extensionBlock.append("#extension ").append(ext).append(" : require\n");
}
for (String define : preprocessorDefines) {
if (define.startsWith("GLSL_FUNC:")) {
functionBlock.append(define.substring(10)).append("\n");
} else {
defineBlock.append(define).append("\n");
}
}
// Find where preprocessor section ends and code begins
final String[] lines = shader.split("\n");
final StringBuilder shaderResult = new StringBuilder();
boolean extensionsInjected = false, definesInjected = false, functionsInjected = false;
for (String line : lines) {
final String trimmed = line.trim();
final boolean isPreprocessor = trimmed.startsWith("#");
final boolean isExtension = trimmed.startsWith("#extension");
final boolean isVersion = trimmed.startsWith("#version");
// Inject our extensions right after #version
if (isVersion) {
shaderResult.append(line).append("\n");
shaderResult.append(extensionBlock);
extensionsInjected = true;
continue;
}
// Inject our defines after the last #extension line (before other preprocessor or code)
if (!definesInjected && extensionsInjected && !isExtension && !trimmed.isEmpty()) {
shaderResult.append(defineBlock);
definesInjected = true;
}
// Inject our functions after all preprocessor lines
if (!functionsInjected && definesInjected && !isPreprocessor && !trimmed.isEmpty()) {
shaderResult.append(functionBlock);
functionsInjected = true;
}
shaderResult.append(line).append("\n");
}
if (!definesInjected) shaderResult.append(defineBlock);
if (!functionsInjected) shaderResult.append(functionBlock);
return shaderResult.toString();
}
public static String getFormattedShader(ParseTree tree, String string) {
final StringBuilder sb = new StringBuilder(string + "\n");
final String[] tabHolder = {""};
@@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableSet;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
import net.coderbot.iris.gl.texture.DepthBufferFormat;
import net.coderbot.iris.gl.texture.DepthCopyStrategy;
@@ -46,9 +47,15 @@ public class RenderTargets {
renderTargets.forEach((index, settings) -> {
// TODO: Handle mipmapping?
Vector2i dimensions = packDirectives.getTextureScaleOverride(index, width, height);
// Apply format fallback for opengl versions with limited color-renderable format support (e.g., macOS GL 2.1)
var requestedFormat = settings.getInternalFormat();
var actualFormat = requestedFormat.getColorRenderableFallback();
if (actualFormat != requestedFormat) {
Iris.logger.info("Render target {} using fallback format {} (requested {})", index, actualFormat, requestedFormat);
}
targets[index] = RenderTarget.builder().setDimensions(dimensions.x, dimensions.y)
.setInternalFormat(settings.getInternalFormat())
.setPixelFormat(settings.getInternalFormat().getPixelFormat()).build();
.setInternalFormat(actualFormat)
.setPixelFormat(actualFormat.getPixelFormat()).build();
});
this.currentDepthTexture = depthTexture;
this.currentDepthFormat = depthFormat;
@@ -315,6 +315,10 @@ public class ShaderPack {
// directly. This removes one obstacle to accurate reporting of line numbers for errors,
// though there exist many more (such as relocating all #extension directives and similar things)
String source = builder.toString();
// Apply shader pack workarounds for version compatibility (before preprocessing)
source = ShaderPackWorkarounds.apply(source);
source = JcppProcessor.glslPreprocessSource(source, finalEnvironmentDefines1);
return source;
@@ -0,0 +1,37 @@
package net.coderbot.iris.shaderpack;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import java.util.List;
/**
* Shader pack workarounds for specific GLSL versions. These are applied before preprocessing.
*/
public class ShaderPackWorkarounds {
private record Workaround(int maxVersion, String find, String replace) {
boolean appliesTo(int glslVersion) {
return glslVersion <= maxVersion;
}
}
private static final List<Workaround> WORKAROUNDS = List.of(
// Complementary: Catmull-Rom TAA filter causes "creeping black fog" on OGL 2.1
new Workaround(120,
"#define TAA_MOVEMENT_IMPROVEMENT_FILTER",
"// #define TAA_MOVEMENT_IMPROVEMENT_FILTER // disabled: OGL 2.1 workaround"
)
);
public static String apply(String source) {
int glslVersion = RenderSystem.getMaxGlslVersion();
for (Workaround w : WORKAROUNDS) {
if (w.appliesTo(glslVersion)) {
source = source.replace(w.find, w.replace);
}
}
return source;
}
}
@@ -321,7 +321,8 @@ public class ShaderProperties {
if (pass.contains(".")) {
if (!RenderSystem.supportsBufferBlending()) {
throw new RuntimeException("Buffer blending is not supported on this platform, however it was attempted to be used!");
Iris.logger.warn("Per-buffer blending directive '{}' ignored - buffer blending requires OpenGL 4.0 or GL_ARB_draw_buffers_blend", key);
return;
}
final String[] parts = pass.split("\\.");