Dynamic model loading on Fabric

This commit is contained in:
embeddedt 2024-12-07 16:59:09 -05:00
parent 145896cc99
commit b822f5ce87
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
5 changed files with 361 additions and 58 deletions

View File

@ -0,0 +1,75 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.renderer.block.BlockModelShaper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IModelHoldingBlockState;
import org.embeddedt.modernfix.dynamicresources.ModelLocationCache;
import org.embeddedt.modernfix.util.DynamicOverridableMap;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
@Mixin(BlockModelShaper.class)
@ClientOnlyMixin
public class BlockModelShaperMixin {
@Shadow @Final private ModelManager modelManager;
@Shadow
private Map<BlockState, BakedModel> modelByStateCache;
@Inject(method = { "<init>", "replaceCache" }, at = @At("RETURN"))
private void replaceModelMap(CallbackInfo ci) {
// replace the backing map for mods which will access it
this.modelByStateCache = new DynamicOverridableMap<>(BlockState.class, state -> modelManager.getModel(ModelLocationCache.get(state)));
// Clear the cached models on blockstate objects
for(Block block : BuiltInRegistries.BLOCK) {
for(BlockState state : block.getStateDefinition().getPossibleStates()) {
if(state instanceof IModelHoldingBlockState modelHolder) {
modelHolder.mfix$setModel(null);
}
}
}
}
private BakedModel cacheBlockModel(BlockState state) {
// Do all model system accesses in the unlocked path
ModelResourceLocation mrl = ModelLocationCache.get(state);
BakedModel model = mrl == null ? null : modelManager.getModel(mrl);
if (model == null) {
model = modelManager.getMissingModel();
}
return model;
}
/**
* @author embeddedt
* @reason get the model from the dynamic model provider
*/
@Overwrite
public BakedModel getBlockModel(BlockState state) {
if(state instanceof IModelHoldingBlockState modelHolder) {
BakedModel model = modelHolder.mfix$getModel();
if(model != null) {
return model;
}
model = this.cacheBlockModel(state);
modelHolder.mfix$setModel(model);
return model;
} else {
return this.cacheBlockModel(state);
}
}
}

View File

@ -0,0 +1,27 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.world.level.block.state.BlockBehaviour;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IModelHoldingBlockState;
import org.spongepowered.asm.mixin.Mixin;
import java.lang.ref.SoftReference;
@Mixin(BlockBehaviour.BlockStateBase.class)
@ClientOnlyMixin
public class BlockStateBaseMixin implements IModelHoldingBlockState {
private volatile SoftReference<BakedModel> mfix$model;
@Override
public BakedModel mfix$getModel() {
var ref = mfix$model;
return ref != null ? ref.get() : null;
}
@Override
public void mfix$setModel(BakedModel model) {
mfix$model = model != null ? new SoftReference<>(model) : null;
}
}

View File

