From 38b1d9a0c37b4e94fe3f63d4879ba1941fa8f25a Mon Sep 17 00:00:00 2001 From: Jason Mitchell Date: Fri, 2 Oct 2026 20:33:42 -0700 Subject: [PATCH] Speed up boot (#2190) --- .../angelica/glsm/GlslTransformUtils.java | 6 +- .../angelica/glsm/hooks/GLSMHooks.java | 1 + .../angelica/glsm/ReservedWordRenameTest.java | 64 ++++ .../angelica/sdlgpu/shader/ShaderManager.java | 2 + .../gtnewhorizons/angelica/mixins/Mixins.java | 4 +- .../com/prupe/mcpatcher/cit/CITUtils.java | 2 +- .../com/prupe/mcpatcher/ctm/CTMUtils.java | 1 + .../resource/TexturePackChangeHandler.java | 41 ++- .../prupe/mcpatcher/mal/tile/TileLoader.java | 3 + src/main/java/net/coderbot/iris/Iris.java | 41 ++- .../iris/texture/pbr/PBRResourceIndex.java | 232 ++++++++++++++ .../texture/pbr/loader/AtlasPBRLoader.java | 5 + .../texture/pbr/loader/SimplePBRLoader.java | 5 + .../MixinTextureManager_ReloadCount.java | 2 + .../TexturePackChangeHandlerDeferralTest.java | 212 +++++++++++++ .../iris/IrisProgramSetWarmupTest.java | 57 ++++ .../texture/pbr/PBRResourceIndexTest.java | 282 ++++++++++++++++++ 17 files changed, 937 insertions(+), 23 deletions(-) create mode 100644 glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ReservedWordRenameTest.java create mode 100644 src/main/java/net/coderbot/iris/texture/pbr/PBRResourceIndex.java create mode 100644 src/test/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandlerDeferralTest.java create mode 100644 src/test/java/net/coderbot/iris/IrisProgramSetWarmupTest.java create mode 100644 src/test/java/net/coderbot/iris/texture/pbr/PBRResourceIndexTest.java diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GlslTransformUtils.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GlslTransformUtils.java index 1dfe0ef34..3789cbb53 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GlslTransformUtils.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GlslTransformUtils.java @@ -49,11 +49,11 @@ public class GlslTransformUtils { private static final Map> VERSIONED_RESERVED_WORDS = Map.of( // Some reserved words to always rename 0, List.of( - new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"), - new ReservedWordRename(Pattern.compile("\\bnew\\b"), RENAMED_PREFIX + "new") + new ReservedWordRename(Pattern.compile("sample(?<=\\bsample)\\b"), RENAMED_PREFIX + "sample"), + new ReservedWordRename(Pattern.compile("new(?<=\\bnew)\\b"), RENAMED_PREFIX + "new") ), 400, List.of( - new ReservedWordRename(Pattern.compile("\\bsampler\\b(?!\\d)"), RENAMED_PREFIX + "sampler") + new ReservedWordRename(Pattern.compile("sampler(?<=\\bsampler)\\b(?!\\d)"), RENAMED_PREFIX + "sampler") ) ); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java index 257d30295..48f44ca70 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java @@ -14,6 +14,7 @@ public final class GLSMHooks { public static ShaderTransformPostProcessor postTransformProcessor; public static PerFrameUniformBlock perFrameUniformBlock; public static PerFrameUniformBlock perPassUniformBlock; + public static Runnable uniformBlockBarrier; public static PendingProgramSelection pendingProgramSelection; public static void resolvePendingProgram() { diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ReservedWordRenameTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ReservedWordRenameTest.java new file mode 100644 index 000000000..6fbcef00b --- /dev/null +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ReservedWordRenameTest.java @@ -0,0 +1,64 @@ +package com.gtnewhorizons.angelica.glsm; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Random; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ReservedWordRenameTest { + + private static final List LEGACY_ALWAYS = List.of(Pattern.compile("\\bsample\\b"), Pattern.compile("\\bnew\\b")); + private static final Pattern LEGACY_400 = Pattern.compile("\\bsampler\\b(?!\\d)"); + private static final String[] WORDS = { "sample", "sampler", "new" }; + + private static String legacyRename(String source, int targetVersion) { + String out = source; + for (Pattern p : LEGACY_ALWAYS) { + final String word = p.pattern().substring(2, p.pattern().length() - 2); + out = p.matcher(out).replaceAll("angelica_renamed_" + word); + } + if (targetVersion >= 400) out = LEGACY_400.matcher(out).replaceAll("angelica_renamed_sampler"); + return out; + } + + private static void assertSameAsLegacy(String source) { + for (int version : new int[] { 120, 330, 400, 460 }) { + assertEquals(legacyRename(source, version), GlslTransformUtils.renameReservedWords(source, version), "version " + version + " source: " + source); + } + } + + @Test + void edgeCasesMatchLegacy() { + final String[] cases = { + "", "sample", "new", "sampler", " sample ", "sample;", "(sample)", "_sample", "sample_", "sample2", "2sample", + "samples", "resample", "sampler2D", "samplerX", "sampler_", "sampler;", "uniform sampler s;", "sampler2DShadow", + "new_", "renew", "news", "new;new new", "sample sample sample", "samplesampler", "sampler sample new", + "ésample", "sampleé", "ßnewß", "中sampler", "sampler中", "samplé", + "// sample in a comment\nfloat sample = 1.0;\n", "#define new sample\n", "vec4 c = texture(sampler, uv) * sample;", + "float x = sample.x + new_value + newValue + sample1;", "sample\nnew\nsampler\n", "\tsample\t", "sample-new+sampler*", + }; + for (String c : cases) assertSameAsLegacy(c); + } + + @Test + void randomizedInputsMatchLegacy() { + final Random random = new Random(0x5A3D1E); + final String[] fragments = { "sample", "sampler", "new", "s", "a", "_", "1", "2", "D", "x", " ", "\n", ";", "(", ")", ".", "é", "中", "́", "//", "#" }; + for (int i = 0; i < 5000; i++) { + final StringBuilder sb = new StringBuilder(); + final int n = 1 + random.nextInt(24); + for (int j = 0; j < n; j++) sb.append(fragments[random.nextInt(fragments.length)]); + assertSameAsLegacy(sb.toString()); + } + } + + @Test + void literalWordsAreTheOnlyCandidates() { + for (String w : WORDS) { + assertEquals("angelica_renamed_" + w, GlslTransformUtils.renameReservedWords(w, 460)); + } + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java index a905f3339..a35ac603a 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java @@ -146,6 +146,8 @@ public final class ShaderManager { public void shaderSource(int shader, CharSequence source) { final ShaderObject obj = shaderObjects.get(shader); if (obj == null) return; + final Runnable barrier = GLSMHooks.uniformBlockBarrier; + if (barrier != null && !AngelicaWorkers.isWorkerThread()) barrier.run(); discardSpirvFuture(obj); String raw = source.toString(); diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java index 41fab590f..d82457d92 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java @@ -1012,8 +1012,8 @@ public enum Mixins implements IMixins { MCPATCHER_FORGE_CTM_OR_CC(new MixinBuilder("MCP:F Connected Textures or Custom Colors") .setPhase(Phase.EARLY) .setApplyIf(() -> AngelicaConfig.enableMCPatcherForgeFeatures - && MCPatcherForgeConfig.ConnectedTextures.enabled - || MCPatcherForgeConfig.CustomColors.enabled) + && (MCPatcherForgeConfig.ConnectedTextures.enabled + || MCPatcherForgeConfig.CustomColors.enabled)) .addClientMixins("mcpatcherforge.ctm_cc.MixinTextureMap") ), //End from NotFine diff --git a/src/main/java/com/prupe/mcpatcher/cit/CITUtils.java b/src/main/java/com/prupe/mcpatcher/cit/CITUtils.java index 3bd3d1284..176913562 100644 --- a/src/main/java/com/prupe/mcpatcher/cit/CITUtils.java +++ b/src/main/java/com/prupe/mcpatcher/cit/CITUtils.java @@ -56,7 +56,7 @@ public class CITUtils { private static final List allItemEnchantments = new ArrayList<>(); private static final Map> armors = new IdentityHashMap<>(); - static boolean useGlint; + static boolean useGlint = true; private static EnchantmentList armorMatches; private static int armorMatchIndex; diff --git a/src/main/java/com/prupe/mcpatcher/ctm/CTMUtils.java b/src/main/java/com/prupe/mcpatcher/ctm/CTMUtils.java index 28895bc63..0fcdd3806 100644 --- a/src/main/java/com/prupe/mcpatcher/ctm/CTMUtils.java +++ b/src/main/java/com/prupe/mcpatcher/ctm/CTMUtils.java @@ -353,6 +353,7 @@ public class CTMUtils { } private static void setBlankResourceWithoutLock() { + if (tileLoader == null) return; RenderBlocksUtils.blankIcon = tileLoader.getIcon(RenderPassAPI.instance.getBlankResource()); } diff --git a/src/main/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandler.java b/src/main/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandler.java index 0e71dc578..1b99d15b4 100644 --- a/src/main/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandler.java +++ b/src/main/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandler.java @@ -7,8 +7,12 @@ import java.util.Set; import net.minecraft.client.resources.IResourcePack; +import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.prupe.mcpatcher.MCLogger; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.LoaderState; + abstract public class TexturePackChangeHandler implements Comparable { private static final MCLogger logger = MCLogger.getLogger(MCLogger.Category.TEXTURE_PACK); @@ -18,6 +22,8 @@ abstract public class TexturePackChangeHandler implements Comparable 0 || isBootDeferred()) { + deferredDepth++; + return; + } logger.finer("beforeChange1 depth %d", recurseDepth); if (recurseDepth++ > 0) { return; @@ -115,6 +140,10 @@ abstract public class TexturePackChangeHandler implements Comparable 0) { + deferredDepth--; + return; + } logger.finer("afterChange1 depth %d", recurseDepth - 1); if (--recurseDepth > 0) { return; diff --git a/src/main/java/com/prupe/mcpatcher/mal/tile/TileLoader.java b/src/main/java/com/prupe/mcpatcher/mal/tile/TileLoader.java index 8213f85c1..008ea9ccd 100644 --- a/src/main/java/com/prupe/mcpatcher/mal/tile/TileLoader.java +++ b/src/main/java/com/prupe/mcpatcher/mal/tile/TileLoader.java @@ -92,6 +92,9 @@ public class TileLoader { } public static void registerIcons(TextureMap textureMap, String mapName, Map map) { + if (TexturePackChangeHandler.isBootDeferred()) { + return; + } mapName = mapName.replaceFirst("/$", ""); logger.fine("before registerIcons(%s) %d icons", mapName, map.size()); if (!changeHandlerCalled) { diff --git a/src/main/java/net/coderbot/iris/Iris.java b/src/main/java/net/coderbot/iris/Iris.java index b061f264f..3cc428cda 100644 --- a/src/main/java/net/coderbot/iris/Iris.java +++ b/src/main/java/net/coderbot/iris/Iris.java @@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.Tags; import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.glsm.RenderSystem; import com.gtnewhorizons.angelica.glsm.backend.BackendManager; +import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks; import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess; import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler; import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers; @@ -97,6 +98,7 @@ public class Iris { private static ShaderpackDirectoryManager shaderpacksDirectoryManager; private static ShaderPack currentPack; + private static CompletableFuture programSetWarmup; @Getter private static String currentPackName; @Getter @@ -164,14 +166,13 @@ public class Iris { public static void warmup() { final String vertexShader = "#version 120\nvoid main() { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; }"; final String fragmentShader = "#version 120\nvoid main() { gl_FragColor = vec4(1.0); }"; - try { - submitTracked(() -> { - TransformPatcher.patchComposite(vertexShader, null, fragmentShader); - TransformPatcher.patchAttributes(vertexShader, null, fragmentShader, InputAvailability.of(true, true)); - }).get(); - } catch (Exception e) { + submitTracked(() -> { + TransformPatcher.patchComposite(vertexShader, null, fragmentShader); + TransformPatcher.patchAttributes(vertexShader, null, fragmentShader, InputAvailability.of(true, true)); + }).exceptionally(e -> { logger.warn("Warmup failed", e); - } + return null; + }); } public static CompletableFuture submitTracked(Supplier supplier) { @@ -394,19 +395,35 @@ public class Iris { } public static void warmupShaderTransforms() { - if (currentPack == null) return; + final ShaderPack pack = currentPack; + if (pack == null) return; try { if (BackendManager.RENDER_BACKEND.isSDLGPU()) { IrisGLSMBridge.installPostTransformHook(); } - final ProgramSet programs = currentPack.getProgramSet(lastDimensionName != null ? lastDimensionName : "Overworld"); - PerFrameUniformBlockHarvester.harvest(programs); - DeferredWorldRenderingPipeline.warmupTransforms(programs); + final String dimensionName = lastDimensionName != null ? lastDimensionName : "Overworld"; + GLSMHooks.uniformBlockBarrier = Iris::awaitProgramSetWarmup; + programSetWarmup = ShaderTransformExecutor.submitTracked(() -> { + final ProgramSet programs = pack.getProgramSet(dimensionName); + PerFrameUniformBlockHarvester.harvest(programs); + DeferredWorldRenderingPipeline.warmupTransforms(programs); + }); } catch (Throwable t) { logger.warn("Early shader transform warmup failed; transforms will run at pipeline creation", t); } } + private static void awaitProgramSetWarmup() { + final CompletableFuture warmup = programSetWarmup; + if (warmup == null) return; + programSetWarmup = null; + try { + warmup.join(); + } catch (RuntimeException e) { + logger.warn("Early shader transform warmup failed; transforms will run at pipeline creation", e); + } + } + private static boolean loadExternalShaderpack(String name) { final Path shaderPackRoot; final Path shaderPackConfigTxt; @@ -682,6 +699,7 @@ public class Iris { * Destroys and deallocates all created OpenGL resources. Useful as part of a reload. */ private static void destroyEverything() { + awaitProgramSetWarmup(); currentPack = null; getPipelineManager().destroyPipeline(); @@ -746,6 +764,7 @@ public class Iris { * Supports dimension.properties mappings with wildcard fallback. */ private static WorldRenderingPipeline createPipeline(String dimensionName) { + awaitProgramSetWarmup(); if (currentPack == null) { // Completely disables shader-based rendering PerFrameUniformBlockHarvester.clear(); diff --git a/src/main/java/net/coderbot/iris/texture/pbr/PBRResourceIndex.java b/src/main/java/net/coderbot/iris/texture/pbr/PBRResourceIndex.java new file mode 100644 index 000000000..8ce3e7f1a --- /dev/null +++ b/src/main/java/net/coderbot/iris/texture/pbr/PBRResourceIndex.java @@ -0,0 +1,232 @@ +package net.coderbot.iris.texture.pbr; + +import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers; +import cpw.mods.fml.client.FMLFileResourcePack; +import cpw.mods.fml.client.FMLFolderResourcePack; +import net.minecraft.client.resources.AbstractResourcePack; +import net.minecraft.client.resources.FallbackResourceManager; +import net.minecraft.client.resources.FileResourcePack; +import net.minecraft.client.resources.FolderResourcePack; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.client.resources.SimpleReloadableResourceManager; +import net.minecraft.util.ResourceLocation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public final class PBRResourceIndex { + private static final Logger LOGGER = LogManager.getLogger("Angelica"); + private static final String ASSETS = "assets"; + private static final String NORMAL_NAME = PBRType.NORMAL.getSuffix() + ".png"; + private static final String SPECULAR_NAME = PBRType.SPECULAR.getSuffix() + ".png"; + + private static IResourceManager owner; + private static CompletableFuture> pending; + private static Map built; + private static boolean disabled; + private static boolean warned; + + private PBRResourceIndex() { + } + + private record PackIndex(Set names, boolean lowerCase) {} + + private record ScanTarget(IResourcePack pack, ZipFile zip, File folder) {} + + public static synchronized void start(IResourceManager resourceManager) { + drop(); + owner = resourceManager; + disabled = false; + if (!(resourceManager instanceof SimpleReloadableResourceManager manager)) { + return; + } + try { + final List targets = collectTargets(manager); + pending = AngelicaWorkers.submit(() -> scan(targets)); + } catch (RuntimeException e) { + disable(e); + } + } + + private static synchronized void drop() { + owner = null; + pending = null; + built = null; + } + + public static synchronized boolean mayExist(IResourceManager resourceManager, ResourceLocation location) { + if (!(resourceManager instanceof SimpleReloadableResourceManager manager)) { + return true; + } + final String path = location.getResourcePath(); + if (!path.endsWith(NORMAL_NAME) && !path.endsWith(SPECULAR_NAME)) { + return true; + } + if (owner != resourceManager) { + start(resourceManager); + } + if (disabled) { + return true; + } + if (built == null) { + try { + built = pending.join(); + } catch (RuntimeException e) { + disable(e); + return true; + } + } + final Object domain = manager.domainResourceManagers.get(location.getResourceDomain()); + if (domain == null) { + return false; + } + if (!(domain instanceof FallbackResourceManager fallback)) { + return true; + } + final String name = ASSETS + "/" + location.getResourceDomain() + "/" + path; + String lower = null; + final List packs = fallback.resourcePacks; + for (int i = packs.size() - 1; i >= 0; i--) { + final IResourcePack pack = packs.get(i); + final PackIndex index = built.get(pack); + final boolean present; + if (index == null) { + present = pack.resourceExists(location); + } else if (index.lowerCase()) { + if (lower == null) lower = name.toLowerCase(Locale.ROOT); + present = index.names().contains(lower); + } else { + present = index.names().contains(name); + } + if (present) { + return true; + } + } + return false; + } + + private static void disable(RuntimeException e) { + disabled = true; + built = null; + pending = null; + if (!warned) { + warned = true; + LOGGER.warn("PBR resource index disabled, falling back to direct lookups", e); + } + } + + private static List collectTargets(SimpleReloadableResourceManager manager) { + final Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + final List targets = new ArrayList<>(); + for (Object domain : manager.domainResourceManagers.values()) { + if (domain instanceof FallbackResourceManager fallback) { + for (IResourcePack pack : fallback.resourcePacks) { + if (seen.add(pack)) { + final ScanTarget target = target(pack); + if (target != null) { + targets.add(target); + } + } + } + } + } + return targets; + } + + private static ScanTarget target(IResourcePack pack) { + final Class type = pack.getClass(); + if (type == FileResourcePack.class || type == FMLFileResourcePack.class) { + final FileResourcePack zipPack = (FileResourcePack) pack; + try { + return new ScanTarget(pack, zipPack.getResourcePackZipFile(), null); + } catch (IOException e) { + throw new IllegalStateException("Unable to index " + zipPack.resourcePackFile, e); + } + } + if (type == FolderResourcePack.class || type == FMLFolderResourcePack.class) { + return new ScanTarget(pack, null, ((AbstractResourcePack) pack).resourcePackFile); + } + return null; + } + + private static Map scan(List targets) { + final Map result = new IdentityHashMap<>(); + for (ScanTarget t : targets) { + result.put(t.pack(), t.zip() != null ? scanZip(t.zip()) : scanFolder(t.folder())); + } + return result; + } + + private static boolean isPbrName(String name) { + return name.endsWith(NORMAL_NAME) || name.endsWith(SPECULAR_NAME); + } + + private static PackIndex scanZip(ZipFile zip) { + final Set names = new HashSet<>(); + final Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + final String name = entries.nextElement().getName(); + final String key = name.endsWith("/") ? name.substring(0, name.length() - 1) : name; + if (isPbrName(key)) { + names.add(key); + } + } + return new PackIndex(names, false); + } + + private static PackIndex scanFolder(File file) { + final Set names = new HashSet<>(); + final Path root = file.toPath(); + final Path assets = root.resolve(ASSETS); + final boolean caseInsensitive = Files.exists(root.resolve(ASSETS.toUpperCase(Locale.ROOT))); + if (Files.isDirectory(assets)) { + try { + Files.walkFileTree(assets, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) { + final String relative = root.relativize(path).toString().replace(File.separatorChar, '/'); + final String name = caseInsensitive ? relative.toLowerCase(Locale.ROOT) : relative; + if (isPbrName(name)) { + names.add(name); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path path, IOException e) throws IOException { + if (e instanceof NoSuchFileException) { + return FileVisitResult.CONTINUE; + } + throw e; + } + }); + } catch (IOException e) { + throw new IllegalStateException("Unable to index " + file, e); + } + } + return new PackIndex(names, caseInsensitive); + } +} diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java index 5adf9da60..89e818d8d 100644 --- a/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/AtlasPBRLoader.java @@ -14,6 +14,7 @@ import net.coderbot.iris.texture.mipmap.ChannelMipmapGenerator; import net.coderbot.iris.texture.mipmap.CustomMipmapGenerator; import net.coderbot.iris.texture.mipmap.LinearBlendFunction; import net.coderbot.iris.texture.pbr.PBRAtlasTexture; +import net.coderbot.iris.texture.pbr.PBRResourceIndex; import net.coderbot.iris.texture.pbr.PBRSpriteHolder; import net.coderbot.iris.texture.pbr.PBRType; import net.coderbot.iris.texture.pbr.TextureAtlasSpriteExtension; @@ -119,6 +120,10 @@ public class AtlasPBRLoader implements PBRTextureLoader { final ResourceLocation imageLocation = texMap.completeResourceLocation(spriteName, 0); final ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation); + if (!PBRResourceIndex.mayExist(resourceManager, pbrImageLocation)) { + return null; + } + TextureAtlasSprite pbrSprite = null; try { diff --git a/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java b/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java index b9a9afd53..c12b83c6e 100644 --- a/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java +++ b/src/main/java/net/coderbot/iris/texture/pbr/loader/SimplePBRLoader.java @@ -1,5 +1,6 @@ package net.coderbot.iris.texture.pbr.loader; +import net.coderbot.iris.texture.pbr.PBRResourceIndex; import net.coderbot.iris.texture.pbr.PBRType; import net.minecraft.client.renderer.texture.AbstractTexture; import net.minecraft.client.renderer.texture.SimpleTexture; @@ -29,6 +30,10 @@ public class SimplePBRLoader implements PBRTextureLoader { protected AbstractTexture createPBRTexture(ResourceLocation imageLocation, IResourceManager resourceManager, PBRType pbrType) { ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation); + if (!PBRResourceIndex.mayExist(resourceManager, pbrImageLocation)) { + return null; + } + SimpleTexture pbrTexture = new SimpleTexture(pbrImageLocation); try { pbrTexture.loadTexture(resourceManager); diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinTextureManager_ReloadCount.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinTextureManager_ReloadCount.java index 2da91a818..461e96de0 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinTextureManager_ReloadCount.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinTextureManager_ReloadCount.java @@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.mixins.early.shaders; import net.coderbot.iris.Iris; import net.coderbot.iris.texture.format.TextureFormatLoader; +import net.coderbot.iris.texture.pbr.PBRResourceIndex; import net.coderbot.iris.texture.pbr.PBRTextureManager; import net.coderbot.iris.uniforms.CapturedRenderingState; import net.minecraft.client.renderer.texture.TextureManager; @@ -25,6 +26,7 @@ public class MixinTextureManager_ReloadCount { Iris.logger.error("Failed to reload the shader pipeline for a texture format change", e); } PBRTextureManager.INSTANCE.clear(); + PBRResourceIndex.start(resourceManager); CapturedRenderingState.INSTANCE.incrementTextureReloadCount(); } } diff --git a/src/test/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandlerDeferralTest.java b/src/test/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandlerDeferralTest.java new file mode 100644 index 000000000..8dad5fe5a --- /dev/null +++ b/src/test/java/com/prupe/mcpatcher/mal/resource/TexturePackChangeHandlerDeferralTest.java @@ -0,0 +1,212 @@ +package com.prupe.mcpatcher.mal.resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.IResourceManager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; + +import cpw.mods.fml.common.LoadController; +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.LoaderState; + +class TexturePackChangeHandlerDeferralTest { + + private static final class CountingHandler extends TexturePackChangeHandler { + + int before; + int after; + int after2; + + CountingHandler() { + super("counting", 0); + } + + @Override + public void beforeChange() { + before++; + } + + @Override + public void afterChange() { + after++; + } + + @Override + public void afterChange2() { + after2++; + } + } + + private boolean savedFeatures; + private Object savedLoader; + private Object savedMinecraft; + private Object savedResourceList; + private List savedHandlers; + private LoadController controller; + + @BeforeEach + void stubLoader() { + savedFeatures = AngelicaConfig.enableMCPatcherForgeFeatures; + savedLoader = Reflect.getStatic(Loader.class, "instance"); + savedMinecraft = Reflect.getStatic(Minecraft.class, "theMinecraft"); + savedResourceList = Reflect.getStatic(ResourceList.class, "instance"); + final List handlers = Reflect.getStatic(TexturePackChangeHandler.class, "handlers"); + savedHandlers = new ArrayList<>(handlers); + handlers.clear(); + + AngelicaConfig.enableMCPatcherForgeFeatures = true; + controller = Reflect.allocate(LoadController.class); + setState(LoaderState.PREINITIALIZATION); + final Loader loader = Reflect.allocate(Loader.class); + Reflect.setDeclared(Loader.class, loader, "modController", controller); + Reflect.setStatic(Loader.class, "instance", loader); + + final Minecraft minecraft = mock(Minecraft.class); + when(minecraft.getResourceManager()).thenReturn(mock(IResourceManager.class)); + Reflect.setStatic(Minecraft.class, "theMinecraft", minecraft); + + resetGate(); + } + + @AfterEach + void restore() { + final List handlers = Reflect.getStatic(TexturePackChangeHandler.class, "handlers"); + handlers.clear(); + handlers.addAll(savedHandlers); + resetGate(); + Reflect.setStatic(ResourceList.class, "instance", savedResourceList); + Reflect.setStatic(Minecraft.class, "theMinecraft", savedMinecraft); + Reflect.setStatic(Loader.class, "instance", savedLoader); + AngelicaConfig.enableMCPatcherForgeFeatures = savedFeatures; + } + + private void setState(LoaderState state) { + Reflect.setDeclared(LoadController.class, controller, "state", state); + } + + private static void resetGate() { + Reflect.setStatic(TexturePackChangeHandler.class, "loaderAvailable", false); + Reflect.setStatic(TexturePackChangeHandler.class, "deferredDepth", 0); + Reflect.setStatic(TexturePackChangeHandler.class, "recurseDepth", 0); + } + + private static int recurseDepth() { + return Reflect.getStatic(TexturePackChangeHandler.class, "recurseDepth"); + } + + private static int deferredDepth() { + return Reflect.getStatic(TexturePackChangeHandler.class, "deferredDepth"); + } + + private static void reload() { + TexturePackChangeHandler.beforeChange1(); + TexturePackChangeHandler.afterChange1(); + } + + @Test + void beforeAvailableRegistrationAndReloadsDoNothing() { + final ResourceList sentinel = Reflect.allocate(ResourceList.class); + Reflect.setStatic(ResourceList.class, "instance", sentinel); + final CountingHandler handler = new CountingHandler(); + + TexturePackChangeHandler.register(handler); + reload(); + TexturePackChangeHandler.beforeChange1(); + reload(); + TexturePackChangeHandler.afterChange1(); + + assertTrue(TexturePackChangeHandler.isBootDeferred()); + assertEquals(0, handler.before); + assertEquals(0, handler.after); + assertEquals(0, handler.after2); + assertEquals(0, recurseDepth()); + assertEquals(0, deferredDepth()); + assertSame(sentinel, Reflect.getStatic(ResourceList.class, "instance")); + assertTrue(Reflect.>getStatic(TexturePackChangeHandler.class, "handlers").contains(handler)); + } + + @Test + void firstReloadAfterAvailableRunsDeferredHandlersOnce() { + final CountingHandler handler = new CountingHandler(); + TexturePackChangeHandler.register(handler); + setState(LoaderState.AVAILABLE); + + reload(); + + assertEquals(1, handler.before); + assertEquals(1, handler.after); + assertEquals(1, handler.after2); + assertEquals(0, recurseDepth()); + assertNull(Reflect.getStatic(ResourceList.class, "instance")); + } + + @Test + void availableLatchesThroughLaterStates() { + setState(LoaderState.AVAILABLE); + assertFalse(TexturePackChangeHandler.isBootDeferred()); + setState(LoaderState.ERRORED); + assertFalse(TexturePackChangeHandler.isBootDeferred()); + final CountingHandler handler = new CountingHandler(); + reload(); + TexturePackChangeHandler.register(handler); + reload(); + assertEquals(2, handler.before); + assertEquals(2, handler.after); + } + + @Test + void registrationAfterAvailableInitializesImmediately() { + setState(LoaderState.AVAILABLE); + final CountingHandler handler = new CountingHandler(); + TexturePackChangeHandler.register(handler); + assertEquals(1, handler.before); + assertEquals(1, handler.after); + assertEquals(0, handler.after2); + } + + @Test + void disabledFeaturesNeverDefer() { + AngelicaConfig.enableMCPatcherForgeFeatures = false; + final CountingHandler handler = new CountingHandler(); + TexturePackChangeHandler.register(handler); + assertFalse(TexturePackChangeHandler.isBootDeferred()); + assertEquals(1, handler.before); + assertEquals(1, handler.after); + } + + @Test + void bracketOpenedDeferredStaysDeferredAcrossAvailable() { + final CountingHandler handler = new CountingHandler(); + TexturePackChangeHandler.register(handler); + TexturePackChangeHandler.beforeChange1(); + setState(LoaderState.AVAILABLE); + reload(); + TexturePackChangeHandler.afterChange1(); + + assertEquals(0, handler.before); + assertEquals(0, handler.after); + assertEquals(0, deferredDepth()); + assertEquals(0, recurseDepth()); + + reload(); + assertEquals(1, handler.before); + assertEquals(1, handler.after); + assertEquals(0, recurseDepth()); + } +} diff --git a/src/test/java/net/coderbot/iris/IrisProgramSetWarmupTest.java b/src/test/java/net/coderbot/iris/IrisProgramSetWarmupTest.java new file mode 100644 index 000000000..37646eb63 --- /dev/null +++ b/src/test/java/net/coderbot/iris/IrisProgramSetWarmupTest.java @@ -0,0 +1,57 @@ +package net.coderbot.iris; + +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class IrisProgramSetWarmupTest { + @AfterEach + void reset() { + Reflect.setStatic(Iris.class, "programSetWarmup", null); + } + + private static void await() { + Reflect.invokeStatic(Iris.class, "awaitProgramSetWarmup", new Class[0]); + } + + @Test + void noPendingWarmupIsNoOp() { + Reflect.setStatic(Iris.class, "programSetWarmup", null); + await(); + assertNull(Reflect.getStatic(Iris.class, "programSetWarmup")); + } + + @Test + void blocksUntilWarmupCompletes() throws Exception { + final CompletableFuture warmup = new CompletableFuture<>(); + Reflect.setStatic(Iris.class, "programSetWarmup", warmup); + final CountDownLatch done = new CountDownLatch(1); + final Thread joiner = new Thread(() -> { + await(); + done.countDown(); + }); + joiner.start(); + assertFalse(done.await(200, TimeUnit.MILLISECONDS)); + warmup.complete(null); + assertTrue(done.await(5, TimeUnit.SECONDS)); + joiner.join(); + assertNull(Reflect.getStatic(Iris.class, "programSetWarmup")); + } + + @Test + void failedWarmupIsSwallowedAndCleared() { + final CompletableFuture warmup = new CompletableFuture<>(); + warmup.completeExceptionally(new IllegalStateException("boom")); + Reflect.setStatic(Iris.class, "programSetWarmup", warmup); + await(); + assertNull(Reflect.getStatic(Iris.class, "programSetWarmup")); + } +} diff --git a/src/test/java/net/coderbot/iris/texture/pbr/PBRResourceIndexTest.java b/src/test/java/net/coderbot/iris/texture/pbr/PBRResourceIndexTest.java new file mode 100644 index 000000000..8efa74c1a --- /dev/null +++ b/src/test/java/net/coderbot/iris/texture/pbr/PBRResourceIndexTest.java @@ -0,0 +1,282 @@ +package net.coderbot.iris.texture.pbr; + +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import net.minecraft.client.resources.FallbackResourceManager; +import net.minecraft.client.resources.FileResourcePack; +import net.minecraft.client.resources.FolderResourcePack; +import net.minecraft.client.resources.IResourcePack; +import net.minecraft.client.resources.SimpleReloadableResourceManager; +import net.minecraft.client.resources.data.IMetadataSection; +import net.minecraft.client.resources.data.IMetadataSerializer; +import net.minecraft.util.ResourceLocation; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PBRResourceIndexTest { + + @TempDir + Path tmp; + + private final List zipPacks = new ArrayList<>(); + + @AfterEach + void cleanup() throws IOException { + Reflect.invokeStatic(PBRResourceIndex.class, "drop", new Class[0]); + for (FileResourcePack pack : zipPacks) { + pack.close(); + } + } + + private static final class StubPack implements IResourcePack { + + private final Set claims = new HashSet<>(); + + StubPack(ResourceLocation... locations) { + Collections.addAll(claims, locations); + } + + @Override + public InputStream getInputStream(ResourceLocation location) throws IOException { + return new ByteArrayInputStream("stub".getBytes(StandardCharsets.UTF_8)); + } + + @Override + public boolean resourceExists(ResourceLocation location) { + return claims.contains(location); + } + + @Override + public Set getResourceDomains() { + return Collections.emptySet(); + } + + @Override + public IMetadataSection getPackMetadata(IMetadataSerializer serializer, String name) { + return null; + } + + @Override + public BufferedImage getPackImage() { + return null; + } + + @Override + public String getPackName() { + return "stub"; + } + } + + private FileResourcePack zip(String name, String... entries) throws IOException { + final File file = tmp.resolve(name).toFile(); + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file))) { + for (String entry : entries) { + zos.putNextEntry(new ZipEntry(entry)); + if (!entry.endsWith("/")) { + zos.write(entry.getBytes(StandardCharsets.UTF_8)); + } + zos.closeEntry(); + } + } + final FileResourcePack pack = new FileResourcePack(file); + zipPacks.add(pack); + return pack; + } + + private FolderResourcePack folder(String name, String... files) throws IOException { + final Path root = tmp.resolve(name); + for (String file : files) { + final Path target = root.resolve(file); + Files.createDirectories(target.getParent()); + Files.write(target, file.getBytes(StandardCharsets.UTF_8)); + } + Files.createDirectories(root); + return new FolderResourcePack(root.toFile()); + } + + private static FallbackResourceManager domain(IMetadataSerializer serializer, IResourcePack... packs) { + final FallbackResourceManager manager = new FallbackResourceManager(serializer); + for (IResourcePack pack : packs) { + manager.resourcePacks.add(pack); + } + return manager; + } + + private static boolean loads(SimpleReloadableResourceManager manager, ResourceLocation location) { + try { + manager.getResource(location).getInputStream().close(); + return true; + } catch (IOException e) { + return false; + } + } + + private static void assertAgrees(SimpleReloadableResourceManager manager, ResourceLocation location) { + final boolean loaded = loads(manager, location); + assertEquals(loaded, PBRResourceIndex.mayExist(manager, location), location.toString()); + } + + private SimpleReloadableResourceManager build(IMetadataSerializer serializer) throws IOException { + final ResourceLocation stubbed = new ResourceLocation("minecraft", "textures/blocks/stub_n.png"); + final ResourceLocation stubbedShadowed = new ResourceLocation("minecraft", "textures/blocks/lowzip_s.png"); + final FileResourcePack low = zip( + "low.zip", + "assets/minecraft/textures/blocks/lowzip_n.png", + "assets/minecraft/textures/blocks/lowzip_s.png", + "assets/minecraft/textures/blocks/both_n.png", + "assets/minecraft/textures/blocks/dirlike_n.png/", + "assets/minecraft/textures/blocks/plain.png", + "assets/minecraft/textures/blocks/notpbr_x.png", + "assets/minecraft/textures/blocks/tail_n.png.bak", + "assets/other/textures/blocks/other_n.png", + "pack.mcmeta"); + final FolderResourcePack mid = folder( + "mid", + "assets/minecraft/textures/blocks/folder_s.png", + "assets/minecraft/textures/blocks/both_n.png", + "assets/minecraft/textures/blocks/Mixed_n.png", + "assets/minecraft/textures/blocks/deep/er/nested_n.png", + "assets/other/textures/blocks/otherfolder_s.png"); + final StubPack high = new StubPack(stubbed, stubbedShadowed); + + final SimpleReloadableResourceManager manager = new SimpleReloadableResourceManager(serializer); + manager.domainResourceManagers.put("minecraft", domain(serializer, low, mid, high)); + manager.domainResourceManagers.put("other", domain(serializer, low, mid)); + manager.domainResourceManagers.put("stubonly", domain(serializer, new StubPack(new ResourceLocation("stubonly", "textures/x_n.png")))); + return manager; + } + + @Test + void agreesWithGetResourceAcrossPackTypesAndDomains() throws Exception { + final IMetadataSerializer serializer = new IMetadataSerializer(); + final SimpleReloadableResourceManager manager = build(serializer); + PBRResourceIndex.start(manager); + + final String[][] locations = { + { "minecraft", "textures/blocks/lowzip_n.png" }, + { "minecraft", "textures/blocks/lowzip_s.png" }, + { "minecraft", "textures/blocks/both_n.png" }, + { "minecraft", "textures/blocks/both_s.png" }, + { "minecraft", "textures/blocks/dirlike_n.png" }, + { "minecraft", "textures/blocks/folder_s.png" }, + { "minecraft", "textures/blocks/folder_n.png" }, + { "minecraft", "textures/blocks/Mixed_n.png" }, + { "minecraft", "textures/blocks/mixed_n.png" }, + { "minecraft", "textures/blocks/MIXED_n.png" }, + { "minecraft", "textures/blocks/deep/er/nested_n.png" }, + { "minecraft", "textures/blocks/deep/nested_n.png" }, + { "minecraft", "textures/blocks/stub_n.png" }, + { "minecraft", "textures/blocks/stub_s.png" }, + { "minecraft", "textures/blocks/absent_n.png" }, + { "minecraft", "textures/blocks/absent_s.png" }, + { "minecraft", "textures/blocks/tail_n.png" }, + { "other", "textures/blocks/other_n.png" }, + { "other", "textures/blocks/otherfolder_s.png" }, + { "other", "textures/blocks/stub_n.png" }, + { "other", "textures/blocks/lowzip_n.png" }, + { "stubonly", "textures/x_n.png" }, + { "stubonly", "textures/y_n.png" }, + { "ghost", "textures/blocks/lowzip_n.png" }, + }; + for (String[] location : locations) { + assertAgrees(manager, new ResourceLocation(location[0], location[1])); + } + } + + @Test + void presentLocationsAreReportedPresent() throws Exception { + final SimpleReloadableResourceManager manager = build(new IMetadataSerializer()); + PBRResourceIndex.start(manager); + + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/lowzip_n.png"))); + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/folder_s.png"))); + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/stub_n.png"))); + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("other", "textures/blocks/otherfolder_s.png"))); + assertFalse(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/absent_n.png"))); + assertFalse(PBRResourceIndex.mayExist(manager, new ResourceLocation("other", "textures/blocks/stub_n.png"))); + assertFalse(PBRResourceIndex.mayExist(manager, new ResourceLocation("ghost", "textures/blocks/lowzip_n.png"))); + } + + @Test + void nonPbrLocationsAreNeverRuledOut() throws Exception { + final SimpleReloadableResourceManager manager = build(new IMetadataSerializer()); + PBRResourceIndex.start(manager); + + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/plain.png"))); + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/nothing.png"))); + } + + @Test + void lazyBuildWithoutStartAgrees() throws Exception { + final SimpleReloadableResourceManager manager = build(new IMetadataSerializer()); + + assertAgrees(manager, new ResourceLocation("minecraft", "textures/blocks/lowzip_n.png")); + assertAgrees(manager, new ResourceLocation("minecraft", "textures/blocks/absent_n.png")); + } + + @Test + void startAfterReloadSeesNewPacks() throws Exception { + final IMetadataSerializer serializer = new IMetadataSerializer(); + final SimpleReloadableResourceManager manager = build(serializer); + PBRResourceIndex.start(manager); + final ResourceLocation added = new ResourceLocation("minecraft", "textures/blocks/added_n.png"); + assertFalse(PBRResourceIndex.mayExist(manager, added)); + + manager.domainResourceManagers.get("minecraft").resourcePacks.add(zip("added.zip", "assets/minecraft/textures/blocks/added_n.png")); + PBRResourceIndex.start(manager); + + assertTrue(PBRResourceIndex.mayExist(manager, added)); + assertAgrees(manager, added); + } + + @Test + void buildFailureDisablesIndex() throws Exception { + final IMetadataSerializer serializer = new IMetadataSerializer(); + final SimpleReloadableResourceManager manager = new SimpleReloadableResourceManager(serializer); + final FileResourcePack missing = new FileResourcePack(tmp.resolve("missing.zip").toFile()); + manager.domainResourceManagers.put("minecraft", domain(serializer, missing)); + PBRResourceIndex.start(manager); + + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/absent_n.png"))); + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("ghost", "textures/blocks/absent_n.png"))); + } + + @Test + void zipScanSharesThePacksOpenJar() throws Exception { + final IMetadataSerializer serializer = new IMetadataSerializer(); + final SimpleReloadableResourceManager manager = new SimpleReloadableResourceManager(serializer); + final FileResourcePack pack = zip("shared.zip", "assets/minecraft/textures/blocks/shared_n.png"); + manager.domainResourceManagers.put("minecraft", domain(serializer, pack)); + PBRResourceIndex.start(manager); + + assertTrue(PBRResourceIndex.mayExist(manager, new ResourceLocation("minecraft", "textures/blocks/shared_n.png"))); + final ZipFile jar = pack.getResourcePackZipFile(); + assertSame(jar, pack.getResourcePackZipFile()); + assertNotNull(jar.getEntry("assets/minecraft/textures/blocks/shared_n.png")); + } +}