diff --git a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java index 99b12a07..094cfa99 100644 --- a/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java +++ b/src/main/java/org/embeddedt/modernfix/core/config/ModernFixEarlyConfig.java @@ -174,6 +174,7 @@ public class ModernFixEarlyConfig { .put("mixin.feature.blockentity_incorrect_thread", false) .put("mixin.perf.clear_mixin_classinfo", false) .put("mixin.perf.deduplicate_climate_parameters", false) + .put("mixin.perf.faster_capabilities.bytecode_analysis", false) .put("mixin.bugfix.packet_leak", false) .put("mixin.perf.deduplicate_location", false) .put("mixin.perf.dynamic_entity_renderers", false) diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java index 8a70b589..05dc3c4d 100644 --- a/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator.java @@ -2,13 +2,21 @@ package org.embeddedt.modernfix.forge.capability; import net.minecraftforge.common.capabilities.ICapabilityProvider; import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer; +import org.embeddedt.modernfix.forge.capability.analysis.CapabilityRef; import org.objectweb.asm.*; import org.objectweb.asm.commons.GeneratorAdapter; import org.objectweb.asm.commons.Method; +import java.lang.reflect.Modifier; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -23,6 +31,7 @@ import static org.objectweb.asm.Opcodes.*; * and performs direct dispatch instead of megamorphic virtual calls. */ public class CapabilityProviderDispatcherGenerator { + private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", ""); private static final ConcurrentHashMap>, MethodHandle> cache = new ConcurrentHashMap<>(); @@ -67,10 +76,27 @@ public class CapabilityProviderDispatcherGenerator { } private static MethodHandle generateClass(List> providerTypes) { - ModernFix.LOGGER.debug("Generating capability dispatcher for types: [{}]", providerTypes.stream().map(Class::getName).collect(Collectors.joining(", "))); try { - String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet(); - byte[] classBytes = generateClassBytes(className, providerTypes); + // Analyze each provider type + List analysisResults = new ArrayList<>(providerTypes.size()); + for (Class type : providerTypes) { + CapabilityAnalysisResult result = CapabilityAnalyzer.analyze(type); + analysisResults.add(result); + } + + int generatedClassId = classCounter.incrementAndGet(); + String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId; + + ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < providerTypes.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(providerTypes.get(i).getName()).append(" -> ").append(formatAnalysisResult(analysisResults.get(i))); + } + return sb; + }); + + byte[] classBytes = generateClassBytes(className, providerTypes, analysisResults); // Define the hidden class MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass( @@ -79,6 +105,12 @@ public class CapabilityProviderDispatcherGenerator { MethodHandles.Lookup.ClassOption.NESTMATE ); + if (!GENERATED_CLASSES_FOLDER.isBlank()) { + Path path = Paths.get(GENERATED_CLASSES_FOLDER, "generatedDispatcher" + generatedClassId + ".class"); + Files.createDirectories(path.getParent()); + Files.write(path, classBytes); + } + // Return a MethodHandle to the constructor // Constructor signature: (ICapabilityProvider[])V // The constructor is adapted to take an Object and return an ICapabilityProvider to match @@ -92,7 +124,7 @@ public class CapabilityProviderDispatcherGenerator { } } - private static byte[] generateClassBytes(String className, List> providerTypes) { + private static byte[] generateClassBytes(String className, List> providerTypes, List analysisResults) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); // Class declaration: implements ICapabilityProvider @@ -105,28 +137,36 @@ public class CapabilityProviderDispatcherGenerator { new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" } ); + // Compute field descriptors: use concrete type when possible for JIT devirtualization + String[] fieldDescs = new String[providerTypes.size()]; + for (int i = 0; i < providerTypes.size(); i++) { + Class type = providerTypes.get(i); + fieldDescs[i] = (!type.isHidden() && Modifier.isPublic(type.getModifiers())) + ? Type.getDescriptor(type) : ICAP_PROVIDER_DESC; + } + // Generate final fields for each provider for (int i = 0; i < providerTypes.size(); i++) { cw.visitField( ACC_PRIVATE | ACC_FINAL, "provider" + i, - ICAP_PROVIDER_DESC, + fieldDescs[i], null, null ).visitEnd(); } // Generate constructor - generateConstructor(cw, className, providerTypes.size()); + generateConstructor(cw, className, providerTypes.size(), fieldDescs); // Generate getCapability method with sided parameter - generateGetCapabilityMethod(cw, className, providerTypes.size()); + generateGetCapabilityMethod(cw, className, fieldDescs, analysisResults); cw.visitEnd(); return cw.toByteArray(); } - private static void generateConstructor(ClassWriter cw, String className, int providerCount) { + private static void generateConstructor(ClassWriter cw, String className, int providerCount, String[] fieldDescs) { Method constructor = Method.getMethod("void (net.minecraftforge.common.capabilities.ICapabilityProvider[])"); GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw); @@ -136,14 +176,18 @@ public class CapabilityProviderDispatcherGenerator { // Unpack array into final fields for (int i = 0; i < providerCount; i++) { + Type fieldType = Type.getType(fieldDescs[i]); mg.loadThis(); // this mg.loadArg(0); // array mg.push(i); // index mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i] + if (!fieldDescs[i].equals(ICAP_PROVIDER_DESC)) { + mg.checkCast(fieldType); + } mg.putField( Type.getObjectType(className.replace('.', '/')), "provider" + i, - Type.getType(ICAP_PROVIDER_DESC) + fieldType ); } @@ -151,7 +195,9 @@ public class CapabilityProviderDispatcherGenerator { mg.endMethod(); } - private static void generateGetCapabilityMethod(ClassWriter cw, String className, int providerCount) { + private static void generateGetCapabilityMethod(ClassWriter cw, String className, String[] fieldDescs, List analysisResults) { + int providerCount = fieldDescs.length; + // Method: LazyOptional getCapability(Capability, Direction) MethodVisitor mv = cw.visitMethod( ACC_PUBLIC, @@ -168,15 +214,45 @@ public class CapabilityProviderDispatcherGenerator { Label endLabel = new Label(); for (int i = 0; i < providerCount; i++) { + CapabilityAnalysisResult analysis = analysisResults.get(i); Label nextLabel = new Label(); + // AlwaysEmpty: skip code generation for this provider entirely + if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) { + continue; + } + + // KnownCapabilities: emit guard checks before dispatch + if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known + && known.capabilities().size() <= 5) { + if (known.capabilities().size() == 1) { + // Single cap: if (cap != KNOWN_CAP) goto nextProvider + CapabilityRef ref = known.capabilities().iterator().next(); + mv.visitVarInsn(ALOAD, 1); // cap parameter + mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + mv.visitJumpInsn(IF_ACMPNE, nextLabel); + } else { + // Multiple caps: check each, jump to callProvider on match + Label callProvider = new Label(); + for (CapabilityRef ref : known.capabilities()) { + mv.visitVarInsn(ALOAD, 1); // cap parameter + mv.visitFieldInsn(GETSTATIC, ref.owner(), ref.fieldName(), CAPABILITY_DESC); + mv.visitJumpInsn(IF_ACMPEQ, callProvider); + } + // No match, skip this provider + mv.visitJumpInsn(GOTO, nextLabel); + mv.visitLabel(callProvider); + } + } + // Indeterminate: no guard, fall through to dispatch + // LazyOptional result = this.providerN.getCapability(cap, side); mv.visitVarInsn(ALOAD, 0); // this mv.visitFieldInsn( GETFIELD, className.replace('.', '/'), "provider" + i, - ICAP_PROVIDER_DESC + fieldDescs[i] ); mv.visitVarInsn(ALOAD, 1); // cap parameter mv.visitVarInsn(ALOAD, 2); // side parameter @@ -211,9 +287,6 @@ public class CapabilityProviderDispatcherGenerator { mv.visitInsn(ARETURN); mv.visitLabel(nextLabel); - if (i < providerCount - 1) { - mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); - } } // If no provider returned a capability, return empty @@ -231,29 +304,16 @@ public class CapabilityProviderDispatcherGenerator { mv.visitEnd(); } - /** - * Creates an instance of the optimized dispatcher for the given providers. - */ - public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) { - try { - MethodHandle constructor = getOrGenerateConstructor(providers); - return (ICapabilityProvider) constructor.invoke(providers); - } catch (Throwable e) { - throw new RuntimeException("Failed to create capability dispatcher instance", e); + private static String formatAnalysisResult(CapabilityAnalysisResult result) { + if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) { + return "always empty (skipped)"; + } else if (result instanceof CapabilityAnalysisResult.KnownCapabilities known) { + return "known caps: " + known.capabilities().stream() + .map(ref -> ref.owner() + "#" + ref.fieldName()) + .collect(Collectors.joining(", ")); + } else if (result instanceof CapabilityAnalysisResult.Indeterminate ind) { + return "indeterminate (" + ind.reason() + ")"; } - } - - /** - * Clears the cache of generated classes. Use with caution. - */ - public static void clearCache() { - cache.clear(); - } - - /** - * Returns the number of cached dispatcher classes. - */ - public static int getCacheSize() { - return cache.size(); + return result.toString(); } } \ No newline at end of file diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java new file mode 100644 index 00000000..4795a410 --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalysisResult.java @@ -0,0 +1,23 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +import java.util.Set; + +/** + * Result of analyzing a capability provider's {@code getCapability} bytecode. + */ +public sealed interface CapabilityAnalysisResult { + /** + * The provider can only return non-empty for these specific capabilities. + */ + record KnownCapabilities(Set capabilities) implements CapabilityAnalysisResult {} + + /** + * The provider always returns {@code LazyOptional.empty()}. + */ + record AlwaysEmpty() implements CapabilityAnalysisResult {} + + /** + * Analysis could not determine the set of capabilities; fall back to unguarded dispatch. + */ + record Indeterminate(String reason) implements CapabilityAnalysisResult {} +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java new file mode 100644 index 00000000..bcd8e1ae --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityAnalyzer.java @@ -0,0 +1,392 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +import net.minecraft.core.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.core.ModernFixMixinPlugin; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceValue; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Analyzes {@code getCapability} bytecode to determine which capabilities a provider handles. + */ +public class CapabilityAnalyzer { + + private static final ConcurrentHashMap, CapabilityAnalysisResult> cache = new ConcurrentHashMap<>(); + + private static final String CAPABILITY_INTERNAL = "net/minecraftforge/common/capabilities/Capability"; + private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;"; + private static final String LAZY_OPTIONAL_INTERNAL = "net/minecraftforge/common/util/LazyOptional"; + private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;"; + private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;"; + private static final String GET_CAPABILITY_DESC = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC; + private static final String ICAP_PROVIDER_INTERNAL = "net/minecraftforge/common/capabilities/ICapabilityProvider"; + + public static CapabilityAnalysisResult analyze(Class clazz) { + if (!ModernFixMixinPlugin.instance.isOptionEnabled("perf.faster_capabilities.bytecode_analysis.CapabilityAnalyzer")) { + return new CapabilityAnalysisResult.Indeterminate("bytecode analysis disabled"); + } + CapabilityAnalysisResult result = cache.get(clazz); + if (result != null) return result; + result = doAnalyzeSafe(clazz); + CapabilityAnalysisResult existing = cache.putIfAbsent(clazz, result); + return existing != null ? existing : result; + } + + private static CapabilityAnalysisResult doAnalyzeSafe(Class clazz) { + try { + return doAnalyze(clazz); + } catch (Exception e) { + ModernFix.LOGGER.debug("Capability analysis failed for {}: {}", clazz.getName(), e.getMessage()); + return new CapabilityAnalysisResult.Indeterminate("analysis exception: " + e.getMessage()); + } + } + + private static CapabilityAnalysisResult doAnalyze(Class clazz) throws IOException, AnalyzerException { + // Find the class that actually declares getCapability via reflection + Class declaringClass; + try { + declaringClass = clazz.getMethod("getCapability", Capability.class, Direction.class).getDeclaringClass(); + } catch (NoSuchMethodException e) { + return new CapabilityAnalysisResult.AlwaysEmpty(); + } + + if (declaringClass.getName().replace('.', '/').equals(ICAP_PROVIDER_INTERNAL)) { + return new CapabilityAnalysisResult.AlwaysEmpty(); + } + + String declaringClassName = declaringClass.getName().replace('.', '/'); + ClassNode declaringClassNode = readClass(declaringClass); + if (declaringClassNode == null) { + return new CapabilityAnalysisResult.Indeterminate("cannot read bytecode for " + declaringClass.getName()); + } + + MethodNode getCapMethod = findGetCapabilityMethod(declaringClassNode); + if (getCapMethod == null) { + return new CapabilityAnalysisResult.Indeterminate("method not found in bytecode for " + declaringClass.getName()); + } + + // Run the source analysis + CapabilitySourceInterpreter interpreter = new CapabilitySourceInterpreter(); + Analyzer analyzer = new Analyzer<>(interpreter); + Frame[] frames = analyzer.analyze(declaringClassName, getCapMethod); + + // Build if-guard map: maps instruction indices to CapabilityRef for guarded regions + List guardRegions = findGuardRegions(getCapMethod, frames); + + // Classify each ARETURN + InsnList instructions = getCapMethod.instructions; + Set knownCaps = new HashSet<>(); + boolean hasIndeterminate = false; + String indeterminateReason = null; + + for (int i = 0; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + if (insn.getOpcode() != Opcodes.ARETURN) continue; + + Frame frame = frames[i]; + if (frame == null) continue; // dead code + + SourceValue topOfStack = frame.getStack(frame.getStackSize() - 1); + + ReturnClassification classification = classifyReturnSources( + topOfStack, interpreter, i, guardRegions, clazz, instructions); + + if (classification instanceof ReturnClassification.Known known) { + knownCaps.addAll(known.caps); + } else if (classification instanceof ReturnClassification.Unknown unknown) { + hasIndeterminate = true; + indeterminateReason = unknown.reason; + } + // Empty: skip + } + + if (hasIndeterminate) { + CapabilityAnalysisResult result = new CapabilityAnalysisResult.Indeterminate(indeterminateReason); + ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result); + return result; + } + + if (knownCaps.isEmpty()) { + ModernFix.LOGGER.debug("Capability analysis for {}: AlwaysEmpty", clazz.getName()); + return new CapabilityAnalysisResult.AlwaysEmpty(); + } + + CapabilityAnalysisResult result = new CapabilityAnalysisResult.KnownCapabilities(Set.copyOf(knownCaps)); + ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result); + return result; + } + + private static ReturnClassification classifyReturnSources( + SourceValue topOfStack, + CapabilitySourceInterpreter interpreter, + int returnIndex, + List guardRegions, + Class originalClass, + InsnList instructions) { + + Set caps = new HashSet<>(); + List unknownSources = new ArrayList<>(); + String unknownReason = null; + + for (AbstractInsnNode source : topOfStack.insns) { + ReturnClassification sourceClassification = classifySingleSource( + source, interpreter, originalClass); + + if (sourceClassification instanceof ReturnClassification.Unknown unknown) { + unknownSources.add(source); + unknownReason = unknown.reason; + } else if (sourceClassification instanceof ReturnClassification.Known known) { + caps.addAll(known.caps); + } + } + + // If any source is unknown, try the guard region fallback before giving up + if (!unknownSources.isEmpty()) { + // Check if the return itself is in a guard region + for (GuardRegion guard : guardRegions) { + if (returnIndex > guard.guardIndex && returnIndex < guard.targetIndex) { + return new ReturnClassification.Known(Set.of(guard.capabilityRef)); + } + } + // Also check if each unknown source instruction is inside a guard region. + // This handles ternary patterns where both branches merge into a single + // ARETURN after the guard, but the unknown value was produced inside it. + boolean allResolved = true; + for (AbstractInsnNode unknownSource : unknownSources) { + int sourceIndex = instructions.indexOf(unknownSource); + boolean resolved = false; + for (GuardRegion guard : guardRegions) { + if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) { + caps.add(guard.capabilityRef); + resolved = true; + break; + } + } + if (!resolved) { + allResolved = false; + break; + } + } + if (!allResolved) { + return new ReturnClassification.Unknown(unknownReason); + } + } + + if (caps.isEmpty()) { + return ReturnClassification.EMPTY; + } + return new ReturnClassification.Known(caps); + } + + private static ReturnClassification classifySingleSource( + AbstractInsnNode source, + CapabilitySourceInterpreter interpreter, + Class originalClass) { + + if (source instanceof MethodInsnNode methodInsn) { + // Case: Capability.orEmpty(...) + if (methodInsn.getOpcode() == Opcodes.INVOKEVIRTUAL + && methodInsn.owner.equals(CAPABILITY_INTERNAL) + && methodInsn.name.equals("orEmpty")) { + return classifyOrEmptyCall(methodInsn, interpreter); + } + + // Case: LazyOptional.empty() + if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC + && methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL) + && methodInsn.name.equals("empty")) { + return ReturnClassification.EMPTY; + } + + // Case: LazyOptional.of(null) + if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC + && methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL) + && methodInsn.name.equals("of")) { + List args = interpreter.getCallArguments(methodInsn); + if (!args.isEmpty()) { + SourceValue arg = args.get(0); + if (arg.insns.size() == 1 + && arg.insns.iterator().next().getOpcode() == Opcodes.ACONST_NULL) { + return ReturnClassification.EMPTY; + } + } + } + + // Case: super.getCapability(...) + if (methodInsn.getOpcode() == Opcodes.INVOKESPECIAL + && methodInsn.name.equals("getCapability") + && methodInsn.desc.equals(GET_CAPABILITY_DESC)) { + return classifySuperDelegation(originalClass); + } + } + + return new ReturnClassification.Unknown("unclassified source: " + source.getClass().getSimpleName() + + " opcode=" + source.getOpcode()); + } + + private static ReturnClassification classifyOrEmptyCall( + MethodInsnNode methodInsn, CapabilitySourceInterpreter interpreter) { + List args = interpreter.getCallArguments(methodInsn); + if (args.isEmpty()) { + return new ReturnClassification.Unknown("orEmpty call with no recorded arguments"); + } + + // arg 0 is the receiver (the Capability instance) + SourceValue receiver = args.get(0); + for (AbstractInsnNode recvSource : receiver.insns) { + if (recvSource instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETSTATIC + && fieldInsn.desc.equals(CAPABILITY_DESC)) { + return new ReturnClassification.Known( + Set.of(new CapabilityRef(fieldInsn.owner, fieldInsn.name))); + } + } + + return new ReturnClassification.Unknown("orEmpty receiver is not a static Capability field"); + } + + private static ReturnClassification classifySuperDelegation(Class originalClass) { + Class superClass = originalClass.getSuperclass(); + if (superClass == null || superClass == Object.class) { + return ReturnClassification.EMPTY; + } + + @SuppressWarnings("unchecked") + Class superProvider = + (Class) superClass; + CapabilityAnalysisResult superResult = analyze(superProvider); + if (superResult instanceof CapabilityAnalysisResult.KnownCapabilities known) { + return new ReturnClassification.Known(known.capabilities()); + } else if (superResult instanceof CapabilityAnalysisResult.AlwaysEmpty) { + return ReturnClassification.EMPTY; + } else if (superResult instanceof CapabilityAnalysisResult.Indeterminate ind) { + return new ReturnClassification.Unknown("super delegation: " + ind.reason()); + } + return ReturnClassification.EMPTY; + } + + private static List findGuardRegions(MethodNode method, Frame[] frames) { + List regions = new ArrayList<>(); + InsnList instructions = method.instructions; + + for (int i = 0; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + int opcode = insn.getOpcode(); + if (opcode != Opcodes.IF_ACMPEQ && opcode != Opcodes.IF_ACMPNE) continue; + + Frame frame = frames[i]; + if (frame == null || frame.getStackSize() < 2) continue; + + SourceValue val1 = frame.getStack(frame.getStackSize() - 2); + SourceValue val2 = frame.getStack(frame.getStackSize() - 1); + + // Check if one traces to ALOAD 1 (cap parameter) and other to GETSTATIC Capability + CapabilityRef capRef = findCapRef(val1); + if (capRef == null) capRef = findCapRef(val2); + boolean hasParamLoad = isCapParamLoad(val1) || isCapParamLoad(val2); + + if (capRef != null && hasParamLoad) { + JumpInsnNode jumpInsn = (JumpInsnNode) insn; + int targetIndex = instructions.indexOf(jumpInsn.label); + + if (opcode == Opcodes.IF_ACMPNE) { + // if (cap != KNOWN_CAP) goto target -> the code between insn and target is for KNOWN_CAP + regions.add(new GuardRegion(capRef, i, targetIndex)); + } else { + // IF_ACMPEQ: if (cap == KNOWN_CAP) goto target -> the target is the guarded code + // Find the end of the guarded region (next label or return) + // For simplicity, mark from target to the next unconditional branch or return + // TODO: that simplification may have edge cases + int endIndex = findGuardedRegionEnd(instructions, targetIndex); + regions.add(new GuardRegion(capRef, targetIndex, endIndex)); + } + } + } + + return regions; + } + + private static int findGuardedRegionEnd(InsnList instructions, int startIndex) { + for (int i = startIndex; i < instructions.size(); i++) { + AbstractInsnNode insn = instructions.get(i); + int opcode = insn.getOpcode(); + if (opcode == Opcodes.GOTO || opcode == Opcodes.ARETURN + || opcode == Opcodes.RETURN || opcode == Opcodes.ATHROW) { + return i + 1; + } + } + return instructions.size(); + } + + private static CapabilityRef findCapRef(SourceValue sv) { + CapabilityRef ref = null; + for (AbstractInsnNode src : sv.insns) { + if (src instanceof FieldInsnNode fieldInsn + && fieldInsn.getOpcode() == Opcodes.GETSTATIC + && fieldInsn.desc.equals(CAPABILITY_DESC)) { + if (ref == null) { + ref = new CapabilityRef(fieldInsn.owner, fieldInsn.name); + } else if (!ref.owner().equals(fieldInsn.owner) || !ref.fieldName().equals(fieldInsn.name)) { + return null; // ambiguous: multiple different capability fields + } + } else { + return null; // non-capability source + } + } + return ref; + } + + private static boolean isCapParamLoad(SourceValue sv) { + if (sv.insns.isEmpty()) return false; + for (AbstractInsnNode src : sv.insns) { + if (!(src instanceof VarInsnNode varInsn) + || varInsn.getOpcode() != Opcodes.ALOAD + || varInsn.var != 1) { + return false; + } + } + return true; + } + + private static MethodNode findGetCapabilityMethod(ClassNode classNode) { + for (MethodNode method : classNode.methods) { + if (method.name.equals("getCapability") && method.desc.equals(GET_CAPABILITY_DESC)) { + return method; + } + } + return null; + } + + private static ClassNode readClass(Class clazz) throws IOException { + String resourcePath = "/" + clazz.getName().replace('.', '/') + ".class"; + try (InputStream is = clazz.getResourceAsStream(resourcePath)) { + if (is == null) return null; + ClassReader reader = new ClassReader(is); + ClassNode node = new ClassNode(); + reader.accept(node, 0); + return node; + } + } + + private sealed interface ReturnClassification { + ReturnClassification EMPTY = new Empty(); + + record Known(Set caps) implements ReturnClassification {} + record Empty() implements ReturnClassification {} + record Unknown(String reason) implements ReturnClassification {} + } + + private record GuardRegion(CapabilityRef capabilityRef, int guardIndex, int targetIndex) {} +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java new file mode 100644 index 00000000..d6da747d --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilityRef.java @@ -0,0 +1,14 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +/** + * Identifies a capability by the static field that holds it. + * + * @param owner internal class name (e.g. {@code net/minecraftforge/common/capabilities/ForgeCapabilities}) + * @param fieldName field name (e.g. {@code ITEM_HANDLER}) + */ +public record CapabilityRef(String owner, String fieldName) { + @Override + public String toString() { + return owner + "." + fieldName; + } +} diff --git a/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java new file mode 100644 index 00000000..a2df3d2e --- /dev/null +++ b/src/main/java/org/embeddedt/modernfix/forge/capability/analysis/CapabilitySourceInterpreter.java @@ -0,0 +1,49 @@ +package org.embeddedt.modernfix.forge.capability.analysis; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; + +import java.util.*; + +/** + * Extends {@link SourceInterpreter} with two enhancements: + *
    + *
  • Propagates source values through copy operations (ALOAD/ASTORE), so that + * {@code result = cap.orEmpty(...); return result;} traces back to the INVOKEVIRTUAL.
  • + *
  • Records argument {@link SourceValue}s for each call instruction, so we can later + * inspect the receiver/arguments of {@code orEmpty()} calls.
  • + *
+ */ +public class CapabilitySourceInterpreter extends SourceInterpreter { + + private final Map> callArguments = new HashMap<>(); + + public CapabilitySourceInterpreter() { + super(ASM9); + } + + /** + * Returns the recorded argument SourceValues for a given call instruction. + */ + public List getCallArguments(AbstractInsnNode insn) { + return callArguments.getOrDefault(insn, Collections.emptyList()); + } + + @Override + public SourceValue copyOperation(AbstractInsnNode insn, SourceValue value) { + // Propagate sources through loads/stores instead of creating a new source. + // However, if the value has no sources (e.g. a method parameter), fall back to + // the default behavior so the ALOAD instruction itself is recorded as the source. + if (value.insns.isEmpty()) { + return super.copyOperation(insn, value); + } + return value; + } + + @Override + public SourceValue naryOperation(AbstractInsnNode insn, List values) { + callArguments.put(insn, new ArrayList<>(values)); + return super.naryOperation(insn, values); + } +}