Implement facility to process specific JEI plugins on the main thread

This commit is contained in:
embeddedt 2023-01-06 10:42:48 -05:00
parent da0ee80505
commit f43d54eafd
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
10 changed files with 285 additions and 183 deletions

View File

@ -126,11 +126,6 @@ repositories {
name = "Progwml6 maven"
url = "https://dvs1.progwml6.com/files/maven/"
}
maven {
// location of a maven mirror for JEI files, as a fallback
name = "ModMaven"
url = "https://modmaven.dev"
}
}
dependencies {

View File

@ -16,6 +16,7 @@ import net.minecraftforge.fml.ExtensionPoint;
import net.minecraftforge.fml.InterModComms;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.event.lifecycle.InterModEnqueueEvent;
@ -26,16 +27,19 @@ import net.minecraftforge.fml.network.FMLNetworkConstants;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.core.config.ModernFixConfig;
import java.util.stream.Collectors;
// The value here should match an entry in the META-INF/mods.toml file
@Mod("modernfix")
@Mod(ModernFix.MODID)
public class ModernFix {
// Directly reference a log4j logger.
public static final Logger LOGGER = LogManager.getLogger("ModernFix");
public static final String MODID = "modernfix";
public static ModernFix INSTANCE;
// Used to skip computing the blockstate caches twice
@ -48,5 +52,6 @@ public class ModernFix {
MinecraftForge.EVENT_BUS.register(this);
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> MinecraftForge.EVENT_BUS.register(new ModernFixClient()));
ModLoadingContext.get().registerExtensionPoint(ExtensionPoint.DISPLAYTEST, () -> Pair.of(() -> FMLNetworkConstants.IGNORESERVERONLY, (a, b) -> true));
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ModernFixConfig.COMMON_CONFIG);
}
}

View File

