Files
Angelica/src/main/java/net/coderbot/iris/Iris.java
T

1008 lines
40 KiB
Java

package net.coderbot.iris;
import com.google.common.base.Throwables;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
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.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import cpw.mods.fml.client.registry.ClientRegistry;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.InputEvent;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import lombok.Getter;
import net.coderbot.iris.block_rendering.BlockMaterialMapping;
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
import net.coderbot.iris.celeritas.IrisCeleritasShaderProvider;
import net.coderbot.iris.compat.dh.DHCompat;
import net.coderbot.iris.config.IrisConfig;
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability;
import net.coderbot.iris.gl.shader.StandardMacros;
import net.coderbot.iris.gui.screen.ShaderPackScreen;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.pipeline.FixedFunctionWorldRenderingPipeline;
import net.coderbot.iris.pipeline.PipelineManager;
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
import net.coderbot.iris.pipeline.transform.ShaderTransformer;
import net.coderbot.iris.pipeline.transform.TransformPatcher;
import net.coderbot.iris.shaderpack.OptionalBoolean;
import net.coderbot.iris.shaderpack.ProgramSet;
import net.coderbot.iris.shaderpack.ShaderPack;
import net.coderbot.iris.shaderpack.discovery.ShaderpackDirectoryManager;
import net.coderbot.iris.shaderpack.option.OptionSet;
import net.coderbot.iris.shaderpack.option.Profile;
import net.coderbot.iris.shaderpack.option.values.MutableOptionValues;
import net.coderbot.iris.shaderpack.option.values.OptionValues;
import net.coderbot.iris.texture.pbr.PBRTextureManager;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.coderbot.iris.uniforms.PerFrameUniformBlockHarvester;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.EnumChatFormatting;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.input.Keyboard;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.zip.ZipException;
public class Iris {
public final boolean isDevelopmentEnvironment;
/**
* The user-facing name of the mod. Moved into a constant to facilitate easy branding changes (for forks). You'll still need to change this separately in
* mixin plugin classes & the language files.
*/
public static final String MODNAME = "AngelicaShaders";
public static final IrisLogging logger = new IrisLogging(MODNAME);
// Cached at class load - config must be loaded before Iris. Do not change at runtime.
public static final boolean enabled = AngelicaConfig.enableIris;
public static final boolean hardcodedCustomUniforms = AngelicaConfig.enableHardcodedCustomUniforms;
private static Path shaderpacksDirectory;
private static ShaderpackDirectoryManager shaderpacksDirectoryManager;
private static ShaderPack currentPack;
@Getter
private static String currentPackName;
@Getter
private static boolean initialized;
@Getter
private static int shaderPackLoadId = 0;
private static PipelineManager pipelineManager;
@Getter
private static IrisConfig irisConfig;
private static FileSystem zipFileSystem;
@Getter
private static final Map<String, String> shaderPackOptionQueue = new HashMap<>();
// Flag variable used when reloading
// Used in favor of queueDefaultShaderPackOptionValues() for resetting as the
// behavior is more concrete and therefore is more likely to repair a user's issues
private static boolean resetShaderPackOptions = false;
private static boolean loadShaderPackWhenPossible = false;
private static String IRIS_VERSION;
@Getter
private static boolean fallback;
public static boolean loadedIncompatiblePack() {
return DHCompat.lastPackIncompatible();
}
public static void loadShaderpackWhenPossible() {
loadShaderPackWhenPossible = true;
}
public static void tryLoadShaderpackWhenPossible() {
if (loadShaderPackWhenPossible) {
loadShaderPackWhenPossible = false;
try {
reload();
} catch (IOException e) {
logger.error("Error during deferred shader reload", e);
}
}
}
/**
* Lazy executor for parallelizing shader transformations during shader pack loading.
* Creates threads on demand and shuts down after a period of inactivity.
*/
public static final class ShaderTransformExecutor {
private static final long IDLE_TIMEOUT_SECONDS = 120;
private static final int THREAD_COUNT = Math.max(2, Math.min(12, Runtime.getRuntime().availableProcessors() / 2));
private static final Object lock = new Object();
private static ExecutorService executor;
private static ScheduledExecutorService scheduler;
private static volatile long lastActivityTime;
private static final AtomicInteger inFlight = new AtomicInteger(0);
private static boolean idleCheckScheduled;
private static final ThreadLocal<Boolean> ON_WORKER = ThreadLocal.withInitial(() -> Boolean.FALSE);
private ShaderTransformExecutor() {}
public static boolean isOnWorker() {
return ON_WORKER.get();
}
private static void noteActivity() {
lastActivityTime = System.nanoTime();
}
public static ExecutorService get() {
synchronized (lock) {
noteActivity();
if (executor != null && !executor.isShutdown()) {
return executor;
}
final AtomicInteger threadCounter = new AtomicInteger();
final ThreadFactory factory = r -> {
final Thread t = new Thread(r, "Shader-Transform-" + threadCounter.getAndIncrement());
t.setDaemon(true);
return t;
};
final ThreadPoolExecutor tpe = new ThreadPoolExecutor(
THREAD_COUNT,
THREAD_COUNT,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
factory);
tpe.prestartAllCoreThreads();
executor = tpe;
logger.debug("Created shader transform executor with " + THREAD_COUNT + " prestarted threads");
if (scheduler == null || scheduler.isShutdown()) {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Shader-Transform-Scheduler");
t.setDaemon(true);
return t;
});
}
if (!idleCheckScheduled) {
idleCheckScheduled = true;
scheduleIdleCheck(IDLE_TIMEOUT_SECONDS);
}
return executor;
}
}
private static void scheduleIdleCheck(long delaySeconds) {
try {
scheduler.schedule(ShaderTransformExecutor::checkIdleShutdown, delaySeconds, TimeUnit.SECONDS);
} catch (Exception e) {
// If scheduling fails, reset the flag so a future get() can try again
idleCheckScheduled = false;
logger.warn("Failed to schedule idle check", e);
}
}
/** Idempotent: ensures the pool is created. {@link #get()} prestarts all workers. */
public static void prepare() {
get();
}
/**
* Warm up the threadpool by running a representative shader transformation.
*/
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, new InputAvailability(true, true));
}).get();
} catch (Exception e) {
logger.warn("Warmup failed", e);
}
}
public static <T> CompletableFuture<T> submitTracked(Supplier<T> supplier) {
Objects.requireNonNull(supplier);
noteActivity();
inFlight.incrementAndGet();
try {
return CompletableFuture.supplyAsync(() -> {
ON_WORKER.set(Boolean.TRUE);
try {
return supplier.get();
} finally {
ON_WORKER.set(Boolean.FALSE);
noteActivity();
inFlight.decrementAndGet();
}
}, get());
} catch (Exception e) {
inFlight.decrementAndGet();
throw e;
}
}
public static CompletableFuture<Void> submitTracked(Runnable runnable) {
Objects.requireNonNull(runnable);
noteActivity();
inFlight.incrementAndGet();
try {
return CompletableFuture.runAsync(() -> {
ON_WORKER.set(Boolean.TRUE);
try {
runnable.run();
} finally {
ON_WORKER.set(Boolean.FALSE);
noteActivity();
inFlight.decrementAndGet();
}
}, get());
} catch (Exception e) {
inFlight.decrementAndGet();
throw e;
}
}
private static void checkIdleShutdown() {
synchronized (lock) {
final ExecutorService current = executor;
if (current == null || current.isShutdown()) {
// Executor already gone, shut down scheduler
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
scheduler = null;
}
idleCheckScheduled = false;
return;
}
final long idleNanos = System.nanoTime() - lastActivityTime;
final long idleSeconds = TimeUnit.NANOSECONDS.toSeconds(idleNanos);
if (idleSeconds >= IDLE_TIMEOUT_SECONDS && inFlight.get() == 0) {
logger.debug("Shutting down idle shader transform executor after " + idleSeconds + " seconds");
current.shutdown();
executor = null;
scheduler.shutdown();
scheduler = null;
idleCheckScheduled = false;
// Clear transformation caches - no longer needed after loading
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
GlslVulkanPreprocess.clearCache();
if (RenderSystem.isGLES() || BackendManager.RENDER_BACKEND.isSDLGPU()) SpirvCompiler.clearCache();
SDLGPUGate.clearShaderPrewarmCache();
} else {
// Still active (or in-flight), schedule another check.
final long remainingSeconds = Math.max(1, IDLE_TIMEOUT_SECONDS - idleSeconds + 1);
scheduleIdleCheck(remainingSeconds);
}
}
}
}
private static KeyBinding reloadKeybind;
private static KeyBinding toggleShadersKeybind;
private static KeyBinding shaderpackScreenKeybind;
public static Iris INSTANCE = new Iris();
private Iris() {
// Guard against null blackboard in test environments
final Object deobfEnv = Launch.blackboard != null ? Launch.blackboard.get("fml.deobfuscatedEnvironment") : null;
isDevelopmentEnvironment = deobfEnv != null && (boolean) deobfEnv;
}
@SubscribeEvent
public void onKeypress(InputEvent.KeyInputEvent event) {
if (reloadKeybind.isPressed()) {
final Minecraft mc = Minecraft.getMinecraft();
try {
reload();
if (mc.thePlayer != null) mc.thePlayer.addChatMessage(new ChatComponentText(I18n.format("iris.shaders.reloaded")));
} catch (Exception e) {
logger.error("Error while reloading Shaders for Iris!", e);
if (mc.thePlayer != null) mc.thePlayer.addChatMessage(new ChatComponentText(I18n.format("iris.shaders.reloaded.failure", Throwables.getRootCause(e).getMessage())));
}
} else if (toggleShadersKeybind.isPressed()) {
final Minecraft mc = Minecraft.getMinecraft();
try {
toggleShaders(mc, !irisConfig.areShadersEnabled());
} catch (Exception e) {
logger.error("Error while toggling shaders!", e);
if (mc.thePlayer != null) mc.thePlayer.addChatMessage(new ChatComponentText(I18n.format("iris.shaders.toggled.failure", Throwables.getRootCause(e).getMessage())));
setShadersDisabled();
fallback = true;
}
} else if (shaderpackScreenKeybind.isPressed()) {
final Minecraft mc = Minecraft.getMinecraft();
mc.displayGuiScreen(new ShaderPackScreen(null));
}
}
@SubscribeEvent
public void keyUp(InputEvent.KeyInputEvent event) {
final int key = Keyboard.getEventKey();
final boolean released = !Keyboard.getEventKeyState();
if (Minecraft.getMinecraft().gameSettings.showDebugInfo && GuiScreen.isShiftKeyDown() && GuiScreen.isCtrlKeyDown() && released) {
if (key == Keyboard.KEY_N) {
ClientProxy.animationsMode.next();
}
}
}
/**
* Called very early on in Minecraft initialization. At this point we *cannot* safely access OpenGL, but we can do some very basic setup, config loading,
* and environment checks.
*
* <p>This is roughly equivalent to Fabric Loader's ClientModInitializer#onInitializeClient entrypoint, except
* it's entirely cross platform & we get to decide its exact semantics.</p>
*
* <p>This is called right before options are loaded, so we can add key bindings here.</p>
*/
public void onEarlyInitialize() {
DHCompat.run();
try {
if (!Files.exists(getShaderpacksDirectory())) {
Files.createDirectories(getShaderpacksDirectory());
}
} catch (IOException e) {
logger.warn("Failed to create the shaderpacks directory!");
logger.warn("", e);
}
irisConfig = new IrisConfig(Minecraft.getMinecraft().mcDataDir.toPath().resolve("config").resolve("shaders.properties"));
try {
irisConfig.initialize();
} catch (IOException e) {
logger.error("Failed to initialize Angelica configuration, default values will be used instead");
logger.error("", e);
}
initialized = true;
}
/**
* Called once RenderSystem#initRenderer has completed. This means that we can safely access OpenGL.
*/
public static void onRenderSystemInit() {
if (!initialized) {
Iris.logger.warn("Iris::onRenderSystemInit was called, but Iris::onEarlyInitialize was not called."
+ " Trying to avoid a crash but this is an odd state.");
return;
}
IrisShaderProviderHolder.setProvider(new IrisCeleritasShaderProvider());
// Warm up the threadpool so shader transformations are faster when we need them
ShaderTransformExecutor.warmup();
// Initialize version hoisting pattern based on GL capabilities
ShaderTransformer.init();
PBRTextureManager.INSTANCE.init();
boolean isDHLoaded;
try {
Class.forName("com.seibel.distanthorizons.DistantHorizonsTweaker");
isDHLoaded = true;
}
catch (Exception e) {
isDHLoaded = false;
}
// When DH is present, defer shaderpack loading until its init callback has run.
if (!isDHLoaded) {
loadShaderpack();
}
}
/**
* Called when the title screen is initialized for the first time.
*/
public static void onLoadingComplete() {
if (!initialized) {
Iris.logger.warn("Iris::onLoadingComplete was called, but Iris::onEarlyInitialize was not called."
+ " Trying to avoid a crash but this is an odd state.");
return;
}
// Initialize the pipeline now so that we don't increase world loading time. Just going to guess that
// the player is in the overworld.
// See: https://github.com/IrisShaders/Iris/issues/323
lastDimensionName = "Overworld";
Iris.getPipelineManager().preparePipeline("Overworld");
BlockRenderingSettings.INSTANCE.reloadRendererIfRequired();
}
public static void toggleShaders(Minecraft minecraft, boolean enabled) throws IOException {
irisConfig.setShadersEnabled(enabled);
irisConfig.save();
reload();
if (minecraft.thePlayer != null) {
minecraft.thePlayer.addChatMessage(new ChatComponentText(enabled ? I18n.format("iris.shaders.toggled", currentPackName) : I18n.format("iris.shaders.disabled")));
}
}
/**
* Toggles shader debug mode. When enabled, patched shader sources are dumped to disk (see
* {@link net.coderbot.iris.pipeline.PatchedShaderPrinter}) and logging is more verbose.
*/
public static void setDebug(boolean enable) {
try {
irisConfig.setDebugEnabled(enable);
irisConfig.save();
reload();
} catch (IOException e) {
logger.error("Failed to " + (enable ? "enable" : "disable") + " debug mode", e);
}
logger.info("Debug functionality is " + (enable ? "enabled, logging will be more verbose!" : "disabled."));
}
/**
* Toggles whether unrecognized shader packs may load, exposing the {@code ALLOWS_UNKNOWN_SHADERS} macro.
*/
public static void setAllowUnknownShaders(boolean allow) {
try {
irisConfig.setUnknown(allow);
reload();
} catch (IOException e) {
logger.error("Failed to " + (allow ? "allow" : "disallow") + " unknown shaders", e);
}
}
public static void loadShaderpack() {
if (irisConfig == null) {
if (!initialized) {
throw new IllegalStateException("Iris::loadShaderpack was called, but Iris::onInitializeClient wasn't" + " called yet. How did this happen?");
} else {
throw new NullPointerException("Iris.irisConfig was null unexpectedly");
}
}
if (!irisConfig.areShadersEnabled()) {
logger.info("Shaders are disabled because enableShaders is set to false in shaders.properties");
setShadersDisabled();
return;
}
// Attempt to load an external shaderpack if it is available
final Optional<String> externalName = irisConfig.getShaderPackName();
if (externalName.isEmpty()) {
logger.info("Shaders are disabled because no valid shaderpack is selected");
setShadersDisabled();
return;
}
if (!loadExternalShaderpack(externalName.get())) {
logger.warn("Falling back to normal rendering without shaders because the shaderpack could not be loaded");
setShadersDisabled();
fallback = true;
}
}
public static void warmupShaderTransforms() {
if (currentPack == 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);
} catch (Throwable t) {
logger.warn("Early shader transform warmup failed; transforms will run at pipeline creation", t);
}
}
private static boolean loadExternalShaderpack(String name) {
final Path shaderPackRoot;
final Path shaderPackConfigTxt;
try {
shaderPackRoot = getShaderpacksDirectory().resolve(name);
shaderPackConfigTxt = getShaderpacksDirectory().resolve(name + ".txt");
} catch (InvalidPathException e) {
logger.error("Failed to load the shaderpack \"{}\" because it contains invalid characters in its path", name);
return false;
}
final Path shaderPackPath;
if (shaderPackRoot.toString().endsWith(".zip")) {
final Optional<Path> optionalPath;
try {
optionalPath = loadExternalZipShaderpack(shaderPackRoot);
} catch (FileSystemNotFoundException | NoSuchFileException e) {
logger.error("Failed to load the shaderpack \"{}\" because it does not exist in your shaderpacks folder!", name);
return false;
} catch (ZipException e) {
logger.error("The shaderpack \"{}\" appears to be corrupted, please try downloading it again!", name);
return false;
} catch (IOException e) {
logger.error("Failed to load the shaderpack \"{}\"!", name);
logger.error("", e);
return false;
}
if (optionalPath.isPresent()) {
shaderPackPath = optionalPath.get();
} else {
logger.error("Could not load the shaderpack \"{}\" because it appears to lack a \"shaders\" directory", name);
return false;
}
} else {
if (!Files.exists(shaderPackRoot)) {
logger.error("Failed to load the shaderpack \"{}\" because it does not exist!", name);
return false;
}
// If it's a folder-based shaderpack, just use the shaders subdirectory
shaderPackPath = shaderPackRoot.resolve("shaders");
if (!Files.exists(shaderPackPath)) {
logger.error("Could not load the shaderpack \"{}\" because it appears to lack a \"shaders\" directory", name);
return false;
}
}
@SuppressWarnings("unchecked")
final Map<String, String> changedConfigs = tryReadConfigProperties(shaderPackConfigTxt).map(properties -> (Map<String, String>) (Map<?, ?>) properties)
.orElse(new HashMap<>());
changedConfigs.putAll(shaderPackOptionQueue);
clearShaderPackOptionQueue();
if (resetShaderPackOptions) {
changedConfigs.clear();
}
resetShaderPackOptions = false;
try {
currentPack = new ShaderPack(shaderPackPath, changedConfigs, StandardMacros.createStandardEnvironmentDefines());
final MutableOptionValues changedConfigsValues = currentPack.getShaderPackOptions().getOptionValues().mutableCopy();
// Store changed values from those currently in use by the shader pack
final Properties configsToSave = new Properties();
changedConfigsValues.getBooleanValues().forEach((k, v) -> configsToSave.setProperty(k, Boolean.toString(v)));
changedConfigsValues.getStringValues().forEach(configsToSave::setProperty);
tryUpdateConfigPropertiesFile(shaderPackConfigTxt, configsToSave);
} catch (Exception e) {
logger.error("Failed to load the shaderpack \"{}\"!", name);
logger.error("", e);
return false;
}
fallback = false;
currentPackName = name;
logger.info("Using shaderpack: " + name);
return true;
}
private static Optional<Path> loadExternalZipShaderpack(Path shaderpackPath) throws IOException {
final FileSystem zipSystem = FileSystems.newFileSystem(shaderpackPath, Iris.class.getClassLoader());
zipFileSystem = zipSystem;
// Should only be one root directory for a zip shaderpack
final Path root = zipSystem.getRootDirectories().iterator().next();
final Path potentialShaderDir = zipSystem.getPath("shaders");
// If the shaders dir was immediately found return it
// Otherwise, manually search through each directory path until it ends with "shaders"
if (Files.exists(potentialShaderDir)) {
return Optional.of(potentialShaderDir);
}
// Sometimes shaderpacks have their shaders directory within another folder in the shaderpack
// For example Sildurs-Vibrant-Shaders.zip/shaders
// While other packs have Trippy-Shaderpack-master.zip/Trippy-Shaderpack-master/shaders
// This makes it hard to determine what is the actual shaders dir
try (Stream<Path> stream = Files.walk(root)) {
return stream.filter(Files::isDirectory).filter(path -> path.endsWith("shaders")).findFirst();
}
}
private static void setShadersDisabled() {
currentPack = null;
fallback = false;
currentPackName = "(off)";
logger.info("Shaders are disabled");
}
private static Optional<Properties> tryReadConfigProperties(Path path) {
final Properties properties = new Properties();
if (Files.exists(path)) {
try (InputStream is = Files.newInputStream(path)) {
// NB: config properties are specified to be encoded with ISO-8859-1 by OptiFine,
// so we don't need to do the UTF-8 workaround here.
properties.load(is);
} catch (IOException e) {
// TODO: Better error handling
return Optional.empty();
}
}
return Optional.of(properties);
}
private static void tryUpdateConfigPropertiesFile(Path path, Properties properties) {
try {
if (properties.isEmpty()) {
// Delete the file or don't create it if there are no changed configs
if (Files.exists(path)) {
Files.delete(path);
}
return;
}
try (OutputStream out = Files.newOutputStream(path)) {
properties.store(out, null);
}
} catch (IOException e) {
// TODO: Better error handling
}
}
public static boolean isValidShaderpack(Path pack) {
if (Files.isDirectory(pack)) {
// Sometimes the shaderpack directory itself can be
// identified as a shader pack due to it containing
// folders which contain "shaders" folders, this is
// necessary to check against that
if (pack.equals(getShaderpacksDirectory())) {
return false;
}
try (Stream<Path> stream = Files.walk(pack)) {
return stream.filter(Files::isDirectory)
// Prevent a pack simply named "shaders" from being
// identified as a valid pack
.filter(path -> !path.equals(pack)).anyMatch(path -> path.endsWith("shaders"));
} catch (IOException ignored) {
// ignored, not a valid shader pack.
}
}
if (pack.toString().endsWith(".zip")) {
try (FileSystem zipSystem = FileSystems.newFileSystem(pack, Iris.class.getClassLoader())) {
final Path root = zipSystem.getRootDirectories().iterator().next();
try (Stream<Path> stream = Files.walk(root)) {
return stream.filter(Files::isDirectory).anyMatch(path -> path.endsWith("shaders"));
}
} catch (ZipException e) {
Iris.logger.warn("The ZIP at " + pack + " is corrupt");
} catch (IOException ignored) {
// ignored, not a valid shader pack.
}
}
return false;
}
public static void queueShaderPackOptionsFromProfile(Profile profile) {
getShaderPackOptionQueue().putAll(profile.optionValues);
}
public static void queueShaderPackOptionsFromProperties(Properties properties) {
queueDefaultShaderPackOptionValues();
properties.stringPropertyNames().forEach(key -> getShaderPackOptionQueue().put(key, properties.getProperty(key)));
}
// Used in favor of resetShaderPackOptions as the aforementioned requires the pack to be reloaded
public static void queueDefaultShaderPackOptionValues() {
clearShaderPackOptionQueue();
getCurrentPack().ifPresent(pack -> {
final OptionSet options = pack.getShaderPackOptions().getOptionSet();
final OptionValues values = pack.getShaderPackOptions().getOptionValues();
options.getStringOptions().forEach((key, mOpt) -> {
if (values.getStringValue(key).isPresent()) {
getShaderPackOptionQueue().put(key, mOpt.getOption().getDefaultValue());
}
});
options.getBooleanOptions().forEach((key, mOpt) -> {
if (values.getBooleanValue(key) != OptionalBoolean.DEFAULT) {
getShaderPackOptionQueue().put(key, Boolean.toString(mOpt.getOption().getDefaultValue()));
}
});
});
}
public static void clearShaderPackOptionQueue() {
getShaderPackOptionQueue().clear();
}
public static void resetShaderPackOptionsOnNextReload() {
resetShaderPackOptions = true;
}
public static boolean shouldResetShaderPackOptionsOnNextReload() {
return resetShaderPackOptions;
}
public static void reload() throws IOException {
// allows shaderpacks to be changed at runtime
irisConfig.initialize();
// Reset the texture reload counter
CapturedRenderingState.INSTANCE.resetTextureReloadCount();
// Destroy all allocated resources
destroyEverything();
// Load the new shaderpack
loadShaderpack();
// Very important - we need to re-create the pipeline straight away.
// https://github.com/IrisShaders/Iris/issues/1330
if (Minecraft.getMinecraft().theWorld != null) {
Iris.getPipelineManager().preparePipeline(Iris.getCurrentDimensionName());
BlockRenderingSettings.INSTANCE.reloadRendererIfRequired();
}
if (loadedIncompatiblePack() && Minecraft.getMinecraft().thePlayer != null) {
Iris.logger.warn("Incompatible pack for DH!");
Minecraft.getMinecraft().thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.BOLD.toString() + EnumChatFormatting.RED + "This pack doesn't have DH support."));
Minecraft.getMinecraft().thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "Distant Horizons (DH) chunks won't show up. This isn't a bug, get another shader."));
}
}
/**
* Destroys and deallocates all created OpenGL resources. Useful as part of a reload.
*/
private static void destroyEverything() {
currentPack = null;
getPipelineManager().destroyPipeline();
// Close the zip filesystem that the shaderpack was loaded from
//
// This prevents a FileSystemAlreadyExistsException when reloading shaderpacks.
if (zipFileSystem != null) {
try {
zipFileSystem.close();
} catch (NoSuchFileException e) {
logger.warn("Failed to close the shaderpack zip when reloading because it was deleted, proceeding anyways.");
} catch (IOException e) {
logger.error("Failed to close zip file system?", e);
}
}
}
public static String lastDimensionName = null;
public static int lastDimensionId = 0;
/**
* Gets the dimension name for the current world.
* Returns the dimension name from WorldProvider.getDimensionName() if available.
* Falls back to lastDimensionName when no world is loaded.
*/
public static String getCurrentDimensionName() {
final WorldClient level = Minecraft.getMinecraft().theWorld;
if (level != null && level.provider != null) {
String dimensionName = level.provider.getDimensionName();
if (dimensionName == null) {
dimensionName = "Overworld";
logger.warn("WorldProvider.getDimensionName() returned null for dimension ID {}, defaulting to 'Overworld'", level.provider.dimensionId);
}
lastDimensionName = dimensionName;
lastDimensionId = level.provider.dimensionId;
return dimensionName;
} else {
// This prevents us from reloading the shaderpack unless we need to. Otherwise, if the player is in
// another dimension and quits the game, we might end up reloading the shaders on exit and on entry to the level
// because the code thinks that the dimension changed.
return lastDimensionName != null ? lastDimensionName : "Overworld";
}
}
/**
* Gets the current dimension ID.
*/
public static int getCurrentDimensionId() {
final WorldClient level = Minecraft.getMinecraft().theWorld;
if (level != null && level.provider != null) {
return level.provider.dimensionId;
}
return lastDimensionId;
}
/**
* Creates a pipeline for a dimension using the dimension name from WorldProvider.getDimensionName().
* Supports dimension.properties mappings with wildcard fallback.
*/
private static WorldRenderingPipeline createPipeline(String dimensionName) {
if (currentPack == null) {
// Completely disables shader-based rendering
PerFrameUniformBlockHarvester.clear();
return new FixedFunctionWorldRenderingPipeline();
}
final ProgramSet programs = currentPack.getProgramSet(dimensionName);
try {
shaderPackLoadId++;
ShaderTransformExecutor.prepare();
PerFrameUniformBlockHarvester.harvest(programs);
long startTime = System.nanoTime();
WorldRenderingPipeline pipeline = new DeferredWorldRenderingPipeline(programs);
long endTime = System.nanoTime();
logger.info("[Load #{}] Total shaderpack load time for '{}' in dimension '{}': {} ms", shaderPackLoadId, currentPackName, dimensionName, String.format("%.1f", (endTime - startTime) / 1_000_000.0));
return pipeline;
} catch (Exception e) {
logger.error("Failed to create shader rendering pipeline, disabling shaders!", e);
// TODO: This should be reverted if a dimension change causes shaders to compile again
fallback = true;
PerFrameUniformBlockHarvester.clear();
return new FixedFunctionWorldRenderingPipeline();
}
}
@NotNull
public static PipelineManager getPipelineManager() {
if (pipelineManager == null) {
pipelineManager = new PipelineManager(Iris::createPipeline);
}
return pipelineManager;
}
@Nullable
public static PipelineManager getPipelineManagerNullable() {
return pipelineManager;
}
@NotNull
public static Optional<ShaderPack> getCurrentPack() {
return Optional.ofNullable(currentPack);
}
public static String getVersion() {
if (IRIS_VERSION == null) {
return "Version info unknown!";
}
return IRIS_VERSION;
}
public static String getFormattedVersion() {
final EnumChatFormatting color;
String version = getVersion();
if (version.endsWith("-development-environment")) {
color = EnumChatFormatting.GOLD;
version = version.replace("-development-environment", " (Development Environment)");
} else if (version.endsWith("-dirty") || version.contains("unknown") || version.endsWith("-nogit")) {
color = EnumChatFormatting.RED;
} else if (version.contains("+rev.")) {
color = EnumChatFormatting.LIGHT_PURPLE;
} else {
color = EnumChatFormatting.GREEN;
}
return color + version;
}
public static Path getShaderpacksDirectory() {
if (shaderpacksDirectory == null) {
shaderpacksDirectory = Minecraft.getMinecraft().mcDataDir.toPath().resolve("shaderpacks");
}
return shaderpacksDirectory;
}
public static ShaderpackDirectoryManager getShaderpacksDirectoryManager() {
if (shaderpacksDirectoryManager == null) {
shaderpacksDirectoryManager = new ShaderpackDirectoryManager(getShaderpacksDirectory());
}
return shaderpacksDirectoryManager;
}
public void fmlInitEvent() {
IRIS_VERSION = Tags.VERSION;
reloadKeybind = new KeyBinding("iris.keybind.reload", 0, "key.category.iris.keybinds");
toggleShadersKeybind = new KeyBinding("iris.keybind.toggleShaders", 0, "key.category.iris.keybinds");
shaderpackScreenKeybind = new KeyBinding("iris.keybind.shaderPackSelection", 0, "key.category.iris.keybinds");
ClientRegistry.registerKeyBinding(reloadKeybind);
ClientRegistry.registerKeyBinding(toggleShadersKeybind);
ClientRegistry.registerKeyBinding(shaderpackScreenKeybind);
}
public static void setShaderMaterialOverride(Block block, int meta) {
if (!enabled)
return;
final Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches = BlockRenderingSettings.INSTANCE.getBlockMetaMatches();
if (blockMetaMatches == null)
return;
final Int2IntMap metaMap = blockMetaMatches.get(block);
final int blockId = metaMap != null ? BlockMaterialMapping.resolveId(metaMap, meta) : -1;
if (TessellatorManager.get() instanceof StateAwareTessellator tess)
tess.angelica$setShaderOverrideBlockId((short) blockId);
}
public static void resetShaderMaterialOverride() {
if (!enabled)
return;
if (TessellatorManager.get() instanceof StateAwareTessellator tess)
tess.angelica$setShaderOverrideBlockId((short) -1);
}
}