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.CapturingTessellator ;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager ;
2023-12-24 21:49:28 -08:00
import com.gtnewhorizons.angelica.AngelicaMod ;
import com.gtnewhorizons.angelica.Tags ;
2025-02-03 16:13:53 +01:00
import com.gtnewhorizons.angelica.config.AngelicaConfig ;
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 ;
2025-02-03 16:13:53 +01:00
import it.unimi.dsi.fastutil.objects.Object2IntMap ;
2026-01-19 13:03:56 -08:00
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap ;
2023-12-24 21:49:28 -08:00
import lombok.Getter ;
2025-02-03 16:13:53 +01:00
import net.coderbot.iris.block_rendering.BlockRenderingSettings ;
2025-12-28 20:29:21 -08:00
import com.gtnewhorizons.angelica.config.AngelicaConfig ;
import net.coderbot.iris.celeritas.IrisCeleritasShaderProvider ;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder ;
2023-12-24 21:49:28 -08:00
import net.coderbot.iris.config.IrisConfig ;
import net.coderbot.iris.gl.shader.StandardMacros ;
import net.coderbot.iris.gui.screen.ShaderPackScreen ;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline ;
2025-12-23 09:11:32 -08:00
import net.coderbot.iris.pipeline.transform.ShaderTransformer ;
import net.coderbot.iris.pipeline.transform.TransformPatcher ;
import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability ;
2023-12-24 21:49:28 -08:00
import net.coderbot.iris.pipeline.FixedFunctionWorldRenderingPipeline ;
import net.coderbot.iris.pipeline.PipelineManager ;
import net.coderbot.iris.pipeline.WorldRenderingPipeline ;
import net.coderbot.iris.shaderpack.DimensionId ;
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 ;
2025-12-28 20:29:21 -08:00
import net.coderbot.iris.block_context.BlockContextHolder ;
2023-12-24 21:49:28 -08:00
import net.coderbot.iris.texture.pbr.PBRTextureManager ;
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.atomic.AtomicInteger ;
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 ;
import java.util.function.Supplier ;
2023-12-24 21:49:28 -08:00
import java.util.stream.Stream ;
import java.util.zip.ZipError ;
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 ;
private static String IRIS_VERSION ;
@Getter
private static boolean fallback ;
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 ) {
AngelicaMod . 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 () {
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 ();
2023-12-24 21:49:28 -08:00
PBRTextureManager . INSTANCE . init ();
// Only load the shader pack when we can access OpenGL
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
lastDimension = DimensionId . OVERWORLD ;
Iris . getPipelineManager (). preparePipeline ( DimensionId . 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" )));
}
}
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 . isPresent ()) {
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" );
}
if ( ! Files . exists ( shaderPackPath )) {
logger . error ( "Could not load the shaderpack \"{}\" because it appears to lack a \"shaders\" directory" , name );
return false ;
}
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" ));
}
} catch ( ZipError zipError ) {
// Java 8 seems to throw a ZipError instead of a subclass of IOException
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 ();
// 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 . getCurrentDimension ());
2025-12-28 20:29:21 -08:00
BlockRenderingSettings . INSTANCE . reloadRendererIfRequired ();
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 );
}
}
}
public static DimensionId lastDimension = null ;
public static DimensionId getCurrentDimension () {
final WorldClient level = Minecraft . getMinecraft (). theWorld ;
if ( level != null ) {
if ( level . provider == null ) return DimensionId . OVERWORLD ;
2025-02-01 12:18:29 -05:00
if ( level . provider . isHellWorld || level . provider . dimensionId == - 1 ) {
2023-12-24 21:49:28 -08:00
return DimensionId . NETHER ;
} else if ( level . provider . dimensionId == 1 ) {
return DimensionId . END ;
} else {
return DimensionId . OVERWORLD ;
}
} else {
// This prevents us from reloading the shaderpack unless we need to. Otherwise, if the player is in the
// nether 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 lastDimension ;
}
}
private static WorldRenderingPipeline createPipeline ( DimensionId dimensionId ) {
if ( currentPack == null ) {
// Completely disables shader-based rendering
return new FixedFunctionWorldRenderingPipeline ();
}
final ProgramSet programs = currentPack . getProgramSet ( dimensionId );
try {
2025-12-23 09:11:32 -08:00
shaderPackLoadId ++ ;
long startTime = System . nanoTime ();
WorldRenderingPipeline pipeline = new DeferredWorldRenderingPipeline ( programs );
long endTime = System . nanoTime ();
logger . info ( "[Load #{}] Total shaderpack load time for '{}': {} ms" , shaderPackLoadId , currentPackName , String . format ( "%.1f" , ( endTime - startTime ) / 1_000_000 . 0 ));
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
static BlockContextHolder contextHolder ;
private static int getShaderMaterialOverrideId ( Block block , int meta ) {
if ( contextHolder == null ) {
2026-01-19 13:03:56 -08:00
final Reference2ObjectMap < Block , Int2IntMap > blockMetaMatches = BlockRenderingSettings . INSTANCE . getBlockMetaMatches ();
if ( blockMetaMatches == null ) {
2025-02-03 16:13:53 +01:00
return - 1 ;
}
2026-01-19 13:03:56 -08:00
contextHolder = new BlockContextHolder ( blockMetaMatches );
2025-02-03 16:13:53 +01:00
}
2026-01-19 13:03:56 -08:00
contextHolder . set ( block , meta , ( short ) block . getRenderType ());
2025-02-03 16:13:53 +01:00
return contextHolder . blockId ;
}
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 ;
int blockId = getShaderMaterialOverrideId ( block , meta );
2025-05-27 22:59:52 +02:00
if ( TessellatorManager . get () instanceof CapturingTessellator tess )
tess . setShaderBlockId ( 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 ;
2025-05-27 22:59:52 +02:00
if ( TessellatorManager . get () instanceof CapturingTessellator tess )
tess . setShaderBlockId ( - 1 );
2025-02-03 16:13:53 +01:00
}
2023-12-24 21:49:28 -08:00
}