2023-12-24 21:49:28 -08:00
package net.coderbot.iris ;
import com.google.common.base.Throwables ;
2025-02-03 16:13:53 +01:00
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager ;
2023-12-24 21:49:28 -08:00
import com.gtnewhorizons.angelica.Tags ;
2025-02-03 16:13:53 +01:00
import com.gtnewhorizons.angelica.config.AngelicaConfig ;
2026-03-25 20:59:23 +01:00
import com.gtnewhorizons.angelica.proxy.ClientProxy ;
2026-04-02 23:18:55 +02:00
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator ;
2026-03-25 20:59:23 +01:00
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder ;
2023-12-24 21:49:28 -08:00
import cpw.mods.fml.client.registry.ClientRegistry ;
import cpw.mods.fml.common.eventhandler.SubscribeEvent ;
import cpw.mods.fml.common.gameevent.InputEvent ;
2026-01-19 13:03:56 -08:00
import it.unimi.dsi.fastutil.ints.Int2IntMap ;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap ;
2023-12-24 21:49:28 -08:00
import lombok.Getter ;
2026-06-11 20:58:22 -04:00
import net.coderbot.iris.block_rendering.BlockMaterialMapping ;
2025-02-03 16:13:53 +01:00
import net.coderbot.iris.block_rendering.BlockRenderingSettings ;
2025-12-28 20:29:21 -08:00
import net.coderbot.iris.celeritas.IrisCeleritasShaderProvider ;
2026-03-25 18:48:14 +01:00
import net.coderbot.iris.compat.dh.DHCompat ;
2023-12-24 21:49:28 -08:00
import net.coderbot.iris.config.IrisConfig ;
2026-03-25 20:59:23 +01:00
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability ;
2023-12-24 21:49:28 -08:00
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 ;
2026-03-25 20:59:23 +01:00
import net.coderbot.iris.pipeline.transform.ShaderTransformer ;
import net.coderbot.iris.pipeline.transform.TransformPatcher ;
2023-12-24 21:49:28 -08:00
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 ;
2026-06-27 17:48:51 -04:00
import net.coderbot.iris.uniforms.CapturedRenderingState ;
2025-02-03 16:13:53 +01:00
import net.minecraft.block.Block ;
2023-12-24 21:49:28 -08:00
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 ;
2025-07-21 10:59:04 +02:00
import net.minecraft.util.EnumChatFormatting ;
2023-12-24 21:49:28 -08:00
import org.jetbrains.annotations.NotNull ;
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 ;
2025-12-23 09:11:32 -08:00
import java.util.Objects ;
2023-12-24 21:49:28 -08:00
import java.util.Optional ;
import java.util.Properties ;
2025-12-23 09:11:32 -08:00
import java.util.concurrent.CompletableFuture ;
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.Executors ;
import java.util.concurrent.ForkJoinPool ;
import java.util.concurrent.ForkJoinWorkerThread ;
import java.util.concurrent.ScheduledExecutorService ;
import java.util.concurrent.TimeUnit ;
2026-03-25 20:59:23 +01:00
import java.util.concurrent.atomic.AtomicInteger ;
2025-12-23 09:11:32 -08:00
import java.util.function.Supplier ;
2023-12-24 21:49:28 -08:00
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 );
2025-12-02 21:43:17 -08:00
// Cached at class load - config must be loaded before Iris. Do not change at runtime.
public static final boolean enabled = AngelicaConfig . enableIris ;
2023-12-24 21:49:28 -08:00
private static Path shaderpacksDirectory ;
private static ShaderpackDirectoryManager shaderpacksDirectoryManager ;
private static ShaderPack currentPack ;
@Getter
private static String currentPackName ;
@Getter
private static boolean initialized ;
2025-12-23 09:11:32 -08:00
@Getter
private static int shaderPackLoadId = 0 ;
2023-12-24 21:49:28 -08:00
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 ;
2026-03-25 18:48:14 +01:00
private static boolean loadShaderPackWhenPossible = false ;
2023-12-24 21:49:28 -08:00
private static String IRIS_VERSION ;
@Getter
private static boolean fallback ;
2026-03-25 18:48:14 +01:00
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 );
}
}
}
2025-12-23 09:11:32 -08:00
/**
* 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 ShaderTransformExecutor () {}
private static void noteActivity () {
lastActivityTime = System . nanoTime ();
}
public static ExecutorService get () {
synchronized ( lock ) {
noteActivity ();
if ( executor != null && ! executor . isShutdown ()) {
return executor ;
}
executor = new ForkJoinPool (
THREAD_COUNT ,
pool -> {
ForkJoinWorkerThread t = ForkJoinPool . defaultForkJoinWorkerThreadFactory . newThread ( pool );
t . setName ( "Shader-Transform-" + t . getPoolIndex ());
t . setDaemon ( true );
return t ;
},
null , // UncaughtExceptionHandler
true // asyncMode - FIFO, better for independent tasks
);
logger . debug ( "Created shader transform executor with " + THREAD_COUNT + " 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 );
}
}
/**
* Ensures the executor exists and begins spinning up worker threads.
* Intended for UI entrypoints (eg. shader settings screen open).
*/
public static void prepare () {
// Submit empty tasks to warm up all worker threads without blocking the UI thread.
for ( int i = 0 ; i < THREAD_COUNT ; i ++ ) {
submitTracked (() -> { });
}
}
/**
* 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 (() -> {
try {
return supplier . get ();
} finally {
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 (() -> {
try {
runnable . run ();
} finally {
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 ();
} else {
// Still active (or in-flight), schedule another check.
final long remainingSeconds = Math . max ( 1 , IDLE_TIMEOUT_SECONDS - idleSeconds + 1 );
scheduleIdleCheck ( remainingSeconds );
}
}
}
}
2023-12-24 21:49:28 -08:00
private static KeyBinding reloadKeybind ;
private static KeyBinding toggleShadersKeybind ;
private static KeyBinding shaderpackScreenKeybind ;
public static Iris INSTANCE = new Iris ();
private Iris () {
2025-12-02 21:43:17 -08:00
// Guard against null blackboard in test environments
final Object deobfEnv = Launch . blackboard != null ? Launch . blackboard . get ( "fml.deobfuscatedEnvironment" ) : null ;
isDevelopmentEnvironment = deobfEnv != null && ( boolean ) deobfEnv ;
2023-12-24 21:49:28 -08:00
}
@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 ( "Shaders Reloaded!" ));
} catch ( Exception e ) {
logger . error ( "Error while reloading Shaders for Iris!" , e );
if ( mc . thePlayer != null ) mc . thePlayer . addChatMessage ( new ChatComponentText ( "Failed tgo reload shaders! Reason: " + 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 ( "Failed tgo toggle shaders! Reason: " + 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 ) {
2026-03-25 20:59:23 +01:00
ClientProxy . animationsMode . next ();
2023-12-24 21:49:28 -08:00
}
}
}
/**
* 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 () {
2026-03-25 18:48:14 +01:00
DHCompat . run ();
2023-12-24 21:49:28 -08:00
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 ;
}
2025-12-28 20:29:21 -08:00
// Register the Celeritas shader provider for Iris integration (only when Celeritas is enabled)
if ( AngelicaConfig . enableCeleritas ) {
IrisShaderProviderHolder . setProvider ( new IrisCeleritasShaderProvider ());
}
2025-12-23 09:11:32 -08:00
// Warm up the threadpool so shader transformations are faster when we need them
ShaderTransformExecutor . warmup ();
2026-01-26 21:08:09 -08:00
// Initialize version hoisting pattern based on GL capabilities
ShaderTransformer . init ();
2023-12-24 21:49:28 -08:00
PBRTextureManager . INSTANCE . init ();
2026-03-25 18:48:14 +01:00
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 ();
}
2023-12-24 21:49:28 -08:00
}
/**
* 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
2026-01-22 22:17:16 -05:00
lastDimensionName = "Overworld" ;
Iris . getPipelineManager (). preparePipeline ( "Overworld" );
2025-12-28 20:29:21 -08:00
BlockRenderingSettings . INSTANCE . reloadRendererIfRequired ();
2023-12-24 21:49:28 -08:00
}
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" )));
}
}
2026-06-06 20:12:38 -04:00
/**
* 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 );
}
}
2023-12-24 21:49:28 -08:00
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 ();
2026-03-25 20:59:23 +01:00
if ( externalName . isEmpty ()) {
2023-12-24 21:49:28 -08:00
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 ;
}
}
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" );
2026-06-27 17:48:51 -04:00
if ( ! Files . exists ( shaderPackPath )) {
logger . error ( "Could not load the shaderpack \"{}\" because it appears to lack a \"shaders\" directory" , name );
return false ;
}
2023-12-24 21:49:28 -08:00
}
2025-03-09 05:41:47 +01:00
@SuppressWarnings ( "unchecked" )
final Map < String , String > changedConfigs = tryReadConfigProperties ( shaderPackConfigTxt ). map ( properties -> ( Map < String , String > ) ( Map <? , ?> ) properties )
2023-12-24 21:49:28 -08:00
. 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" ));
}
2026-06-06 20:12:38 -04:00
} catch ( ZipException e ) {
2023-12-24 21:49:28 -08:00
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 ();
2026-06-27 17:48:51 -04:00
// Reset the texture reload counter
CapturedRenderingState . INSTANCE . resetTextureReloadCount ();
2023-12-24 21:49:28 -08:00
// 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 ) {
2026-01-22 22:17:16 -05:00
Iris . getPipelineManager (). preparePipeline ( Iris . getCurrentDimensionName ());
2025-12-28 20:29:21 -08:00
BlockRenderingSettings . INSTANCE . reloadRendererIfRequired ();
2023-12-24 21:49:28 -08:00
}
2026-03-25 18:48:14 +01:00
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." ));
}
2023-12-24 21:49:28 -08:00
}
/**
* 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 );
}
}
}
2026-01-22 22:17:16 -05:00
public static String lastDimensionName = null ;
public static int lastDimensionId = 0 ;
2023-12-24 21:49:28 -08:00
2026-01-22 22:17:16 -05:00
/**
* 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 () {
2023-12-24 21:49:28 -08:00
final WorldClient level = Minecraft . getMinecraft (). theWorld ;
2026-01-22 22:17:16 -05:00
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 );
2023-12-24 21:49:28 -08:00
}
2026-01-22 22:17:16 -05:00
lastDimensionName = dimensionName ;
lastDimensionId = level . provider . dimensionId ;
return dimensionName ;
2023-12-24 21:49:28 -08:00
} else {
2026-01-22 22:17:16 -05:00
// 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
2023-12-24 21:49:28 -08:00
// because the code thinks that the dimension changed.
2026-01-22 22:17:16 -05:00
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 ;
2023-12-24 21:49:28 -08:00
}
2026-01-22 22:17:16 -05:00
return lastDimensionId ;
2023-12-24 21:49:28 -08:00
}
2026-01-22 22:17:16 -05:00
/**
* 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 ) {
2023-12-24 21:49:28 -08:00
if ( currentPack == null ) {
// Completely disables shader-based rendering
return new FixedFunctionWorldRenderingPipeline ();
}
2026-01-22 22:17:16 -05:00
final ProgramSet programs = currentPack . getProgramSet ( dimensionName );
2023-12-24 21:49:28 -08:00
try {
2025-12-23 09:11:32 -08:00
shaderPackLoadId ++ ;
long startTime = System . nanoTime ();
WorldRenderingPipeline pipeline = new DeferredWorldRenderingPipeline ( programs );
long endTime = System . nanoTime ();
2026-01-22 22:17:16 -05:00
logger . info ( "[Load #{}] Total shaderpack load time for '{}' in dimension '{}': {} ms" , shaderPackLoadId , currentPackName , dimensionName , String . format ( "%.1f" , ( endTime - startTime ) / 1_000_000 . 0 ));
2025-12-23 09:11:32 -08:00
return pipeline ;
2023-12-24 21:49:28 -08:00
} 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 ;
return new FixedFunctionWorldRenderingPipeline ();
}
}
@NotNull
public static PipelineManager getPipelineManager () {
if ( pipelineManager == null ) {
pipelineManager = new PipelineManager ( Iris :: createPipeline );
}
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 () {
2025-07-21 10:59:04 +02:00
final EnumChatFormatting color ;
2023-12-24 21:49:28 -08:00
String version = getVersion ();
if ( version . endsWith ( "-development-environment" )) {
2025-07-21 10:59:04 +02:00
color = EnumChatFormatting . GOLD ;
2023-12-24 21:49:28 -08:00
version = version . replace ( "-development-environment" , " (Development Environment)" );
} else if ( version . endsWith ( "-dirty" ) || version . contains ( "unknown" ) || version . endsWith ( "-nogit" )) {
2025-07-21 10:59:04 +02:00
color = EnumChatFormatting . RED ;
2023-12-24 21:49:28 -08:00
} else if ( version . contains ( "+rev." )) {
2025-07-21 10:59:04 +02:00
color = EnumChatFormatting . LIGHT_PURPLE ;
2023-12-24 21:49:28 -08:00
} else {
2025-07-21 10:59:04 +02:00
color = EnumChatFormatting . GREEN ;
2023-12-24 21:49:28 -08:00
}
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 ( "Reload Shaders" , 0 , "Iris Keybinds" );
toggleShadersKeybind = new KeyBinding ( "Toggle Shaders" , 0 , "Iris Keybinds" );
shaderpackScreenKeybind = new KeyBinding ( "Shaderpack Selection Screen" , 0 , "Iris Keybinds" );
ClientRegistry . registerKeyBinding ( reloadKeybind );
ClientRegistry . registerKeyBinding ( toggleShadersKeybind );
ClientRegistry . registerKeyBinding ( shaderpackScreenKeybind );
}
2025-02-03 16:13:53 +01:00
public static void setShaderMaterialOverride ( Block block , int meta ) {
2025-12-02 21:43:17 -08:00
if ( ! enabled )
2025-02-03 16:13:53 +01:00
return ;
2026-04-02 23:18:55 +02:00
final Reference2ObjectMap < Block , Int2IntMap > blockMetaMatches = BlockRenderingSettings . INSTANCE . getBlockMetaMatches ();
if ( blockMetaMatches == null )
return ;
final Int2IntMap metaMap = blockMetaMatches . get ( block );
2026-06-11 20:58:22 -04:00
final int blockId = metaMap != null ? BlockMaterialMapping . resolveId ( metaMap , meta ) : - 1 ;
2025-02-03 16:13:53 +01:00
2026-04-02 23:18:55 +02:00
if ( TessellatorManager . get () instanceof StateAwareTessellator tess )
tess . angelica$setShaderOverrideBlockId (( short ) blockId );
2025-02-03 16:13:53 +01:00
}
public static void resetShaderMaterialOverride () {
2025-12-02 21:43:17 -08:00
if ( ! enabled )
2025-02-03 16:13:53 +01:00
return ;
2026-04-02 23:18:55 +02:00
if ( TessellatorManager . get () instanceof StateAwareTessellator tess )
tess . angelica$setShaderOverrideBlockId (( short ) - 1 );
2025-02-03 16:13:53 +01:00
}
2023-12-24 21:49:28 -08:00
}