Skip to content

Commit

Permalink
Switch to simplier dynamic texture loading
Browse files Browse the repository at this point in the history
After some empirical testing, in most cases listResources was slower than hasResource
That said, hasResource is slower than a hash lookup, so since I already have the logic to build and clear the cache, use that to cache the result of hasResource.
  • Loading branch information
KnightMiner committed Nov 22, 2022
1 parent 6c4b55a commit c379d30
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 155 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ private ResourceLocation getLocation(ResourceLocation base) {

@Override
public boolean exists(ResourceLocation path) {
return DynamicTextureLoader.textureExists(manager, DynamicTextureLoader.getTextureFolder(path), path);
return DynamicTextureLoader.textureExists(manager, path);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,103 +10,44 @@
import net.minecraftforge.client.event.TextureStitchEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.EventPriority;
import slimeknights.mantle.data.IEarlySafeManagerReloadListener;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

/**
* Logic to handle dynamic texture scans. Instead of calling {@link ResourceManager#hasResource(ResourceLocation)} per texture, we take advantage of the fact
* our models are always looking for many textures in a single folder and do a per folder call to {@link ResourceManager#listResources(String, Predicate)}
* Logic to handle dynamic texture scans. Takes advantage of the fact that we reuse some tool textures to reduce resource lookups
*/
@Log4j2
public class DynamicTextureLoader {
/** Start of the path to trim when caching existing textures */
private static final int TRIM_START = "textures/".length();
/** End of the path to trim when caching existing textures */
private static final int TRIM_END = ".png".length();

/** If textures are placed here, only need a single scan instead of one per tool */
private static final String PREFERRED_FOLDER = "item/tool";

/** Set of all folders that have been scanned, so we can avoid scanning them twice */
private static final Set<String> SCANNED_FOLDERS = new HashSet<>();
/** Map of discovered textures */
private static final Set<ResourceLocation> EXISTING_TEXTURES = new HashSet<>();
private static final Map<ResourceLocation,Boolean> EXISTING_TEXTURES = new HashMap<>();
/** Set of all textures that are missing from the resource pack, to avoid logging twice */
private static final Set<ResourceLocation> SKIPPED_TEXTURES = new HashSet<>();
/** Reload listener to clear caches */
private static final IEarlySafeManagerReloadListener RELOAD_LISTENER = manager -> {
clearCache();
};

/** Clears all cached texture names */
public static void clearCache() {
SCANNED_FOLDERS.clear();
EXISTING_TEXTURES.clear();
SKIPPED_TEXTURES.clear();
}

/** Registers this manager */
public static void init(RegisterClientReloadListenersEvent event) {
event.registerReloadListener(RELOAD_LISTENER);
// clear cache on texture stitch, no longer need it then as its too late to lookup textures
MinecraftForge.EVENT_BUS.addListener(EventPriority.NORMAL, false, TextureStitchEvent.Pre.class, e -> clearCache());
}

/** Scans the given folder to add all textures */
private static void scanFolder(ResourceManager manager, String folder) {
manager.listResources("textures/" + folder, name -> name.endsWith(".png")).stream()
.map(loc -> {
String path = loc.getPath();
return new ResourceLocation(loc.getNamespace(), path.substring(TRIM_START, path.length() - TRIM_END));
})
.forEach(EXISTING_TEXTURES::add);
}

/** Checks if the given folder is not yet scanned */
private static boolean checkFolderNotScanned(ResourceManager manager, String originalFolder) {
// if we already checked the folder, no work to do
if (SCANNED_FOLDERS.contains(originalFolder)) {
return false;
}

// if the folder we are looking for starts with the preferred folder, we can immediately resolve it by resolving the whole preferred folder
if (originalFolder.startsWith(PREFERRED_FOLDER)) {
SCANNED_FOLDERS.add(originalFolder);
if (!SCANNED_FOLDERS.contains(PREFERRED_FOLDER)) {
SCANNED_FOLDERS.add(PREFERRED_FOLDER);
scanFolder(manager, PREFERRED_FOLDER);
}
return false;
}

// if a folder has not been scanned yet, check if any of its parent's have been scanned
// list resources will fetch all sub folders, so this saves us calling it multiple times per tool
String folder = originalFolder;
int lastPos = folder.lastIndexOf('/');
while (lastPos != -1) {
folder = folder.substring(0, lastPos);
if (SCANNED_FOLDERS.contains(folder)) {
// if we scanned a parent, no work to do, but mark ourself as scanned, may find more textures here later
SCANNED_FOLDERS.add(originalFolder);
return false;
}
lastPos = folder.lastIndexOf('/');
}
// mark this folder as searched for next time, return true to fetch texture names
SCANNED_FOLDERS.add(originalFolder);
return true;
MinecraftForge.EVENT_BUS.addListener(EventPriority.NORMAL, false, TextureStitchEvent.Post.class, e -> clearCache());
}

/** Checks if a texture exists */
public static boolean textureExists(ResourceManager manager, String folder, ResourceLocation location) {
if (checkFolderNotScanned(manager, folder)) {
scanFolder(manager, folder);
public static boolean textureExists(ResourceManager manager, ResourceLocation location) {
Boolean found = EXISTING_TEXTURES.get(location);
if (found == null) {
found = manager.hasResource(new ResourceLocation(location.getNamespace(), "textures/" + location.getPath() + ".png"));
EXISTING_TEXTURES.put(location, found);
}
return EXISTING_TEXTURES.contains(location);
return found;
}

/** Logs that a dynamic texture is missing, config option to disable */
Expand All @@ -120,16 +61,15 @@ public static void logMissingTexture(ResourceLocation location) {
/**
* Gets a consumer to add textures to the given collection
* @param allTextures Collection of textures
* @param folder Folder the texture is expected to reside in. Will give inconsistent behavior if the location is not a member of the folder, this is not validated
* @param logMissingTextures If true, log textures that were not found
* @return Texture consumer
*/
public static Predicate<Material> getTextureAdder(String folder, Collection<Material> allTextures, boolean logMissingTextures) {
public static Predicate<Material> getTextureAdder(Collection<Material> allTextures, boolean logMissingTextures) {
ResourceManager manager = Minecraft.getInstance().getResourceManager();
return mat -> {
// either must be non-blocks, or must exist. We have fallbacks if it does not exist
ResourceLocation loc = mat.texture();
if (!InventoryMenu.BLOCK_ATLAS.equals(mat.atlasLocation()) || textureExists(manager, folder, loc)) {
if (!InventoryMenu.BLOCK_ATLAS.equals(mat.atlasLocation()) || textureExists(manager, loc)) {
allTextures.add(mat);
return true;
}
Expand All @@ -139,14 +79,4 @@ public static Predicate<Material> getTextureAdder(String folder, Collection<Mate
return false;
};
}

/** Gets the folder containing the given texture */
public static String getTextureFolder(ResourceLocation location) {
String path = location.getPath();
int index = path.lastIndexOf('/');
if (index == -1) {
return path;
}
return path.substring(0, index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import com.mojang.math.Vector3f;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.ItemOverrides;
Expand All @@ -29,7 +28,6 @@
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec2;
import net.minecraftforge.client.model.BakedItemModel;
Expand Down Expand Up @@ -81,28 +79,10 @@ public Collection<Material> getTextures(IModelConfiguration owner, Function<Reso
return allTextures;
}

/** Checks if a texture exists */
@Deprecated
private static boolean textureExists(ResourceManager manager, ResourceLocation location) {
return manager.hasResource(new ResourceLocation(location.getNamespace(), "textures/" + location.getPath() + ".png"));
}

/** @deprecated use {@link DynamicTextureLoader#getTextureAdder(String, Collection, boolean)} */
/** @deprecated use {@link DynamicTextureLoader#getTextureAdder(Collection, boolean)} */
@Deprecated
public static Predicate<Material> getTextureAdder(Collection<Material> allTextures, boolean logMissingTextures) {
ResourceManager manager = Minecraft.getInstance().getResourceManager();
return mat -> {
// either must be non-blocks, or must exist. We have fallbacks if it does not exist
ResourceLocation loc = mat.texture();
if (!InventoryMenu.BLOCK_ATLAS.equals(mat.atlasLocation()) || textureExists(manager, loc)) {
allTextures.add(mat);
return true;
}
if (logMissingTextures) {
DynamicTextureLoader.logMissingTexture(loc);
}
return false;
};
return DynamicTextureLoader.getTextureAdder(allTextures, logMissingTextures);
}

/**
Expand All @@ -119,7 +99,7 @@ public static void getMaterialTextures(Collection<Material> allTextures, IModelC
// if the texture is missing, stop here
if (!MissingTextureAtlasSprite.getLocation().equals(texture.texture())) {
// texture should exist in item/tool, or the validator cannot handle them
Predicate<Material> textureAdder = DynamicTextureLoader.getTextureAdder(DynamicTextureLoader.getTextureFolder(texture.texture()), allTextures, Config.CLIENT.logMissingMaterialTextures.get());
Predicate<Material> textureAdder = DynamicTextureLoader.getTextureAdder(allTextures, Config.CLIENT.logMissingMaterialTextures.get());
// if no specific material is set, load all materials as dependencies. If just one material, use just that one
if (material == null) {
MaterialRenderInfoLoader.INSTANCE.getAllRenderInfos().forEach(info -> info.getTextureDependencies(textureAdder, texture));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import net.minecraftforge.fml.event.IModBusEvent;
import slimeknights.mantle.data.IEarlySafeManagerReloadListener;
import slimeknights.mantle.util.JsonHelper;
import slimeknights.tconstruct.common.config.Config;
import slimeknights.tconstruct.library.client.model.DynamicTextureLoader;
import slimeknights.tconstruct.library.modifiers.ModifierId;

Expand Down Expand Up @@ -137,60 +136,34 @@ public void onReloadSafe(ResourceManager manager) {
modifierModels = models;
}

/** Record handling the texture adder for each given modifier folder */
private record ModifierTextureLoader(ResourceLocation root, Predicate<Material> textureAdder) {
private ModifierTextureLoader(ResourceLocation root, Collection<Material> textures, boolean logMissingTextures) {
// the texture root may be a folder, in which case that method will trim off the trailing slash
// it may be instead a folder with a prefix, in that case ignore the prefix
this(root, DynamicTextureLoader.getTextureAdder(DynamicTextureLoader.getTextureFolder(root), textures, logMissingTextures));
}

/**
* Gets the path to the texture for a given modifier
* @param modifierRoot Modifier root location
* @param modifierId Specific modifier ID
* @return Path to the modifier
*/
private static Material getModifierTexture(ResourceLocation modifierRoot, ResourceLocation modifierId, String suffix) {
return ForgeHooksClient.getBlockMaterial(new ResourceLocation(modifierRoot.getNamespace(), modifierRoot.getPath() + modifierId.getNamespace() + "_" + modifierId.getPath() + suffix));
}

/** Gets the texture for the given modifier and suffix */
@Nullable
public Material getTexture(ResourceLocation modifier, String suffix) {
Material texture = getModifierTexture(root, modifier, suffix);
if (textureAdder.test(texture)) {
return texture;
}
return null;
}
}

/** Gets the texture loaders for the given modifier textures */
private static List<ModifierTextureLoader> getModifierTextureLoaders(List<ResourceLocation> modifierRoots, Collection<Material> allTextures) {
if (modifierRoots.isEmpty()) {
return Collections.emptyList();
}
return modifierRoots.stream().map(root -> new ModifierTextureLoader(root, allTextures, Config.CLIENT.logMissingModifierTextures.get())).toList();
/**
* Gets the path to the texture for a given modifier
* @param modifierRoot Modifier root location
* @param modifierId Specific modifier ID
* @return Path to the modifier
*/
private static Material getModifierTexture(ResourceLocation modifierRoot, ResourceLocation modifierId, String suffix) {
return ForgeHooksClient.getBlockMaterial(new ResourceLocation(modifierRoot.getNamespace(), modifierRoot.getPath() + modifierId.getNamespace() + "_" + modifierId.getPath() + suffix));
}

/**
* Gets the texture for the given parameters
* @param loaders List of texture loaders, tries each one in sequence until one finds it
* @param modifier Modifier to fetch
* @param suffix Additional suffix for the fetched texture
* @param modifierRoots List of modifier root locations to try
* @param textureAdder Logic to add a texture to the model
* @param modifier Modifier to fetch
* @param suffix Additional suffix for the fetched texture
* @return Texture, or null if missing
*/
@Nullable
private static Material getTexture(List<ModifierTextureLoader> loaders, ResourceLocation modifier, String suffix) {
if (loaders.isEmpty()) {
private static Material getTexture(List<ResourceLocation> modifierRoots, Predicate<Material> textureAdder, ResourceLocation modifier, String suffix) {
if (modifierModels.isEmpty()) {
return null;
}

// try the non-logging ones first
for (ModifierTextureLoader root : loaders) {
Material texture = root.getTexture(modifier, suffix);
if (texture != null) {
for (ResourceLocation root : modifierRoots) {
Material texture = getModifierTexture(root, modifier, suffix);
if (textureAdder.test(texture)) {
return texture;
}
}
Expand All @@ -215,16 +188,15 @@ public static Map<ModifierId,IBakedModifierModel> getModelsForTool(List<Resource
ImmutableMap.Builder<ModifierId,IBakedModifierModel> modelMap = ImmutableMap.builder();

// create two texture adders, so we only log on the final option if missing
List<ModifierTextureLoader> smallTextureLoaders = getModifierTextureLoaders(smallModifierRoots, textures);
List<ModifierTextureLoader> largeTextureLoaders = getModifierTextureLoaders(largeModifierRoots, textures);
Predicate<Material> textureAdder = DynamicTextureLoader.getTextureAdder(textures, true);

// load each modifier
for (Entry<ModifierId, IUnbakedModifierModel> entry : modifierModels.entrySet()) {
ModifierId id = entry.getKey();
IUnbakedModifierModel model = entry.getValue();
IBakedModifierModel toolModel = model.forTool(
name -> getTexture(smallTextureLoaders, id, name),
name -> getTexture(largeTextureLoaders, id, name));
name -> getTexture(smallModifierRoots, textureAdder, id, name),
name -> getTexture(largeModifierRoots, textureAdder, id, name));
if (toolModel != null) {
modelMap.put(id, toolModel);
}
Expand Down

0 comments on commit c379d30

Please sign in to comment.