Rewrite dynamic resources once again

This commit is contained in:
embeddedt 2024-10-28 11:37:03 -04:00
parent d8b86708e5
commit 5450a16aad
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
6 changed files with 252 additions and 166 deletions

View File

@ -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();
}
}

View File

@ -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<ResourceLocation, BakedModel> bakedRegistry;
@Shadow private BakedModel missingModel;
@Unique
private DynamicModelProvider mfix$modelProvider;
@Unique
private Map<ResourceLocation, AtlasSet.StitchResult> mfix$stitchResults;
@Inject(method = "<init>", 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<ResourceLocation, AtlasSet.StitchResult> map, ModelBakery modelBakery, Object2IntMap<BlockState> 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;
}
}
}

View File

@ -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<K> {
private final Reference2ReferenceLinkedOpenHashMap<K, BakedModel> cache = new Reference2ReferenceLinkedOpenHashMap<>();
private final StampedLock lock = new StampedLock();
private final Function<K, BakedModel> modelRetriever;
private final boolean allowNulls;
public DynamicModelCache(Function<K, BakedModel> 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;
}
}

View File

@ -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<ResourceLocation, UnbakedModel> internalModels;
private final Cache<ResourceLocation, Optional<UnbakedModel>> loadedModels =
private final LoadingCache<ResourceLocation, Optional<BlockStateModelLoader.LoadedModels>> loadedStateDefinitions =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.maximumSize(1000)
.concurrencyLevel(8)
.softValues()
.build();
.build(new CacheLoader<>() {
@Override
public Optional<BlockStateModelLoader.LoadedModels> load(ResourceLocation key) {
return loadBlockStateDefinition(key);
}
});
public DynamicModelProvider(Map<ResourceLocation, UnbakedModel> initialModels) {
this.internalModels = initialModels;
private final LoadingCache<ResourceLocation, Optional<UnbakedModel>> loadedBlockModels =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.maximumSize(1000)
.concurrencyLevel(8)
.softValues()
.build(new CacheLoader<>() {
@Override
public Optional<UnbakedModel> load(ResourceLocation key) {
return loadBlockModel(key);
}
});
private final LoadingCache<ModelResourceLocation, Optional<UnbakedModel>> loadedUnbakedModels =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.maximumSize(1000)
.concurrencyLevel(8)
.softValues()
.build(new CacheLoader<>() {
@Override
public Optional<UnbakedModel> load(ModelResourceLocation key) {
return loadModel(key);
}
});
private final LoadingCache<ModelResourceLocation, Optional<BakedModel>> loadedBakedModels =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.maximumSize(1000)
.concurrencyLevel(8)
.softValues()
.build(new CacheLoader<>() {
@Override
public Optional<BakedModel> load(ModelResourceLocation key) {
return loadBakedModel(key);
}
});
private final Map<ModelResourceLocation, BakedModel> initialBakedRegistry;
private final BakedModel missingModel;
private final UnbakedModel unbakedMissingModel;
private final Function<ResourceLocation, StateDefinition<Block, BlockState>> stateMapper;
private final ResourceManager resourceManager;
private final BlockStateModelLoader blockStateModelLoader;
private final ModelBakery.TextureGetter textureGetter;
private final DynamicMap<ResourceLocation, UnbakedModel> fakeUnbakedModelMap;
private final DynamicResolver resolver;
public DynamicModelProvider(Map<ModelResourceLocation, BakedModel> initialBakedRegistry, BakedModel missingModel, ResourceManager resourceManager, Map<ResourceLocation, AtlasSet.StitchResult> 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<BlockStateModelLoader.LoadedModels> loadBlockStateDefinition(ResourceLocation location) {
StateDefinition<Block, BlockState> stateDefinition = this.stateMapper.apply(location);
if(stateDefinition == null) {
return Optional.empty();
}
List<Resource> resources = resourceManager.getResourceStack(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "blockstates/" + location.getPath() + ".json"));
List<BlockStateModelLoader.LoadedBlockModelDefinition> 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<BakedModel> loadBakedModel(ModelResourceLocation location) {
var unbakedModel = this.loadedUnbakedModels.getUnchecked(location);
return unbakedModel.map(model -> this.bakeModel(model, location));
}
private Optional<UnbakedModel> 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<UnbakedModel> 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<ResourceLocation> stack = new ObjectOpenHashSet<>(4);
private final Set<ResourceLocation> 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();
}
}
}

View File

@ -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 <T extends Comparable<T>, V extends T> BlockState setPropertyGeneric(BlockState state, Property<T> prop, Object o) {
return state.setValue(prop, (V)o);
}

View File

@ -1,8 +0,0 @@
package org.embeddedt.modernfix.dynamicresources;
import net.minecraft.client.renderer.block.model.BlockFaceUV;
public class UVController {
public static final ThreadLocal<Boolean> useDummyUv = ThreadLocal.withInitial(() -> Boolean.FALSE);
public static final BlockFaceUV dummyUv = new BlockFaceUV(new float[4], 0);
}