Speed up boot (#2190)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-02 20:33:42 -07:00
1 parent 12fa08ab46
commit 38b1d9a0c3
17 files changed
+937 -23

No files matched your search

@@ -49,11 +49,11 @@ public class GlslTransformUtils {
private static final Map<Integer, List<ReservedWordRename>> 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")
)
);
@@ -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() {
@@ -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<Pattern> 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));
}
}
}
@@ -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();
@@ -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
@@ -56,7 +56,7 @@ public class CITUtils {
private static final List<Enchantment> allItemEnchantments = new ArrayList<>();
private static final Map<Item, List<ArmorOverride>> armors = new IdentityHashMap<>();
static boolean useGlint;
static boolean useGlint = true;
private static EnchantmentList armorMatches;
private static int armorMatchIndex;
@@ -353,6 +353,7 @@ public class CTMUtils {
}
private static void setBlankResourceWithoutLock() {
if (tileLoader == null) return;
RenderBlocksUtils.blankIcon = tileLoader.getIcon(RenderPassAPI.instance.getBlankResource());
}
@@ -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<TexturePackChangeHandler> {
private static final MCLogger logger = MCLogger.getLogger(MCLogger.Category.TEXTURE_PACK);
@@ -18,6 +22,8 @@ abstract public class TexturePackChangeHandler implements Comparable<TexturePack
private static boolean initialized;
private static long startTime;
private static long startMem;
private static boolean loaderAvailable;
private static int deferredDepth;
private boolean updateNeeded;
@@ -50,14 +56,29 @@ abstract public class TexturePackChangeHandler implements Comparable<TexturePack
return this.order - that.order;
}
public static boolean isBootDeferred() {
if (loaderAvailable) {
return false;
}
if (!AngelicaConfig.enableMCPatcherForgeFeatures || Loader.instance().hasReachedState(LoaderState.AVAILABLE)) {
loaderAvailable = true;
return false;
}
return true;
}
public static void register(TexturePackChangeHandler handler) {
if (handler != null) {
try {
logger.info("initializing %s...", handler.name);
handler.initialize();
} catch (Exception e) {
e.printStackTrace();
logger.severe("%s initialization failed", handler.name);
if (isBootDeferred()) {
logger.fine("deferring %s initialization until mod loading completes", handler.name);
} else {
try {
logger.info("initializing %s...", handler.name);
handler.initialize();
} catch (Exception e) {
e.printStackTrace();
logger.severe("%s initialization failed", handler.name);
}
}
handlers.add(handler);
logger.fine("registered texture pack handler %s, priority %d", handler.name, handler.order);
@@ -81,6 +102,10 @@ abstract public class TexturePackChangeHandler implements Comparable<TexturePack
}
public static void beforeChange1() {
if (deferredDepth > 0 || isBootDeferred()) {
deferredDepth++;
return;
}
logger.finer("beforeChange1 depth %d", recurseDepth);
if (recurseDepth++ > 0) {
return;
@@ -115,6 +140,10 @@ abstract public class TexturePackChangeHandler implements Comparable<TexturePack
}
public static void afterChange1() {
if (deferredDepth > 0) {
deferredDepth--;
return;
}
logger.finer("afterChange1 depth %d", recurseDepth - 1);
if (--recurseDepth > 0) {
return;
@@ -92,6 +92,9 @@ public class TileLoader {
}
public static void registerIcons(TextureMap textureMap, String mapName, Map<String, TextureAtlasSprite> map) {
if (TexturePackChangeHandler.isBootDeferred()) {
return;
}
mapName = mapName.replaceFirst("/$", "");
logger.fine("before registerIcons(%s) %d icons", mapName, map.size());
if (!changeHandlerCalled) {
+30 -11
View File
@@ -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<Void> 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 <T> CompletableFuture<T> submitTracked(Supplier<T> 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<Void> 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();
@@ -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<Map<IResourcePack, PackIndex>> pending;
private static Map<IResourcePack, PackIndex> built;
private static boolean disabled;
private static boolean warned;
private PBRResourceIndex() {
}
private record PackIndex(Set<String> 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<ScanTarget> 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<IResourcePack> 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<ScanTarget> collectTargets(SimpleReloadableResourceManager manager) {
final Set<IResourcePack> seen = Collections.newSetFromMap(new IdentityHashMap<>());
final List<ScanTarget> 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<IResourcePack, PackIndex> scan(List<ScanTarget> targets) {
final Map<IResourcePack, PackIndex> 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<String> names = new HashSet<>();
final Enumeration<? extends ZipEntry> 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<String> 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);
}
}
@@ -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<TextureMap> {
final ResourceLocation imageLocation = texMap.completeResourceLocation(spriteName, 0);
final ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation);
if (!PBRResourceIndex.mayExist(resourceManager, pbrImageLocation)) {
return null;
}
TextureAtlasSprite pbrSprite = null;
try {
@@ -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<SimpleTexture> {
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);
@@ -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();
}
}
@@ -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<TexturePackChangeHandler> 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<TexturePackChangeHandler> 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<TexturePackChangeHandler> 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.<List<TexturePackChangeHandler>>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());
}
}
@@ -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<Void> 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<Void> warmup = new CompletableFuture<>();
warmup.completeExceptionally(new IllegalStateException("boom"));
Reflect.setStatic(Iris.class, "programSetWarmup", warmup);
await();
assertNull(Reflect.getStatic(Iris.class, "programSetWarmup"));
}
}
@@ -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<FileResourcePack> 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<ResourceLocation> 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<String> 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"));
}
}