Compare commits

..
7 Commits
20 changed files with 680 additions and 43 deletions

No files matched your search

@@ -5814,5 +5814,7 @@ public class GLStateManager {
public static void glGetUniform(int program, int location, IntBuffer params) {
RENDER_BACKEND.getUniformiv(program, location, params);
}
public static void glGetTexImage(int target, int level, int format, int type, long pixels) {
GL11.glGetTexImage(target, level, format, type, pixels);
}
}
@@ -36,8 +36,11 @@ public class GlslTransformUtils {
/** Reserved words added in later GLSL versions that may appear as identifiers in older shaders. */
private record ReservedWordRename(Pattern pattern, String replacement) {}
private static final Map<Integer, List<ReservedWordRename>> VERSIONED_RESERVED_WORDS = Map.of(
// Always rename sample, glsl-transformation-lib doesn't like it
0, List.of(
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample")
),
400, List.of(
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"),
new ReservedWordRename(Pattern.compile("\\bsampler\\b(?!\\d)"), RENAMED_PREFIX + "sampler")
)
);
@@ -273,21 +273,23 @@ public final class FragmentShaderGenerator {
sb.append(" // Alpha test\n");
final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc());
sb.append(" float qa = round(color.a * 255.0);\n");
sb.append(" float qr = round(u_AlphaRef * 255.0);\n");
switch (func) {
case GL11.GL_NEVER ->
sb.append(" discard;\n");
case GL11.GL_LESS ->
sb.append(" if (color.a >= u_AlphaRef) discard;\n");
sb.append(" if (qa >= qr) discard;\n");
case GL11.GL_EQUAL ->
sb.append(" if (color.a != u_AlphaRef) discard;\n");
sb.append(" if (qa != qr) discard;\n");
case GL11.GL_LEQUAL ->
sb.append(" if (color.a > u_AlphaRef) discard;\n");
sb.append(" if (qa > qr) discard;\n");
case GL11.GL_GREATER ->
sb.append(" if (color.a <= u_AlphaRef) discard;\n");
sb.append(" if (qa <= qr) discard;\n");
case GL11.GL_NOTEQUAL ->
sb.append(" if (color.a == u_AlphaRef) discard;\n");
sb.append(" if (qa == qr) discard;\n");
case GL11.GL_GEQUAL ->
sb.append(" if (color.a < u_AlphaRef) discard;\n");
sb.append(" if (qa < qr) discard;\n");
case GL11.GL_ALWAYS -> {}
}
}
@@ -14,6 +14,7 @@ import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -79,6 +80,9 @@ public class GLSMRedirector {
glCapRedirects.put(org.lwjgl.opengl.GL11.GL_SCISSOR_TEST, "ScissorTest");
final var gl11 = RedirectMap.newMap()
// glEnable/Disable - Special cased in GLSMRedirector, but included here for the external API
.add("glEnable")
.add("glDisable")
.add("glAlphaFunc")
.add("glBegin")
.add("glBindTexture")
@@ -495,6 +499,22 @@ public class GLSMRedirector {
return CORE_EXCLUSIONS.clone();
}
/** The internal-name prefix used to match GL version classes (e.g., {@code "org/lwjgl/opengl/GL"}). */
public static String getGLPrefix() { return GL_PREFIX; }
/** The internal name of the redirect target class. */
public static String getTargetClassName() { return GLStateManager; }
/** Method redirects for GL-prefix classes. Key: original method name, Value: GLStateManager method name. */
public static Map<String, String> getGLPrefixMethodRedirects() {
return Collections.unmodifiableMap(glMethodRedirects);
}
/** Method redirects for named (non-GL-prefix) classes. Key: internal class name, Value: per-method redirect map. */
public static Map<String, Map<String, String>> getNamedClassMethodRedirects() {
return Collections.unmodifiableMap(methodRedirects);
}
public boolean shouldTransform(byte[] basicClass) {
return cstPoolParser.find(basicClass, true);
}
@@ -0,0 +1,118 @@
package com.gtnewhorizons.angelica.api;
import com.gtnewhorizons.angelica.glsm.redirect.GLSMRedirector;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.regex.Pattern;
/**
* Public API for querying Angelica's GL call redirections.
* <p>
* Angelica's bytecode transformer redirects GL calls (e.g., {@code GL11.glPushMatrix()}) to
* {@link com.gtnewhorizons.angelica.glsm.GLStateManager GLStateManager} for state tracking and
* core profile compatibility. This works for normal Java bytecode loaded through the Forge classloader,
* but callers that bypass class transformation (e.g., Nashorn/script engines, reflection, MethodHandles)
* need to perform their own redirection.
* <p>
* This class provides the redirect mapping data and a precompiled regex pattern for transforming
* script source text before evaluation.
*
* <h3>Example: transforming a Nashorn script before eval</h3>
* Given a script that uses {@code importPackage(Packages.org.lwjgl.opengl)} and calls
* {@code GL11.glPushMatrix()}, the text transformation rewrites those calls before evaluation:
* <pre>{@code
* // Make GLStateManager available to the script -- either via importPackage:
* script = "importPackage(Packages.com.gtnewhorizons.angelica.glsm);\n" + script;
* // ... or via Java.type for an explicit binding:
* // script = "var GLStateManager = Java.type('" + GLRedirects.TARGET_CLASS + "');\n" + script;
*
* // Rewrite: GL11.glPushMatrix() -> GLStateManager.glPushMatrix()
* script = GLRedirects.getMethodRedirectPattern().matcher(script).replaceAll(mr -> {
* String target = GLRedirects.getTargetMethodName(mr.group(0));
* return target != null ? "GLStateManager." + target : mr.group(0);
* });
*
* engine.eval(script);
* }</pre>
*/
public final class GLRedirects {
/** Fully qualified name of the GLStateManager class that all methods redirect to. */
@SuppressWarnings("unused")
public static final String TARGET_CLASS = "com.gtnewhorizons.angelica.glsm.GLStateManager";
private static final String SHORT_GL_PREFIX;
private static final Map<String, Map<String, String>> NAMED_CLASS_SHORT_NAME_MAP;
private static final Map<String, String> GL_PREFIX_METHOD_REDIRECTS;
private static final Pattern METHOD_REDIRECT_PATTERN;
static {
SHORT_GL_PREFIX = buildShortGLPrefix();
NAMED_CLASS_SHORT_NAME_MAP = buildNamedClassShortNameMap();
GL_PREFIX_METHOD_REDIRECTS = GLSMRedirector.getGLPrefixMethodRedirects();
METHOD_REDIRECT_PATTERN = buildMethodRedirectPattern();
}
private GLRedirects() {}
/**
* Returns a precompiled pattern that matches redirected {@code ClassName.methodName} references
* using short (simple) class names, as they might typically appear in script text.
*
* @return compiled regex pattern
*/
public static Pattern getMethodRedirectPattern() {
return METHOD_REDIRECT_PATTERN;
}
/**
* Looks up the GLStateManager target method name for a short-name reference.
*
* @param classAndMethod short class name and method, e.g., {@code "GL11.glPushMatrix"}
* @return the target method name on GLStateManager (e.g., {@code "glPushMatrix"}), or {@code null} if this reference is not redirected
*/
public static String getTargetMethodName(String classAndMethod) {
final int dot = classAndMethod.indexOf('.');
if (dot < 0) return null;
final String className = classAndMethod.substring(0, dot);
final String methodName = classAndMethod.substring(dot + 1);
final Map<String, String> classRedirects = NAMED_CLASS_SHORT_NAME_MAP.get(className);
if (classRedirects != null) {
return classRedirects.get(methodName);
}
if (className.startsWith(SHORT_GL_PREFIX)) {
return GL_PREFIX_METHOD_REDIRECTS.get(methodName);
}
return null;
}
private static String buildShortGLPrefix() {
final String glPrefix = GLSMRedirector.getGLPrefix();
return glPrefix.substring(glPrefix.lastIndexOf('/') + 1);
}
private static Map<String, Map<String, String>> buildNamedClassShortNameMap() {
final Map<String, Map<String, String>> named = new HashMap<>();
for (final var entry : GLSMRedirector.getNamedClassMethodRedirects().entrySet()) {
final String internalName = entry.getKey();
final String shortName = internalName.substring(internalName.lastIndexOf('/') + 1);
named.put(shortName, entry.getValue());
}
return named;
}
private static Pattern buildMethodRedirectPattern() {
final TreeSet<String> alternatives = new TreeSet<>();
alternatives.add(Pattern.quote(SHORT_GL_PREFIX) + "\\w+");
for (final String shortName : NAMED_CLASS_SHORT_NAME_MAP.keySet()) {
alternatives.add(Pattern.quote(shortName));
}
final String classAlt = String.join("|", alternatives);
return Pattern.compile("\\b(" + classAlt + ")\\.\\w+\\b");
}
}
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.client.rendering;
import com.github.bsideup.jabel.Desugar;
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
import com.gtnewhorizon.gtnhlib.client.renderer.ITessellatorInstance;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
@@ -33,10 +34,30 @@ class DeferredBatchTessellator extends DirectTessellator {
private final Vector3f scratch = new Vector3f();
private Matrix4fc currentTransform;
private Tessellator parentTessellator;
private boolean inIntercept;
DeferredBatchTessellator(ByteBuffer buffer) {
super(buffer, false); // deleteAfter=false: keep buffer across resets for reuse
}
void setParentTessellator(Tessellator parent) {
this.parentTessellator = parent;
}
@Override
public int draw() {
if (!inIntercept) {
final Tessellator parent = parentTessellator;
if (parent != null && parent.isDrawing) {
interceptDraw(parent);
((ITessellatorInstance) parent).discard();
}
}
isDrawing = false;
return 0;
}
@Override
protected long writeVertexData(VertexFormat format, int[] rawBuffer, int rawBufferIndex) {
if (currentTransform != null) {
@@ -53,27 +74,32 @@ class DeferredBatchTessellator extends DirectTessellator {
@Override
protected int interceptDraw(Tessellator tess) {
// Compute delta: if current MV differs from default, pre-transform vertices
final Matrix4f currentMV = GLStateManager.getModelViewMatrix();
if (!currentMV.equals(defaultModelview)) {
defaultModelviewInverse.mul(currentMV, deltaMatrix);
this.currentTransform = deltaMatrix;
} else {
this.currentTransform = null;
inIntercept = true;
try {
// Compute delta: if current MV differs from default, pre-transform vertices
final Matrix4f currentMV = GLStateManager.getModelViewMatrix();
if (!currentMV.equals(defaultModelview)) {
defaultModelviewInverse.mul(currentMV, deltaMatrix);
this.currentTransform = deltaMatrix;
} else {
this.currentTransform = null;
}
final int byteOffsetBefore = bufferLimit();
final int vertCount = tess.vertexCount;
final int drawMode = tess.drawMode;
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
final long stateKey = DeferredDrawBatcher.captureStateKey();
final int result = super.interceptDraw(tess); // repacks int[] -> ByteBuffer + transforms if vertexTransform set
final int byteOffsetAfter = bufferLimit();
ranges.add(new DrawRange(stateKey, byteOffsetBefore, byteOffsetAfter - byteOffsetBefore, vertCount, drawMode, flags));
this.currentTransform = null; // clear for next draw
return result;
} finally {
inIntercept = false;
}
final int byteOffsetBefore = bufferLimit();
final int vertCount = tess.vertexCount;
final int drawMode = tess.drawMode;
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
final long stateKey = DeferredDrawBatcher.captureStateKey();
final int result = super.interceptDraw(tess); // repacks int[] → ByteBuffer + transforms if vertexTransform set
final int byteOffsetAfter = bufferLimit();
ranges.add(new DrawRange(stateKey, byteOffsetBefore, byteOffsetAfter - byteOffsetBefore, vertCount, drawMode, flags));
this.currentTransform = null; // clear for next draw
return result;
}
List<DrawRange> getRanges() { return ranges; }
@@ -86,6 +112,7 @@ class DeferredBatchTessellator extends DirectTessellator {
@Override
protected void onRemovedFromStack() {
parentTessellator = null;
clearRanges();
super.onRemovedFromStack(); // calls reset(), keeps baseBuffer (deleteAfter=false)
}
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.client.rendering;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import lombok.Getter;
import net.minecraft.client.renderer.Tessellator;
import org.lwjgl.opengl.GL11;
import java.util.Comparator;
@@ -37,6 +38,7 @@ public class DeferredDrawBatcher {
batchTessellator = new DeferredBatchTessellator(memAlloc(INITIAL_BUFFER_SIZE));
}
batchTessellator.clearRanges();
batchTessellator.setParentTessellator(Tessellator.instance);
batchTessellator.snapshotDefaultModelview();
TessellatorManager.startCapturingDirect(batchTessellator);
}
@@ -15,6 +15,7 @@ public class AngelicaRfbPlugin implements RfbPlugin {
@Override
public void onConstruction(@NotNull PluginContext ctx) {
Launch.blackboard.put("angelica.rfbPluginLoaded", Boolean.TRUE);
Launch.classLoader.addClassLoaderExclusion("com.gtnewhorizons.angelica.glsm.redirect.");
}
@Override
@@ -37,7 +37,10 @@ public enum Mixins implements IMixins {
, "angelica.MixinMinecraft_FrameHook"
, "angelica.MixinMinecraftServer"
, "angelica.bugfixes.MixinItemRenderer_EdgeDepth"
, "angelica.bugfixes.MixinModelWither_ArmorCentering"
, "angelica.bugfixes.MixinRenderBlocks_CrossedSquaresNormal"
, "angelica.bugfixes.MixinRenderGlobal_DestroyBlock"
, "angelica.bugfixes.MixinRenderWither_ArmorCentering"
, "angelica.bugfixes.MixinRendererLivingEntity_EyeDepth"
, "angelica.debug.MixinMinecraft_FPSCap"
, "angelica.ffp.MixinTessellator_CoreProfile"
@@ -0,0 +1,5 @@
package com.gtnewhorizons.angelica.rendering;
public class WitherArmorState {
public static boolean pendingInflate = false;
}
@@ -200,14 +200,17 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
public DeferredWorldRenderingPipeline(ProgramSet programs) {
Objects.requireNonNull(programs);
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> prepareTransformFutures = submitCompositeTransforms(programs.getPrepare());
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> deferredTransformFutures = submitCompositeTransforms(programs.getDeferred());
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> compositeTransformFutures = submitCompositeTransforms(programs.getComposite());
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> prepareTransformFutures =
submitCompositeTransforms(programs.getPrepare(), TextureStage.PREPARE, programs.getPackDirectives().getTextureMap());
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> deferredTransformFutures =
submitCompositeTransforms(programs.getDeferred(), TextureStage.DEFERRED, programs.getPackDirectives().getTextureMap());
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> compositeTransformFutures =
submitCompositeTransforms(programs.getComposite(), TextureStage.COMPOSITE_AND_FINAL, programs.getPackDirectives().getTextureMap());
final CompletableFuture<Map<PatchShaderType, String>> finalTransformFuture =
programs.getCompositeFinal()
.filter(ProgramSource::isValid)
.map(DeferredWorldRenderingPipeline::submitCompositeTransform)
.map(source -> submitCompositeTransform(source, TextureStage.COMPOSITE_AND_FINAL, programs.getPackDirectives().getTextureMap()))
.orElse(null);
resolver = new ProgramFallbackResolver(programs);
@@ -1710,11 +1713,20 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
private static final InputAvailability INPUT_TEXTURE_LIGHTMAP = new InputAvailability(true, true);
private static final InputAvailability[] INPUT_AVAILABILITIES = { INPUT_NONE, INPUT_TEXTURE, INPUT_TEXTURE_LIGHTMAP };
private static CompletableFuture<Map<PatchShaderType, String>> submitCompositeTransform(ProgramSource source) {
return Iris.ShaderTransformExecutor.submitTracked(() -> TransformPatcher.patchComposite(source.getVertexSource().orElse(null), source.getGeometrySource().orElse(null), source.getTessControlSource().orElse(null), source.getTessEvalSource().orElse(null), source.getFragmentSource().orElse(null)));
private static CompletableFuture<Map<PatchShaderType, String>> submitCompositeTransform(ProgramSource source, TextureStage stage,
Object2ObjectMap<Tri<String, TextureType, TextureStage>, String> textureMap) {
return Iris.ShaderTransformExecutor.submitTracked(() -> TransformPatcher.patchComposite(
source.getVertexSource().orElse(null),
source.getGeometrySource().orElse(null),
source.getTessControlSource().orElse(null),
source.getTessEvalSource().orElse(null),
source.getFragmentSource().orElse(null),
stage,
textureMap));
}
private static Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> submitCompositeTransforms(ProgramSource[] sources) {
private static Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> submitCompositeTransforms(ProgramSource[] sources,
TextureStage stage, Object2ObjectMap<Tri<String, TextureType, TextureStage>, String> textureMap) {
// Count valid sources for initial capacity
int count = 0;
for (ProgramSource source : sources) {
@@ -1723,7 +1735,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
final Map<Integer, CompletableFuture<Map<PatchShaderType, String>>> futures = new HashMap<>(count);
for (int i = 0; i < sources.length; i++) {
if (sources[i] != null && sources[i].isValid()) {
futures.put(i, submitCompositeTransform(sources[i]));
futures.put(i, submitCompositeTransform(sources[i], stage, textureMap));
}
}
return futures;
@@ -448,6 +448,7 @@ public class ShaderTransformer {
default:
throw new IllegalStateException("Unknown patch type: " + patchType.name());
}
TextureTransformer.transform(transformer, parameters);
CompatibilityTransformer.transformEach(transformer, parameters);
}
@@ -0,0 +1,51 @@
package net.coderbot.iris.pipeline.transform;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import net.coderbot.iris.gl.texture.TextureType;
import net.coderbot.iris.helpers.Tri;
import net.coderbot.iris.pipeline.transform.parameter.Parameters;
import net.coderbot.iris.shaderpack.texture.TextureStage;
import org.taumc.glsl.Transformer;
import org.taumc.glsl.grammar.GLSLLexer;
final class TextureTransformer {
private TextureTransformer() {
}
static void transform(Transformer transformer, Parameters parameters) {
final Object2ObjectMap<Tri<String, TextureType, TextureStage>, String> textureMap = parameters.getTextureMap();
if (textureMap == null || textureMap.isEmpty()) {
return;
}
final TextureStage stage = parameters.getTextureStage();
textureMap.forEach((key, replacement) -> {
if (key.third() != stage) {
return;
}
final String name = key.first();
final int type = transformer.findType(name);
if (type != 0 && isTypeValid(key.second(), type)) {
transformer.rename(name, replacement);
}
});
}
private static boolean isTypeValid(TextureType expectedType, int extractedType) {
return switch (expectedType) {
case TEXTURE_1D -> extractedType == GLSLLexer.SAMPLER1D ||
extractedType == GLSLLexer.ISAMPLER1D ||
extractedType == GLSLLexer.USAMPLER1D;
case TEXTURE_RECTANGLE -> extractedType == GLSLLexer.SAMPLER2DRECT ||
extractedType == GLSLLexer.ISAMPLER2DRECT ||
extractedType == GLSLLexer.USAMPLER2DRECT;
case TEXTURE_2D -> extractedType == GLSLLexer.SAMPLER2D ||
extractedType == GLSLLexer.ISAMPLER2D ||
extractedType == GLSLLexer.USAMPLER2D;
case TEXTURE_3D -> extractedType == GLSLLexer.SAMPLER3D ||
extractedType == GLSLLexer.ISAMPLER3D ||
extractedType == GLSLLexer.USAMPLER3D;
};
}
}
@@ -120,10 +120,6 @@ public class TransformPatcher {
return patchComposite(vertex, geometry, null, null, fragment, TextureStage.COMPOSITE_AND_FINAL, null);
}
public static Map<PatchShaderType, String> patchComposite(String vertex, String geometry, String tessControl, String tessEval, String fragment) {
return patchComposite(vertex, geometry, tessControl, tessEval, fragment, TextureStage.COMPOSITE_AND_FINAL, null);
}
public static Map<PatchShaderType, String> patchComposite(String vertex, String geometry, String tessControl, String tessEval, String fragment, TextureStage stage, Object2ObjectMap<Tri<String, TextureType, TextureStage>, String> textureMap) {
return transform(vertex, geometry, tessControl, tessEval, fragment, new TextureStageParameters(Patch.COMPOSITE, stage, textureMap));
}
@@ -146,7 +146,7 @@ public class FinalPassRenderer {
GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, 0);
}
private static Map<PatchShaderType, String> getTransformed(ProgramSource source, CompletableFuture<Map<PatchShaderType, String>> precomputedTransformFuture, String stageName) {
private Map<PatchShaderType, String> getTransformed(ProgramSource source, CompletableFuture<Map<PatchShaderType, String>> precomputedTransformFuture, String stageName) {
if (precomputedTransformFuture != null) {
try {
final Map<PatchShaderType, String> result = precomputedTransformFuture.join();
@@ -157,7 +157,14 @@ public class FinalPassRenderer {
throw new RuntimeException("Shader transformation failed for '" + source.getName() + "' in stage '" + stageName + "'", e.getCause() != null ? e.getCause() : e);
}
}
return TransformPatcher.patchComposite(source.getVertexSource().orElseThrow(NullPointerException::new), source.getGeometrySource().orElse(null), source.getTessControlSource().orElse(null), source.getTessEvalSource().orElse(null), source.getFragmentSource().orElseThrow(NullPointerException::new));
return TransformPatcher.patchComposite(
source.getVertexSource().orElseThrow(NullPointerException::new),
source.getGeometrySource().orElse(null),
source.getTessControlSource().orElse(null),
source.getTessEvalSource().orElse(null),
source.getFragmentSource().orElseThrow(NullPointerException::new),
TextureStage.COMPOSITE_AND_FINAL,
pipeline != null ? pipeline.getTextureMap() : null);
}
private static final class Pass {
@@ -263,7 +263,9 @@ public class ShadowCompositeRenderer {
source.getGeometrySource().orElse(null),
source.getTessControlSource().orElse(null),
source.getTessEvalSource().orElse(null),
source.getFragmentSource().orElseThrow(NullPointerException::new));
source.getFragmentSource().orElseThrow(NullPointerException::new),
TextureStage.SHADOWCOMP,
pipeline.getTextureMap());
String vertex = transformed.get(PatchShaderType.VERTEX);
String geometry = transformed.get(PatchShaderType.GEOMETRY);
String tessControl = transformed.get(PatchShaderType.TESS_CONTROL);
@@ -0,0 +1,71 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.bugfixes;
import com.gtnewhorizons.angelica.rendering.WitherArmorState;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.model.ModelWither;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Creates a centered armor model for the Wither.
*
* Vanilla 1.7.10 uses glScalef(1.1) on the base model for the armor overlay, which
* shifts off-center and rotating parts away from their correct positions. Modern Minecraft
* (1.21+) replaced this with a separate model using CubeDeformation(0.5).
* This mixin backports that approach.
*/
@Mixin(ModelWither.class)
public class MixinModelWither_ArmorCentering {
// Contains body segments: [0] shoulder, [1] upper body + ribs, [2] tail
@Shadow private ModelRenderer[] field_82905_a;
// Heads: [0] center, [1] left, [2] right
@Shadow private ModelRenderer[] field_82904_b;
private static final float ARMOR_INFLATE = 0.5F;
// If the armor inflate flag is set, rebuild all boxes with uniform inflate.
@Inject(method = "<init>", at = @At("TAIL"))
private void angelica$applyInflate(CallbackInfo ci) {
if (!WitherArmorState.pendingInflate) return;
WitherArmorState.pendingInflate = false;
ModelWither self = (ModelWither) (Object) this;
// [0] Shoulder
field_82905_a[0] = new ModelRenderer(self, 0, 16);
field_82905_a[0].addBox(-10.0F, 3.9F, -0.5F, 20, 3, 3, ARMOR_INFLATE);
// [1] Upper body (spine + ribs things)
field_82905_a[1] = (new ModelRenderer(self)).setTextureSize(self.textureWidth, self.textureHeight);
field_82905_a[1].setRotationPoint(-2.0F, 6.9F, -0.5F);
field_82905_a[1].setTextureOffset(0, 22).addBox(0.0F, 0.0F, 0.0F, 3, 10, 3, ARMOR_INFLATE);
field_82905_a[1].setTextureOffset(24, 22).addBox(-4.0F, 1.5F, 0.5F, 11, 2, 2, ARMOR_INFLATE);
field_82905_a[1].setTextureOffset(24, 22).addBox(-4.0F, 4.0F, 0.5F, 11, 2, 2, ARMOR_INFLATE);
field_82905_a[1].setTextureOffset(24, 22).addBox(-4.0F, 6.5F, 0.5F, 11, 2, 2, ARMOR_INFLATE);
// [2] Tail
field_82905_a[2] = new ModelRenderer(self, 12, 22);
field_82905_a[2].addBox(0.0F, 0.0F, 0.0F, 3, 6, 3, ARMOR_INFLATE);
// [0] Center head
field_82904_b[0] = new ModelRenderer(self, 0, 0);
field_82904_b[0].addBox(-4.0F, -4.0F, -4.0F, 8, 8, 8, ARMOR_INFLATE);
// [1] Left head
field_82904_b[1] = new ModelRenderer(self, 32, 0);
field_82904_b[1].addBox(-4.0F, -4.0F, -4.0F, 6, 6, 6, ARMOR_INFLATE);
field_82904_b[1].rotationPointX = -8.0F;
field_82904_b[1].rotationPointY = 4.0F;
// [2] Right head
field_82904_b[2] = new ModelRenderer(self, 32, 0);
field_82904_b[2].addBox(-4.0F, -4.0F, -4.0F, 6, 6, 6, ARMOR_INFLATE);
field_82904_b[2].rotationPointX = 10.0F;
field_82904_b[2].rotationPointY = 4.0F;
}
}
@@ -0,0 +1,75 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.bugfixes;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.util.IIcon;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(RenderBlocks.class)
public abstract class MixinRenderBlocks_CrossedSquaresNormal {
@Shadow public IIcon overrideBlockTexture;
@Shadow public abstract boolean hasOverrideBlockTexture();
/**
* Vanilla sets a single (0, -1, 0) normal for all crossed-square vertices in renderBlockAsItem.
* This downward-pointing normal causes shader packs to compute incorrect shadow bias (pushing the
* shadow sample into the parent entity's body, e.g. mooshroom and anything that extends mooshrooms)
* and zero diffuse sun lighting.
*
* This mixin replaces the singular normal with per-quad face normals perpendicular to each diagonal plane.
* This gives each face correct directional lighting and shadow bias that pushes samples away from the face rather
* than into the entity body.
*/
@Redirect(
method = "renderBlockAsItem(Lnet/minecraft/block/Block;IF)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderBlocks;drawCrossedSquares(Lnet/minecraft/util/IIcon;DDDF)V")
)
private void angelica$drawCrossedSquaresWithPerFaceNormals(RenderBlocks instance, IIcon icon, double x, double y, double z, float scale) {
if (this.hasOverrideBlockTexture()) {
icon = this.overrideBlockTexture;
}
final Tessellator tess = Tessellator.instance;
final double minU = icon.getMinU();
final double minV = icon.getMinV();
final double maxU = icon.getMaxU();
final double maxV = icon.getMaxV();
final double d7 = 0.45D * scale;
final double x0 = x + 0.5D - d7;
final double x1 = x + 0.5D + d7;
final double z0 = z + 0.5D - d7;
final double z1 = z + 0.5D + d7;
final double yTop = y + (double) scale;
final float inv_sqrt_2 = (float) (1.0 / Math.sqrt(2.0));
tess.setNormal(inv_sqrt_2, 0.0F, -inv_sqrt_2);
tess.addVertexWithUV(x0, yTop, z0, minU, minV);
tess.addVertexWithUV(x0, y, z0, minU, maxV);
tess.addVertexWithUV(x1, y, z1, maxU, maxV);
tess.addVertexWithUV(x1, yTop, z1, maxU, minV);
tess.setNormal(-inv_sqrt_2, 0.0F, inv_sqrt_2);
tess.addVertexWithUV(x1, yTop, z1, minU, minV);
tess.addVertexWithUV(x1, y, z1, minU, maxV);
tess.addVertexWithUV(x0, y, z0, maxU, maxV);
tess.addVertexWithUV(x0, yTop, z0, maxU, minV);
tess.setNormal(-inv_sqrt_2, 0.0F, -inv_sqrt_2);
tess.addVertexWithUV(x0, yTop, z1, minU, minV);
tess.addVertexWithUV(x0, y, z1, minU, maxV);
tess.addVertexWithUV(x1, y, z0, maxU, maxV);
tess.addVertexWithUV(x1, yTop, z0, maxU, minV);
tess.setNormal(inv_sqrt_2, 0.0F, inv_sqrt_2);
tess.addVertexWithUV(x1, yTop, z0, minU, minV);
tess.addVertexWithUV(x1, y, z0, minU, maxV);
tess.addVertexWithUV(x0, y, z1, maxU, maxV);
tess.addVertexWithUV(x0, yTop, z1, maxU, minV);
}
}
@@ -0,0 +1,58 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.bugfixes;
import com.gtnewhorizons.angelica.rendering.WitherArmorState;
import net.minecraft.client.model.ModelBase;
import net.minecraft.client.model.ModelWither;
import net.minecraft.client.renderer.entity.RenderWither;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Replaces the Wither's glScalef(1.1) armor approach with a per-box inflated model.
* This fixes the armor being visually off-center on off-center and rotating parts.
* Not exactly mod friendly but we can figure that out later.
*/
@Mixin(RenderWither.class)
public class MixinRenderWither_ArmorCentering {
@Unique
private ModelWither angelica$armorModel;
@Inject(method = "<init>", at = @At("TAIL"))
private void angelica$createArmorModel(CallbackInfo ci) {
WitherArmorState.pendingInflate = true;
angelica$armorModel = new ModelWither();
}
// Use the inflated armor model instead of mainModel.
@Redirect(
method = "shouldRenderPass(Lnet/minecraft/entity/boss/EntityWither;IF)I",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/renderer/entity/RenderWither;setRenderPassModel(Lnet/minecraft/client/model/ModelBase;)V")
)
private void angelica$useInflatedModel(RenderWither instance, ModelBase model) {
((RenderWither) (Object) this).setRenderPassModel(angelica$armorModel);
}
// No-op glTranslatef
@Redirect(
method = "shouldRenderPass(Lnet/minecraft/entity/boss/EntityWither;IF)I",
at = @At(value = "INVOKE",
target = "Lorg/lwjgl/opengl/GL11;glTranslatef(FFF)V", ordinal = 1, remap = false)
)
private void angelica$skipTranslate(float x, float y, float z) {
}
// No-op glScalef
@Redirect(
method = "shouldRenderPass(Lnet/minecraft/entity/boss/EntityWither;IF)I",
at = @At(value = "INVOKE",
target = "Lorg/lwjgl/opengl/GL11;glScalef(FFF)V", remap = false)
)
private void angelica$skipScale(float x, float y, float z) {
}
}
@@ -0,0 +1,181 @@
package com.gtnewhorizons.angelica.api;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class GLRedirectsTest {
@ParameterizedTest(name = "{0}.{1} -> {2}")
@CsvSource({
"GL11, glPushMatrix, glPushMatrix",
"GL11, glPopMatrix, glPopMatrix",
"GL11, glTranslatef, glTranslatef",
"GL11, glRotatef, glRotatef",
"GL11, glScalef, glScalef",
"GL11, glEnable, glEnable",
"GL11, glDisable, glDisable",
"GL11, glBegin, glBegin",
"GL11, glEnd, glEnd",
"GL11, glVertex3f, glVertex3f",
"GL11, glColor4f, glColor4f",
"GL11, glNormal3f, glNormal3f",
"GL11, glTexCoord2f, glTexCoord2f",
"GL11, glBindTexture, glBindTexture",
"GL11, glBlendFunc, glBlendFunc",
"GL11, glDepthMask, glDepthMask",
"GL11, glDepthFunc, glDepthFunc",
"GL11, glAlphaFunc, glAlphaFunc",
"GL11, glMatrixMode, glMatrixMode",
"GL11, glLoadIdentity, glLoadIdentity",
"GL11, glShadeModel, glShadeModel",
"GL11, glDrawArrays, glDrawArrays",
"GL11, glDrawElements, glDrawElements",
"GL11, glNewList, glNewList",
"GL11, glEndList, glEndList",
"GL11, glCallList, glCallList",
"GL11, glViewport, glViewport",
"GL12, glTexImage3D, glTexImage3D",
"GL13, glActiveTexture, glActiveTexture",
"GL13, glMultiTexCoord2f, glMultiTexCoord2f",
"GL14, glBlendFuncSeparate, tryBlendFuncSeparate",
"GL14, glBlendColor, glBlendColor",
"GL15, glGenBuffers, glGenBuffers",
"GL15, glBindBuffer, glBindBuffer",
"GL15, glBufferData, glBufferData",
"GL15, glDeleteBuffers, glDeleteBuffers",
"GL20, glUseProgram, glUseProgram",
"GL20, glCreateShader, glCreateShader",
"GL20, glShaderSource, glShaderSource",
"GL20, glLinkProgram, glLinkProgram",
"GL20, glUniform1f, glUniform1f",
"GL20, glUniform4f, glUniform4f",
"GL20, glGetUniformLocation, glGetUniformLocation",
"GL20, glVertexAttribPointer, glVertexAttribPointer",
"GL30, glGenFramebuffers, glGenFramebuffers",
"GL30, glBindFramebuffer, glBindFramebuffer",
"GL30, glGenVertexArrays, glGenVertexArrays",
"GL30, glBindVertexArray, glBindVertexArray",
"GL30, glGenerateMipmap, glGenerateMipmap",
"GL31, glDrawElementsInstanced, glDrawElementsInstanced",
"GL33, glGenSamplers, glGenSamplers",
"GL33, glBindSampler, glBindSampler",
"GL42, glTexStorage2D, glTexStorage2D",
"GL43, glDispatchCompute, glDispatchCompute",
"GL44, glBufferStorage, glBufferStorage",
})
void glVersionClassMethodRedirects(String className, String method, String expected) {
assertEquals(expected, GLRedirects.getTargetMethodName(className + "." + method));
}
@ParameterizedTest(name = "{0}.{1} -> {2}")
@CsvSource({
"GL11C, glDrawArrays, glDrawArrays",
"GL20C, glUseProgram, glUseProgram",
"GL30C, glBindFramebuffer, glBindFramebuffer",
"GL33C, glBindSampler, glBindSampler",
})
void cVariantsUseGLPrefixMap(String className, String method, String expected) {
assertEquals(expected, GLRedirects.getTargetMethodName(className + "." + method));
}
@ParameterizedTest(name = "{0}.{1} -> {2}")
@CsvSource({
"EXTBlendFuncSeparate, glBlendFuncSeparateEXT, tryBlendFuncSeparate",
"ARBMultitexture, glActiveTextureARB, glActiveTextureARB",
"ARBShaderObjects, glUseProgramObjectARB, glUseProgram",
"ARBShaderObjects, glCreateShaderObjectARB, glCreateShader",
"ARBShaderObjects, glCompileShaderARB, glCompileShader",
"ARBShaderObjects, glAttachObjectARB, glAttachShader",
"ARBShaderObjects, glUniform1fARB, glUniform1f",
"ARBShaderObjects, glUniformMatrix4ARB, glUniformMatrix4",
"ARBShaderObjects, glGetUniformLocationARB, glGetUniformLocation",
"ARBVertexArrayObject, glBindVertexArray, glBindVertexArray",
"ARBInstancedArrays, glVertexAttribDivisorARB, glVertexAttribDivisorARB",
"APPLEVertexArrayObject, glBindVertexArrayAPPLE, glBindVertexArray",
"UniversalVAO, bindVertexArray, glBindVertexArray",
"UniversalVAO, deleteVertexArrays, glDeleteVertexArrays",
"OpenGlHelper, glBlendFunc, tryBlendFuncSeparate",
"OpenGlHelper, setActiveTexture, setActiveTexture",
"OpenGlHelper, setLightmapTextureCoords, setLightmapTextureCoords",
"OpenGlHelper, isFramebufferEnabled, isFramebufferEnabled",
"GLU, gluPerspective, gluPerspective",
"GLU, gluLookAt, gluLookAt",
"GLU, gluOrtho2D, gluOrtho2D",
"Project, gluPerspective, gluPerspective",
"Project, gluPickMatrix, gluPickMatrix",
})
void namedClassMethodRedirects(String className, String method, String expected) {
assertEquals(expected, GLRedirects.getTargetMethodName(className + "." + method));
}
@ParameterizedTest(name = "{0} -> null")
@CsvSource({
"GL11.GL_TEXTURE_2D",
"GL11.GL_BLEND",
"GL20.GL_FRAGMENT_SHADER",
"Foo.bar",
"String.valueOf",
"ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB",
})
void nonRedirectedReturnsNull(String classAndMethod) {
assertNull(GLRedirects.getTargetMethodName(classAndMethod));
}
@Test
void nullDotReturnsNull() {
assertNull(GLRedirects.getTargetMethodName("noDotHere"));
}
@ParameterizedTest(name = "pattern matches {0}")
@CsvSource({
"GL11.glPushMatrix",
"GL20C.glUseProgram",
"GL44.glBufferStorage",
"ARBShaderObjects.glUseProgramObjectARB",
"OpenGlHelper.setActiveTexture",
"GLU.gluPerspective",
"UniversalVAO.bindVertexArray",
})
void patternMatchesRedirectedClasses(String input) {
assertTrue(GLRedirects.getMethodRedirectPattern().matcher(input).find(),
"Pattern should match: " + input);
}
@ParameterizedTest(name = "pattern does not match {0}")
@CsvSource({
"String.valueOf",
"Math.abs",
"Tessellator.instance",
"System.currentTimeMillis",
})
void patternDoesNotMatchUnrelatedClasses(String input) {
assertFalse(GLRedirects.getMethodRedirectPattern().matcher(input).find(),
"Pattern should not match: " + input);
}
@Test
void endToEndScriptTransformation() {
String script = "GL11.glPushMatrix();\nGL11.glTranslatef(1,2,3);\nGL11.glEnable(GL11.GL_BLEND);";
String result = GLRedirects.getMethodRedirectPattern().matcher(script).replaceAll(mr -> {
String target = GLRedirects.getTargetMethodName(mr.group(0));
return target != null ? "GLStateManager." + target : mr.group(0);
});
assertTrue(result.contains("GLStateManager.glPushMatrix()"), "glPushMatrix not redirected: " + result);
assertTrue(result.contains("GLStateManager.glTranslatef(1,2,3)"), "glTranslatef not redirected: " + result);
assertTrue(result.contains("GLStateManager.glEnable(GL11.GL_BLEND)"), "glEnable not redirected or GL_BLEND mangled: " + result);
}
@Test
void patternIsNotNull() {
assertNotNull(GLRedirects.getMethodRedirectPattern());
}
}