@ -2,7 +2,7 @@ package org.embeddedt.modernfix.core;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.core.config.ModernFixConfig;
import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig;
import org.embeddedt.modernfix.core.config.Option;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
@ -16,12 +16,12 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
private static final String MIXIN_PACKAGE_ROOT = "org.embeddedt.modernfix.mixin.";
private final Logger logger = LogManager.getLogger("ModernFix");
public static ModernFixConfig config = null;
public static ModernFixEarlyConfig config = null;
@Override
public void onLoad(String mixinPackage) {
try {
config = ModernFixConfig.load(new File("./config/modernfix-mixins.properties"));
config = ModernFixEarlyConfig.load(new File("./config/modernfix-mixins.properties"));
} catch (Exception e) {
throw new RuntimeException("Could not load configuration file for ModernFix", e);
}

View File

@ -1,179 +1,52 @@
package org.embeddedt.modernfix.core.config;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.ForgeConfigSpec;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.config.ModConfig;
import org.embeddedt.modernfix.ModernFix;
import net.minecraftforge.fml.loading.FMLLoader;
import java.io.*;
import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Mod.EventBusSubscriber(modid = ModernFix.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModernFixConfig {
private static final Logger LOGGER = LogManager.getLogger("ModernFixConfig");
private static final ForgeConfigSpec.Builder COMMON_BUILDER = new ForgeConfigSpec.Builder();
public static ForgeConfigSpec COMMON_CONFIG;
private final Map<String, Option> options = new HashMap<>();
public static ForgeConfigSpec.ConfigValue<List<? extends String>> BLACKLIST_ASYNC_JEI_PLUGINS;
private ModernFixConfig() {
// Defines the default rules which can be configured by the user or other mods.
// You must manually add a rule for any new mixins not covered by an existing package rule.
this.addMixinRule("core", true); // TODO: Don't actually allow the user to disable this
this.addMixinRule("feature.measure_time", true);
this.addMixinRule("perf.remove_biome_temperature_cache", true);
this.addMixinRule("perf.resourcepacks", true);
this.addMixinRule("perf.reduce_blockstate_cache_rebuilds", true);
this.addMixinRule("perf.boost_worker_count", true);
this.addMixinRule("perf.skip_first_datapack_reload", true);
this.addMixinRule("perf.parallelize_model_loading", true);
this.addMixinRule("perf.trim_model_caches", true);
this.addMixinRule("bugfix.concurrency", true);
this.addMixinRule("bugfix.edge_chunk_not_saved", true);
this.addMixinRule("perf.async_jei", true);
public static Set<ResourceLocation> jeiPluginBlacklist;
static {
List<? extends String> empty = Collections.emptyList();
Predicate<Object> locationValidator = o -> o instanceof String && ((String)o).contains(":");
BLACKLIST_ASYNC_JEI_PLUGINS = COMMON_BUILDER
.comment("These JEI plugins will be loaded on the main thread")
.defineList("blacklist_async_jei_plugins", ImmutableList.of(
"jepb:jei_plugin"
), locationValidator);
}
/**
* Defines a Mixin rule which can be configured by users and other mods.
* @throws IllegalStateException If a rule with that name already exists
* @param mixin The name of the mixin package which will be controlled by this rule
* @param enabled True if the rule will be enabled by default, otherwise false
*/
private void addMixinRule(String mixin, boolean enabled) {
String name = getMixinRuleName(mixin);
static {
COMMON_CONFIG = COMMON_BUILDER.build();
}
if (this.options.putIfAbsent(name, new Option(name, enabled, false)) != null) {
throw new IllegalStateException("Mixin rule already defined: " + mixin);
@SubscribeEvent
public static void onModConfigEvent(final ModConfig.ModConfigEvent configEvent) {
if (configEvent.getConfig().getSpec() == COMMON_CONFIG) {
bakeConfig();
}
}
private void readProperties(Properties props) {
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
Option option = this.options.get(key);
if (option == null) {
LOGGER.warn("No configuration key exists with name '{}', ignoring", key);
continue;
}
boolean enabled;
if (value.equalsIgnoreCase("true")) {
enabled = true;
} else if (value.equalsIgnoreCase("false")) {
enabled = false;
} else {
LOGGER.warn("Invalid value '{}' encountered for configuration key '{}', ignoring", value, key);
continue;
}
option.setEnabled(enabled, true);
}
public static void bakeConfig() {
jeiPluginBlacklist = BLACKLIST_ASYNC_JEI_PLUGINS.get().stream().map(ResourceLocation::new).collect(Collectors.toSet());
}
/**
* Returns the effective option for the specified class name. This traverses the package path of the given mixin
* and checks each root for configuration rules. If a configuration rule disables a package, all mixins located in
* that package and its children will be disabled. The effective option is that of the highest-priority rule, either
* a enable rule at the end of the chain or a disable rule at the earliest point in the chain.
*
* @return Null if no options matched the given mixin name, otherwise the effective option for this Mixin
*/
public Option getEffectiveOptionForMixin(String mixinClassName) {
int lastSplit = 0;
int nextSplit;
Option rule = null;
while ((nextSplit = mixinClassName.indexOf('.', lastSplit)) != -1) {
String key = getMixinRuleName(mixinClassName.substring(0, nextSplit));
Option candidate = this.options.get(key);
if (candidate != null) {
rule = candidate;
if (!rule.isEnabled()) {
return rule;
}
}
lastSplit = nextSplit + 1;
}
return rule;
}
/**
* Loads the configuration file from the specified location. If it does not exist, a new configuration file will be
* created. The file on disk will then be updated to include any new options.
*/
public static ModernFixConfig load(File file) {
ModernFixConfig config = new ModernFixConfig();
Properties props = new Properties();
if(file.exists()) {
try (FileInputStream fin = new FileInputStream(file)){
props.load(fin);
} catch (IOException e) {
throw new RuntimeException("Could not load config file", e);
}
config.readProperties(props);
}
try {
config.writeConfig(file, props);
} catch (IOException e) {
LOGGER.warn("Could not write configuration file", e);
}
return config;
}
private void writeConfig(File file, Properties props) throws IOException {
File dir = file.getParentFile();
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new IOException("Could not create parent directories");
}
} else if (!dir.isDirectory()) {
throw new IOException("The parent file is not a directory");
}
try (Writer writer = new FileWriter(file)) {
writer.write("# This is the configuration file for ModernFix.\n");
writer.write("#\n");
writer.write("# The following options are enabled by default and should only be disabled if there is a.\n");
writer.write("# compatibility issue. Add a line mixin.example_name=false without the # sign to disable a rule.\n");
List<String> lines = this.options.keySet().stream()
.filter(key -> !key.equals("mixin.core"))
.sorted()
.map(key -> "# " + key + "\n")
.collect(Collectors.toList());
for(String line : lines) {
writer.write(line);
}
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
writer.write(key + "=" + value + "\n");
}
}
}
private static String getMixinRuleName(String name) {
return "mixin." + name;
}
public int getOptionCount() {
return this.options.size();
}
public int getOptionOverrideCount() {
return (int) this.options.values()
.stream()
.filter(Option::isOverridden)
.count();
}
}

View File

@ -0,0 +1,177 @@
package org.embeddedt.modernfix.core.config;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
public class ModernFixEarlyConfig {
private static final Logger LOGGER = LogManager.getLogger("ModernFixConfig");
private final Map<String, Option> options = new HashMap<>();
private ModernFixEarlyConfig() {
// Defines the default rules which can be configured by the user or other mods.
// You must manually add a rule for any new mixins not covered by an existing package rule.
this.addMixinRule("core", true); // TODO: Don't actually allow the user to disable this
this.addMixinRule("feature.measure_time", true);
this.addMixinRule("perf.remove_biome_temperature_cache", true);
this.addMixinRule("perf.resourcepacks", true);
this.addMixinRule("perf.reduce_blockstate_cache_rebuilds", true);
this.addMixinRule("perf.boost_worker_count", true);
this.addMixinRule("perf.skip_first_datapack_reload", true);
this.addMixinRule("perf.parallelize_model_loading", true);
this.addMixinRule("perf.trim_model_caches", true);
this.addMixinRule("bugfix.concurrency", true);
this.addMixinRule("bugfix.edge_chunk_not_saved", true);
this.addMixinRule("perf.async_jei", true);
}
/**
* Defines a Mixin rule which can be configured by users and other mods.
* @throws IllegalStateException If a rule with that name already exists
* @param mixin The name of the mixin package which will be controlled by this rule
* @param enabled True if the rule will be enabled by default, otherwise false
*/
private void addMixinRule(String mixin, boolean enabled) {
String name = getMixinRuleName(mixin);
if (this.options.putIfAbsent(name, new Option(name, enabled, false)) != null) {
throw new IllegalStateException("Mixin rule already defined: " + mixin);
}
}
private void readProperties(Properties props) {
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
Option option = this.options.get(key);
if (option == null) {
LOGGER.warn("No configuration key exists with name '{}', ignoring", key);
continue;
}
boolean enabled;
if (value.equalsIgnoreCase("true")) {
enabled = true;
} else if (value.equalsIgnoreCase("false")) {
enabled = false;
} else {
LOGGER.warn("Invalid value '{}' encountered for configuration key '{}', ignoring", value, key);
continue;
}
option.setEnabled(enabled, true);
}
}
/**
* Returns the effective option for the specified class name. This traverses the package path of the given mixin
* and checks each root for configuration rules. If a configuration rule disables a package, all mixins located in
* that package and its children will be disabled. The effective option is that of the highest-priority rule, either
* a enable rule at the end of the chain or a disable rule at the earliest point in the chain.
*
* @return Null if no options matched the given mixin name, otherwise the effective option for this Mixin
*/
public Option getEffectiveOptionForMixin(String mixinClassName) {
int lastSplit = 0;
int nextSplit;
Option rule = null;
while ((nextSplit = mixinClassName.indexOf('.', lastSplit)) != -1) {
String key = getMixinRuleName(mixinClassName.substring(0, nextSplit));
Option candidate = this.options.get(key);
if (candidate != null) {
rule = candidate;
if (!rule.isEnabled()) {
return rule;
}
}
lastSplit = nextSplit + 1;
}
return rule;
}
/**
* Loads the configuration file from the specified location. If it does not exist, a new configuration file will be
* created. The file on disk will then be updated to include any new options.
*/
public static ModernFixEarlyConfig load(File file) {
ModernFixEarlyConfig config = new ModernFixEarlyConfig();
Properties props = new Properties();
if(file.exists()) {
try (FileInputStream fin = new FileInputStream(file)){
props.load(fin);
} catch (IOException e) {
throw new RuntimeException("Could not load config file", e);
}
config.readProperties(props);
}
try {
config.writeConfig(file, props);
} catch (IOException e) {
LOGGER.warn("Could not write configuration file", e);
}
return config;
}
private void writeConfig(File file, Properties props) throws IOException {
File dir = file.getParentFile();
if (!dir.exists()) {
if (!dir.mkdirs()) {
throw new IOException("Could not create parent directories");
}
} else if (!dir.isDirectory()) {
throw new IOException("The parent file is not a directory");
}
try (Writer writer = new FileWriter(file)) {
writer.write("# This is the configuration file for ModernFix.\n");
writer.write("#\n");
writer.write("# The following options are enabled by default and should only be disabled if there is a.\n");
writer.write("# compatibility issue. Add a line mixin.example_name=false without the # sign to disable a rule.\n");
List<String> lines = this.options.keySet().stream()
.filter(key -> !key.equals("mixin.core"))
.sorted()
.map(key -> "# " + key + "\n")
.collect(Collectors.toList());
for(String line : lines) {
writer.write(line);
}
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String key = (String) entry.getKey();
String value = (String) entry.getValue();
writer.write(key + "=" + value + "\n");
}
}
}
private static String getMixinRuleName(String name) {
return "mixin." + name;
}
public int getOptionCount() {
return this.options.size();
}
public int getOptionOverrideCount() {
return (int) this.options.values()
.stream()
.filter(Option::isOverridden)
.count();
}
}

