diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/MinecraftMixin_ModelTicking.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/MinecraftMixin_ModelTicking.java deleted file mode 100644 index 32b22087..00000000 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/MinecraftMixin_ModelTicking.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.model.ModelManager; -import org.embeddedt.modernfix.annotation.ClientOnlyMixin; -import org.embeddedt.modernfix.duck.IExtendedModelManager; -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; - -@Mixin(Minecraft.class) -@ClientOnlyMixin -public abstract class MinecraftMixin_ModelTicking { - @Shadow public abstract ModelManager getModelManager(); - - @Inject(method = "tick", at = @At(value = "RETURN")) - private void tickModels(CallbackInfo ci) { - ((IExtendedModelManager)this.getModelManager()).mfix$tick(); - } -} diff --git a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java index 31a9c680..efbf715d 100644 --- a/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java +++ b/common/src/main/java/org/embeddedt/modernfix/common/mixin/perf/dynamic_resources/ModelManagerMixin.java @@ -1,23 +1,32 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources; import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.block.model.BlockModel; +import net.minecraft.client.resources.model.AtlasSet; import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import org.embeddedt.modernfix.annotation.ClientOnlyMixin; -import org.embeddedt.modernfix.duck.IExtendedModelManager; +import org.embeddedt.modernfix.dynamicresources.DynamicModelProvider; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.util.HashMap; import java.util.Map; @@ -26,9 +35,16 @@ import java.util.concurrent.Executor; @Mixin(ModelManager.class) @ClientOnlyMixin -public class ModelManagerMixin implements IExtendedModelManager { +public class ModelManagerMixin { @Shadow private Map bakedRegistry; + @Shadow private BakedModel missingModel; + @Unique + private DynamicModelProvider mfix$modelProvider; + + @Unique + private Map mfix$stitchResults; + @Inject(method = "", at = @At("RETURN")) private void injectDummyBakedRegistry(CallbackInfo ci) { if(this.bakedRegistry == null) { @@ -51,8 +67,31 @@ public class ModelManagerMixin implements IExtendedModelManager { return ImmutableList.of(); } - @Override - public void mfix$tick() { + @Inject(method = "loadModels", at = @At("HEAD")) + private void saveStitchResults(ProfilerFiller profilerFiller, Map map, ModelBakery modelBakery, Object2IntMap object2IntMap, CallbackInfoReturnable cir) { + this.mfix$stitchResults = map; + } + @Inject(method = "apply", at = @At("RETURN")) + private void createModelProvider(CallbackInfo ci) { + this.mfix$modelProvider = new DynamicModelProvider( + null, // TODO + this.missingModel, + Minecraft.getInstance().getResourceManager(), + this.mfix$stitchResults + ); + } + + /** + * @author embeddedt + * @reason use dynamic model system + */ + @Overwrite + public BakedModel getModel(ModelResourceLocation modelLocation) { + if(this.mfix$modelProvider != null) { + return this.mfix$modelProvider.getModel(modelLocation); + } else { + return this.missingModel; + } } } diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelCache.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelCache.java deleted file mode 100644 index a2c90a9f..00000000 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelCache.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.embeddedt.modernfix.dynamicresources; - -import it.unimi.dsi.fastutil.Function; -import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap; -import net.minecraft.client.resources.model.BakedModel; - -import java.util.concurrent.locks.StampedLock; - -/** - * The Mojang Triple-based baked cache system is too slow to be hitting on every model retrieval, so - * we need a fast, concurrency-safe wrapper on top. - */ -public class DynamicModelCache { - private final Reference2ReferenceLinkedOpenHashMap cache = new Reference2ReferenceLinkedOpenHashMap<>(); - private final StampedLock lock = new StampedLock(); - private final Function modelRetriever; - private final boolean allowNulls; - - public DynamicModelCache(Function modelRetriever, boolean allowNulls) { - this.modelRetriever = modelRetriever; - this.allowNulls = allowNulls; - } - - public void clear() { - long stamp = lock.writeLock(); - try { - cache.clear(); - } finally { - lock.unlock(stamp); - } - } - - private boolean needToPopulate(K state) { - long stamp = lock.readLock(); - try { - return !cache.containsKey(state); - } finally { - lock.unlock(stamp); - } - } - - private BakedModel getModelFromCache(K state) { - long stamp = lock.readLock(); - try { - return cache.get(state); - } finally { - lock.unlock(stamp); - } - } - - private BakedModel cacheModel(K state) { - BakedModel model = modelRetriever.apply(state); - - // Lock and modify our local, faster cache - long stamp = lock.writeLock(); - - try { - cache.putAndMoveToFirst(state, model); - // TODO: choose less arbitrary number - if(cache.size() >= 1000) { - cache.removeLast(); - } - } finally { - lock.unlock(stamp); - } - - return model; - } - - public BakedModel get(K key) { - BakedModel model = getModelFromCache(key); - - if(model == null && (!allowNulls || needToPopulate(key))) { - model = cacheModel(key); - } - - return model; - } -} diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java index 67bd354c..ad9f7b6f 100644 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java +++ b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/DynamicModelProvider.java @@ -1,41 +1,236 @@ package org.embeddedt.modernfix.dynamicresources; -import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.client.renderer.block.model.BlockModel; +import net.minecraft.client.renderer.block.model.BlockModelDefinition; +import net.minecraft.client.resources.model.AtlasSet; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.client.resources.model.MissingBlockModel; +import net.minecraft.client.resources.model.ModelBakery; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.client.resources.model.SpecialModels; import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import org.embeddedt.modernfix.ModernFix; +import org.embeddedt.modernfix.util.DynamicMap; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ExecutionException; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; /** * Handles loading models dynamically, rather than at startup time. */ public class DynamicModelProvider { - private final Map internalModels; - private final Cache> loadedModels = + private final LoadingCache> loadedStateDefinitions = CacheBuilder.newBuilder() .expireAfterAccess(3, TimeUnit.MINUTES) .maximumSize(1000) .concurrencyLevel(8) .softValues() - .build(); + .build(new CacheLoader<>() { + @Override + public Optional load(ResourceLocation key) { + return loadBlockStateDefinition(key); + } + }); - public DynamicModelProvider(Map initialModels) { - this.internalModels = initialModels; + private final LoadingCache> loadedBlockModels = + CacheBuilder.newBuilder() + .expireAfterAccess(3, TimeUnit.MINUTES) + .maximumSize(1000) + .concurrencyLevel(8) + .softValues() + .build(new CacheLoader<>() { + @Override + public Optional load(ResourceLocation key) { + return loadBlockModel(key); + } + }); + + private final LoadingCache> loadedUnbakedModels = + CacheBuilder.newBuilder() + .expireAfterAccess(3, TimeUnit.MINUTES) + .maximumSize(1000) + .concurrencyLevel(8) + .softValues() + .build(new CacheLoader<>() { + @Override + public Optional load(ModelResourceLocation key) { + return loadModel(key); + } + }); + + private final LoadingCache> loadedBakedModels = + CacheBuilder.newBuilder() + .expireAfterAccess(3, TimeUnit.MINUTES) + .maximumSize(1000) + .concurrencyLevel(8) + .softValues() + .build(new CacheLoader<>() { + @Override + public Optional load(ModelResourceLocation key) { + return loadBakedModel(key); + } + }); + + private final Map initialBakedRegistry; + private final BakedModel missingModel; + private final UnbakedModel unbakedMissingModel; + private final Function> stateMapper; + private final ResourceManager resourceManager; + private final BlockStateModelLoader blockStateModelLoader; + private final ModelBakery.TextureGetter textureGetter; + private final DynamicMap fakeUnbakedModelMap; + private final DynamicResolver resolver; + + public DynamicModelProvider(Map initialBakedRegistry, BakedModel missingModel, ResourceManager resourceManager, Map atlasMap) { + this.initialBakedRegistry = initialBakedRegistry; + this.missingModel = missingModel; + this.textureGetter = (mrl, material) -> { + var atlas = atlasMap.get(material.atlasLocation()); + var sprite = atlas.getSprite(material.texture()); + if (sprite != null) { + return sprite; + } else { + ModernFix.LOGGER.warn("Unable to find sprite '{}' referenced by model '{}'", material.texture(), mrl); + return atlas.missing(); + } + }; + this.stateMapper = BlockStateModelLoader.definitionLocationToBlockMapper(); + this.resourceManager = resourceManager; + this.unbakedMissingModel = MissingBlockModel.missingModel(); + this.blockStateModelLoader = new BlockStateModelLoader(this.unbakedMissingModel); + this.fakeUnbakedModelMap = new DynamicMap<>(ResourceLocation.class, key -> this.loadedBlockModels.getUnchecked(key).orElse(null)); + this.resolver = new DynamicResolver(); } - public UnbakedModel getModel(ResourceLocation location) { - try { - return loadedModels.get(location, () -> Optional.ofNullable(loadModel(location))).orElse(null); - } catch(ExecutionException e) { - throw new RuntimeException(e.getCause()); + private Optional loadBlockStateDefinition(ResourceLocation location) { + StateDefinition stateDefinition = this.stateMapper.apply(location); + if(stateDefinition == null) { + return Optional.empty(); + } + List resources = resourceManager.getResourceStack(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "blockstates/" + location.getPath() + ".json")); + List loadedDefinitions = new ArrayList<>(resources.size()); + for(Resource resource : resources) { + try(Reader reader = resource.openAsReader()) { + JsonObject jsonObject = GsonHelper.parse(reader); + BlockModelDefinition blockModelDefinition = BlockModelDefinition.fromJsonElement(jsonObject); + loadedDefinitions.add(new BlockStateModelLoader.LoadedBlockModelDefinition(resource.sourcePackId(), blockModelDefinition)); + } catch(Exception e) { + ModernFix.LOGGER.error("Failed to load blockstate definition {} from pack '{}'", location, resource.sourcePackId(), e); + } + } + return Optional.of(this.blockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions)); + } + + private BakedModel bakeModel(UnbakedModel model, ModelResourceLocation location) { + synchronized (this) { + this.resolver.clearResolver(); + model.resolveDependencies(this.resolver); + var modelBakery = new ModelBakery(Map.of(location, model), this.fakeUnbakedModelMap, this.unbakedMissingModel); + modelBakery.bakeModels(this.textureGetter); + return modelBakery.getBakedTopLevelModels().get(location); } } - private UnbakedModel loadModel(ResourceLocation location) { - return null; /* TODO :) */ + private Optional loadBakedModel(ModelResourceLocation location) { + var unbakedModel = this.loadedUnbakedModels.getUnchecked(location); + return unbakedModel.map(model -> this.bakeModel(model, location)); + } + + private Optional loadBlockModel(ResourceLocation location) { + if(location.equals(SpecialModels.BUILTIN_GENERATED)) { + return Optional.of(SpecialModels.GENERATED_MARKER); + } else if(location.equals(SpecialModels.BUILTIN_BLOCK_ENTITY)) { + return Optional.of(SpecialModels.BLOCK_ENTITY_MARKER); + } + var resource = this.resourceManager.getResource(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "models/" + location.getPath() + ".json")); + if(resource.isPresent()) { + try(Reader reader = resource.get().openAsReader()) { + BlockModel blockModel = BlockModel.fromStream(reader); + blockModel.name = location.toString(); + return Optional.of(blockModel); + } catch(Exception e) { + ModernFix.LOGGER.error("Failed to load block model {} from '{}'", location, resource.get().sourcePackId(), e); + return Optional.empty(); + } + } else { + ModernFix.LOGGER.warn("Model '{}' does not exist in any resource packs", location); + return Optional.empty(); + } + } + + private Optional loadModel(ModelResourceLocation location) { + if (location.variant().equals(ModelResourceLocation.INVENTORY_VARIANT)) { + return this.loadedBlockModels.getUnchecked(ResourceLocation.fromNamespaceAndPath(location.id().getNamespace(), "item/" + location.id().getPath())); + } else if (location.variant().equals("standalone") || location.variant().equals("fabric_resource")) { + return this.loadedBlockModels.getUnchecked(location.id()); + } else { + var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id()); + return optLoadedModels.map(loadedModels -> { + var loadedModel = loadedModels.models().get(location); + if(loadedModel != null) { + return loadedModel.model(); + } else { + return null; + } + }); + } + } + + public BakedModel getModel(ModelResourceLocation location) { + return this.loadedBakedModels.getUnchecked(location).orElse(this.missingModel); + } + + /** + * Based on the Mojang impl but with some changes to make it slightly more efficient. + */ + private class DynamicResolver implements UnbakedModel.Resolver { + private final Set stack = new ObjectOpenHashSet<>(4); + private final Set resolvedModels = new ObjectOpenHashSet<>(); + + @Override + public UnbakedModel resolve(ResourceLocation resourceLocation) { + if (this.stack.contains(resourceLocation)) { + ModernFix.LOGGER.warn("Detected model loading loop: {}->{}", this.stacktraceToString(), resourceLocation); + return DynamicModelProvider.this.unbakedMissingModel; + } else { + UnbakedModel unbakedModel = DynamicModelProvider.this.loadedBlockModels.getUnchecked(resourceLocation).orElse(DynamicModelProvider.this.unbakedMissingModel); + if (this.resolvedModels.add(resourceLocation)) { + this.stack.add(resourceLocation); + unbakedModel.resolveDependencies(this); + this.stack.remove(resourceLocation); + } + + return unbakedModel; + } + } + + private String stacktraceToString() { + return this.stack.stream().map(ResourceLocation::toString).collect(Collectors.joining("->")); + } + + public void clearResolver() { + this.stack.clear(); + this.resolvedModels.clear(); + } } } diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java index ec0962fd..27dc727d 100644 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java +++ b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/ModelBakeryHelpers.java @@ -11,45 +11,6 @@ import net.minecraft.world.level.block.state.properties.Property; import java.util.*; public class ModelBakeryHelpers { - /** - * The maximum number of baked models kept in memory at once. - */ - public static final int MAX_BAKED_MODEL_COUNT = 10000; - /** - * The maximum number of unbaked models kept in memory at once. - */ - public static final int MAX_UNBAKED_MODEL_COUNT = 10000; - /** - * The time in seconds after which a model becomes eligible for eviction if not used. - */ - public static final int MAX_MODEL_LIFETIME_SECS = 300; - - /** - * These folders will have all textures stitched onto the atlas when dynamic resources is enabled. - */ - public static String[] getExtraTextureFolders() { - return new String[] { - "attachment", - "bettergrass", - "block", - "blocks", - "cape", - "entity/bed", - "entity/chest", - "item", - "items", - "model", - "models", - "part", - "pipe", - "ropebridge", - "runes", - "solid_block", - "spell_effect", - "spell_projectile" - }; - } - private static , V extends T> BlockState setPropertyGeneric(BlockState state, Property prop, Object o) { return state.setValue(prop, (V)o); } diff --git a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/UVController.java b/common/src/main/java/org/embeddedt/modernfix/dynamicresources/UVController.java deleted file mode 100644 index 7b2efe7e..00000000 --- a/common/src/main/java/org/embeddedt/modernfix/dynamicresources/UVController.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.embeddedt.modernfix.dynamicresources; - -import net.minecraft.client.renderer.block.model.BlockFaceUV; - -public class UVController { - public static final ThreadLocal useDummyUv = ThreadLocal.withInitial(() -> Boolean.FALSE); - public static final BlockFaceUV dummyUv = new BlockFaceUV(new float[4], 0); -}