mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
1008 lines
40 KiB
Java
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);
|
|
}
|
|
}
|