View File

@ -0,0 +1,34 @@
package org.embeddedt.modernfix.mixin.bugfix.concurrency;
import net.minecraft.client.Minecraft;
import net.minecraft.util.concurrent.ThreadTaskExecutor;
import org.embeddedt.modernfix.ModernFix;
import org.spongepowered.asm.mixin.Mixin;
import java.util.function.BooleanSupplier;
@Mixin(Minecraft.class)
public abstract class MinecraftMixin<R extends Runnable> extends ThreadTaskExecutor<R> {
protected MinecraftMixin(String p_i50403_1_) {
super(p_i50403_1_);
}
@Override
public void managedBlock(BooleanSupplier pIsDone) {
if(!this.isSameThread()) {
ModernFix.LOGGER.warn("A mod is calling Minecraft.managedBlock from the wrong thread. This is most likely related to one of our parallelizations.");
ModernFix.LOGGER.warn("ModernFix will work around this, however ideally the issue should be patched in the other mod.");
ModernFix.LOGGER.warn("Stacktrace", new IllegalThreadStateException());
while(!pIsDone.getAsBoolean()) {
try {
Thread.sleep(100);
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} else {
super.managedBlock(pIsDone);
}
}
}

View File

@ -27,17 +27,17 @@ import java.util.List;
@Mixin(ClientLifecycleHandler.class)
public class ClientLifecycleHandlerMixin {
@Shadow @Final private JeiStarter starter;
@Shadow @Final private List<IModPlugin> plugins;
@Shadow @Final private Textures textures;
@Shadow @Final private IClientConfig clientConfig;
@Shadow @Final private IEditModeConfig editModeConfig;
@Shadow @Final private IngredientFilterConfig ingredientFilterConfig;
@Shadow @Final private WorldConfig worldConfig;
@Shadow @Final private BookmarkConfig bookmarkConfig;
@Shadow @Final private IModIdHelper modIdHelper;
@Shadow @Final private RecipeCategorySortingConfig recipeCategorySortingConfig;
@Shadow @Final private IIngredientSorter ingredientSorter;
@Shadow(remap = false) @Final private JeiStarter starter;
@Shadow(remap = false) @Final private List<IModPlugin> plugins;
@Shadow(remap = false) @Final private Textures textures;
@Shadow(remap = false) @Final private IClientConfig clientConfig;
@Shadow(remap = false) @Final private IEditModeConfig editModeConfig;
@Shadow(remap = false) @Final private IngredientFilterConfig ingredientFilterConfig;
@Shadow(remap = false) @Final private WorldConfig worldConfig;
@Shadow(remap = false) @Final private BookmarkConfig bookmarkConfig;
@Shadow(remap = false) @Final private IModIdHelper modIdHelper;
@Shadow(remap = false) @Final private RecipeCategorySortingConfig recipeCategorySortingConfig;
@Shadow(remap = false) @Final private IIngredientSorter ingredientSorter;
private volatile Thread reloadThread = null;
@Inject(method = "setupJEI", at = @At(value = "INVOKE", target = "Lmezz/jei/startup/ClientLifecycleHandler;startJEI()V"), cancellable = true, remap = false)
private void startAsync(CallbackInfo ci) {

View File

@ -26,7 +26,7 @@ import java.util.function.Consumer;
@Mixin(JeiStarter.class)
public class JeiStarterMixin {
@Shadow private boolean started;
@Shadow(remap = false) private boolean started;
@Inject(method = "start", at = @At(value = "INVOKE", target = "Lmezz/jei/util/ErrorUtil;checkNotEmpty(Ljava/util/Collection;Ljava/lang/String;)V", ordinal = 0, shift = At.Shift.AFTER), remap = false)
private void setStartedFlag(List<IModPlugin> plugins, Textures textures, IClientConfig clientConfig, IEditModeConfig editModeConfig, IIngredientFilterConfig ingredientFilterConfig, IWorldConfig worldConfig, BookmarkConfig bookmarkConfig, IModIdHelper modIdHelper, RecipeCategorySortingConfig recipeCategorySortingConfig, IIngredientSorter ingredientSorter, CallbackInfo ci) {

View File

@ -2,6 +2,9 @@ package org.embeddedt.modernfix.mixin.perf.async_jei;
import mezz.jei.api.IModPlugin;
import mezz.jei.load.PluginCaller;
import net.minecraft.client.Minecraft;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.core.config.ModernFixConfig;
import org.embeddedt.modernfix.jei.async.IAsyncJeiStarter;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -10,6 +13,8 @@ import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
@Mixin(PluginCaller.class)
@ -18,4 +23,16 @@ public class PluginCallerMixin {
private static void checkForInterrupt(String title, List<IModPlugin> plugins, Consumer<IModPlugin> func, CallbackInfo ci) {
IAsyncJeiStarter.checkForLoadInterruption();
}
@SuppressWarnings({"unchecked","rawtypes"})
@Redirect(method = "callOnPlugins", at = @At(value = "INVOKE", target = "Ljava/util/function/Consumer;accept(Ljava/lang/Object;)V"), remap = false)
private static void runOnMainThreadIfNeeded(Consumer instance, Object pluginObj) {
IModPlugin plugin = (IModPlugin)pluginObj;
if(ModernFixConfig.jeiPluginBlacklist.contains(plugin.getPluginUid())) {
ModernFix.LOGGER.warn("Going to main thread for " + plugin.getPluginUid());
Minecraft.getInstance().executeBlocking(() -> instance.accept(plugin));
} else {
instance.accept(plugin);
}
}
}

View File

@ -21,6 +21,7 @@
"client": [
"perf.skip_first_datapack_reload.MinecraftMixin",
"bugfix.concurrency.RenderTypeMixin",
"bugfix.concurrency.MinecraftMixin",
"perf.parallelize_model_loading.ModelBakeryMixin",
"perf.trim_model_caches.ModelManagerMixin",
"perf.async_jei.IngredientListElementFactoryMixin",