@ -0,0 +1,102 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.geom.EntityModelSet;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.block.state.BlockState;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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 java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@Mixin(ModelManager.class)
@ClientOnlyMixin
public class ModelManagerMixin {
@Shadow private BakedModel missingModel;
@Shadow private ItemModel missingItemModel;
@Shadow private EntityModelSet entityModelSet;
@Unique
private DynamicModelProvider mfix$modelProvider;
@Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockModels(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<Map<ResourceLocation, BlockModel>> deferBlockModelLoad(ResourceManager manager, Executor executor) {
return CompletableFuture.completedFuture(Map.of());
}
@Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BlockStateModelLoader;loadBlockStates(Lnet/minecraft/client/resources/model/UnbakedModel;Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<BlockStateModelLoader.LoadedModels> deferBlockStateLoad(UnbakedModel unbakedModel, ResourceManager resourceManager, Executor executor) {
return CompletableFuture.completedFuture(new BlockStateModelLoader.LoadedModels(Map.of()));
}
/**
* @author embeddedt
* @reason disable map creation
*/
@Overwrite
private static Map<BlockState, BakedModel> createBlockStateToModelDispatch(Map<ModelResourceLocation, BakedModel> map, BakedModel bakedModel) {
return Map.of();
}
@Inject(method = "apply", at = @At("RETURN"))
private void createModelProvider(ModelManager.ReloadState reloadState, ProfilerFiller profiler, CallbackInfo ci) {
this.mfix$modelProvider = new DynamicModelProvider(
null, // TODO
this.missingModel,
this.missingItemModel,
Minecraft.getInstance().getResourceManager(),
this.entityModelSet,
reloadState.atlasPreparations()
);
}
/**
* @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;
}
}
/**
* @author embeddedt
* @reason use dynamic model system
*/
@Overwrite
public ItemModel getItemModel(ResourceLocation resourceLocation) {
return this.mfix$modelProvider.getItemModel(resourceLocation);
}
/**
* @author embeddedt
* @reason use dynamic model system
*/
@Overwrite
public ClientItem.Properties getItemProperties(ResourceLocation resourceLocation) {
return this.mfix$modelProvider.getClientItemProperties(resourceLocation);
}
}

View File

@ -4,15 +4,30 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.client.model.geom.EntityModelSet;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.renderer.block.model.BlockModelDefinition;
import net.minecraft.client.renderer.block.model.ItemModelGenerator;
import net.minecraft.client.renderer.block.model.UnbakedBlockStateModel;
import net.minecraft.client.renderer.item.ClientItem;
import net.minecraft.client.renderer.item.ItemModel;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.AtlasSet;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.MissingBlockModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelDebugName;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.SpriteGetter;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
@ -38,7 +53,6 @@ import java.util.stream.Collectors;
* Handles loading models dynamically, rather than at startup time.
*/
public class DynamicModelProvider {
/*
private final LoadingCache<ResourceLocation, Optional<BlockStateModelLoader.LoadedModels>> loadedStateDefinitions =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
@ -65,19 +79,6 @@ public class DynamicModelProvider {
}
});
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)
@ -91,35 +92,72 @@ public class DynamicModelProvider {
}
});
private final Map<ModelResourceLocation, BakedModel> initialBakedRegistry;
private final LoadingCache<ResourceLocation, Optional<ClientItem>> loadedClientItemProperties =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.maximumSize(1000)
.concurrencyLevel(8)
.softValues()
.build(new CacheLoader<>() {
@Override
public Optional<ClientItem> load(ResourceLocation key) {
return loadClientItemProperties(key);
}
});
private final LoadingCache<ResourceLocation, Optional<ItemModel>> loadedItemModels =
CacheBuilder.newBuilder()
.expireAfterAccess(3, TimeUnit.MINUTES)
.maximumSize(1000)
.concurrencyLevel(8)
.softValues()
.build(new CacheLoader<>() {
@Override
public Optional<ItemModel> load(ResourceLocation key) {
return loadItemModel(key);
}
});
private final BakedModel missingModel;
private final ItemModel missingItemModel;
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;
private final EntityModelSet entityModelSet;
private final ItemModelGenerator itemModelGenerator;
public DynamicModelProvider(Map<ModelResourceLocation, BakedModel> initialBakedRegistry, BakedModel missingModel, ResourceManager resourceManager, Map<ResourceLocation, AtlasSet.StitchResult> atlasMap) {
this.initialBakedRegistry = initialBakedRegistry;
public DynamicModelProvider(Map<ModelResourceLocation, BakedModel> initialBakedRegistry, BakedModel missingModel,
ItemModel missingItemModel, ResourceManager resourceManager, EntityModelSet entityModelSet,
Map<ResourceLocation, AtlasSet.StitchResult> atlasMap) {
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.missingItemModel = missingItemModel;
this.entityModelSet = entityModelSet;
var missing = atlasMap.get(TextureAtlas.LOCATION_BLOCKS).missing();
this.textureGetter = new ModelBakery.TextureGetter() {
@Override
public TextureAtlasSprite get(ModelDebugName modelDebugName, Material 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(), modelDebugName.get());
return missing;
}
}
@Override
public TextureAtlasSprite reportMissingReference(ModelDebugName modelDebugName, String string) {
return 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();
this.itemModelGenerator = new ItemModelGenerator();
}
private Optional<BlockStateModelLoader.LoadedModels> loadBlockStateDefinition(ResourceLocation location) {
@ -138,35 +176,56 @@ public class DynamicModelProvider {
ModernFix.LOGGER.error("Failed to load blockstate definition {} from pack '{}'", location, resource.sourcePackId(), e);
}
}
return Optional.of(this.blockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions));
return Optional.of(BlockStateModelLoader.loadBlockStateDefinitionStack(location, stateDefinition, loadedDefinitions, this.unbakedMissingModel));
}
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);
var modelBaker = new DynamicBaker(location::toString);
return UnbakedModel.bakeWithTopModelValues(model, modelBaker, BlockModelRotation.X0_Y0);
}
}
private BakedModel bakeModel(UnbakedBlockStateModel model, ModelResourceLocation location) {
synchronized (this) {
this.resolver.clearResolver();
model.resolveDependencies(this.resolver);
var modelBaker = new DynamicBaker(location::toString);
return model.bake(modelBaker);
}
}
private Optional<BakedModel> loadBakedModel(ModelResourceLocation location) {
var unbakedModel = this.loadedUnbakedModels.getUnchecked(location);
return unbakedModel.map(model -> this.bakeModel(model, location));
if (location.variant().equals("standalone") || location.variant().equals("fabric_resource")) {
return this.loadedBlockModels.getUnchecked(location.id()).map(unbakedModel -> {
return this.bakeModel(unbakedModel, location);
});
} else {
var optLoadedModels = this.loadedStateDefinitions.getUnchecked(location.id());
Optional<UnbakedBlockStateModel> unbakedModelOpt = optLoadedModels.map(loadedModels -> {
var loadedModel = loadedModels.models().get(location);
if(loadedModel != null) {
return loadedModel.model();
} else {
return null;
}
});
return unbakedModelOpt.map(unbakedModel -> {
return this.bakeModel(unbakedModel, 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);
if (location.equals(ItemModelGenerator.GENERATED_ITEM_MODEL_ID)) {
return Optional.of(this.itemModelGenerator);
}
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);
@ -178,34 +237,71 @@ public class DynamicModelProvider {
}
}
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());
private Optional<ClientItem> loadClientItemProperties(ResourceLocation location) {
var resource = this.resourceManager.getResource(ResourceLocation.fromNamespaceAndPath(location.getNamespace(), "items/" + location.getPath() + ".json"));
if(resource.isPresent()) {
try(Reader reader = resource.get().openAsReader()) {
ClientItem clientItem = ClientItem.CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).getOrThrow();
return Optional.of(clientItem);
} catch(Exception e) {
ModernFix.LOGGER.error("Failed to load block model {} from '{}'", location, resource.get().sourcePackId(), e);
return Optional.empty();
}
} 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;
}
});
ModernFix.LOGGER.warn("Client item '{}' does not exist in any resource packs", location);
return Optional.empty();
}
}
private Optional<ItemModel> loadItemModel(ResourceLocation location) {
return this.loadedClientItemProperties.getUnchecked(location).map(clientItem -> {
var bakingContext = new ItemModel.BakingContext(new DynamicBaker(location::toString), this.entityModelSet, this.missingItemModel);
return clientItem.model().bake(bakingContext);
});
}
public BakedModel getModel(ModelResourceLocation location) {
return this.loadedBakedModels.getUnchecked(location).orElse(this.missingModel);
}
*/
public ClientItem.Properties getClientItemProperties(ResourceLocation location) {
return this.loadedClientItemProperties.getUnchecked(location).map(ClientItem::properties).orElse(ClientItem.Properties.DEFAULT);
}
public ItemModel getItemModel(ResourceLocation location) {
return this.loadedItemModels.getUnchecked(location).orElse(this.missingItemModel);
}
private class DynamicBaker implements ModelBaker {
private final ModelDebugName modelDebugName;
private DynamicBaker(ModelDebugName modelDebugName) {
this.modelDebugName = modelDebugName;
}
@Override
public BakedModel bake(ResourceLocation location, ModelState transform) {
return DynamicModelProvider.this.loadBlockModel(location).map(unbakedModel -> {
DynamicModelProvider.this.resolver.clearResolver();
unbakedModel.resolveDependencies(DynamicModelProvider.this.resolver);
return UnbakedModel.bakeWithTopModelValues(unbakedModel, this, transform);
}).orElse(DynamicModelProvider.this.missingModel);
}
@Override
public SpriteGetter sprites() {
return DynamicModelProvider.this.textureGetter.bind(this.modelDebugName);
}
@Override
public ModelDebugName rootName() {
return this.modelDebugName;
}
}
/**
* 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<>();
@ -236,6 +332,4 @@ public class DynamicModelProvider {
this.resolvedModels.clear();
}
}
*/
}

View File

@ -42,6 +42,11 @@ accessible class net/minecraft/client/resources/model/ModelBakery$BakedCacheKey
accessible method net/minecraft/client/resources/model/ModelBakery$BakedCacheKey <init> (Lnet/minecraft/resources/ResourceLocation;Lcom/mojang/math/Transformation;Z)V
accessible class net/minecraft/client/resources/model/ModelBakery$ModelBakerImpl
accessible method net/minecraft/client/resources/model/ModelBakery$ModelBakerImpl <init> (Lnet/minecraft/client/resources/model/ModelBakery;Lnet/minecraft/client/resources/model/ModelBakery$TextureGetter;Lnet/minecraft/client/resources/model/ModelResourceLocation;)V
accessible class net/minecraft/client/resources/model/ModelManager$ReloadState
accessible method net/minecraft/client/resources/model/BlockStateModelLoader definitionLocationToBlockMapper ()Ljava/util/function/Function;
accessible method net/minecraft/client/resources/model/BlockStateModelLoader loadBlockStateDefinitionStack (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/world/level/block/state/StateDefinition;Ljava/util/List;Lnet/minecraft/client/resources/model/UnbakedModel;)Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedModels;
accessible class net/minecraft/client/resources/model/BlockStateModelLoader$LoadedBlockModelDefinition
accessible method net/minecraft/client/resources/model/BlockStateModelLoader$LoadedBlockModelDefinition <init> (Ljava/lang/String;Lnet/minecraft/client/renderer/block/model/BlockModelDefinition;)V
accessible class net/minecraft/world/level/chunk/PalettedContainer$Data
accessible field net/minecraft/server/MinecraftServer resources Lnet/minecraft/server/MinecraftServer$ReloadableResources;
accessible class net/minecraft/server/MinecraftServer$ReloadableResources